(cherry picked from commit bbe5b66324
)
Co-authored-by: Justin Starry <justin@solana.com>
This commit is contained in:
@ -7,6 +7,7 @@ use {
|
|||||||
solana_address_lookup_table_program::instruction::close_lookup_table,
|
solana_address_lookup_table_program::instruction::close_lookup_table,
|
||||||
solana_program_test::*,
|
solana_program_test::*,
|
||||||
solana_sdk::{
|
solana_sdk::{
|
||||||
|
clock::Clock,
|
||||||
instruction::InstructionError,
|
instruction::InstructionError,
|
||||||
pubkey::Pubkey,
|
pubkey::Pubkey,
|
||||||
signature::{Keypair, Signer},
|
signature::{Keypair, Signer},
|
||||||
@ -77,6 +78,38 @@ async fn test_close_lookup_table_not_deactivated() {
|
|||||||
.await;
|
.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]
|
#[tokio::test]
|
||||||
async fn test_close_lookup_table_recently_deactivated() {
|
async fn test_close_lookup_table_recently_deactivated() {
|
||||||
let mut context = setup_test_context().await;
|
let mut context = setup_test_context().await;
|
||||||
|
@ -2,8 +2,8 @@ use {
|
|||||||
crate::{
|
crate::{
|
||||||
instruction::ProgramInstruction,
|
instruction::ProgramInstruction,
|
||||||
state::{
|
state::{
|
||||||
AddressLookupTable, LookupTableMeta, ProgramState, LOOKUP_TABLE_MAX_ADDRESSES,
|
AddressLookupTable, LookupTableMeta, LookupTableStatus, ProgramState,
|
||||||
LOOKUP_TABLE_META_SIZE,
|
LOOKUP_TABLE_MAX_ADDRESSES, LOOKUP_TABLE_META_SIZE,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
solana_program_runtime::{ic_msg, invoke_context::InvokeContext},
|
solana_program_runtime::{ic_msg, invoke_context::InvokeContext},
|
||||||
@ -15,7 +15,7 @@ use {
|
|||||||
keyed_account::keyed_account_at_index,
|
keyed_account::keyed_account_at_index,
|
||||||
program_utils::limited_deserialize,
|
program_utils::limited_deserialize,
|
||||||
pubkey::{Pubkey, PUBKEY_BYTES},
|
pubkey::{Pubkey, PUBKEY_BYTES},
|
||||||
slot_hashes::{SlotHashes, MAX_ENTRIES},
|
slot_hashes::SlotHashes,
|
||||||
system_instruction,
|
system_instruction,
|
||||||
sysvar::{
|
sysvar::{
|
||||||
clock::{self, Clock},
|
clock::{self, Clock},
|
||||||
@ -419,26 +419,25 @@ impl Processor {
|
|||||||
if lookup_table.meta.authority != Some(*authority_account.unsigned_key()) {
|
if lookup_table.meta.authority != Some(*authority_account.unsigned_key()) {
|
||||||
return Err(InstructionError::IncorrectAuthority);
|
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
|
let clock: Clock = invoke_context.get_sysvar(&clock::id())?;
|
||||||
// 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 slot_hashes: SlotHashes = invoke_context.get_sysvar(&slot_hashes::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);
|
match lookup_table.meta.status(clock.slot, &slot_hashes) {
|
||||||
ic_msg!(
|
LookupTableStatus::Activated => {
|
||||||
invoke_context,
|
ic_msg!(invoke_context, "Lookup table is not deactivated");
|
||||||
"Table cannot be closed until its derivation slot expires in {} blocks",
|
Err(InstructionError::InvalidArgument)
|
||||||
expiration
|
}
|
||||||
);
|
LookupTableStatus::Deactivating { remaining_blocks } => {
|
||||||
return Err(InstructionError::InvalidArgument);
|
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);
|
drop(lookup_table_account_ref);
|
||||||
|
|
||||||
|
@ -1,7 +1,12 @@
|
|||||||
use {
|
use {
|
||||||
serde::{Deserialize, Serialize},
|
serde::{Deserialize, Serialize},
|
||||||
solana_frozen_abi_macro::{AbiEnumVisitor, AbiExample},
|
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,
|
std::borrow::Cow,
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -21,6 +26,14 @@ pub enum ProgramState {
|
|||||||
LookupTable(LookupTableMeta),
|
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
|
/// Address lookup table metadata
|
||||||
#[derive(Debug, Serialize, Deserialize, PartialEq, Clone, AbiExample)]
|
#[derive(Debug, Serialize, Deserialize, PartialEq, Clone, AbiExample)]
|
||||||
pub struct LookupTableMeta {
|
pub struct LookupTableMeta {
|
||||||
@ -61,6 +74,41 @@ impl LookupTableMeta {
|
|||||||
..LookupTableMeta::default()
|
..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)]
|
#[derive(Debug, PartialEq, Clone, AbiExample)]
|
||||||
@ -127,6 +175,7 @@ impl<'a> AddressLookupTable<'a> {
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
use solana_sdk::hash::Hash;
|
||||||
|
|
||||||
impl AddressLookupTable<'_> {
|
impl AddressLookupTable<'_> {
|
||||||
fn new_for_tests(meta: LookupTableMeta, num_addresses: usize) -> Self {
|
fn new_for_tests(meta: LookupTableMeta, num_addresses: usize) -> Self {
|
||||||
@ -161,6 +210,74 @@ mod tests {
|
|||||||
assert_eq!(meta_size as usize, 24);
|
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]
|
#[test]
|
||||||
fn test_overwrite_meta_data() {
|
fn test_overwrite_meta_data() {
|
||||||
let meta = LookupTableMeta::new_for_tests();
|
let meta = LookupTableMeta::new_for_tests();
|
||||||
|
Reference in New Issue
Block a user