Skip to content
This repository was archived by the owner on Jan 22, 2025. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 33 additions & 0 deletions programs/address-lookup-table-tests/tests/close_lookup_table_ix.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ use {
solana_address_lookup_table_program::instruction::close_lookup_table,
solana_program_test::*,
solana_sdk::{
clock::Clock,
instruction::InstructionError,
pubkey::Pubkey,
signature::{Keypair, Signer},
Expand Down Expand Up @@ -77,6 +78,38 @@ async fn test_close_lookup_table_not_deactivated() {
.await;
}

#[tokio::test]
async fn test_close_lookup_table_deactivated_in_current_slot() {
let mut context = setup_test_context().await;

let clock = context.banks_client.get_sysvar::<Clock>().await.unwrap();
let authority_keypair = Keypair::new();
let initialized_table = {
let mut table = new_address_lookup_table(Some(authority_keypair.pubkey()), 0);
table.meta.deactivation_slot = clock.slot;
table
};
let lookup_table_address = Pubkey::new_unique();
add_lookup_table_account(&mut context, lookup_table_address, initialized_table).await;

let ix = close_lookup_table(
lookup_table_address,
authority_keypair.pubkey(),
context.payer.pubkey(),
);

// Context sets up the slot hashes sysvar to have an entry
// for slot 0 which is when the table was deactivated.
// Because that slot is present, the ix should fail.
assert_ix_error(
&mut context,
ix,
Some(&authority_keypair),
InstructionError::InvalidArgument,
)
.await;
}

#[tokio::test]
async fn test_close_lookup_table_recently_deactivated() {
let mut context = setup_test_context().await;
Expand Down
41 changes: 20 additions & 21 deletions programs/address-lookup-table/src/processor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@ use {
crate::{
instruction::ProgramInstruction,
state::{
AddressLookupTable, LookupTableMeta, ProgramState, LOOKUP_TABLE_MAX_ADDRESSES,
LOOKUP_TABLE_META_SIZE,
AddressLookupTable, LookupTableMeta, LookupTableStatus, ProgramState,
LOOKUP_TABLE_MAX_ADDRESSES, LOOKUP_TABLE_META_SIZE,
},
},
solana_program_runtime::{ic_msg, invoke_context::InvokeContext},
Expand All @@ -15,7 +15,7 @@ use {
keyed_account::keyed_account_at_index,
program_utils::limited_deserialize,
pubkey::{Pubkey, PUBKEY_BYTES},
slot_hashes::{SlotHashes, MAX_ENTRIES},
slot_hashes::SlotHashes,
system_instruction,
sysvar::{
clock::{self, Clock},
Expand Down Expand Up @@ -419,26 +419,25 @@ impl Processor {
if lookup_table.meta.authority != Some(*authority_account.unsigned_key()) {
return Err(InstructionError::IncorrectAuthority);
}
if lookup_table.meta.deactivation_slot == Slot::MAX {
ic_msg!(invoke_context, "Lookup table is not deactivated");
return Err(InstructionError::InvalidArgument);
}

// Assert that the deactivation slot is no longer recent to give in-flight transactions
// enough time to land and to remove indeterminism caused by transactions loading
// addresses in the same slot when a table is closed. This enforced delay has a side
// effect of not allowing lookup tables to be recreated at the same derived address
// because tables must be created at an address derived from a recent slot.
let clock: Clock = invoke_context.get_sysvar(&clock::id())?;
let slot_hashes: SlotHashes = invoke_context.get_sysvar(&slot_hashes::id())?;
if let Some(position) = slot_hashes.position(&lookup_table.meta.deactivation_slot) {
let expiration = MAX_ENTRIES.saturating_sub(position);
ic_msg!(
invoke_context,
"Table cannot be closed until its derivation slot expires in {} blocks",
expiration
);
return Err(InstructionError::InvalidArgument);
}

match lookup_table.meta.status(clock.slot, &slot_hashes) {
LookupTableStatus::Activated => {
ic_msg!(invoke_context, "Lookup table is not deactivated");
Err(InstructionError::InvalidArgument)
}
LookupTableStatus::Deactivating { remaining_blocks } => {
ic_msg!(
invoke_context,
"Table cannot be closed until it's fully deactivated in {} blocks",
remaining_blocks
);
Err(InstructionError::InvalidArgument)
}
LookupTableStatus::Deactivated => Ok(()),
}?;

drop(lookup_table_account_ref);

Expand Down
119 changes: 118 additions & 1 deletion programs/address-lookup-table/src/state.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
use {
serde::{Deserialize, Serialize},
solana_frozen_abi_macro::{AbiEnumVisitor, AbiExample},
solana_sdk::{clock::Slot, instruction::InstructionError, pubkey::Pubkey},
solana_sdk::{
clock::Slot,
instruction::InstructionError,
pubkey::Pubkey,
slot_hashes::{SlotHashes, MAX_ENTRIES},
},
std::borrow::Cow,
};

Expand All @@ -21,6 +26,14 @@ pub enum ProgramState {
LookupTable(LookupTableMeta),
}

/// Activation status of a lookup table
#[derive(Debug, PartialEq, Clone)]
pub enum LookupTableStatus {
Activated,
Deactivating { remaining_blocks: usize },
Deactivated,
}

/// Address lookup table metadata
#[derive(Debug, Serialize, Deserialize, PartialEq, Clone, AbiExample)]
pub struct LookupTableMeta {
Expand Down Expand Up @@ -61,6 +74,41 @@ impl LookupTableMeta {
..LookupTableMeta::default()
}
}

/// Returns whether the table is considered active for address lookups
pub fn is_active(&self, current_slot: Slot, slot_hashes: &SlotHashes) -> bool {
match self.status(current_slot, slot_hashes) {
LookupTableStatus::Activated => true,
LookupTableStatus::Deactivating { .. } => true,
LookupTableStatus::Deactivated => false,
}
}

/// Return the current status of the lookup table
pub fn status(&self, current_slot: Slot, slot_hashes: &SlotHashes) -> LookupTableStatus {
if self.deactivation_slot == Slot::MAX {
LookupTableStatus::Activated
} else if self.deactivation_slot == current_slot {
LookupTableStatus::Deactivating {
remaining_blocks: MAX_ENTRIES.saturating_add(1),
}
} else if let Some(slot_hash_position) = slot_hashes.position(&self.deactivation_slot) {
// Deactivation requires a cool-down period to give in-flight transactions
// enough time to land and to remove indeterminism caused by transactions loading
// addresses in the same slot when a table is closed. The cool-down period is
// equivalent to the amount of time it takes for a slot to be removed from the
// slot hash list.
//
// By using the slot hash to enforce the cool-down, there is a side effect
// of not allowing lookup tables to be recreated at the same derived address
// because tables must be created at an address derived from a recent slot.
LookupTableStatus::Deactivating {
remaining_blocks: MAX_ENTRIES.saturating_sub(slot_hash_position),
}
} else {
LookupTableStatus::Deactivated
}
}
}

#[derive(Debug, PartialEq, Clone, AbiExample)]
Expand Down Expand Up @@ -127,6 +175,7 @@ impl<'a> AddressLookupTable<'a> {
#[cfg(test)]
mod tests {
use super::*;
use solana_sdk::hash::Hash;

impl AddressLookupTable<'_> {
fn new_for_tests(meta: LookupTableMeta, num_addresses: usize) -> Self {
Expand Down Expand Up @@ -161,6 +210,74 @@ mod tests {
assert_eq!(meta_size as usize, 24);
}

#[test]
fn test_lookup_table_meta_status() {
let mut slot_hashes = SlotHashes::default();
for slot in 1..=MAX_ENTRIES as Slot {
slot_hashes.add(slot, Hash::new_unique());
}

let most_recent_slot = slot_hashes.first().unwrap().0;
let least_recent_slot = slot_hashes.last().unwrap().0;
assert!(least_recent_slot < most_recent_slot);

// 10 was chosen because the current slot isn't necessarily the next
// slot after the most recent block
let current_slot = most_recent_slot + 10;

let active_table = LookupTableMeta {
deactivation_slot: Slot::MAX,
..LookupTableMeta::default()
};

let just_started_deactivating_table = LookupTableMeta {
deactivation_slot: current_slot,
..LookupTableMeta::default()
};

let recently_started_deactivating_table = LookupTableMeta {
deactivation_slot: most_recent_slot,
..LookupTableMeta::default()
};

let almost_deactivated_table = LookupTableMeta {
deactivation_slot: least_recent_slot,
..LookupTableMeta::default()
};

let deactivated_table = LookupTableMeta {
deactivation_slot: least_recent_slot - 1,
..LookupTableMeta::default()
};

assert_eq!(
active_table.status(current_slot, &slot_hashes),
LookupTableStatus::Activated
);
assert_eq!(
just_started_deactivating_table.status(current_slot, &slot_hashes),
LookupTableStatus::Deactivating {
remaining_blocks: MAX_ENTRIES.saturating_add(1),
}
);
assert_eq!(
recently_started_deactivating_table.status(current_slot, &slot_hashes),
LookupTableStatus::Deactivating {
remaining_blocks: MAX_ENTRIES,
}
);
assert_eq!(
almost_deactivated_table.status(current_slot, &slot_hashes),
LookupTableStatus::Deactivating {
remaining_blocks: 1,
}
);
assert_eq!(
deactivated_table.status(current_slot, &slot_hashes),
LookupTableStatus::Deactivated
);
}

#[test]
fn test_overwrite_meta_data() {
let meta = LookupTableMeta::new_for_tests();
Expand Down