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::extend_lookup_table, state::{AddressLookupTable, LookupTableMeta}, }, solana_program_test::*, solana_sdk::{ account::{ReadableAccount, WritableAccount}, clock::Clock, instruction::{Instruction, InstructionError}, pubkey::{Pubkey, PUBKEY_BYTES}, signature::{Keypair, Signer}, transaction::{Transaction, TransactionError}, }, std::{borrow::Cow, 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(), Some(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 }, deactivation_slot: lookup_table.meta.deactivation_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_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()), 0); 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, wrong_authority.pubkey(), Some(context.payer.pubkey()), new_addresses, ); assert_ix_error( &mut context, ix, Some(&wrong_authority), InstructionError::IncorrectAuthority, ) .await; } #[tokio::test] async fn test_extend_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 new_addresses = vec![Pubkey::new_unique()]; let mut ix = extend_lookup_table( lookup_table_address, authority.pubkey(), Some(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(), Some(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(), Some(context.payer.pubkey()), new_addresses, ); assert_ix_error( &mut context, ix, Some(&authority), InstructionError::Immutable, ) .await; } #[tokio::test] async fn test_extend_lookup_table_without_payer() { 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 new_addresses = vec![Pubkey::new_unique()]; let ix = extend_lookup_table( lookup_table_address, authority.pubkey(), None, new_addresses, ); assert_ix_error( &mut context, ix, Some(&authority), InstructionError::NotEnoughAccountKeys, ) .await; } #[tokio::test] async fn test_extend_prepaid_lookup_table_without_payer() { let mut context = setup_test_context().await; let authority = Keypair::new(); let lookup_table_address = Pubkey::new_unique(); let expected_state = { // initialize lookup table let empty_lookup_table = new_address_lookup_table(Some(authority.pubkey()), 0); let mut lookup_table_account = add_lookup_table_account(&mut context, lookup_table_address, empty_lookup_table).await; // calculate required rent exempt balance for adding one address let mut temp_lookup_table = new_address_lookup_table(Some(authority.pubkey()), 1); let data = temp_lookup_table.clone().serialize_for_tests().unwrap(); let rent = context.banks_client.get_rent().await.unwrap(); let rent_exempt_balance = rent.minimum_balance(data.len()); // prepay for one address lookup_table_account.set_lamports(rent_exempt_balance); context.set_account(&lookup_table_address, &lookup_table_account); // test will extend table in the current bank's slot let clock = context.banks_client.get_sysvar::().await.unwrap(); temp_lookup_table.meta.last_extended_slot = clock.slot; ExpectedTableAccount { lamports: rent_exempt_balance, data_len: data.len(), state: temp_lookup_table, } }; let new_addresses = expected_state.state.addresses.to_vec(); let instruction = extend_lookup_table( lookup_table_address, authority.pubkey(), None, new_addresses, ); run_test_case( &mut context, TestCase { lookup_table_address, instruction, extra_signer: Some(&authority), expected_result: Ok(expected_state), }, ) .await; }