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
This commit is contained in:
Michael Vines 2019-06-10 22:18:32 -07:00 committed by GitHub
parent a4035a3c65
commit e4d8ea11ac
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 391 additions and 115 deletions

1
Cargo.lock generated
View File

@ -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)",
]

View File

@ -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)

View File

@ -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:

View File

@ -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<transaction::Result<()>> {
@ -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);

View File

@ -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 {

View File

@ -53,8 +53,12 @@ pub fn append_primordial_accounts(file: &str, genesis_block: &mut GenesisBlock)
fn main() -> Result<(), Box<dyn error::Error>> {
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<dyn error::Error>> {
.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<dyn error::Error>> {
&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 =

View File

@ -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[@]}"

3
run.sh
View File

@ -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 \

View File

@ -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<Fork, usize>,
txs: &[Transaction],
lock_results: Vec<Result<()>>,
fee_calculator: &FeeCalculator,
hash_queue: &BlockhashQueue,
error_counters: &mut ErrorCounters,
) -> Vec<Result<(InstructionAccounts, InstructionLoaders, InstructionCredits)>> {
//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<Result<(InstructionAccounts, InstructionLoaders, InstructionCredits)>> {
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
}

View File

@ -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<Result<()>> = 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<Result<()>> {
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
);
}

View File

@ -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<u64> {

View File

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

View File

@ -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"

View File

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