Prevent lookup tables from being closed during deactivation slot (#22221)
This commit is contained in:
parent
0e4ede46d1
commit
bbe5b66324
|
@ -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},
|
||||
|
@ -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;
|
||||
|
|
|
@ -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},
|
||||
|
@ -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},
|
||||
|
@ -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);
|
||||
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
|
||||
|
@ -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 {
|
||||
|
@ -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)]
|
||||
|
@ -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 {
|
||||
|
@ -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();
|
||||
|
|
Loading…
Reference in New Issue