* Add address lookup table program (#21616)
* Add address lookup table program
* feedback
(cherry picked from commit 9b41ddd9ba
)
# Conflicts:
# runtime/Cargo.toml
* resolve conflicts
Co-authored-by: Justin Starry <justin@solana.com>
This commit is contained in:
33
programs/address-lookup-table/Cargo.toml
Normal file
33
programs/address-lookup-table/Cargo.toml
Normal file
@ -0,0 +1,33 @@
|
||||
[package]
|
||||
name = "solana-address-lookup-table-program"
|
||||
version = "1.9.0"
|
||||
description = "Solana address lookup table program"
|
||||
authors = ["Solana Maintainers <maintainers@solana.foundation>"]
|
||||
repository = "https://github.com/solana-labs/solana"
|
||||
license = "Apache-2.0"
|
||||
homepage = "https://solana.com/"
|
||||
documentation = "https://docs.rs/solana-address-loookup-table-program"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
bincode = "1.3.3"
|
||||
bytemuck = "1.7.2"
|
||||
log = "0.4.14"
|
||||
num-derive = "0.3"
|
||||
num-traits = "0.2"
|
||||
serde = { version = "1.0.127", features = ["derive"] }
|
||||
solana-frozen-abi = { path = "../../frozen-abi", version = "=1.9.0" }
|
||||
solana-frozen-abi-macro = { path = "../../frozen-abi/macro", version = "=1.9.0" }
|
||||
solana-program-runtime = { path = "../../program-runtime", version = "=1.9.0" }
|
||||
solana-sdk = { path = "../../sdk", version = "=1.9.0" }
|
||||
thiserror = "1.0"
|
||||
|
||||
[build-dependencies]
|
||||
rustc_version = "0.4"
|
||||
|
||||
[lib]
|
||||
crate-type = ["lib"]
|
||||
name = "solana_address_lookup_table_program"
|
||||
|
||||
[package.metadata.docs.rs]
|
||||
targets = ["x86_64-unknown-linux-gnu"]
|
1
programs/address-lookup-table/build.rs
Symbolic link
1
programs/address-lookup-table/build.rs
Symbolic link
@ -0,0 +1 @@
|
||||
../../frozen-abi/build.rs
|
147
programs/address-lookup-table/src/instruction.rs
Normal file
147
programs/address-lookup-table/src/instruction.rs
Normal file
@ -0,0 +1,147 @@
|
||||
use {
|
||||
crate::id,
|
||||
serde::{Deserialize, Serialize},
|
||||
solana_sdk::{
|
||||
clock::Slot,
|
||||
instruction::{AccountMeta, Instruction},
|
||||
pubkey::Pubkey,
|
||||
system_program,
|
||||
},
|
||||
};
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone)]
|
||||
pub enum ProgramInstruction {
|
||||
/// Create an address lookup table
|
||||
///
|
||||
/// # Account references
|
||||
/// 0. `[WRITE]` Uninitialized address lookup table account
|
||||
/// 1. `[SIGNER]` Account used to derive and control the new address lookup table.
|
||||
/// 2. `[SIGNER, WRITE]` Account that will fund the new address lookup table.
|
||||
/// 3. `[]` System program for CPI.
|
||||
CreateLookupTable {
|
||||
/// A recent slot must be used in the derivation path
|
||||
/// for each initialized table. When closing table accounts,
|
||||
/// the initialization slot must no longer be "recent" to prevent
|
||||
/// address tables from being recreated with reordered or
|
||||
/// otherwise malicious addresses.
|
||||
recent_slot: Slot,
|
||||
/// Address tables are always initialized at program-derived
|
||||
/// addresses using the funding address, recent blockhash, and
|
||||
/// the user-passed `bump_seed`.
|
||||
bump_seed: u8,
|
||||
},
|
||||
|
||||
/// Permanently freeze a address lookup table, making it immutable.
|
||||
///
|
||||
/// # Account references
|
||||
/// 0. `[WRITE]` Address lookup table account to freeze
|
||||
/// 1. `[SIGNER]` Current authority
|
||||
FreezeLookupTable,
|
||||
|
||||
/// Extend an address lookup table with new addresses
|
||||
///
|
||||
/// # Account references
|
||||
/// 0. `[WRITE]` Address lookup table account to extend
|
||||
/// 1. `[SIGNER]` Current authority
|
||||
/// 2. `[SIGNER, WRITE]` Account that will fund the table reallocation
|
||||
/// 3. `[]` System program for CPI.
|
||||
ExtendLookupTable { new_addresses: Vec<Pubkey> },
|
||||
|
||||
/// Close an address lookup table account
|
||||
///
|
||||
/// # Account references
|
||||
/// 0. `[WRITE]` Address lookup table account to close
|
||||
/// 1. `[SIGNER]` Current authority
|
||||
/// 2. `[WRITE]` Recipient of closed account lamports
|
||||
CloseLookupTable,
|
||||
}
|
||||
|
||||
/// Derives the address of an address table account from a wallet address and a recent block's slot.
|
||||
pub fn derive_lookup_table_address(
|
||||
authority_address: &Pubkey,
|
||||
recent_block_slot: Slot,
|
||||
) -> (Pubkey, u8) {
|
||||
Pubkey::find_program_address(
|
||||
&[authority_address.as_ref(), &recent_block_slot.to_le_bytes()],
|
||||
&id(),
|
||||
)
|
||||
}
|
||||
|
||||
/// Constructs an instruction to create a table account and returns
|
||||
/// the instruction and the table account's derived address.
|
||||
pub fn create_lookup_table(
|
||||
authority_address: Pubkey,
|
||||
payer_address: Pubkey,
|
||||
recent_slot: Slot,
|
||||
) -> (Instruction, Pubkey) {
|
||||
let (lookup_table_address, bump_seed) =
|
||||
derive_lookup_table_address(&authority_address, recent_slot);
|
||||
let instruction = Instruction::new_with_bincode(
|
||||
id(),
|
||||
&ProgramInstruction::CreateLookupTable {
|
||||
recent_slot,
|
||||
bump_seed,
|
||||
},
|
||||
vec![
|
||||
AccountMeta::new(lookup_table_address, false),
|
||||
AccountMeta::new_readonly(authority_address, true),
|
||||
AccountMeta::new(payer_address, true),
|
||||
AccountMeta::new_readonly(system_program::id(), false),
|
||||
],
|
||||
);
|
||||
|
||||
(instruction, lookup_table_address)
|
||||
}
|
||||
|
||||
/// Constructs an instruction that freezes an address lookup
|
||||
/// table so that it can never be closed or extended again. Empty
|
||||
/// lookup tables cannot be frozen.
|
||||
pub fn freeze_lookup_table(lookup_table_address: Pubkey, authority_address: Pubkey) -> Instruction {
|
||||
Instruction::new_with_bincode(
|
||||
id(),
|
||||
&ProgramInstruction::FreezeLookupTable,
|
||||
vec![
|
||||
AccountMeta::new(lookup_table_address, false),
|
||||
AccountMeta::new_readonly(authority_address, true),
|
||||
],
|
||||
)
|
||||
}
|
||||
|
||||
/// Constructs an instruction which extends an address lookup
|
||||
/// table account with new addresses.
|
||||
pub fn extend_lookup_table(
|
||||
lookup_table_address: Pubkey,
|
||||
authority_address: Pubkey,
|
||||
payer_address: Pubkey,
|
||||
new_addresses: Vec<Pubkey>,
|
||||
) -> Instruction {
|
||||
Instruction::new_with_bincode(
|
||||
id(),
|
||||
&ProgramInstruction::ExtendLookupTable { new_addresses },
|
||||
vec![
|
||||
AccountMeta::new(lookup_table_address, false),
|
||||
AccountMeta::new_readonly(authority_address, true),
|
||||
AccountMeta::new(payer_address, true),
|
||||
AccountMeta::new_readonly(system_program::id(), false),
|
||||
],
|
||||
)
|
||||
}
|
||||
|
||||
/// Returns an instruction that closes an address lookup table
|
||||
/// account. The account will be deallocated and the lamports
|
||||
/// will be drained to the recipient address.
|
||||
pub fn close_lookup_table(
|
||||
lookup_table_address: Pubkey,
|
||||
authority_address: Pubkey,
|
||||
recipient_address: Pubkey,
|
||||
) -> Instruction {
|
||||
Instruction::new_with_bincode(
|
||||
id(),
|
||||
&ProgramInstruction::CloseLookupTable,
|
||||
vec![
|
||||
AccountMeta::new(lookup_table_address, false),
|
||||
AccountMeta::new_readonly(authority_address, true),
|
||||
AccountMeta::new(recipient_address, false),
|
||||
],
|
||||
)
|
||||
}
|
11
programs/address-lookup-table/src/lib.rs
Normal file
11
programs/address-lookup-table/src/lib.rs
Normal file
@ -0,0 +1,11 @@
|
||||
#![allow(incomplete_features)]
|
||||
#![cfg_attr(RUSTC_WITH_SPECIALIZATION, feature(specialization))]
|
||||
#![cfg_attr(RUSTC_NEEDS_PROC_MACRO_HYGIENE, feature(proc_macro_hygiene))]
|
||||
|
||||
use solana_sdk::declare_id;
|
||||
|
||||
pub mod instruction;
|
||||
pub mod processor;
|
||||
pub mod state;
|
||||
|
||||
declare_id!("AddressLookupTab1e1111111111111111111111111");
|
388
programs/address-lookup-table/src/processor.rs
Normal file
388
programs/address-lookup-table/src/processor.rs
Normal file
@ -0,0 +1,388 @@
|
||||
use {
|
||||
crate::{
|
||||
instruction::ProgramInstruction,
|
||||
state::{
|
||||
AddressLookupTable, LookupTableMeta, ProgramState, LOOKUP_TABLE_MAX_ADDRESSES,
|
||||
LOOKUP_TABLE_META_SIZE,
|
||||
},
|
||||
},
|
||||
solana_program_runtime::{ic_msg, invoke_context::InvokeContext},
|
||||
solana_sdk::{
|
||||
account::{ReadableAccount, WritableAccount},
|
||||
account_utils::State,
|
||||
clock::Slot,
|
||||
instruction::InstructionError,
|
||||
keyed_account::keyed_account_at_index,
|
||||
program_utils::limited_deserialize,
|
||||
pubkey::{Pubkey, PUBKEY_BYTES},
|
||||
slot_hashes::{SlotHashes, MAX_ENTRIES},
|
||||
system_instruction,
|
||||
sysvar::{
|
||||
clock::{self, Clock},
|
||||
rent::{self, Rent},
|
||||
slot_hashes,
|
||||
},
|
||||
},
|
||||
std::convert::TryFrom,
|
||||
};
|
||||
|
||||
pub fn process_instruction(
|
||||
first_instruction_account: usize,
|
||||
instruction_data: &[u8],
|
||||
invoke_context: &mut InvokeContext,
|
||||
) -> Result<(), InstructionError> {
|
||||
match limited_deserialize(instruction_data)? {
|
||||
ProgramInstruction::CreateLookupTable {
|
||||
recent_slot,
|
||||
bump_seed,
|
||||
} => Processor::create_lookup_table(
|
||||
invoke_context,
|
||||
first_instruction_account,
|
||||
recent_slot,
|
||||
bump_seed,
|
||||
),
|
||||
ProgramInstruction::FreezeLookupTable => {
|
||||
Processor::freeze_lookup_table(invoke_context, first_instruction_account)
|
||||
}
|
||||
ProgramInstruction::ExtendLookupTable { new_addresses } => {
|
||||
Processor::extend_lookup_table(invoke_context, first_instruction_account, new_addresses)
|
||||
}
|
||||
ProgramInstruction::CloseLookupTable => {
|
||||
Processor::close_lookup_table(invoke_context, first_instruction_account)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn checked_add(a: usize, b: usize) -> Result<usize, InstructionError> {
|
||||
a.checked_add(b).ok_or(InstructionError::ArithmeticOverflow)
|
||||
}
|
||||
|
||||
pub struct Processor;
|
||||
impl Processor {
|
||||
fn create_lookup_table(
|
||||
invoke_context: &mut InvokeContext,
|
||||
first_instruction_account: usize,
|
||||
untrusted_recent_slot: Slot,
|
||||
bump_seed: u8,
|
||||
) -> 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.data_len()? > 0 {
|
||||
ic_msg!(invoke_context, "Table account must not be allocated");
|
||||
return Err(InstructionError::AccountAlreadyInitialized);
|
||||
}
|
||||
|
||||
let authority_account =
|
||||
keyed_account_at_index(keyed_accounts, checked_add(first_instruction_account, 1)?)?;
|
||||
let authority_key = *authority_account.signer_key().ok_or_else(|| {
|
||||
ic_msg!(invoke_context, "Authority account must be a signer");
|
||||
InstructionError::MissingRequiredSignature
|
||||
})?;
|
||||
|
||||
let payer_account =
|
||||
keyed_account_at_index(keyed_accounts, checked_add(first_instruction_account, 2)?)?;
|
||||
let payer_key = *payer_account.signer_key().ok_or_else(|| {
|
||||
ic_msg!(invoke_context, "Payer account must be a signer");
|
||||
InstructionError::MissingRequiredSignature
|
||||
})?;
|
||||
|
||||
let derivation_slot = {
|
||||
let slot_hashes: SlotHashes = invoke_context.get_sysvar(&slot_hashes::id())?;
|
||||
if slot_hashes.get(&untrusted_recent_slot).is_some() {
|
||||
Ok(untrusted_recent_slot)
|
||||
} else {
|
||||
ic_msg!(
|
||||
invoke_context,
|
||||
"{} is not a recent slot",
|
||||
untrusted_recent_slot
|
||||
);
|
||||
Err(InstructionError::InvalidInstructionData)
|
||||
}
|
||||
}?;
|
||||
|
||||
// Use a derived address to ensure that an address table can never be
|
||||
// initialized more than once at the same address.
|
||||
let derived_table_key = Pubkey::create_program_address(
|
||||
&[
|
||||
authority_key.as_ref(),
|
||||
&derivation_slot.to_le_bytes(),
|
||||
&[bump_seed],
|
||||
],
|
||||
&crate::id(),
|
||||
)?;
|
||||
|
||||
let table_key = *lookup_table_account.unsigned_key();
|
||||
if table_key != derived_table_key {
|
||||
ic_msg!(
|
||||
invoke_context,
|
||||
"Table address must match derived address: {}",
|
||||
derived_table_key
|
||||
);
|
||||
return Err(InstructionError::InvalidArgument);
|
||||
}
|
||||
|
||||
let table_account_data_len = LOOKUP_TABLE_META_SIZE;
|
||||
let rent: Rent = invoke_context.get_sysvar(&rent::id())?;
|
||||
let required_lamports = rent
|
||||
.minimum_balance(table_account_data_len)
|
||||
.max(1)
|
||||
.saturating_sub(lookup_table_account.lamports()?);
|
||||
|
||||
if required_lamports > 0 {
|
||||
invoke_context.native_invoke(
|
||||
system_instruction::transfer(&payer_key, &table_key, required_lamports),
|
||||
&[payer_key],
|
||||
)?;
|
||||
}
|
||||
|
||||
invoke_context.native_invoke(
|
||||
system_instruction::allocate(&table_key, table_account_data_len as u64),
|
||||
&[table_key],
|
||||
)?;
|
||||
|
||||
invoke_context.native_invoke(
|
||||
system_instruction::assign(&table_key, &crate::id()),
|
||||
&[table_key],
|
||||
)?;
|
||||
|
||||
let keyed_accounts = invoke_context.get_keyed_accounts()?;
|
||||
let lookup_table_account =
|
||||
keyed_account_at_index(keyed_accounts, first_instruction_account)?;
|
||||
lookup_table_account.set_state(&ProgramState::LookupTable(LookupTableMeta::new(
|
||||
authority_key,
|
||||
derivation_slot,
|
||||
)))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn freeze_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 already frozen");
|
||||
return Err(InstructionError::Immutable);
|
||||
}
|
||||
if lookup_table.meta.authority != Some(*authority_account.unsigned_key()) {
|
||||
return Err(InstructionError::IncorrectAuthority);
|
||||
}
|
||||
if lookup_table.addresses.is_empty() {
|
||||
ic_msg!(invoke_context, "Empty lookup tables cannot be frozen");
|
||||
return Err(InstructionError::InvalidInstructionData);
|
||||
}
|
||||
|
||||
let mut lookup_table_meta = lookup_table.meta;
|
||||
drop(lookup_table_account_ref);
|
||||
|
||||
lookup_table_meta.authority = None;
|
||||
AddressLookupTable::overwrite_meta_data(
|
||||
lookup_table_account
|
||||
.try_account_ref_mut()?
|
||||
.data_as_mut_slice(),
|
||||
lookup_table_meta,
|
||||
)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn extend_lookup_table(
|
||||
invoke_context: &mut InvokeContext,
|
||||
first_instruction_account: usize,
|
||||
new_addresses: Vec<Pubkey>,
|
||||
) -> 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 payer_account =
|
||||
keyed_account_at_index(keyed_accounts, checked_add(first_instruction_account, 2)?)?;
|
||||
let payer_key = if let Some(payer_key) = payer_account.signer_key() {
|
||||
*payer_key
|
||||
} else {
|
||||
ic_msg!(invoke_context, "Payer account must be a signer");
|
||||
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 mut lookup_table = AddressLookupTable::deserialize(lookup_table_data)?;
|
||||
|
||||
if lookup_table.meta.authority.is_none() {
|
||||
return Err(InstructionError::Immutable);
|
||||
}
|
||||
if lookup_table.meta.authority != Some(*authority_account.unsigned_key()) {
|
||||
return Err(InstructionError::IncorrectAuthority);
|
||||
}
|
||||
if lookup_table.addresses.len() >= LOOKUP_TABLE_MAX_ADDRESSES {
|
||||
ic_msg!(
|
||||
invoke_context,
|
||||
"Lookup table is full and cannot contain more addresses"
|
||||
);
|
||||
return Err(InstructionError::InvalidArgument);
|
||||
}
|
||||
|
||||
if new_addresses.is_empty() {
|
||||
ic_msg!(invoke_context, "Must extend with at least one address");
|
||||
return Err(InstructionError::InvalidInstructionData);
|
||||
}
|
||||
|
||||
let new_table_addresses_len = lookup_table
|
||||
.addresses
|
||||
.len()
|
||||
.saturating_add(new_addresses.len());
|
||||
if new_table_addresses_len > LOOKUP_TABLE_MAX_ADDRESSES {
|
||||
ic_msg!(
|
||||
invoke_context,
|
||||
"Extended lookup table length {} would exceed max capacity of {}",
|
||||
new_table_addresses_len,
|
||||
LOOKUP_TABLE_MAX_ADDRESSES
|
||||
);
|
||||
return Err(InstructionError::InvalidInstructionData);
|
||||
}
|
||||
|
||||
let clock: Clock = invoke_context.get_sysvar(&clock::id())?;
|
||||
if clock.slot != lookup_table.meta.last_extended_slot {
|
||||
lookup_table.meta.last_extended_slot = clock.slot;
|
||||
lookup_table.meta.last_extended_slot_start_index =
|
||||
u8::try_from(lookup_table.addresses.len()).map_err(|_| {
|
||||
// This is impossible as long as the length of new_addresses
|
||||
// is non-zero and LOOKUP_TABLE_MAX_ADDRESSES == u8::MAX + 1.
|
||||
InstructionError::InvalidAccountData
|
||||
})?;
|
||||
}
|
||||
|
||||
let lookup_table_meta = lookup_table.meta;
|
||||
drop(lookup_table_account_ref);
|
||||
|
||||
let new_table_data_len = checked_add(
|
||||
LOOKUP_TABLE_META_SIZE,
|
||||
new_table_addresses_len.saturating_mul(PUBKEY_BYTES),
|
||||
)?;
|
||||
|
||||
{
|
||||
let mut lookup_table_account_ref_mut = lookup_table_account.try_account_ref_mut()?;
|
||||
AddressLookupTable::overwrite_meta_data(
|
||||
lookup_table_account_ref_mut.data_as_mut_slice(),
|
||||
lookup_table_meta,
|
||||
)?;
|
||||
|
||||
let table_data = lookup_table_account_ref_mut.data_mut();
|
||||
for new_address in new_addresses {
|
||||
table_data.extend_from_slice(new_address.as_ref());
|
||||
}
|
||||
}
|
||||
|
||||
let rent: Rent = invoke_context.get_sysvar(&rent::id())?;
|
||||
let required_lamports = rent
|
||||
.minimum_balance(new_table_data_len)
|
||||
.max(1)
|
||||
.saturating_sub(lookup_table_account.lamports()?);
|
||||
|
||||
let table_key = *lookup_table_account.unsigned_key();
|
||||
if required_lamports > 0 {
|
||||
invoke_context.native_invoke(
|
||||
system_instruction::transfer(&payer_key, &table_key, required_lamports),
|
||||
&[payer_key],
|
||||
)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn close_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 recipient_account =
|
||||
keyed_account_at_index(keyed_accounts, checked_add(first_instruction_account, 2)?)?;
|
||||
if recipient_account.unsigned_key() == lookup_table_account.unsigned_key() {
|
||||
ic_msg!(
|
||||
invoke_context,
|
||||
"Lookup table cannot be the recipient of reclaimed lamports"
|
||||
);
|
||||
return Err(InstructionError::InvalidArgument);
|
||||
}
|
||||
|
||||
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() {
|
||||
return Err(InstructionError::Immutable);
|
||||
}
|
||||
if lookup_table.meta.authority != Some(*authority_account.unsigned_key()) {
|
||||
return Err(InstructionError::IncorrectAuthority);
|
||||
}
|
||||
|
||||
// Assert that the slot used in the derivation path of the lookup table address
|
||||
// is no longer recent and can't be reused to initialize an account at the same address.
|
||||
let slot_hashes: SlotHashes = invoke_context.get_sysvar(&slot_hashes::id())?;
|
||||
if let Some(position) = slot_hashes.position(&lookup_table.meta.derivation_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);
|
||||
}
|
||||
|
||||
drop(lookup_table_account_ref);
|
||||
|
||||
let withdrawn_lamports = lookup_table_account.lamports()?;
|
||||
recipient_account
|
||||
.try_account_ref_mut()?
|
||||
.checked_add_lamports(withdrawn_lamports)?;
|
||||
|
||||
let mut lookup_table_account = lookup_table_account.try_account_ref_mut()?;
|
||||
lookup_table_account.set_data(Vec::new());
|
||||
lookup_table_account.set_lamports(0);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
198
programs/address-lookup-table/src/state.rs
Normal file
198
programs/address-lookup-table/src/state.rs
Normal file
@ -0,0 +1,198 @@
|
||||
use {
|
||||
serde::{Deserialize, Serialize},
|
||||
solana_frozen_abi_macro::{AbiEnumVisitor, AbiExample},
|
||||
solana_sdk::{clock::Slot, instruction::InstructionError, pubkey::Pubkey},
|
||||
std::borrow::Cow,
|
||||
};
|
||||
|
||||
/// The maximum number of addresses that a lookup table can hold
|
||||
pub const LOOKUP_TABLE_MAX_ADDRESSES: usize = 256;
|
||||
|
||||
/// The serialized size of lookup table metadata
|
||||
pub const LOOKUP_TABLE_META_SIZE: usize = 56;
|
||||
|
||||
/// Program account states
|
||||
#[derive(Debug, Serialize, Deserialize, PartialEq, Clone, AbiExample, AbiEnumVisitor)]
|
||||
#[allow(clippy::large_enum_variant)]
|
||||
pub enum ProgramState {
|
||||
/// Account is not initialized.
|
||||
Uninitialized,
|
||||
/// Initialized `LookupTable` account.
|
||||
LookupTable(LookupTableMeta),
|
||||
}
|
||||
|
||||
/// Address lookup table metadata
|
||||
#[derive(Debug, Default, Serialize, Deserialize, PartialEq, Clone, AbiExample)]
|
||||
pub struct LookupTableMeta {
|
||||
/// The slot used to derive the table's address. The table cannot
|
||||
/// be closed until the derivation slot is no longer "recent"
|
||||
/// (not accessible in the `SlotHashes` sysvar).
|
||||
pub derivation_slot: Slot,
|
||||
/// The slot that the table was last extended. Address tables may
|
||||
/// only be used to lookup addresses that were extended before
|
||||
/// the current bank's slot.
|
||||
pub last_extended_slot: Slot,
|
||||
/// The start index where the table was last extended from during
|
||||
/// the `last_extended_slot`.
|
||||
pub last_extended_slot_start_index: u8,
|
||||
/// Authority address which must sign for each modification.
|
||||
pub authority: Option<Pubkey>,
|
||||
// Padding to keep addresses 8-byte aligned
|
||||
pub _padding: u16,
|
||||
// Raw list of addresses follows this serialized structure in
|
||||
// the account's data, starting from `LOOKUP_TABLE_META_SIZE`.
|
||||
}
|
||||
|
||||
impl LookupTableMeta {
|
||||
pub fn new(authority: Pubkey, derivation_slot: Slot) -> Self {
|
||||
LookupTableMeta {
|
||||
derivation_slot,
|
||||
authority: Some(authority),
|
||||
..LookupTableMeta::default()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Clone, AbiExample)]
|
||||
pub struct AddressLookupTable<'a> {
|
||||
pub meta: LookupTableMeta,
|
||||
pub addresses: Cow<'a, [Pubkey]>,
|
||||
}
|
||||
|
||||
impl<'a> AddressLookupTable<'a> {
|
||||
/// Serialize an address table's updated meta data and zero
|
||||
/// any leftover bytes.
|
||||
pub fn overwrite_meta_data(
|
||||
data: &mut [u8],
|
||||
lookup_table_meta: LookupTableMeta,
|
||||
) -> Result<(), InstructionError> {
|
||||
let meta_data = data
|
||||
.get_mut(0..LOOKUP_TABLE_META_SIZE)
|
||||
.ok_or(InstructionError::InvalidAccountData)?;
|
||||
meta_data.fill(0);
|
||||
bincode::serialize_into(meta_data, &ProgramState::LookupTable(lookup_table_meta))
|
||||
.map_err(|_| InstructionError::GenericError)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Serialize an address table including its addresses
|
||||
pub fn serialize_for_tests(self, data: &mut Vec<u8>) -> Result<(), InstructionError> {
|
||||
data.resize(LOOKUP_TABLE_META_SIZE, 0);
|
||||
Self::overwrite_meta_data(data, self.meta)?;
|
||||
self.addresses.iter().for_each(|address| {
|
||||
data.extend_from_slice(address.as_ref());
|
||||
});
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Efficiently deserialize an address table without allocating
|
||||
/// for stored addresses.
|
||||
pub fn deserialize(data: &'a [u8]) -> Result<AddressLookupTable<'a>, InstructionError> {
|
||||
let program_state: ProgramState =
|
||||
bincode::deserialize(data).map_err(|_| InstructionError::InvalidAccountData)?;
|
||||
|
||||
let meta = match program_state {
|
||||
ProgramState::LookupTable(meta) => Ok(meta),
|
||||
ProgramState::Uninitialized => Err(InstructionError::UninitializedAccount),
|
||||
}?;
|
||||
|
||||
let raw_addresses_data = data.get(LOOKUP_TABLE_META_SIZE..).ok_or({
|
||||
// Should be impossible because table accounts must
|
||||
// always be LOOKUP_TABLE_META_SIZE in length
|
||||
InstructionError::InvalidAccountData
|
||||
})?;
|
||||
let addresses: &[Pubkey] = bytemuck::try_cast_slice(raw_addresses_data).map_err(|_| {
|
||||
// Should be impossible because raw address data
|
||||
// should be aligned and sized in multiples of 32 bytes
|
||||
InstructionError::InvalidAccountData
|
||||
})?;
|
||||
|
||||
Ok(Self {
|
||||
meta,
|
||||
addresses: Cow::Borrowed(addresses),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
impl AddressLookupTable<'_> {
|
||||
fn new_for_tests(meta: LookupTableMeta, num_addresses: usize) -> Self {
|
||||
let mut addresses = Vec::with_capacity(num_addresses);
|
||||
addresses.resize_with(num_addresses, Pubkey::new_unique);
|
||||
AddressLookupTable {
|
||||
meta,
|
||||
addresses: Cow::Owned(addresses),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl LookupTableMeta {
|
||||
fn new_for_tests() -> Self {
|
||||
Self {
|
||||
authority: Some(Pubkey::new_unique()),
|
||||
..LookupTableMeta::default()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_lookup_table_meta_size() {
|
||||
let lookup_table = ProgramState::LookupTable(LookupTableMeta::new_for_tests());
|
||||
let meta_size = bincode::serialized_size(&lookup_table).unwrap();
|
||||
assert!(meta_size as usize <= LOOKUP_TABLE_META_SIZE);
|
||||
assert_eq!(meta_size as usize, 56);
|
||||
|
||||
let lookup_table = ProgramState::LookupTable(LookupTableMeta::default());
|
||||
let meta_size = bincode::serialized_size(&lookup_table).unwrap();
|
||||
assert!(meta_size as usize <= LOOKUP_TABLE_META_SIZE);
|
||||
assert_eq!(meta_size as usize, 24);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_overwrite_meta_data() {
|
||||
let meta = LookupTableMeta::new_for_tests();
|
||||
let empty_table = ProgramState::LookupTable(meta.clone());
|
||||
let mut serialized_table_1 = bincode::serialize(&empty_table).unwrap();
|
||||
serialized_table_1.resize(LOOKUP_TABLE_META_SIZE, 0);
|
||||
|
||||
let address_table = AddressLookupTable::new_for_tests(meta, 0);
|
||||
let mut serialized_table_2 = Vec::new();
|
||||
serialized_table_2.resize(LOOKUP_TABLE_META_SIZE, 0);
|
||||
AddressLookupTable::overwrite_meta_data(&mut serialized_table_2, address_table.meta)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(serialized_table_1, serialized_table_2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_deserialize() {
|
||||
assert_eq!(
|
||||
AddressLookupTable::deserialize(&[]).err(),
|
||||
Some(InstructionError::InvalidAccountData),
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
AddressLookupTable::deserialize(&[0u8; LOOKUP_TABLE_META_SIZE]).err(),
|
||||
Some(InstructionError::UninitializedAccount),
|
||||
);
|
||||
|
||||
fn test_case(num_addresses: usize) {
|
||||
let lookup_table_meta = LookupTableMeta::new_for_tests();
|
||||
let address_table = AddressLookupTable::new_for_tests(lookup_table_meta, num_addresses);
|
||||
let mut address_table_data = Vec::new();
|
||||
AddressLookupTable::serialize_for_tests(address_table.clone(), &mut address_table_data)
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
AddressLookupTable::deserialize(&address_table_data).unwrap(),
|
||||
address_table,
|
||||
);
|
||||
}
|
||||
|
||||
for case in [0, 1, 10, 255, 256] {
|
||||
test_case(case);
|
||||
}
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user