diff --git a/Cargo.lock b/Cargo.lock index d2ae0df42..33b98f30e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4459,6 +4459,35 @@ dependencies = [ "tokio-postgres", ] +[[package]] +name = "solana-address-lookup-table-program" +version = "1.10.0" +dependencies = [ + "bincode", + "bytemuck", + "log 0.4.14", + "num-derive", + "num-traits", + "rustc_version 0.4.0", + "serde", + "solana-frozen-abi 1.10.0", + "solana-frozen-abi-macro 1.10.0", + "solana-program-runtime", + "solana-sdk", + "thiserror", +] + +[[package]] +name = "solana-address-lookup-table-program-tests" +version = "1.10.0" +dependencies = [ + "assert_matches", + "bincode", + "solana-address-lookup-table-program", + "solana-program-test", + "solana-sdk", +] + [[package]] name = "solana-banking-bench" version = "1.10.0" @@ -5714,6 +5743,7 @@ dependencies = [ "rustc_version 0.4.0", "serde", "serde_derive", + "solana-address-lookup-table-program", "solana-bucket-map", "solana-compute-budget-program", "solana-config-program", diff --git a/Cargo.toml b/Cargo.toml index 124e11a57..3a75258f2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -46,6 +46,8 @@ members = [ "poh", "poh-bench", "program-test", + "programs/address-lookup-table", + "programs/address-lookup-table-tests", "programs/bpf_loader", "programs/compute-budget", "programs/config", diff --git a/program-test/src/lib.rs b/program-test/src/lib.rs index eebb76b7c..4bd9e33a8 100644 --- a/program-test/src/lib.rs +++ b/program-test/src/lib.rs @@ -41,7 +41,7 @@ use { sysvar::{ clock, epoch_schedule, fees::{self}, - rent, Sysvar, + rent, Sysvar, SysvarId, }, }, solana_vote_program::vote_state::{VoteState, VoteStateVersions}, @@ -1045,6 +1045,18 @@ impl ProgramTestContext { bank.store_account(address, account); } + /// Create or overwrite a sysvar, subverting normal runtime checks. + /// + /// This method exists to make it easier to set up artificial situations + /// that would be difficult to replicate on a new test cluster. Beware + /// that it can be used to create states that would not be reachable + /// under normal conditions! + pub fn set_sysvar(&self, sysvar: &T) { + let bank_forks = self.bank_forks.read().unwrap(); + let bank = bank_forks.working_bank(); + bank.set_sysvar_for_tests(sysvar); + } + /// Force the working bank ahead to a new slot pub fn warp_to_slot(&mut self, warp_slot: Slot) -> Result<(), ProgramTestError> { let mut bank_forks = self.bank_forks.write().unwrap(); diff --git a/programs/address-lookup-table-tests/Cargo.toml b/programs/address-lookup-table-tests/Cargo.toml new file mode 100644 index 000000000..c89377eb2 --- /dev/null +++ b/programs/address-lookup-table-tests/Cargo.toml @@ -0,0 +1,22 @@ +# This package only exists to avoid circular dependencies during cargo publish: +# solana-runtime -> solana-address-program-runtime -> solana-program-test -> solana-runtime + +[package] +name = "solana-address-lookup-table-program-tests" +version = "1.10.0" +authors = ["Solana Maintainers "] +repository = "https://github.com/solana-labs/solana" +license = "Apache-2.0" +homepage = "https://solana.com/" +edition = "2021" +publish = false + +[dev-dependencies] +assert_matches = "1.5.0" +bincode = "1.3.3" +solana-address-lookup-table-program = { path = "../address-lookup-table", version = "=1.10.0" } +solana-program-test = { path = "../../program-test", version = "=1.10.0" } +solana-sdk = { path = "../../sdk", version = "=1.10.0" } + +[package.metadata.docs.rs] +targets = ["x86_64-unknown-linux-gnu"] diff --git a/programs/address-lookup-table-tests/tests/close_lookup_table_ix.rs b/programs/address-lookup-table-tests/tests/close_lookup_table_ix.rs new file mode 100644 index 000000000..0ffc88455 --- /dev/null +++ b/programs/address-lookup-table-tests/tests/close_lookup_table_ix.rs @@ -0,0 +1,151 @@ +use { + assert_matches::assert_matches, + common::{ + add_lookup_table_account, assert_ix_error, new_address_lookup_table, + overwrite_slot_hashes_with_slots, setup_test_context, + }, + solana_address_lookup_table_program::instruction::close_lookup_table, + solana_program_test::*, + solana_sdk::{ + instruction::InstructionError, + pubkey::Pubkey, + signature::{Keypair, Signer}, + transaction::Transaction, + }, +}; + +mod common; + +#[tokio::test] +async fn test_close_lookup_table() { + let mut context = setup_test_context().await; + 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(); + add_lookup_table_account(&mut context, lookup_table_address, initialized_table).await; + + let client = &mut context.banks_client; + let payer = &context.payer; + let recent_blockhash = context.last_blockhash; + let transaction = Transaction::new_signed_with_payer( + &[close_lookup_table( + lookup_table_address, + authority_keypair.pubkey(), + context.payer.pubkey(), + )], + Some(&payer.pubkey()), + &[payer, &authority_keypair], + recent_blockhash, + ); + + assert_matches!(client.process_transaction(transaction).await, Ok(())); + assert!(client + .get_account(lookup_table_address) + .await + .unwrap() + .is_none()); +} + +#[tokio::test] +async fn test_close_lookup_table_too_recent() { + let mut context = setup_test_context().await; + + let authority_keypair = Keypair::new(); + let initialized_table = new_address_lookup_table(Some(authority_keypair.pubkey()), 0); + 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 what the default initialized table + // has as its derivation slot. 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_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 = close_lookup_table( + lookup_table_address, + authority.pubkey(), + Pubkey::new_unique(), + ); + + assert_ix_error( + &mut context, + ix, + Some(&authority), + InstructionError::Immutable, + ) + .await; +} + +#[tokio::test] +async fn test_close_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 = close_lookup_table( + lookup_table_address, + wrong_authority.pubkey(), + Pubkey::new_unique(), + ); + + assert_ix_error( + &mut context, + ix, + Some(&wrong_authority), + InstructionError::IncorrectAuthority, + ) + .await; +} + +#[tokio::test] +async fn test_close_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 = close_lookup_table( + lookup_table_address, + authority.pubkey(), + Pubkey::new_unique(), + ); + ix.accounts[1].is_signer = false; + + assert_ix_error( + &mut context, + ix, + None, + InstructionError::MissingRequiredSignature, + ) + .await; +} diff --git a/programs/address-lookup-table-tests/tests/common.rs b/programs/address-lookup-table-tests/tests/common.rs new file mode 100644 index 000000000..a29fd6010 --- /dev/null +++ b/programs/address-lookup-table-tests/tests/common.rs @@ -0,0 +1,103 @@ +#![allow(dead_code)] +use { + solana_address_lookup_table_program::{ + id, + processor::process_instruction, + state::{AddressLookupTable, LookupTableMeta}, + }, + solana_program_test::*, + solana_sdk::{ + account::AccountSharedData, + clock::Slot, + hash::Hash, + instruction::Instruction, + instruction::InstructionError, + pubkey::Pubkey, + signature::{Keypair, Signer}, + slot_hashes::SlotHashes, + transaction::{Transaction, TransactionError}, + }, + std::borrow::Cow, +}; + +pub async fn setup_test_context() -> ProgramTestContext { + let program_test = ProgramTest::new("", id(), Some(process_instruction)); + program_test.start_with_context().await +} + +pub async fn assert_ix_error( + context: &mut ProgramTestContext, + ix: Instruction, + authority_keypair: Option<&Keypair>, + expected_err: InstructionError, +) { + let client = &mut context.banks_client; + let payer = &context.payer; + let recent_blockhash = context.last_blockhash; + + let mut signers = vec![payer]; + if let Some(authority) = authority_keypair { + signers.push(authority); + } + + let transaction = Transaction::new_signed_with_payer( + &[ix], + Some(&payer.pubkey()), + &signers, + recent_blockhash, + ); + + assert_eq!( + client + .process_transaction(transaction) + .await + .unwrap_err() + .unwrap(), + TransactionError::InstructionError(0, expected_err), + ); +} + +pub fn new_address_lookup_table( + authority: Option, + num_addresses: usize, +) -> AddressLookupTable<'static> { + let mut addresses = Vec::with_capacity(num_addresses); + addresses.resize_with(num_addresses, Pubkey::new_unique); + AddressLookupTable { + meta: LookupTableMeta { + authority, + ..LookupTableMeta::default() + }, + addresses: Cow::Owned(addresses), + } +} + +pub async fn add_lookup_table_account( + context: &mut ProgramTestContext, + account_address: Pubkey, + address_lookup_table: AddressLookupTable<'static>, +) -> AccountSharedData { + let mut data = Vec::new(); + address_lookup_table.serialize_for_tests(&mut data).unwrap(); + + let rent = context.banks_client.get_rent().await.unwrap(); + let rent_exempt_balance = rent.minimum_balance(data.len()); + + let mut account = AccountSharedData::new( + rent_exempt_balance, + data.len(), + &solana_address_lookup_table_program::id(), + ); + account.set_data(data); + context.set_account(&account_address, &account); + + account +} + +pub fn overwrite_slot_hashes_with_slots(context: &mut ProgramTestContext, slots: &[Slot]) { + let mut slot_hashes = SlotHashes::default(); + for slot in slots { + slot_hashes.add(*slot, Hash::new_unique()); + } + context.set_sysvar(&slot_hashes); +} diff --git a/programs/address-lookup-table-tests/tests/create_lookup_table_ix.rs b/programs/address-lookup-table-tests/tests/create_lookup_table_ix.rs new file mode 100644 index 000000000..7f4da6f27 --- /dev/null +++ b/programs/address-lookup-table-tests/tests/create_lookup_table_ix.rs @@ -0,0 +1,158 @@ +use { + assert_matches::assert_matches, + common::{assert_ix_error, overwrite_slot_hashes_with_slots, setup_test_context}, + solana_address_lookup_table_program::{ + id, + instruction::create_lookup_table, + state::{AddressLookupTable, LOOKUP_TABLE_META_SIZE}, + }, + solana_program_test::*, + solana_sdk::{ + clock::Slot, instruction::InstructionError, pubkey::Pubkey, rent::Rent, signature::Signer, + signer::keypair::Keypair, transaction::Transaction, + }, +}; + +mod common; + +#[tokio::test] +async fn test_create_lookup_table() { + let mut context = setup_test_context().await; + + let test_recent_slot = 123; + overwrite_slot_hashes_with_slots(&mut context, &[test_recent_slot]); + + let client = &mut context.banks_client; + let payer = &context.payer; + let recent_blockhash = context.last_blockhash; + let authority_keypair = Keypair::new(); + let authority_address = authority_keypair.pubkey(); + let (create_lookup_table_ix, lookup_table_address) = + create_lookup_table(authority_address, payer.pubkey(), test_recent_slot); + + // First create should succeed + { + let transaction = Transaction::new_signed_with_payer( + &[create_lookup_table_ix.clone()], + Some(&payer.pubkey()), + &[payer, &authority_keypair], + recent_blockhash, + ); + + assert_matches!(client.process_transaction(transaction).await, Ok(())); + let lookup_table_account = client + .get_account(lookup_table_address) + .await + .unwrap() + .unwrap(); + assert_eq!(lookup_table_account.owner, crate::id()); + assert_eq!(lookup_table_account.data.len(), LOOKUP_TABLE_META_SIZE); + assert_eq!( + lookup_table_account.lamports, + Rent::default().minimum_balance(LOOKUP_TABLE_META_SIZE) + ); + 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.authority, Some(authority_address)); + assert_eq!(lookup_table.meta.last_extended_slot, 0); + assert_eq!(lookup_table.meta.last_extended_slot_start_index, 0); + assert_eq!(lookup_table.addresses.len(), 0); + } + + // Second create should fail + { + context.last_blockhash = client + .get_new_latest_blockhash(&recent_blockhash) + .await + .unwrap(); + assert_ix_error( + &mut context, + create_lookup_table_ix, + Some(&authority_keypair), + InstructionError::AccountAlreadyInitialized, + ) + .await; + } +} + +#[tokio::test] +async fn test_create_lookup_table_use_payer_as_authority() { + let mut context = setup_test_context().await; + + let test_recent_slot = 123; + overwrite_slot_hashes_with_slots(&mut context, &[test_recent_slot]); + + let client = &mut context.banks_client; + let payer = &context.payer; + let recent_blockhash = context.last_blockhash; + let authority_address = payer.pubkey(); + let transaction = Transaction::new_signed_with_payer( + &[create_lookup_table(authority_address, payer.pubkey(), test_recent_slot).0], + Some(&payer.pubkey()), + &[payer], + recent_blockhash, + ); + + assert_matches!(client.process_transaction(transaction).await, Ok(())); +} + +#[tokio::test] +async fn test_create_lookup_table_without_signer() { + let mut context = setup_test_context().await; + let unsigned_authority_address = Pubkey::new_unique(); + + let mut ix = create_lookup_table( + unsigned_authority_address, + context.payer.pubkey(), + Slot::MAX, + ) + .0; + ix.accounts[1].is_signer = false; + + assert_ix_error( + &mut context, + ix, + None, + InstructionError::MissingRequiredSignature, + ) + .await; +} + +#[tokio::test] +async fn test_create_lookup_table_not_recent_slot() { + let mut context = setup_test_context().await; + let payer = &context.payer; + let authority_keypair = Keypair::new(); + let authority_address = authority_keypair.pubkey(); + + let ix = create_lookup_table(authority_address, payer.pubkey(), Slot::MAX).0; + + assert_ix_error( + &mut context, + ix, + Some(&authority_keypair), + InstructionError::InvalidInstructionData, + ) + .await; +} + +#[tokio::test] +async fn test_create_lookup_table_pda_mismatch() { + let mut context = setup_test_context().await; + let test_recent_slot = 123; + overwrite_slot_hashes_with_slots(&mut context, &[test_recent_slot]); + let payer = &context.payer; + let authority_keypair = Keypair::new(); + let authority_address = authority_keypair.pubkey(); + + let mut ix = create_lookup_table(authority_address, payer.pubkey(), test_recent_slot).0; + ix.accounts[0].pubkey = Pubkey::new_unique(); + + assert_ix_error( + &mut context, + ix, + Some(&authority_keypair), + InstructionError::InvalidArgument, + ) + .await; +} diff --git a/programs/address-lookup-table-tests/tests/extend_lookup_table_ix.rs b/programs/address-lookup-table-tests/tests/extend_lookup_table_ix.rs new file mode 100644 index 000000000..ffed5c619 --- /dev/null +++ b/programs/address-lookup-table-tests/tests/extend_lookup_table_ix.rs @@ -0,0 +1,214 @@ +use { + assert_matches::assert_matches, + common::{add_lookup_table_account, new_address_lookup_table, setup_test_context}, + solana_address_lookup_table_program::{ + instruction::extend_lookup_table, + state::{AddressLookupTable, LookupTableMeta}, + }, + solana_program_test::*, + solana_sdk::{ + account::ReadableAccount, + instruction::Instruction, + instruction::InstructionError, + pubkey::{Pubkey, PUBKEY_BYTES}, + signature::{Keypair, Signer}, + transaction::{Transaction, TransactionError}, + }, + std::borrow::Cow, + std::result::Result, +}; + +mod common; + +struct ExpectedTableAccount { + lamports: u64, + data_len: usize, + state: AddressLookupTable<'static>, +} + +struct TestCase<'a> { + lookup_table_address: Pubkey, + instruction: Instruction, + extra_signer: Option<&'a Keypair>, + expected_result: Result, +} + +async fn run_test_case(context: &mut ProgramTestContext, test_case: TestCase<'_>) { + let client = &mut context.banks_client; + let payer = &context.payer; + let recent_blockhash = context.last_blockhash; + + let mut signers = vec![payer]; + if let Some(extra_signer) = test_case.extra_signer { + signers.push(extra_signer); + } + + let transaction = Transaction::new_signed_with_payer( + &[test_case.instruction], + Some(&payer.pubkey()), + &signers, + recent_blockhash, + ); + + let process_result = client.process_transaction(transaction).await; + + match test_case.expected_result { + Ok(expected_account) => { + assert_matches!(process_result, Ok(())); + + let table_account = client + .get_account(test_case.lookup_table_address) + .await + .unwrap() + .unwrap(); + + let lookup_table = AddressLookupTable::deserialize(&table_account.data).unwrap(); + assert_eq!(lookup_table, expected_account.state); + assert_eq!(table_account.lamports(), expected_account.lamports); + assert_eq!(table_account.data().len(), expected_account.data_len); + } + Err(expected_err) => { + assert_eq!( + process_result.unwrap_err().unwrap(), + TransactionError::InstructionError(0, expected_err), + ); + } + } +} + +#[tokio::test] +async fn test_extend_lookup_table() { + let mut context = setup_test_context().await; + let authority = Keypair::new(); + let current_bank_slot = 1; + let rent = context.banks_client.get_rent().await.unwrap(); + + for extend_same_slot in [true, false] { + for (num_existing_addresses, num_new_addresses, expected_result) in [ + (0, 0, Err(InstructionError::InvalidInstructionData)), + (0, 1, Ok(())), + (0, 10, Ok(())), + (1, 1, Ok(())), + (1, 10, Ok(())), + (255, 1, Ok(())), + (255, 2, Err(InstructionError::InvalidInstructionData)), + (246, 10, Ok(())), + (256, 1, Err(InstructionError::InvalidArgument)), + ] { + let mut lookup_table = + new_address_lookup_table(Some(authority.pubkey()), num_existing_addresses); + if extend_same_slot { + lookup_table.meta.last_extended_slot = current_bank_slot; + } + + let lookup_table_address = Pubkey::new_unique(); + let lookup_table_account = + add_lookup_table_account(&mut context, lookup_table_address, lookup_table.clone()) + .await; + + let mut new_addresses = Vec::with_capacity(num_new_addresses); + new_addresses.resize_with(num_new_addresses, Pubkey::new_unique); + let instruction = extend_lookup_table( + lookup_table_address, + authority.pubkey(), + context.payer.pubkey(), + new_addresses.clone(), + ); + + let mut expected_addresses: Vec = lookup_table.addresses.to_vec(); + expected_addresses.extend(new_addresses); + + let expected_result = expected_result.map(|_| { + let expected_data_len = + lookup_table_account.data().len() + num_new_addresses * PUBKEY_BYTES; + let expected_lamports = rent.minimum_balance(expected_data_len); + let expected_lookup_table = AddressLookupTable { + meta: LookupTableMeta { + last_extended_slot: current_bank_slot, + last_extended_slot_start_index: if extend_same_slot { + 0u8 + } else { + num_existing_addresses as u8 + }, + derivation_slot: lookup_table.meta.derivation_slot, + authority: lookup_table.meta.authority, + _padding: 0u16, + }, + addresses: Cow::Owned(expected_addresses), + }; + ExpectedTableAccount { + lamports: expected_lamports, + data_len: expected_data_len, + state: expected_lookup_table, + } + }); + + let test_case = TestCase { + lookup_table_address, + instruction, + extra_signer: Some(&authority), + expected_result, + }; + + run_test_case(&mut context, test_case).await; + } + } +} + +#[tokio::test] +async fn test_extend_addresses_authority_errors() { + let mut context = setup_test_context().await; + let authority = Keypair::new(); + + for (existing_authority, ix_authority, use_signer, expected_err) in [ + ( + 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 _ = add_lookup_table_account(&mut context, lookup_table_address, lookup_table.clone()) + .await; + + let num_new_addresses = 1; + let mut new_addresses = Vec::with_capacity(num_new_addresses); + new_addresses.resize_with(num_new_addresses, Pubkey::new_unique); + let mut instruction = extend_lookup_table( + lookup_table_address, + ix_authority.pubkey(), + context.payer.pubkey(), + new_addresses.clone(), + ); + if !use_signer { + instruction.accounts[1].is_signer = false; + } + + let mut expected_addresses: Vec = lookup_table.addresses.to_vec(); + expected_addresses.extend(new_addresses); + + let extra_signer = if use_signer { + Some(&ix_authority) + } else { + None + }; + + let test_case = TestCase { + lookup_table_address, + instruction, + extra_signer, + expected_result: Err(expected_err), + }; + + run_test_case(&mut context, test_case).await; + } +} diff --git a/programs/address-lookup-table-tests/tests/freeze_lookup_table_ix.rs b/programs/address-lookup-table-tests/tests/freeze_lookup_table_ix.rs new file mode 100644 index 000000000..acb638a39 --- /dev/null +++ b/programs/address-lookup-table-tests/tests/freeze_lookup_table_ix.rs @@ -0,0 +1,141 @@ +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::freeze_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_freeze_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( + &[freeze_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.authority, None); + + // Check that only the authority changed + initialized_table.meta.authority = None; + assert_eq!(initialized_table, lookup_table); +} + +#[tokio::test] +async fn test_freeze_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 = freeze_lookup_table(lookup_table_address, authority.pubkey()); + + assert_ix_error( + &mut context, + ix, + Some(&authority), + InstructionError::Immutable, + ) + .await; +} + +#[tokio::test] +async fn test_freeze_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 = freeze_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_freeze_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 = freeze_lookup_table(lookup_table_address, authority.pubkey()); + ix.accounts[1].is_signer = false; + + assert_ix_error( + &mut context, + ix, + None, + InstructionError::MissingRequiredSignature, + ) + .await; +} + +#[tokio::test] +async fn test_freeze_empty_lookup_table() { + let mut context = setup_test_context().await; + + let authority = Keypair::new(); + let initialized_table = new_address_lookup_table(Some(authority.pubkey()), 0); + 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::InvalidInstructionData, + ) + .await; +} diff --git a/programs/address-lookup-table/Cargo.toml b/programs/address-lookup-table/Cargo.toml new file mode 100644 index 000000000..8062cb31b --- /dev/null +++ b/programs/address-lookup-table/Cargo.toml @@ -0,0 +1,33 @@ +[package] +name = "solana-address-lookup-table-program" +version = "1.10.0" +description = "Solana address lookup table program" +authors = ["Solana Maintainers "] +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.10.0" } +solana-frozen-abi-macro = { path = "../../frozen-abi/macro", version = "=1.10.0" } +solana-program-runtime = { path = "../../program-runtime", version = "=1.10.0" } +solana-sdk = { path = "../../sdk", version = "=1.10.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"] diff --git a/programs/address-lookup-table/build.rs b/programs/address-lookup-table/build.rs new file mode 120000 index 000000000..84539edda --- /dev/null +++ b/programs/address-lookup-table/build.rs @@ -0,0 +1 @@ +../../frozen-abi/build.rs \ No newline at end of file diff --git a/programs/address-lookup-table/src/instruction.rs b/programs/address-lookup-table/src/instruction.rs new file mode 100644 index 000000000..6ba3dfe80 --- /dev/null +++ b/programs/address-lookup-table/src/instruction.rs @@ -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 }, + + /// 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, +) -> 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), + ], + ) +} diff --git a/programs/address-lookup-table/src/lib.rs b/programs/address-lookup-table/src/lib.rs new file mode 100644 index 000000000..11433e64c --- /dev/null +++ b/programs/address-lookup-table/src/lib.rs @@ -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"); diff --git a/programs/address-lookup-table/src/processor.rs b/programs/address-lookup-table/src/processor.rs new file mode 100644 index 000000000..ce2b9f161 --- /dev/null +++ b/programs/address-lookup-table/src/processor.rs @@ -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 { + 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, + ) -> 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(()) + } +} diff --git a/programs/address-lookup-table/src/state.rs b/programs/address-lookup-table/src/state.rs new file mode 100644 index 000000000..906487962 --- /dev/null +++ b/programs/address-lookup-table/src/state.rs @@ -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, + // 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) -> 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, 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); + } + } +} diff --git a/programs/bpf/Cargo.lock b/programs/bpf/Cargo.lock index a5cb1f0c6..319656b8a 100644 --- a/programs/bpf/Cargo.lock +++ b/programs/bpf/Cargo.lock @@ -2571,6 +2571,24 @@ dependencies = [ "zstd", ] +[[package]] +name = "solana-address-lookup-table-program" +version = "1.10.0" +dependencies = [ + "bincode", + "bytemuck", + "log", + "num-derive", + "num-traits", + "rustc_version 0.4.0", + "serde", + "solana-frozen-abi 1.10.0", + "solana-frozen-abi-macro 1.10.0", + "solana-program-runtime", + "solana-sdk", + "thiserror", +] + [[package]] name = "solana-banks-client" version = "1.10.0" @@ -3415,6 +3433,7 @@ dependencies = [ "rustc_version 0.4.0", "serde", "serde_derive", + "solana-address-lookup-table-program", "solana-bucket-map", "solana-compute-budget-program", "solana-config-program", diff --git a/runtime/Cargo.toml b/runtime/Cargo.toml index 074adbe77..6325e6689 100644 --- a/runtime/Cargo.toml +++ b/runtime/Cargo.toml @@ -34,6 +34,7 @@ rayon = "1.5.1" regex = "1.5.4" serde = { version = "1.0.131", features = ["rc"] } serde_derive = "1.0.103" +solana-address-lookup-table-program = { path = "../programs/address-lookup-table", version = "=1.10.0" } solana-config-program = { path = "../programs/config", version = "=1.10.0" } solana-compute-budget-program = { path = "../programs/compute-budget", version = "=1.10.0" } solana-frozen-abi = { path = "../frozen-abi", version = "=1.10.0" } diff --git a/runtime/src/bank.rs b/runtime/src/bank.rs index 7fc596e6d..0cfd6e1ed 100644 --- a/runtime/src/bank.rs +++ b/runtime/src/bank.rs @@ -121,7 +121,7 @@ use { slot_hashes::SlotHashes, slot_history::SlotHistory, system_transaction, - sysvar::{self}, + sysvar::{self, Sysvar, SysvarId}, timing::years_as_slots, transaction::{ Result, SanitizedTransaction, Transaction, TransactionError, @@ -1898,6 +1898,18 @@ impl Bank { }); } + pub fn set_sysvar_for_tests(&self, sysvar: &T) + where + T: Sysvar + SysvarId, + { + self.update_sysvar_account(&T::id(), |account| { + create_account( + sysvar, + self.inherit_specially_retained_account_fields(account), + ) + }); + } + fn update_slot_history(&self) { self.update_sysvar_account(&sysvar::slot_history::id(), |account| { let mut slot_history = account diff --git a/runtime/src/builtins.rs b/runtime/src/builtins.rs index 3299820cf..87eb17660 100644 --- a/runtime/src/builtins.rs +++ b/runtime/src/builtins.rs @@ -172,6 +172,15 @@ fn feature_builtins() -> Vec<(Builtin, Pubkey, ActivationType)> { feature_set::prevent_calling_precompiles_as_programs::id(), ActivationType::RemoveProgram, ), + ( + Builtin::new( + "address_lookup_table_program", + solana_address_lookup_table_program::id(), + solana_address_lookup_table_program::processor::process_instruction, + ), + feature_set::versioned_tx_message_enabled::id(), + ActivationType::NewProgram, + ), ] } diff --git a/sdk/program/src/slot_hashes.rs b/sdk/program/src/slot_hashes.rs index 1b1e65d8e..ae9efd7c5 100644 --- a/sdk/program/src/slot_hashes.rs +++ b/sdk/program/src/slot_hashes.rs @@ -25,6 +25,9 @@ impl SlotHashes { } (self.0).truncate(MAX_ENTRIES); } + pub fn position(&self, slot: &Slot) -> Option { + self.binary_search_by(|(probe, _)| slot.cmp(probe)).ok() + } #[allow(clippy::trivially_copy_pass_by_ref)] pub fn get(&self, slot: &Slot) -> Option<&Hash> { self.binary_search_by(|(probe, _)| slot.cmp(probe)) diff --git a/sdk/src/account.rs b/sdk/src/account.rs index ca19f91a8..2e8e2fc34 100644 --- a/sdk/src/account.rs +++ b/sdk/src/account.rs @@ -103,6 +103,7 @@ pub trait WritableAccount: ReadableAccount { ); Ok(()) } + fn data_mut(&mut self) -> &mut Vec; fn data_as_mut_slice(&mut self) -> &mut [u8]; fn set_owner(&mut self, owner: Pubkey); fn copy_into_owner_from_slice(&mut self, source: &[u8]); @@ -156,6 +157,9 @@ impl WritableAccount for Account { fn set_lamports(&mut self, lamports: u64) { self.lamports = lamports; } + fn data_mut(&mut self) -> &mut Vec { + &mut self.data + } fn data_as_mut_slice(&mut self) -> &mut [u8] { &mut self.data } @@ -192,9 +196,11 @@ impl WritableAccount for AccountSharedData { fn set_lamports(&mut self, lamports: u64) { self.lamports = lamports; } + fn data_mut(&mut self) -> &mut Vec { + Arc::make_mut(&mut self.data) + } fn data_as_mut_slice(&mut self) -> &mut [u8] { - let data = Arc::make_mut(&mut self.data); - &mut data[..] + &mut self.data_mut()[..] } fn set_owner(&mut self, owner: Pubkey) { self.owner = owner;