diff --git a/programs/address-lookup-table/src/error.rs b/programs/address-lookup-table/src/error.rs index d0a063ec44c7a6..7accaf6b5ccecb 100644 --- a/programs/address-lookup-table/src/error.rs +++ b/programs/address-lookup-table/src/error.rs @@ -1,5 +1,5 @@ #[cfg(not(target_arch = "bpf"))] -use solana_sdk::transaction::TransactionError; +use solana_program::message::AddressLoaderError; use thiserror::Error; #[derive(Debug, Error, PartialEq, Eq, Clone)] @@ -22,13 +22,13 @@ pub enum AddressLookupError { } #[cfg(not(target_arch = "bpf"))] -impl From for TransactionError { +impl From for AddressLoaderError { fn from(err: AddressLookupError) -> Self { match err { - AddressLookupError::LookupTableAccountNotFound => Self::AddressLookupTableNotFound, - AddressLookupError::InvalidAccountOwner => Self::InvalidAddressLookupTableOwner, - AddressLookupError::InvalidAccountData => Self::InvalidAddressLookupTableData, - AddressLookupError::InvalidLookupIndex => Self::InvalidAddressLookupTableIndex, + AddressLookupError::LookupTableAccountNotFound => Self::LookupTableAccountNotFound, + AddressLookupError::InvalidAccountOwner => Self::InvalidAccountOwner, + AddressLookupError::InvalidAccountData => Self::InvalidAccountData, + AddressLookupError::InvalidLookupIndex => Self::InvalidLookupIndex, } } } diff --git a/rpc/src/rpc.rs b/rpc/src/rpc.rs index 6c53b47b990a18..312737670b795b 100644 --- a/rpc/src/rpc.rs +++ b/rpc/src/rpc.rs @@ -64,7 +64,7 @@ use { feature_set::{self, nonce_must_be_writable}, fee_calculator::FeeCalculator, hash::Hash, - message::{Message, SanitizedMessage}, + message::SanitizedMessage, pubkey::{Pubkey, PUBKEY_BYTES}, signature::{Keypair, Signature, Signer}, stake::state::{StakeActivationStatus, StakeState}, @@ -2157,16 +2157,6 @@ impl JsonRpcRequestProcessor { let is_valid = bank.is_blockhash_valid(blockhash); Ok(new_response(&bank, is_valid)) } - - fn get_fee_for_message( - &self, - message: &SanitizedMessage, - config: RpcContextConfig, - ) -> Result>> { - let bank = self.get_bank_with_config(config)?; - let fee = bank.get_fee_for_message(message); - Ok(new_response(&bank, fee)) - } } fn optimize_filters(filters: &mut [RpcFilterType]) { @@ -3255,7 +3245,10 @@ pub mod rpc_accounts { // Full RPC interface that an API node is expected to provide // (rpc_minimal should also be provided by an API node) pub mod rpc_full { - use super::*; + use { + super::*, + solana_sdk::message::{SanitizedVersionedMessage, VersionedMessage}, + }; #[rpc] pub trait Full { type Metadata; @@ -3956,12 +3949,21 @@ pub mod rpc_full { config: Option, ) -> Result>> { debug!("get_fee_for_message rpc request received"); - let (_, message) = - decode_and_deserialize::(data, TransactionBinaryEncoding::Base64)?; - let sanitized_message = SanitizedMessage::try_from(message).map_err(|err| { - Error::invalid_params(format!("invalid transaction message: {}", err)) - })?; - meta.get_fee_for_message(&sanitized_message, config.unwrap_or_default()) + let (_, message) = decode_and_deserialize::( + data, + TransactionBinaryEncoding::Base64, + )?; + let bank = &*meta.get_bank_with_config(config.unwrap_or_default())?; + let sanitized_versioned_message = SanitizedVersionedMessage::try_from(message) + .map_err(|err| { + Error::invalid_params(format!("invalid transaction message: {}", err)) + })?; + let sanitized_message = SanitizedMessage::try_new(sanitized_versioned_message, bank) + .map_err(|err| { + Error::invalid_params(format!("invalid transaction message: {}", err)) + })?; + let fee = bank.get_fee_for_message(&sanitized_message); + Ok(new_response(bank, fee)) } } } @@ -4565,10 +4567,13 @@ pub mod tests { solana_sdk::{ account::{Account, WritableAccount}, clock::MAX_RECENT_BLOCKHASHES, - fee_calculator::DEFAULT_BURN_PERCENT, + fee_calculator::{FeeRateGovernor, DEFAULT_BURN_PERCENT}, hash::{hash, Hash}, instruction::InstructionError, - message::{v0, v0::MessageAddressTableLookup, MessageHeader, VersionedMessage}, + message::{ + v0::{self, MessageAddressTableLookup}, + Message, MessageHeader, VersionedMessage, + }, nonce::{self, state::DurableNonce}, rpc_port, signature::{Keypair, Signer}, @@ -4599,7 +4604,8 @@ pub mod tests { std::{borrow::Cow, collections::HashMap}, }; - const TEST_MINT_LAMPORTS: u64 = 1_000_000; + const TEST_MINT_LAMPORTS: u64 = 1_000_000_000; + const TEST_SIGNATURE_FEE: u64 = 5_000; const TEST_SLOTS_PER_EPOCH: u64 = DELINQUENT_VALIDATOR_SLOT_DISTANCE + 1; fn create_test_request(method: &str, params: Option) -> serde_json::Value { @@ -4744,8 +4750,12 @@ pub mod tests { let keypair3 = Keypair::new(); let bank = self.working_bank(); let rent_exempt_amount = bank.get_minimum_balance_for_rent_exemption(0); - bank.transfer(rent_exempt_amount, mint_keypair, &keypair2.pubkey()) - .unwrap(); + bank.transfer( + rent_exempt_amount + TEST_SIGNATURE_FEE, + mint_keypair, + &keypair2.pubkey(), + ) + .unwrap(); let (entries, signatures) = create_test_transaction_entries( vec![&self.mint_keypair, &keypair1, &keypair2, &keypair3], @@ -5373,7 +5383,7 @@ pub mod tests { "context": {"slot": 0, "apiVersion": RpcApiVersion::default()}, "value":{ "owner": "11111111111111111111111111111111", - "lamports": 1_000_000, + "lamports": TEST_MINT_LAMPORTS, "data": "", "executable": false, "rentEpoch": 0 @@ -5450,7 +5460,7 @@ pub mod tests { let expected = json!([ { "owner": "11111111111111111111111111111111", - "lamports": 1_000_000, + "lamports": TEST_MINT_LAMPORTS, "data": ["", "base64"], "executable": false, "rentEpoch": 0 @@ -5482,7 +5492,7 @@ pub mod tests { let expected = json!([ { "owner": "11111111111111111111111111111111", - "lamports": 1_000_000, + "lamports": TEST_MINT_LAMPORTS, "data": ["", "base58"], "executable": false, "rentEpoch": 0 @@ -6063,7 +6073,7 @@ pub mod tests { "value":{ "blockhash": recent_blockhash.to_string(), "feeCalculator": { - "lamportsPerSignature": 0, + "lamportsPerSignature": TEST_SIGNATURE_FEE, } }, }, @@ -6092,7 +6102,7 @@ pub mod tests { "value": { "blockhash": recent_blockhash.to_string(), "feeCalculator": { - "lamportsPerSignature": 0, + "lamportsPerSignature": TEST_SIGNATURE_FEE, }, "lastValidSlot": MAX_RECENT_BLOCKHASHES, "lastValidBlockHeight": MAX_RECENT_BLOCKHASHES, @@ -6172,9 +6182,9 @@ pub mod tests { "value":{ "feeRateGovernor": { "burnPercent": DEFAULT_BURN_PERCENT, - "maxLamportsPerSignature": 0, - "minLamportsPerSignature": 0, - "targetLamportsPerSignature": 0, + "maxLamportsPerSignature": TEST_SIGNATURE_FEE, + "minLamportsPerSignature": TEST_SIGNATURE_FEE, + "targetLamportsPerSignature": TEST_SIGNATURE_FEE, "targetSignaturesPerSlot": 0 } }, @@ -6440,6 +6450,7 @@ pub mod tests { genesis_config.rent.exemption_threshold = 2.0; genesis_config.epoch_schedule = EpochSchedule::custom(TEST_SLOTS_PER_EPOCH, TEST_SLOTS_PER_EPOCH, false); + genesis_config.fee_rate_governor = FeeRateGovernor::new(TEST_SIGNATURE_FEE, 0); let bank = Bank::new_for_tests(&genesis_config); ( @@ -8430,8 +8441,56 @@ pub mod tests { assert_eq!( sanitize_transaction(versioned_tx, SimpleAddressLoader::Disabled).unwrap_err(), Error::invalid_params( - "invalid transaction: Transaction loads an address table account that doesn't exist".to_string(), + "invalid transaction: Transaction version is unsupported".to_string(), ) ); } + + #[test] + fn test_get_fee_for_message() { + let rpc = RpcHandler::start(); + let bank = rpc.working_bank(); + // Slot hashes is necessary for processing versioned txs. + bank.set_sysvar_for_tests(&SlotHashes::default()); + // Correct blockhash is needed because fees are specific to blockhashes + let recent_blockhash = bank.last_blockhash(); + + { + let legacy_msg = VersionedMessage::Legacy(Message { + header: MessageHeader { + num_required_signatures: 1, + ..MessageHeader::default() + }, + recent_blockhash, + account_keys: vec![Pubkey::new_unique()], + ..Message::default() + }); + + let request = create_test_request( + "getFeeForMessage", + Some(json!([base64::encode(&serialize(&legacy_msg).unwrap())])), + ); + let response: RpcResponse = parse_success_result(rpc.handle_request_sync(request)); + assert_eq!(response.value, TEST_SIGNATURE_FEE); + } + + { + let v0_msg = VersionedMessage::V0(v0::Message { + header: MessageHeader { + num_required_signatures: 1, + ..MessageHeader::default() + }, + recent_blockhash, + account_keys: vec![Pubkey::new_unique()], + ..v0::Message::default() + }); + + let request = create_test_request( + "getFeeForMessage", + Some(json!([base64::encode(&serialize(&v0_msg).unwrap())])), + ); + let response: RpcResponse = parse_success_result(rpc.handle_request_sync(request)); + assert_eq!(response.value, TEST_SIGNATURE_FEE); + } + } } diff --git a/runtime/src/bank/address_lookup_table.rs b/runtime/src/bank/address_lookup_table.rs index 0a4bc732f45ba6..7da3b125491a19 100644 --- a/runtime/src/bank/address_lookup_table.rs +++ b/runtime/src/bank/address_lookup_table.rs @@ -4,8 +4,11 @@ use { solana_address_lookup_table_program::error::AddressLookupError, solana_sdk::{ feature_set::return_none_for_zero_lamport_accounts, - message::v0::{LoadedAddresses, MessageAddressTableLookup}, - transaction::{AddressLoader, Result as TransactionResult, TransactionError}, + message::{ + v0::{LoadedAddresses, MessageAddressTableLookup}, + AddressLoaderError, + }, + transaction::AddressLoader, }, }; @@ -13,9 +16,9 @@ impl AddressLoader for &Bank { fn load_addresses( self, address_table_lookups: &[MessageAddressTableLookup], - ) -> TransactionResult { + ) -> Result { if !self.versioned_tx_message_enabled() { - return Err(TransactionError::UnsupportedVersion); + return Err(AddressLoaderError::Disabled); } let load_zero_lamports = if self @@ -32,7 +35,7 @@ impl AddressLoader for &Bank { .read() .unwrap() .get_slot_hashes() - .map_err(|_| TransactionError::AccountNotFound)?; + .map_err(|_| AddressLoaderError::SlotHashesSysvarNotFound)?; Ok(address_table_lookups .iter() diff --git a/sdk/program/src/message/address_loader.rs b/sdk/program/src/message/address_loader.rs new file mode 100644 index 00000000000000..83e514cd0bd63b --- /dev/null +++ b/sdk/program/src/message/address_loader.rs @@ -0,0 +1,56 @@ +use { + super::v0::{LoadedAddresses, MessageAddressTableLookup}, + thiserror::Error, +}; + +#[derive(Debug, Error, PartialEq, Eq, Clone)] +pub enum AddressLoaderError { + /// Address loading from lookup tables is disabled + #[error("Address loading from lookup tables is disabled")] + Disabled, + + /// Failed to load slot hashes sysvar + #[error("Failed to load slot hashes sysvar")] + SlotHashesSysvarNotFound, + + /// 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, +} + +pub trait AddressLoader: Clone { + fn load_addresses( + self, + lookups: &[MessageAddressTableLookup], + ) -> Result; +} + +#[derive(Clone)] +pub enum SimpleAddressLoader { + Disabled, + Enabled(LoadedAddresses), +} + +impl AddressLoader for SimpleAddressLoader { + fn load_addresses( + self, + _lookups: &[MessageAddressTableLookup], + ) -> Result { + match self { + Self::Disabled => Err(AddressLoaderError::Disabled), + Self::Enabled(loaded_addresses) => Ok(loaded_addresses), + } + } +} diff --git a/sdk/program/src/message/mod.rs b/sdk/program/src/message/mod.rs index be1a0db4fef44f..7f343ff067dd34 100644 --- a/sdk/program/src/message/mod.rs +++ b/sdk/program/src/message/mod.rs @@ -44,10 +44,11 @@ pub mod legacy; #[path = ""] mod non_bpf_modules { mod account_keys; + mod address_loader; mod sanitized; mod versions; - pub use {account_keys::*, sanitized::*, versions::*}; + pub use {account_keys::*, address_loader::*, sanitized::*, versions::*}; } use compiled_keys::*; diff --git a/sdk/program/src/message/sanitized.rs b/sdk/program/src/message/sanitized.rs index 4e938eb93f1e61..4335efa237acb2 100644 --- a/sdk/program/src/message/sanitized.rs +++ b/sdk/program/src/message/sanitized.rs @@ -5,7 +5,8 @@ use { message::{ legacy::Message as LegacyMessage, v0::{self, LoadedAddresses}, - AccountKeys, MessageHeader, + AccountKeys, AddressLoader, AddressLoaderError, MessageHeader, + SanitizedVersionedMessage, VersionedMessage, }, nonce::NONCED_TX_MARKER_IX_INDEX, program_utils::limited_deserialize, @@ -35,6 +36,8 @@ pub enum SanitizeMessageError { ValueOutOfBounds, #[error("invalid value")] InvalidValue, + #[error("{0}")] + AddressLoaderError(#[from] AddressLoaderError), } impl From for SanitizeMessageError { @@ -56,6 +59,23 @@ impl TryFrom for SanitizedMessage { } impl SanitizedMessage { + /// Create a sanitized message from a sanitized versioned message. + /// If the input message uses address tables, attempt to look up the + /// address for each table index. + pub fn try_new( + sanitized_msg: SanitizedVersionedMessage, + address_loader: impl AddressLoader, + ) -> Result { + Ok(match sanitized_msg.message { + VersionedMessage::Legacy(message) => SanitizedMessage::Legacy(message), + VersionedMessage::V0(message) => { + let loaded_addresses = + address_loader.load_addresses(&message.address_table_lookups)?; + SanitizedMessage::V0(v0::LoadedMessage::new(message, loaded_addresses)) + } + }) + } + /// Return true if this message contains duplicate account keys pub fn has_duplicates(&self) -> bool { match self { diff --git a/sdk/src/transaction/error.rs b/sdk/src/transaction/error.rs index 2fe4e0e3756adf..3f25fa5a62541a 100644 --- a/sdk/src/transaction/error.rs +++ b/sdk/src/transaction/error.rs @@ -1,6 +1,8 @@ use { crate::{ - instruction::InstructionError, message::SanitizeMessageError, sanitize::SanitizeError, + instruction::InstructionError, + message::{AddressLoaderError, SanitizeMessageError}, + sanitize::SanitizeError, }, serde::Serialize, thiserror::Error, @@ -156,7 +158,23 @@ impl From for TransactionError { } impl From for TransactionError { - fn from(_err: SanitizeMessageError) -> Self { - Self::SanitizeFailure + fn from(err: SanitizeMessageError) -> Self { + match err { + SanitizeMessageError::AddressLoaderError(err) => Self::from(err), + _ => Self::SanitizeFailure, + } + } +} + +impl From for TransactionError { + fn from(err: AddressLoaderError) -> Self { + match err { + AddressLoaderError::Disabled => Self::UnsupportedVersion, + AddressLoaderError::SlotHashesSysvarNotFound => Self::AccountNotFound, + AddressLoaderError::LookupTableAccountNotFound => Self::AddressLookupTableNotFound, + AddressLoaderError::InvalidAccountOwner => Self::InvalidAddressLookupTableOwner, + AddressLoaderError::InvalidAccountData => Self::InvalidAddressLookupTableData, + AddressLoaderError::InvalidLookupIndex => Self::InvalidAddressLookupTableIndex, + } } } diff --git a/sdk/src/transaction/sanitized.rs b/sdk/src/transaction/sanitized.rs index 28136fdc79498a..8ba05eae37a5f1 100644 --- a/sdk/src/transaction/sanitized.rs +++ b/sdk/src/transaction/sanitized.rs @@ -1,11 +1,12 @@ #![cfg(feature = "full")] +pub use crate::message::{AddressLoader, SimpleAddressLoader}; use { super::SanitizedVersionedTransaction, crate::{ hash::Hash, message::{ - v0::{self, LoadedAddresses, MessageAddressTableLookup}, + v0::{self, LoadedAddresses}, SanitizedMessage, VersionedMessage, }, precompiles::verify_if_precompile, @@ -42,25 +43,6 @@ pub struct TransactionAccountLocks<'a> { pub writable: Vec<&'a Pubkey>, } -pub trait AddressLoader: Clone { - fn load_addresses(self, lookups: &[MessageAddressTableLookup]) -> Result; -} - -#[derive(Clone)] -pub enum SimpleAddressLoader { - Disabled, - Enabled(LoadedAddresses), -} - -impl AddressLoader for SimpleAddressLoader { - fn load_addresses(self, _lookups: &[MessageAddressTableLookup]) -> Result { - match self { - Self::Disabled => Err(TransactionError::AddressLookupTableNotFound), - Self::Enabled(loaded_addresses) => Ok(loaded_addresses), - } - } -} - /// Type that represents whether the transaction message has been precomputed or /// not. pub enum MessageHash {