From e4d8ea11ac27837a2f27b0fc68bca5c256bb9c17 Mon Sep 17 00:00:00 2001 From: Michael Vines Date: Mon, 10 Jun 2019 22:18:32 -0700 Subject: [PATCH] Make lamports_per_signature dynamic based on cluster load (#4562) * Make lamports_per_signature dynamic based on cluster load * Move transaction-fees.md to implemented --- Cargo.lock | 1 + book/src/SUMMARY.md | 10 +- book/src/transaction-fees.md | 16 +-- core/src/rpc.rs | 28 +++-- core/src/storage_stage.rs | 2 +- genesis/src/main.rs | 40 +++++-- multinode-demo/setup.sh | 3 +- run.sh | 3 +- runtime/src/accounts.rs | 20 ++-- runtime/src/bank.rs | 204 ++++++++++++++++++++++++--------- runtime/src/bank_client.rs | 4 +- runtime/src/blockhash_queue.rs | 27 +++-- sdk/Cargo.toml | 1 + sdk/src/fee_calculator.rs | 147 +++++++++++++++++++++++- 14 files changed, 391 insertions(+), 115 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index a435eb5e68..c36425350e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2681,6 +2681,7 @@ dependencies = [ "serde_json 1.0.39 (registry+https://github.com/rust-lang/crates.io-index)", "sha2 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)", "solana-ed25519-dalek 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)", + "solana-logger 0.16.0", "untrusted 0.6.2 (registry+https://github.com/rust-lang/crates.io-index)", ] diff --git a/book/src/SUMMARY.md b/book/src/SUMMARY.md index 93c41e06d2..70905bc4fe 100644 --- a/book/src/SUMMARY.md +++ b/book/src/SUMMARY.md @@ -55,15 +55,15 @@ - [References](ed_references.md) - [Cluster Test Framework](cluster-test-framework.md) - [Credit-only Accounts](credit-only-credit-debit-accounts.md) - - [Deterministic Transaction Fees](transaction-fees.md) - [Validator](validator-proposal.md) - [Implemented Design Proposals](implemented-proposals.md) + - [Cluster Software Installation and Updates](installer.md) + - [Deterministic Transaction Fees](transaction-fees.md) - [Fork Selection](fork-selection.md) - [Leader-to-Leader Transition](leader-leader-transition.md) - [Leader-to-Validator Transition](leader-validator-transition.md) - - [Testing Programs](testing-programs.md) - - [Reliable Vote Transmission](reliable-vote-transmission.md) - - [Persistent Account Storage](persistent-account-storage.md) - - [Cluster Software Installation and Updates](installer.md) - [Passive Stake Delegation and Rewards](passive-stake-delegation-and-rewards.md) + - [Persistent Account Storage](persistent-account-storage.md) + - [Reliable Vote Transmission](reliable-vote-transmission.md) + - [Testing Programs](testing-programs.md) diff --git a/book/src/transaction-fees.md b/book/src/transaction-fees.md index ccefd89a41..f78ccfb801 100644 --- a/book/src/transaction-fees.md +++ b/book/src/transaction-fees.md @@ -8,17 +8,14 @@ client won't know how much was collected until the transaction is confirmed by the cluster and the remaining balance is checked. It smells of exactly what we dislike about Ethereum's "gas", non-determinism. -## Implementation Status - -This design is not yet implemented, but is written as though it has been. Once -implemented, delete this comment. - ### Congestion-driven fees Each validator uses *signatures per slot* (SPS) to estimate network congestion and *SPS target* to estimate the desired processing capacity of the cluster. The validator learns the SPS target from the genesis block, whereas it -calculates SPS from the ledger data in the previous epoch. +calculates SPS from recently processed transactions. The genesis block also +defines a target `lamports_per_signature`, which is the fee to charge per +signature when the cluster is operating at *SPS target*. ### Calculating fees @@ -37,8 +34,11 @@ lamports as returned by the fee calculator. In the first implementation of this design, the only fee parameter is `lamports_per_signature`. The more signatures the cluster needs to verify, the higher the fee. The exact number of lamports is determined by the ratio of SPS -to the SPS target. The cluster lowers `lamports_per_signature` when SPS is -below the target and raises it when at or above the target. +to the SPS target. At the end of each slot, the cluster lowers +`lamports_per_signature` when SPS is below the target and raises it when above +the target. The minimum value for `lamports_per_signature` is 50% of the target +`lamports_per_signature` and the maximum value is 10x the target +`lamports_per_signature' Future parameters might include: diff --git a/core/src/rpc.rs b/core/src/rpc.rs index 2bf08cb622..e5e1e11c9e 100644 --- a/core/src/rpc.rs +++ b/core/src/rpc.rs @@ -75,10 +75,8 @@ impl JsonRpcRequestProcessor { } fn get_recent_blockhash(&self) -> (String, FeeCalculator) { - ( - self.bank().confirmed_last_blockhash().to_string(), - self.bank().fee_calculator.clone(), - ) + let (blockhash, fee_calculator) = self.bank().confirmed_last_blockhash(); + (blockhash.to_string(), fee_calculator) } pub fn get_signature_status(&self, signature: Signature) -> Option> { @@ -358,7 +356,8 @@ impl RpcSol for RpcSolImpl { .read() .unwrap() .bank() - .confirmed_last_blockhash(); + .confirmed_last_blockhash() + .0; let transaction = request_airdrop_transaction(&drone_addr, &pubkey, lamports, blockhash) .map_err(|err| { info!("request_airdrop_transaction failed: {:?}", err); @@ -493,7 +492,7 @@ mod tests { let bank = bank_forks.read().unwrap().working_bank(); let exit = Arc::new(AtomicBool::new(false)); - let blockhash = bank.confirmed_last_blockhash(); + let blockhash = bank.confirmed_last_blockhash().0; let tx = system_transaction::transfer(&alice, pubkey, 20, blockhash); bank.process_transaction(&tx).expect("process transaction"); @@ -536,7 +535,7 @@ mod tests { &exit, ); thread::spawn(move || { - let blockhash = bank.confirmed_last_blockhash(); + let blockhash = bank.confirmed_last_blockhash().0; let tx = system_transaction::transfer(&alice, &bob_pubkey, 20, blockhash); bank.process_transaction(&tx).expect("process transaction"); }) @@ -731,12 +730,17 @@ mod tests { let req = format!(r#"{{"jsonrpc":"2.0","id":1,"method":"getRecentBlockhash"}}"#); let res = io.handle_request_sync(&req, meta); - let expected = format!( - r#"{{"jsonrpc":"2.0","result":["{}", {{"lamportsPerSignature": 0}}],"id":1}}"#, - blockhash - ); + let expected = json!({ + "jsonrpc": "2.0", + "result": [ blockhash.to_string(), { + "lamportsPerSignature": 0, + "targetLamportsPerSignature": 0, + "targetSignaturesPerSlot": 0 + }], + "id": 1 + }); let expected: Response = - serde_json::from_str(&expected).expect("expected response deserialization"); + serde_json::from_value(expected).expect("expected response deserialization"); let result: Response = serde_json::from_str(&res.expect("actual response")) .expect("actual response deserialization"); assert_eq!(expected, result); diff --git a/core/src/storage_stage.rs b/core/src/storage_stage.rs index bcdead33de..45c1128927 100644 --- a/core/src/storage_stage.rs +++ b/core/src/storage_stage.rs @@ -253,7 +253,7 @@ impl StorageStage { transactions_socket: &UdpSocket, ) -> io::Result<()> { let working_bank = bank_forks.read().unwrap().working_bank(); - let blockhash = working_bank.confirmed_last_blockhash(); + let blockhash = working_bank.confirmed_last_blockhash().0; let keypair_balance = working_bank.get_balance(&keypair.pubkey()); if keypair_balance == 0 { diff --git a/genesis/src/main.rs b/genesis/src/main.rs index 0eadf946a6..4f3c007a3c 100644 --- a/genesis/src/main.rs +++ b/genesis/src/main.rs @@ -53,8 +53,12 @@ pub fn append_primordial_accounts(file: &str, genesis_block: &mut GenesisBlock) fn main() -> Result<(), Box> { let default_bootstrap_leader_lamports = &BOOTSTRAP_LEADER_LAMPORTS.to_string(); - let default_lamports_per_signature = - &FeeCalculator::default().lamports_per_signature.to_string(); + let default_target_lamports_per_signature = &FeeCalculator::default() + .target_lamports_per_signature + .to_string(); + let default_target_signatures_per_slot = &FeeCalculator::default() + .target_signatures_per_slot + .to_string(); let default_target_tick_duration = &timing::duration_as_ms(&PohConfig::default().target_tick_duration).to_string(); let default_ticks_per_slot = &timing::DEFAULT_TICKS_PER_SLOT.to_string(); @@ -144,12 +148,28 @@ fn main() -> Result<(), Box> { .help("Number of lamports to assign to the bootstrap leader's stake account"), ) .arg( - Arg::with_name("lamports_per_signature") - .long("lamports-per-signature") + Arg::with_name("target_lamports_per_signature") + .long("target-lamports-per-signature") .value_name("LAMPORTS") .takes_value(true) - .default_value(default_lamports_per_signature) - .help("Number of lamports the cluster will charge for signature verification"), + .default_value(default_target_lamports_per_signature) + .help( + "The cost in lamports that the cluster will charge for signature \ + verification when the cluster is operating at target-signatures-per-slot", + ), + ) + .arg( + Arg::with_name("target_signatures_per_slot") + .long("target-signatures-per-slot") + .value_name("NUMBER") + .takes_value(true) + .default_value(default_target_signatures_per_slot) + .help( + "Used to estimate the desired processing capacity of the cluster. + When the latest slot processes fewer/greater signatures than this \ + value, the lamports-per-signature fee will decrease/increase for \ + the next slot. A value of 0 disables signature-based fee adjustments", + ), ) .arg( Arg::with_name("target_tick_duration") @@ -266,8 +286,12 @@ fn main() -> Result<(), Box> { &bootstrap_storage_keypair.pubkey(), ); - genesis_block.fee_calculator.lamports_per_signature = - value_t_or_exit!(matches, "lamports_per_signature", u64); + genesis_block.fee_calculator.target_lamports_per_signature = + value_t_or_exit!(matches, "target_lamports_per_signature", u64); + genesis_block.fee_calculator.target_signatures_per_slot = + value_t_or_exit!(matches, "target_signatures_per_slot", usize); + genesis_block.fee_calculator = FeeCalculator::new_derived(&genesis_block.fee_calculator, 0); + genesis_block.ticks_per_slot = value_t_or_exit!(matches, "ticks_per_slot", u64); genesis_block.slots_per_epoch = value_t_or_exit!(matches, "slots_per_epoch", u64); genesis_block.poh_config.target_tick_duration = diff --git a/multinode-demo/setup.sh b/multinode-demo/setup.sh index 1a1d187fa3..b93cbea802 100755 --- a/multinode-demo/setup.sh +++ b/multinode-demo/setup.sh @@ -23,7 +23,8 @@ default_arg --ledger "$SOLANA_RSYNC_CONFIG_DIR"/ledger default_arg --mint "$SOLANA_CONFIG_DIR"/mint-keypair.json default_arg --lamports 100000000000000 default_arg --bootstrap-leader-lamports 424242 -default_arg --lamports-per-signature 1 +default_arg --target-lamports-per-signature 42 +default_arg --target-signatures-per-slot 42 default_arg --hashes-per-tick auto $solana_genesis "${args[@]}" diff --git a/run.sh b/run.sh index 91df02d0c8..3050d6ac79 100755 --- a/run.sh +++ b/run.sh @@ -71,7 +71,8 @@ leaderVoteAccountPubkey=$(\ solana-genesis \ --lamports 1000000000 \ --bootstrap-leader-lamports 10000000 \ - --lamports-per-signature 1 \ + --target-lamports-per-signature 42 \ + --target-signatures-per-slot 42 \ --hashes-per-tick sleep \ --mint "$dataDir"/drone-keypair.json \ --bootstrap-leader-keypair "$dataDir"/leader-keypair.json \ diff --git a/runtime/src/accounts.rs b/runtime/src/accounts.rs index 70ca1d7226..531a20e34e 100644 --- a/runtime/src/accounts.rs +++ b/runtime/src/accounts.rs @@ -4,13 +4,13 @@ use crate::accounts_db::{ }; use crate::accounts_index::{AccountsIndex, Fork}; use crate::append_vec::StoredAccount; +use crate::blockhash_queue::BlockhashQueue; use crate::message_processor::has_duplicates; use bincode::serialize; use log::*; use serde::{Deserialize, Serialize}; use solana_metrics::inc_new_counter_error; use solana_sdk::account::{Account, LamportCredit}; -use solana_sdk::fee_calculator::FeeCalculator; use solana_sdk::hash::{Hash, Hasher}; use solana_sdk::native_loader; use solana_sdk::pubkey::Pubkey; @@ -239,7 +239,7 @@ impl Accounts { ancestors: &HashMap, txs: &[Transaction], lock_results: Vec>, - fee_calculator: &FeeCalculator, + hash_queue: &BlockhashQueue, error_counters: &mut ErrorCounters, ) -> Vec> { //PERF: hold the lock to scan for the references, but not to clone the accounts @@ -250,6 +250,10 @@ impl Accounts { .zip(lock_results.into_iter()) .map(|etx| match etx { (tx, Ok(())) => { + let fee_calculator = hash_queue + .get_fee_calculator(&tx.message().recent_blockhash) + .ok_or(TransactionError::BlockhashNotFound)?; + let fee = fee_calculator.calculate_fee(tx.message()); let (accounts, credits) = Self::load_tx_accounts( &storage, @@ -484,6 +488,7 @@ mod tests { use bincode::{deserialize_from, serialize_into, serialized_size}; use rand::{thread_rng, Rng}; use solana_sdk::account::Account; + use solana_sdk::fee_calculator::FeeCalculator; use solana_sdk::hash::Hash; use solana_sdk::instruction::CompiledInstruction; use solana_sdk::signature::{Keypair, KeypairUtil}; @@ -497,19 +502,16 @@ mod tests { fee_calculator: &FeeCalculator, error_counters: &mut ErrorCounters, ) -> Vec> { + let mut hash_queue = BlockhashQueue::new(100); + hash_queue.register_hash(&tx.message().recent_blockhash, &fee_calculator); let accounts = Accounts::new(None); for ka in ka.iter() { accounts.store_slow(0, &ka.0, &ka.1); } let ancestors = vec![(0, 0)].into_iter().collect(); - let res = accounts.load_accounts( - &ancestors, - &[tx], - vec![Ok(())], - &fee_calculator, - error_counters, - ); + let res = + accounts.load_accounts(&ancestors, &[tx], vec![Ok(())], &hash_queue, error_counters); res } diff --git a/runtime/src/bank.rs b/runtime/src/bank.rs index 7a6b2560c1..86a162fda3 100644 --- a/runtime/src/bank.rs +++ b/runtime/src/bank.rs @@ -207,6 +207,11 @@ pub struct Bank { #[serde(deserialize_with = "deserialize_atomicusize")] tick_height: AtomicUsize, // TODO: Use AtomicU64 if/when available + /// The number of signatures from valid transactions in this slot + #[serde(serialize_with = "serialize_atomicusize")] + #[serde(deserialize_with = "deserialize_atomicusize")] + signature_count: AtomicUsize, + // Bank max_tick_height max_tick_height: u64, @@ -227,8 +232,8 @@ pub struct Bank { #[serde(deserialize_with = "deserialize_atomicusize")] collector_fees: AtomicUsize, // TODO: Use AtomicU64 if/when available - /// An object to calculate transaction fees. - pub fee_calculator: FeeCalculator, + /// Latest transaction fees for transactions processed by this bank + fee_calculator: FeeCalculator, /// initialized from genesis epoch_schedule: EpochSchedule, @@ -290,7 +295,8 @@ impl Bank { bank.blockhash_queue = RwLock::new(parent.blockhash_queue.read().unwrap().clone()); bank.src.status_cache = parent.src.status_cache.clone(); bank.bank_height = parent.bank_height + 1; - bank.fee_calculator = parent.fee_calculator.clone(); + bank.fee_calculator = + FeeCalculator::new_derived(&parent.fee_calculator, parent.signature_count()); bank.transaction_count .store(parent.transaction_count() as usize, Ordering::Relaxed); @@ -335,7 +341,7 @@ impl Bank { bank.ancestors.insert(p.slot(), i + 1); }); bank.update_current(); - + bank.update_fees(); bank } @@ -478,7 +484,7 @@ impl Bank { self.blockhash_queue .write() .unwrap() - .genesis_hash(&genesis_block.hash()); + .genesis_hash(&genesis_block.hash(), &self.fee_calculator); self.ticks_per_slot = genesis_block.ticks_per_slot; self.max_tick_height = (self.slot + 1) * self.ticks_per_slot - 1; @@ -523,16 +529,27 @@ impl Bank { self.blockhash_queue.read().unwrap().last_hash() } - /// Return a confirmed blockhash with NUM_BLOCKHASH_CONFIRMATIONS - pub fn confirmed_last_blockhash(&self) -> Hash { + pub fn last_blockhash_with_fee_calculator(&self) -> (Hash, FeeCalculator) { + let blockhash_queue = self.blockhash_queue.read().unwrap(); + let last_hash = blockhash_queue.last_hash(); + ( + last_hash, + blockhash_queue + .get_fee_calculator(&last_hash) + .unwrap() + .clone(), + ) + } + + pub fn confirmed_last_blockhash(&self) -> (Hash, FeeCalculator) { const NUM_BLOCKHASH_CONFIRMATIONS: usize = 3; let parents = self.parents(); if parents.is_empty() { - self.last_blockhash() + self.last_blockhash_with_fee_calculator() } else { let index = cmp::min(NUM_BLOCKHASH_CONFIRMATIONS, parents.len() - 1); - parents[index].last_blockhash() + parents[index].last_blockhash_with_fee_calculator() } } @@ -615,7 +632,10 @@ impl Bank { // Register a new block hash if at the last tick in the slot if current_tick_height % self.ticks_per_slot == self.ticks_per_slot - 1 { - self.blockhash_queue.write().unwrap().register_hash(hash); + self.blockhash_queue + .write() + .unwrap() + .register_hash(hash, &self.fee_calculator); } } @@ -661,7 +681,7 @@ impl Bank { &self.ancestors, txs, results, - &self.fee_calculator, + &self.blockhash_queue.read().unwrap(), error_counters, ) } @@ -695,7 +715,7 @@ impl Bank { .zip(lock_results.into_iter()) .map(|(tx, lock_res)| { if lock_res.is_ok() - && !hash_queue.check_hash_age(tx.message().recent_blockhash, max_age) + && !hash_queue.check_hash_age(&tx.message().recent_blockhash, max_age) { error_counters.reserve_blockhash += 1; Err(TransactionError::BlockhashNotFound) @@ -830,14 +850,17 @@ impl Bank { let load_elapsed = now.elapsed(); let now = Instant::now(); + let mut signature_count = 0; let executed: Vec> = loaded_accounts .iter_mut() .zip(txs.iter()) .map(|(accs, tx)| match accs { Err(e) => Err(e.clone()), - Ok((ref mut accounts, ref mut loaders, ref mut credits)) => self - .message_processor - .process_message(tx.message(), loaders, accounts, credits), + Ok((ref mut accounts, ref mut loaders, ref mut credits)) => { + signature_count += tx.message().header.num_required_signatures as usize; + self.message_processor + .process_message(tx.message(), loaders, accounts, credits) + } }) .collect(); @@ -873,8 +896,10 @@ impl Bank { } self.increment_transaction_count(tx_count); + self.increment_signature_count(signature_count); inc_new_counter_info!("bank-process_transactions-txs", tx_count, 0, 1000); + inc_new_counter_info!("bank-process_transactions-sigs", signature_count, 0, 1000); Self::update_error_counters(&error_counters); (loaded_accounts, executed) } @@ -884,12 +909,17 @@ impl Bank { txs: &[Transaction], executed: &[Result<()>], ) -> Vec> { + let hash_queue = self.blockhash_queue.read().unwrap(); let mut fees = 0; let results = txs .iter() .zip(executed.iter()) .map(|(tx, res)| { - let fee = self.fee_calculator.calculate_fee(tx.message()); + let fee_calculator = hash_queue + .get_fee_calculator(&tx.message().recent_blockhash) + .ok_or(TransactionError::BlockhashNotFound)?; + let fee = fee_calculator.calculate_fee(tx.message()); + let message = tx.message(); match *res { Err(TransactionError::InstructionError(_, _)) => { @@ -1069,11 +1099,21 @@ impl Bank { pub fn transaction_count(&self) -> u64 { self.transaction_count.load(Ordering::Relaxed) as u64 } + fn increment_transaction_count(&self, tx_count: usize) { self.transaction_count .fetch_add(tx_count, Ordering::Relaxed); } + pub fn signature_count(&self) -> usize { + self.signature_count.load(Ordering::Relaxed) + } + + fn increment_signature_count(&self, signature_count: usize) { + self.signature_count + .fetch_add(signature_count, Ordering::Relaxed); + } + pub fn get_signature_confirmation_status( &self, signature: &Signature, @@ -1381,9 +1421,9 @@ mod tests { // This test demonstrates that fees are paid even when a program fails. #[test] fn test_detect_failed_duplicate_transactions() { - let (genesis_block, mint_keypair) = create_genesis_block(2); - let mut bank = Bank::new(&genesis_block); - bank.fee_calculator.lamports_per_signature = 1; + let (mut genesis_block, mint_keypair) = create_genesis_block(2); + genesis_block.fee_calculator.lamports_per_signature = 1; + let bank = Bank::new(&genesis_block); let dest = Keypair::new(); @@ -1496,67 +1536,123 @@ mod tests { assert_eq!(bank.get_balance(&key.pubkey()), 1); } + fn goto_end_of_slot(bank: &mut Bank) { + let mut tick_hash = bank.last_blockhash(); + loop { + tick_hash = extend_and_hash(&tick_hash, &[42]); + bank.register_tick(&tick_hash); + if tick_hash == bank.last_blockhash() { + bank.freeze(); + return; + } + } + } + #[test] fn test_bank_tx_fee() { let leader = Pubkey::new_rand(); let GenesisBlockInfo { - genesis_block, + mut genesis_block, mint_keypair, .. } = create_genesis_block_with_leader(100, &leader, 3); + genesis_block.fee_calculator.lamports_per_signature = 3; let mut bank = Bank::new(&genesis_block); - bank.fee_calculator.lamports_per_signature = 3; - - let key1 = Keypair::new(); - let key2 = Keypair::new(); + let key = Keypair::new(); let tx = - system_transaction::transfer(&mint_keypair, &key1.pubkey(), 2, genesis_block.hash()); + system_transaction::transfer(&mint_keypair, &key.pubkey(), 2, bank.last_blockhash()); let initial_balance = bank.get_balance(&leader); assert_eq!(bank.process_transaction(&tx), Ok(())); + assert_eq!(bank.get_balance(&key.pubkey()), 2); + assert_eq!(bank.get_balance(&mint_keypair.pubkey()), 100 - 5); + assert_eq!(bank.get_balance(&leader), initial_balance); - assert_eq!(bank.get_balance(&key1.pubkey()), 2); - assert_eq!(bank.get_balance(&mint_keypair.pubkey()), 100 - 5); - bank.freeze(); - assert_eq!(bank.get_balance(&leader), initial_balance + 3); // leader collects fee after the bank is frozen + goto_end_of_slot(&mut bank); + assert_eq!(bank.signature_count(), 1); + assert_eq!(bank.get_balance(&leader), initial_balance + 3); // Leader collects fee after the bank is frozen + // Verify that an InstructionError collects fees, too let mut bank = Bank::new_from_parent(&Arc::new(bank), &leader, 1); - bank.fee_calculator.lamports_per_signature = 1; - let tx = system_transaction::transfer(&key1, &key2.pubkey(), 1, genesis_block.hash()); - - assert_eq!(bank.process_transaction(&tx), Ok(())); - assert_eq!(bank.get_balance(&leader), initial_balance + 3); - assert_eq!(bank.get_balance(&key1.pubkey()), 0); - assert_eq!(bank.get_balance(&key2.pubkey()), 1); - assert_eq!(bank.get_balance(&mint_keypair.pubkey()), 100 - 5); - bank.freeze(); - assert_eq!(bank.get_balance(&leader), initial_balance + 4); // leader collects fee after the bank is frozen - - // verify that an InstructionError collects fees, too - let bank = Bank::new_from_parent(&Arc::new(bank), &leader, 2); let mut tx = - system_transaction::transfer(&mint_keypair, &key2.pubkey(), 1, genesis_block.hash()); - // send a bogus instruction to system_program, cause an instruction error + 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"); // fails with an instruction error - assert_eq!(bank.get_balance(&key2.pubkey()), 1); // our fee --V - assert_eq!(bank.get_balance(&mint_keypair.pubkey()), 100 - 5 - 1); - bank.freeze(); - assert_eq!(bank.get_balance(&leader), initial_balance + 5); // gots our bucks + .expect_err("instruction error"); + assert_eq!(bank.get_balance(&key.pubkey()), 2); // no change + assert_eq!(bank.get_balance(&mint_keypair.pubkey()), 100 - 5 - 3); // 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 + 6); + } + + #[test] + fn test_bank_blockhash_fee_schedule() { + //solana_logger::setup(); + + let leader = Pubkey::new_rand(); + let GenesisBlockInfo { + mut genesis_block, + mint_keypair, + .. + } = create_genesis_block_with_leader(1_000_000, &leader, 3); + genesis_block.fee_calculator.target_lamports_per_signature = 1000; + genesis_block.fee_calculator.target_signatures_per_slot = 1; + + let mut bank = Bank::new(&genesis_block); + goto_end_of_slot(&mut bank); + let (cheap_blockhash, cheap_fee_calculator) = bank.last_blockhash_with_fee_calculator(); + assert_eq!(cheap_fee_calculator.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, expensive_fee_calculator) = + bank.last_blockhash_with_fee_calculator(); + assert!( + cheap_fee_calculator.lamports_per_signature + < expensive_fee_calculator.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); + assert_eq!( + bank.get_balance(&mint_keypair.pubkey()), + initial_mint_balance - 1 - cheap_fee_calculator.lamports_per_signature + ); + + // 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); + assert_eq!( + bank.get_balance(&mint_keypair.pubkey()), + initial_mint_balance - 1 - expensive_fee_calculator.lamports_per_signature + ); } #[test] fn test_filter_program_errors_and_collect_fee() { let leader = Pubkey::new_rand(); let GenesisBlockInfo { - genesis_block, + mut genesis_block, mint_keypair, .. } = create_genesis_block_with_leader(100, &leader, 3); - let mut bank = Bank::new(&genesis_block); + genesis_block.fee_calculator.lamports_per_signature = 2; + let bank = Bank::new(&genesis_block); let key = Keypair::new(); let tx1 = @@ -1572,7 +1668,6 @@ mod tests { )), ]; - bank.fee_calculator.lamports_per_signature = 2; let initial_balance = bank.get_balance(&leader); let results = bank.filter_program_errors_and_collect_fee(&vec![tx1, tx2], &results); bank.freeze(); @@ -2176,11 +2271,12 @@ mod tests { #[test] fn test_bank_inherit_fee_calculator() { let (mut genesis_block, _mint_keypair) = create_genesis_block(500); - genesis_block.fee_calculator.lamports_per_signature = 123; + genesis_block.fee_calculator.target_lamports_per_signature = 123; + assert_eq!(genesis_block.fee_calculator.target_signatures_per_slot, 0); let bank0 = Arc::new(Bank::new(&genesis_block)); let bank1 = Arc::new(new_from_parent(&bank0)); assert_eq!( - bank0.fee_calculator.lamports_per_signature, + bank0.fee_calculator.target_lamports_per_signature, bank1.fee_calculator.lamports_per_signature ); } diff --git a/runtime/src/bank_client.rs b/runtime/src/bank_client.rs index b8685c7004..b5f9a04861 100644 --- a/runtime/src/bank_client.rs +++ b/runtime/src/bank_client.rs @@ -107,9 +107,7 @@ impl SyncClient for BankClient { } fn get_recent_blockhash(&self) -> Result<(Hash, FeeCalculator)> { - let last_blockhash = self.bank.last_blockhash(); - let fee_calculator = self.bank.fee_calculator.clone(); - Ok((last_blockhash, fee_calculator)) + Ok(self.bank.last_blockhash_with_fee_calculator()) } fn get_transaction_count(&self) -> Result { diff --git a/runtime/src/blockhash_queue.rs b/runtime/src/blockhash_queue.rs index 8ad557f880..8c7bb34c7e 100644 --- a/runtime/src/blockhash_queue.rs +++ b/runtime/src/blockhash_queue.rs @@ -1,12 +1,14 @@ use serde::{Deserialize, Serialize}; +use solana_sdk::fee_calculator::FeeCalculator; use solana_sdk::hash::Hash; use solana_sdk::timing::timestamp; use std::collections::HashMap; #[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize)] struct HashAge { - timestamp: u64, + fee_calculator: FeeCalculator, hash_height: u64, + timestamp: u64, } /// Low memory overhead, so can be cloned for every checkpoint @@ -43,25 +45,31 @@ impl BlockhashQueue { self.last_hash.expect("no hash has been set") } + pub fn get_fee_calculator(&self, hash: &Hash) -> Option<&FeeCalculator> { + self.ages.get(hash).map(|hash_age| &hash_age.fee_calculator) + } + /// Check if the age of the hash is within the max_age /// return false for any hashes with an age above max_age - pub fn check_hash_age(&self, hash: Hash, max_age: usize) -> bool { - let hash_age = self.ages.get(&hash); + pub fn check_hash_age(&self, hash: &Hash, max_age: usize) -> bool { + let hash_age = self.ages.get(hash); match hash_age { Some(age) => self.hash_height - age.hash_height <= max_age as u64, _ => false, } } + /// check if hash is valid #[cfg(test)] pub fn check_hash(&self, hash: Hash) -> bool { self.ages.get(&hash).is_some() } - pub fn genesis_hash(&mut self, hash: &Hash) { + pub fn genesis_hash(&mut self, hash: &Hash, fee_calculator: &FeeCalculator) { self.ages.insert( *hash, HashAge { + fee_calculator: fee_calculator.clone(), hash_height: 0, timestamp: timestamp(), }, @@ -74,7 +82,7 @@ impl BlockhashQueue { hash_height - age.hash_height <= max_age as u64 } - pub fn register_hash(&mut self, hash: &Hash) { + pub fn register_hash(&mut self, hash: &Hash, fee_calculator: &FeeCalculator) { self.hash_height += 1; let hash_height = self.hash_height; @@ -88,6 +96,7 @@ impl BlockhashQueue { self.ages.insert( *hash, HashAge { + fee_calculator: fee_calculator.clone(), hash_height, timestamp: timestamp(), }, @@ -117,7 +126,7 @@ mod tests { let last_hash = Hash::default(); let mut hash_queue = BlockhashQueue::new(100); assert!(!hash_queue.check_hash(last_hash)); - hash_queue.register_hash(&last_hash); + hash_queue.register_hash(&last_hash, &FeeCalculator::default()); assert!(hash_queue.check_hash(last_hash)); assert_eq!(hash_queue.hash_height(), 1); } @@ -127,7 +136,7 @@ mod tests { let last_hash = hash(&serialize(&0).unwrap()); for i in 0..102 { let last_hash = hash(&serialize(&i).unwrap()); - hash_queue.register_hash(&last_hash); + hash_queue.register_hash(&last_hash, &FeeCalculator::default()); } // Assert we're no longer able to use the oldest hash. assert!(!hash_queue.check_hash(last_hash)); @@ -137,8 +146,8 @@ mod tests { fn test_queue_init_blockhash() { let last_hash = Hash::default(); let mut hash_queue = BlockhashQueue::new(100); - hash_queue.register_hash(&last_hash); + hash_queue.register_hash(&last_hash, &FeeCalculator::default()); assert_eq!(last_hash, hash_queue.last_hash()); - assert!(hash_queue.check_hash_age(last_hash, 0)); + assert!(hash_queue.check_hash_age(&last_hash, 0)); } } diff --git a/sdk/Cargo.toml b/sdk/Cargo.toml index 33bbe55875..d825ecad4b 100644 --- a/sdk/Cargo.toml +++ b/sdk/Cargo.toml @@ -27,4 +27,5 @@ serde_derive = "1.0.92" serde_json = "1.0.39" sha2 = "0.8.0" solana-ed25519-dalek = "0.2.0" +solana-logger = { path = "../logger", version = "0.16.0" } untrusted = "0.6.2" diff --git a/sdk/src/fee_calculator.rs b/sdk/src/fee_calculator.rs index 7335c5fc4a..a2fb9b3fb1 100644 --- a/sdk/src/fee_calculator.rs +++ b/sdk/src/fee_calculator.rs @@ -1,16 +1,105 @@ use crate::message::Message; +use log::*; -#[derive(Serialize, Deserialize, Clone, Debug, Default)] +#[derive(Serialize, Deserialize, PartialEq, Eq, Clone, Debug, Default)] #[serde(rename_all = "camelCase")] pub struct FeeCalculator { + // The current cost of a signature This amount may increase/decrease over time based on + // cluster processing load. pub lamports_per_signature: u64, + + // The target cost of a signature when the cluster is operating around target_signatures_per_slot + // signatures + pub target_lamports_per_signature: u64, + + // Used to estimate the desired processing capacity of the cluster. As the signatures for + // recent slots are fewer/greater than this value, lamports_per_signature will decrease/increase + // for the next slot. A value of 0 disables lamports_per_signature fee adjustments + pub target_signatures_per_slot: usize, + + #[serde(skip)] + pub min_lamports_per_signature: u64, + #[serde(skip)] + pub max_lamports_per_signature: u64, } impl FeeCalculator { - pub fn new(lamports_per_signature: u64) -> Self { - Self { - lamports_per_signature, + pub fn new(target_lamports_per_signature: u64) -> Self { + let base_fee_calculator = Self { + target_lamports_per_signature, + lamports_per_signature: target_lamports_per_signature, + ..FeeCalculator::default() + }; + + Self::new_derived(&base_fee_calculator, 0) + } + + pub fn new_derived( + base_fee_calculator: &FeeCalculator, + latest_signatures_per_slot: usize, + ) -> Self { + let mut me = base_fee_calculator.clone(); + + if me.target_signatures_per_slot > 0 { + // lamports_per_signature can range from 50% to 1000% of + // target_lamports_per_signature + // + // TODO: Are these decent limits? + // + me.min_lamports_per_signature = std::cmp::max(1, me.target_lamports_per_signature / 2); + me.max_lamports_per_signature = me.target_lamports_per_signature * 10; + + // What the cluster should charge at `latest_signatures_per_slot` + let desired_lamports_per_signature = std::cmp::min( + me.max_lamports_per_signature, + std::cmp::max( + me.min_lamports_per_signature, + me.target_lamports_per_signature + * std::cmp::min(latest_signatures_per_slot, std::u32::MAX as usize) as u64 + / me.target_signatures_per_slot as u64, + ), + ); + + trace!( + "desired_lamports_per_signature: {}", + desired_lamports_per_signature + ); + + let gap = desired_lamports_per_signature as i64 + - base_fee_calculator.lamports_per_signature as i64; + + if gap == 0 { + me.lamports_per_signature = desired_lamports_per_signature; + } else { + // Adjust fee by 5% of target_lamports_per_signature to produce a smooth increase/decrease in fees over time. + // + // TODO: Is this fee curve smooth enough or too smooth? + // + let gap_adjust = + std::cmp::max(1, me.target_lamports_per_signature as i64 / 20) * gap.signum(); + + trace!( + "lamports_per_signature gap is {}, adjusting by {}", + gap, + gap_adjust + ); + + me.lamports_per_signature = std::cmp::min( + me.max_lamports_per_signature, + std::cmp::max( + me.min_lamports_per_signature, + (base_fee_calculator.lamports_per_signature as i64 + gap_adjust) as u64, + ), + ); + } + } else { + me.lamports_per_signature = base_fee_calculator.target_lamports_per_signature; } + debug!( + "new_derived(): lamports_per_signature: {}", + me.lamports_per_signature + ); + me } pub fn calculate_fee(&self, message: &Message) -> u64 { @@ -46,4 +135,54 @@ mod tests { let message = Message::new(vec![ix0, ix1]); assert_eq!(FeeCalculator::new(2).calculate_fee(&message), 4); } + + #[test] + fn test_fee_calculator_derived_default() { + solana_logger::setup(); + + let mut f0 = FeeCalculator::default(); + assert_eq!(f0.target_signatures_per_slot, 0); + assert_eq!(f0.target_lamports_per_signature, 0); + assert_eq!(f0.lamports_per_signature, 0); + f0.target_lamports_per_signature = 42; + + let f1 = FeeCalculator::new_derived(&f0, 4242); + assert_eq!(f1.target_signatures_per_slot, 0); + assert_eq!(f1.target_lamports_per_signature, 42); + assert_eq!(f1.lamports_per_signature, 42); + } + + #[test] + fn test_fee_calculator_derived_adjust() { + solana_logger::setup(); + + let mut f = FeeCalculator::default(); + f.target_lamports_per_signature = 100; + f.target_signatures_per_slot = 100; + f = FeeCalculator::new_derived(&f, 0); + + // Ramp fees up + while f.lamports_per_signature < f.max_lamports_per_signature { + f = FeeCalculator::new_derived(&f, std::usize::MAX); + info!("[up] f.lamports_per_signature={}", f.lamports_per_signature); + } + + // Ramp fees down + while f.lamports_per_signature > f.min_lamports_per_signature { + f = FeeCalculator::new_derived(&f, 0); + info!( + "[down] f.lamports_per_signature={}", + f.lamports_per_signature + ); + } + + // Arrive at target rate + while f.lamports_per_signature != f.target_lamports_per_signature { + f = FeeCalculator::new_derived(&f, f.target_signatures_per_slot); + info!( + "[target] f.lamports_per_signature={}", + f.lamports_per_signature + ); + } + } }