diff --git a/config-example.toml b/config-example.toml index 5d346a701..72c14ddc4 100644 --- a/config-example.toml +++ b/config-example.toml @@ -60,3 +60,8 @@ consensus.eth_block_gas_limit = 84000000 consensus.gas_price = "4_761_904_800_000" consensus.genesis_fork = { at_height = 0, call_mode_1_sets_caller_to_parent_caller = false, failed_scilla_call_from_gas_exempt_caller_causes_revert = false, scilla_messages_can_call_evm_contracts = false, scilla_contract_creation_increments_account_balance = false, scilla_json_preserve_order = false } + +txn_pool.maximum_global_size = 100000 +txn_pool.maximum_txn_count_per_sender = 20 +txn_pool.total_slots_for_all_senders = 50000 + diff --git a/config-single-node.toml b/config-single-node.toml index d4e4a456a..42a28f711 100644 --- a/config-single-node.toml +++ b/config-single-node.toml @@ -30,3 +30,8 @@ consensus.gas_price = "4_761_904_800_000" consensus.local_address = "host.docker.internal" consensus.genesis_fork = { at_height = 0, call_mode_1_sets_caller_to_parent_caller = false, failed_scilla_call_from_gas_exempt_caller_causes_revert = false, scilla_messages_can_call_evm_contracts = false, scilla_contract_creation_increments_account_balance = true, scilla_json_preserve_order = true } + +txn_pool.maximum_global_size = 100000 +txn_pool.maximum_txn_count_per_sender = 20 +txn_pool.total_slots_for_all_senders = 50000 + diff --git a/z2/resources/chain-specs/zq2-devnet.toml b/z2/resources/chain-specs/zq2-devnet.toml index f06ff1681..42b950185 100644 --- a/z2/resources/chain-specs/zq2-devnet.toml +++ b/z2/resources/chain-specs/zq2-devnet.toml @@ -24,3 +24,7 @@ consensus.scilla_call_gas_exempt_addrs = [] consensus.contract_upgrade_block_heights = { deposit_v3 = 3600, deposit_v4 = 428400 } api_servers = [{ port = 4201, enabled_apis = [{ namespace = "eth", apis = ["blockNumber"] }] }, { port = 4202, enabled_apis = ["admin", "debug", "erigon", "eth", "net", "ots", "trace", "txpool", "web3", "zilliqa"] }] + +txn_pool.maximum_global_size = 100000 +txn_pool.maximum_txn_count_per_sender = 200 +txn_pool.total_slots_for_all_senders = 50000 \ No newline at end of file diff --git a/z2/resources/chain-specs/zq2-infratest.toml b/z2/resources/chain-specs/zq2-infratest.toml index 5465efa4a..cd8f082f2 100644 --- a/z2/resources/chain-specs/zq2-infratest.toml +++ b/z2/resources/chain-specs/zq2-infratest.toml @@ -23,3 +23,7 @@ consensus.scilla_call_gas_exempt_addrs = [] consensus.contract_upgrade_block_heights = { deposit_v4 = 0 } api_servers = [{ port = 4201, enabled_apis = [{ namespace = "eth", apis = ["blockNumber"] }] }, { port = 4202, enabled_apis = ["admin", "debug", "erigon", "eth", "net", "ots", "trace", "txpool", "web3", "zilliqa"] }] + +txn_pool.maximum_global_size = 100000 +txn_pool.maximum_txn_count_per_sender = 200 +txn_pool.total_slots_for_all_senders = 50000 \ No newline at end of file diff --git a/z2/resources/chain-specs/zq2-perftest.toml b/z2/resources/chain-specs/zq2-perftest.toml index 9c3ff3107..72a340ea9 100644 --- a/z2/resources/chain-specs/zq2-perftest.toml +++ b/z2/resources/chain-specs/zq2-perftest.toml @@ -19,3 +19,7 @@ consensus.scilla_call_gas_exempt_addrs = [] consensus.contract_upgrade_block_heights = { deposit_v4 = 0 } api_servers = [{ port = 4201, enabled_apis = [{ namespace = "eth", apis = ["blockNumber"] }] }, { port = 4202, enabled_apis = ["admin", "debug", "erigon", "eth", "net", "ots", "trace", "txpool", "web3", "zilliqa"] }] + +txn_pool.maximum_global_size = 100000 +txn_pool.maximum_txn_count_per_sender = 200 +txn_pool.total_slots_for_all_senders = 50000 \ No newline at end of file diff --git a/z2/resources/chain-specs/zq2-protomainnet.toml b/z2/resources/chain-specs/zq2-protomainnet.toml index c454c2005..e83b7cc1f 100644 --- a/z2/resources/chain-specs/zq2-protomainnet.toml +++ b/z2/resources/chain-specs/zq2-protomainnet.toml @@ -27,4 +27,8 @@ consensus.genesis_fork = { at_height = 0, call_mode_1_sets_caller_to_parent_call consensus.forks = [ { at_height = 5342400, failed_scilla_call_from_gas_exempt_caller_causes_revert = true, call_mode_1_sets_caller_to_parent_caller = true }, { at_height = 7966800, scilla_messages_can_call_evm_contracts = true, scilla_contract_creation_increments_account_balance = true, scilla_json_preserve_order = true }, -] \ No newline at end of file +] + +txn_pool.maximum_global_size = 100000 +txn_pool.maximum_txn_count_per_sender = 200 +txn_pool.total_slots_for_all_senders = 50000 \ No newline at end of file diff --git a/z2/resources/chain-specs/zq2-prototestnet.toml b/z2/resources/chain-specs/zq2-prototestnet.toml index 10688321a..73bc0c0e2 100644 --- a/z2/resources/chain-specs/zq2-prototestnet.toml +++ b/z2/resources/chain-specs/zq2-prototestnet.toml @@ -28,4 +28,8 @@ consensus.forks = [ { at_height = 8404000, failed_scilla_call_from_gas_exempt_caller_causes_revert = true, call_mode_1_sets_caller_to_parent_caller = true }, { at_height = 10200000, scilla_messages_can_call_evm_contracts = true }, { at_height = 11152000, scilla_contract_creation_increments_account_balance = true, scilla_json_preserve_order = true }, -] \ No newline at end of file +] + +txn_pool.maximum_global_size = 100000 +txn_pool.maximum_txn_count_per_sender = 200 +txn_pool.total_slots_for_all_senders = 50000 \ No newline at end of file diff --git a/z2/resources/chain-specs/zq2-richard.toml b/z2/resources/chain-specs/zq2-richard.toml index fa6dc1885..4689e8cd1 100644 --- a/z2/resources/chain-specs/zq2-richard.toml +++ b/z2/resources/chain-specs/zq2-richard.toml @@ -19,3 +19,7 @@ consensus.scilla_call_gas_exempt_addrs = [] consensus.contract_upgrade_block_heights = { deposit_v4 = 0 } api_servers = [{ port = 4201, enabled_apis = [{ namespace = "eth", apis = ["blockNumber"] }] }, { port = 4202, enabled_apis = ["admin", "debug", "erigon", "eth", "net", "ots", "trace", "txpool", "web3", "zilliqa"] }] + +txn_pool.maximum_global_size = 100000 +txn_pool.maximum_txn_count_per_sender = 200 +txn_pool.total_slots_for_all_senders = 50000 \ No newline at end of file diff --git a/z2/resources/chain-specs/zq2-uccbtest.toml b/z2/resources/chain-specs/zq2-uccbtest.toml index c31ccc339..8ae413c70 100644 --- a/z2/resources/chain-specs/zq2-uccbtest.toml +++ b/z2/resources/chain-specs/zq2-uccbtest.toml @@ -19,3 +19,7 @@ consensus.scilla_call_gas_exempt_addrs = [] consensus.contract_upgrade_block_heights = { deposit_v4 = 0 } api_servers = [{ port = 4201, enabled_apis = [{ namespace = "eth", apis = ["blockNumber"] }] }, { port = 4202, enabled_apis = ["admin", "debug", "erigon", "eth", "net", "ots", "trace", "txpool", "web3", "zilliqa"] }] + +txn_pool.maximum_global_size = 100000 +txn_pool.maximum_txn_count_per_sender = 200 +txn_pool.total_slots_for_all_senders = 50000 \ No newline at end of file diff --git a/z2/src/setup.rs b/z2/src/setup.rs index 8639dfcb4..5a7907cc2 100644 --- a/z2/src/setup.rs +++ b/z2/src/setup.rs @@ -31,7 +31,7 @@ use zilliqa::{ eth_chain_id_default, failed_request_sleep_duration_default, local_address_default, max_blocks_in_flight_default, scilla_address_default, scilla_ext_libs_path_default, scilla_stdlib_dir_default, state_rpc_limit_default, total_native_token_supply_default, - Amount, ConsensusConfig, ContractUpgradesBlockHeights, GenesisDeposit, + Amount, ConsensusConfig, ContractUpgradesBlockHeights, GenesisDeposit, TxnPoolConfig, }, transaction::EvmGas, }; @@ -544,6 +544,7 @@ impl Setup { forks: vec![], genesis_fork: genesis_fork_default(), }, + txn_pool: TxnPoolConfig::default(), block_request_limit: block_request_limit_default(), max_blocks_in_flight: max_blocks_in_flight_default(), block_request_batch_size: block_request_batch_size_default(), diff --git a/zilliqa/benches/it.rs b/zilliqa/benches/it.rs index 37d255630..2e0fa3c83 100644 --- a/zilliqa/benches/it.rs +++ b/zilliqa/benches/it.rs @@ -89,6 +89,9 @@ fn process_empty(c: &mut Criterion) { "0x0000000000000000000000000000000000000001", ], ] + txn_pool.maximum_global_size = 10000000000 + txn_pool.maximum_txn_count_per_sender = 200000000 + txn_pool.total_slots_for_all_senders = 50000000 "#, secret_key.node_public_key() )) @@ -200,6 +203,9 @@ fn consensus( consensus.genesis_fork.scilla_messages_can_call_evm_contracts = true consensus.genesis_fork.scilla_contract_creation_increments_account_balance = true consensus.genesis_fork.scilla_json_preserve_order = true + txn_pool.maximum_global_size = 10000000000 + txn_pool.maximum_txn_count_per_sender = 200000000 + txn_pool.total_slots_for_all_senders = 50000000 "#, ) .unwrap(); diff --git a/zilliqa/src/api/zilliqa.rs b/zilliqa/src/api/zilliqa.rs index b978d24f5..5460c78cd 100644 --- a/zilliqa/src/api/zilliqa.rs +++ b/zilliqa/src/api/zilliqa.rs @@ -338,7 +338,10 @@ fn create_transaction( | ValidationOutcome::InsufficientGasEvm(_, _) | ValidationOutcome::NonceTooLow(_, _) | ValidationOutcome::InsufficientFunds(_, _) - | ValidationOutcome::BlockGasLimitExceeded(_, _) => { + | ValidationOutcome::BlockGasLimitExceeded(_, _) + | ValidationOutcome::TotalNumberOfSlotsExceeded + | ValidationOutcome::GlobalTransactionCountExceeded + | ValidationOutcome::TransactionCountExceededForSender => { RPCErrorCode::RpcInvalidParameter } _ => RPCErrorCode::RpcVerifyRejected, diff --git a/zilliqa/src/cfg.rs b/zilliqa/src/cfg.rs index b974487d2..d006a70ef 100644 --- a/zilliqa/src/cfg.rs +++ b/zilliqa/src/cfg.rs @@ -122,6 +122,8 @@ pub struct NodeConfig { pub eth_chain_id: u64, /// Consensus-specific data. pub consensus: ConsensusConfig, + /// Transaction pool config + pub txn_pool: TxnPoolConfig, /// The maximum duration between a recieved block's timestamp and the current time. Defaults to 10 seconds. #[serde(default = "allowed_timestamp_skew_default")] pub allowed_timestamp_skew: Duration, @@ -167,6 +169,7 @@ impl Default for NodeConfig { api_servers: vec![], eth_chain_id: eth_chain_id_default(), consensus: ConsensusConfig::default(), + txn_pool: TxnPoolConfig::default(), allowed_timestamp_skew: allowed_timestamp_skew_default(), data_dir: None, state_cache_size: state_cache_size_default(), @@ -472,6 +475,39 @@ impl Default for ConsensusConfig { } } +#[derive(Clone, Serialize, Deserialize, Debug)] +#[serde(deny_unknown_fields)] +pub struct TxnPoolConfig { + /// Maximum number of transactions transaction pool can hold + pub maximum_global_size: u64, + /// Maximum number of transactions per single sender + pub maximum_txn_count_per_sender: u64, + /// total slots for all senders + pub total_slots_for_all_senders: u64, +} + +pub fn maximum_txn_pool_global_size() -> u64 { + 10000 +} + +pub fn maximum_txn_pool_txn_count_per_user() -> u64 { + 500 +} + +pub fn total_slots_for_all_senders() -> u64 { + 1_000_000 +} + +impl Default for TxnPoolConfig { + fn default() -> Self { + Self { + maximum_global_size: maximum_txn_pool_global_size(), + maximum_txn_count_per_sender: maximum_txn_pool_txn_count_per_user(), + total_slots_for_all_senders: total_slots_for_all_senders(), + } + } +} + #[derive(Clone, Debug, Serialize, Deserialize)] pub struct Forks(Vec); diff --git a/zilliqa/src/consensus.rs b/zilliqa/src/consensus.rs index 952e824e7..c44f178bc 100644 --- a/zilliqa/src/consensus.rs +++ b/zilliqa/src/consensus.rs @@ -318,6 +318,7 @@ impl Consensus { peers.clone(), )?; + let txn_pool_config = config.txn_pool.clone(); let mut consensus = Consensus { secret_key, config, @@ -334,7 +335,7 @@ impl Consensus { db, receipts_cache: HashMap::new(), receipts_cache_hash: Hash::ZERO, - transaction_pool: Default::default(), + transaction_pool: TransactionPool::new(txn_pool_config), early_proposal: None, create_next_block_on_timeout: false, view_updated_at: SystemTime::now(), diff --git a/zilliqa/src/pool.rs b/zilliqa/src/pool.rs index 9239e4fc7..c506af381 100644 --- a/zilliqa/src/pool.rs +++ b/zilliqa/src/pool.rs @@ -8,14 +8,16 @@ use anyhow::{anyhow, Result}; use tracing::debug; use crate::{ + cfg::TxnPoolConfig, crypto::Hash, + pool::TxAddResult::ValidationFailed, state::State, transaction::{SignedTransaction, ValidationOutcome, VerifiedTransaction}, }; /// The result of trying to add a transaction to the mempool. The argument is /// a human-readable string to be returned to the user. -#[derive(Debug, Copy, Clone)] +#[derive(Debug, Copy, Clone, PartialEq)] pub enum TxAddResult { /// Transaction was successfully added to the mempool AddedToMempool, @@ -78,8 +80,9 @@ type GasCollection = BTreeMap>; /// A pool that manages uncommitted transactions. /// /// It provides transactions to the chain via [`TransactionPool::best_transaction`]. -#[derive(Clone, Debug, Default)] +#[derive(Clone, Debug)] pub struct TransactionPool { + config: TxnPoolConfig, /// All transactions in the pool. These transactions are all valid, or might become /// valid at some point in the future. transactions: BTreeMap, @@ -89,6 +92,20 @@ pub struct TransactionPool { /// Keeps transactions sorted by gas_price, each gas_price index can contain more than one txn /// These are candidates to be included in the next block gas_index: GasCollection, + /// Tracks number of transactions per sender + sender_txn_counter: HashMap, +} + +impl TransactionPool { + pub fn new(config: TxnPoolConfig) -> Self { + Self { + config, + transactions: BTreeMap::new(), + hash_to_index: BTreeMap::new(), + gas_index: GasCollection::new(), + sender_txn_counter: HashMap::new(), + } + } } /// A wrapper for (gas price, sender, nonce), stored in the `ready` heap of [TransactionPool]. @@ -264,7 +281,8 @@ impl TransactionPool { return TxAddResult::NonceTooLow(txn.tx.nonce().unwrap(), account_nonce); } - if let Some(existing_txn) = self.transactions.get(&txn.mempool_index()) { + let is_replacement = if let Some(existing_txn) = self.transactions.get(&txn.mempool_index()) + { // Only proceed if the new transaction is better. Note that if they are // equally good, we prioritise the existing transaction to avoid the need // to broadcast a new transaction to the network. @@ -280,6 +298,35 @@ impl TransactionPool { Self::remove_from_gas_index(&mut self.gas_index, existing_txn); // Remove the existing transaction from `hash_to_index` if we're about to replace it. self.hash_to_index.remove(&existing_txn.hash); + // Decrease the count of transactions tracked by this sender + self.decrease_counter_for_user(existing_txn.signer); + true + } else { + false + }; + + // If it's a transaction that will increase the count in the pool + if !is_replacement { + // Check global counter + if self.transactions.len() + 1 > self.config.maximum_global_size as usize { + return ValidationFailed(ValidationOutcome::GlobalTransactionCountExceeded); + } + // Check total number of slots for senders + if !self.sender_txn_counter.contains_key(&txn.signer) + && self.sender_txn_counter.len() + 1 + > self.config.total_slots_for_all_senders as usize + { + return ValidationFailed(ValidationOutcome::TotalNumberOfSlotsExceeded); + } + // Check per sender counter + let sender_counter = self + .sender_txn_counter + .get(&txn.signer) + .copied() + .unwrap_or_default(); + if sender_counter + 1 > self.config.maximum_txn_count_per_sender { + return ValidationFailed(ValidationOutcome::TransactionCountExceededForSender); + } } // If this transaction either has a nonce equal to the account's current nonce, @@ -291,6 +338,8 @@ impl TransactionPool { debug!("Txn added to mempool. Hash: {:?}, from: {:?}, nonce: {:?}, account nonce: {account_nonce}", txn.hash, txn.signer, txn.tx.nonce()); + // Increase the counter for a sender + *self.sender_txn_counter.entry(txn.signer).or_insert(0) += 1; // Finally we insert it into the tx store and the hash reverse-index self.hash_to_index.insert(txn.hash, txn.mempool_index()); self.transactions.insert(txn.mempool_index(), txn); @@ -355,6 +404,7 @@ impl TransactionPool { Self::remove_from_gas_index(&mut self.gas_index, txn); } + *self.sender_txn_counter.entry(txn.signer).or_insert(0) += 1; Self::add_to_gas_index(&mut self.gas_index, &txn); self.hash_to_index.insert(txn.hash, txn.mempool_index()); self.transactions.insert(txn.mempool_index(), txn); @@ -376,11 +426,21 @@ impl TransactionPool { self.hash_to_index.remove(&txn.hash); Self::remove_from_gas_index(&mut self.gas_index, txn); + self.decrease_counter_for_user(txn.signer); if let Some(next) = tx_index.next().and_then(|idx| self.transactions.get(&idx)) { Self::add_to_gas_index(&mut self.gas_index, next); } } + fn decrease_counter_for_user(&mut self, sender: Address) { + if let Some(counter) = self.sender_txn_counter.get_mut(&sender) { + *counter = counter.saturating_sub(1); + if *counter == 0 { + self.sender_txn_counter.remove(&sender); + } + } + } + /// Clear the transaction pool, returning all remaining transactions in an unspecified order. pub fn drain(&mut self) -> impl Iterator { self.hash_to_index.clear(); @@ -405,13 +465,15 @@ mod tests { use anyhow::Result; use rand::{seq::SliceRandom, thread_rng}; - use super::TransactionPool; + use super::{TransactionPool, TxAddResult}; use crate::{ - cfg::NodeConfig, + cfg::{NodeConfig, TxnPoolConfig}, crypto::Hash, db::Db, state::State, - transaction::{EvmGas, SignedTransaction, TxIntershard, VerifiedTransaction}, + transaction::{ + EvmGas, SignedTransaction, TxIntershard, ValidationOutcome, VerifiedTransaction, + }, }; fn transaction(from_addr: Address, nonce: u8, gas_price: u128) -> VerifiedTransaction { @@ -478,9 +540,18 @@ mod tests { state.save_account(address, acc) } + fn get_pool() -> TransactionPool { + let config = TxnPoolConfig { + maximum_txn_count_per_sender: 5, + maximum_global_size: 10, + total_slots_for_all_senders: 5, + }; + TransactionPool::new(config) + } + #[test] fn nonces_returned_in_order() -> Result<()> { - let mut pool = TransactionPool::default(); + let mut pool = get_pool(); let from = "0x0000000000000000000000000000000000001234".parse()?; let mut state = get_in_memory_state()?; @@ -523,13 +594,13 @@ mod tests { #[test] fn nonces_returned_in_order_same_gas() -> Result<()> { - let mut pool = TransactionPool::default(); + let mut pool = get_pool(); let from = "0x0000000000000000000000000000000000001234".parse()?; let mut state = get_in_memory_state()?; create_acc(&mut state, from, 100, 0)?; - const COUNT: u64 = 100; + const COUNT: u64 = 5; let mut nonces = (0..COUNT).collect::>(); let mut rng = thread_rng(); @@ -553,7 +624,7 @@ mod tests { #[test] fn ordered_by_gas_price() -> Result<()> { - let mut pool = TransactionPool::default(); + let mut pool = get_pool(); let from1 = "0x0000000000000000000000000000000000000001".parse()?; let from2 = "0x0000000000000000000000000000000000000002".parse()?; let from3 = "0x0000000000000000000000000000000000000003".parse()?; @@ -596,7 +667,7 @@ mod tests { #[test] fn update_nonce_discards_invalid_transaction() -> Result<()> { - let mut pool = TransactionPool::default(); + let mut pool = get_pool(); let from = "0x0000000000000000000000000000000000001234".parse()?; let mut state = get_in_memory_state()?; @@ -620,7 +691,7 @@ mod tests { #[test] fn too_expensive_tranactions_are_not_proposed() -> Result<()> { - let mut pool = TransactionPool::default(); + let mut pool = get_pool(); let from = "0x0000000000000000000000000000000000001234".parse()?; let mut state = get_in_memory_state()?; @@ -656,7 +727,7 @@ mod tests { #[test] fn preview_content_test() -> Result<()> { - let mut pool = TransactionPool::default(); + let mut pool = get_pool(); let from = "0x0000000000000000000000000000000000001234".parse()?; let mut state = get_in_memory_state()?; @@ -682,4 +753,153 @@ mod tests { Ok(()) } + + #[test] + fn global_counter_exceeded() -> Result<()> { + let mut pool = get_pool(); + + let mut state = get_in_memory_state()?; + + const COUNT: usize = 5; + + let addresses = std::iter::repeat_with(|| Address::random()) + .take(COUNT) + .collect::>(); + + for address in addresses.iter() { + create_acc(&mut state, *address, 100, 0)?; + for nonce in 0..2 { + assert_eq!( + TxAddResult::AddedToMempool, + pool.insert_transaction(transaction(*address, nonce, 1), 0) + ); + } + } + + // Can't add the following one due to global limit being exceeded + let rand_addr = Address::random(); + create_acc(&mut state, rand_addr, 100, 0)?; + assert_eq!( + TxAddResult::ValidationFailed(ValidationOutcome::GlobalTransactionCountExceeded), + pool.insert_transaction(transaction(rand_addr, 0, 1), 0) + ); + + // Remove all txns sent by one sender + pool.mark_executed(&transaction(addresses[0], 0, 1)); + pool.mark_executed(&transaction(addresses[0], 1, 1)); + // And try to insert again - it should succeed + assert_eq!( + TxAddResult::AddedToMempool, + pool.insert_transaction(transaction(rand_addr, 0, 1), 0) + ); + + Ok(()) + } + + #[test] + fn per_sender_counter_exceeded() -> Result<()> { + let mut pool = get_pool(); + + let mut state = get_in_memory_state()?; + + const COUNT: u8 = 5; + + let address = Address::random(); + create_acc(&mut state, address, 100, 0)?; + + for nonce in 0..COUNT { + assert_eq!( + TxAddResult::AddedToMempool, + pool.insert_transaction(transaction(address, nonce, 1), 0) + ); + } + + // Can't add the following one due to per user limit being exceeded + assert_eq!( + TxAddResult::ValidationFailed(ValidationOutcome::TransactionCountExceededForSender), + pool.insert_transaction(transaction(address, COUNT, 1), 0) + ); + + // Remove a single txn + pool.mark_executed(&transaction(address, 0, 1)); + // And try to insert again - it should succeed + assert_eq!( + TxAddResult::AddedToMempool, + pool.insert_transaction(transaction(address, COUNT, 1), 0) + ); + + Ok(()) + } + + #[test] + fn replacement_not_affect_counter() -> Result<()> { + let mut pool = get_pool(); + + let mut state = get_in_memory_state()?; + + const COUNT: u8 = 5; + + let address = Address::random(); + create_acc(&mut state, address, 100, 0)?; + + for nonce in 0..COUNT { + assert_eq!( + TxAddResult::AddedToMempool, + pool.insert_transaction(transaction(address, nonce, 1), 0) + ); + } + + // Can't add the following one due to per user limit being exceeded + assert_eq!( + TxAddResult::ValidationFailed(ValidationOutcome::TransactionCountExceededForSender), + pool.insert_transaction(transaction(address, COUNT, 1), 0) + ); + + // Try replacing existing one with higher gas price + assert_eq!( + TxAddResult::AddedToMempool, + pool.insert_transaction(transaction(address, 0, 2), 0) + ); + + Ok(()) + } + + #[test] + fn total_slots_per_senders_exceeded() -> Result<()> { + let mut pool = get_pool(); + + let mut state = get_in_memory_state()?; + + const COUNT: usize = 5; + + let addresses = std::iter::repeat_with(|| Address::random()) + .take(COUNT) + .collect::>(); + + for address in addresses.iter() { + create_acc(&mut state, *address, 100, 0)?; + assert_eq!( + TxAddResult::AddedToMempool, + pool.insert_transaction(transaction(*address, 0, 1), 0) + ); + } + + // Can't add the following one due to total number of slots per all senders being exceeded + let rand_addr = Address::random(); + create_acc(&mut state, rand_addr, 100, 0)?; + assert_eq!( + TxAddResult::ValidationFailed(ValidationOutcome::TotalNumberOfSlotsExceeded), + pool.insert_transaction(transaction(rand_addr, 0, 1), 0) + ); + + // Remove a single txn + pool.mark_executed(&transaction(addresses[0], 0, 1)); + // And try to insert again - it should succeed + assert_eq!( + TxAddResult::AddedToMempool, + pool.insert_transaction(transaction(rand_addr, 0, 1), 0) + ); + + Ok(()) + } } diff --git a/zilliqa/src/transaction.rs b/zilliqa/src/transaction.rs index f43914b5e..c288f5093 100644 --- a/zilliqa/src/transaction.rs +++ b/zilliqa/src/transaction.rs @@ -48,7 +48,7 @@ use crate::{ /// Result>, which would be confusing. /// The argument is a human-readable error message which can be returned to the /// user to indicate the problem with the transaction. -#[derive(Debug, Copy, Clone)] +#[derive(Debug, Copy, Clone, PartialEq)] pub enum ValidationOutcome { Success, /// Transaction input size exceeds configured limit - (size, limit) @@ -69,6 +69,12 @@ pub enum ValidationOutcome { NonceTooLow(u64, u64), /// Unrecognised type - not invocation, creation or transfer UnknownTransactionType, + /// Global transaction count exceeded + GlobalTransactionCountExceeded, + /// Transaction counter exceeded for a sender + TransactionCountExceededForSender, + /// Total nunber of sender slots exceeded + TotalNumberOfSlotsExceeded, } impl ValidationOutcome { @@ -119,6 +125,15 @@ impl ValidationOutcome { Self::UnknownTransactionType => { "Txn is not transfer, contract creation or contract invocation".to_string() } + Self::GlobalTransactionCountExceeded => { + "Global number of transactions stored in the mempool has been exceeded!".to_string() + } + Self::TransactionCountExceededForSender => { + "Transactions count kept per user has been exceeded!".to_string() + } + Self::TotalNumberOfSlotsExceeded => { + "Total number of slots for all senders has been exceeded".to_string() + } } } } diff --git a/zilliqa/tests/it/main.rs b/zilliqa/tests/it/main.rs index fa4fd8d5b..1a265363c 100644 --- a/zilliqa/tests/it/main.rs +++ b/zilliqa/tests/it/main.rs @@ -72,7 +72,7 @@ use zilliqa::{ scilla_address_default, scilla_ext_libs_path_default, scilla_stdlib_dir_default, staker_withdrawal_period_default, state_cache_size_default, state_rpc_limit_default, total_native_token_supply_default, Amount, ApiServer, Checkpoint, ConsensusConfig, - ContractUpgradesBlockHeights, GenesisDeposit, NodeConfig, + ContractUpgradesBlockHeights, GenesisDeposit, NodeConfig, TxnPoolConfig, }, crypto::{SecretKey, TransactionPublicKey}, db, @@ -363,6 +363,7 @@ impl Network { forks: vec![], genesis_fork: genesis_fork_default(), }, + txn_pool: TxnPoolConfig::default(), api_servers: vec![ApiServer { port: 4201, enabled_apis: api::all_enabled(), @@ -506,6 +507,7 @@ impl Network { forks: vec![], genesis_fork: genesis_fork_default(), }, + txn_pool: TxnPoolConfig::default(), block_request_limit: block_request_limit_default(), max_blocks_in_flight: max_blocks_in_flight_default(), block_request_batch_size: block_request_batch_size_default(),