Prevent lookup tables from being closed during deactivation slot (#22221)

This commit is contained in:
Justin Starry 2022-01-04 04:42:29 +08:00 committed by GitHub
parent 0e4ede46d1
commit bbe5b66324
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 171 additions and 22 deletions

View File

@ -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;

View File

@ -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);

View File

@ -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();