From 3d9874b95a4bda9bb99cb067f168811296d208cc Mon Sep 17 00:00:00 2001 From: Jack May Date: Fri, 11 Feb 2022 16:23:16 -0800 Subject: [PATCH] Add fees to tx-wide caps (#22081) --- cli/tests/nonce.rs | 7 +- cli/tests/stake.rs | 54 +- cli/tests/transfer.rs | 50 +- .../developing/programming-model/runtime.md | 55 +- program-runtime/src/compute_budget.rs | 105 +-- program-runtime/src/invoke_context.rs | 7 +- programs/bpf/tests/programs.rs | 82 +- rpc/src/transaction_status_service.rs | 6 +- runtime/src/accounts.rs | 29 +- runtime/src/bank.rs | 778 +++++++++++++++++- runtime/src/message_processor.rs | 14 +- sdk/src/compute_budget.rs | 43 +- sdk/src/fee.rs | 67 ++ sdk/src/lib.rs | 1 + tokens/src/commands.rs | 37 +- 15 files changed, 1149 insertions(+), 186 deletions(-) create mode 100644 sdk/src/fee.rs diff --git a/cli/tests/nonce.rs b/cli/tests/nonce.rs index 3da1aae1ba..3dd2be6348 100644 --- a/cli/tests/nonce.rs +++ b/cli/tests/nonce.rs @@ -238,6 +238,7 @@ fn full_battery_tests( #[test] #[allow(clippy::redundant_closure)] fn test_create_account_with_seed() { + const ONE_SIG_FEE: f64 = 0.000005; solana_logger::setup(); let mint_keypair = Keypair::new(); let mint_pubkey = mint_keypair.pubkey(); @@ -310,7 +311,7 @@ fn test_create_account_with_seed() { &offline_nonce_authority_signer.pubkey(), ); check_balance!( - sol_to_lamports(4000.999999999), + sol_to_lamports(4001.0 - ONE_SIG_FEE), &rpc_client, &online_nonce_creator_signer.pubkey(), ); @@ -381,12 +382,12 @@ fn test_create_account_with_seed() { process_command(&submit_config).unwrap(); check_balance!(sol_to_lamports(241.0), &rpc_client, &nonce_address); check_balance!( - sol_to_lamports(31.999999999), + sol_to_lamports(32.0 - ONE_SIG_FEE), &rpc_client, &offline_nonce_authority_signer.pubkey(), ); check_balance!( - sol_to_lamports(4000.999999999), + sol_to_lamports(4001.0 - ONE_SIG_FEE), &rpc_client, &online_nonce_creator_signer.pubkey(), ); diff --git a/cli/tests/stake.rs b/cli/tests/stake.rs index 61396130cb..7161ee8452 100644 --- a/cli/tests/stake.rs +++ b/cli/tests/stake.rs @@ -18,6 +18,7 @@ use { solana_sdk::{ account_utils::StateMut, commitment_config::CommitmentConfig, + fee::FeeStructure, nonce::State as NonceState, pubkey::Pubkey, signature::{keypair_from_seed, Keypair, Signer}, @@ -876,14 +877,15 @@ fn test_stake_authorize() { #[test] fn test_stake_authorize_with_fee_payer() { solana_logger::setup(); - const SIG_FEE: u64 = 42; + let fee_one_sig = FeeStructure::default().get_max_fee(1, 0); + let fee_two_sig = FeeStructure::default().get_max_fee(2, 0); let mint_keypair = Keypair::new(); let mint_pubkey = mint_keypair.pubkey(); let faucet_addr = run_local_faucet(mint_keypair, None); let test_validator = TestValidator::with_custom_fees( mint_pubkey, - SIG_FEE, + 1, Some(faucet_addr), SocketAddrSpace::Unspecified, ); @@ -912,14 +914,14 @@ fn test_stake_authorize_with_fee_payer() { config_offline.command = CliCommand::ClusterVersion; process_command(&config_offline).unwrap_err(); - request_and_confirm_airdrop(&rpc_client, &config, &default_pubkey, 100_000).unwrap(); - check_balance!(100_000, &rpc_client, &config.signers[0].pubkey()); + request_and_confirm_airdrop(&rpc_client, &config, &default_pubkey, 5_000_000).unwrap(); + check_balance!(5_000_000, &rpc_client, &config.signers[0].pubkey()); - request_and_confirm_airdrop(&rpc_client, &config_payer, &payer_pubkey, 100_000).unwrap(); - check_balance!(100_000, &rpc_client, &payer_pubkey); + request_and_confirm_airdrop(&rpc_client, &config_payer, &payer_pubkey, 5_000_000).unwrap(); + check_balance!(5_000_000, &rpc_client, &payer_pubkey); - request_and_confirm_airdrop(&rpc_client, &config_offline, &offline_pubkey, 100_000).unwrap(); - check_balance!(100_000, &rpc_client, &offline_pubkey); + request_and_confirm_airdrop(&rpc_client, &config_offline, &offline_pubkey, 5_000_000).unwrap(); + check_balance!(5_000_000, &rpc_client, &offline_pubkey); check_ready(&rpc_client); @@ -934,7 +936,7 @@ fn test_stake_authorize_with_fee_payer() { withdrawer: None, withdrawer_signer: None, lockup: Lockup::default(), - amount: SpendAmount::Some(50_000), + amount: SpendAmount::Some(1_000_000), sign_only: false, dump_transaction_message: false, blockhash_query: BlockhashQuery::All(blockhash_query::Source::Cluster), @@ -945,8 +947,7 @@ fn test_stake_authorize_with_fee_payer() { from: 0, }; process_command(&config).unwrap(); - // `config` balance should be 50,000 - 1 stake account sig - 1 fee sig - check_balance!(50_000 - SIG_FEE - SIG_FEE, &rpc_client, &default_pubkey); + check_balance!(4_000_000 - fee_two_sig, &rpc_client, &default_pubkey); // Assign authority with separate fee payer config.signers = vec![&default_signer, &payer_keypair]; @@ -970,10 +971,10 @@ fn test_stake_authorize_with_fee_payer() { }; process_command(&config).unwrap(); // `config` balance has not changed, despite submitting the TX - check_balance!(50_000 - SIG_FEE - SIG_FEE, &rpc_client, &default_pubkey); + check_balance!(4_000_000 - fee_two_sig, &rpc_client, &default_pubkey); // `config_payer` however has paid `config`'s authority sig // and `config_payer`'s fee sig - check_balance!(100_000 - SIG_FEE - SIG_FEE, &rpc_client, &payer_pubkey); + check_balance!(5_000_000 - fee_two_sig, &rpc_client, &payer_pubkey); // Assign authority with offline fee payer let blockhash = rpc_client.get_latest_blockhash().unwrap(); @@ -1021,10 +1022,10 @@ fn test_stake_authorize_with_fee_payer() { }; process_command(&config).unwrap(); // `config`'s balance again has not changed - check_balance!(50_000 - SIG_FEE - SIG_FEE, &rpc_client, &default_pubkey); + check_balance!(4_000_000 - fee_two_sig, &rpc_client, &default_pubkey); // `config_offline` however has paid 1 sig due to being both authority // and fee payer - check_balance!(100_000 - SIG_FEE, &rpc_client, &offline_pubkey); + check_balance!(5_000_000 - fee_one_sig, &rpc_client, &offline_pubkey); } #[test] @@ -1058,12 +1059,17 @@ fn test_stake_split() { config_offline.command = CliCommand::ClusterVersion; process_command(&config_offline).unwrap_err(); - request_and_confirm_airdrop(&rpc_client, &config, &config.signers[0].pubkey(), 500_000) - .unwrap(); - check_balance!(500_000, &rpc_client, &config.signers[0].pubkey()); + request_and_confirm_airdrop( + &rpc_client, + &config, + &config.signers[0].pubkey(), + 50_000_000, + ) + .unwrap(); + check_balance!(50_000_000, &rpc_client, &config.signers[0].pubkey()); - request_and_confirm_airdrop(&rpc_client, &config_offline, &offline_pubkey, 100_000).unwrap(); - check_balance!(100_000, &rpc_client, &offline_pubkey); + request_and_confirm_airdrop(&rpc_client, &config_offline, &offline_pubkey, 1_000_000).unwrap(); + check_balance!(1_000_000, &rpc_client, &offline_pubkey); // Create stake account, identity is authority let minimum_stake_balance = rpc_client @@ -1207,12 +1213,12 @@ fn test_stake_set_lockup() { config_offline.command = CliCommand::ClusterVersion; process_command(&config_offline).unwrap_err(); - request_and_confirm_airdrop(&rpc_client, &config, &config.signers[0].pubkey(), 500_000) + request_and_confirm_airdrop(&rpc_client, &config, &config.signers[0].pubkey(), 5_000_000) .unwrap(); - check_balance!(500_000, &rpc_client, &config.signers[0].pubkey()); + check_balance!(5_000_000, &rpc_client, &config.signers[0].pubkey()); - request_and_confirm_airdrop(&rpc_client, &config_offline, &offline_pubkey, 100_000).unwrap(); - check_balance!(100_000, &rpc_client, &offline_pubkey); + request_and_confirm_airdrop(&rpc_client, &config_offline, &offline_pubkey, 1_000_000).unwrap(); + check_balance!(1_000_000, &rpc_client, &offline_pubkey); // Create stake account, identity is authority let minimum_stake_balance = rpc_client diff --git a/cli/tests/transfer.rs b/cli/tests/transfer.rs index 1613cc2546..0f966b5e90 100644 --- a/cli/tests/transfer.rs +++ b/cli/tests/transfer.rs @@ -16,6 +16,7 @@ use { solana_faucet::faucet::run_local_faucet, solana_sdk::{ commitment_config::CommitmentConfig, + fee::FeeStructure, native_token::sol_to_lamports, nonce::State as NonceState, pubkey::Pubkey, @@ -29,6 +30,8 @@ use { #[test] fn test_transfer() { solana_logger::setup(); + let fee_one_sig = FeeStructure::default().get_max_fee(1, 0); + let fee_two_sig = FeeStructure::default().get_max_fee(2, 0); let mint_keypair = Keypair::new(); let mint_pubkey = mint_keypair.pubkey(); let faucet_addr = run_local_faucet(mint_keypair, None); @@ -77,7 +80,11 @@ fn test_transfer() { derived_address_program_id: None, }; process_command(&config).unwrap(); - check_balance!(sol_to_lamports(4.0) - 1, &rpc_client, &sender_pubkey); + check_balance!( + sol_to_lamports(4.0) - fee_one_sig, + &rpc_client, + &sender_pubkey + ); check_balance!(sol_to_lamports(1.0), &rpc_client, &recipient_pubkey); // Plain ole transfer, failure due to InsufficientFundsForSpendAndFee @@ -98,7 +105,11 @@ fn test_transfer() { derived_address_program_id: None, }; assert!(process_command(&config).is_err()); - check_balance!(sol_to_lamports(4.0) - 1, &rpc_client, &sender_pubkey); + check_balance!( + sol_to_lamports(4.0) - fee_one_sig, + &rpc_client, + &sender_pubkey + ); check_balance!(sol_to_lamports(1.0), &rpc_client, &recipient_pubkey); let mut offline = CliConfig::recent_for_tests(); @@ -154,7 +165,11 @@ fn test_transfer() { derived_address_program_id: None, }; process_command(&config).unwrap(); - check_balance!(sol_to_lamports(0.5) - 1, &rpc_client, &offline_pubkey); + check_balance!( + sol_to_lamports(0.5) - fee_one_sig, + &rpc_client, + &offline_pubkey + ); check_balance!(sol_to_lamports(1.5), &rpc_client, &recipient_pubkey); // Create nonce account @@ -172,7 +187,7 @@ fn test_transfer() { }; process_command(&config).unwrap(); check_balance!( - sol_to_lamports(4.0) - 3 - minimum_nonce_balance, + sol_to_lamports(4.0) - fee_one_sig - fee_two_sig - minimum_nonce_balance, &rpc_client, &sender_pubkey, ); @@ -210,7 +225,7 @@ fn test_transfer() { }; process_command(&config).unwrap(); check_balance!( - sol_to_lamports(3.0) - 4 - minimum_nonce_balance, + sol_to_lamports(3.0) - 2 * fee_one_sig - fee_two_sig - minimum_nonce_balance, &rpc_client, &sender_pubkey, ); @@ -235,7 +250,7 @@ fn test_transfer() { }; process_command(&config).unwrap(); check_balance!( - sol_to_lamports(3.0) - 5 - minimum_nonce_balance, + sol_to_lamports(3.0) - 3 * fee_one_sig - fee_two_sig - minimum_nonce_balance, &rpc_client, &sender_pubkey, ); @@ -293,13 +308,18 @@ fn test_transfer() { derived_address_program_id: None, }; process_command(&config).unwrap(); - check_balance!(sol_to_lamports(0.1) - 2, &rpc_client, &offline_pubkey); + check_balance!( + sol_to_lamports(0.1) - 2 * fee_one_sig, + &rpc_client, + &offline_pubkey + ); check_balance!(sol_to_lamports(2.9), &rpc_client, &recipient_pubkey); } #[test] fn test_transfer_multisession_signing() { solana_logger::setup(); + let fee = FeeStructure::default().get_max_fee(2, 0); let mint_keypair = Keypair::new(); let mint_pubkey = mint_keypair.pubkey(); let faucet_addr = run_local_faucet(mint_keypair, None); @@ -329,7 +349,7 @@ fn test_transfer_multisession_signing() { &rpc_client, &CliConfig::recent_for_tests(), &offline_fee_payer_signer.pubkey(), - sol_to_lamports(1.0) + 3, + sol_to_lamports(1.0) + 2 * fee, ) .unwrap(); check_balance!( @@ -338,7 +358,7 @@ fn test_transfer_multisession_signing() { &offline_from_signer.pubkey(), ); check_balance!( - sol_to_lamports(1.0) + 3, + sol_to_lamports(1.0) + 2 * fee, &rpc_client, &offline_fee_payer_signer.pubkey(), ); @@ -438,7 +458,7 @@ fn test_transfer_multisession_signing() { &offline_from_signer.pubkey(), ); check_balance!( - sol_to_lamports(1.0) + 1, + sol_to_lamports(1.0) + fee, &rpc_client, &offline_fee_payer_signer.pubkey(), ); @@ -448,6 +468,7 @@ fn test_transfer_multisession_signing() { #[test] fn test_transfer_all() { solana_logger::setup(); + let fee = FeeStructure::default().get_max_fee(1, 0); let mint_keypair = Keypair::new(); let mint_pubkey = mint_keypair.pubkey(); let faucet_addr = run_local_faucet(mint_keypair, None); @@ -470,8 +491,8 @@ fn test_transfer_all() { let sender_pubkey = config.signers[0].pubkey(); let recipient_pubkey = Pubkey::new(&[1u8; 32]); - request_and_confirm_airdrop(&rpc_client, &config, &sender_pubkey, 50_000).unwrap(); - check_balance!(50_000, &rpc_client, &sender_pubkey); + request_and_confirm_airdrop(&rpc_client, &config, &sender_pubkey, 500_000).unwrap(); + check_balance!(500_000, &rpc_client, &sender_pubkey); check_balance!(0, &rpc_client, &recipient_pubkey); check_ready(&rpc_client); @@ -495,7 +516,7 @@ fn test_transfer_all() { }; process_command(&config).unwrap(); check_balance!(0, &rpc_client, &sender_pubkey); - check_balance!(49_999, &rpc_client, &recipient_pubkey); + check_balance!(500_000 - fee, &rpc_client, &recipient_pubkey); } #[test] @@ -554,6 +575,7 @@ fn test_transfer_unfunded_recipient() { #[test] fn test_transfer_with_seed() { solana_logger::setup(); + let fee = FeeStructure::default().get_max_fee(1, 0); let mint_keypair = Keypair::new(); let mint_pubkey = mint_keypair.pubkey(); let faucet_addr = run_local_faucet(mint_keypair, None); @@ -612,7 +634,7 @@ fn test_transfer_with_seed() { derived_address_program_id: Some(derived_address_program_id), }; process_command(&config).unwrap(); - check_balance!(sol_to_lamports(1.0) - 1, &rpc_client, &sender_pubkey); + check_balance!(sol_to_lamports(1.0) - fee, &rpc_client, &sender_pubkey); check_balance!(sol_to_lamports(5.0), &rpc_client, &recipient_pubkey); check_balance!(0, &rpc_client, &derived_address); } diff --git a/docs/src/developing/programming-model/runtime.md b/docs/src/developing/programming-model/runtime.md index ee23df0b3e..e73ee82a68 100644 --- a/docs/src/developing/programming-model/runtime.md +++ b/docs/src/developing/programming-model/runtime.md @@ -48,8 +48,12 @@ The policy is as follows: To prevent a program from abusing computation resources each instruction in a transaction is given a compute budget. The budget consists of computation units that are consumed as the program performs various operations and bounds that the -program may not exceed. When the program consumes its entire budget or exceeds -a bound then the runtime halts the program and returns an error. +program may not exceed. When the program consumes its entire budget or exceeds a +bound then the runtime halts the program and returns an error. + +Note: The compute budget currently applies per-instruction but is moving toward +a per-transaction model. For more information see [Transaction-wide Compute +Budget](#transaction-wide-compute-buget). The following operations incur a compute cost: @@ -60,12 +64,12 @@ The following operations incur a compute cost: - cross-program invocations - ... -For cross-program invocations the programs invoked inherit the budget of their +For cross-program invocations, the programs invoked inherit the budget of their parent. If an invoked program consume the budget or exceeds a bound the entire -invocation chain and the parent are halted. +invocation chain is halted. The current [compute -budget](https://github.com/solana-labs/solana/blob/d3a3a7548c857f26ec2cb10e270da72d373020ec/sdk/src/process_instruction.rs#L65) +budget](https://github.com/solana-labs/solana/blob/0224a8b127ace4c6453dd6492a38c66cb999abd2/sdk/src/compute_budget.rs#L102) can be found in the Solana SDK. For example, if the current budget is: @@ -80,6 +84,7 @@ max_invoke_depth: 4, max_call_depth: 64, stack_frame_size: 4096, log_pubkey_units: 100, +... ``` Then the program @@ -90,7 +95,7 @@ Then the program - Can not exceed a BPF call depth of 64 - Cannot exceed 4 levels of cross-program invocations. -Since the compute budget is consumed incrementally as the program executes the +Since the compute budget is consumed incrementally as the program executes, the total budget consumption will be a combination of the various costs of the operations it performs. @@ -98,12 +103,38 @@ At runtime a program may log how much of the compute budget remains. See [debugging](developing/on-chain-programs/debugging.md#monitoring-compute-budget-consumption) for more information. -The budget values are conditional on feature enablement, take a look at the -compute budget's -[new](https://github.com/solana-labs/solana/blob/d3a3a7548c857f26ec2cb10e270da72d373020ec/sdk/src/process_instruction.rs#L97) -function to find out how the budget is constructed. An understanding of how -[features](runtime.md#features) work and what features are enabled on the -cluster being used are required to determine the current budget's values. +## Transaction-wide Compute Budget + +Transactions are processed as a single entity and are the primary unit of block +scheduling. In order to facilitate better block scheduling and account for the +computational cost of each transaction, the compute budget is moving to a +transaction-wide budget rather than per-instruction. + +For information on what the compute budget is and how it is applied see [Compute +Budget](#compute-budget). + +With a transaction-wide compute budget the `max_units` cap is applied to the +entire transaction rather than to each instruction within the transaction. The +default number of maximum units remains at 200k which means the sum of the +compute units used by each instruction in the transaction must not exceed that +value. The number of maximum units allows is intentionally kept small to +facilitate optimized programs and form the bases for a minimum fee level. + +There are a lot of uses cases that require more than 200k units +transaction-wide. To enable these uses cases transactions can include a +[``ComputeBudgetInstruction`](https://github.com/solana-labs/solana/blob/0224a8b127ace4c6453dd6492a38c66cb999abd2/sdk/src/compute_budget.rs#L44) +requesting a higher compute unit cap. Higher compute caps will be charged +higher fees. + +Compute Budget instructions don't require any accounts and must lie in the first +3 instructions of a transaction otherwise they will be ignored. + +The `ComputeBudgetInstruction::request_units` function can be used to crate +these instructions: + +```rust +let instruction = ComputeBudgetInstruction::request_units(300_000); +``` ## New Features diff --git a/program-runtime/src/compute_budget.rs b/program-runtime/src/compute_budget.rs index b57a52864b..cba062f2e6 100644 --- a/program-runtime/src/compute_budget.rs +++ b/program-runtime/src/compute_budget.rs @@ -1,16 +1,13 @@ -use { - solana_sdk::{ - borsh::try_from_slice_unchecked, - compute_budget::{self, ComputeBudgetInstruction}, - entrypoint::HEAP_LENGTH as MIN_HEAP_FRAME_BYTES, - feature_set::{requestable_heap_size, FeatureSet}, - instruction::InstructionError, - transaction::{SanitizedTransaction, TransactionError}, - }, - std::sync::Arc, +use solana_sdk::{ + borsh::try_from_slice_unchecked, + compute_budget::{self, ComputeBudgetInstruction}, + entrypoint::HEAP_LENGTH as MIN_HEAP_FRAME_BYTES, + instruction::InstructionError, + message::SanitizedMessage, + transaction::TransactionError, }; -const MAX_UNITS: u32 = 1_000_000; +const MAX_UNITS: u32 = 1_400_000; const MAX_HEAP_FRAME_BYTES: u32 = 256 * 1024; #[cfg(RUSTC_WITH_SPECIALIZATION)] @@ -68,14 +65,19 @@ pub struct ComputeBudget { impl Default for ComputeBudget { fn default() -> Self { - Self::new() + Self::new(true) } } impl ComputeBudget { - pub fn new() -> Self { + pub fn new(use_max_units_default: bool) -> Self { + let max_units = if use_max_units_default { + MAX_UNITS + } else { + 200_000 + } as u64; ComputeBudget { - max_units: 200_000, + max_units, log_64_units: 100, create_program_address_units: 1500, invoke_units: 1000, @@ -97,25 +99,27 @@ impl ComputeBudget { } } - pub fn process_transaction( + pub fn process_message( &mut self, - tx: &SanitizedTransaction, - feature_set: Arc, - ) -> Result<(), TransactionError> { + message: &SanitizedMessage, + requestable_heap_size: bool, + ) -> Result { + let mut requested_additional_fee = 0; let error = TransactionError::InstructionError(0, InstructionError::InvalidInstructionData); // Compute budget instruction must be in the 1st 3 instructions (avoid // nonce marker), otherwise ignored - for (program_id, instruction) in tx.message().program_instructions_iter().take(3) { + for (program_id, instruction) in message.program_instructions_iter().take(3) { if compute_budget::check_id(program_id) { match try_from_slice_unchecked(&instruction.data) { - Ok(ComputeBudgetInstruction::RequestUnits(units)) => { - if units > MAX_UNITS { - return Err(error); - } - self.max_units = units as u64; + Ok(ComputeBudgetInstruction::RequestUnits { + units, + additional_fee, + }) => { + self.max_units = units.min(MAX_UNITS) as u64; + requested_additional_fee = additional_fee as u64; } Ok(ComputeBudgetInstruction::RequestHeapFrame(bytes)) => { - if !feature_set.is_active(&requestable_heap_size::id()) + if !requestable_heap_size || bytes > MAX_HEAP_FRAME_BYTES || bytes < MIN_HEAP_FRAME_BYTES as u32 || bytes % 1024 != 0 @@ -128,7 +132,7 @@ impl ComputeBudget { } } } - Ok(()) + Ok(requested_additional_fee) } } @@ -137,8 +141,13 @@ mod tests { use { super::*, solana_sdk::{ - hash::Hash, instruction::Instruction, message::Message, pubkey::Pubkey, - signature::Keypair, signer::Signer, transaction::Transaction, + hash::Hash, + instruction::Instruction, + message::Message, + pubkey::Pubkey, + signature::Keypair, + signer::Signer, + transaction::{SanitizedTransaction, Transaction}, }, }; @@ -150,24 +159,23 @@ mod tests { Message::new($instructions, Some(&payer_keypair.pubkey())), Hash::default(), )); - let feature_set = Arc::new(FeatureSet::all_enabled()); let mut compute_budget = ComputeBudget::default(); - let result = compute_budget.process_transaction(&tx, feature_set); - assert_eq!($expected_error as Result<(), TransactionError>, result); + let result = compute_budget.process_message(&tx.message(), true); + assert_eq!($expected_error, result); assert_eq!(compute_budget, $expected_budget); }; } #[test] - fn test_process_transaction() { + fn test_process_mesage() { // Units - test!(&[], Ok(()), ComputeBudget::default()); + test!(&[], Ok(0), ComputeBudget::default()); test!( &[ - ComputeBudgetInstruction::request_units(1), + ComputeBudgetInstruction::request_units(1, 0), Instruction::new_with_bincode(Pubkey::new_unique(), &0, vec![]), ], - Ok(()), + Ok(0), ComputeBudget { max_units: 1, ..ComputeBudget::default() @@ -175,21 +183,18 @@ mod tests { ); test!( &[ - ComputeBudgetInstruction::request_units(MAX_UNITS + 1), + ComputeBudgetInstruction::request_units(MAX_UNITS + 1, 0), Instruction::new_with_bincode(Pubkey::new_unique(), &0, vec![]), ], - Err(TransactionError::InstructionError( - 0, - InstructionError::InvalidInstructionData, - )), + Ok(0), ComputeBudget::default() ); test!( &[ Instruction::new_with_bincode(Pubkey::new_unique(), &0, vec![]), - ComputeBudgetInstruction::request_units(MAX_UNITS), + ComputeBudgetInstruction::request_units(MAX_UNITS, 0), ], - Ok(()), + Ok(0), ComputeBudget { max_units: MAX_UNITS as u64, ..ComputeBudget::default() @@ -200,20 +205,20 @@ mod tests { Instruction::new_with_bincode(Pubkey::new_unique(), &0, vec![]), Instruction::new_with_bincode(Pubkey::new_unique(), &0, vec![]), Instruction::new_with_bincode(Pubkey::new_unique(), &0, vec![]), - ComputeBudgetInstruction::request_units(1), + ComputeBudgetInstruction::request_units(1, 0), ], - Ok(()), + Ok(0), ComputeBudget::default() ); // HeapFrame - test!(&[], Ok(()), ComputeBudget::default()); + test!(&[], Ok(0), ComputeBudget::default()); test!( &[ ComputeBudgetInstruction::request_heap_frame(40 * 1024), Instruction::new_with_bincode(Pubkey::new_unique(), &0, vec![]), ], - Ok(()), + Ok(0), ComputeBudget { heap_size: Some(40 * 1024), ..ComputeBudget::default() @@ -257,7 +262,7 @@ mod tests { Instruction::new_with_bincode(Pubkey::new_unique(), &0, vec![]), ComputeBudgetInstruction::request_heap_frame(MAX_HEAP_FRAME_BYTES), ], - Ok(()), + Ok(0), ComputeBudget { heap_size: Some(MAX_HEAP_FRAME_BYTES as usize), ..ComputeBudget::default() @@ -270,7 +275,7 @@ mod tests { Instruction::new_with_bincode(Pubkey::new_unique(), &0, vec![]), ComputeBudgetInstruction::request_heap_frame(1), // ignored ], - Ok(()), + Ok(0), ComputeBudget::default() ); @@ -279,9 +284,9 @@ mod tests { &[ Instruction::new_with_bincode(Pubkey::new_unique(), &0, vec![]), ComputeBudgetInstruction::request_heap_frame(MAX_HEAP_FRAME_BYTES), - ComputeBudgetInstruction::request_units(MAX_UNITS), + ComputeBudgetInstruction::request_units(MAX_UNITS, 0), ], - Ok(()), + Ok(0), ComputeBudget { max_units: MAX_UNITS as u64, heap_size: Some(MAX_HEAP_FRAME_BYTES as usize), diff --git a/program-runtime/src/invoke_context.rs b/program-runtime/src/invoke_context.rs index 42aba3e517..3e0a3bf990 100644 --- a/program-runtime/src/invoke_context.rs +++ b/program-runtime/src/invoke_context.rs @@ -1610,11 +1610,12 @@ mod tests { let mut transaction_context = TransactionContext::new(accounts, 1, 3); let mut invoke_context = InvokeContext::new_mock(&mut transaction_context, &[]); invoke_context.feature_set = Arc::new(feature_set); + invoke_context.compute_budget = ComputeBudget::new(false); invoke_context.push(&[], &[0], &[]).unwrap(); assert_eq!( *invoke_context.get_compute_budget(), - ComputeBudget::default() + ComputeBudget::new(false) ); invoke_context.pop().unwrap(); @@ -1622,7 +1623,7 @@ mod tests { let expected_compute_budget = ComputeBudget { max_units: 500_000, heap_size: Some(256_usize.saturating_mul(1024)), - ..ComputeBudget::default() + ..ComputeBudget::new(false) }; assert_eq!( *invoke_context.get_compute_budget(), @@ -1633,7 +1634,7 @@ mod tests { invoke_context.push(&[], &[0], &[]).unwrap(); assert_eq!( *invoke_context.get_compute_budget(), - ComputeBudget::default() + ComputeBudget::new(false) ); invoke_context.pop().unwrap(); } diff --git a/programs/bpf/tests/programs.rs b/programs/bpf/tests/programs.rs index e3a55407d6..66b10ec7ce 100644 --- a/programs/bpf/tests/programs.rs +++ b/programs/bpf/tests/programs.rs @@ -46,6 +46,9 @@ use solana_sdk::{ clock::MAX_PROCESSING_AGE, compute_budget::ComputeBudgetInstruction, entrypoint::{MAX_PERMITTED_DATA_INCREASE, SUCCESS}, + feature_set::FeatureSet, + fee::FeeStructure, + fee_calculator::FeeRateGovernor, instruction::{AccountMeta, CompiledInstruction, Instruction, InstructionError}, loader_instruction, message::{v0::LoadedAddresses, Message, SanitizedMessage}, @@ -397,7 +400,7 @@ fn execute_transactions( ), } .expect("lamports_per_signature must be available"); - let fee = Bank::get_fee_for_message_with_lamports_per_signature( + let fee = bank.get_fee_for_message_with_lamports_per_signature( &SanitizedMessage::try_from(tx.message().clone()).unwrap(), lamports_per_signature, ); @@ -1383,7 +1386,7 @@ fn test_program_bpf_compute_budget() { ); let message = Message::new( &[ - ComputeBudgetInstruction::request_units(1), + ComputeBudgetInstruction::request_units(1, 0), Instruction::new_with_bincode(program_id, &0, vec![]), ], Some(&mint_keypair.pubkey()), @@ -2886,8 +2889,8 @@ fn test_program_bpf_realloc() { .unwrap(); } -#[cfg(feature = "bpf_rust")] #[test] +#[cfg(feature = "bpf_rust")] fn test_program_bpf_realloc_invoke() { solana_logger::setup(); @@ -3418,3 +3421,76 @@ fn test_program_bpf_processed_inner_instruction() { .send_and_confirm_message(&[&mint_keypair], message) .is_ok()); } + +#[test] +#[cfg(feature = "bpf_rust")] +fn test_program_fees() { + solana_logger::setup(); + + let congestion_multiplier = 1; + + let GenesisConfigInfo { + mut genesis_config, + mint_keypair, + .. + } = create_genesis_config(500_000_000); + genesis_config.fee_rate_governor = FeeRateGovernor::new(congestion_multiplier, 0); + let mut bank = Bank::new_for_tests(&genesis_config); + let fee_structure = + FeeStructure::new(0.000005, 0.0, vec![(200, 0.0000005), (1400000, 0.000005)]); + bank.fee_structure = fee_structure.clone(); + bank.feature_set = Arc::new(FeatureSet::all_enabled()); + + let (name, id, entrypoint) = solana_bpf_loader_program!(); + bank.add_builtin(&name, &id, entrypoint); + let bank_client = BankClient::new(bank); + + let program_id = load_bpf_program( + &bank_client, + &bpf_loader::id(), + &mint_keypair, + "solana_bpf_rust_noop", + ); + + let pre_balance = bank_client.get_balance(&mint_keypair.pubkey()).unwrap(); + let message = Message::new( + &[Instruction::new_with_bytes(program_id, &[], vec![])], + Some(&mint_keypair.pubkey()), + ); + + let sanitized_message = SanitizedMessage::try_from(message.clone()).unwrap(); + let expected_max_fee = Bank::calculate_fee( + &sanitized_message, + congestion_multiplier, + &fee_structure, + true, + ); + bank_client + .send_and_confirm_message(&[&mint_keypair], message) + .unwrap(); + let post_balance = bank_client.get_balance(&mint_keypair.pubkey()).unwrap(); + assert_eq!(pre_balance - post_balance, expected_max_fee); + + let pre_balance = bank_client.get_balance(&mint_keypair.pubkey()).unwrap(); + let message = Message::new( + &[ + ComputeBudgetInstruction::request_units(100, 42), + Instruction::new_with_bytes(program_id, &[], vec![]), + ], + Some(&mint_keypair.pubkey()), + ); + let sanitized_message = SanitizedMessage::try_from(message.clone()).unwrap(); + let expected_min_fee = Bank::calculate_fee( + &sanitized_message, + congestion_multiplier, + &fee_structure, + true, + ); + assert!(expected_min_fee < expected_max_fee); + + bank_client + .send_and_confirm_message(&[&mint_keypair], message) + .unwrap(); + let post_balance = bank_client.get_balance(&mint_keypair.pubkey()).unwrap(); + assert_eq!(pre_balance - post_balance, expected_min_fee); +} diff --git a/rpc/src/transaction_status_service.rs b/rpc/src/transaction_status_service.rs index 2f49d62c27..3b1afb2343 100644 --- a/rpc/src/transaction_status_service.rs +++ b/rpc/src/transaction_status_service.rs @@ -7,7 +7,7 @@ use { blockstore_processor::{TransactionStatusBatch, TransactionStatusMessage}, }, solana_runtime::bank::{ - Bank, DurableNonceFee, TransactionExecutionDetails, TransactionExecutionResult, + DurableNonceFee, TransactionExecutionDetails, TransactionExecutionResult, }, solana_transaction_status::{ extract_and_fmt_memos, InnerInstructions, Reward, TransactionStatusMeta, @@ -109,7 +109,7 @@ impl TransactionStatusService { ), } .expect("lamports_per_signature must be available"); - let fee = Bank::get_fee_for_message_with_lamports_per_signature( + let fee = bank.get_fee_for_message_with_lamports_per_signature( transaction.message(), lamports_per_signature, ); @@ -204,7 +204,7 @@ pub(crate) mod tests { dashmap::DashMap, solana_account_decoder::parse_token::token_amount_to_ui_amount, solana_ledger::{genesis_utils::create_genesis_config, get_tmp_ledger_path}, - solana_runtime::bank::{NonceFull, NoncePartial, RentDebits, TransactionBalancesSet}, + solana_runtime::bank::{Bank, NonceFull, NoncePartial, RentDebits, TransactionBalancesSet}, solana_sdk::{ account_utils::StateMut, clock::Slot, diff --git a/runtime/src/accounts.rs b/runtime/src/accounts.rs index 483fb8eb22..6dc827d688 100644 --- a/runtime/src/accounts.rs +++ b/runtime/src/accounts.rs @@ -28,7 +28,8 @@ use { account_utils::StateMut, bpf_loader_upgradeable::{self, UpgradeableLoaderState}, clock::{BankId, Slot, INITIAL_RENT_EPOCH}, - feature_set::{self, FeatureSet}, + feature_set::{self, tx_wide_compute_cap, FeatureSet}, + fee::FeeStructure, genesis_config::ClusterType, hash::Hash, message::{ @@ -472,6 +473,7 @@ impl Accounts { error_counters: &mut ErrorCounters, rent_collector: &RentCollector, feature_set: &FeatureSet, + fee_structure: &FeeStructure, ) -> Vec { txs.iter() .zip(lock_results) @@ -484,7 +486,12 @@ impl Accounts { hash_queue.get_lamports_per_signature(tx.message().recent_blockhash()) }); let fee = if let Some(lamports_per_signature) = lamports_per_signature { - Bank::calculate_fee(tx.message(), lamports_per_signature) + Bank::calculate_fee( + tx.message(), + lamports_per_signature, + fee_structure, + feature_set.is_active(&tx_wide_compute_cap::id()), + ) } else { return (Err(TransactionError::BlockhashNotFound), None); }; @@ -1359,6 +1366,8 @@ mod tests { lamports_per_signature: u64, rent_collector: &RentCollector, error_counters: &mut ErrorCounters, + feature_set: &FeatureSet, + fee_structure: &FeeStructure, ) -> Vec { let mut hash_queue = BlockhashQueue::new(100); hash_queue.register_hash(&tx.message().recent_blockhash, lamports_per_signature); @@ -1382,7 +1391,8 @@ mod tests { &hash_queue, error_counters, rent_collector, - &FeatureSet::all_enabled(), + feature_set, + fee_structure, ) } @@ -1398,6 +1408,8 @@ mod tests { lamports_per_signature, &RentCollector::default(), error_counters, + &FeatureSet::all_enabled(), + &FeeStructure::default(), ) } @@ -1549,6 +1561,8 @@ mod tests { let fee = Bank::calculate_fee( &SanitizedMessage::try_from(tx.message().clone()).unwrap(), 10, + &FeeStructure::default(), + false, ); assert_eq!(fee, 10); @@ -1595,6 +1609,8 @@ mod tests { #[test] fn test_load_accounts_fee_payer_is_nonce() { let mut error_counters = ErrorCounters::default(); + let mut feature_set = FeatureSet::all_enabled(); + feature_set.deactivate(&tx_wide_compute_cap::id()); let rent_collector = RentCollector::new( 0, &EpochSchedule::default(), @@ -1631,6 +1647,8 @@ mod tests { min_balance, &rent_collector, &mut error_counters, + &feature_set, + &FeeStructure::default(), ); assert_eq!(loaded_accounts.len(), 1); let (load_res, _nonce) = &loaded_accounts[0]; @@ -1645,6 +1663,8 @@ mod tests { min_balance, &rent_collector, &mut error_counters, + &feature_set, + &FeeStructure::default(), ); assert_eq!(loaded_accounts.len(), 1); let (load_res, _nonce) = &loaded_accounts[0]; @@ -1658,6 +1678,8 @@ mod tests { min_balance, &rent_collector, &mut error_counters, + &feature_set, + &FeeStructure::default(), ); assert_eq!(loaded_accounts.len(), 1); let (load_res, _nonce) = &loaded_accounts[0]; @@ -2982,6 +3004,7 @@ mod tests { &mut error_counters, &rent_collector, &FeatureSet::all_enabled(), + &FeeStructure::default(), ) } diff --git a/runtime/src/bank.rs b/runtime/src/bank.rs index 77ea18faaa..a4b7585d4e 100644 --- a/runtime/src/bank.rs +++ b/runtime/src/bank.rs @@ -99,8 +99,10 @@ use { epoch_schedule::EpochSchedule, feature, feature_set::{ - self, disable_fee_calculator, nonce_must_be_writable, tx_wide_compute_cap, FeatureSet, + self, disable_fee_calculator, nonce_must_be_writable, requestable_heap_size, + tx_wide_compute_cap, FeatureSet, }, + fee::FeeStructure, fee_calculator::{FeeCalculator, FeeRateGovernor}, genesis_config::{ClusterType, GenesisConfig}, hard_forks::HardForks, @@ -1225,6 +1227,9 @@ pub struct Bank { /// Current size of the accounts data. Used when processing messages to enforce a limit on its /// maximum size. accounts_data_len: AtomicU64, + + /// Transaction fee structure + pub fee_structure: FeeStructure, } impl Default for BlockhashQueue { @@ -1366,6 +1371,7 @@ impl Bank { cost_tracker: RwLock::::default(), sysvar_cache: RwLock::::default(), accounts_data_len: AtomicU64::default(), + fee_structure: FeeStructure::default(), }; let total_accounts_stats = bank.get_total_accounts_stats().unwrap(); @@ -1690,6 +1696,7 @@ impl Bank { cost_tracker: RwLock::new(CostTracker::default()), sysvar_cache: RwLock::new(SysvarCache::default()), accounts_data_len: AtomicU64::new(parent.load_accounts_data_len()), + fee_structure: parent.fee_structure.clone(), }; let (_, ancestors_time) = Measure::this( @@ -1978,6 +1985,7 @@ impl Bank { cost_tracker: RwLock::new(CostTracker::default()), sysvar_cache: RwLock::new(SysvarCache::default()), accounts_data_len: AtomicU64::new(accounts_data_len), + fee_structure: FeeStructure::default(), }; bank.finish_init( genesis_config, @@ -3252,15 +3260,25 @@ impl Bank { NoncePartial::new(address, account).lamports_per_signature() }) })?; - - Some(Self::calculate_fee(message, lamports_per_signature)) + Some(Self::calculate_fee( + message, + lamports_per_signature, + &self.fee_structure, + self.feature_set.is_active(&tx_wide_compute_cap::id()), + )) } pub fn get_fee_for_message_with_lamports_per_signature( + &self, message: &SanitizedMessage, lamports_per_signature: u64, ) -> u64 { - Self::calculate_fee(message, lamports_per_signature) + Self::calculate_fee( + message, + lamports_per_signature, + &self.fee_structure, + self.feature_set.is_active(&tx_wide_compute_cap::id()), + ) } #[deprecated( @@ -3972,6 +3990,7 @@ impl Bank { &mut error_counters, &self.rent_collector, &self.feature_set, + &self.fee_structure, ); load_time.stop(); @@ -3994,12 +4013,17 @@ impl Bank { signature_count += u64::from(tx.message().header().num_required_signatures); - let mut compute_budget = self.compute_budget.unwrap_or_else(ComputeBudget::new); - if feature_set.is_active(&tx_wide_compute_cap::id()) { + let tx_wide_compute_cap = feature_set.is_active(&tx_wide_compute_cap::id()); + let mut compute_budget = self + .compute_budget + .unwrap_or_else(|| ComputeBudget::new(tx_wide_compute_cap)); + if tx_wide_compute_cap { let mut compute_budget_process_transaction_time = Measure::start("compute_budget_process_transaction_time"); - let process_transaction_result = - compute_budget.process_transaction(tx, feature_set); + let process_transaction_result = compute_budget.process_message( + tx.message(), + feature_set.is_active(&requestable_heap_size::id()), + ); compute_budget_process_transaction_time.stop(); saturating_add_assign!( timings @@ -4181,9 +4205,10 @@ impl Bank { .unwrap(); } - /// Calculate fee for `SanitizedMessage` - pub fn calculate_fee(message: &SanitizedMessage, lamports_per_signature: u64) -> u64 { + fn get_num_signatures_in_message(message: &SanitizedMessage) -> u64 { let mut num_signatures = u64::from(message.header().num_required_signatures); + // This next part is really calculating the number of pre-processor + // operations being done and treating them like a signature for (program_id, instruction) in message.program_instructions_iter() { if secp256k1_program::check_id(program_id) || ed25519_program::check_id(program_id) { if let Some(num_verifies) = instruction.data.get(0) { @@ -4191,8 +4216,64 @@ impl Bank { } } } + num_signatures + } - lamports_per_signature.saturating_mul(num_signatures) + fn get_num_write_locks_in_message(message: &SanitizedMessage) -> u64 { + message + .account_keys() + .len() + .saturating_sub(message.num_readonly_accounts()) as u64 + } + + /// Calculate fee for `SanitizedMessage` + pub fn calculate_fee( + message: &SanitizedMessage, + lamports_per_signature: u64, + fee_structure: &FeeStructure, + tx_wide_compute_cap: bool, + ) -> u64 { + if tx_wide_compute_cap { + // Fee based on compute units and signatures + const BASE_CONGESTION: f64 = 5_000.0; + let current_congestion = BASE_CONGESTION.max(lamports_per_signature as f64); + let congestion_multiplier = if lamports_per_signature == 0 { + 0.0 // test only + } else { + BASE_CONGESTION / current_congestion + }; + + let mut compute_budget = ComputeBudget::default(); + let additional_fee = compute_budget + .process_message(message, false) + .unwrap_or_default(); + let signature_fee = Self::get_num_signatures_in_message(message) + .saturating_mul(fee_structure.lamports_per_signature); + let write_lock_fee = Self::get_num_write_locks_in_message(message) + .saturating_mul(fee_structure.lamports_per_write_lock); + let compute_fee = fee_structure + .compute_fee_bins + .iter() + .find(|bin| compute_budget.max_units <= bin.limit) + .map(|bin| bin.fee) + .unwrap_or_else(|| { + fee_structure + .compute_fee_bins + .last() + .map(|bin| bin.fee) + .unwrap_or_default() + }); + + ((additional_fee + .saturating_add(signature_fee) + .saturating_add(write_lock_fee) + .saturating_add(compute_fee) as f64) + * congestion_multiplier) + .round() as u64 + } else { + // Fee based only on signatures + lamports_per_signature.saturating_mul(Self::get_num_signatures_in_message(message)) + } } fn filter_program_errors_and_collect_fee( @@ -4226,7 +4307,12 @@ impl Bank { let lamports_per_signature = lamports_per_signature.ok_or(TransactionError::BlockhashNotFound)?; - let fee = Self::calculate_fee(tx.message(), lamports_per_signature); + let fee = Self::calculate_fee( + tx.message(), + lamports_per_signature, + &self.fee_structure, + self.feature_set.is_active(&tx_wide_compute_cap::id()), + ); // In case of instruction error, even though no accounts // were stored we still need to charge the payer the @@ -9264,6 +9350,7 @@ pub(crate) mod tests { genesis_config.fee_rate_governor.burn(expected_fee_paid); let mut bank = Bank::new_for_tests(&genesis_config); + bank.deactivate_feature(&tx_wide_compute_cap::id()); let capitalization = bank.capitalization(); @@ -9349,7 +9436,119 @@ pub(crate) mod tests { } #[test] - fn test_bank_blockhash_fee_schedule() { + fn test_bank_tx_compute_unit_fee() { + solana_logger::setup(); + + let key = Keypair::new(); + let arbitrary_transfer_amount = 42; + let mint = arbitrary_transfer_amount * 10_000_000; + let leader = solana_sdk::pubkey::new_rand(); + let GenesisConfigInfo { + mut genesis_config, + mint_keypair, + .. + } = create_genesis_config_with_leader(mint, &leader, 3); + genesis_config.fee_rate_governor = FeeRateGovernor::new(4, 0); // something divisible by 2 + + let expected_fee_paid = Bank::calculate_fee( + &SanitizedMessage::try_from(Message::new(&[], Some(&Pubkey::new_unique()))).unwrap(), + genesis_config + .fee_rate_governor + .create_fee_calculator() + .lamports_per_signature, + &FeeStructure::default(), + true, + ); + + let (expected_fee_collected, expected_fee_burned) = + genesis_config.fee_rate_governor.burn(expected_fee_paid); + + let mut bank = Bank::new_for_tests(&genesis_config); + + let capitalization = bank.capitalization(); + + let tx = system_transaction::transfer( + &mint_keypair, + &key.pubkey(), + arbitrary_transfer_amount, + bank.last_blockhash(), + ); + + let initial_balance = bank.get_balance(&leader); + assert_eq!(bank.process_transaction(&tx), Ok(())); + assert_eq!(bank.get_balance(&key.pubkey()), arbitrary_transfer_amount); + assert_eq!( + bank.get_balance(&mint_keypair.pubkey()), + mint - arbitrary_transfer_amount - expected_fee_paid + ); + + assert_eq!(bank.get_balance(&leader), initial_balance); + goto_end_of_slot(&mut bank); + assert_eq!(bank.signature_count(), 1); + assert_eq!( + bank.get_balance(&leader), + initial_balance + expected_fee_collected + ); // Leader collects fee after the bank is frozen + + // verify capitalization + let sysvar_and_builtin_program_delta = 1; + assert_eq!( + capitalization - expected_fee_burned + sysvar_and_builtin_program_delta, + bank.capitalization() + ); + + assert_eq!( + *bank.rewards.read().unwrap(), + vec![( + leader, + RewardInfo { + reward_type: RewardType::Fee, + lamports: expected_fee_collected as i64, + post_balance: initial_balance + expected_fee_collected, + commission: None, + } + )] + ); + + // Verify that an InstructionError collects fees, too + let mut bank = Bank::new_from_parent(&Arc::new(bank), &leader, 1); + let mut tx = + system_transaction::transfer(&mint_keypair, &key.pubkey(), 1, bank.last_blockhash()); + // Create a bogus instruction to system_program to cause an instruction error + tx.message.instructions[0].data[0] = 40; + + bank.process_transaction(&tx) + .expect_err("instruction error"); + assert_eq!(bank.get_balance(&key.pubkey()), arbitrary_transfer_amount); // no change + assert_eq!( + bank.get_balance(&mint_keypair.pubkey()), + mint - arbitrary_transfer_amount - 2 * expected_fee_paid + ); // mint_keypair still pays a fee + goto_end_of_slot(&mut bank); + assert_eq!(bank.signature_count(), 1); + + // Profit! 2 transaction signatures processed at 3 lamports each + assert_eq!( + bank.get_balance(&leader), + initial_balance + 2 * expected_fee_collected + ); + + assert_eq!( + *bank.rewards.read().unwrap(), + vec![( + leader, + RewardInfo { + reward_type: RewardType::Fee, + lamports: expected_fee_collected as i64, + post_balance: initial_balance + 2 * expected_fee_collected, + commission: None, + } + )] + ); + } + + #[test] + fn test_bank_blockhash_fee_structure() { //solana_logger::setup(); let leader = solana_sdk::pubkey::new_rand(); @@ -9370,6 +9569,7 @@ pub(crate) mod tests { assert_eq!(cheap_lamports_per_signature, 0); let mut bank = Bank::new_from_parent(&Arc::new(bank), &leader, 1); + bank.deactivate_feature(&tx_wide_compute_cap::id()); goto_end_of_slot(&mut bank); let expensive_blockhash = bank.last_blockhash(); let expensive_lamports_per_signature = bank.get_lamports_per_signature(); @@ -9400,6 +9600,70 @@ pub(crate) mod tests { ); } + #[test] + fn test_bank_blockhash_compute_unit_fee_structure() { + //solana_logger::setup(); + + let leader = solana_sdk::pubkey::new_rand(); + let GenesisConfigInfo { + mut genesis_config, + mint_keypair, + .. + } = create_genesis_config_with_leader(1_000_000_000, &leader, 3); + genesis_config + .fee_rate_governor + .target_lamports_per_signature = 1000; + genesis_config.fee_rate_governor.target_signatures_per_slot = 1; + + let mut bank = Bank::new_for_tests(&genesis_config); + goto_end_of_slot(&mut bank); + let cheap_blockhash = bank.last_blockhash(); + let cheap_lamports_per_signature = bank.get_lamports_per_signature(); + assert_eq!(cheap_lamports_per_signature, 0); + + let mut bank = Bank::new_from_parent(&Arc::new(bank), &leader, 1); + goto_end_of_slot(&mut bank); + let expensive_blockhash = bank.last_blockhash(); + let expensive_lamports_per_signature = bank.get_lamports_per_signature(); + assert!(cheap_lamports_per_signature < expensive_lamports_per_signature); + + let bank = Bank::new_from_parent(&Arc::new(bank), &leader, 2); + + // Send a transfer using cheap_blockhash + let key = Keypair::new(); + let initial_mint_balance = bank.get_balance(&mint_keypair.pubkey()); + let tx = system_transaction::transfer(&mint_keypair, &key.pubkey(), 1, cheap_blockhash); + assert_eq!(bank.process_transaction(&tx), Ok(())); + assert_eq!(bank.get_balance(&key.pubkey()), 1); + let cheap_fee = Bank::calculate_fee( + &SanitizedMessage::try_from(Message::new(&[], Some(&Pubkey::new_unique()))).unwrap(), + cheap_lamports_per_signature, + &FeeStructure::default(), + true, + ); + assert_eq!( + bank.get_balance(&mint_keypair.pubkey()), + initial_mint_balance - 1 - cheap_fee + ); + + // Send a transfer using expensive_blockhash + let key = Keypair::new(); + let initial_mint_balance = bank.get_balance(&mint_keypair.pubkey()); + let tx = system_transaction::transfer(&mint_keypair, &key.pubkey(), 1, expensive_blockhash); + assert_eq!(bank.process_transaction(&tx), Ok(())); + assert_eq!(bank.get_balance(&key.pubkey()), 1); + let expensive_fee = Bank::calculate_fee( + &SanitizedMessage::try_from(Message::new(&[], Some(&Pubkey::new_unique()))).unwrap(), + expensive_lamports_per_signature, + &FeeStructure::default(), + true, + ); + assert_eq!( + bank.get_balance(&mint_keypair.pubkey()), + initial_mint_balance - 1 - expensive_fee + ); + } + #[test] fn test_filter_program_errors_and_collect_fee() { let leader = solana_sdk::pubkey::new_rand(); @@ -9409,6 +9673,58 @@ pub(crate) mod tests { .. } = create_genesis_config_with_leader(100, &leader, 3); genesis_config.fee_rate_governor = FeeRateGovernor::new(2, 0); + let mut bank = Bank::new_for_tests(&genesis_config); + bank.deactivate_feature(&tx_wide_compute_cap::id()); + + let key = Keypair::new(); + let tx1 = SanitizedTransaction::from_transaction_for_tests(system_transaction::transfer( + &mint_keypair, + &key.pubkey(), + 2, + genesis_config.hash(), + )); + let tx2 = SanitizedTransaction::from_transaction_for_tests(system_transaction::transfer( + &mint_keypair, + &key.pubkey(), + 5, + genesis_config.hash(), + )); + + let results = vec![ + new_execution_result(Ok(()), None), + new_execution_result( + Err(TransactionError::InstructionError( + 1, + SystemError::ResultWithNegativeLamports.into(), + )), + None, + ), + ]; + let initial_balance = bank.get_balance(&leader); + + let results = bank.filter_program_errors_and_collect_fee(&[tx1, tx2], &results); + bank.freeze(); + assert_eq!( + bank.get_balance(&leader), + initial_balance + + bank + .fee_rate_governor + .burn(bank.fee_rate_governor.lamports_per_signature * 2) + .0 + ); + assert_eq!(results[0], Ok(())); + assert_eq!(results[1], Ok(())); + } + + #[test] + fn test_filter_program_errors_and_collect_compute_unit_fee() { + let leader = solana_sdk::pubkey::new_rand(); + let GenesisConfigInfo { + mut genesis_config, + mint_keypair, + .. + } = create_genesis_config_with_leader(1000000, &leader, 3); + genesis_config.fee_rate_governor = FeeRateGovernor::new(2, 0); let bank = Bank::new_for_tests(&genesis_config); let key = Keypair::new(); @@ -9444,7 +9760,21 @@ pub(crate) mod tests { initial_balance + bank .fee_rate_governor - .burn(bank.fee_rate_governor.lamports_per_signature * 2) + .burn( + Bank::calculate_fee( + &SanitizedMessage::try_from(Message::new( + &[], + Some(&Pubkey::new_unique()) + )) + .unwrap(), + genesis_config + .fee_rate_governor + .create_fee_calculator() + .lamports_per_signature, + &FeeStructure::default(), + true, + ) * 2 + ) .0 ); assert_eq!(results[0], Ok(())); @@ -11188,6 +11518,7 @@ pub(crate) mod tests { custodian_lamports: u64, nonce_lamports: u64, nonce_authority: Option, + feature_set: FeatureSet, ) -> Result<(Arc, Keypair, Keypair, Keypair)> where F: FnMut(&mut GenesisConfig), @@ -11195,7 +11526,9 @@ pub(crate) mod tests { let (mut genesis_config, mint_keypair) = create_genesis_config(supply_lamports); genesis_config.rent.lamports_per_byte_year = 0; genesis_cfg_fn(&mut genesis_config); - let mut bank = Arc::new(Bank::new_for_tests(&genesis_config)); + let mut bank = Bank::new_for_tests(&genesis_config); + bank.feature_set = Arc::new(feature_set); + let mut bank = Arc::new(bank); // Banks 0 and 1 have no fees, wait two blocks before // initializing our nonce accounts @@ -11216,8 +11549,11 @@ pub(crate) mod tests { #[test] fn test_check_transaction_for_nonce_ok() { + let mut feature_set = FeatureSet::all_enabled(); + feature_set.deactivate(&tx_wide_compute_cap::id()); let (bank, _mint_keypair, custodian_keypair, nonce_keypair) = - setup_nonce_with_bank(10_000_000, |_| {}, 5_000_000, 250_000, None).unwrap(); + setup_nonce_with_bank(10_000_000, |_| {}, 5_000_000, 250_000, None, feature_set) + .unwrap(); let custodian_pubkey = custodian_keypair.pubkey(); let nonce_pubkey = nonce_keypair.pubkey(); @@ -11240,8 +11576,11 @@ pub(crate) mod tests { #[test] fn test_check_transaction_for_nonce_not_nonce_fail() { + let mut feature_set = FeatureSet::all_enabled(); + feature_set.deactivate(&tx_wide_compute_cap::id()); let (bank, _mint_keypair, custodian_keypair, nonce_keypair) = - setup_nonce_with_bank(10_000_000, |_| {}, 5_000_000, 250_000, None).unwrap(); + setup_nonce_with_bank(10_000_000, |_| {}, 5_000_000, 250_000, None, feature_set) + .unwrap(); let custodian_pubkey = custodian_keypair.pubkey(); let nonce_pubkey = nonce_keypair.pubkey(); @@ -11262,8 +11601,11 @@ pub(crate) mod tests { #[test] fn test_check_transaction_for_nonce_missing_ix_pubkey_fail() { + let mut feature_set = FeatureSet::all_enabled(); + feature_set.deactivate(&tx_wide_compute_cap::id()); let (bank, _mint_keypair, custodian_keypair, nonce_keypair) = - setup_nonce_with_bank(10_000_000, |_| {}, 5_000_000, 250_000, None).unwrap(); + setup_nonce_with_bank(10_000_000, |_| {}, 5_000_000, 250_000, None, feature_set) + .unwrap(); let custodian_pubkey = custodian_keypair.pubkey(); let nonce_pubkey = nonce_keypair.pubkey(); @@ -11285,8 +11627,11 @@ pub(crate) mod tests { #[test] fn test_check_transaction_for_nonce_nonce_acc_does_not_exist_fail() { + let mut feature_set = FeatureSet::all_enabled(); + feature_set.deactivate(&tx_wide_compute_cap::id()); let (bank, _mint_keypair, custodian_keypair, nonce_keypair) = - setup_nonce_with_bank(10_000_000, |_| {}, 5_000_000, 250_000, None).unwrap(); + setup_nonce_with_bank(10_000_000, |_| {}, 5_000_000, 250_000, None, feature_set) + .unwrap(); let custodian_pubkey = custodian_keypair.pubkey(); let nonce_pubkey = nonce_keypair.pubkey(); let missing_keypair = Keypair::new(); @@ -11309,8 +11654,11 @@ pub(crate) mod tests { #[test] fn test_check_transaction_for_nonce_bad_tx_hash_fail() { + let mut feature_set = FeatureSet::all_enabled(); + feature_set.deactivate(&tx_wide_compute_cap::id()); let (bank, _mint_keypair, custodian_keypair, nonce_keypair) = - setup_nonce_with_bank(10_000_000, |_| {}, 5_000_000, 250_000, None).unwrap(); + setup_nonce_with_bank(10_000_000, |_| {}, 5_000_000, 250_000, None, feature_set) + .unwrap(); let custodian_pubkey = custodian_keypair.pubkey(); let nonce_pubkey = nonce_keypair.pubkey(); @@ -11357,8 +11705,139 @@ pub(crate) mod tests { #[test] fn test_nonce_transaction() { + let mut feature_set = FeatureSet::all_enabled(); + feature_set.deactivate(&tx_wide_compute_cap::id()); let (mut bank, _mint_keypair, custodian_keypair, nonce_keypair) = - setup_nonce_with_bank(10_000_000, |_| {}, 5_000_000, 250_000, None).unwrap(); + setup_nonce_with_bank(10_000_000, |_| {}, 5_000_000, 250_000, None, feature_set) + .unwrap(); + let alice_keypair = Keypair::new(); + let alice_pubkey = alice_keypair.pubkey(); + let custodian_pubkey = custodian_keypair.pubkey(); + let nonce_pubkey = nonce_keypair.pubkey(); + + assert_eq!(bank.get_balance(&custodian_pubkey), 4_750_000); + assert_eq!(bank.get_balance(&nonce_pubkey), 250_000); + + /* Grab the hash stored in the nonce account */ + let nonce_hash = get_nonce_blockhash(&bank, &nonce_pubkey).unwrap(); + + /* Kick nonce hash off the blockhash_queue */ + for _ in 0..MAX_RECENT_BLOCKHASHES + 1 { + goto_end_of_slot(Arc::get_mut(&mut bank).unwrap()); + bank = Arc::new(new_from_parent(&bank)); + } + + /* Expect a non-Nonce transfer to fail */ + assert_eq!( + bank.process_transaction(&system_transaction::transfer( + &custodian_keypair, + &alice_pubkey, + 100_000, + nonce_hash + ),), + Err(TransactionError::BlockhashNotFound), + ); + /* Check fee not charged */ + assert_eq!(bank.get_balance(&custodian_pubkey), 4_750_000); + + /* Nonce transfer */ + let nonce_tx = Transaction::new_signed_with_payer( + &[ + system_instruction::advance_nonce_account(&nonce_pubkey, &nonce_pubkey), + system_instruction::transfer(&custodian_pubkey, &alice_pubkey, 100_000), + ], + Some(&custodian_pubkey), + &[&custodian_keypair, &nonce_keypair], + nonce_hash, + ); + assert_eq!(bank.process_transaction(&nonce_tx), Ok(())); + + /* Check balances */ + let mut recent_message = nonce_tx.message; + recent_message.recent_blockhash = bank.last_blockhash(); + let mut expected_balance = 4_650_000 + - bank + .get_fee_for_message(&recent_message.try_into().unwrap()) + .unwrap(); + assert_eq!(bank.get_balance(&custodian_pubkey), expected_balance); + assert_eq!(bank.get_balance(&nonce_pubkey), 250_000); + assert_eq!(bank.get_balance(&alice_pubkey), 100_000); + + /* Confirm stored nonce has advanced */ + let new_nonce = get_nonce_blockhash(&bank, &nonce_pubkey).unwrap(); + assert_ne!(nonce_hash, new_nonce); + + /* Nonce re-use fails */ + let nonce_tx = Transaction::new_signed_with_payer( + &[ + system_instruction::advance_nonce_account(&nonce_pubkey, &nonce_pubkey), + system_instruction::transfer(&custodian_pubkey, &alice_pubkey, 100_000), + ], + Some(&custodian_pubkey), + &[&custodian_keypair, &nonce_keypair], + nonce_hash, + ); + assert_eq!( + bank.process_transaction(&nonce_tx), + Err(TransactionError::BlockhashNotFound) + ); + /* Check fee not charged and nonce not advanced */ + assert_eq!(bank.get_balance(&custodian_pubkey), expected_balance); + assert_eq!( + new_nonce, + get_nonce_blockhash(&bank, &nonce_pubkey).unwrap() + ); + + let nonce_hash = new_nonce; + + /* Kick nonce hash off the blockhash_queue */ + for _ in 0..MAX_RECENT_BLOCKHASHES + 1 { + goto_end_of_slot(Arc::get_mut(&mut bank).unwrap()); + bank = Arc::new(new_from_parent(&bank)); + } + + let nonce_tx = Transaction::new_signed_with_payer( + &[ + system_instruction::advance_nonce_account(&nonce_pubkey, &nonce_pubkey), + system_instruction::transfer(&custodian_pubkey, &alice_pubkey, 100_000_000), + ], + Some(&custodian_pubkey), + &[&custodian_keypair, &nonce_keypair], + nonce_hash, + ); + assert_eq!( + bank.process_transaction(&nonce_tx), + Err(TransactionError::InstructionError( + 1, + system_instruction::SystemError::ResultWithNegativeLamports.into(), + )) + ); + /* Check fee charged and nonce has advanced */ + let mut recent_message = nonce_tx.message.clone(); + recent_message.recent_blockhash = bank.last_blockhash(); + expected_balance -= bank + .get_fee_for_message(&SanitizedMessage::try_from(recent_message).unwrap()) + .unwrap(); + assert_eq!(bank.get_balance(&custodian_pubkey), expected_balance); + assert_ne!( + nonce_hash, + get_nonce_blockhash(&bank, &nonce_pubkey).unwrap() + ); + /* Confirm replaying a TX that failed with InstructionError::* now + * fails with TransactionError::BlockhashNotFound + */ + assert_eq!( + bank.process_transaction(&nonce_tx), + Err(TransactionError::BlockhashNotFound), + ); + } + + #[test] + fn test_nonce_transaction_with_tx_wide_caps() { + let feature_set = FeatureSet::all_enabled(); + let (mut bank, _mint_keypair, custodian_keypair, nonce_keypair) = + setup_nonce_with_bank(10_000_000, |_| {}, 5_000_000, 250_000, None, feature_set) + .unwrap(); let alice_keypair = Keypair::new(); let alice_pubkey = alice_keypair.pubkey(); let custodian_pubkey = custodian_keypair.pubkey(); @@ -11484,8 +11963,11 @@ pub(crate) mod tests { #[test] fn test_nonce_authority() { solana_logger::setup(); + let mut feature_set = FeatureSet::all_enabled(); + feature_set.deactivate(&tx_wide_compute_cap::id()); let (mut bank, _mint_keypair, custodian_keypair, nonce_keypair) = - setup_nonce_with_bank(10_000_000, |_| {}, 5_000_000, 250_000, None).unwrap(); + setup_nonce_with_bank(10_000_000, |_| {}, 5_000_000, 250_000, None, feature_set) + .unwrap(); let alice_keypair = Keypair::new(); let alice_pubkey = alice_keypair.pubkey(); let custodian_pubkey = custodian_keypair.pubkey(); @@ -11544,9 +12026,17 @@ pub(crate) mod tests { fn test_nonce_payer() { solana_logger::setup(); let nonce_starting_balance = 250_000; - let (mut bank, _mint_keypair, custodian_keypair, nonce_keypair) = - setup_nonce_with_bank(10_000_000, |_| {}, 5_000_000, nonce_starting_balance, None) - .unwrap(); + let mut feature_set = FeatureSet::all_enabled(); + feature_set.deactivate(&tx_wide_compute_cap::id()); + let (mut bank, _mint_keypair, custodian_keypair, nonce_keypair) = setup_nonce_with_bank( + 10_000_000, + |_| {}, + 5_000_000, + nonce_starting_balance, + None, + feature_set, + ) + .unwrap(); let alice_keypair = Keypair::new(); let alice_pubkey = alice_keypair.pubkey(); let custodian_pubkey = custodian_keypair.pubkey(); @@ -11597,11 +12087,150 @@ pub(crate) mod tests { ); } + #[test] + fn test_nonce_payer_tx_wide_cap() { + solana_logger::setup(); + let nonce_starting_balance = + 250_000 + FeeStructure::default().compute_fee_bins.last().unwrap().fee; + let feature_set = FeatureSet::all_enabled(); + let (mut bank, _mint_keypair, custodian_keypair, nonce_keypair) = setup_nonce_with_bank( + 10_000_000, + |_| {}, + 5_000_000, + nonce_starting_balance, + None, + feature_set, + ) + .unwrap(); + let alice_keypair = Keypair::new(); + let alice_pubkey = alice_keypair.pubkey(); + let custodian_pubkey = custodian_keypair.pubkey(); + let nonce_pubkey = nonce_keypair.pubkey(); + + debug!("alice: {}", alice_pubkey); + debug!("custodian: {}", custodian_pubkey); + debug!("nonce: {}", nonce_pubkey); + debug!("nonce account: {:?}", bank.get_account(&nonce_pubkey)); + debug!("cust: {:?}", bank.get_account(&custodian_pubkey)); + let nonce_hash = get_nonce_blockhash(&bank, &nonce_pubkey).unwrap(); + + for _ in 0..MAX_RECENT_BLOCKHASHES + 1 { + goto_end_of_slot(Arc::get_mut(&mut bank).unwrap()); + bank = Arc::new(new_from_parent(&bank)); + } + + let nonce_tx = Transaction::new_signed_with_payer( + &[ + system_instruction::advance_nonce_account(&nonce_pubkey, &nonce_pubkey), + system_instruction::transfer(&custodian_pubkey, &alice_pubkey, 100_000_000), + ], + Some(&nonce_pubkey), + &[&custodian_keypair, &nonce_keypair], + nonce_hash, + ); + debug!("{:?}", nonce_tx); + + assert_eq!( + bank.process_transaction(&nonce_tx), + Err(TransactionError::InstructionError( + 1, + system_instruction::SystemError::ResultWithNegativeLamports.into(), + )) + ); + /* Check fee charged and nonce has advanced */ + let mut recent_message = nonce_tx.message; + recent_message.recent_blockhash = bank.last_blockhash(); + assert_eq!( + bank.get_balance(&nonce_pubkey), + nonce_starting_balance + - bank + .get_fee_for_message(&recent_message.try_into().unwrap()) + .unwrap() + ); + assert_ne!( + nonce_hash, + get_nonce_blockhash(&bank, &nonce_pubkey).unwrap() + ); + } + #[test] fn test_nonce_fee_calculator_updates() { let (mut genesis_config, mint_keypair) = create_genesis_config(1_000_000); genesis_config.rent.lamports_per_byte_year = 0; - let mut bank = Arc::new(Bank::new_for_tests(&genesis_config)); + let mut bank = Bank::new_for_tests(&genesis_config); + bank.feature_set = Arc::new(FeatureSet::all_enabled()); + bank.deactivate_feature(&tx_wide_compute_cap::id()); + let mut bank = Arc::new(bank); + + // Deliberately use bank 0 to initialize nonce account, so that nonce account fee_calculator indicates 0 fees + let (custodian_keypair, nonce_keypair) = + nonce_setup(&mut bank, &mint_keypair, 500_000, 100_000, None).unwrap(); + let custodian_pubkey = custodian_keypair.pubkey(); + let nonce_pubkey = nonce_keypair.pubkey(); + + // Grab the hash and fee_calculator stored in the nonce account + let (stored_nonce_hash, stored_fee_calculator) = bank + .get_account(&nonce_pubkey) + .and_then(|acc| { + let state = + StateMut::::state(&acc).map(|v| v.convert_to_current()); + match state { + Ok(nonce::State::Initialized(ref data)) => { + Some((data.blockhash, data.fee_calculator.clone())) + } + _ => None, + } + }) + .unwrap(); + + // Kick nonce hash off the blockhash_queue + for _ in 0..MAX_RECENT_BLOCKHASHES + 1 { + goto_end_of_slot(Arc::get_mut(&mut bank).unwrap()); + bank = Arc::new(new_from_parent(&bank)); + } + + // Nonce transfer + let nonce_tx = Transaction::new_signed_with_payer( + &[ + system_instruction::advance_nonce_account(&nonce_pubkey, &nonce_pubkey), + system_instruction::transfer( + &custodian_pubkey, + &solana_sdk::pubkey::new_rand(), + 100_000, + ), + ], + Some(&custodian_pubkey), + &[&custodian_keypair, &nonce_keypair], + stored_nonce_hash, + ); + bank.process_transaction(&nonce_tx).unwrap(); + + // Grab the new hash and fee_calculator; both should be updated + let (nonce_hash, fee_calculator) = bank + .get_account(&nonce_pubkey) + .and_then(|acc| { + let state = + StateMut::::state(&acc).map(|v| v.convert_to_current()); + match state { + Ok(nonce::State::Initialized(ref data)) => { + Some((data.blockhash, data.fee_calculator.clone())) + } + _ => None, + } + }) + .unwrap(); + + assert_ne!(stored_nonce_hash, nonce_hash); + assert_ne!(stored_fee_calculator, fee_calculator); + } + + #[test] + fn test_nonce_fee_calculator_updates_tx_wide_cap() { + let (mut genesis_config, mint_keypair) = create_genesis_config(1_000_000); + genesis_config.rent.lamports_per_byte_year = 0; + let mut bank = Bank::new_for_tests(&genesis_config); + bank.feature_set = Arc::new(FeatureSet::all_enabled()); + let mut bank = Arc::new(bank); // Deliberately use bank 0 to initialize nonce account, so that nonce account fee_calculator indicates 0 fees let (custodian_keypair, nonce_keypair) = @@ -11667,8 +12296,11 @@ pub(crate) mod tests { #[test] fn test_check_ro_durable_nonce_fails() { + let mut feature_set = FeatureSet::all_enabled(); + feature_set.deactivate(&tx_wide_compute_cap::id()); let (mut bank, _mint_keypair, custodian_keypair, nonce_keypair) = - setup_nonce_with_bank(10_000_000, |_| {}, 5_000_000, 250_000, None).unwrap(); + setup_nonce_with_bank(10_000_000, |_| {}, 5_000_000, 250_000, None, feature_set) + .unwrap(); Arc::get_mut(&mut bank) .unwrap() .activate_feature(&feature_set::nonce_must_be_writable::id()); @@ -15136,7 +15768,7 @@ pub(crate) mod tests { let message = Message::new( &[ - ComputeBudgetInstruction::request_units(1), + ComputeBudgetInstruction::request_units(1, 0), ComputeBudgetInstruction::request_heap_frame(48 * 1024), Instruction::new_with_bincode(program_id, &0, vec![]), ], @@ -15181,7 +15813,7 @@ pub(crate) mod tests { let message = Message::new( &[ - ComputeBudgetInstruction::request_units(1), + ComputeBudgetInstruction::request_units(1, 0), ComputeBudgetInstruction::request_heap_frame(48 * 1024), Instruction::new_with_bincode(program_id, &0, vec![]), ], @@ -15352,10 +15984,16 @@ pub(crate) mod tests { // Default: no fee. let message = SanitizedMessage::try_from(Message::new(&[], Some(&Pubkey::new_unique()))).unwrap(); - assert_eq!(Bank::calculate_fee(&message, 0), 0); + assert_eq!( + Bank::calculate_fee(&message, 0, &FeeStructure::default(), false), + 0 + ); // One signature, a fee. - assert_eq!(Bank::calculate_fee(&message, 1), 1); + assert_eq!( + Bank::calculate_fee(&message, 1, &FeeStructure::default(), false), + 1 + ); // Two signatures, double the fee. let key0 = Pubkey::new_unique(); @@ -15363,7 +16001,68 @@ pub(crate) mod tests { let ix0 = system_instruction::transfer(&key0, &key1, 1); let ix1 = system_instruction::transfer(&key1, &key0, 1); let message = SanitizedMessage::try_from(Message::new(&[ix0, ix1], Some(&key0))).unwrap(); - assert_eq!(Bank::calculate_fee(&message, 2), 4); + assert_eq!( + Bank::calculate_fee(&message, 2, &FeeStructure::default(), false), + 4 + ); + } + + #[test] + fn test_calculate_fee_compute_units() { + let fee_structure = FeeStructure::default(); + let max_fee = fee_structure.compute_fee_bins.last().unwrap().fee; + let lamports_per_signature = fee_structure.lamports_per_signature; + + // One signature, no unit request + + let message = + SanitizedMessage::try_from(Message::new(&[], Some(&Pubkey::new_unique()))).unwrap(); + assert_eq!( + Bank::calculate_fee(&message, 1, &fee_structure, true), + max_fee + lamports_per_signature + ); + + // Three signatures, two instructions, no unit request + + let ix0 = system_instruction::transfer(&Pubkey::new_unique(), &Pubkey::new_unique(), 1); + let ix1 = system_instruction::transfer(&Pubkey::new_unique(), &Pubkey::new_unique(), 1); + let message = + SanitizedMessage::try_from(Message::new(&[ix0, ix1], Some(&Pubkey::new_unique()))) + .unwrap(); + assert_eq!( + Bank::calculate_fee(&message, 1, &fee_structure, true), + max_fee + 3 * lamports_per_signature + ); + + // Explicit fee schedule + + let expected_fee_structure = &[ + // (units requested, fee in SOL), + (0, 0.0), + (5_000, 0.0), + (10_000, 0.0), + (100_000, 0.0), + (300_000, 0.0), + (500_000, 0.0), + (700_000, 0.0), + (900_000, 0.0), + (1_100_000, 0.0), + (1_300_000, 0.0), + (1_500_000, 0.0), // ComputeBudget capped + ]; + for pair in expected_fee_structure.iter() { + const ADDITIONAL_FEE: u64 = 42; + let ix0 = ComputeBudgetInstruction::request_units(pair.0, ADDITIONAL_FEE as u32); + let ix1 = Instruction::new_with_bincode(Pubkey::new_unique(), &0, vec![]); + let message = + SanitizedMessage::try_from(Message::new(&[ix0, ix1], Some(&Pubkey::new_unique()))) + .unwrap(); + let fee = Bank::calculate_fee(&message, 1, &fee_structure, true); + assert_eq!( + fee, + sol_to_lamports(pair.1) + lamports_per_signature + ADDITIONAL_FEE + ); + } } #[test] @@ -15392,7 +16091,10 @@ pub(crate) mod tests { Some(&key0), )) .unwrap(); - assert_eq!(Bank::calculate_fee(&message, 1), 2); + assert_eq!( + Bank::calculate_fee(&message, 1, &FeeStructure::default(), false), + 2 + ); secp_instruction1.data = vec![0]; secp_instruction2.data = vec![10]; @@ -15401,7 +16103,10 @@ pub(crate) mod tests { Some(&key0), )) .unwrap(); - assert_eq!(Bank::calculate_fee(&message, 1), 11); + assert_eq!( + Bank::calculate_fee(&message, 1, &FeeStructure::default(), false), + 11 + ); } #[test] @@ -15812,9 +16517,12 @@ pub(crate) mod tests { &mut error_counters, &bank.rent_collector, &bank.feature_set, + &FeeStructure::default(), ); - let compute_budget = bank.compute_budget.unwrap_or_else(ComputeBudget::new); + let compute_budget = bank + .compute_budget + .unwrap_or_else(|| ComputeBudget::new(false)); let transaction_context = TransactionContext::new( loaded_txs[0].0.as_ref().unwrap().accounts.clone(), compute_budget.max_invoke_depth.saturating_add(1), diff --git a/runtime/src/message_processor.rs b/runtime/src/message_processor.rs index 1805630e04..4d4645075c 100644 --- a/runtime/src/message_processor.rs +++ b/runtime/src/message_processor.rs @@ -272,7 +272,7 @@ mod tests { None, executors.clone(), Arc::new(FeatureSet::all_enabled()), - ComputeBudget::new(), + ComputeBudget::default(), &mut ExecuteTimings::default(), &sysvar_cache, Hash::default(), @@ -314,7 +314,7 @@ mod tests { None, executors.clone(), Arc::new(FeatureSet::all_enabled()), - ComputeBudget::new(), + ComputeBudget::default(), &mut ExecuteTimings::default(), &sysvar_cache, Hash::default(), @@ -346,7 +346,7 @@ mod tests { None, executors, Arc::new(FeatureSet::all_enabled()), - ComputeBudget::new(), + ComputeBudget::default(), &mut ExecuteTimings::default(), &sysvar_cache, Hash::default(), @@ -481,7 +481,7 @@ mod tests { None, executors.clone(), Arc::new(FeatureSet::all_enabled()), - ComputeBudget::new(), + ComputeBudget::default(), &mut ExecuteTimings::default(), &sysvar_cache, Hash::default(), @@ -514,7 +514,7 @@ mod tests { None, executors.clone(), Arc::new(FeatureSet::all_enabled()), - ComputeBudget::new(), + ComputeBudget::default(), &mut ExecuteTimings::default(), &sysvar_cache, Hash::default(), @@ -544,7 +544,7 @@ mod tests { None, executors, Arc::new(FeatureSet::all_enabled()), - ComputeBudget::new(), + ComputeBudget::default(), &mut ExecuteTimings::default(), &sysvar_cache, Hash::default(), @@ -623,7 +623,7 @@ mod tests { None, Rc::new(RefCell::new(Executors::default())), Arc::new(FeatureSet::all_enabled()), - ComputeBudget::new(), + ComputeBudget::default(), &mut ExecuteTimings::default(), &sysvar_cache, Hash::default(), diff --git a/sdk/src/compute_budget.rs b/sdk/src/compute_budget.rs index 0662c83c35..314a05bc4d 100644 --- a/sdk/src/compute_budget.rs +++ b/sdk/src/compute_budget.rs @@ -2,38 +2,49 @@ use { crate::instruction::Instruction, - borsh::{BorshDeserialize, BorshSchema, BorshSerialize}, + borsh::{BorshDeserialize, BorshSerialize}, }; crate::declare_id!("ComputeBudget111111111111111111111111111111"); /// Compute Budget Instructions #[derive( - Serialize, - Deserialize, - BorshSerialize, - BorshDeserialize, - BorshSchema, - Debug, - Clone, - PartialEq, AbiExample, AbiEnumVisitor, + BorshDeserialize, + BorshSerialize, + Clone, + Debug, + Deserialize, + PartialEq, + Serialize, )] pub enum ComputeBudgetInstruction { /// Request a specific maximum number of compute units the transaction is - /// allowed to consume. - RequestUnits(u32), - /// Request a specific transaction-wide program heap frame size in bytes. - /// The value requested must be a multiple of 1024. This new heap frame size - /// applies to each program executed, including all calls to CPIs. + /// allowed to consume and an additional fee to pay. + RequestUnits { + /// Units to request + units: u32, + /// Additional fee to add + additional_fee: u32, + }, + /// Request a specific transaction-wide program heap region size in bytes. + /// The value requested must be a multiple of 1024. This new heap region + /// size applies to each program executed, including all calls to CPIs. RequestHeapFrame(u32), } impl ComputeBudgetInstruction { /// Create a `ComputeBudgetInstruction::RequestUnits` `Instruction` - pub fn request_units(units: u32) -> Instruction { - Instruction::new_with_borsh(id(), &ComputeBudgetInstruction::RequestUnits(units), vec![]) + pub fn request_units(units: u32, additional_fee: u32) -> Instruction { + Instruction::new_with_borsh( + id(), + &ComputeBudgetInstruction::RequestUnits { + units, + additional_fee, + }, + vec![], + ) } /// Create a `ComputeBudgetInstruction::RequestHeapFrame` `Instruction` diff --git a/sdk/src/fee.rs b/sdk/src/fee.rs new file mode 100644 index 0000000000..69428a3db1 --- /dev/null +++ b/sdk/src/fee.rs @@ -0,0 +1,67 @@ +use crate::native_token::sol_to_lamports; + +/// A fee and its associated compute unit limit +#[derive(Debug, Default, Clone)] +pub struct FeeBin { + /// maximum compute units for which this fee will be charged + pub limit: u64, + /// fee in lamports + pub fee: u64, +} + +/// Information used to calculate fees +#[derive(Debug, Clone)] +pub struct FeeStructure { + /// lamports per signature + pub lamports_per_signature: u64, + /// lamports_per_write_lock + pub lamports_per_write_lock: u64, + /// Compute unit fee bins + pub compute_fee_bins: Vec, +} + +impl FeeStructure { + pub fn new( + sol_per_signature: f64, + sol_per_write_lock: f64, + compute_fee_bins: Vec<(u64, f64)>, + ) -> Self { + let compute_fee_bins = compute_fee_bins + .iter() + .map(|(limit, sol)| FeeBin { + limit: *limit, + fee: sol_to_lamports(*sol), + }) + .collect::>(); + FeeStructure { + lamports_per_signature: sol_to_lamports(sol_per_signature), + lamports_per_write_lock: sol_to_lamports(sol_per_write_lock), + compute_fee_bins, + } + } + + pub fn get_max_fee(&self, num_signatures: u64, num_write_locks: u64) -> u64 { + num_signatures + .saturating_mul(self.lamports_per_signature) + .saturating_add(num_write_locks.saturating_mul(self.lamports_per_write_lock)) + .saturating_add( + self.compute_fee_bins + .last() + .map(|bin| bin.fee) + .unwrap_or_default(), + ) + } +} + +impl Default for FeeStructure { + fn default() -> Self { + Self::new(0.000005, 0.0, vec![(1_400_000, 0.0)]) + } +} + +#[cfg(RUSTC_WITH_SPECIALIZATION)] +impl ::solana_frozen_abi::abi_example::AbiExample for FeeStructure { + fn example() -> Self { + FeeStructure::default() + } +} diff --git a/sdk/src/lib.rs b/sdk/src/lib.rs index d51f03fb10..bc3c65dcbd 100644 --- a/sdk/src/lib.rs +++ b/sdk/src/lib.rs @@ -25,6 +25,7 @@ pub mod example_mocks; pub mod exit; pub mod feature; pub mod feature_set; +pub mod fee; pub mod genesis_config; pub mod hard_forks; pub mod hash; diff --git a/tokens/src/commands.rs b/tokens/src/commands.rs index d282f64776..76148a1d15 100644 --- a/tokens/src/commands.rs +++ b/tokens/src/commands.rs @@ -1585,13 +1585,10 @@ mod tests { #[test] fn test_check_payer_balances_distribute_tokens_single_payer() { - let fees = 10_000; - let fees_in_sol = lamports_to_sol(fees); - let alice = Keypair::new(); let test_validator = TestValidator::with_custom_fees( alice.pubkey(), - fees, + 10_000, None, SocketAddrSpace::Unspecified, ); @@ -1601,6 +1598,11 @@ mod tests { let sender_keypair_file = tmp_file_path("keypair_file", &alice.pubkey()); write_keypair_file(&alice, &sender_keypair_file).unwrap(); + let fees = client + .get_fee_for_message(&one_signer_message(&client)) + .unwrap(); + let fees_in_sol = lamports_to_sol(fees); + let allocation_amount = 1000.0; // Fully funded payer @@ -1678,12 +1680,10 @@ mod tests { #[test] fn test_check_payer_balances_distribute_tokens_separate_payers() { - let fees = 10_000; - let fees_in_sol = lamports_to_sol(fees); let alice = Keypair::new(); let test_validator = TestValidator::with_custom_fees( alice.pubkey(), - fees, + 10_000, None, SocketAddrSpace::Unspecified, ); @@ -1691,6 +1691,11 @@ mod tests { let client = RpcClient::new_with_commitment(url, CommitmentConfig::processed()); + let fees = client + .get_fee_for_message(&one_signer_message(&client)) + .unwrap(); + let fees_in_sol = lamports_to_sol(fees); + let sender_keypair_file = tmp_file_path("keypair_file", &alice.pubkey()); write_keypair_file(&alice, &sender_keypair_file).unwrap(); @@ -1802,18 +1807,21 @@ mod tests { #[test] fn test_check_payer_balances_distribute_stakes_single_payer() { - let fees = 10_000; - let fees_in_sol = lamports_to_sol(fees); let alice = Keypair::new(); let test_validator = TestValidator::with_custom_fees( alice.pubkey(), - fees, + 10_000, None, SocketAddrSpace::Unspecified, ); let url = test_validator.rpc_url(); let client = RpcClient::new_with_commitment(url, CommitmentConfig::processed()); + let fees = client + .get_fee_for_message(&one_signer_message(&client)) + .unwrap(); + let fees_in_sol = lamports_to_sol(fees); + let sender_keypair_file = tmp_file_path("keypair_file", &alice.pubkey()); write_keypair_file(&alice, &sender_keypair_file).unwrap(); @@ -1925,12 +1933,10 @@ mod tests { #[test] fn test_check_payer_balances_distribute_stakes_separate_payers() { - let fees = 10_000; - let fees_in_sol = lamports_to_sol(fees); let alice = Keypair::new(); let test_validator = TestValidator::with_custom_fees( alice.pubkey(), - fees, + 10_000, None, SocketAddrSpace::Unspecified, ); @@ -1938,6 +1944,11 @@ mod tests { let client = RpcClient::new_with_commitment(url, CommitmentConfig::processed()); + let fees = client + .get_fee_for_message(&one_signer_message(&client)) + .unwrap(); + let fees_in_sol = lamports_to_sol(fees); + let sender_keypair_file = tmp_file_path("keypair_file", &alice.pubkey()); write_keypair_file(&alice, &sender_keypair_file).unwrap();