diff --git a/accountsdb-plugin-postgres/scripts/create_schema.sql b/accountsdb-plugin-postgres/scripts/create_schema.sql index 994a2176cfb178..4863e54b7e1274 100644 --- a/accountsdb-plugin-postgres/scripts/create_schema.sql +++ b/accountsdb-plugin-postgres/scripts/create_schema.sql @@ -48,7 +48,11 @@ Create TYPE "TransactionErrorCode" AS ENUM ( 'WouldExceedMaxBlockCostLimit', 'UnsupportedVersion', 'InvalidWritableAccount', - 'WouldExceedMaxAccountDataCostLimit' + 'WouldExceedMaxAccountDataCostLimit', + 'AddressLookupTableNotFound', + 'InvalidAddressLookupTableOwner', + 'InvalidAddressLookupTableData', + 'InvalidAddressLookupTableIndex', ); CREATE TYPE "TransactionError" AS ( diff --git a/accountsdb-plugin-postgres/src/postgres_client/postgres_client_transaction.rs b/accountsdb-plugin-postgres/src/postgres_client/postgres_client_transaction.rs index b2019f70f50211..76fe128c69f378 100644 --- a/accountsdb-plugin-postgres/src/postgres_client/postgres_client_transaction.rs +++ b/accountsdb-plugin-postgres/src/postgres_client/postgres_client_transaction.rs @@ -331,6 +331,10 @@ pub enum DbTransactionErrorCode { UnsupportedVersion, InvalidWritableAccount, WouldExceedMaxAccountDataCostLimit, + AddressLookupTableNotFound, + InvalidAddressLookupTableOwner, + InvalidAddressLookupTableData, + InvalidAddressLookupTableIndex, } impl From<&TransactionError> for DbTransactionErrorCode { @@ -362,6 +366,14 @@ impl From<&TransactionError> for DbTransactionErrorCode { TransactionError::WouldExceedMaxAccountDataCostLimit => { Self::WouldExceedMaxAccountDataCostLimit } + TransactionError::AddressLookupTableNotFound => Self::AddressLookupTableNotFound, + TransactionError::InvalidAddressLookupTableOwner => { + Self::InvalidAddressLookupTableOwner + } + TransactionError::InvalidAddressLookupTableData => Self::InvalidAddressLookupTableData, + TransactionError::InvalidAddressLookupTableIndex => { + Self::InvalidAddressLookupTableIndex + } } } } diff --git a/programs/address-lookup-table/src/state.rs b/programs/address-lookup-table/src/state.rs index f2ee64f0139260..cdfe84c9c53144 100644 --- a/programs/address-lookup-table/src/state.rs +++ b/programs/address-lookup-table/src/state.rs @@ -6,6 +6,7 @@ use { instruction::InstructionError, pubkey::Pubkey, slot_hashes::{SlotHashes, MAX_ENTRIES}, + transaction::AddressLookupError, }, std::borrow::Cow, }; @@ -133,6 +134,43 @@ impl<'a> AddressLookupTable<'a> { Ok(()) } + /// Lookup addresses for provided table indexes. Since lookups are performed on + /// tables which are not read-locked, this implementation needs to be careful + /// about resolving addresses consistently. + pub fn lookup( + &self, + current_slot: Slot, + indexes: &[u8], + slot_hashes: &SlotHashes, + ) -> Result, AddressLookupError> { + if !self.meta.is_active(current_slot, slot_hashes) { + // Once a lookup table is no longer active, it can be closed + // at any point, so returning a specific error for deactivated + // lookup tables could result in a race condition. + return Err(AddressLookupError::LookupTableAccountNotFound); + } + + // If the address table was extended in the same slot in which it is used + // to lookup addresses for another transaction, the recently extended + // addresses are not considered active and won't be accessible. + let active_addresses_len = if current_slot > self.meta.last_extended_slot { + self.addresses.len() + } else { + self.meta.last_extended_slot_start_index as usize + }; + + let active_addresses = &self.addresses[0..active_addresses_len]; + indexes + .into_iter() + .map(|idx| { + active_addresses + .get(*idx as usize) + .cloned() + .ok_or(AddressLookupError::InvalidLookupIndex) + }) + .collect::>() + } + /// 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); @@ -322,4 +360,117 @@ mod tests { test_case(case); } } + + #[test] + fn test_lookup_from_empty_table() { + let lookup_table = AddressLookupTable { + meta: LookupTableMeta::default(), + addresses: Cow::Owned(vec![]), + }; + + assert_eq!( + lookup_table.lookup(0, &[], &SlotHashes::default()), + Ok(vec![]) + ); + assert_eq!( + lookup_table.lookup(0, &[0], &SlotHashes::default()), + Err(AddressLookupError::InvalidLookupIndex) + ); + } + + #[test] + fn test_lookup_from_deactivating_table() { + let current_slot = 1; + let slot_hashes = SlotHashes::default(); + let addresses = vec![Pubkey::new_unique()]; + let lookup_table = AddressLookupTable { + meta: LookupTableMeta { + deactivation_slot: current_slot, + last_extended_slot: current_slot - 1, + ..LookupTableMeta::default() + }, + addresses: Cow::Owned(addresses.clone()), + }; + + assert_eq!( + lookup_table.meta.status(current_slot, &slot_hashes), + LookupTableStatus::Deactivating { + remaining_blocks: MAX_ENTRIES + 1 + } + ); + + assert_eq!( + lookup_table.lookup(current_slot, &[0], &slot_hashes), + Ok(vec![addresses[0]]), + ); + } + + #[test] + fn test_lookup_from_deactivated_table() { + let current_slot = 1; + let slot_hashes = SlotHashes::default(); + let lookup_table = AddressLookupTable { + meta: LookupTableMeta { + deactivation_slot: current_slot - 1, + last_extended_slot: current_slot - 1, + ..LookupTableMeta::default() + }, + addresses: Cow::Owned(vec![]), + }; + + assert_eq!( + lookup_table.meta.status(current_slot, &slot_hashes), + LookupTableStatus::Deactivated + ); + assert_eq!( + lookup_table.lookup(current_slot, &[0], &slot_hashes), + Err(AddressLookupError::LookupTableAccountNotFound) + ); + } + + #[test] + fn test_lookup_from_table_extended_in_current_slot() { + let current_slot = 0; + let addresses: Vec<_> = (0..2).map(|_| Pubkey::new_unique()).collect(); + let lookup_table = AddressLookupTable { + meta: LookupTableMeta { + last_extended_slot: current_slot, + last_extended_slot_start_index: 1, + ..LookupTableMeta::default() + }, + addresses: Cow::Owned(addresses.clone()), + }; + + assert_eq!( + lookup_table.lookup(current_slot, &[0], &SlotHashes::default()), + Ok(vec![addresses[0]]) + ); + assert_eq!( + lookup_table.lookup(current_slot, &[1], &SlotHashes::default()), + Err(AddressLookupError::InvalidLookupIndex), + ); + } + + #[test] + fn test_lookup_from_table_extended_in_previous_slot() { + let current_slot = 1; + let addresses: Vec<_> = (0..10).map(|_| Pubkey::new_unique()).collect(); + let lookup_table = AddressLookupTable { + meta: LookupTableMeta { + last_extended_slot: current_slot - 1, + last_extended_slot_start_index: 1, + ..LookupTableMeta::default() + }, + addresses: Cow::Owned(addresses.clone()), + }; + + assert_eq!( + lookup_table.lookup(current_slot, &[0, 3, 1, 5], &SlotHashes::default()), + Ok(vec![addresses[0], addresses[3], addresses[1], addresses[5]]) + ); + assert_eq!( + lookup_table.lookup(current_slot, &[10], &SlotHashes::default()), + Err(AddressLookupError::InvalidLookupIndex), + ); + } } diff --git a/runtime/src/accounts.rs b/runtime/src/accounts.rs index 9b562b36b82d80..560e8d390b7f1f 100644 --- a/runtime/src/accounts.rs +++ b/runtime/src/accounts.rs @@ -22,6 +22,7 @@ use { }, log::*, rand::{thread_rng, Rng}, + solana_address_lookup_table_program::state::AddressLookupTable, solana_sdk::{ account::{Account, AccountSharedData, ReadableAccount, WritableAccount}, account_utils::StateMut, @@ -30,13 +31,17 @@ use { feature_set::{self, FeatureSet}, genesis_config::ClusterType, hash::Hash, - message::SanitizedMessage, + message::{ + v0::{LoadedAddresses, MessageAddressTableLookup}, + SanitizedMessage, + }, native_loader, nonce::{state::Versions as NonceVersions, State as NonceState}, pubkey::Pubkey, + slot_hashes::SlotHashes, system_program, sysvar::{self, instructions::construct_instructions_data}, - transaction::{Result, SanitizedTransaction, TransactionError}, + transaction::{AddressLookupError, Result, SanitizedTransaction, TransactionError}, transaction_context::TransactionAccount, }, std::{ @@ -514,6 +519,40 @@ impl Accounts { .collect() } + pub fn load_lookup_table_addresses( + &self, + ancestors: &Ancestors, + address_table_lookup: &MessageAddressTableLookup, + slot_hashes: &SlotHashes, + ) -> std::result::Result { + let table_account = self + .accounts_db + .load_with_fixed_root(ancestors, &address_table_lookup.account_key) + .map(|(account, _rent)| account) + .ok_or(AddressLookupError::LookupTableAccountNotFound)?; + + if table_account.owner() == &solana_address_lookup_table_program::id() { + let current_slot = ancestors.max_slot(); + let lookup_table = AddressLookupTable::deserialize(table_account.data()) + .map_err(|_ix_err| AddressLookupError::InvalidAccountData)?; + + Ok(LoadedAddresses { + writable: lookup_table.lookup( + current_slot, + &address_table_lookup.writable_indexes, + slot_hashes, + )?, + readonly: lookup_table.lookup( + current_slot, + &address_table_lookup.readonly_indexes, + slot_hashes, + )?, + }) + } else { + Err(AddressLookupError::InvalidAccountOwner) + } + } + fn filter_zero_lamport_account( account: AccountSharedData, slot: Slot, @@ -1243,6 +1282,7 @@ mod tests { use { super::*, crate::rent_collector::RentCollector, + solana_address_lookup_table_program::state::LookupTableMeta, solana_sdk::{ account::{AccountSharedData, WritableAccount}, epoch_schedule::EpochSchedule, @@ -1257,6 +1297,7 @@ mod tests { transaction::Transaction, }, std::{ + borrow::Cow, convert::TryFrom, sync::atomic::{AtomicBool, AtomicU64, Ordering}, thread, time, @@ -1829,6 +1870,149 @@ mod tests { } } + #[test] + fn test_load_lookup_table_addresses_account_not_found() { + let ancestors = vec![(0, 0)].into_iter().collect(); + let accounts = Accounts::new_with_config_for_tests( + Vec::new(), + &ClusterType::Development, + AccountSecondaryIndexes::default(), + false, + AccountShrinkThreshold::default(), + ); + + let invalid_table_key = Pubkey::new_unique(); + let address_table_lookup = MessageAddressTableLookup { + account_key: invalid_table_key, + writable_indexes: vec![], + readonly_indexes: vec![], + }; + + assert_eq!( + accounts.load_lookup_table_addresses( + &ancestors, + &address_table_lookup, + &SlotHashes::default(), + ), + Err(AddressLookupError::LookupTableAccountNotFound), + ); + } + + #[test] + fn test_load_lookup_table_addresses_invalid_account_owner() { + let ancestors = vec![(0, 0)].into_iter().collect(); + let accounts = Accounts::new_with_config_for_tests( + Vec::new(), + &ClusterType::Development, + AccountSecondaryIndexes::default(), + false, + AccountShrinkThreshold::default(), + ); + + let invalid_table_key = Pubkey::new_unique(); + let invalid_table_account = AccountSharedData::default(); + accounts.store_slow_uncached(0, &invalid_table_key, &invalid_table_account); + + let address_table_lookup = MessageAddressTableLookup { + account_key: invalid_table_key, + writable_indexes: vec![], + readonly_indexes: vec![], + }; + + assert_eq!( + accounts.load_lookup_table_addresses( + &ancestors, + &address_table_lookup, + &SlotHashes::default(), + ), + Err(AddressLookupError::InvalidAccountOwner), + ); + } + + #[test] + fn test_load_lookup_table_addresses_invalid_account_data() { + let ancestors = vec![(0, 0)].into_iter().collect(); + let accounts = Accounts::new_with_config_for_tests( + Vec::new(), + &ClusterType::Development, + AccountSecondaryIndexes::default(), + false, + AccountShrinkThreshold::default(), + ); + + let invalid_table_key = Pubkey::new_unique(); + let invalid_table_account = + AccountSharedData::new(1, 0, &solana_address_lookup_table_program::id()); + accounts.store_slow_uncached(0, &invalid_table_key, &invalid_table_account); + + let address_table_lookup = MessageAddressTableLookup { + account_key: invalid_table_key, + writable_indexes: vec![], + readonly_indexes: vec![], + }; + + assert_eq!( + accounts.load_lookup_table_addresses( + &ancestors, + &address_table_lookup, + &SlotHashes::default(), + ), + Err(AddressLookupError::InvalidAccountData), + ); + } + + #[test] + fn test_load_lookup_table_addresses() { + let ancestors = vec![(1, 1), (0, 0)].into_iter().collect(); + let accounts = Accounts::new_with_config_for_tests( + Vec::new(), + &ClusterType::Development, + AccountSecondaryIndexes::default(), + false, + AccountShrinkThreshold::default(), + ); + + let table_key = Pubkey::new_unique(); + let table_addresses = vec![Pubkey::new_unique(), Pubkey::new_unique()]; + let table_account = { + let table_state = AddressLookupTable { + meta: LookupTableMeta::default(), + addresses: Cow::Owned(table_addresses.clone()), + }; + let table_data = { + let mut data = vec![]; + table_state.serialize_for_tests(&mut data).unwrap(); + data + }; + AccountSharedData::create( + 1, + table_data, + solana_address_lookup_table_program::id(), + false, + 0, + ) + }; + accounts.store_slow_uncached(0, &table_key, &table_account); + + let address_table_lookup = MessageAddressTableLookup { + account_key: table_key, + writable_indexes: vec![0], + readonly_indexes: vec![1], + }; + + assert_eq!( + accounts.load_lookup_table_addresses( + &ancestors, + &address_table_lookup, + &SlotHashes::default(), + ), + Ok(LoadedAddresses { + writable: vec![table_addresses[0]], + readonly: vec![table_addresses[1]], + }), + ); + } + #[test] fn test_load_by_program_slot() { let accounts = Accounts::new_with_config_for_tests( diff --git a/sdk/src/transaction/error.rs b/sdk/src/transaction/error.rs index 60ed8c39f5563d..80df2d0a930b61 100644 --- a/sdk/src/transaction/error.rs +++ b/sdk/src/transaction/error.rs @@ -105,6 +105,22 @@ pub enum TransactionError { /// Transaction would exceed max account data limit within the block #[error("Transaction would exceed max account data limit within the block")] WouldExceedMaxAccountDataCostLimit, + + /// Address lookup table not found + #[error("Transaction loads an address table account that doesn't exist")] + AddressLookupTableNotFound, + + /// Attempted to lookup addresses from an account owned by the wrong program + #[error("Transaction loads an address table account with an invalid owner")] + InvalidAddressLookupTableOwner, + + /// Attempted to lookup addresses from an invalid account + #[error("Transaction loads an address table account with invalid data")] + InvalidAddressLookupTableData, + + /// Address table lookup uses an invalid index + #[error("Transaction address table lookup uses an invalid index")] + InvalidAddressLookupTableIndex, } impl From for TransactionError { @@ -123,3 +139,33 @@ impl From for TransactionError { } } } + +#[derive(Debug, Error, PartialEq, Eq, Clone)] +pub enum AddressLookupError { + /// Attempted to lookup addresses from a table that does not exist + #[error("Attempted to lookup addresses from a table that does not exist")] + LookupTableAccountNotFound, + + /// Attempted to lookup addresses from an account owned by the wrong program + #[error("Attempted to lookup addresses from an account owned by the wrong program")] + InvalidAccountOwner, + + /// Attempted to lookup addresses from an invalid account + #[error("Attempted to lookup addresses from an invalid account")] + InvalidAccountData, + + /// Address lookup contains an invalid index + #[error("Address lookup contains an invalid index")] + InvalidLookupIndex, +} + +impl From for TransactionError { + fn from(err: AddressLookupError) -> Self { + match err { + AddressLookupError::LookupTableAccountNotFound => Self::AddressLookupTableNotFound, + AddressLookupError::InvalidAccountOwner => Self::InvalidAddressLookupTableOwner, + AddressLookupError::InvalidAccountData => Self::InvalidAddressLookupTableData, + AddressLookupError::InvalidLookupIndex => Self::InvalidAddressLookupTableIndex, + } + } +} diff --git a/storage-proto/proto/transaction_by_addr.proto b/storage-proto/proto/transaction_by_addr.proto index 582c31991f3b4a..5825251bb1be14 100644 --- a/storage-proto/proto/transaction_by_addr.proto +++ b/storage-proto/proto/transaction_by_addr.proto @@ -46,6 +46,10 @@ enum TransactionErrorType { INVALID_WRITABLE_ACCOUNT = 19; WOULD_EXCEED_MAX_ACCOUNT_COST_LIMIT = 20; WOULD_EXCEED_MAX_ACCOUNT_DATA_COST_LIMIT = 21; + ADDRESS_LOOKUP_TABLE_NOT_FOUND = 22; + INVALID_ADDRESS_LOOKUP_TABLE_OWNER = 23; + INVALID_ADDRESS_LOOKUP_TABLE_DATA = 24; + INVALID_ADDRESS_LOOKUP_TABLE_INDEX = 25; } message InstructionError { diff --git a/storage-proto/src/convert.rs b/storage-proto/src/convert.rs index 423732196010a5..718115fbfa56c2 100644 --- a/storage-proto/src/convert.rs +++ b/storage-proto/src/convert.rs @@ -569,6 +569,10 @@ impl TryFrom for TransactionError { 19 => TransactionError::InvalidWritableAccount, 20 => TransactionError::WouldExceedMaxAccountCostLimit, 21 => TransactionError::WouldExceedMaxAccountDataCostLimit, + 22 => TransactionError::AddressLookupTableNotFound, + 23 => TransactionError::InvalidAddressLookupTableOwner, + 24 => TransactionError::InvalidAddressLookupTableData, + 25 => TransactionError::InvalidAddressLookupTableIndex, _ => return Err("Invalid TransactionError"), }) } @@ -642,6 +646,18 @@ impl From for tx_by_addr::TransactionError { TransactionError::WouldExceedMaxAccountDataCostLimit => { tx_by_addr::TransactionErrorType::WouldExceedMaxAccountDataCostLimit } + TransactionError::AddressLookupTableNotFound => { + tx_by_addr::TransactionErrorType::AddressLookupTableNotFound + } + TransactionError::InvalidAddressLookupTableOwner => { + tx_by_addr::TransactionErrorType::InvalidAddressLookupTableOwner + } + TransactionError::InvalidAddressLookupTableData => { + tx_by_addr::TransactionErrorType::InvalidAddressLookupTableData + } + TransactionError::InvalidAddressLookupTableIndex => { + tx_by_addr::TransactionErrorType::InvalidAddressLookupTableIndex + } } as i32, instruction_error: match transaction_error { TransactionError::InstructionError(index, ref instruction_error) => {