Add deactivation cooldown before address lookup tables can be closed (#22011)

This commit is contained in:
Justin Starry
2021-12-20 17:33:46 -06:00
committed by GitHub
parent c0c3d7c1f2
commit f5d1115468
8 changed files with 430 additions and 72 deletions

View File

@ -21,9 +21,13 @@ async fn test_close_lookup_table() {
let mut context = setup_test_context().await; let mut context = setup_test_context().await;
overwrite_slot_hashes_with_slots(&mut context, &[]); overwrite_slot_hashes_with_slots(&mut context, &[]);
let authority_keypair = Keypair::new();
let initialized_table = new_address_lookup_table(Some(authority_keypair.pubkey()), 0);
let lookup_table_address = Pubkey::new_unique(); let lookup_table_address = Pubkey::new_unique();
let authority_keypair = Keypair::new();
let initialized_table = {
let mut table = new_address_lookup_table(Some(authority_keypair.pubkey()), 0);
table.meta.deactivation_slot = 0;
table
};
add_lookup_table_account(&mut context, lookup_table_address, initialized_table).await; add_lookup_table_account(&mut context, lookup_table_address, initialized_table).await;
let client = &mut context.banks_client; let client = &mut context.banks_client;
@ -49,7 +53,7 @@ async fn test_close_lookup_table() {
} }
#[tokio::test] #[tokio::test]
async fn test_close_lookup_table_too_recent() { async fn test_close_lookup_table_not_deactivated() {
let mut context = setup_test_context().await; let mut context = setup_test_context().await;
let authority_keypair = Keypair::new(); let authority_keypair = Keypair::new();
@ -63,10 +67,38 @@ async fn test_close_lookup_table_too_recent() {
context.payer.pubkey(), context.payer.pubkey(),
); );
// The ix should fail because the table hasn't been deactivated yet
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;
let authority_keypair = Keypair::new();
let initialized_table = {
let mut table = new_address_lookup_table(Some(authority_keypair.pubkey()), 0);
table.meta.deactivation_slot = 0;
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 // Context sets up the slot hashes sysvar to have an entry
// for slot 0 which is what the default initialized table // for slot 0 which is when the table was deactivated.
// has as its derivation slot. Because that slot is present, // Because that slot is present, the ix should fail.
// the ix should fail.
assert_ix_error( assert_ix_error(
&mut context, &mut context,
ix, ix,

View File

@ -52,7 +52,7 @@ async fn test_create_lookup_table() {
Rent::default().minimum_balance(LOOKUP_TABLE_META_SIZE) Rent::default().minimum_balance(LOOKUP_TABLE_META_SIZE)
); );
let lookup_table = AddressLookupTable::deserialize(&lookup_table_account.data).unwrap(); let lookup_table = AddressLookupTable::deserialize(&lookup_table_account.data).unwrap();
assert_eq!(lookup_table.meta.derivation_slot, test_recent_slot); assert_eq!(lookup_table.meta.deactivation_slot, Slot::MAX);
assert_eq!(lookup_table.meta.authority, Some(authority_address)); assert_eq!(lookup_table.meta.authority, Some(authority_address));
assert_eq!(lookup_table.meta.last_extended_slot, 0); assert_eq!(lookup_table.meta.last_extended_slot, 0);
assert_eq!(lookup_table.meta.last_extended_slot_start_index, 0); assert_eq!(lookup_table.meta.last_extended_slot_start_index, 0);

View File

@ -0,0 +1,145 @@
use {
assert_matches::assert_matches,
common::{
add_lookup_table_account, assert_ix_error, new_address_lookup_table, setup_test_context,
},
solana_address_lookup_table_program::{
instruction::deactivate_lookup_table, state::AddressLookupTable,
},
solana_program_test::*,
solana_sdk::{
instruction::InstructionError,
pubkey::Pubkey,
signature::{Keypair, Signer},
transaction::Transaction,
},
};
mod common;
#[tokio::test]
async fn test_deactivate_lookup_table() {
let mut context = setup_test_context().await;
let authority = Keypair::new();
let mut initialized_table = new_address_lookup_table(Some(authority.pubkey()), 10);
let lookup_table_address = Pubkey::new_unique();
add_lookup_table_account(
&mut context,
lookup_table_address,
initialized_table.clone(),
)
.await;
let client = &mut context.banks_client;
let payer = &context.payer;
let recent_blockhash = context.last_blockhash;
let transaction = Transaction::new_signed_with_payer(
&[deactivate_lookup_table(
lookup_table_address,
authority.pubkey(),
)],
Some(&payer.pubkey()),
&[payer, &authority],
recent_blockhash,
);
assert_matches!(client.process_transaction(transaction).await, Ok(()));
let table_account = client
.get_account(lookup_table_address)
.await
.unwrap()
.unwrap();
let lookup_table = AddressLookupTable::deserialize(&table_account.data).unwrap();
assert_eq!(lookup_table.meta.deactivation_slot, 1);
// Check that only the deactivation slot changed
initialized_table.meta.deactivation_slot = 1;
assert_eq!(initialized_table, lookup_table);
}
#[tokio::test]
async fn test_deactivate_immutable_lookup_table() {
let mut context = setup_test_context().await;
let initialized_table = new_address_lookup_table(None, 10);
let lookup_table_address = Pubkey::new_unique();
add_lookup_table_account(&mut context, lookup_table_address, initialized_table).await;
let authority = Keypair::new();
let ix = deactivate_lookup_table(lookup_table_address, authority.pubkey());
assert_ix_error(
&mut context,
ix,
Some(&authority),
InstructionError::Immutable,
)
.await;
}
#[tokio::test]
async fn test_deactivate_already_deactivated() {
let mut context = setup_test_context().await;
let authority = Keypair::new();
let initialized_table = {
let mut table = new_address_lookup_table(Some(authority.pubkey()), 0);
table.meta.deactivation_slot = 0;
table
};
let lookup_table_address = Pubkey::new_unique();
add_lookup_table_account(&mut context, lookup_table_address, initialized_table).await;
let ix = deactivate_lookup_table(lookup_table_address, authority.pubkey());
assert_ix_error(
&mut context,
ix,
Some(&authority),
InstructionError::InvalidArgument,
)
.await;
}
#[tokio::test]
async fn test_deactivate_lookup_table_with_wrong_authority() {
let mut context = setup_test_context().await;
let authority = Keypair::new();
let wrong_authority = Keypair::new();
let initialized_table = new_address_lookup_table(Some(authority.pubkey()), 10);
let lookup_table_address = Pubkey::new_unique();
add_lookup_table_account(&mut context, lookup_table_address, initialized_table).await;
let ix = deactivate_lookup_table(lookup_table_address, wrong_authority.pubkey());
assert_ix_error(
&mut context,
ix,
Some(&wrong_authority),
InstructionError::IncorrectAuthority,
)
.await;
}
#[tokio::test]
async fn test_deactivate_lookup_table_without_signing() {
let mut context = setup_test_context().await;
let authority = Keypair::new();
let initialized_table = new_address_lookup_table(Some(authority.pubkey()), 10);
let lookup_table_address = Pubkey::new_unique();
add_lookup_table_account(&mut context, lookup_table_address, initialized_table).await;
let mut ix = deactivate_lookup_table(lookup_table_address, authority.pubkey());
ix.accounts[1].is_signer = false;
assert_ix_error(
&mut context,
ix,
None,
InstructionError::MissingRequiredSignature,
)
.await;
}

View File

@ -1,6 +1,8 @@
use { use {
assert_matches::assert_matches, assert_matches::assert_matches,
common::{add_lookup_table_account, new_address_lookup_table, setup_test_context}, common::{
add_lookup_table_account, assert_ix_error, new_address_lookup_table, setup_test_context,
},
solana_address_lookup_table_program::{ solana_address_lookup_table_program::{
instruction::extend_lookup_table, instruction::extend_lookup_table,
state::{AddressLookupTable, LookupTableMeta}, state::{AddressLookupTable, LookupTableMeta},
@ -130,7 +132,7 @@ async fn test_extend_lookup_table() {
} else { } else {
num_existing_addresses as u8 num_existing_addresses as u8
}, },
derivation_slot: lookup_table.meta.derivation_slot, deactivation_slot: lookup_table.meta.deactivation_slot,
authority: lookup_table.meta.authority, authority: lookup_table.meta.authority,
_padding: 0u16, _padding: 0u16,
}, },
@ -156,59 +158,111 @@ async fn test_extend_lookup_table() {
} }
#[tokio::test] #[tokio::test]
async fn test_extend_addresses_authority_errors() { async fn test_extend_lookup_table_with_wrong_authority() {
let mut context = setup_test_context().await; let mut context = setup_test_context().await;
let authority = Keypair::new(); let authority = Keypair::new();
let wrong_authority = Keypair::new();
for (existing_authority, ix_authority, use_signer, expected_err) in [ let initialized_table = new_address_lookup_table(Some(authority.pubkey()), 0);
(
Some(authority.pubkey()),
Keypair::new(),
true,
InstructionError::IncorrectAuthority,
),
(
Some(authority.pubkey()),
authority,
false,
InstructionError::MissingRequiredSignature,
),
(None, Keypair::new(), true, InstructionError::Immutable),
] {
let lookup_table = new_address_lookup_table(existing_authority, 0);
let lookup_table_address = Pubkey::new_unique(); let lookup_table_address = Pubkey::new_unique();
let _ = add_lookup_table_account(&mut context, lookup_table_address, lookup_table.clone()) add_lookup_table_account(&mut context, lookup_table_address, initialized_table).await;
.await;
let num_new_addresses = 1; let new_addresses = vec![Pubkey::new_unique()];
let mut new_addresses = Vec::with_capacity(num_new_addresses); let ix = extend_lookup_table(
new_addresses.resize_with(num_new_addresses, Pubkey::new_unique);
let mut instruction = extend_lookup_table(
lookup_table_address, lookup_table_address,
ix_authority.pubkey(), wrong_authority.pubkey(),
context.payer.pubkey(), context.payer.pubkey(),
new_addresses.clone(), new_addresses,
); );
if !use_signer {
instruction.accounts[1].is_signer = false;
}
let mut expected_addresses: Vec<Pubkey> = lookup_table.addresses.to_vec(); assert_ix_error(
expected_addresses.extend(new_addresses); &mut context,
ix,
let extra_signer = if use_signer { Some(&wrong_authority),
Some(&ix_authority) InstructionError::IncorrectAuthority,
} else { )
None .await;
}; }
let test_case = TestCase { #[tokio::test]
lookup_table_address, async fn test_extend_lookup_table_without_signing() {
instruction, let mut context = setup_test_context().await;
extra_signer,
expected_result: Err(expected_err), let authority = Keypair::new();
}; let initialized_table = new_address_lookup_table(Some(authority.pubkey()), 10);
let lookup_table_address = Pubkey::new_unique();
run_test_case(&mut context, test_case).await; add_lookup_table_account(&mut context, lookup_table_address, initialized_table).await;
}
let new_addresses = vec![Pubkey::new_unique()];
let mut ix = extend_lookup_table(
lookup_table_address,
authority.pubkey(),
context.payer.pubkey(),
new_addresses,
);
ix.accounts[1].is_signer = false;
assert_ix_error(
&mut context,
ix,
None,
InstructionError::MissingRequiredSignature,
)
.await;
}
#[tokio::test]
async fn test_extend_deactivated_lookup_table() {
let mut context = setup_test_context().await;
let authority = Keypair::new();
let initialized_table = {
let mut table = new_address_lookup_table(Some(authority.pubkey()), 0);
table.meta.deactivation_slot = 0;
table
};
let lookup_table_address = Pubkey::new_unique();
add_lookup_table_account(&mut context, lookup_table_address, initialized_table).await;
let new_addresses = vec![Pubkey::new_unique()];
let ix = extend_lookup_table(
lookup_table_address,
authority.pubkey(),
context.payer.pubkey(),
new_addresses,
);
assert_ix_error(
&mut context,
ix,
Some(&authority),
InstructionError::InvalidArgument,
)
.await;
}
#[tokio::test]
async fn test_extend_immutable_lookup_table() {
let mut context = setup_test_context().await;
let authority = Keypair::new();
let initialized_table = new_address_lookup_table(None, 1);
let lookup_table_address = Pubkey::new_unique();
add_lookup_table_account(&mut context, lookup_table_address, initialized_table).await;
let new_addresses = vec![Pubkey::new_unique()];
let ix = extend_lookup_table(
lookup_table_address,
authority.pubkey(),
context.payer.pubkey(),
new_addresses,
);
assert_ix_error(
&mut context,
ix,
Some(&authority),
InstructionError::Immutable,
)
.await;
} }

View File

@ -78,6 +78,30 @@ async fn test_freeze_immutable_lookup_table() {
.await; .await;
} }
#[tokio::test]
async fn test_freeze_deactivated_lookup_table() {
let mut context = setup_test_context().await;
let authority = Keypair::new();
let initialized_table = {
let mut table = new_address_lookup_table(Some(authority.pubkey()), 10);
table.meta.deactivation_slot = 0;
table
};
let lookup_table_address = Pubkey::new_unique();
add_lookup_table_account(&mut context, lookup_table_address, initialized_table).await;
let ix = freeze_lookup_table(lookup_table_address, authority.pubkey());
assert_ix_error(
&mut context,
ix,
Some(&authority),
InstructionError::InvalidArgument,
)
.await;
}
#[tokio::test] #[tokio::test]
async fn test_freeze_lookup_table_with_wrong_authority() { async fn test_freeze_lookup_table_with_wrong_authority() {
let mut context = setup_test_context().await; let mut context = setup_test_context().await;

View File

@ -31,7 +31,7 @@ pub enum ProgramInstruction {
bump_seed: u8, bump_seed: u8,
}, },
/// Permanently freeze a address lookup table, making it immutable. /// Permanently freeze an address lookup table, making it immutable.
/// ///
/// # Account references /// # Account references
/// 0. `[WRITE]` Address lookup table account to freeze /// 0. `[WRITE]` Address lookup table account to freeze
@ -47,6 +47,14 @@ pub enum ProgramInstruction {
/// 3. `[]` System program for CPI. /// 3. `[]` System program for CPI.
ExtendLookupTable { new_addresses: Vec<Pubkey> }, ExtendLookupTable { new_addresses: Vec<Pubkey> },
/// Deactivate an address lookup table, making it unusable and
/// eligible for closure after a short period of time.
///
/// # Account references
/// 0. `[WRITE]` Address lookup table account to deactivate
/// 1. `[SIGNER]` Current authority
DeactivateLookupTable,
/// Close an address lookup table account /// Close an address lookup table account
/// ///
/// # Account references /// # Account references
@ -127,6 +135,23 @@ pub fn extend_lookup_table(
) )
} }
/// Constructs an instruction that deactivates an address lookup
/// table so that it cannot be extended again and will be unusable
/// and eligible for closure after a short amount of time.
pub fn deactivate_lookup_table(
lookup_table_address: Pubkey,
authority_address: Pubkey,
) -> Instruction {
Instruction::new_with_bincode(
id(),
&ProgramInstruction::DeactivateLookupTable,
vec![
AccountMeta::new(lookup_table_address, false),
AccountMeta::new_readonly(authority_address, true),
],
)
}
/// Returns an instruction that closes an address lookup table /// Returns an instruction that closes an address lookup table
/// account. The account will be deallocated and the lamports /// account. The account will be deallocated and the lamports
/// will be drained to the recipient address. /// will be drained to the recipient address.

View File

@ -47,6 +47,9 @@ pub fn process_instruction(
ProgramInstruction::ExtendLookupTable { new_addresses } => { ProgramInstruction::ExtendLookupTable { new_addresses } => {
Processor::extend_lookup_table(invoke_context, first_instruction_account, new_addresses) Processor::extend_lookup_table(invoke_context, first_instruction_account, new_addresses)
} }
ProgramInstruction::DeactivateLookupTable => {
Processor::deactivate_lookup_table(invoke_context, first_instruction_account)
}
ProgramInstruction::CloseLookupTable => { ProgramInstruction::CloseLookupTable => {
Processor::close_lookup_table(invoke_context, first_instruction_account) Processor::close_lookup_table(invoke_context, first_instruction_account)
} }
@ -152,7 +155,6 @@ impl Processor {
keyed_account_at_index(keyed_accounts, first_instruction_account)?; keyed_account_at_index(keyed_accounts, first_instruction_account)?;
lookup_table_account.set_state(&ProgramState::LookupTable(LookupTableMeta::new( lookup_table_account.set_state(&ProgramState::LookupTable(LookupTableMeta::new(
authority_key, authority_key,
derivation_slot,
)))?; )))?;
Ok(()) Ok(())
@ -187,6 +189,10 @@ 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, "Deactivated tables cannot be frozen");
return Err(InstructionError::InvalidArgument);
}
if lookup_table.addresses.is_empty() { if lookup_table.addresses.is_empty() {
ic_msg!(invoke_context, "Empty lookup tables cannot be frozen"); ic_msg!(invoke_context, "Empty lookup tables cannot be frozen");
return Err(InstructionError::InvalidInstructionData); return Err(InstructionError::InvalidInstructionData);
@ -244,6 +250,10 @@ 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, "Deactivated tables cannot be extended");
return Err(InstructionError::InvalidArgument);
}
if lookup_table.addresses.len() >= LOOKUP_TABLE_MAX_ADDRESSES { if lookup_table.addresses.len() >= LOOKUP_TABLE_MAX_ADDRESSES {
ic_msg!( ic_msg!(
invoke_context, invoke_context,
@ -320,6 +330,56 @@ impl Processor {
Ok(()) Ok(())
} }
fn deactivate_lookup_table(
invoke_context: &mut InvokeContext,
first_instruction_account: usize,
) -> Result<(), InstructionError> {
let keyed_accounts = invoke_context.get_keyed_accounts()?;
let lookup_table_account =
keyed_account_at_index(keyed_accounts, first_instruction_account)?;
if lookup_table_account.owner()? != crate::id() {
return Err(InstructionError::InvalidAccountOwner);
}
let authority_account =
keyed_account_at_index(keyed_accounts, checked_add(first_instruction_account, 1)?)?;
if authority_account.signer_key().is_none() {
return Err(InstructionError::MissingRequiredSignature);
}
let lookup_table_account_ref = lookup_table_account.try_account_ref()?;
let lookup_table_data = lookup_table_account_ref.data();
let lookup_table = AddressLookupTable::deserialize(lookup_table_data)?;
if lookup_table.meta.authority.is_none() {
ic_msg!(invoke_context, "Lookup table is frozen");
return Err(InstructionError::Immutable);
}
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 already deactivated");
return Err(InstructionError::InvalidArgument);
}
let mut lookup_table_meta = lookup_table.meta;
drop(lookup_table_account_ref);
let clock: Clock = invoke_context.get_sysvar(&clock::id())?;
lookup_table_meta.deactivation_slot = clock.slot;
AddressLookupTable::overwrite_meta_data(
lookup_table_account
.try_account_ref_mut()?
.data_as_mut_slice(),
lookup_table_meta,
)?;
Ok(())
}
fn close_lookup_table( fn close_lookup_table(
invoke_context: &mut InvokeContext, invoke_context: &mut InvokeContext,
first_instruction_account: usize, first_instruction_account: usize,
@ -353,16 +413,24 @@ impl Processor {
let lookup_table = AddressLookupTable::deserialize(lookup_table_data)?; let lookup_table = AddressLookupTable::deserialize(lookup_table_data)?;
if lookup_table.meta.authority.is_none() { if lookup_table.meta.authority.is_none() {
ic_msg!(invoke_context, "Lookup table is frozen");
return Err(InstructionError::Immutable); return Err(InstructionError::Immutable);
} }
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 slot used in the derivation path of the lookup table address // Assert that the deactivation slot is no longer recent to give in-flight transactions
// is no longer recent and can't be reused to initialize an account at the same address. // 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.derivation_slot) { if let Some(position) = slot_hashes.position(&lookup_table.meta.deactivation_slot) {
let expiration = MAX_ENTRIES.saturating_sub(position); let expiration = MAX_ENTRIES.saturating_sub(position);
ic_msg!( ic_msg!(
invoke_context, invoke_context,

View File

@ -22,12 +22,11 @@ pub enum ProgramState {
} }
/// Address lookup table metadata /// Address lookup table metadata
#[derive(Debug, Default, Serialize, Deserialize, PartialEq, Clone, AbiExample)] #[derive(Debug, Serialize, Deserialize, PartialEq, Clone, AbiExample)]
pub struct LookupTableMeta { pub struct LookupTableMeta {
/// The slot used to derive the table's address. The table cannot /// Lookup tables cannot be closed until the deactivation slot is
/// be closed until the derivation slot is no longer "recent" /// no longer "recent" (not accessible in the `SlotHashes` sysvar).
/// (not accessible in the `SlotHashes` sysvar). pub deactivation_slot: Slot,
pub derivation_slot: Slot,
/// The slot that the table was last extended. Address tables may /// The slot that the table was last extended. Address tables may
/// only be used to lookup addresses that were extended before /// only be used to lookup addresses that were extended before
/// the current bank's slot. /// the current bank's slot.
@ -43,10 +42,21 @@ pub struct LookupTableMeta {
// the account's data, starting from `LOOKUP_TABLE_META_SIZE`. // the account's data, starting from `LOOKUP_TABLE_META_SIZE`.
} }
impl Default for LookupTableMeta {
fn default() -> Self {
Self {
deactivation_slot: Slot::MAX,
last_extended_slot: 0,
last_extended_slot_start_index: 0,
authority: None,
_padding: 0,
}
}
}
impl LookupTableMeta { impl LookupTableMeta {
pub fn new(authority: Pubkey, derivation_slot: Slot) -> Self { pub fn new(authority: Pubkey) -> Self {
LookupTableMeta { LookupTableMeta {
derivation_slot,
authority: Some(authority), authority: Some(authority),
..LookupTableMeta::default() ..LookupTableMeta::default()
} }