Add address lookup table program (backport #21616) (#21789)

* 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:
mergify[bot]
2021-12-11 05:26:46 +00:00
committed by GitHub
parent 7782d34bbf
commit 5bf4445ae6
21 changed files with 1665 additions and 4 deletions

View File

@ -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.9.0"
authors = ["Solana Maintainers <maintainers@solana.foundation>"]
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.9.0" }
solana-program-test = { path = "../../program-test", version = "=1.9.0" }
solana-sdk = { path = "../../sdk", version = "=1.9.0" }
[package.metadata.docs.rs]
targets = ["x86_64-unknown-linux-gnu"]

View File

@ -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;
}

View File

@ -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<Pubkey>,
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);
}

View File

@ -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;
}

View File

@ -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<ExpectedTableAccount, InstructionError>,
}
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<Pubkey> = 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<Pubkey> = 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;
}
}

View File

@ -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;
}