From 29dd1309861d17898c65bcf87fc5b57614c23e1e Mon Sep 17 00:00:00 2001 From: AurelienFT Date: Thu, 12 Sep 2024 14:56:19 +0200 Subject: [PATCH 01/28] start definition of the pool --- crates/services/txpool_v2/src/service.rs | 12 ++++++++++-- crates/services/txpool_v2/src/storage/graph.rs | 2 ++ 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/crates/services/txpool_v2/src/service.rs b/crates/services/txpool_v2/src/service.rs index cce13d11b8f..b4b8ef96cc4 100644 --- a/crates/services/txpool_v2/src/service.rs +++ b/crates/services/txpool_v2/src/service.rs @@ -4,10 +4,14 @@ use fuel_core_services::{ ServiceRunner, StateWatcher, }; -use fuel_core_types::fuel_tx::Transaction; +use fuel_core_types::fuel_tx::{Transaction, TxId}; + +use crate::{collision_manager::basic::BasicCollisionManager, pool::Pool, selection_algorithms::ratio_tip_gas::RatioTipGasSelection, storage::graph::{GraphStorage, GraphStorageIndex}}; #[derive(Clone)] -pub struct SharedState; +pub struct SharedState { + pool: Pool<, GraphStorage, BasicCollisionManager, RatioTipGasSelection> +} impl SharedState { // TODO: Implement conversion from `Transaction` to `PoolTransaction`. (with all the verifications that it implies): https://github.com/FuelLabs/fuel-core/issues/2186 @@ -15,6 +19,10 @@ impl SharedState { // Move verif of wasm there vec![] } + + pub fn find_one(&self, tx_id: TxId) -> Option<&Transaction> { + self + } } pub type Service = ServiceRunner; diff --git a/crates/services/txpool_v2/src/storage/graph.rs b/crates/services/txpool_v2/src/storage/graph.rs index bd21959159e..5ed7b4e703f 100644 --- a/crates/services/txpool_v2/src/storage/graph.rs +++ b/crates/services/txpool_v2/src/storage/graph.rs @@ -48,6 +48,8 @@ use super::{ StorageData, }; +pub type GraphStorageIndex = NodeIndex; + pub struct GraphStorage { /// The configuration of the graph config: GraphConfig, From dd6f7c1103dfbbab88e72b0083aff850d4ebe259 Mon Sep 17 00:00:00 2001 From: AurelienFT Date: Thu, 12 Sep 2024 23:34:00 +0200 Subject: [PATCH 02/28] Add insertion skeleton --- Cargo.lock | 1 + crates/services/txpool_v2/src/pool.rs | 34 ++--- crates/services/txpool_v2/src/ports.rs | 13 ++ crates/services/txpool_v2/src/service.rs | 153 ++++++++++++++++++++--- 4 files changed, 159 insertions(+), 42 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 26fbd7b47f3..42c667ab29f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3739,6 +3739,7 @@ dependencies = [ "fuel-core-storage", "fuel-core-types", "num-rational", + "parking_lot", "petgraph", "tokio", "tokio-rayon", diff --git a/crates/services/txpool_v2/src/pool.rs b/crates/services/txpool_v2/src/pool.rs index 2dd72bdf741..b8ffac5761b 100644 --- a/crates/services/txpool_v2/src/pool.rs +++ b/crates/services/txpool_v2/src/pool.rs @@ -1,14 +1,14 @@ +use std::collections::HashMap; + use fuel_core_types::{ - fuel_tx::field::BlobId, + fuel_tx::{ + field::BlobId, + TxId, + }, services::txpool::PoolTransaction, }; use tracing::instrument; -#[cfg(test)] -use fuel_core_types::fuel_tx::TxId; -#[cfg(test)] -use std::collections::HashMap; - use crate::{ collision_manager::{ CollisionManager, @@ -41,7 +41,7 @@ pub struct Pool { selection_algorithm: SA, /// The persistent storage of the pool. persistent_storage_provider: PSProvider, - #[cfg(test)] + /// Mapping from tx_id to storage_id. tx_id_to_storage_id: HashMap, } @@ -60,7 +60,6 @@ impl Pool { selection_algorithm, persistent_storage_provider, config, - #[cfg(test)] tx_id_to_storage_id: HashMap::new(), } } @@ -92,7 +91,6 @@ where .persistent_storage_provider .latest_view() .map_err(|e| Error::Database(format!("{:?}", e)))?; - #[cfg(test)] let tx_id = tx.id(); if self.storage.count() >= self.config.max_txs { return Err(Error::NotInsertedLimitHit); @@ -122,10 +120,7 @@ where dependencies, collisions.colliding_txs, )?; - #[cfg(test)] - { - self.tx_id_to_storage_id.insert(tx_id, storage_id); - } + self.tx_id_to_storage_id.insert(tx_id, storage_id); // No dependencies directly in the graph and the sorted transactions if !has_dependencies { self.selection_algorithm @@ -137,10 +132,7 @@ where .map(|tx| { self.collision_manager.on_removed_transaction(&tx)?; self.selection_algorithm.on_removed_transaction(&tx)?; - #[cfg(test)] - { - self.tx_id_to_storage_id.remove(&tx.id()); - } + self.tx_id_to_storage_id.remove(&tx.id()); Ok(tx) }) .collect(); @@ -172,11 +164,8 @@ where .on_removed_transaction(&storage_data.transaction)?; self.selection_algorithm .on_removed_transaction(&storage_data.transaction)?; - #[cfg(test)] - { - self.tx_id_to_storage_id - .remove(&storage_data.transaction.id()); - } + self.tx_id_to_storage_id + .remove(&storage_data.transaction.id()); Ok(storage_data.transaction) }) .collect() @@ -187,7 +176,6 @@ where Ok(vec![]) } - #[cfg(test)] pub fn find_one(&self, tx_id: &TxId) -> Option<&PoolTransaction> { Storage::get(&self.storage, self.tx_id_to_storage_id.get(tx_id)?) .map(|data| &data.transaction) diff --git a/crates/services/txpool_v2/src/ports.rs b/crates/services/txpool_v2/src/ports.rs index e433e134aae..fc5a9f59025 100644 --- a/crates/services/txpool_v2/src/ports.rs +++ b/crates/services/txpool_v2/src/ports.rs @@ -1,5 +1,8 @@ +use std::sync::Arc; + use fuel_core_storage::Result as StorageResult; use fuel_core_types::{ + blockchain::header::ConsensusParametersVersion, entities::{ coins::coin::CompressedCoin, relayer::message::Message, @@ -7,6 +10,7 @@ use fuel_core_types::{ fuel_tx::{ BlobId, Bytes32, + ConsensusParameters, ContractId, UtxoId, }, @@ -31,6 +35,15 @@ pub trait AtomicView: Send + Sync { fn latest_view(&self) -> StorageResult; } +/// Trait for getting the latest consensus parameters. +#[cfg_attr(feature = "test-helpers", mockall::automock)] +pub trait ConsensusParametersProvider { + /// Get latest consensus parameters. + fn latest_consensus_parameters( + &self, + ) -> (ConsensusParametersVersion, Arc); +} + pub trait TxPoolPersistentStorage: Send + Sync { fn utxo(&self, utxo_id: &UtxoId) -> StorageResult>; diff --git a/crates/services/txpool_v2/src/service.rs b/crates/services/txpool_v2/src/service.rs index b4b8ef96cc4..75781699b9c 100644 --- a/crates/services/txpool_v2/src/service.rs +++ b/crates/services/txpool_v2/src/service.rs @@ -1,43 +1,128 @@ +use std::sync::Arc; + use fuel_core_services::{ RunnableService, RunnableTask, ServiceRunner, StateWatcher, }; -use fuel_core_types::fuel_tx::{Transaction, TxId}; +use fuel_core_types::{ + blockchain::consensus::Consensus, + fuel_tx::{ + Transaction, + TxId, + }, + fuel_types::BlockHeight, + services::txpool::PoolTransaction, +}; +use parking_lot::RwLock; -use crate::{collision_manager::basic::BasicCollisionManager, pool::Pool, selection_algorithms::ratio_tip_gas::RatioTipGasSelection, storage::graph::{GraphStorage, GraphStorageIndex}}; +use crate::{ + collision_manager::basic::BasicCollisionManager, config::Config, error::Error, pool::Pool, ports::{ + AtomicView, + ConsensusParametersProvider, + TxPoolPersistentStorage, + }, selection_algorithms::ratio_tip_gas::RatioTipGasSelection, storage::graph::{ + GraphConfig, GraphStorage, GraphStorageIndex + }, transaction_conversion::check_transactions +}; -#[derive(Clone)] -pub struct SharedState { - pool: Pool<, GraphStorage, BasicCollisionManager, RatioTipGasSelection> +pub struct SharedState { + pool: Arc< + RwLock< + Pool< + PSProvider, + GraphStorage, + BasicCollisionManager, + RatioTipGasSelection, + >, + >, + >, + current_height: Arc>, + consensus_parameters_provider: Arc, } -impl SharedState { +impl Clone + for SharedState +{ + fn clone(&self) -> Self { + SharedState { + pool: self.pool.clone(), + current_height: self.current_height.clone(), + consensus_parameters_provider: self.consensus_parameters_provider.clone(), + } + } +} + +impl + SharedState +where + PSProvider: AtomicView, + PSView: TxPoolPersistentStorage, + ConsensusParamsProvider: ConsensusParametersProvider, +{ // TODO: Implement conversion from `Transaction` to `PoolTransaction`. (with all the verifications that it implies): https://github.com/FuelLabs/fuel-core/issues/2186 - fn insert(&mut self, transactions: Vec) -> Vec<()> { + async fn insert( + &mut self, + transactions: Vec, + ) -> Result, Error>>, Error> { + let current_height = *self.current_height.read(); + let (version, params) = self + .consensus_parameters_provider + .latest_consensus_parameters(); + + let checked_txs = check_transactions( + transactions, + current_height, + self.utxo_validation, + params.as_ref(), + &self.gas_price_provider, + self.memory_pool.clone(), + ) + .await; + + let mut valid_txs = vec![]; + + let checked_txs: Vec<_> = checked_txs + .into_iter() + .map(|tx_check| match tx_check { + Ok(tx) => { + valid_txs.push(tx); + None + } + Err(err) => Some(err), + }) + .collect(); // Move verif of wasm there - vec![] + + self.pool.insert(transactions) } - pub fn find_one(&self, tx_id: TxId) -> Option<&Transaction> { - self + pub fn find_one(&self, tx_id: &TxId) -> Option { + self.pool.find_one(tx_id).map(|tx| tx.into()) } } -pub type Service = ServiceRunner; +pub type Service = + ServiceRunner>; -pub struct Task { - shared_state: SharedState, +pub struct Task { + shared_state: SharedState, } #[async_trait::async_trait] -impl RunnableService for Task { +impl RunnableService + for Task +where + PSProvider: AtomicView, + PSView: TxPoolPersistentStorage, + ConsensusParamsProvider: ConsensusParametersProvider + Send + Sync, +{ const NAME: &'static str = "TxPoolv2"; - type SharedData = SharedState; + type SharedData = SharedState; - type Task = Task; + type Task = Task; type TaskParams = (); @@ -55,7 +140,13 @@ impl RunnableService for Task { } #[async_trait::async_trait] -impl RunnableTask for Task { +impl RunnableTask + for Task +where + PSProvider: AtomicView, + PSView: TxPoolPersistentStorage, + ConsensusParamsProvider: ConsensusParametersProvider + Send + Sync, +{ async fn run(&mut self, watcher: &mut StateWatcher) -> anyhow::Result { let should_continue; tokio::select! { @@ -71,8 +162,32 @@ impl RunnableTask for Task { } } -pub fn new_service() -> Service { +pub fn new_service( + ps_provider: PSProvider, + consensus_parameters_provider: ConsensusParamsProvider, + current_height: BlockHeight, + config: Config, +) -> Service +where + PSProvider: AtomicView, + PSView: TxPoolPersistentStorage, + ConsensusParamsProvider: ConsensusParametersProvider + Send + Sync, +{ Service::new(Task { - shared_state: SharedState, + shared_state: SharedState { + // Maybe try to take the arc from the paramater + consensus_parameters_provider: Arc::new(consensus_parameters_provider), + current_height: Arc::new(RwLock::new(current_height)), + pool: Arc::new(RwLock::new(Pool::new( + ps_provider, + // TODO: Real value + GraphStorage::new(GraphConfig { + max_dependent_txn_count: 100 + }), + BasicCollisionManager::new(), + RatioTipGasSelection::new(), + config, + ))), + }, }) } From 460beef3212131d7bcc85b08f8fa87f3bcb91c70 Mon Sep 17 00:00:00 2001 From: AurelienFT Date: Fri, 13 Sep 2024 12:18:24 +0200 Subject: [PATCH 03/28] Add all the verifications in order. Still some todo and compil errors --- crates/services/txpool_v2/Cargo.toml | 1 + crates/services/txpool_v2/src/error.rs | 6 + crates/services/txpool_v2/src/lib.rs | 1 + crates/services/txpool_v2/src/pool.rs | 1 + crates/services/txpool_v2/src/service.rs | 219 ++++++++++++++---- crates/services/txpool_v2/src/tests/pool.rs | 22 -- .../services/txpool_v2/src/verifications.rs | 205 ++++++++++++++++ 7 files changed, 383 insertions(+), 72 deletions(-) create mode 100644 crates/services/txpool_v2/src/verifications.rs diff --git a/crates/services/txpool_v2/Cargo.toml b/crates/services/txpool_v2/Cargo.toml index 8c984c7e3fd..170172ff8f6 100644 --- a/crates/services/txpool_v2/Cargo.toml +++ b/crates/services/txpool_v2/Cargo.toml @@ -19,6 +19,7 @@ fuel-core-services = { workspace = true } fuel-core-storage = { workspace = true, features = ["std"] } fuel-core-types = { workspace = true, features = ["test-helpers"] } num-rational = { workspace = true } +parking_lot = { workspace = true } petgraph = "0.6.5" tokio = { workspace = true, default-features = false, features = ["sync"] } tokio-rayon = { workspace = true } diff --git a/crates/services/txpool_v2/src/error.rs b/crates/services/txpool_v2/src/error.rs index 561266a92e1..0fc2e33eef2 100644 --- a/crates/services/txpool_v2/src/error.rs +++ b/crates/services/txpool_v2/src/error.rs @@ -10,6 +10,8 @@ use fuel_core_types::{ fuel_vm::checked_transaction::CheckError, }; +use crate::ports::WasmValidityError; + #[derive(Debug, derive_more::Display)] pub enum Error { #[display(fmt = "Gas price not found for block height {_0}")] @@ -83,6 +85,10 @@ pub enum Error { NotInsertedLimitHit, #[display(fmt = "Storage error: {_0}")] Storage(String), + #[display(fmt = "Error with Wasm validity: {:?}", _0)] + WasmValidity(WasmValidityError), + #[display(fmt = "Transaction is not inserted. Mint transaction is not allowed")] + MintIsDisallowed, } impl From for Error { diff --git a/crates/services/txpool_v2/src/lib.rs b/crates/services/txpool_v2/src/lib.rs index 5e6bd93f619..25b0e74cf49 100644 --- a/crates/services/txpool_v2/src/lib.rs +++ b/crates/services/txpool_v2/src/lib.rs @@ -10,6 +10,7 @@ mod selection_algorithms; mod service; mod storage; mod transaction_conversion; +mod verifications; type GasPrice = Word; diff --git a/crates/services/txpool_v2/src/pool.rs b/crates/services/txpool_v2/src/pool.rs index b8ffac5761b..f0282f8e463 100644 --- a/crates/services/txpool_v2/src/pool.rs +++ b/crates/services/txpool_v2/src/pool.rs @@ -26,6 +26,7 @@ use crate::{ SelectionAlgorithmStorage, }, storage::Storage, + verifications::FullyVerifiedTx, }; /// The pool is the main component of the txpool service. It is responsible for storing transactions diff --git a/crates/services/txpool_v2/src/service.rs b/crates/services/txpool_v2/src/service.rs index 75781699b9c..36721a83a10 100644 --- a/crates/services/txpool_v2/src/service.rs +++ b/crates/services/txpool_v2/src/service.rs @@ -8,6 +8,7 @@ use fuel_core_services::{ }; use fuel_core_types::{ blockchain::consensus::Consensus, + entities::relayer::transaction, fuel_tx::{ Transaction, TxId, @@ -18,16 +19,35 @@ use fuel_core_types::{ use parking_lot::RwLock; use crate::{ - collision_manager::basic::BasicCollisionManager, config::Config, error::Error, pool::Pool, ports::{ + collision_manager::basic::BasicCollisionManager, + config::Config, + error::Error, + pool::Pool, + ports::{ AtomicView, ConsensusParametersProvider, + GasPriceProvider as GasPriceProviderTrait, + MemoryPool as MemoryPoolTrait, TxPoolPersistentStorage, - }, selection_algorithms::ratio_tip_gas::RatioTipGasSelection, storage::graph::{ - GraphConfig, GraphStorage, GraphStorageIndex - }, transaction_conversion::check_transactions + WasmChecker as WasmCheckerTrait, + }, + selection_algorithms::ratio_tip_gas::RatioTipGasSelection, + storage::graph::{ + GraphConfig, + GraphStorage, + GraphStorageIndex, + }, + transaction_conversion::check_transactions, + verifications::perform_all_verifications, }; -pub struct SharedState { +pub struct SharedState< + PSProvider, + ConsensusParamsProvider, + GasPriceProvider, + WasmChecker, + MemoryPool, +> { pool: Arc< RwLock< Pool< @@ -40,26 +60,56 @@ pub struct SharedState { >, current_height: Arc>, consensus_parameters_provider: Arc, + gas_price_provider: Arc, + wasm_checker: Arc, + memory: Arc, + utxo_validation: bool, } -impl Clone - for SharedState +impl Clone + for SharedState< + PSProvider, + ConsensusParamsProvider, + GasPriceProvider, + WasmChecker, + MemoryPool, + > { fn clone(&self) -> Self { SharedState { pool: self.pool.clone(), current_height: self.current_height.clone(), consensus_parameters_provider: self.consensus_parameters_provider.clone(), + gas_price_provider: self.gas_price_provider.clone(), + wasm_checker: self.wasm_checker.clone(), + memory: self.memory.clone(), + utxo_validation: self.utxo_validation, } } } -impl - SharedState +impl< + PSProvider, + PSView, + ConsensusParamsProvider, + GasPriceProvider, + WasmChecker, + MemoryPool, + > + SharedState< + PSProvider, + ConsensusParamsProvider, + GasPriceProvider, + WasmChecker, + MemoryPool, + > where PSProvider: AtomicView, PSView: TxPoolPersistentStorage, ConsensusParamsProvider: ConsensusParametersProvider, + GasPriceProvider: GasPriceProviderTrait, + WasmChecker: WasmCheckerTrait, + MemoryPool: MemoryPoolTrait + Send + Sync, { // TODO: Implement conversion from `Transaction` to `PoolTransaction`. (with all the verifications that it implies): https://github.com/FuelLabs/fuel-core/issues/2186 async fn insert( @@ -70,59 +120,95 @@ where let (version, params) = self .consensus_parameters_provider .latest_consensus_parameters(); - - let checked_txs = check_transactions( - transactions, - current_height, - self.utxo_validation, - params.as_ref(), - &self.gas_price_provider, - self.memory_pool.clone(), - ) - .await; - - let mut valid_txs = vec![]; - - let checked_txs: Vec<_> = checked_txs - .into_iter() - .map(|tx_check| match tx_check { - Ok(tx) => { - valid_txs.push(tx); - None - } - Err(err) => Some(err), - }) - .collect(); - // Move verif of wasm there - - self.pool.insert(transactions) + let insertable_transactions = vec![]; + for transaction in transactions { + let checked_tx = perform_all_verifications( + transaction, + current_height, + ¶ms, + version, + &self.gas_price_provider, + &self.wasm_checker, + self.memory.clone(), + ) + .await?; + insertable_transactions.push(checked_tx); + } + self.pool.write().insert(insertable_transactions) } pub fn find_one(&self, tx_id: &TxId) -> Option { - self.pool.find_one(tx_id).map(|tx| tx.into()) + self.pool.read().find_one(tx_id).map(|tx| tx.into()) } } -pub type Service = - ServiceRunner>; +pub type Service< + PSProvider, + ConsensusParamsProvider, + GasPriceProvider, + WasmChecker, + MemoryPool, +> = ServiceRunner< + Task, +>; -pub struct Task { - shared_state: SharedState, +pub struct Task< + PSProvider, + ConsensusParamsProvider, + GasPriceProvider, + WasmChecker, + MemoryPool, +> { + shared_state: SharedState< + PSProvider, + ConsensusParamsProvider, + GasPriceProvider, + WasmChecker, + MemoryPool, + >, } #[async_trait::async_trait] -impl RunnableService - for Task +impl< + PSProvider, + PSView, + ConsensusParamsProvider, + GasPriceProvider, + WasmChecker, + MemoryPool, + > RunnableService + for Task< + PSProvider, + ConsensusParamsProvider, + GasPriceProvider, + WasmChecker, + MemoryPool, + > where PSProvider: AtomicView, PSView: TxPoolPersistentStorage, ConsensusParamsProvider: ConsensusParametersProvider + Send + Sync, + GasPriceProvider: GasPriceProviderTrait + Send + Sync, + WasmChecker: WasmCheckerTrait + Send + Sync, + MemoryPool: MemoryPoolTrait + Send + Sync, { const NAME: &'static str = "TxPoolv2"; - type SharedData = SharedState; + type SharedData = SharedState< + PSProvider, + ConsensusParamsProvider, + GasPriceProvider, + WasmChecker, + MemoryPool, + >; - type Task = Task; + type Task = Task< + PSProvider, + ConsensusParamsProvider, + GasPriceProvider, + WasmChecker, + MemoryPool, + >; type TaskParams = (); @@ -140,12 +226,28 @@ where } #[async_trait::async_trait] -impl RunnableTask - for Task +impl< + PSProvider, + PSView, + ConsensusParamsProvider, + GasPriceProvider, + WasmChecker, + MemoryPool, + > RunnableTask + for Task< + PSProvider, + ConsensusParamsProvider, + GasPriceProvider, + WasmChecker, + MemoryPool, + > where PSProvider: AtomicView, PSView: TxPoolPersistentStorage, ConsensusParamsProvider: ConsensusParametersProvider + Send + Sync, + GasPriceProvider: GasPriceProviderTrait + Send + Sync, + WasmChecker: WasmCheckerTrait + Send + Sync, + MemoryPool: MemoryPoolTrait + Send + Sync, { async fn run(&mut self, watcher: &mut StateWatcher) -> anyhow::Result { let should_continue; @@ -162,27 +264,44 @@ where } } -pub fn new_service( +pub fn new_service< + PSProvider, + PSView, + ConsensusParamsProvider, + GasPriceProvider, + WasmChecker, + MemoryPool, +>( + config: Config, ps_provider: PSProvider, consensus_parameters_provider: ConsensusParamsProvider, current_height: BlockHeight, - config: Config, -) -> Service + gas_price_provider: GasPriceProvider, + wasm_checker: WasmChecker, + memory_pool: MemoryPool, +) -> Service where PSProvider: AtomicView, PSView: TxPoolPersistentStorage, ConsensusParamsProvider: ConsensusParametersProvider + Send + Sync, + GasPriceProvider: GasPriceProviderTrait + Send + Sync, + WasmChecker: WasmCheckerTrait + Send + Sync, + MemoryPool: MemoryPoolTrait + Send + Sync, { Service::new(Task { shared_state: SharedState { // Maybe try to take the arc from the paramater consensus_parameters_provider: Arc::new(consensus_parameters_provider), + gas_price_provider: Arc::new(gas_price_provider), + wasm_checker: Arc::new(wasm_checker), + memory: Arc::new(memory_pool), current_height: Arc::new(RwLock::new(current_height)), + utxo_validation: config.utxo_validation, pool: Arc::new(RwLock::new(Pool::new( ps_provider, // TODO: Real value GraphStorage::new(GraphConfig { - max_dependent_txn_count: 100 + max_dependent_txn_count: 100, }), BasicCollisionManager::new(), RatioTipGasSelection::new(), diff --git a/crates/services/txpool_v2/src/tests/pool.rs b/crates/services/txpool_v2/src/tests/pool.rs index 838a6401bc9..c6e136b6b4e 100644 --- a/crates/services/txpool_v2/src/tests/pool.rs +++ b/crates/services/txpool_v2/src/tests/pool.rs @@ -56,28 +56,6 @@ use std::vec; const GAS_LIMIT: Word = 100000; -// TODO: Move out of tests -fn check_tx_to_pool(checked_tx: Checked) -> PoolTransaction { - match checked_tx.into() { - CheckedTransaction::Blob(tx) => { - PoolTransaction::Blob(tx, ConsensusParametersVersion::MIN) - } - CheckedTransaction::Create(tx) => { - PoolTransaction::Create(tx, ConsensusParametersVersion::MIN) - } - CheckedTransaction::Script(tx) => { - PoolTransaction::Script(tx, ConsensusParametersVersion::MIN) - } - CheckedTransaction::Upgrade(tx) => { - PoolTransaction::Upgrade(tx, ConsensusParametersVersion::MIN) - } - CheckedTransaction::Upload(tx) => { - PoolTransaction::Upload(tx, ConsensusParametersVersion::MIN) - } - _ => panic!("Unexpected transaction type"), - } -} - #[tokio::test] async fn insert_simple_tx_succeeds() { let mut context = PoolContext::default(); diff --git a/crates/services/txpool_v2/src/verifications.rs b/crates/services/txpool_v2/src/verifications.rs new file mode 100644 index 00000000000..fa1a1f24404 --- /dev/null +++ b/crates/services/txpool_v2/src/verifications.rs @@ -0,0 +1,205 @@ +use fuel_core_types::{ + blockchain::{ + consensus, + header::ConsensusParametersVersion, + }, + fuel_tx::{ + field::UpgradePurpose as _, + ConsensusParameters, + Transaction, + UpgradePurpose, + }, + fuel_types::BlockHeight, + fuel_vm::{ + checked_transaction::{ + CheckPredicateParams, + CheckPredicates, + Checked, + CheckedTransaction, + Checks, + IntoChecked, + }, + interpreter::Memory, + }, + services::txpool::PoolTransaction, +}; + +use crate::{ + error::Error, + ports::{ + GasPriceProvider, + WasmChecker, + }, + GasPrice, +}; + +#[derive(Debug, Clone, Eq, PartialEq, Hash)] +pub struct UnverifiedTx(Transaction); + +#[derive(Debug, Clone, Eq, PartialEq, Hash)] +pub struct BasicVerifiedTx(Checked); + +#[derive(Debug, Clone, Eq, PartialEq, Hash)] +pub struct InputDependenciesVerifiedTx(Checked); + +#[derive(Debug, Clone, Eq, PartialEq, Hash)] +pub struct InputComputationVerifiedTx(Checked); + +#[derive(Debug, Clone, Eq, PartialEq, Hash)] +pub struct FullyVerifiedTx(Checked); + +pub enum TransactionVerifState { + Unverified(UnverifiedTx), + BasicVerified(BasicVerifiedTx), + InputDependenciesVerified(InputDependenciesVerifiedTx), + InputComputationVerified(InputComputationVerifiedTx), + FullyVerified(FullyVerifiedTx), +} + +impl UnverifiedTx { + pub async fn perform_basic_verifications( + self, + current_height: BlockHeight, + consensus_params: &ConsensusParameters, + gas_price_provider: &impl GasPriceProvider, + ) -> Result { + if self.0.is_mint() { + return Err(Error::NotSupportedTransactionType); + } + let tx = self + .0 + .into_checked_basic(current_height, consensus_params)?; + let gas_price = gas_price_provider.next_gas_price().await?; + let tx = verify_tx_min_gas_price(tx, consensus_params, gas_price)?; + Ok(BasicVerifiedTx(tx)) + } +} + +impl BasicVerifiedTx { + pub fn perform_inputs_verifications( + self, + ) -> Result { + // TODO: Use the pool + Ok(InputDependenciesVerifiedTx(self.0)) + } +} + +impl InputDependenciesVerifiedTx { + pub async fn perform_input_computation_verifications( + self, + consensus_params: &ConsensusParameters, + wasm_checker: &impl WasmChecker, + memory: M, + ) -> Result + where + M: Memory + Send + Sync + 'static, + { + if let Transaction::Upgrade(upgrade) = self.0.transaction() { + if let UpgradePurpose::StateTransition { root } = upgrade.upgrade_purpose() { + wasm_checker + .validate_uploaded_wasm(root) + .map_err(|err| Error::WasmValidity(err))?; + } + } + + let parameters = CheckPredicateParams::from(consensus_params); + let tx = + tokio_rayon::spawn_fifo(move || self.0.check_predicates(¶meters, memory)) + .await?; + + Ok(InputComputationVerifiedTx(tx)) + } +} + +impl InputComputationVerifiedTx { + pub fn perform_final_verifications( + self, + consensus_params: &ConsensusParameters, + ) -> Result { + let tx = self.0.check_signatures(&consensus_params.chain_id())?; + debug_assert!(tx.checks().contains(Checks::all())); + Ok(FullyVerifiedTx(tx)) + } +} + +impl FullyVerifiedTx { + pub fn into_pool_transaction( + self, + version: ConsensusParametersVersion, + ) -> Result { + let tx: CheckedTransaction = self.0.into(); + + match tx { + CheckedTransaction::Script(tx) => Ok(PoolTransaction::Script(tx, version)), + CheckedTransaction::Create(tx) => Ok(PoolTransaction::Create(tx, version)), + CheckedTransaction::Mint(_) => Err(Error::MintIsDisallowed), + CheckedTransaction::Upgrade(tx) => Ok(PoolTransaction::Upgrade(tx, version)), + CheckedTransaction::Upload(tx) => Ok(PoolTransaction::Upload(tx, version)), + CheckedTransaction::Blob(tx) => Ok(PoolTransaction::Blob(tx, version)), + } + } +} + +pub async fn perform_all_verifications( + tx: Transaction, + current_height: BlockHeight, + consensus_params: &ConsensusParameters, + consensus_params_version: ConsensusParametersVersion, + gas_price_provider: &impl GasPriceProvider, + wasm_checker: &impl WasmChecker, + memory: M, +) -> Result +where + M: Memory + Send + Sync + 'static, +{ + let unverified = UnverifiedTx(tx); + let basically_verified_tx = unverified + .perform_basic_verifications(current_height, consensus_params, gas_price_provider) + .await?; + let inputs_verified_tx = basically_verified_tx.perform_inputs_verifications()?; + let input_computation_verified_tx = inputs_verified_tx + .perform_input_computation_verifications(consensus_params, wasm_checker, memory) + .await?; + let fully_verified_tx = + input_computation_verified_tx.perform_final_verifications(consensus_params)?; + fully_verified_tx.into_pool_transaction(consensus_params_version) +} + +fn verify_tx_min_gas_price( + tx: Checked, + consensus_params: &ConsensusParameters, + gas_price: GasPrice, +) -> Result, Error> { + let tx: CheckedTransaction = tx.into(); + let gas_costs = consensus_params.gas_costs(); + let fee_parameters = consensus_params.fee_params(); + let read = match tx { + CheckedTransaction::Script(script) => { + let ready = script.into_ready(gas_price, gas_costs, fee_parameters)?; + let (_, checked) = ready.decompose(); + CheckedTransaction::Script(checked) + } + CheckedTransaction::Create(create) => { + let ready = create.into_ready(gas_price, gas_costs, fee_parameters)?; + let (_, checked) = ready.decompose(); + CheckedTransaction::Create(checked) + } + CheckedTransaction::Upgrade(tx) => { + let ready = tx.into_ready(gas_price, gas_costs, fee_parameters)?; + let (_, checked) = ready.decompose(); + CheckedTransaction::Upgrade(checked) + } + CheckedTransaction::Upload(tx) => { + let ready = tx.into_ready(gas_price, gas_costs, fee_parameters)?; + let (_, checked) = ready.decompose(); + CheckedTransaction::Upload(checked) + } + CheckedTransaction::Blob(tx) => { + let ready = tx.into_ready(gas_price, gas_costs, fee_parameters)?; + let (_, checked) = ready.decompose(); + CheckedTransaction::Blob(checked) + } + CheckedTransaction::Mint(_) => return Err(Error::NotSupportedTransactionType), + }; + Ok(read.into()) +} From 75fb5988b7cd7ae1b317ead76cf1c91d87e8b2c0 Mon Sep 17 00:00:00 2001 From: AurelienFT Date: Fri, 13 Sep 2024 12:42:37 +0200 Subject: [PATCH 04/28] Fix compil errors --- Cargo.lock | 1 + crates/services/txpool_v2/Cargo.toml | 8 ++++++++ crates/services/txpool_v2/src/service.rs | 12 ++++++------ 3 files changed, 15 insertions(+), 6 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 42c667ab29f..fc36374bd1e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3738,6 +3738,7 @@ dependencies = [ "fuel-core-services", "fuel-core-storage", "fuel-core-types", + "mockall", "num-rational", "parking_lot", "petgraph", diff --git a/crates/services/txpool_v2/Cargo.toml b/crates/services/txpool_v2/Cargo.toml index 170172ff8f6..2f080772e1f 100644 --- a/crates/services/txpool_v2/Cargo.toml +++ b/crates/services/txpool_v2/Cargo.toml @@ -18,9 +18,17 @@ derive_more = { workspace = true } fuel-core-services = { workspace = true } fuel-core-storage = { workspace = true, features = ["std"] } fuel-core-types = { workspace = true, features = ["test-helpers"] } +mockall = { workspace = true, optional = true } num-rational = { workspace = true } parking_lot = { workspace = true } petgraph = "0.6.5" tokio = { workspace = true, default-features = false, features = ["sync"] } tokio-rayon = { workspace = true } tracing = { workspace = true } + +[features] +test-helpers = [ + "dep:mockall", + "fuel-core-types/test-helpers", + "fuel-core-storage/test-helpers", +] diff --git a/crates/services/txpool_v2/src/service.rs b/crates/services/txpool_v2/src/service.rs index 36721a83a10..5ac41ec7782 100644 --- a/crates/services/txpool_v2/src/service.rs +++ b/crates/services/txpool_v2/src/service.rs @@ -107,8 +107,8 @@ where PSProvider: AtomicView, PSView: TxPoolPersistentStorage, ConsensusParamsProvider: ConsensusParametersProvider, - GasPriceProvider: GasPriceProviderTrait, - WasmChecker: WasmCheckerTrait, + GasPriceProvider: GasPriceProviderTrait + Send + Sync, + WasmChecker: WasmCheckerTrait + Send + Sync, MemoryPool: MemoryPoolTrait + Send + Sync, { // TODO: Implement conversion from `Transaction` to `PoolTransaction`. (with all the verifications that it implies): https://github.com/FuelLabs/fuel-core/issues/2186 @@ -120,16 +120,16 @@ where let (version, params) = self .consensus_parameters_provider .latest_consensus_parameters(); - let insertable_transactions = vec![]; + let mut insertable_transactions = vec![]; for transaction in transactions { let checked_tx = perform_all_verifications( transaction, current_height, ¶ms, version, - &self.gas_price_provider, - &self.wasm_checker, - self.memory.clone(), + self.gas_price_provider.as_ref(), + self.wasm_checker.as_ref(), + self.memory.get_memory().await, ) .await?; insertable_transactions.push(checked_tx); From a7666fc69e01149138559448b7c6f49cb598ac3a Mon Sep 17 00:00:00 2001 From: AurelienFT Date: Mon, 16 Sep 2024 11:04:33 +0200 Subject: [PATCH 05/28] Finish verification on all inputs --- crates/services/txpool_v2/src/lib.rs | 1 - crates/services/txpool_v2/src/pool.rs | 42 +++++- crates/services/txpool_v2/src/service.rs | 34 +++-- .../services/txpool_v2/src/storage/graph.rs | 34 ++++- crates/services/txpool_v2/src/storage/mod.rs | 15 +- .../services/txpool_v2/src/tests/context.rs | 49 ------ .../txpool_v2/src/transaction_conversion.rs | 139 ------------------ .../services/txpool_v2/src/verifications.rs | 58 +++++--- 8 files changed, 133 insertions(+), 239 deletions(-) delete mode 100644 crates/services/txpool_v2/src/transaction_conversion.rs diff --git a/crates/services/txpool_v2/src/lib.rs b/crates/services/txpool_v2/src/lib.rs index 25b0e74cf49..d79b329d46a 100644 --- a/crates/services/txpool_v2/src/lib.rs +++ b/crates/services/txpool_v2/src/lib.rs @@ -9,7 +9,6 @@ mod ports; mod selection_algorithms; mod service; mod storage; -mod transaction_conversion; mod verifications; type GasPrice = Word; diff --git a/crates/services/txpool_v2/src/pool.rs b/crates/services/txpool_v2/src/pool.rs index 1afe0310894..b374b3bde30 100644 --- a/crates/services/txpool_v2/src/pool.rs +++ b/crates/services/txpool_v2/src/pool.rs @@ -3,8 +3,10 @@ use std::collections::HashMap; use fuel_core_types::{ fuel_tx::{ field::BlobId, + Transaction, TxId, }, + fuel_vm::checked_transaction::Checked, services::txpool::PoolTransaction, }; use tracing::instrument; @@ -93,17 +95,18 @@ where let collisions = self .collision_manager .collect_colliding_transactions(&tx, &self.storage)?; - let dependencies = self.storage.collect_dependencies_transactions( - &tx, - collisions.reasons, - &latest_view, - self.config.utxo_validation, - )?; + let dependencies = + self.storage.validate_inputs_and_collect_dependencies( + &tx, + collisions.reasons, + &latest_view, + self.config.utxo_validation, + )?; let has_dependencies = !dependencies.is_empty(); let (storage_id, removed_transactions) = self.storage.store_transaction( tx, - dependencies, - collisions.colliding_txs, + &dependencies, + &collisions.colliding_txs, )?; self.tx_id_to_storage_id.insert(tx_id, storage_id); // No dependencies directly in the graph and the sorted transactions @@ -120,6 +123,29 @@ where .collect()) } + /// Check if a transaction can be inserted into the pool. + pub fn can_insert_transaction(&self, tx: &PoolTransaction) -> Result<(), Error> { + let persistent_storage = self + .persistent_storage_provider + .latest_view() + .map_err(|e| Error::Database(format!("{:?}", e)))?; + self.check_pool_is_not_full()?; + self.config.black_list.check_blacklisting(tx)?; + Self::check_blob_does_not_exist(tx, &persistent_storage)?; + let collisions = self + .collision_manager + .collect_colliding_transactions(tx, &self.storage)?; + let dependencies = self.storage.validate_inputs_and_collect_dependencies( + tx, + collisions.reasons, + &persistent_storage, + self.config.utxo_validation, + )?; + self.storage + .can_store_transaction(tx, &dependencies, &collisions.colliding_txs); + Ok(()) + } + // TODO: Use block space also (https://github.com/FuelLabs/fuel-core/issues/2133) /// Extract transactions for a block. /// Returns a list of transactions that were selected for the block diff --git a/crates/services/txpool_v2/src/service.rs b/crates/services/txpool_v2/src/service.rs index 5ac41ec7782..ae103a2ce26 100644 --- a/crates/services/txpool_v2/src/service.rs +++ b/crates/services/txpool_v2/src/service.rs @@ -37,10 +37,23 @@ use crate::{ GraphStorage, GraphStorageIndex, }, - transaction_conversion::check_transactions, verifications::perform_all_verifications, }; +pub type RemovedTransactions = Vec; +pub type InsertionResult = Result; + +pub type TxPool = Arc< + RwLock< + Pool< + PSProvider, + GraphStorage, + BasicCollisionManager, + RatioTipGasSelection, + >, + >, +>; + pub struct SharedState< PSProvider, ConsensusParamsProvider, @@ -48,16 +61,7 @@ pub struct SharedState< WasmChecker, MemoryPool, > { - pool: Arc< - RwLock< - Pool< - PSProvider, - GraphStorage, - BasicCollisionManager, - RatioTipGasSelection, - >, - >, - >, + pool: TxPool, current_height: Arc>, consensus_parameters_provider: Arc, gas_price_provider: Arc, @@ -111,11 +115,10 @@ where WasmChecker: WasmCheckerTrait + Send + Sync, MemoryPool: MemoryPoolTrait + Send + Sync, { - // TODO: Implement conversion from `Transaction` to `PoolTransaction`. (with all the verifications that it implies): https://github.com/FuelLabs/fuel-core/issues/2186 async fn insert( &mut self, transactions: Vec, - ) -> Result, Error>>, Error> { + ) -> Result, Error> { let current_height = *self.current_height.read(); let (version, params) = self .consensus_parameters_provider @@ -124,6 +127,7 @@ where for transaction in transactions { let checked_tx = perform_all_verifications( transaction, + self.pool.clone(), current_height, ¶ms, version, @@ -290,7 +294,6 @@ where { Service::new(Task { shared_state: SharedState { - // Maybe try to take the arc from the paramater consensus_parameters_provider: Arc::new(consensus_parameters_provider), gas_price_provider: Arc::new(gas_price_provider), wasm_checker: Arc::new(wasm_checker), @@ -299,9 +302,8 @@ where utxo_validation: config.utxo_validation, pool: Arc::new(RwLock::new(Pool::new( ps_provider, - // TODO: Real value GraphStorage::new(GraphConfig { - max_dependent_txn_count: 100, + max_dependent_txn_count: config.max_dependent_txn_count, }), BasicCollisionManager::new(), RatioTipGasSelection::new(), diff --git a/crates/services/txpool_v2/src/storage/graph.rs b/crates/services/txpool_v2/src/storage/graph.rs index 61e488eb791..259ab155e09 100644 --- a/crates/services/txpool_v2/src/storage/graph.rs +++ b/crates/services/txpool_v2/src/storage/graph.rs @@ -266,8 +266,8 @@ impl Storage for GraphStorage { fn store_transaction( &mut self, transaction: PoolTransaction, - dependencies: Vec, - collided_transactions: Vec, + dependencies: &[Self::StorageIndex], + collided_transactions: &[Self::StorageIndex], ) -> Result<(Self::StorageIndex, RemovedTransactions), Error> { let tx_id = transaction.id(); @@ -275,7 +275,7 @@ impl Storage for GraphStorage { let mut removed_transactions = vec![]; for collision in collided_transactions { removed_transactions - .extend(self.remove_node_and_dependent_sub_graph(collision)?); + .extend(self.remove_node_and_dependent_sub_graph(*collision)?); } // Add the new transaction to the graph and update the others in consequence let tip = transaction.tip(); @@ -292,7 +292,7 @@ impl Storage for GraphStorage { let mut whole_tx_chain = vec![]; // Check if the dependency chain is too big - let mut to_check = dependencies.clone(); + let mut to_check = dependencies.to_vec(); while let Some(node_id) = to_check.pop() { let Some(dependency_node) = self.graph.node_weight(node_id) else { return Err(Error::Storage(format!( @@ -311,7 +311,7 @@ impl Storage for GraphStorage { // Add the transaction to the graph let node_id = self.graph.add_node(node); for dependency in dependencies { - self.graph.add_edge(dependency, node_id, ()); + self.graph.add_edge(*dependency, node_id, ()); } self.cache_tx_infos(&outputs, &tx_id, node_id)?; @@ -332,6 +332,28 @@ impl Storage for GraphStorage { Ok((node_id, removed_transactions)) } + fn can_store_transaction( + &self, + transaction: &PoolTransaction, + dependencies: &[Self::StorageIndex], + collided_transactions: &[Self::StorageIndex], + ) -> Result<(), Error> { + let mut to_check = dependencies.to_vec(); + while let Some(node_id) = to_check.pop() { + let Some(dependency_node) = self.graph.node_weight(node_id) else { + return Err(Error::Storage(format!( + "Node with id {:?} not found", + node_id + ))); + }; + if dependency_node.number_txs_in_chain >= self.config.max_dependent_txn_count + { + return Err(Error::NotInsertedChainDependencyTooBig); + } + } + Ok(()) + } + fn get(&self, index: &Self::StorageIndex) -> Result<&StorageData, Error> { self.get_inner(index) } @@ -353,7 +375,7 @@ impl Storage for GraphStorage { self.get_dependents_inner(&index) } - fn collect_dependencies_transactions( + fn validate_inputs_and_collect_dependencies( &self, transaction: &PoolTransaction, collisions: std::collections::HashSet, diff --git a/crates/services/txpool_v2/src/storage/mod.rs b/crates/services/txpool_v2/src/storage/mod.rs index 45c4ff466ab..18e63fa22ae 100644 --- a/crates/services/txpool_v2/src/storage/mod.rs +++ b/crates/services/txpool_v2/src/storage/mod.rs @@ -40,10 +40,19 @@ pub trait Storage { fn store_transaction( &mut self, transaction: PoolTransaction, - dependencies: Vec, - collided_transactions: Vec, + dependencies: &[Self::StorageIndex], + collided_transactions: &[Self::StorageIndex], ) -> Result<(Self::StorageIndex, RemovedTransactions), Error>; + /// Check if a transaction could be stored in the storage. This shouldn't be expected to be called before store_transaction. + /// Its just a way to perform some checks without storing the transaction. + fn can_store_transaction( + &self, + transaction: &PoolTransaction, + dependencies: &[Self::StorageIndex], + collided_transactions: &[Self::StorageIndex], + ) -> Result<(), Error>; + /// Get the storage data by its index. fn get(&self, index: &Self::StorageIndex) -> Result<&StorageData, Error>; @@ -62,7 +71,7 @@ pub trait Storage { /// Collect the storage indexes of the transactions that are dependent on the given transaction. /// The collisions can be useful as they implies that some verifications had already been done. /// Returns the storage indexes of the dependencies transactions. - fn collect_dependencies_transactions( + fn validate_inputs_and_collect_dependencies( &self, transaction: &PoolTransaction, collisions: HashSet, diff --git a/crates/services/txpool_v2/src/tests/context.rs b/crates/services/txpool_v2/src/tests/context.rs index f1a8d156003..3907080d4c4 100644 --- a/crates/services/txpool_v2/src/tests/context.rs +++ b/crates/services/txpool_v2/src/tests/context.rs @@ -70,7 +70,6 @@ use crate::{ MockDBProvider, MockDb, }, - transaction_conversion::check_single_tx, GasPrice, }; // TDOO: Reorganize this file @@ -338,51 +337,3 @@ impl GasPriceProvider for MockTxPoolGasPrice { .ok_or(Error::GasPriceNotFound("Gas price not found".to_string())) } } - -pub async fn check_unwrap_tx(tx: Transaction, config: &Config) -> Checked { - let gas_price = 0; - check_unwrap_tx_with_gas_price(tx, config, gas_price).await -} - -pub async fn check_unwrap_tx_with_gas_price( - tx: Transaction, - config: &Config, - gas_price: GasPrice, -) -> Checked { - let gas_price_provider = MockTxPoolGasPrice::new(gas_price); - check_single_tx( - tx, - Default::default(), - config.utxo_validation, - &ConsensusParameters::default(), - &gas_price_provider, - MemoryInstance::new(), - ) - .await - .expect("Transaction should be checked") -} - -pub async fn check_tx( - tx: Transaction, - config: &Config, -) -> Result, Error> { - let gas_price = 0; - check_tx_with_gas_price(tx, config, gas_price).await -} - -pub async fn check_tx_with_gas_price( - tx: Transaction, - config: &Config, - gas_price: GasPrice, -) -> Result, Error> { - let gas_price_provider = MockTxPoolGasPrice::new(gas_price); - check_single_tx( - tx, - Default::default(), - config.utxo_validation, - &ConsensusParameters::default(), - &gas_price_provider, - MemoryInstance::new(), - ) - .await -} diff --git a/crates/services/txpool_v2/src/transaction_conversion.rs b/crates/services/txpool_v2/src/transaction_conversion.rs deleted file mode 100644 index 071b45fb3bd..00000000000 --- a/crates/services/txpool_v2/src/transaction_conversion.rs +++ /dev/null @@ -1,139 +0,0 @@ -use std::sync::Arc; - -use fuel_core_types::{ - fuel_tx::{ - ConsensusParameters, - Transaction, - }, - fuel_types::BlockHeight, - fuel_vm::{ - checked_transaction::{ - CheckPredicateParams, - CheckPredicates, - Checked, - CheckedTransaction, - Checks, - IntoChecked, - }, - interpreter::Memory, - }, -}; - -use crate::{ - error::Error, - ports::{ - GasPriceProvider, - MemoryPool, - }, - GasPrice, -}; - -pub async fn check_transactions( - txs: Vec, - current_height: BlockHeight, - utxp_validation: bool, - consensus_params: &ConsensusParameters, - gas_price_provider: &Provider, - memory_pool: Arc, -) -> Vec, Error>> -where - Provider: GasPriceProvider, - MP: MemoryPool, -{ - let mut checked_txs = Vec::with_capacity(txs.len()); - - for tx in txs.into_iter() { - checked_txs.push( - check_single_tx( - tx, - current_height, - utxp_validation, - consensus_params, - gas_price_provider, - memory_pool.get_memory().await, - ) - .await, - ); - } - - checked_txs -} - -pub async fn check_single_tx( - tx: Transaction, - current_height: BlockHeight, - utxo_validation: bool, - consensus_params: &ConsensusParameters, - gas_price_provider: &GasPrice, - memory: M, -) -> Result, Error> -where - GasPrice: GasPriceProvider, - M: Memory + Send + Sync + 'static, -{ - if tx.is_mint() { - return Err(Error::NotSupportedTransactionType); - } - - let tx: Checked = if utxo_validation { - let tx = tx - .into_checked_basic(current_height, consensus_params)? - .check_signatures(&consensus_params.chain_id())?; - - let parameters = CheckPredicateParams::from(consensus_params); - let tx = - tokio_rayon::spawn_fifo(move || tx.check_predicates(¶meters, memory)) - .await?; - - debug_assert!(tx.checks().contains(Checks::all())); - - tx - } else { - tx.into_checked_basic(current_height, consensus_params)? - }; - - let gas_price = gas_price_provider.next_gas_price().await?; - - let tx = verify_tx_min_gas_price(tx, consensus_params, gas_price)?; - - Ok(tx) -} - -fn verify_tx_min_gas_price( - tx: Checked, - consensus_params: &ConsensusParameters, - gas_price: GasPrice, -) -> Result, Error> { - let tx: CheckedTransaction = tx.into(); - let gas_costs = consensus_params.gas_costs(); - let fee_parameters = consensus_params.fee_params(); - let ready_tx = match tx { - CheckedTransaction::Script(script) => { - let ready = script.into_ready(gas_price, gas_costs, fee_parameters)?; - let (_, checked) = ready.decompose(); - CheckedTransaction::Script(checked) - } - CheckedTransaction::Create(create) => { - let ready = create.into_ready(gas_price, gas_costs, fee_parameters)?; - let (_, checked) = ready.decompose(); - CheckedTransaction::Create(checked) - } - CheckedTransaction::Upgrade(tx) => { - let ready = tx.into_ready(gas_price, gas_costs, fee_parameters)?; - let (_, checked) = ready.decompose(); - CheckedTransaction::Upgrade(checked) - } - CheckedTransaction::Upload(tx) => { - let ready = tx.into_ready(gas_price, gas_costs, fee_parameters)?; - let (_, checked) = ready.decompose(); - CheckedTransaction::Upload(checked) - } - CheckedTransaction::Blob(tx) => { - let ready = tx.into_ready(gas_price, gas_costs, fee_parameters)?; - let (_, checked) = ready.decompose(); - CheckedTransaction::Blob(checked) - } - CheckedTransaction::Mint(_) => return Err(Error::NotSupportedTransactionType), - }; - Ok(ready_tx.into()) -} diff --git a/crates/services/txpool_v2/src/verifications.rs b/crates/services/txpool_v2/src/verifications.rs index fa1a1f24404..1787cf66443 100644 --- a/crates/services/txpool_v2/src/verifications.rs +++ b/crates/services/txpool_v2/src/verifications.rs @@ -26,10 +26,14 @@ use fuel_core_types::{ use crate::{ error::Error, + pool::Pool, ports::{ + AtomicView, GasPriceProvider, + TxPoolPersistentStorage, WasmChecker, }, + service::TxPool, GasPrice, }; @@ -76,11 +80,21 @@ impl UnverifiedTx { } impl BasicVerifiedTx { - pub fn perform_inputs_verifications( + pub fn perform_inputs_verifications( self, - ) -> Result { - // TODO: Use the pool - Ok(InputDependenciesVerifiedTx(self.0)) + pool: TxPool, + version: ConsensusParametersVersion, + ) -> Result + where + PSProvider: AtomicView, + PSView: TxPoolPersistentStorage, + { + let checked_transaction = self.0.into(); + let pool_tx = checked_tx_into_pool(checked_transaction, version)?; + + pool.read().can_insert_transaction(&pool_tx)?; + let checked_tx: CheckedTransaction = (&pool_tx).into(); + Ok(InputDependenciesVerifiedTx(checked_tx.into())) } } @@ -98,7 +112,7 @@ impl InputDependenciesVerifiedTx { if let UpgradePurpose::StateTransition { root } = upgrade.upgrade_purpose() { wasm_checker .validate_uploaded_wasm(root) - .map_err(|err| Error::WasmValidity(err))?; + .map_err(Error::WasmValidity)?; } } @@ -127,21 +141,14 @@ impl FullyVerifiedTx { self, version: ConsensusParametersVersion, ) -> Result { - let tx: CheckedTransaction = self.0.into(); - - match tx { - CheckedTransaction::Script(tx) => Ok(PoolTransaction::Script(tx, version)), - CheckedTransaction::Create(tx) => Ok(PoolTransaction::Create(tx, version)), - CheckedTransaction::Mint(_) => Err(Error::MintIsDisallowed), - CheckedTransaction::Upgrade(tx) => Ok(PoolTransaction::Upgrade(tx, version)), - CheckedTransaction::Upload(tx) => Ok(PoolTransaction::Upload(tx, version)), - CheckedTransaction::Blob(tx) => Ok(PoolTransaction::Blob(tx, version)), - } + checked_tx_into_pool(self.0.into(), version) } } -pub async fn perform_all_verifications( +#[allow(clippy::too_many_arguments)] +pub async fn perform_all_verifications( tx: Transaction, + pool: TxPool, current_height: BlockHeight, consensus_params: &ConsensusParameters, consensus_params_version: ConsensusParametersVersion, @@ -151,12 +158,15 @@ pub async fn perform_all_verifications( ) -> Result where M: Memory + Send + Sync + 'static, + PSProvider: AtomicView, + PSView: TxPoolPersistentStorage, { let unverified = UnverifiedTx(tx); let basically_verified_tx = unverified .perform_basic_verifications(current_height, consensus_params, gas_price_provider) .await?; - let inputs_verified_tx = basically_verified_tx.perform_inputs_verifications()?; + let inputs_verified_tx = basically_verified_tx + .perform_inputs_verifications(pool, consensus_params_version)?; let input_computation_verified_tx = inputs_verified_tx .perform_input_computation_verifications(consensus_params, wasm_checker, memory) .await?; @@ -203,3 +213,17 @@ fn verify_tx_min_gas_price( }; Ok(read.into()) } + +pub fn checked_tx_into_pool( + tx: CheckedTransaction, + version: ConsensusParametersVersion, +) -> Result { + match tx { + CheckedTransaction::Script(tx) => Ok(PoolTransaction::Script(tx, version)), + CheckedTransaction::Create(tx) => Ok(PoolTransaction::Create(tx, version)), + CheckedTransaction::Mint(_) => Err(Error::MintIsDisallowed), + CheckedTransaction::Upgrade(tx) => Ok(PoolTransaction::Upgrade(tx, version)), + CheckedTransaction::Upload(tx) => Ok(PoolTransaction::Upload(tx, version)), + CheckedTransaction::Blob(tx) => Ok(PoolTransaction::Blob(tx, version)), + } +} From 4426134f5637b9555703d1dd3b32c7b66a88fc3f Mon Sep 17 00:00:00 2001 From: AurelienFT Date: Tue, 17 Sep 2024 10:56:23 +0200 Subject: [PATCH 06/28] fix verifications to make no copy --- crates/services/txpool_v2/src/service.rs | 5 +-- crates/services/txpool_v2/src/tests/pool.rs | 4 --- .../services/txpool_v2/src/verifications.rs | 13 ++++--- crates/types/src/services/txpool.rs | 34 +++++++++++++++++++ 4 files changed, 41 insertions(+), 15 deletions(-) diff --git a/crates/services/txpool_v2/src/service.rs b/crates/services/txpool_v2/src/service.rs index ae103a2ce26..4872db7c8b7 100644 --- a/crates/services/txpool_v2/src/service.rs +++ b/crates/services/txpool_v2/src/service.rs @@ -14,6 +14,7 @@ use fuel_core_types::{ TxId, }, fuel_types::BlockHeight, + fuel_vm::checked_transaction::CheckedTransaction, services::txpool::PoolTransaction, }; use parking_lot::RwLock; @@ -140,10 +141,6 @@ where } self.pool.write().insert(insertable_transactions) } - - pub fn find_one(&self, tx_id: &TxId) -> Option { - self.pool.read().find_one(tx_id).map(|tx| tx.into()) - } } pub type Service< diff --git a/crates/services/txpool_v2/src/tests/pool.rs b/crates/services/txpool_v2/src/tests/pool.rs index 16c71e6777b..94d249e75d0 100644 --- a/crates/services/txpool_v2/src/tests/pool.rs +++ b/crates/services/txpool_v2/src/tests/pool.rs @@ -4,10 +4,6 @@ use crate::{ config::Config, error::Error, tests::context::{ - check_tx, - check_tx_with_gas_price, - check_unwrap_tx, - check_unwrap_tx_with_gas_price, create_coin_output, create_contract_input, create_contract_output, diff --git a/crates/services/txpool_v2/src/verifications.rs b/crates/services/txpool_v2/src/verifications.rs index 1787cf66443..c38621fa855 100644 --- a/crates/services/txpool_v2/src/verifications.rs +++ b/crates/services/txpool_v2/src/verifications.rs @@ -41,7 +41,7 @@ use crate::{ pub struct UnverifiedTx(Transaction); #[derive(Debug, Clone, Eq, PartialEq, Hash)] -pub struct BasicVerifiedTx(Checked); +pub struct BasicVerifiedTx(CheckedTransaction); #[derive(Debug, Clone, Eq, PartialEq, Hash)] pub struct InputDependenciesVerifiedTx(Checked); @@ -89,12 +89,11 @@ impl BasicVerifiedTx { PSProvider: AtomicView, PSView: TxPoolPersistentStorage, { - let checked_transaction = self.0.into(); - let pool_tx = checked_tx_into_pool(checked_transaction, version)?; + let pool_tx = checked_tx_into_pool(self.0, version)?; pool.read().can_insert_transaction(&pool_tx)?; - let checked_tx: CheckedTransaction = (&pool_tx).into(); - Ok(InputDependenciesVerifiedTx(checked_tx.into())) + let checked_transaction: CheckedTransaction = pool_tx.into(); + Ok(InputDependenciesVerifiedTx(checked_transaction.into())) } } @@ -179,7 +178,7 @@ fn verify_tx_min_gas_price( tx: Checked, consensus_params: &ConsensusParameters, gas_price: GasPrice, -) -> Result, Error> { +) -> Result { let tx: CheckedTransaction = tx.into(); let gas_costs = consensus_params.gas_costs(); let fee_parameters = consensus_params.fee_params(); @@ -211,7 +210,7 @@ fn verify_tx_min_gas_price( } CheckedTransaction::Mint(_) => return Err(Error::NotSupportedTransactionType), }; - Ok(read.into()) + Ok(read) } pub fn checked_tx_into_pool( diff --git a/crates/types/src/services/txpool.rs b/crates/types/src/services/txpool.rs index e1b047e27ff..22ed0d9eade 100644 --- a/crates/types/src/services/txpool.rs +++ b/crates/types/src/services/txpool.rs @@ -160,6 +160,40 @@ impl PoolTransaction { } } +impl From for Transaction { + fn from(tx: PoolTransaction) -> Self { + match tx { + PoolTransaction::Script(tx, _) => { + Transaction::Script(Into::<(Script, _)>::into(tx).0) + } + PoolTransaction::Create(tx, _) => { + Transaction::Create(Into::<(Create, _)>::into(tx).0) + } + PoolTransaction::Upgrade(tx, _) => { + Transaction::Upgrade(Into::<(Upgrade, _)>::into(tx).0) + } + PoolTransaction::Upload(tx, _) => { + Transaction::Upload(Into::<(Upload, _)>::into(tx).0) + } + PoolTransaction::Blob(tx, _) => { + Transaction::Blob(Into::<(Blob, _)>::into(tx).0) + } + } + } +} + +impl From for CheckedTransaction { + fn from(tx: PoolTransaction) -> Self { + match tx { + PoolTransaction::Script(tx, _) => CheckedTransaction::Script(tx), + PoolTransaction::Create(tx, _) => CheckedTransaction::Create(tx), + PoolTransaction::Upgrade(tx, _) => CheckedTransaction::Upgrade(tx), + PoolTransaction::Upload(tx, _) => CheckedTransaction::Upload(tx), + PoolTransaction::Blob(tx, _) => CheckedTransaction::Blob(tx), + } + } +} + impl From<&PoolTransaction> for Transaction { fn from(tx: &PoolTransaction) -> Self { match tx { From 86a7c869cb9168429d58254515ac76e1d8b47461 Mon Sep 17 00:00:00 2001 From: AurelienFT Date: Tue, 17 Sep 2024 14:59:51 +0200 Subject: [PATCH 07/28] Reorganize insertion and opti some code. Start rework of tests. --- crates/services/txpool_v2/src/config.rs | 1 + crates/services/txpool_v2/src/pool.rs | 81 +- crates/services/txpool_v2/src/service.rs | 9 +- .../services/txpool_v2/src/storage/graph.rs | 9 +- crates/services/txpool_v2/src/storage/mod.rs | 2 +- .../services/txpool_v2/src/tests/context.rs | 233 +- .../src/tests/{mock_db.rs => mocks.rs} | 58 +- crates/services/txpool_v2/src/tests/mod.rs | 2 +- crates/services/txpool_v2/src/tests/pool.rs | 2561 ++++++++--------- 9 files changed, 1455 insertions(+), 1501 deletions(-) rename crates/services/txpool_v2/src/tests/{mock_db.rs => mocks.rs} (67%) diff --git a/crates/services/txpool_v2/src/config.rs b/crates/services/txpool_v2/src/config.rs index 232634a598f..ff43bc386e9 100644 --- a/crates/services/txpool_v2/src/config.rs +++ b/crates/services/txpool_v2/src/config.rs @@ -115,6 +115,7 @@ impl BlackList { } } +#[derive(Clone)] pub struct Config { /// Enable UTXO validation (will check if UTXO exists in the database and has correct data). pub utxo_validation: bool, diff --git a/crates/services/txpool_v2/src/pool.rs b/crates/services/txpool_v2/src/pool.rs index b374b3bde30..8ae242a1cdc 100644 --- a/crates/services/txpool_v2/src/pool.rs +++ b/crates/services/txpool_v2/src/pool.rs @@ -77,50 +77,41 @@ where /// Each result is a list of transactions that were removed from the pool /// because of the insertion of the new transaction. #[instrument(skip(self))] - pub fn insert( - &mut self, - transactions: Vec, - ) -> Result, Error>>, Error> { - Ok(transactions - .into_iter() - .map(|tx| { - let latest_view = self - .persistent_storage_provider - .latest_view() - .map_err(|e| Error::Database(format!("{:?}", e)))?; - let tx_id = tx.id(); - self.check_pool_is_not_full()?; - self.config.black_list.check_blacklisting(&tx)?; - Self::check_blob_does_not_exist(&tx, &latest_view)?; - let collisions = self - .collision_manager - .collect_colliding_transactions(&tx, &self.storage)?; - let dependencies = - self.storage.validate_inputs_and_collect_dependencies( - &tx, - collisions.reasons, - &latest_view, - self.config.utxo_validation, - )?; - let has_dependencies = !dependencies.is_empty(); - let (storage_id, removed_transactions) = self.storage.store_transaction( - tx, - &dependencies, - &collisions.colliding_txs, - )?; - self.tx_id_to_storage_id.insert(tx_id, storage_id); - // No dependencies directly in the graph and the sorted transactions - if !has_dependencies { - self.selection_algorithm - .new_executable_transactions(vec![storage_id], &self.storage)?; - } - self.update_components_and_caches_on_removal(&removed_transactions)?; - let tx = Storage::get(&self.storage, &storage_id)?; - self.collision_manager - .on_stored_transaction(&tx.transaction, storage_id)?; - Ok(removed_transactions) - }) - .collect()) + pub fn insert(&mut self, tx: PoolTransaction) -> Result, Error> { + let latest_view = self + .persistent_storage_provider + .latest_view() + .map_err(|e| Error::Database(format!("{:?}", e)))?; + let tx_id = tx.id(); + self.check_pool_is_not_full()?; + self.config.black_list.check_blacklisting(&tx)?; + Self::check_blob_does_not_exist(&tx, &latest_view)?; + let collisions = self + .collision_manager + .collect_colliding_transactions(&tx, &self.storage)?; + let dependencies = self.storage.validate_inputs_and_collect_dependencies( + &tx, + collisions.reasons, + &latest_view, + self.config.utxo_validation, + )?; + let has_dependencies = !dependencies.is_empty(); + let (storage_id, removed_transactions) = self.storage.store_transaction( + tx, + &dependencies, + &collisions.colliding_txs, + )?; + self.tx_id_to_storage_id.insert(tx_id, storage_id); + // No dependencies directly in the graph and the sorted transactions + if !has_dependencies { + self.selection_algorithm + .new_executable_transactions(vec![storage_id], &self.storage)?; + } + self.update_components_and_caches_on_removal(&removed_transactions)?; + let tx = Storage::get(&self.storage, &storage_id)?; + self.collision_manager + .on_stored_transaction(&tx.transaction, storage_id)?; + Ok(removed_transactions) } /// Check if a transaction can be inserted into the pool. @@ -186,7 +177,7 @@ where } fn check_pool_is_not_full(&self) -> Result<(), Error> { - if self.storage.count() >= self.config.max_txs { + if self.storage.count() >= self.config.max_txs as usize { return Err(Error::NotInsertedLimitHit); } Ok(()) diff --git a/crates/services/txpool_v2/src/service.rs b/crates/services/txpool_v2/src/service.rs index 4872db7c8b7..b29f7c269d9 100644 --- a/crates/services/txpool_v2/src/service.rs +++ b/crates/services/txpool_v2/src/service.rs @@ -124,7 +124,7 @@ where let (version, params) = self .consensus_parameters_provider .latest_consensus_parameters(); - let mut insertable_transactions = vec![]; + let mut results = vec![]; for transaction in transactions { let checked_tx = perform_all_verifications( transaction, @@ -137,9 +137,12 @@ where self.memory.get_memory().await, ) .await?; - insertable_transactions.push(checked_tx); + let result = { + let mut pool = self.pool.write(); + pool.insert(checked_tx) + }; } - self.pool.write().insert(insertable_transactions) + Ok(results) } } diff --git a/crates/services/txpool_v2/src/storage/graph.rs b/crates/services/txpool_v2/src/storage/graph.rs index 259ab155e09..260a148854f 100644 --- a/crates/services/txpool_v2/src/storage/graph.rs +++ b/crates/services/txpool_v2/src/storage/graph.rs @@ -338,9 +338,8 @@ impl Storage for GraphStorage { dependencies: &[Self::StorageIndex], collided_transactions: &[Self::StorageIndex], ) -> Result<(), Error> { - let mut to_check = dependencies.to_vec(); - while let Some(node_id) = to_check.pop() { - let Some(dependency_node) = self.graph.node_weight(node_id) else { + for node_id in dependencies.iter() { + let Some(dependency_node) = self.graph.node_weight(*node_id) else { return Err(Error::Storage(format!( "Node with id {:?} not found", node_id @@ -471,8 +470,8 @@ impl Storage for GraphStorage { }) } - fn count(&self) -> u64 { - self.graph.node_count() as u64 + fn count(&self) -> usize { + self.graph.node_count() } } diff --git a/crates/services/txpool_v2/src/storage/mod.rs b/crates/services/txpool_v2/src/storage/mod.rs index 18e63fa22ae..279865d23de 100644 --- a/crates/services/txpool_v2/src/storage/mod.rs +++ b/crates/services/txpool_v2/src/storage/mod.rs @@ -87,5 +87,5 @@ pub trait Storage { ) -> Result; /// Count the number of transactions in the storage. - fn count(&self) -> u64; + fn count(&self) -> usize; } diff --git a/crates/services/txpool_v2/src/tests/context.rs b/crates/services/txpool_v2/src/tests/context.rs index 3907080d4c4..01a8a973446 100644 --- a/crates/services/txpool_v2/src/tests/context.rs +++ b/crates/services/txpool_v2/src/tests/context.rs @@ -1,3 +1,5 @@ +use std::sync::Arc; + use fuel_core_storage::transactional::AtomicView; use fuel_core_types::{ entities::{ @@ -48,7 +50,9 @@ use fuel_core_types::{ }, interpreter::MemoryInstance, }, + services::txpool::PoolTransaction, }; +use parking_lot::RwLock; use petgraph::graph::NodeIndex; use crate::{ @@ -62,19 +66,29 @@ use crate::{ WasmValidityError, }, selection_algorithms::ratio_tip_gas::RatioTipGasSelection, + service::{ + RemovedTransactions, + TxPool, + }, storage::graph::{ GraphConfig, GraphStorage, }, - tests::mock_db::{ + tests::mocks::{ MockDBProvider, MockDb, }, + verifications::perform_all_verifications, GasPrice, }; + +use super::mocks::{ + MockTxPoolGasPrice, + MockWasmChecker, +}; // TDOO: Reorganize this file -pub(crate) fn create_message_predicate_from_message( +pub fn create_message_predicate_from_message( amount: Word, nonce: u64, ) -> (Message, Input) { @@ -103,11 +117,11 @@ pub(crate) fn create_message_predicate_from_message( ) } -pub(crate) fn create_coin_output() -> Output { +pub fn create_coin_output() -> Output { Output::coin(Default::default(), Default::default(), Default::default()) } -pub(crate) fn create_contract_input( +pub fn create_contract_input( tx_id: TxId, output_index: u16, contract_id: ContractId, @@ -121,81 +135,154 @@ pub(crate) fn create_contract_input( ) } -pub(crate) fn create_contract_output(contract_id: ContractId) -> Output { +pub fn create_contract_output(contract_id: ContractId) -> Output { Output::contract_created(contract_id, Contract::default_state_root()) } // use some arbitrary large amount, this shouldn't affect the txpool logic except for covering // the byte and gas price fees. pub const TEST_COIN_AMOUNT: u64 = 100_000_000u64; +const GAS_LIMIT: Word = 100000; -pub(crate) struct PoolContext { +pub struct TestPoolUniverse { mock_db: MockDb, rng: StdRng, - pub(crate) config: Config, + pub config: Config, + pool: Option>, } -impl Default for PoolContext { +impl Default for TestPoolUniverse { fn default() -> Self { Self { mock_db: MockDb::default(), rng: StdRng::seed_from_u64(0), config: Default::default(), + pool: None, } } } -impl PoolContext { - pub(crate) fn database_mut(&mut self) -> &mut MockDb { +impl TestPoolUniverse { + pub fn database_mut(&mut self) -> &mut MockDb { &mut self.mock_db } - pub(crate) fn config(self, config: Config) -> Self { + pub fn config(self, config: Config) -> Self { + if self.pool.is_some() { + panic!("Pool already built"); + } Self { config, ..self } } - pub(crate) fn build( - self, - ) -> Pool< - MockDBProvider, - GraphStorage, - BasicCollisionManager, - RatioTipGasSelection, - > { - Pool::new( - MockDBProvider(self.mock_db), + pub fn build_pool(&mut self) -> TxPool { + let pool = Arc::new(RwLock::new(Pool::new( + MockDBProvider(self.mock_db.clone()), GraphStorage::new(GraphConfig { max_dependent_txn_count: self.config.max_dependent_txn_count, }), BasicCollisionManager::new(), RatioTipGasSelection::new(), - self.config, - ) + self.config.clone(), + ))); + self.pool = Some(pool.clone()); + pool } - pub(crate) fn setup_coin(&mut self) -> (Coin, Input) { - setup_coin(&mut self.rng, Some(&self.mock_db)) + pub fn build_script_transaction( + &mut self, + inputs: Option>, + outputs: Option>, + tip: u64, + ) -> Transaction { + let mut inputs = inputs.unwrap_or_default(); + let (_, gas_coin) = self.setup_coin(); + inputs.push(gas_coin.into()); + let outputs = outputs.unwrap_or_default(); + let mut tx_builder = TransactionBuilder::script(vec![], vec![]); + tx_builder.script_gas_limit(GAS_LIMIT); + for input in inputs { + tx_builder.add_input(input); + } + for output in outputs { + tx_builder.add_output(output); + } + tx_builder.tip(tip); + tx_builder.max_fee_limit(tip); + tx_builder.finalize().into() } - pub(crate) fn create_output_and_input( + pub async fn verify_and_insert( &mut self, - amount: Word, - ) -> (Output, UnsetInput) { + tx: Transaction, + ) -> Result { + if let Some(pool) = &self.pool { + let tx = perform_all_verifications( + tx, + pool.clone(), + Default::default(), + &ConsensusParameters::default(), + 0, + &MockTxPoolGasPrice::new(0), + &MockWasmChecker::new(Ok(())), + MemoryInstance::new(), + ) + .await?; + pool.write().insert(tx) + } else { + panic!("Pool needs to be built first"); + } + } + + pub fn setup_coin(&mut self) -> (Coin, Input) { + let input = self.random_predicate(AssetId::BASE, TEST_COIN_AMOUNT, None); + + // add coin to the state + let mut coin = CompressedCoin::default(); + coin.set_owner(*input.input_owner().unwrap()); + coin.set_amount(TEST_COIN_AMOUNT); + coin.set_asset_id(*input.asset_id(&AssetId::BASE).unwrap()); + let utxo_id = *input.utxo_id().unwrap(); + self.mock_db + .data + .lock() + .unwrap() + .coins + .insert(utxo_id, coin.clone()); + (coin.uncompress(utxo_id), input) + } + + // TODO: Change this + pub fn create_output_and_input(&mut self, amount: Word) -> (Output, UnsetInput) { let input = self.random_predicate(AssetId::BASE, amount, None); let output = Output::coin(*input.input_owner().unwrap(), amount, AssetId::BASE); (output, UnsetInput(input)) } - pub(crate) fn random_predicate( + pub fn random_predicate( &mut self, asset_id: AssetId, amount: Word, utxo_id: Option, ) -> Input { - random_predicate(&mut self.rng, asset_id, amount, utxo_id) + // use predicate inputs to avoid expensive cryptography for signatures + let mut predicate_code: Vec = vec![op::ret(1)].into_iter().collect(); + // append some randomizing bytes after the predicate has already returned. + predicate_code.push(self.rng.gen()); + let owner = Input::predicate_owner(&predicate_code); + Input::coin_predicate( + utxo_id.unwrap_or_else(|| self.rng.gen()), + owner, + amount, + asset_id, + Default::default(), + Default::default(), + predicate_code, + vec![], + ) + .into_default_estimated() } - pub(crate) fn custom_predicate( + pub fn custom_predicate( &mut self, asset_id: AssetId, amount: Word, @@ -216,65 +303,6 @@ impl PoolContext { } } -pub(crate) fn setup_coin(rng: &mut StdRng, mock_db: Option<&MockDb>) -> (Coin, Input) { - let input = random_predicate(rng, AssetId::BASE, TEST_COIN_AMOUNT, None); - add_coin_to_state(input, mock_db) -} - -pub(crate) fn add_coin_to_state(input: Input, mock_db: Option<&MockDb>) -> (Coin, Input) { - let mut coin = CompressedCoin::default(); - coin.set_owner(*input.input_owner().unwrap()); - coin.set_amount(TEST_COIN_AMOUNT); - coin.set_asset_id(*input.asset_id(&AssetId::BASE).unwrap()); - let utxo_id = *input.utxo_id().unwrap(); - if let Some(mock_db) = mock_db { - mock_db - .data - .lock() - .unwrap() - .coins - .insert(utxo_id, coin.clone()); - } - (coin.uncompress(utxo_id), input) -} - -pub(crate) fn random_predicate( - rng: &mut StdRng, - asset_id: AssetId, - amount: Word, - utxo_id: Option, -) -> Input { - // use predicate inputs to avoid expensive cryptography for signatures - let mut predicate_code: Vec = vec![op::ret(1)].into_iter().collect(); - // append some randomizing bytes after the predicate has already returned. - predicate_code.push(rng.gen()); - let owner = Input::predicate_owner(&predicate_code); - Input::coin_predicate( - utxo_id.unwrap_or_else(|| rng.gen()), - owner, - amount, - asset_id, - Default::default(), - Default::default(), - predicate_code, - vec![], - ) - .into_default_estimated() -} - -pub struct MockWasmChecker { - pub result: Result<(), WasmValidityError>, -} - -impl WasmChecker for MockWasmChecker { - fn validate_uploaded_wasm( - &self, - _wasm_root: &Bytes32, - ) -> Result<(), WasmValidityError> { - self.result - } -} - pub struct UnsetInput(Input); impl UnsetInput { @@ -312,28 +340,3 @@ impl IntoEstimated for Input { tx.inputs()[0].clone() } } - -#[derive(Debug, Clone)] -pub struct MockTxPoolGasPrice { - pub gas_price: Option, -} - -impl MockTxPoolGasPrice { - pub fn new(gas_price: GasPrice) -> Self { - Self { - gas_price: Some(gas_price), - } - } - - pub fn new_none() -> Self { - Self { gas_price: None } - } -} - -#[async_trait::async_trait] -impl GasPriceProvider for MockTxPoolGasPrice { - async fn next_gas_price(&self) -> Result { - self.gas_price - .ok_or(Error::GasPriceNotFound("Gas price not found".to_string())) - } -} diff --git a/crates/services/txpool_v2/src/tests/mock_db.rs b/crates/services/txpool_v2/src/tests/mocks.rs similarity index 67% rename from crates/services/txpool_v2/src/tests/mock_db.rs rename to crates/services/txpool_v2/src/tests/mocks.rs index 91ff57cf08c..2c3c5e3d28d 100644 --- a/crates/services/txpool_v2/src/tests/mock_db.rs +++ b/crates/services/txpool_v2/src/tests/mocks.rs @@ -1,6 +1,13 @@ -use crate::ports::{ - AtomicView, - TxPoolPersistentStorage, +use crate::{ + error::Error, + ports::{ + AtomicView, + GasPriceProvider, + TxPoolPersistentStorage, + WasmChecker, + WasmValidityError, + }, + GasPrice, }; use fuel_core_storage::Result as StorageResult; use fuel_core_types::{ @@ -13,6 +20,7 @@ use fuel_core_types::{ }, fuel_tx::{ BlobId, + Bytes32, Contract, ContractId, UtxoId, @@ -107,3 +115,47 @@ impl AtomicView for MockDBProvider { Ok(self.0.clone()) } } + +#[derive(Debug, Clone)] +pub struct MockTxPoolGasPrice { + pub gas_price: Option, +} + +impl MockTxPoolGasPrice { + pub fn new(gas_price: GasPrice) -> Self { + Self { + gas_price: Some(gas_price), + } + } + + pub fn new_none() -> Self { + Self { gas_price: None } + } +} + +#[async_trait::async_trait] +impl GasPriceProvider for MockTxPoolGasPrice { + async fn next_gas_price(&self) -> Result { + self.gas_price + .ok_or(Error::GasPriceNotFound("Gas price not found".to_string())) + } +} + +pub struct MockWasmChecker { + pub result: Result<(), WasmValidityError>, +} + +impl MockWasmChecker { + pub fn new(result: Result<(), WasmValidityError>) -> Self { + Self { result } + } +} + +impl WasmChecker for MockWasmChecker { + fn validate_uploaded_wasm( + &self, + _wasm_root: &Bytes32, + ) -> Result<(), WasmValidityError> { + self.result + } +} diff --git a/crates/services/txpool_v2/src/tests/mod.rs b/crates/services/txpool_v2/src/tests/mod.rs index 71f7da9ba9c..5d39023e6aa 100644 --- a/crates/services/txpool_v2/src/tests/mod.rs +++ b/crates/services/txpool_v2/src/tests/mod.rs @@ -1,3 +1,3 @@ mod context; -mod mock_db; +mod mocks; mod pool; diff --git a/crates/services/txpool_v2/src/tests/pool.rs b/crates/services/txpool_v2/src/tests/pool.rs index 94d249e75d0..fdb1031755f 100644 --- a/crates/services/txpool_v2/src/tests/pool.rs +++ b/crates/services/txpool_v2/src/tests/pool.rs @@ -9,7 +9,7 @@ use crate::{ create_contract_output, create_message_predicate_from_message, IntoEstimated, - PoolContext, + TestPoolUniverse, TEST_COIN_AMOUNT, }, }; @@ -46,201 +46,142 @@ use fuel_core_types::{ CheckedTransaction, IntoChecked, }, - services::txpool::PoolTransaction, + services::txpool::{ + self, + PoolTransaction, + }, }; use std::vec; -const GAS_LIMIT: Word = 100000; - #[tokio::test] -async fn insert_simple_tx_succeeds() { - let mut context = PoolContext::default(); - - let (_, gas_coin) = context.setup_coin(); - let tx = TransactionBuilder::script(vec![], vec![]) - .script_gas_limit(GAS_LIMIT) - .add_input(gas_coin) - .finalize_as_transaction() - .into_checked_basic(Default::default(), &ConsensusParameters::default()) - .unwrap(); - - let mut txpool = context.build(); - - for result in txpool.insert(vec![check_tx_to_pool(tx)]).unwrap() { - result.expect("Tx should be Ok, got Err"); - } +async fn insert_one_tx_succeeds() { + let mut universe = TestPoolUniverse::default(); + universe.build_pool(); + + // Given + let tx = universe.build_script_transaction(None, None, 0); + + // When + let result = universe.verify_and_insert(tx).await; + + // Then + assert!(result.is_ok()); } #[tokio::test] -async fn insert_simple_tx_with_blacklisted_utxo_id_fails() { - let mut context = PoolContext::default(); - - let (_, gas_coin) = context.setup_coin(); - let tx = TransactionBuilder::script(vec![], vec![]) - .script_gas_limit(GAS_LIMIT) - .add_input(gas_coin.clone()) - .finalize_as_transaction(); +async fn insert__tx_with_blacklisted_utxo_id() { + let mut universe = TestPoolUniverse::default(); // Given - let tx = check_unwrap_tx(tx, &context.config).await; - let utxo_id = *gas_coin.utxo_id().unwrap(); - context.config.black_list.coins.insert(utxo_id); - - let mut txpool = context.build(); + let (_, coin) = universe.setup_coin(); + let utxo_id = *coin.utxo_id().unwrap(); + universe.config.black_list.coins.insert(utxo_id); + universe.build_pool(); + let tx = universe.build_script_transaction(Some(vec![coin]), None, 0); // When - let results = txpool.insert(vec![check_tx_to_pool(tx)]).unwrap(); + let err = universe.verify_and_insert(tx).await.unwrap_err(); // Then - for result in results { - assert!(result.is_err()); - assert!(result - .unwrap_err() - .to_string() - .contains(format!("The UTXO `{}` is blacklisted", utxo_id).as_str())); - } + assert!(matches!(err, Error::BlacklistedUTXO(id) if id == utxo_id)); } #[tokio::test] -async fn insert_simple_tx_with_blacklisted_owner_fails() { - let mut context = PoolContext::default(); - - let (_, gas_coin) = context.setup_coin(); - let tx = TransactionBuilder::script(vec![], vec![]) - .script_gas_limit(GAS_LIMIT) - .add_input(gas_coin.clone()) - .finalize_as_transaction(); +async fn insert__tx_with_blacklisted_owner() { + let mut universe = TestPoolUniverse::default(); // Given - let tx = check_unwrap_tx(tx, &context.config).await; - let owner = *gas_coin.input_owner().unwrap(); - context.config.black_list.owners.insert(owner); - - let mut txpool = context.build(); + let (_, coin) = universe.setup_coin(); + let owner_addr = *coin.input_owner().unwrap(); + universe.config.black_list.owners.insert(owner_addr); + universe.build_pool(); + let tx = universe.build_script_transaction(Some(vec![coin]), None, 0); // When - let results = txpool.insert(vec![check_tx_to_pool(tx)]).unwrap(); + let err = universe.verify_and_insert(tx).await.unwrap_err(); // Then - for result in results { - assert!(result.is_err()); - assert!(result - .unwrap_err() - .to_string() - .contains(format!("The owner `{}` is blacklisted", owner).as_str())); - } + assert!(matches!(err, Error::BlacklistedOwner(id) if id == owner_addr)); } #[tokio::test] -async fn insert_simple_tx_with_blacklisted_contract_fails() { - let mut context = PoolContext::default(); +async fn insert__tx_with_blacklisted_contract() { + let mut universe = TestPoolUniverse::default(); let contract_id = Contract::EMPTY_CONTRACT_ID; - let (_, gas_coin) = context.setup_coin(); - let tx = TransactionBuilder::script(vec![], vec![]) - .script_gas_limit(GAS_LIMIT) - .add_input(gas_coin.clone()) - .add_input(create_contract_input( + // Given + universe.config.black_list.contracts.insert(contract_id); + universe.build_pool(); + let tx = universe.build_script_transaction( + Some(vec![create_contract_input( Default::default(), Default::default(), contract_id, - )) - .add_output(Output::contract(1, Default::default(), Default::default())) - .finalize_as_transaction(); - // Given - let tx = check_unwrap_tx(tx, &context.config).await; - context.config.black_list.contracts.insert(contract_id); - - let mut txpool = context.build(); + )]), + Some(vec![Output::contract( + 0, + Default::default(), + Default::default(), + )]), + 0, + ); // When - let results = txpool.insert(vec![check_tx_to_pool(tx)]).unwrap(); + let err = universe.verify_and_insert(tx).await.unwrap_err(); // Then - for result in results { - assert!(result.is_err()); - assert!(result - .unwrap_err() - .to_string() - .contains(format!("The contract `{}` is blacklisted", contract_id).as_str())); - } + assert!(matches!(err, Error::BlacklistedContract(id) if id == contract_id)); } #[tokio::test] -async fn insert_simple_tx_with_blacklisted_message_fails() { - let mut context = PoolContext::default(); - let (message, input) = create_message_predicate_from_message(5000, 0); - - let tx = TransactionBuilder::script(vec![], vec![]) - .script_gas_limit(GAS_LIMIT) - .add_input(input) - .finalize_as_transaction(); +async fn insert__tx_with_blacklisted_message() { + let mut universe = TestPoolUniverse::default(); - let nonce = *message.nonce(); // Given - let tx = check_unwrap_tx(tx, &context.config).await; - context.config.black_list.messages.insert(nonce); - - let mut txpool = context.build(); + let (message, input) = create_message_predicate_from_message(5000, 0); + let nonce = *message.nonce(); + universe.config.black_list.messages.insert(nonce); + universe.build_pool(); + let tx = universe.build_script_transaction(Some(vec![input]), None, 0); // When - let results = txpool.insert(vec![check_tx_to_pool(tx)]).unwrap(); + let err = universe.verify_and_insert(tx).await.unwrap_err(); // Then - for result in results { - assert!(result.is_err()); - assert!(result - .unwrap_err() - .to_string() - .contains(format!("The message `{}` is blacklisted", nonce).as_str())); - } + assert!(matches!(err, Error::BlacklistedMessage(id) if id == nonce)); } #[tokio::test] -async fn insert_simple_tx_dependency_chain_succeeds() { - let mut context = PoolContext::default(); - - let (_, gas_coin) = context.setup_coin(); - let (output, unset_input) = context.create_output_and_input(1); - let tx1 = TransactionBuilder::script(vec![], vec![]) - .tip(1) - .max_fee_limit(1) - .script_gas_limit(GAS_LIMIT) - .add_input(gas_coin) - .add_output(output) - .finalize_as_transaction(); - - let (_, gas_coin) = context.setup_coin(); - let input = unset_input.into_input(UtxoId::new(tx1.id(&Default::default()), 0)); - let tx2 = TransactionBuilder::script(vec![], vec![]) - .tip(1) - .max_fee_limit(1) - .script_gas_limit(GAS_LIMIT) - .add_input(input) - .add_input(gas_coin) - .finalize_as_transaction(); - - let mut txpool = context.build(); - let tx1 = check_unwrap_tx(tx1, &txpool.config).await; - let tx2 = check_unwrap_tx(tx2, &txpool.config).await; - - for result in txpool - .insert(vec![check_tx_to_pool(tx1), check_tx_to_pool(tx2)]) - .unwrap() - { - result.expect("Tx should be Ok, got Err"); - } +async fn insert_tx2_dependent_tx1() { + let mut universe = TestPoolUniverse::default(); + let mut txpool = universe.build_pool(); + + // Given + let (output, unset_input) = universe.create_output_and_input(1); + let tx1 = universe.build_script_transaction(None, Some(vec![output]), 0); + + let input = unset_input.into_input(UtxoId::new(tx1.id(&ChainId::default()), 0)); + let tx2 = universe.build_script_transaction(Some(vec![input]), None, 0); + + // When + let result1 = universe.verify_and_insert(tx1).await; + let result2 = universe.verify_and_insert(tx2).await; + + // Then + assert!(result1.is_ok()); + assert!(result2.is_ok()); } #[tokio::test] -async fn faulty_tx2_collided_on_contract_id_from_tx1() { - let mut context = PoolContext::default(); +async fn insert__tx2_collided_on_contract_id() { + let mut universe = TestPoolUniverse::default(); + universe.build_pool(); let contract_id = Contract::EMPTY_CONTRACT_ID; // contract creation tx - let (_, gas_coin) = context.setup_coin(); - let (output, unset_input) = context.create_output_and_input(10); + let (_, gas_coin) = universe.setup_coin(); + let (output, unset_input) = universe.create_output_and_input(10); let tx = TransactionBuilder::create( Default::default(), Default::default(), @@ -253,9 +194,10 @@ async fn faulty_tx2_collided_on_contract_id_from_tx1() { .add_output(output) .finalize_as_transaction(); - let (_, gas_coin) = context.setup_coin(); + let (_, gas_coin) = universe.setup_coin(); let input = unset_input.into_input(UtxoId::new(tx.id(&Default::default()), 1)); + // Given // attempt to insert a different creation tx with a valid dependency on the first tx, // but with a conflicting output contract id let tx_faulty = TransactionBuilder::create( @@ -271,26 +213,23 @@ async fn faulty_tx2_collided_on_contract_id_from_tx1() { .add_output(output) .finalize_as_transaction(); - let mut txpool = context.build(); - let tx = check_unwrap_tx(tx, &txpool.config).await; - let tx_faulty = check_unwrap_tx(tx_faulty, &txpool.config).await; + // When + let result1 = universe.verify_and_insert(tx).await; + let result2 = universe.verify_and_insert(tx_faulty).await; - let results = txpool - .insert(vec![check_tx_to_pool(tx), check_tx_to_pool(tx_faulty)]) - .unwrap(); - assert_eq!(results.len(), 2); - assert!(results[0].is_ok()); - let err = results[1].as_ref().expect_err("Tx2 should be Err, got Ok"); + assert!(result1.is_ok()); + let err = result2.unwrap_err(); assert!(matches!(err, Error::Collided(_))); } #[tokio::test] -async fn fail_to_insert_tx_with_dependency_on_invalid_utxo_type() { - let mut context = PoolContext::default(); - +async fn insert__tx_with_dependency_on_invalid_utxo_type() { + let mut universe = TestPoolUniverse::default(); + universe.build_pool(); let contract_id = Contract::EMPTY_CONTRACT_ID; - let (_, gas_coin) = context.setup_coin(); - let tx_faulty = TransactionBuilder::create( + + let (_, gas_coin) = universe.setup_coin(); + let tx = TransactionBuilder::create( Default::default(), Default::default(), Default::default(), @@ -298,1264 +237,1230 @@ async fn fail_to_insert_tx_with_dependency_on_invalid_utxo_type() { .add_input(gas_coin) .add_output(create_contract_output(contract_id)) .finalize_as_transaction(); + let utxo_id = UtxoId::new(tx.id(&Default::default()), 0); + // Given // create a second transaction with utxo id referring to // the wrong type of utxo (contract instead of coin) - let tx = TransactionBuilder::script(vec![], vec![]) - .tip(1) - .max_fee_limit(1) - .max_fee_limit(1) - .script_gas_limit(GAS_LIMIT) - .add_input(context.random_predicate( - AssetId::BASE, - TEST_COIN_AMOUNT, - Some(UtxoId::new(tx_faulty.id(&Default::default()), 0)), - )) - .finalize_as_transaction(); - - let mut txpool = context.build(); - let tx_faulty_id = tx_faulty.id(&ChainId::default()); - let tx_faulty = check_unwrap_tx(tx_faulty, &txpool.config).await; - let tx = check_unwrap_tx(tx, &txpool.config).await; - - let results = txpool - .insert(vec![check_tx_to_pool(tx_faulty), check_tx_to_pool(tx)]) - .unwrap(); - assert_eq!(results.len(), 2); - assert!(results[0].is_ok()); - let err = results[1].as_ref().expect_err("Tx should be Err, got Ok"); - assert!(matches!( - err, - Error::UtxoNotFound(id) if id == &UtxoId::new(tx_faulty_id, 0) - )); + let random_predicate = + universe.random_predicate(AssetId::BASE, TEST_COIN_AMOUNT, Some(utxo_id)); + let tx_faulty = + universe.build_script_transaction(Some(vec![random_predicate]), None, 0); + + // When + let result1 = universe.verify_and_insert(tx).await; + let result2 = universe.verify_and_insert(tx_faulty).await; + + // Then + assert!(result1.is_ok()); + let err = result2.unwrap_err(); + + assert!(matches!(err, Error::UtxoNotFound(id) if id == utxo_id)); } #[tokio::test] -async fn not_inserted_known_tx() { +async fn insert__already_known_tx() { let config = Config { utxo_validation: false, ..Default::default() }; - let context = PoolContext::default().config(config); - let mut txpool = context.build(); - - let tx = TransactionBuilder::script(vec![], vec![]) - .add_random_fee_input() - .finalize() - .into(); - let tx = check_unwrap_tx(tx, &txpool.config).await; - - let results = txpool - .insert(vec![check_tx_to_pool(tx.clone()), check_tx_to_pool(tx)]) - .unwrap(); - assert_eq!(results.len(), 2); - assert!(results[0].is_ok()); - let err = results[1].as_ref().expect_err("Tx should be Err, got Ok"); + let mut universe = TestPoolUniverse::default().config(config); + universe.build_pool(); + + // Given + let tx = universe.build_script_transaction(None, None, 0); + + // When + let result1 = universe.verify_and_insert(tx.clone()).await; + let result2 = universe.verify_and_insert(tx).await; + + // Then + assert!(result1.is_ok()); + let err = result2.unwrap_err(); assert!(matches!(err, Error::Collided(_))); } #[tokio::test] -async fn try_to_insert_tx2_missing_utxo() { - let mut context = PoolContext::default(); - - let input = context.random_predicate(AssetId::BASE, TEST_COIN_AMOUNT, None); - let tx = TransactionBuilder::script(vec![], vec![]) - .tip(10) - .max_fee_limit(10) - .script_gas_limit(GAS_LIMIT) - .add_input(input) - .finalize_as_transaction(); - - let mut txpool = context.build(); - let tx = check_unwrap_tx(tx, &txpool.config).await; - - let results = txpool.insert(vec![check_tx_to_pool(tx)]).unwrap(); - assert_eq!(results.len(), 1); - let err = results[0].as_ref().expect_err("Tx should be Err, got Ok"); - assert!(matches!(err, Error::UtxoNotFound(_))); -} +async fn insert__unknown_utxo() { + let mut universe = TestPoolUniverse::default(); + universe.build_pool(); -#[tokio::test] -async fn higher_priced_tx_removes_lower_priced_tx() { - let mut context = PoolContext::default(); - - let (_, coin_input) = context.setup_coin(); - - let tx1 = TransactionBuilder::script(vec![], vec![]) - .tip(10) - .max_fee_limit(10) - .script_gas_limit(GAS_LIMIT) - .add_input(coin_input.clone()) - .finalize_as_transaction(); - - let tx2 = TransactionBuilder::script(vec![], vec![]) - .tip(20) - .max_fee_limit(20) - .script_gas_limit(GAS_LIMIT) - .add_input(coin_input) - .finalize_as_transaction(); - - let tx1_id = tx1.id(&ChainId::default()); - let mut txpool = context.build(); - let tx1 = check_unwrap_tx(tx1, &txpool.config).await; - let tx2 = check_unwrap_tx(tx2, &txpool.config).await; - - let results = txpool - .insert(vec![ - check_tx_to_pool(tx1.clone()), - check_tx_to_pool(tx2.clone()), - ]) - .unwrap(); - assert_eq!(results.len(), 2); - assert!(results[0].is_ok()); - let vec = results[1].as_ref().unwrap(); - assert_eq!(vec[0].id(), tx1_id, "Tx1 id should be removed"); -} + // Given + let input = universe.random_predicate(AssetId::BASE, TEST_COIN_AMOUNT, None); + let utxo_id = input.utxo_id().cloned().unwrap(); + let tx = universe.build_script_transaction(Some(vec![input]), None, 0); -#[tokio::test] -async fn underpriced_tx1_not_included_coin_collision() { - let mut context = PoolContext::default(); - - let (_, gas_coin) = context.setup_coin(); - let (output, unset_input) = context.create_output_and_input(20); - let tx1 = TransactionBuilder::script(vec![], vec![]) - .tip(20) - .max_fee_limit(20) - .script_gas_limit(GAS_LIMIT) - .add_input(gas_coin) - .add_output(output) - .finalize_as_transaction(); - - let input = unset_input.into_input(UtxoId::new(tx1.id(&Default::default()), 0)); - - let tx2 = TransactionBuilder::script(vec![], vec![]) - .tip(20) - .max_fee_limit(20) - .script_gas_limit(GAS_LIMIT) - .add_input(input.clone()) - .finalize_as_transaction(); - - let tx3 = TransactionBuilder::script(vec![], vec![]) - .tip(10) - .max_fee_limit(10) - .script_gas_limit(GAS_LIMIT) - .add_input(input) - .finalize_as_transaction(); - - let mut txpool = context.build(); - let tx1_checked = check_unwrap_tx(tx1.clone(), &txpool.config).await; - let tx2_checked = check_unwrap_tx(tx2.clone(), &txpool.config).await; - let tx3_checked = check_unwrap_tx(tx3, &txpool.config).await; - - let results = txpool - .insert(vec![ - check_tx_to_pool(tx1_checked), - check_tx_to_pool(tx2_checked), - check_tx_to_pool(tx3_checked), - ]) - .unwrap(); - assert_eq!(results.len(), 3); - assert!(results[0].is_ok()); - assert!(results[1].is_ok()); - let err = results[2].as_ref().expect_err("Tx3 should be Err, got Ok"); + // When + let result = universe.verify_and_insert(tx).await; - assert!(matches!(err, Error::Collided(_))); + // Then + let err = result.unwrap_err(); + assert!(matches!(err, Error::UtxoNotFound(id) if id == utxo_id)); } #[tokio::test] -async fn dependent_contract_input_inserted() { - let mut context = PoolContext::default(); +async fn insert_higher_priced_tx_removes_lower_priced_tx() { + let mut universe = TestPoolUniverse::default(); + universe.build_pool(); - let contract_id = Contract::EMPTY_CONTRACT_ID; - let (_, gas_funds) = context.setup_coin(); - let tx1 = TransactionBuilder::create( - Default::default(), - Default::default(), - Default::default(), - ) - .tip(10) - .max_fee_limit(10) - .add_input(gas_funds) - .add_output(create_contract_output(contract_id)) - .finalize_as_transaction(); + // Given + let (_, coin_input) = universe.setup_coin(); + let tx1 = universe.build_script_transaction(Some(vec![coin_input.clone()]), None, 10); + let tx_id = tx1.id(&ChainId::default()); + let tx2 = universe.build_script_transaction(Some(vec![coin_input]), None, 20); - let (_, gas_funds) = context.setup_coin(); - let tx2 = TransactionBuilder::script(vec![], vec![]) - .tip(10) - .max_fee_limit(10) - .script_gas_limit(GAS_LIMIT) - .add_input(gas_funds) - .add_input(create_contract_input( - Default::default(), - Default::default(), - contract_id, - )) - .add_output(Output::contract(1, Default::default(), Default::default())) - .finalize_as_transaction(); - - let mut txpool = context.build(); - let tx1 = check_unwrap_tx(tx1, &txpool.config).await; - let tx2 = check_unwrap_tx(tx2, &txpool.config).await; - - let results = txpool - .insert(vec![check_tx_to_pool(tx1), check_tx_to_pool(tx2)]) - .unwrap(); - assert_eq!(results.len(), 2); - assert!(results[0].is_ok()); - assert!(results[1].is_ok()); -} + // When + let result1 = universe.verify_and_insert(tx1).await.unwrap(); + let result2 = universe.verify_and_insert(tx2).await.unwrap(); -#[tokio::test] -async fn more_priced_tx3_removes_tx1_and_dependent_tx2() { - let mut context = PoolContext::default(); - - let (_, gas_coin) = context.setup_coin(); - - let (output, unset_input) = context.create_output_and_input(10); - let tx1 = TransactionBuilder::script(vec![], vec![]) - .tip(10) - .max_fee_limit(10) - .script_gas_limit(GAS_LIMIT) - .add_input(gas_coin.clone()) - .add_output(output) - .finalize_as_transaction(); - - let input = unset_input.into_input(UtxoId::new(tx1.id(&Default::default()), 0)); - - let tx2 = TransactionBuilder::script(vec![], vec![]) - .tip(9) - .max_fee_limit(9) - .script_gas_limit(GAS_LIMIT) - .add_input(input) - .finalize_as_transaction(); - - let tx3 = TransactionBuilder::script(vec![], vec![]) - .tip(20) - .max_fee_limit(20) - .script_gas_limit(GAS_LIMIT) - .add_input(gas_coin) - .finalize_as_transaction(); - - let tx1_id = tx1.id(&ChainId::default()); - let tx2_id = tx2.id(&ChainId::default()); - let mut txpool = context.build(); - let tx1 = check_unwrap_tx(tx1, &txpool.config).await; - let tx2 = check_unwrap_tx(tx2, &txpool.config).await; - let tx3 = check_unwrap_tx(tx3, &txpool.config).await; - - let results = txpool - .insert(vec![ - check_tx_to_pool(tx1.clone()), - check_tx_to_pool(tx2.clone()), - check_tx_to_pool(tx3.clone()), - ]) - .unwrap(); - assert_eq!(results.len(), 3); - assert!(results[0].is_ok()); - assert!(results[1].is_ok()); - let removed_transactions = results[2].as_ref().unwrap(); - assert_eq!(removed_transactions.len(), 2); - assert_eq!( - removed_transactions[0].id(), - tx1_id, - "Tx1 id should be removed" - ); - assert_eq!( - removed_transactions[1].id(), - tx2_id, - "Tx2 id should be removed" - ); + // Then + assert_eq!(result2[0].id(), tx_id); } -#[tokio::test] -async fn more_priced_tx2_removes_tx1_and_more_priced_tx3_removes_tx2() { - let mut context = PoolContext::default(); - - let (_, gas_coin) = context.setup_coin(); - - let tx1 = TransactionBuilder::script(vec![], vec![]) - .tip(10) - .max_fee_limit(10) - .script_gas_limit(GAS_LIMIT) - .add_input(gas_coin.clone()) - .finalize_as_transaction(); - - let tx2 = TransactionBuilder::script(vec![], vec![]) - .tip(11) - .max_fee_limit(11) - .script_gas_limit(GAS_LIMIT) - .add_input(gas_coin.clone()) - .finalize_as_transaction(); - - let tx3 = TransactionBuilder::script(vec![], vec![]) - .tip(12) - .max_fee_limit(12) - .script_gas_limit(GAS_LIMIT) - .add_input(gas_coin) - .finalize_as_transaction(); - - let mut txpool = context.build(); - let tx1 = check_unwrap_tx(tx1, &txpool.config).await; - let tx2 = check_unwrap_tx(tx2, &txpool.config).await; - let tx3 = check_unwrap_tx(tx3, &txpool.config).await; - - let results = txpool - .insert(vec![ - check_tx_to_pool(tx1.clone()), - check_tx_to_pool(tx2.clone()), - check_tx_to_pool(tx3.clone()), - ]) - .unwrap(); - assert_eq!(results.len(), 3); - assert!(results[0].is_ok()); - let removed_transactions = results[1].as_ref().unwrap(); - assert_eq!(removed_transactions.len(), 1); - let removed_transactions = results[2].as_ref().unwrap(); - assert_eq!(removed_transactions.len(), 1); -} +// #[tokio::test] +// async fn underpriced_tx1_not_included_coin_collision() { +// let mut context = PoolContext::default(); + +// let (_, gas_coin) = context.setup_coin(); +// let (output, unset_input) = context.create_output_and_input(20); +// let tx1 = TransactionBuilder::script(vec![], vec![]) +// .tip(20) +// .max_fee_limit(20) +// .script_gas_limit(GAS_LIMIT) +// .add_input(gas_coin) +// .add_output(output) +// .finalize_as_transaction(); + +// let input = unset_input.into_input(UtxoId::new(tx1.id(&Default::default()), 0)); + +// let tx2 = TransactionBuilder::script(vec![], vec![]) +// .tip(20) +// .max_fee_limit(20) +// .script_gas_limit(GAS_LIMIT) +// .add_input(input.clone()) +// .finalize_as_transaction(); + +// let tx3 = TransactionBuilder::script(vec![], vec![]) +// .tip(10) +// .max_fee_limit(10) +// .script_gas_limit(GAS_LIMIT) +// .add_input(input) +// .finalize_as_transaction(); -#[tokio::test] -async fn tx_limit_hit() { - let mut context = PoolContext::default().config(Config { - max_txs: 1, - ..Default::default() - }); - - let (_, gas_coin) = context.setup_coin(); - let tx1 = TransactionBuilder::script(vec![], vec![]) - .script_gas_limit(GAS_LIMIT) - .add_input(gas_coin) - .add_output(create_coin_output()) - .finalize_as_transaction(); - - let (_, gas_coin) = context.setup_coin(); - let tx2 = TransactionBuilder::script(vec![], vec![]) - .script_gas_limit(GAS_LIMIT) - .add_input(gas_coin) - .finalize_as_transaction(); - - let mut txpool = context.build(); - let tx1 = check_unwrap_tx(tx1, &txpool.config).await; - let tx2 = check_unwrap_tx(tx2, &txpool.config).await; - let results = txpool - .insert(vec![check_tx_to_pool(tx1), check_tx_to_pool(tx2)]) - .unwrap(); - assert_eq!(results.len(), 2); - assert!(results[0].is_ok()); - let err = results[1].as_ref().expect_err("Tx2 should be Err, got Ok"); - assert!(matches!(err, Error::NotInsertedLimitHit)); -} +// let mut txpool = context.build(); +// let tx1_checked = check_unwrap_tx(tx1.clone(), &txpool.config).await; +// let tx2_checked = check_unwrap_tx(tx2.clone(), &txpool.config).await; +// let tx3_checked = check_unwrap_tx(tx3, &txpool.config).await; + +// let results = txpool +// .insert(vec![ +// check_tx_to_pool(tx1_checked), +// check_tx_to_pool(tx2_checked), +// check_tx_to_pool(tx3_checked), +// ]) +// .unwrap(); +// assert_eq!(results.len(), 3); +// assert!(results[0].is_ok()); +// assert!(results[1].is_ok()); +// let err = results[2].as_ref().expect_err("Tx3 should be Err, got Ok"); + +// assert!(matches!(err, Error::Collided(_))); +// } -#[tokio::test] -async fn tx_chain_length_hit() { - let mut context = PoolContext::default().config(Config { - max_dependent_txn_count: 2, - ..Default::default() - }); - - let (_, gas_coin) = context.setup_coin(); - let (output, unset_input) = context.create_output_and_input(10_000); - let tx1 = TransactionBuilder::script(vec![], vec![]) - .script_gas_limit(GAS_LIMIT) - .add_input(gas_coin) - .add_output(output) - .finalize_as_transaction(); - - let input = unset_input.into_input(UtxoId::new(tx1.id(&Default::default()), 0)); - let (output, unset_input) = context.create_output_and_input(5_000); - let tx2 = TransactionBuilder::script(vec![], vec![]) - .script_gas_limit(GAS_LIMIT) - .add_input(input) - .add_output(output) - .finalize_as_transaction(); - - let input = unset_input.into_input(UtxoId::new(tx2.id(&Default::default()), 0)); - let tx3 = TransactionBuilder::script(vec![], vec![]) - .script_gas_limit(GAS_LIMIT) - .add_input(input) - .finalize_as_transaction(); - - let mut txpool = context.build(); - let tx1 = check_unwrap_tx(tx1, &txpool.config).await; - let tx2 = check_unwrap_tx(tx2, &txpool.config).await; - let tx3 = check_unwrap_tx(tx3, &txpool.config).await; - - let results = txpool - .insert(vec![ - check_tx_to_pool(tx1.clone()), - check_tx_to_pool(tx2.clone()), - check_tx_to_pool(tx3.clone()), - ]) - .unwrap(); - assert_eq!(results.len(), 3); - assert!(results[0].is_ok()); - assert!(results[1].is_ok()); - let err = results[2].as_ref().expect_err("Tx3 should be Err, got Ok"); - assert!(matches!(err, Error::NotInsertedChainDependencyTooBig)); -} +// #[tokio::test] +// async fn dependent_contract_input_inserted() { +// let mut context = PoolContext::default(); -#[tokio::test] -async fn sorted_out_tx1_2_3() { - let mut context = PoolContext::default(); - - let (_, gas_coin) = context.setup_coin(); - let tx1 = TransactionBuilder::script(vec![], vec![]) - .tip(10) - .max_fee_limit(10) - .script_gas_limit(GAS_LIMIT) - .add_input(gas_coin) - .finalize_as_transaction(); - - let (_, gas_coin) = context.setup_coin(); - let tx2 = TransactionBuilder::script(vec![], vec![]) - .tip(9) - .max_fee_limit(9) - .script_gas_limit(GAS_LIMIT) - .add_input(gas_coin) - .finalize_as_transaction(); - - let (_, gas_coin) = context.setup_coin(); - let tx3 = TransactionBuilder::script(vec![], vec![]) - .tip(20) - .max_fee_limit(20) - .script_gas_limit(GAS_LIMIT) - .add_input(gas_coin) - .finalize_as_transaction(); - - let tx1_id = tx1.id(&ChainId::default()); - let tx2_id = tx2.id(&ChainId::default()); - let tx3_id = tx3.id(&ChainId::default()); - - let mut txpool = context.build(); - let tx1 = check_unwrap_tx(tx1, &txpool.config).await; - let tx2 = check_unwrap_tx(tx2, &txpool.config).await; - let tx3 = check_unwrap_tx(tx3, &txpool.config).await; - - let results = txpool - .insert(vec![ - check_tx_to_pool(tx1.clone()), - check_tx_to_pool(tx2.clone()), - check_tx_to_pool(tx3.clone()), - ]) - .unwrap(); - assert_eq!(results.len(), 3); - assert!(results[0].is_ok()); - assert!(results[1].is_ok()); - assert!(results[2].is_ok()); - - let txs = txpool.extract_transactions_for_block().unwrap(); - - assert_eq!(txs.len(), 3, "Should have 3 txs"); - assert_eq!(txs[0].id(), tx3_id, "First should be tx3"); - assert_eq!(txs[1].id(), tx1_id, "Second should be tx1"); - assert_eq!(txs[2].id(), tx2_id, "Third should be tx2"); -} +// let contract_id = Contract::EMPTY_CONTRACT_ID; +// let (_, gas_funds) = context.setup_coin(); +// let tx1 = TransactionBuilder::create( +// Default::default(), +// Default::default(), +// Default::default(), +// ) +// .tip(10) +// .max_fee_limit(10) +// .add_input(gas_funds) +// .add_output(create_contract_output(contract_id)) +// .finalize_as_transaction(); -#[tokio::test] -async fn sorted_out_tx_same_tips() { - let mut context = PoolContext::default(); - - let (_, gas_coin) = context.setup_coin(); - let tx1 = TransactionBuilder::script(vec![], vec![]) - .tip(10) - .max_fee_limit(10) - .script_gas_limit(GAS_LIMIT) - .add_input(gas_coin) - .finalize_as_transaction(); - - let (_, gas_coin) = context.setup_coin(); - let tx2 = TransactionBuilder::script(vec![], vec![]) - .tip(10) - .max_fee_limit(10) - .script_gas_limit(GAS_LIMIT / 2) - .add_input(gas_coin) - .finalize_as_transaction(); - - let (_, gas_coin) = context.setup_coin(); - let tx3 = TransactionBuilder::script(vec![], vec![]) - .tip(10) - .max_fee_limit(10) - .script_gas_limit(GAS_LIMIT / 4) - .add_input(gas_coin) - .finalize_as_transaction(); - - let tx1_id = tx1.id(&ChainId::default()); - let tx2_id = tx2.id(&ChainId::default()); - let tx3_id = tx3.id(&ChainId::default()); - - let mut txpool = context.build(); - let tx1 = check_unwrap_tx(tx1, &txpool.config).await; - let tx2 = check_unwrap_tx(tx2, &txpool.config).await; - let tx3 = check_unwrap_tx(tx3, &txpool.config).await; - - let results = txpool - .insert(vec![ - check_tx_to_pool(tx1.clone()), - check_tx_to_pool(tx2.clone()), - check_tx_to_pool(tx3.clone()), - ]) - .unwrap(); - assert_eq!(results.len(), 3); - assert!(results[0].is_ok()); - assert!(results[1].is_ok()); - assert!(results[2].is_ok()); - - let txs = txpool.extract_transactions_for_block().unwrap(); - assert_eq!(txs.len(), 3, "Should have 3 txs"); - - assert_eq!(txs[0].id(), tx3_id, "First should be tx3"); - assert_eq!(txs[1].id(), tx2_id, "Second should be tx2"); - assert_eq!(txs[2].id(), tx1_id, "Third should be tx1"); -} +// let (_, gas_funds) = context.setup_coin(); +// let tx2 = TransactionBuilder::script(vec![], vec![]) +// .tip(10) +// .max_fee_limit(10) +// .script_gas_limit(GAS_LIMIT) +// .add_input(gas_funds) +// .add_input(create_contract_input( +// Default::default(), +// Default::default(), +// contract_id, +// )) +// .add_output(Output::contract(1, Default::default(), Default::default())) +// .finalize_as_transaction(); -#[tokio::test] -async fn sorted_out_tx_profitable_ratios() { - let mut context = PoolContext::default(); - - let (_, gas_coin) = context.setup_coin(); - let tx1 = TransactionBuilder::script(vec![], vec![]) - .tip(4) - .max_fee_limit(4) - .script_gas_limit(GAS_LIMIT) - .add_input(gas_coin) - .finalize_as_transaction(); - - let (_, gas_coin) = context.setup_coin(); - let tx2 = TransactionBuilder::script(vec![], vec![]) - .tip(2) - .max_fee_limit(2) - .script_gas_limit(GAS_LIMIT / 10) - .add_input(gas_coin) - .finalize_as_transaction(); - - let (_, gas_coin) = context.setup_coin(); - let tx3 = TransactionBuilder::script(vec![], vec![]) - .tip(1) - .max_fee_limit(1) - .script_gas_limit(GAS_LIMIT / 100) - .add_input(gas_coin) - .finalize_as_transaction(); - - let tx1_id = tx1.id(&ChainId::default()); - let tx2_id = tx2.id(&ChainId::default()); - let tx3_id = tx3.id(&ChainId::default()); - - let mut txpool = context.build(); - let tx1 = check_unwrap_tx(tx1, &txpool.config).await; - let tx2 = check_unwrap_tx(tx2, &txpool.config).await; - let tx3 = check_unwrap_tx(tx3, &txpool.config).await; - - let results = txpool - .insert(vec![ - check_tx_to_pool(tx1.clone()), - check_tx_to_pool(tx2.clone()), - check_tx_to_pool(tx3.clone()), - ]) - .unwrap(); - assert_eq!(results.len(), 3); - assert!(results[0].is_ok()); - assert!(results[1].is_ok()); - assert!(results[2].is_ok()); - - let txs = txpool.extract_transactions_for_block().unwrap(); - assert_eq!(txs.len(), 3, "Should have 3 txs"); - - assert_eq!(txs[0].id(), tx3_id, "First should be tx3"); - assert_eq!(txs[1].id(), tx2_id, "Second should be tx2"); - assert_eq!(txs[2].id(), tx1_id, "Third should be tx1"); -} +// let mut txpool = context.build(); +// let tx1 = check_unwrap_tx(tx1, &txpool.config).await; +// let tx2 = check_unwrap_tx(tx2, &txpool.config).await; + +// let results = txpool +// .insert(vec![check_tx_to_pool(tx1), check_tx_to_pool(tx2)]) +// .unwrap(); +// assert_eq!(results.len(), 2); +// assert!(results[0].is_ok()); +// assert!(results[1].is_ok()); +// } -#[tokio::test] -async fn sorted_out_tx_by_creation_instant() { - let mut context = PoolContext::default(); - - let (_, gas_coin) = context.setup_coin(); - let tx1 = TransactionBuilder::script(vec![], vec![]) - .tip(4) - .max_fee_limit(4) - .script_gas_limit(GAS_LIMIT) - .add_input(gas_coin) - .finalize_as_transaction(); - - let (_, gas_coin) = context.setup_coin(); - let tx2 = TransactionBuilder::script(vec![], vec![]) - .tip(4) - .max_fee_limit(4) - .script_gas_limit(GAS_LIMIT) - .add_input(gas_coin) - .finalize_as_transaction(); - - let (_, gas_coin) = context.setup_coin(); - let tx3 = TransactionBuilder::script(vec![], vec![]) - .tip(4) - .max_fee_limit(4) - .script_gas_limit(GAS_LIMIT) - .add_input(gas_coin) - .finalize_as_transaction(); - - let (_, gas_coin) = context.setup_coin(); - let tx4 = TransactionBuilder::script(vec![], vec![]) - .tip(4) - .max_fee_limit(4) - .script_gas_limit(GAS_LIMIT) - .add_input(gas_coin) - .finalize_as_transaction(); - - let tx1_id = tx1.id(&ChainId::default()); - let tx2_id = tx2.id(&ChainId::default()); - let tx3_id = tx3.id(&ChainId::default()); - let tx4_id = tx4.id(&ChainId::default()); - - let mut txpool = context.build(); - let tx1 = check_unwrap_tx(tx1, &txpool.config).await; - let tx2 = check_unwrap_tx(tx2, &txpool.config).await; - let tx3 = check_unwrap_tx(tx3, &txpool.config).await; - let tx4 = check_unwrap_tx(tx4, &txpool.config).await; - - let results = txpool - .insert(vec![ - check_tx_to_pool(tx1.clone()), - check_tx_to_pool(tx2.clone()), - check_tx_to_pool(tx3.clone()), - check_tx_to_pool(tx4.clone()), - ]) - .unwrap(); - assert_eq!(results.len(), 4); - assert!(results[0].is_ok()); - assert!(results[1].is_ok()); - assert!(results[2].is_ok()); - assert!(results[3].is_ok()); - - let txs = txpool.extract_transactions_for_block().unwrap(); - - // This order doesn't match the lexicographical order of the tx ids - // and so it verifies that the txs are sorted by creation instant - // The newest tx should be first - assert_eq!(txs.len(), 4, "Should have 4 txs"); - assert_eq!(txs[0].id(), tx1_id, "First should be tx1"); - assert_eq!(txs[1].id(), tx2_id, "Second should be tx2"); - assert_eq!(txs[2].id(), tx3_id, "Third should be tx3"); - assert_eq!(txs[3].id(), tx4_id, "Fourth should be tx4"); -} +// #[tokio::test] +// async fn more_priced_tx3_removes_tx1_and_dependent_tx2() { +// let mut context = PoolContext::default(); + +// let (_, gas_coin) = context.setup_coin(); + +// let (output, unset_input) = context.create_output_and_input(10); +// let tx1 = TransactionBuilder::script(vec![], vec![]) +// .tip(10) +// .max_fee_limit(10) +// .script_gas_limit(GAS_LIMIT) +// .add_input(gas_coin.clone()) +// .add_output(output) +// .finalize_as_transaction(); + +// let input = unset_input.into_input(UtxoId::new(tx1.id(&Default::default()), 0)); + +// let tx2 = TransactionBuilder::script(vec![], vec![]) +// .tip(9) +// .max_fee_limit(9) +// .script_gas_limit(GAS_LIMIT) +// .add_input(input) +// .finalize_as_transaction(); + +// let tx3 = TransactionBuilder::script(vec![], vec![]) +// .tip(20) +// .max_fee_limit(20) +// .script_gas_limit(GAS_LIMIT) +// .add_input(gas_coin) +// .finalize_as_transaction(); + +// let tx1_id = tx1.id(&ChainId::default()); +// let tx2_id = tx2.id(&ChainId::default()); +// let mut txpool = context.build(); +// let tx1 = check_unwrap_tx(tx1, &txpool.config).await; +// let tx2 = check_unwrap_tx(tx2, &txpool.config).await; +// let tx3 = check_unwrap_tx(tx3, &txpool.config).await; + +// let results = txpool +// .insert(vec![ +// check_tx_to_pool(tx1.clone()), +// check_tx_to_pool(tx2.clone()), +// check_tx_to_pool(tx3.clone()), +// ]) +// .unwrap(); +// assert_eq!(results.len(), 3); +// assert!(results[0].is_ok()); +// assert!(results[1].is_ok()); +// let removed_transactions = results[2].as_ref().unwrap(); +// assert_eq!(removed_transactions.len(), 2); +// assert_eq!( +// removed_transactions[0].id(), +// tx1_id, +// "Tx1 id should be removed" +// ); +// assert_eq!( +// removed_transactions[1].id(), +// tx2_id, +// "Tx2 id should be removed" +// ); +// } -#[tokio::test] -async fn tx_at_least_min_gas_price_is_insertable() { - let gas_price = 10; - let mut context = PoolContext::default().config(Config { - ..Default::default() - }); - - let (_, gas_coin) = context.setup_coin(); - let tx = TransactionBuilder::script(vec![], vec![]) - .tip(10) - .max_fee_limit(1000) - .script_gas_limit(GAS_LIMIT) - .add_input(gas_coin) - .finalize_as_transaction(); - - let mut txpool = context.build(); - let tx = check_unwrap_tx_with_gas_price(tx, &txpool.config, gas_price).await; - - let results = txpool.insert(vec![check_tx_to_pool(tx)]).unwrap(); - assert_eq!(results.len(), 1); - assert!(results[0].is_ok()); -} +// #[tokio::test] +// async fn more_priced_tx2_removes_tx1_and_more_priced_tx3_removes_tx2() { +// let mut context = PoolContext::default(); + +// let (_, gas_coin) = context.setup_coin(); + +// let tx1 = TransactionBuilder::script(vec![], vec![]) +// .tip(10) +// .max_fee_limit(10) +// .script_gas_limit(GAS_LIMIT) +// .add_input(gas_coin.clone()) +// .finalize_as_transaction(); + +// let tx2 = TransactionBuilder::script(vec![], vec![]) +// .tip(11) +// .max_fee_limit(11) +// .script_gas_limit(GAS_LIMIT) +// .add_input(gas_coin.clone()) +// .finalize_as_transaction(); + +// let tx3 = TransactionBuilder::script(vec![], vec![]) +// .tip(12) +// .max_fee_limit(12) +// .script_gas_limit(GAS_LIMIT) +// .add_input(gas_coin) +// .finalize_as_transaction(); -#[tokio::test] -async fn tx_below_min_gas_price_is_not_insertable() { - let mut context = PoolContext::default(); - - let gas_coin = context.random_predicate(AssetId::BASE, TEST_COIN_AMOUNT, None); - let tx = TransactionBuilder::script(vec![], vec![]) - .tip(10) - .max_fee_limit(10) - .script_gas_limit(GAS_LIMIT) - .add_input(gas_coin) - .finalize_as_transaction(); - let gas_price = 11; - - let err = check_tx_with_gas_price( - tx, - &Config { - ..Default::default() - }, - gas_price, - ) - .await - .expect_err("expected insertion failure"); +// let mut txpool = context.build(); +// let tx1 = check_unwrap_tx(tx1, &txpool.config).await; +// let tx2 = check_unwrap_tx(tx2, &txpool.config).await; +// let tx3 = check_unwrap_tx(tx3, &txpool.config).await; + +// let results = txpool +// .insert(vec![ +// check_tx_to_pool(tx1.clone()), +// check_tx_to_pool(tx2.clone()), +// check_tx_to_pool(tx3.clone()), +// ]) +// .unwrap(); +// assert_eq!(results.len(), 3); +// assert!(results[0].is_ok()); +// let removed_transactions = results[1].as_ref().unwrap(); +// assert_eq!(removed_transactions.len(), 1); +// let removed_transactions = results[2].as_ref().unwrap(); +// assert_eq!(removed_transactions.len(), 1); +// } - assert!(matches!( - err, - Error::ConsensusValidity(CheckError::InsufficientMaxFee { .. }) - )); -} +// #[tokio::test] +// async fn tx_limit_hit() { +// let mut context = PoolContext::default().config(Config { +// max_txs: 1, +// ..Default::default() +// }); -#[tokio::test] -async fn tx_inserted_into_pool_when_input_message_id_exists_in_db() { - let mut context = PoolContext::default(); - let (message, input) = create_message_predicate_from_message(5000, 0); +// let (_, gas_coin) = context.setup_coin(); +// let tx1 = TransactionBuilder::script(vec![], vec![]) +// .script_gas_limit(GAS_LIMIT) +// .add_input(gas_coin) +// .add_output(create_coin_output()) +// .finalize_as_transaction(); - let tx = TransactionBuilder::script(vec![], vec![]) - .script_gas_limit(GAS_LIMIT) - .add_input(input) - .finalize_as_transaction(); +// let (_, gas_coin) = context.setup_coin(); +// let tx2 = TransactionBuilder::script(vec![], vec![]) +// .script_gas_limit(GAS_LIMIT) +// .add_input(gas_coin) +// .finalize_as_transaction(); - context.database_mut().insert_message(message); +// let mut txpool = context.build(); +// let tx1 = check_unwrap_tx(tx1, &txpool.config).await; +// let tx2 = check_unwrap_tx(tx2, &txpool.config).await; +// let results = txpool +// .insert(vec![check_tx_to_pool(tx1), check_tx_to_pool(tx2)]) +// .unwrap(); +// assert_eq!(results.len(), 2); +// assert!(results[0].is_ok()); +// let err = results[1].as_ref().expect_err("Tx2 should be Err, got Ok"); +// assert!(matches!(err, Error::NotInsertedLimitHit)); +// } - let tx1_id = tx.id(&ChainId::default()); - let mut txpool = context.build(); +// #[tokio::test] +// async fn tx_chain_length_hit() { +// let mut context = PoolContext::default().config(Config { +// max_dependent_txn_count: 2, +// ..Default::default() +// }); + +// let (_, gas_coin) = context.setup_coin(); +// let (output, unset_input) = context.create_output_and_input(10_000); +// let tx1 = TransactionBuilder::script(vec![], vec![]) +// .script_gas_limit(GAS_LIMIT) +// .add_input(gas_coin) +// .add_output(output) +// .finalize_as_transaction(); + +// let input = unset_input.into_input(UtxoId::new(tx1.id(&Default::default()), 0)); +// let (output, unset_input) = context.create_output_and_input(5_000); +// let tx2 = TransactionBuilder::script(vec![], vec![]) +// .script_gas_limit(GAS_LIMIT) +// .add_input(input) +// .add_output(output) +// .finalize_as_transaction(); + +// let input = unset_input.into_input(UtxoId::new(tx2.id(&Default::default()), 0)); +// let tx3 = TransactionBuilder::script(vec![], vec![]) +// .script_gas_limit(GAS_LIMIT) +// .add_input(input) +// .finalize_as_transaction(); - let tx = check_unwrap_tx(tx, &txpool.config).await; +// let mut txpool = context.build(); +// let tx1 = check_unwrap_tx(tx1, &txpool.config).await; +// let tx2 = check_unwrap_tx(tx2, &txpool.config).await; +// let tx3 = check_unwrap_tx(tx3, &txpool.config).await; + +// let results = txpool +// .insert(vec![ +// check_tx_to_pool(tx1.clone()), +// check_tx_to_pool(tx2.clone()), +// check_tx_to_pool(tx3.clone()), +// ]) +// .unwrap(); +// assert_eq!(results.len(), 3); +// assert!(results[0].is_ok()); +// assert!(results[1].is_ok()); +// let err = results[2].as_ref().expect_err("Tx3 should be Err, got Ok"); +// assert!(matches!(err, Error::NotInsertedChainDependencyTooBig)); +// } - let results = txpool.insert(vec![check_tx_to_pool(tx)]).unwrap(); - assert_eq!(results.len(), 1); - assert!(results[0].is_ok()); +// #[tokio::test] +// async fn sorted_out_tx1_2_3() { +// let mut context = PoolContext::default(); + +// let (_, gas_coin) = context.setup_coin(); +// let tx1 = TransactionBuilder::script(vec![], vec![]) +// .tip(10) +// .max_fee_limit(10) +// .script_gas_limit(GAS_LIMIT) +// .add_input(gas_coin) +// .finalize_as_transaction(); + +// let (_, gas_coin) = context.setup_coin(); +// let tx2 = TransactionBuilder::script(vec![], vec![]) +// .tip(9) +// .max_fee_limit(9) +// .script_gas_limit(GAS_LIMIT) +// .add_input(gas_coin) +// .finalize_as_transaction(); + +// let (_, gas_coin) = context.setup_coin(); +// let tx3 = TransactionBuilder::script(vec![], vec![]) +// .tip(20) +// .max_fee_limit(20) +// .script_gas_limit(GAS_LIMIT) +// .add_input(gas_coin) +// .finalize_as_transaction(); + +// let tx1_id = tx1.id(&ChainId::default()); +// let tx2_id = tx2.id(&ChainId::default()); +// let tx3_id = tx3.id(&ChainId::default()); - let tx = txpool.find_one(&tx1_id).unwrap(); - assert_eq!(tx.id(), tx1_id); -} +// let mut txpool = context.build(); +// let tx1 = check_unwrap_tx(tx1, &txpool.config).await; +// let tx2 = check_unwrap_tx(tx2, &txpool.config).await; +// let tx3 = check_unwrap_tx(tx3, &txpool.config).await; + +// let results = txpool +// .insert(vec![ +// check_tx_to_pool(tx1.clone()), +// check_tx_to_pool(tx2.clone()), +// check_tx_to_pool(tx3.clone()), +// ]) +// .unwrap(); +// assert_eq!(results.len(), 3); +// assert!(results[0].is_ok()); +// assert!(results[1].is_ok()); +// assert!(results[2].is_ok()); + +// let txs = txpool.extract_transactions_for_block().unwrap(); + +// assert_eq!(txs.len(), 3, "Should have 3 txs"); +// assert_eq!(txs[0].id(), tx3_id, "First should be tx3"); +// assert_eq!(txs[1].id(), tx1_id, "Second should be tx1"); +// assert_eq!(txs[2].id(), tx2_id, "Third should be tx2"); +// } -#[tokio::test] -async fn tx_rejected_from_pool_when_input_message_id_does_not_exist_in_db() { - let context = PoolContext::default(); - let (message, input) = create_message_predicate_from_message(5000, 0); - let tx = TransactionBuilder::script(vec![], vec![]) - .script_gas_limit(GAS_LIMIT) - .add_input(input) - .finalize_as_transaction(); - - // Do not insert any messages into the DB to ensure there is no matching message for the - // tx. - let mut txpool = context.build(); - let tx = check_unwrap_tx(tx, &txpool.config).await; - - let results = txpool.insert(vec![check_tx_to_pool(tx)]).unwrap(); - assert_eq!(results.len(), 1); - - let err = results[0].as_ref().expect_err("Tx should be Err, got Ok"); - // check error - assert!(matches!( - err, - Error::NotInsertedInputMessageUnknown(msg_id) if msg_id == message.id() - )); -} +// #[tokio::test] +// async fn sorted_out_tx_same_tips() { +// let mut context = PoolContext::default(); + +// let (_, gas_coin) = context.setup_coin(); +// let tx1 = TransactionBuilder::script(vec![], vec![]) +// .tip(10) +// .max_fee_limit(10) +// .script_gas_limit(GAS_LIMIT) +// .add_input(gas_coin) +// .finalize_as_transaction(); + +// let (_, gas_coin) = context.setup_coin(); +// let tx2 = TransactionBuilder::script(vec![], vec![]) +// .tip(10) +// .max_fee_limit(10) +// .script_gas_limit(GAS_LIMIT / 2) +// .add_input(gas_coin) +// .finalize_as_transaction(); + +// let (_, gas_coin) = context.setup_coin(); +// let tx3 = TransactionBuilder::script(vec![], vec![]) +// .tip(10) +// .max_fee_limit(10) +// .script_gas_limit(GAS_LIMIT / 4) +// .add_input(gas_coin) +// .finalize_as_transaction(); + +// let tx1_id = tx1.id(&ChainId::default()); +// let tx2_id = tx2.id(&ChainId::default()); +// let tx3_id = tx3.id(&ChainId::default()); -#[tokio::test] -async fn tx_rejected_from_pool_when_gas_price_is_lower_than_another_tx_with_same_message_id( -) { - let mut context = PoolContext::default(); - let message_amount = 10_000; - let max_fee_limit = 10u64; - let gas_price_high = 2u64; - let gas_price_low = 1u64; - let (message, conflicting_message_input) = - create_message_predicate_from_message(message_amount, 0); - - let tx_high = TransactionBuilder::script(vec![], vec![]) - .tip(gas_price_high) - .max_fee_limit(max_fee_limit) - .script_gas_limit(GAS_LIMIT) - .add_input(conflicting_message_input.clone()) - .finalize_as_transaction(); - - let tx_low = TransactionBuilder::script(vec![], vec![]) - .tip(gas_price_low) - .max_fee_limit(max_fee_limit) - .script_gas_limit(GAS_LIMIT) - .add_input(conflicting_message_input) - .finalize_as_transaction(); - - context.database_mut().insert_message(message.clone()); - - let mut txpool = context.build(); - - let _tx_high_id = tx_high.id(&ChainId::default()); - let tx_high = - check_unwrap_tx_with_gas_price(tx_high, &txpool.config, gas_price_high).await; - - // Insert a tx for the message id with a high gas amount - let results = txpool.insert(vec![check_tx_to_pool(tx_high)]).unwrap(); - assert_eq!(results.len(), 1); - assert!(results[0].is_ok()); - - let tx_low = - check_unwrap_tx_with_gas_price(tx_low, &txpool.config, gas_price_low).await; - // Insert a tx for the message id with a low gas amount - // Because the new transaction's id matches an existing transaction, we compare the gas - // prices of both the new and existing transactions. Since the existing transaction's gas - // price is higher, we must now reject the new transaction. - let results = txpool.insert(vec![check_tx_to_pool(tx_low)]).unwrap(); - assert_eq!(results.len(), 1); - let err = results[0].as_ref().expect_err("Tx should be Err, got Ok"); - - // check error - assert!(matches!(err, Error::Collided(_))); -} +// let mut txpool = context.build(); +// let tx1 = check_unwrap_tx(tx1, &txpool.config).await; +// let tx2 = check_unwrap_tx(tx2, &txpool.config).await; +// let tx3 = check_unwrap_tx(tx3, &txpool.config).await; + +// let results = txpool +// .insert(vec![ +// check_tx_to_pool(tx1.clone()), +// check_tx_to_pool(tx2.clone()), +// check_tx_to_pool(tx3.clone()), +// ]) +// .unwrap(); +// assert_eq!(results.len(), 3); +// assert!(results[0].is_ok()); +// assert!(results[1].is_ok()); +// assert!(results[2].is_ok()); + +// let txs = txpool.extract_transactions_for_block().unwrap(); +// assert_eq!(txs.len(), 3, "Should have 3 txs"); + +// assert_eq!(txs[0].id(), tx3_id, "First should be tx3"); +// assert_eq!(txs[1].id(), tx2_id, "Second should be tx2"); +// assert_eq!(txs[2].id(), tx1_id, "Third should be tx1"); +// } -#[tokio::test] -async fn higher_priced_tx_squeezes_out_lower_priced_tx_with_same_message_id() { - let mut context = PoolContext::default(); - let message_amount = 10_000; - let gas_price_high = 2u64; - let max_fee_limit = 10u64; - let gas_price_low = 1u64; - let (message, conflicting_message_input) = - create_message_predicate_from_message(message_amount, 0); - - // Insert a tx for the message id with a low gas amount - let tx_low = TransactionBuilder::script(vec![], vec![]) - .tip(gas_price_low) - .max_fee_limit(max_fee_limit) - .script_gas_limit(GAS_LIMIT) - .add_input(conflicting_message_input.clone()) - .finalize_as_transaction(); - - context.database_mut().insert_message(message); - - let mut txpool = context.build(); - let tx_low_id = tx_low.id(&ChainId::default()); - let tx_low = - check_unwrap_tx_with_gas_price(tx_low, &txpool.config, gas_price_low).await; - let results = txpool.insert(vec![check_tx_to_pool(tx_low)]).unwrap(); - assert_eq!(results.len(), 1); - assert!(results[0].is_ok()); - - // Insert a tx for the message id with a high gas amount - // Because the new transaction's id matches an existing transaction, we compare the gas - // prices of both the new and existing transactions. Since the existing transaction's gas - // price is lower, we accept the new transaction and squeeze out the old transaction. - let tx_high = TransactionBuilder::script(vec![], vec![]) - .tip(gas_price_high) - .max_fee_limit(max_fee_limit) - .script_gas_limit(GAS_LIMIT) - .add_input(conflicting_message_input) - .finalize_as_transaction(); - let tx_high = - check_unwrap_tx_with_gas_price(tx_high, &txpool.config, gas_price_high).await; - let results = txpool.insert(vec![check_tx_to_pool(tx_high)]).unwrap(); - assert_eq!(results.len(), 1); - assert!(results[0].is_ok()); - let squeezed_out_txs = results[0].as_ref().unwrap(); - - assert_eq!(squeezed_out_txs.len(), 1); - assert_eq!(squeezed_out_txs[0].id(), tx_low_id,); -} +// #[tokio::test] +// async fn sorted_out_tx_profitable_ratios() { +// let mut context = PoolContext::default(); + +// let (_, gas_coin) = context.setup_coin(); +// let tx1 = TransactionBuilder::script(vec![], vec![]) +// .tip(4) +// .max_fee_limit(4) +// .script_gas_limit(GAS_LIMIT) +// .add_input(gas_coin) +// .finalize_as_transaction(); + +// let (_, gas_coin) = context.setup_coin(); +// let tx2 = TransactionBuilder::script(vec![], vec![]) +// .tip(2) +// .max_fee_limit(2) +// .script_gas_limit(GAS_LIMIT / 10) +// .add_input(gas_coin) +// .finalize_as_transaction(); + +// let (_, gas_coin) = context.setup_coin(); +// let tx3 = TransactionBuilder::script(vec![], vec![]) +// .tip(1) +// .max_fee_limit(1) +// .script_gas_limit(GAS_LIMIT / 100) +// .add_input(gas_coin) +// .finalize_as_transaction(); + +// let tx1_id = tx1.id(&ChainId::default()); +// let tx2_id = tx2.id(&ChainId::default()); +// let tx3_id = tx3.id(&ChainId::default()); -#[tokio::test] -async fn message_of_squeezed_out_tx_can_be_resubmitted_at_lower_gas_price() { - // tx1 (message 1, message 2) gas_price 2 - // tx2 (message 1) gas_price 3 - // squeezes tx1 with higher gas price - // tx3 (message 2) gas_price 1 - // works since tx1 is no longer part of txpool state even though gas price is less - - let mut context = PoolContext::default(); - let (message_1, message_input_1) = create_message_predicate_from_message(10_000, 0); - let (message_2, message_input_2) = create_message_predicate_from_message(20_000, 1); - - // Insert a tx for the message id with a low gas amount - let tx1 = TransactionBuilder::script(vec![], vec![]) - .tip(2) - .max_fee_limit(2) - .script_gas_limit(GAS_LIMIT) - .add_input(message_input_1.clone()) - .add_input(message_input_2.clone()) - .finalize_as_transaction(); - - let tx2 = TransactionBuilder::script(vec![], vec![]) - .tip(3) - .max_fee_limit(3) - .script_gas_limit(GAS_LIMIT) - .add_input(message_input_1) - .finalize_as_transaction(); - - let tx3 = TransactionBuilder::script(vec![], vec![]) - .tip(1) - .max_fee_limit(1) - .script_gas_limit(GAS_LIMIT) - .add_input(message_input_2) - .finalize_as_transaction(); - - context.database_mut().insert_message(message_1); - context.database_mut().insert_message(message_2); - let mut txpool = context.build(); - - let tx1 = check_unwrap_tx(tx1, &txpool.config).await; - let tx2 = check_unwrap_tx(tx2, &txpool.config).await; - let tx3 = check_unwrap_tx(tx3, &txpool.config).await; - - let results = txpool - .insert(vec![ - check_tx_to_pool(tx1.clone()), - check_tx_to_pool(tx2.clone()), - check_tx_to_pool(tx3.clone()), - ]) - .unwrap(); - assert_eq!(results.len(), 3); - assert!(results[0].is_ok()); - assert!(results[1].is_ok()); - assert!(results[2].is_ok()); -} +// let mut txpool = context.build(); +// let tx1 = check_unwrap_tx(tx1, &txpool.config).await; +// let tx2 = check_unwrap_tx(tx2, &txpool.config).await; +// let tx3 = check_unwrap_tx(tx3, &txpool.config).await; + +// let results = txpool +// .insert(vec![ +// check_tx_to_pool(tx1.clone()), +// check_tx_to_pool(tx2.clone()), +// check_tx_to_pool(tx3.clone()), +// ]) +// .unwrap(); +// assert_eq!(results.len(), 3); +// assert!(results[0].is_ok()); +// assert!(results[1].is_ok()); +// assert!(results[2].is_ok()); + +// let txs = txpool.extract_transactions_for_block().unwrap(); +// assert_eq!(txs.len(), 3, "Should have 3 txs"); + +// assert_eq!(txs[0].id(), tx3_id, "First should be tx3"); +// assert_eq!(txs[1].id(), tx2_id, "Second should be tx2"); +// assert_eq!(txs[2].id(), tx1_id, "Third should be tx1"); +// } -#[tokio::test] -async fn predicates_with_incorrect_owner_fails() { - let mut context = PoolContext::default(); - let mut coin = context.random_predicate(AssetId::BASE, TEST_COIN_AMOUNT, None); - if let Input::CoinPredicate(CoinPredicate { owner, .. }) = &mut coin { - *owner = Address::zeroed(); - } - - let tx = TransactionBuilder::script(vec![], vec![]) - .script_gas_limit(GAS_LIMIT) - .add_input(coin) - .finalize_as_transaction(); - - let err = check_tx(tx, &Default::default()) - .await - .expect_err("Transaction should be err, got ok"); - - assert!( - format!("{err:?}").contains("InputPredicateOwner"), - "unexpected error: {err:?}", - ) -} +// #[tokio::test] +// async fn sorted_out_tx_by_creation_instant() { +// let mut context = PoolContext::default(); + +// let (_, gas_coin) = context.setup_coin(); +// let tx1 = TransactionBuilder::script(vec![], vec![]) +// .tip(4) +// .max_fee_limit(4) +// .script_gas_limit(GAS_LIMIT) +// .add_input(gas_coin) +// .finalize_as_transaction(); + +// let (_, gas_coin) = context.setup_coin(); +// let tx2 = TransactionBuilder::script(vec![], vec![]) +// .tip(4) +// .max_fee_limit(4) +// .script_gas_limit(GAS_LIMIT) +// .add_input(gas_coin) +// .finalize_as_transaction(); + +// let (_, gas_coin) = context.setup_coin(); +// let tx3 = TransactionBuilder::script(vec![], vec![]) +// .tip(4) +// .max_fee_limit(4) +// .script_gas_limit(GAS_LIMIT) +// .add_input(gas_coin) +// .finalize_as_transaction(); + +// let (_, gas_coin) = context.setup_coin(); +// let tx4 = TransactionBuilder::script(vec![], vec![]) +// .tip(4) +// .max_fee_limit(4) +// .script_gas_limit(GAS_LIMIT) +// .add_input(gas_coin) +// .finalize_as_transaction(); + +// let tx1_id = tx1.id(&ChainId::default()); +// let tx2_id = tx2.id(&ChainId::default()); +// let tx3_id = tx3.id(&ChainId::default()); +// let tx4_id = tx4.id(&ChainId::default()); -#[tokio::test] -async fn predicate_without_enough_gas_returns_out_of_gas() { - let mut context = PoolContext::default(); +// let mut txpool = context.build(); +// let tx1 = check_unwrap_tx(tx1, &txpool.config).await; +// let tx2 = check_unwrap_tx(tx2, &txpool.config).await; +// let tx3 = check_unwrap_tx(tx3, &txpool.config).await; +// let tx4 = check_unwrap_tx(tx4, &txpool.config).await; + +// let results = txpool +// .insert(vec![ +// check_tx_to_pool(tx1.clone()), +// check_tx_to_pool(tx2.clone()), +// check_tx_to_pool(tx3.clone()), +// check_tx_to_pool(tx4.clone()), +// ]) +// .unwrap(); +// assert_eq!(results.len(), 4); +// assert!(results[0].is_ok()); +// assert!(results[1].is_ok()); +// assert!(results[2].is_ok()); +// assert!(results[3].is_ok()); + +// let txs = txpool.extract_transactions_for_block().unwrap(); + +// // This order doesn't match the lexicographical order of the tx ids +// // and so it verifies that the txs are sorted by creation instant +// // The newest tx should be first +// assert_eq!(txs.len(), 4, "Should have 4 txs"); +// assert_eq!(txs[0].id(), tx1_id, "First should be tx1"); +// assert_eq!(txs[1].id(), tx2_id, "Second should be tx2"); +// assert_eq!(txs[2].id(), tx3_id, "Third should be tx3"); +// assert_eq!(txs[3].id(), tx4_id, "Fourth should be tx4"); +// } - let gas_limit = 10000; +// #[tokio::test] +// async fn tx_at_least_min_gas_price_is_insertable() { +// let gas_price = 10; +// let mut context = PoolContext::default().config(Config { +// ..Default::default() +// }); - let mut consensus_parameters = ConsensusParameters::default(); - consensus_parameters - .set_tx_params(TxParameters::default().with_max_gas_per_tx(gas_limit)); - consensus_parameters.set_predicate_params( - PredicateParameters::default().with_max_gas_per_predicate(gas_limit), - ); +// let (_, gas_coin) = context.setup_coin(); +// let tx = TransactionBuilder::script(vec![], vec![]) +// .tip(10) +// .max_fee_limit(1000) +// .script_gas_limit(GAS_LIMIT) +// .add_input(gas_coin) +// .finalize_as_transaction(); - let coin = context - .custom_predicate( - AssetId::BASE, - TEST_COIN_AMOUNT, - // forever loop - vec![op::jmp(RegId::ZERO)].into_iter().collect(), - None, - ) - .into_estimated(&consensus_parameters); - - let tx = TransactionBuilder::script(vec![], vec![]) - .script_gas_limit(GAS_LIMIT) - .add_input(coin) - .finalize_as_transaction(); - - let err = check_tx(tx, &Default::default()) - .await - .expect_err("Transaction should be err, got ok"); - - assert!( - err.to_string() - .contains("PredicateVerificationFailed(OutOfGas)"), - "unexpected error: {err}", - ) -} +// let mut txpool = context.build(); +// let tx = check_unwrap_tx_with_gas_price(tx, &txpool.config, gas_price).await; -#[tokio::test] -async fn predicate_that_returns_false_is_invalid() { - let mut context = PoolContext::default(); - let coin = context - .custom_predicate( - AssetId::BASE, - TEST_COIN_AMOUNT, - // forever loop - vec![op::ret(RegId::ZERO)].into_iter().collect(), - None, - ) - .into_default_estimated(); - - let tx = TransactionBuilder::script(vec![], vec![]) - .script_gas_limit(GAS_LIMIT) - .add_input(coin) - .finalize_as_transaction(); - - let err = check_tx(tx, &Default::default()) - .await - .expect_err("Transaction should be err, got ok"); - - assert!( - err.to_string().contains("PredicateVerificationFailed"), - "unexpected error: {err}", - ) -} +// let results = txpool.insert(vec![check_tx_to_pool(tx)]).unwrap(); +// assert_eq!(results.len(), 1); +// assert!(results[0].is_ok()); +// } -#[tokio::test] -async fn insert_single__blob_tx_works() { - let program = vec![123; 123]; - let tx = TransactionBuilder::blob(BlobBody { - id: BlobId::compute(program.as_slice()), - witness_index: 0, - }) - .add_witness(program.into()) - .add_random_fee_input() - .finalize_as_transaction(); +// #[tokio::test] +// async fn tx_below_min_gas_price_is_not_insertable() { +// let mut context = PoolContext::default(); + +// let gas_coin = context.random_predicate(AssetId::BASE, TEST_COIN_AMOUNT, None); +// let tx = TransactionBuilder::script(vec![], vec![]) +// .tip(10) +// .max_fee_limit(10) +// .script_gas_limit(GAS_LIMIT) +// .add_input(gas_coin) +// .finalize_as_transaction(); +// let gas_price = 11; + +// let err = check_tx_with_gas_price( +// tx, +// &Config { +// ..Default::default() +// }, +// gas_price, +// ) +// .await +// .expect_err("expected insertion failure"); - let config = Config { - utxo_validation: false, - ..Default::default() - }; - let context = PoolContext::default().config(config); - let mut txpool = context.build(); +// assert!(matches!( +// err, +// Error::ConsensusValidity(CheckError::InsufficientMaxFee { .. }) +// )); +// } - // Given - let tx = check_unwrap_tx(tx, &txpool.config).await; - let id = tx.id(); +// #[tokio::test] +// async fn tx_inserted_into_pool_when_input_message_id_exists_in_db() { +// let mut context = PoolContext::default(); +// let (message, input) = create_message_predicate_from_message(5000, 0); - // When - let results = txpool.insert(vec![check_tx_to_pool(tx)]).unwrap(); - assert_eq!(results.len(), 1); - assert!(results[0].is_ok()); +// let tx = TransactionBuilder::script(vec![], vec![]) +// .script_gas_limit(GAS_LIMIT) +// .add_input(input) +// .finalize_as_transaction(); - assert!(txpool.find_one(&id).is_some(), "Should find tx in pool"); -} +// context.database_mut().insert_message(message); -#[tokio::test] -async fn insert_single__blob_tx_fails_if_blob_already_inserted_and_lower_tip() { - let program = vec![123; 123]; - let blob_id = BlobId::compute(program.as_slice()); - let tx = TransactionBuilder::blob(BlobBody { - id: blob_id, - witness_index: 0, - }) - .add_witness(program.clone().into()) - .add_random_fee_input() - .finalize_as_transaction(); +// let tx1_id = tx.id(&ChainId::default()); +// let mut txpool = context.build(); - let config = Config { - utxo_validation: false, - ..Default::default() - }; - let context = PoolContext::default().config(config); - let mut txpool = context.build(); - let tx = check_unwrap_tx(tx, &txpool.config).await; +// let tx = check_unwrap_tx(tx, &txpool.config).await; - // Given - let results = txpool.insert(vec![check_tx_to_pool(tx.clone())]).unwrap(); - assert_eq!(results.len(), 1); - assert!(results[0].is_ok()); - - let same_blob_tx = TransactionBuilder::blob(BlobBody { - id: blob_id, - witness_index: 1, - }) - .add_random_fee_input() - .add_witness(program.into()) - .finalize_as_transaction(); - let same_blob_tx = check_unwrap_tx(same_blob_tx, &txpool.config).await; +// let results = txpool.insert(vec![check_tx_to_pool(tx)]).unwrap(); +// assert_eq!(results.len(), 1); +// assert!(results[0].is_ok()); - // When - let results = txpool - .insert(vec![check_tx_to_pool(same_blob_tx.clone())]) - .unwrap(); - assert_eq!(results.len(), 1); - let err = results[0].as_ref().expect_err("Tx should be Err, got Ok"); +// let tx = txpool.find_one(&tx1_id).unwrap(); +// assert_eq!(tx.id(), tx1_id); +// } - // Then - assert!(matches!(err, Error::Collided(_))); -} +// #[tokio::test] +// async fn tx_rejected_from_pool_when_input_message_id_does_not_exist_in_db() { +// let context = PoolContext::default(); +// let (message, input) = create_message_predicate_from_message(5000, 0); +// let tx = TransactionBuilder::script(vec![], vec![]) +// .script_gas_limit(GAS_LIMIT) +// .add_input(input) +// .finalize_as_transaction(); + +// // Do not insert any messages into the DB to ensure there is no matching message for the +// // tx. +// let mut txpool = context.build(); +// let tx = check_unwrap_tx(tx, &txpool.config).await; -#[tokio::test] -async fn insert_single__blob_tx_succeeds_if_blob_already_inserted_but_higher_tip() { - let program = vec![123; 123]; - let blob_id = BlobId::compute(program.as_slice()); - let tx = TransactionBuilder::blob(BlobBody { - id: blob_id, - witness_index: 0, - }) - .add_witness(program.clone().into()) - .add_random_fee_input() - .finalize_as_transaction(); +// let results = txpool.insert(vec![check_tx_to_pool(tx)]).unwrap(); +// assert_eq!(results.len(), 1); - let config = Config { - utxo_validation: false, - ..Default::default() - }; - let context = PoolContext::default().config(config); - let mut txpool = context.build(); - let tx = check_unwrap_tx(tx, &txpool.config).await; +// let err = results[0].as_ref().expect_err("Tx should be Err, got Ok"); +// // check error +// assert!(matches!( +// err, +// Error::NotInsertedInputMessageUnknown(msg_id) if msg_id == message.id() +// )); +// } - // Given - let results = txpool.insert(vec![check_tx_to_pool(tx.clone())]).unwrap(); - assert_eq!(results.len(), 1); - assert!(results[0].is_ok()); - - let same_blob_tx = TransactionBuilder::blob(BlobBody { - id: blob_id, - witness_index: 1, - }) - .add_random_fee_input() - .add_witness(program.into()) - .tip(100) - .max_fee_limit(100) - .finalize_as_transaction(); - let same_blob_tx = check_unwrap_tx(same_blob_tx, &txpool.config).await; +// #[tokio::test] +// async fn tx_rejected_from_pool_when_gas_price_is_lower_than_another_tx_with_same_message_id( +// ) { +// let mut context = PoolContext::default(); +// let message_amount = 10_000; +// let max_fee_limit = 10u64; +// let gas_price_high = 2u64; +// let gas_price_low = 1u64; +// let (message, conflicting_message_input) = +// create_message_predicate_from_message(message_amount, 0); + +// let tx_high = TransactionBuilder::script(vec![], vec![]) +// .tip(gas_price_high) +// .max_fee_limit(max_fee_limit) +// .script_gas_limit(GAS_LIMIT) +// .add_input(conflicting_message_input.clone()) +// .finalize_as_transaction(); + +// let tx_low = TransactionBuilder::script(vec![], vec![]) +// .tip(gas_price_low) +// .max_fee_limit(max_fee_limit) +// .script_gas_limit(GAS_LIMIT) +// .add_input(conflicting_message_input) +// .finalize_as_transaction(); + +// context.database_mut().insert_message(message.clone()); - // When - let results = txpool - .insert(vec![check_tx_to_pool(same_blob_tx.clone())]) - .unwrap(); - assert_eq!(results.len(), 1); +// let mut txpool = context.build(); - // Then - assert!(results[0].is_ok()); -} +// let _tx_high_id = tx_high.id(&ChainId::default()); +// let tx_high = +// check_unwrap_tx_with_gas_price(tx_high, &txpool.config, gas_price_high).await; + +// // Insert a tx for the message id with a high gas amount +// let results = txpool.insert(vec![check_tx_to_pool(tx_high)]).unwrap(); +// assert_eq!(results.len(), 1); +// assert!(results[0].is_ok()); + +// let tx_low = +// check_unwrap_tx_with_gas_price(tx_low, &txpool.config, gas_price_low).await; +// // Insert a tx for the message id with a low gas amount +// // Because the new transaction's id matches an existing transaction, we compare the gas +// // prices of both the new and existing transactions. Since the existing transaction's gas +// // price is higher, we must now reject the new transaction. +// let results = txpool.insert(vec![check_tx_to_pool(tx_low)]).unwrap(); +// assert_eq!(results.len(), 1); +// let err = results[0].as_ref().expect_err("Tx should be Err, got Ok"); + +// // check error +// assert!(matches!(err, Error::Collided(_))); +// } -#[tokio::test] -async fn insert_single__blob_tx_fails_if_blob_already_exists_in_database() { - let program = vec![123; 123]; - let blob_id = BlobId::compute(program.as_slice()); - let tx = TransactionBuilder::blob(BlobBody { - id: blob_id, - witness_index: 0, - }) - .add_witness(program.clone().into()) - .add_random_fee_input() - .finalize_as_transaction(); +// #[tokio::test] +// async fn higher_priced_tx_squeezes_out_lower_priced_tx_with_same_message_id() { +// let mut context = PoolContext::default(); +// let message_amount = 10_000; +// let gas_price_high = 2u64; +// let max_fee_limit = 10u64; +// let gas_price_low = 1u64; +// let (message, conflicting_message_input) = +// create_message_predicate_from_message(message_amount, 0); + +// // Insert a tx for the message id with a low gas amount +// let tx_low = TransactionBuilder::script(vec![], vec![]) +// .tip(gas_price_low) +// .max_fee_limit(max_fee_limit) +// .script_gas_limit(GAS_LIMIT) +// .add_input(conflicting_message_input.clone()) +// .finalize_as_transaction(); + +// context.database_mut().insert_message(message); - let config = Config { - utxo_validation: false, - ..Default::default() - }; - let mut context = PoolContext::default().config(config); - // Given - context.database_mut().insert_dummy_blob(blob_id); - let mut txpool = context.build(); - let tx = check_unwrap_tx(tx, &txpool.config).await; +// let mut txpool = context.build(); +// let tx_low_id = tx_low.id(&ChainId::default()); +// let tx_low = +// check_unwrap_tx_with_gas_price(tx_low, &txpool.config, gas_price_low).await; +// let results = txpool.insert(vec![check_tx_to_pool(tx_low)]).unwrap(); +// assert_eq!(results.len(), 1); +// assert!(results[0].is_ok()); + +// // Insert a tx for the message id with a high gas amount +// // Because the new transaction's id matches an existing transaction, we compare the gas +// // prices of both the new and existing transactions. Since the existing transaction's gas +// // price is lower, we accept the new transaction and squeeze out the old transaction. +// let tx_high = TransactionBuilder::script(vec![], vec![]) +// .tip(gas_price_high) +// .max_fee_limit(max_fee_limit) +// .script_gas_limit(GAS_LIMIT) +// .add_input(conflicting_message_input) +// .finalize_as_transaction(); +// let tx_high = +// check_unwrap_tx_with_gas_price(tx_high, &txpool.config, gas_price_high).await; +// let results = txpool.insert(vec![check_tx_to_pool(tx_high)]).unwrap(); +// assert_eq!(results.len(), 1); +// assert!(results[0].is_ok()); +// let squeezed_out_txs = results[0].as_ref().unwrap(); + +// assert_eq!(squeezed_out_txs.len(), 1); +// assert_eq!(squeezed_out_txs[0].id(), tx_low_id,); +// } - // When - let result = txpool.insert(vec![check_tx_to_pool(tx.clone())]).unwrap(); - assert_eq!(result.len(), 1); - let err = result[0].as_ref().expect_err("Tx should be Err, got Ok"); +// #[tokio::test] +// async fn message_of_squeezed_out_tx_can_be_resubmitted_at_lower_gas_price() { +// // tx1 (message 1, message 2) gas_price 2 +// // tx2 (message 1) gas_price 3 +// // squeezes tx1 with higher gas price +// // tx3 (message 2) gas_price 1 +// // works since tx1 is no longer part of txpool state even though gas price is less + +// let mut context = PoolContext::default(); +// let (message_1, message_input_1) = create_message_predicate_from_message(10_000, 0); +// let (message_2, message_input_2) = create_message_predicate_from_message(20_000, 1); + +// // Insert a tx for the message id with a low gas amount +// let tx1 = TransactionBuilder::script(vec![], vec![]) +// .tip(2) +// .max_fee_limit(2) +// .script_gas_limit(GAS_LIMIT) +// .add_input(message_input_1.clone()) +// .add_input(message_input_2.clone()) +// .finalize_as_transaction(); + +// let tx2 = TransactionBuilder::script(vec![], vec![]) +// .tip(3) +// .max_fee_limit(3) +// .script_gas_limit(GAS_LIMIT) +// .add_input(message_input_1) +// .finalize_as_transaction(); + +// let tx3 = TransactionBuilder::script(vec![], vec![]) +// .tip(1) +// .max_fee_limit(1) +// .script_gas_limit(GAS_LIMIT) +// .add_input(message_input_2) +// .finalize_as_transaction(); + +// context.database_mut().insert_message(message_1); +// context.database_mut().insert_message(message_2); +// let mut txpool = context.build(); - // Then - assert!(matches!( - err, - Error::NotInsertedBlobIdAlreadyTaken(b) if *b == blob_id - )); -} +// let tx1 = check_unwrap_tx(tx1, &txpool.config).await; +// let tx2 = check_unwrap_tx(tx2, &txpool.config).await; +// let tx3 = check_unwrap_tx(tx3, &txpool.config).await; + +// let results = txpool +// .insert(vec![ +// check_tx_to_pool(tx1.clone()), +// check_tx_to_pool(tx2.clone()), +// check_tx_to_pool(tx3.clone()), +// ]) +// .unwrap(); +// assert_eq!(results.len(), 3); +// assert!(results[0].is_ok()); +// assert!(results[1].is_ok()); +// assert!(results[2].is_ok()); +// } -#[tokio::test] -async fn insert__if_tx3_depends_and_collides_witg_tx2() { - let mut context = PoolContext::default(); - - // tx1 {inputs: {}, outputs: {coinA}, tip: 1} - let (_, gas_coin) = context.setup_coin(); - let (output_a, unset_input) = context.create_output_and_input(1); - let tx1 = TransactionBuilder::script(vec![], vec![]) - .tip(1) - .max_fee_limit(1) - .script_gas_limit(GAS_LIMIT) - .add_input(gas_coin) - .add_output(output_a) - .finalize_as_transaction(); - - // tx2 {inputs: {coinA}, outputs: {coinB}, tip: 1} - let (_, gas_coin) = context.setup_coin(); - let input_a = unset_input.into_input(UtxoId::new(tx1.id(&Default::default()), 0)); - let (output_b, unset_input) = context.create_output_and_input(1); - let tx2 = TransactionBuilder::script(vec![], vec![]) - .tip(1) - .max_fee_limit(1) - .script_gas_limit(GAS_LIMIT) - .add_input(input_a.clone()) - .add_input(gas_coin) - .add_output(output_b) - .finalize_as_transaction(); +// #[tokio::test] +// async fn predicates_with_incorrect_owner_fails() { +// let mut context = PoolContext::default(); +// let mut coin = context.random_predicate(AssetId::BASE, TEST_COIN_AMOUNT, None); +// if let Input::CoinPredicate(CoinPredicate { owner, .. }) = &mut coin { +// *owner = Address::zeroed(); +// } + +// let tx = TransactionBuilder::script(vec![], vec![]) +// .script_gas_limit(GAS_LIMIT) +// .add_input(coin) +// .finalize_as_transaction(); + +// let err = check_tx(tx, &Default::default()) +// .await +// .expect_err("Transaction should be err, got ok"); + +// assert!( +// format!("{err:?}").contains("InputPredicateOwner"), +// "unexpected error: {err:?}", +// ) +// } - // Given - // tx3 {inputs: {coinA, coinB}, outputs:{}, tip: 20} - let (_, gas_coin) = context.setup_coin(); - let input_b = unset_input.into_input(UtxoId::new(tx2.id(&Default::default()), 0)); - let tx3 = TransactionBuilder::script(vec![], vec![]) - .tip(20) - .max_fee_limit(20) - .script_gas_limit(GAS_LIMIT) - .add_input(input_a) - .add_input(input_b.clone()) - .add_input(gas_coin) - .finalize_as_transaction(); - - let (_, gas_coin) = context.setup_coin(); - - let mut txpool = context.build(); - let tx1 = check_unwrap_tx(tx1, &txpool.config).await; - let tx2 = check_unwrap_tx(tx2, &txpool.config).await; - let tx3 = check_unwrap_tx(tx3, &txpool.config).await; +// #[tokio::test] +// async fn predicate_without_enough_gas_returns_out_of_gas() { +// let mut context = PoolContext::default(); + +// let gas_limit = 10000; + +// let mut consensus_parameters = ConsensusParameters::default(); +// consensus_parameters +// .set_tx_params(TxParameters::default().with_max_gas_per_tx(gas_limit)); +// consensus_parameters.set_predicate_params( +// PredicateParameters::default().with_max_gas_per_predicate(gas_limit), +// ); + +// let coin = context +// .custom_predicate( +// AssetId::BASE, +// TEST_COIN_AMOUNT, +// // forever loop +// vec![op::jmp(RegId::ZERO)].into_iter().collect(), +// None, +// ) +// .into_estimated(&consensus_parameters); + +// let tx = TransactionBuilder::script(vec![], vec![]) +// .script_gas_limit(GAS_LIMIT) +// .add_input(coin) +// .finalize_as_transaction(); + +// let err = check_tx(tx, &Default::default()) +// .await +// .expect_err("Transaction should be err, got ok"); + +// assert!( +// err.to_string() +// .contains("PredicateVerificationFailed(OutOfGas)"), +// "unexpected error: {err}", +// ) +// } - // When - let results = txpool - .insert(vec![ - check_tx_to_pool(tx1.clone()), - check_tx_to_pool(tx2.clone()), - check_tx_to_pool(tx3.clone()), - ]) - .unwrap(); - assert_eq!(results.len(), 3); - assert!(results[0].is_ok()); - assert!(results[1].is_ok()); +// #[tokio::test] +// async fn predicate_that_returns_false_is_invalid() { +// let mut context = PoolContext::default(); +// let coin = context +// .custom_predicate( +// AssetId::BASE, +// TEST_COIN_AMOUNT, +// // forever loop +// vec![op::ret(RegId::ZERO)].into_iter().collect(), +// None, +// ) +// .into_default_estimated(); + +// let tx = TransactionBuilder::script(vec![], vec![]) +// .script_gas_limit(GAS_LIMIT) +// .add_input(coin) +// .finalize_as_transaction(); + +// let err = check_tx(tx, &Default::default()) +// .await +// .expect_err("Transaction should be err, got ok"); + +// assert!( +// err.to_string().contains("PredicateVerificationFailed"), +// "unexpected error: {err}", +// ) +// } - // Then - let err = results[2].as_ref().expect_err("Tx3 should be Err, got Ok"); - assert!(matches!(err, Error::Storage(_))); -} +// #[tokio::test] +// async fn insert_single__blob_tx_works() { +// let program = vec![123; 123]; +// let tx = TransactionBuilder::blob(BlobBody { +// id: BlobId::compute(program.as_slice()), +// witness_index: 0, +// }) +// .add_witness(program.into()) +// .add_random_fee_input() +// .finalize_as_transaction(); + +// let config = Config { +// utxo_validation: false, +// ..Default::default() +// }; +// let context = PoolContext::default().config(config); +// let mut txpool = context.build(); + +// // Given +// let tx = check_unwrap_tx(tx, &txpool.config).await; +// let id = tx.id(); + +// // When +// let results = txpool.insert(vec![check_tx_to_pool(tx)]).unwrap(); +// assert_eq!(results.len(), 1); +// assert!(results[0].is_ok()); + +// assert!(txpool.find_one(&id).is_some(), "Should find tx in pool"); +// } -// TODO: Reinstantiatte when https://github.com/FuelLabs/fuel-core/issues/2186 is implemented // #[tokio::test] -// async fn insert_inner__rejects_upgrade_tx_with_invalid_wasm() { -// let predicate = vec![op::ret(1)].into_iter().collect::>(); -// let privileged_address = Input::predicate_owner(predicate.clone()); +// async fn insert_single__blob_tx_fails_if_blob_already_inserted_and_lower_tip() { +// let program = vec![123; 123]; +// let blob_id = BlobId::compute(program.as_slice()); +// let tx = TransactionBuilder::blob(BlobBody { +// id: blob_id, +// witness_index: 0, +// }) +// .add_witness(program.clone().into()) +// .add_random_fee_input() +// .finalize_as_transaction(); // let config = Config { // utxo_validation: false, // ..Default::default() // }; -// let context = PoolContext::default() -// .config(config) -// .wasm_checker(MockWasmChecker { -// result: Err(WasmValidityError::NotValid), -// }); +// let context = PoolContext::default().config(config); // let mut txpool = context.build(); -// let gas_price_provider = MockTxPoolGasPrice::new(0); +// let tx = check_unwrap_tx(tx, &txpool.config).await; // // Given -// let tx = TransactionBuilder::upgrade(UpgradePurpose::StateTransition { -// root: Bytes32::new([1; 32]), +// let results = txpool.insert(vec![check_tx_to_pool(tx.clone())]).unwrap(); +// assert_eq!(results.len(), 1); +// assert!(results[0].is_ok()); + +// let same_blob_tx = TransactionBuilder::blob(BlobBody { +// id: blob_id, +// witness_index: 1, // }) -// .add_input(Input::coin_predicate( -// UtxoId::new(Bytes32::new([1; 32]), 0), -// privileged_address, -// 1_000_000_000, -// AssetId::BASE, -// Default::default(), -// Default::default(), -// predicate, -// vec![], -// )) +// .add_random_fee_input() +// .add_witness(program.into()) // .finalize_as_transaction(); -// let mut params = ConsensusParameters::default(); -// params.set_privileged_address(privileged_address); -// let tx = check_single_tx( -// tx, -// Default::default(), -// false, -// ¶ms, -// &gas_price_provider, -// MemoryInstance::new(), -// ) -// .await -// .expect("Transaction should be checked"); +// let same_blob_tx = check_unwrap_tx(same_blob_tx, &txpool.config).await; // // When -// let result = txpool.insert_single(tx); +// let results = txpool +// .insert(vec![check_tx_to_pool(same_blob_tx.clone())]) +// .unwrap(); +// assert_eq!(results.len(), 1); +// let err = results[0].as_ref().expect_err("Tx should be Err, got Ok"); // // Then -// assert_eq!(result, Err(Error::NotInsertedInvalidWasm)); +// assert!(matches!(err, Error::Collided(_))); // } + +// #[tokio::test] +// async fn insert_single__blob_tx_succeeds_if_blob_already_inserted_but_higher_tip() { +// let program = vec![123; 123]; +// let blob_id = BlobId::compute(program.as_slice()); +// let tx = TransactionBuilder::blob(BlobBody { +// id: blob_id, +// witness_index: 0, +// }) +// .add_witness(program.clone().into()) +// .add_random_fee_input() +// .finalize_as_transaction(); + +// let config = Config { +// utxo_validation: false, +// ..Default::default() +// }; +// let context = PoolContext::default().config(config); +// let mut txpool = context.build(); +// let tx = check_unwrap_tx(tx, &txpool.config).await; + +// // Given +// let results = txpool.insert(vec![check_tx_to_pool(tx.clone())]).unwrap(); +// assert_eq!(results.len(), 1); +// assert!(results[0].is_ok()); + +// let same_blob_tx = TransactionBuilder::blob(BlobBody { +// id: blob_id, +// witness_index: 1, +// }) +// .add_random_fee_input() +// .add_witness(program.into()) +// .tip(100) +// .max_fee_limit(100) +// .finalize_as_transaction(); +// let same_blob_tx = check_unwrap_tx(same_blob_tx, &txpool.config).await; + +// // When +// let results = txpool +// .insert(vec![check_tx_to_pool(same_blob_tx.clone())]) +// .unwrap(); +// assert_eq!(results.len(), 1); + +// // Then +// assert!(results[0].is_ok()); +// } + +// #[tokio::test] +// async fn insert_single__blob_tx_fails_if_blob_already_exists_in_database() { +// let program = vec![123; 123]; +// let blob_id = BlobId::compute(program.as_slice()); +// let tx = TransactionBuilder::blob(BlobBody { +// id: blob_id, +// witness_index: 0, +// }) +// .add_witness(program.clone().into()) +// .add_random_fee_input() +// .finalize_as_transaction(); + +// let config = Config { +// utxo_validation: false, +// ..Default::default() +// }; +// let mut context = PoolContext::default().config(config); +// // Given +// context.database_mut().insert_dummy_blob(blob_id); +// let mut txpool = context.build(); +// let tx = check_unwrap_tx(tx, &txpool.config).await; + +// // When +// let result = txpool.insert(vec![check_tx_to_pool(tx.clone())]).unwrap(); +// assert_eq!(result.len(), 1); +// let err = result[0].as_ref().expect_err("Tx should be Err, got Ok"); + +// // Then +// assert!(matches!( +// err, +// Error::NotInsertedBlobIdAlreadyTaken(b) if *b == blob_id +// )); +// } + +// #[tokio::test] +// async fn insert__if_tx3_depends_and_collides_witg_tx2() { +// let mut context = PoolContext::default(); + +// // tx1 {inputs: {}, outputs: {coinA}, tip: 1} +// let (_, gas_coin) = context.setup_coin(); +// let (output_a, unset_input) = context.create_output_and_input(1); +// let tx1 = TransactionBuilder::script(vec![], vec![]) +// .tip(1) +// .max_fee_limit(1) +// .script_gas_limit(GAS_LIMIT) +// .add_input(gas_coin) +// .add_output(output_a) +// .finalize_as_transaction(); + +// // tx2 {inputs: {coinA}, outputs: {coinB}, tip: 1} +// let (_, gas_coin) = context.setup_coin(); +// let input_a = unset_input.into_input(UtxoId::new(tx1.id(&Default::default()), 0)); +// let (output_b, unset_input) = context.create_output_and_input(1); +// let tx2 = TransactionBuilder::script(vec![], vec![]) +// .tip(1) +// .max_fee_limit(1) +// .script_gas_limit(GAS_LIMIT) +// .add_input(input_a.clone()) +// .add_input(gas_coin) +// .add_output(output_b) +// .finalize_as_transaction(); + +// // Given +// // tx3 {inputs: {coinA, coinB}, outputs:{}, tip: 20} +// let (_, gas_coin) = context.setup_coin(); +// let input_b = unset_input.into_input(UtxoId::new(tx2.id(&Default::default()), 0)); +// let tx3 = TransactionBuilder::script(vec![], vec![]) +// .tip(20) +// .max_fee_limit(20) +// .script_gas_limit(GAS_LIMIT) +// .add_input(input_a) +// .add_input(input_b.clone()) +// .add_input(gas_coin) +// .finalize_as_transaction(); + +// let (_, gas_coin) = context.setup_coin(); + +// let mut txpool = context.build(); +// let tx1 = check_unwrap_tx(tx1, &txpool.config).await; +// let tx2 = check_unwrap_tx(tx2, &txpool.config).await; +// let tx3 = check_unwrap_tx(tx3, &txpool.config).await; + +// // When +// let results = txpool +// .insert(vec![ +// check_tx_to_pool(tx1.clone()), +// check_tx_to_pool(tx2.clone()), +// check_tx_to_pool(tx3.clone()), +// ]) +// .unwrap(); +// assert_eq!(results.len(), 3); +// assert!(results[0].is_ok()); +// assert!(results[1].is_ok()); + +// // Then +// let err = results[2].as_ref().expect_err("Tx3 should be Err, got Ok"); +// assert!(matches!(err, Error::Storage(_))); +// } + +// // TODO: Reinstantiatte when https://github.com/FuelLabs/fuel-core/issues/2186 is implemented +// // #[tokio::test] +// // async fn insert_inner__rejects_upgrade_tx_with_invalid_wasm() { +// // let predicate = vec![op::ret(1)].into_iter().collect::>(); +// // let privileged_address = Input::predicate_owner(predicate.clone()); + +// // let config = Config { +// // utxo_validation: false, +// // ..Default::default() +// // }; +// // let context = PoolContext::default() +// // .config(config) +// // .wasm_checker(MockWasmChecker { +// // result: Err(WasmValidityError::NotValid), +// // }); +// // let mut txpool = context.build(); +// // let gas_price_provider = MockTxPoolGasPrice::new(0); + +// // // Given +// // let tx = TransactionBuilder::upgrade(UpgradePurpose::StateTransition { +// // root: Bytes32::new([1; 32]), +// // }) +// // .add_input(Input::coin_predicate( +// // UtxoId::new(Bytes32::new([1; 32]), 0), +// // privileged_address, +// // 1_000_000_000, +// // AssetId::BASE, +// // Default::default(), +// // Default::default(), +// // predicate, +// // vec![], +// // )) +// // .finalize_as_transaction(); +// // let mut params = ConsensusParameters::default(); +// // params.set_privileged_address(privileged_address); +// // let tx = check_single_tx( +// // tx, +// // Default::default(), +// // false, +// // ¶ms, +// // &gas_price_provider, +// // MemoryInstance::new(), +// // ) +// // .await +// // .expect("Transaction should be checked"); + +// // // When +// // let result = txpool.insert_single(tx); + +// // // Then +// // assert_eq!(result, Err(Error::NotInsertedInvalidWasm)); +// // } From 96e0cc92a2a707f30977472557ed24953efb5667 Mon Sep 17 00:00:00 2001 From: AurelienFT Date: Tue, 17 Sep 2024 17:21:33 +0200 Subject: [PATCH 08/28] Update all the tests --- .../services/txpool_v2/src/tests/context.rs | 55 +- crates/services/txpool_v2/src/tests/pool.rs | 2055 +++++++---------- 2 files changed, 940 insertions(+), 1170 deletions(-) diff --git a/crates/services/txpool_v2/src/tests/context.rs b/crates/services/txpool_v2/src/tests/context.rs index 01a8a973446..c477748a763 100644 --- a/crates/services/txpool_v2/src/tests/context.rs +++ b/crates/services/txpool_v2/src/tests/context.rs @@ -142,7 +142,7 @@ pub fn create_contract_output(contract_id: ContractId) -> Output { // use some arbitrary large amount, this shouldn't affect the txpool logic except for covering // the byte and gas price fees. pub const TEST_COIN_AMOUNT: u64 = 100_000_000u64; -const GAS_LIMIT: Word = 100000; +pub const GAS_LIMIT: Word = 100000; pub struct TestPoolUniverse { mock_db: MockDb, @@ -207,7 +207,7 @@ impl TestPoolUniverse { tx_builder.add_output(output); } tx_builder.tip(tip); - tx_builder.max_fee_limit(tip); + tx_builder.max_fee_limit(10000); tx_builder.finalize().into() } @@ -233,6 +233,57 @@ impl TestPoolUniverse { } } + pub async fn verify_and_insert_with_gas_price( + &mut self, + tx: Transaction, + gas_price: GasPrice, + ) -> Result { + if let Some(pool) = &self.pool { + let tx = perform_all_verifications( + tx, + pool.clone(), + Default::default(), + &ConsensusParameters::default(), + 0, + &MockTxPoolGasPrice::new(gas_price), + &MockWasmChecker::new(Ok(())), + MemoryInstance::new(), + ) + .await?; + pool.write().insert(tx) + } else { + panic!("Pool needs to be built first"); + } + } + + pub async fn verify_and_insert_with_consensus_params_wasm_checker( + &mut self, + tx: Transaction, + consensus_params: ConsensusParameters, + wasm_checker: MockWasmChecker, + ) -> Result { + if let Some(pool) = &self.pool { + let tx = perform_all_verifications( + tx, + pool.clone(), + Default::default(), + &consensus_params, + 0, + &MockTxPoolGasPrice::new(0), + &wasm_checker, + MemoryInstance::new(), + ) + .await?; + pool.write().insert(tx) + } else { + panic!("Pool needs to be built first"); + } + } + + pub fn get_pool(&self) -> TxPool { + self.pool.clone().unwrap() + } + pub fn setup_coin(&mut self) -> (Coin, Input) { let input = self.random_predicate(AssetId::BASE, TEST_COIN_AMOUNT, None); diff --git a/crates/services/txpool_v2/src/tests/pool.rs b/crates/services/txpool_v2/src/tests/pool.rs index fdb1031755f..9483b15874e 100644 --- a/crates/services/txpool_v2/src/tests/pool.rs +++ b/crates/services/txpool_v2/src/tests/pool.rs @@ -3,14 +3,19 @@ use crate::{ config::Config, error::Error, - tests::context::{ - create_coin_output, - create_contract_input, - create_contract_output, - create_message_predicate_from_message, - IntoEstimated, - TestPoolUniverse, - TEST_COIN_AMOUNT, + ports::WasmValidityError, + tests::{ + context::{ + create_coin_output, + create_contract_input, + create_contract_output, + create_message_predicate_from_message, + IntoEstimated, + TestPoolUniverse, + GAS_LIMIT, + TEST_COIN_AMOUNT, + }, + mocks::MockWasmChecker, }, }; use fuel_core_types::{ @@ -20,31 +25,39 @@ use fuel_core_types::{ RegId, Word, }, + fuel_merkle::common, fuel_tx::{ + consensus_parameters::gas, input::coin::CoinPredicate, Address, AssetId, BlobBody, BlobId, BlobIdExt, + Bytes32, ConsensusParameters, Contract, Finalizable, Input, Output, + PanicReason, PredicateParameters, Transaction, TransactionBuilder, TxParameters, UniqueIdentifier, + UpgradePurpose, UtxoId, }, fuel_types::ChainId, - fuel_vm::checked_transaction::{ - CheckError, - Checked, - CheckedTransaction, - IntoChecked, + fuel_vm::{ + checked_transaction::{ + CheckError, + Checked, + CheckedTransaction, + IntoChecked, + }, + PredicateVerificationFailed, }, services::txpool::{ self, @@ -73,7 +86,7 @@ async fn insert__tx_with_blacklisted_utxo_id() { let mut universe = TestPoolUniverse::default(); // Given - let (_, coin) = universe.setup_coin(); + let coin = universe.setup_coin().1; let utxo_id = *coin.utxo_id().unwrap(); universe.config.black_list.coins.insert(utxo_id); universe.build_pool(); @@ -91,7 +104,7 @@ async fn insert__tx_with_blacklisted_owner() { let mut universe = TestPoolUniverse::default(); // Given - let (_, coin) = universe.setup_coin(); + let coin = universe.setup_coin().1; let owner_addr = *coin.input_owner().unwrap(); universe.config.black_list.owners.insert(owner_addr); universe.build_pool(); @@ -260,11 +273,10 @@ async fn insert__tx_with_dependency_on_invalid_utxo_type() { #[tokio::test] async fn insert__already_known_tx() { - let config = Config { + let mut universe = TestPoolUniverse::default().config(Config { utxo_validation: false, ..Default::default() - }; - let mut universe = TestPoolUniverse::default().config(config); + }); universe.build_pool(); // Given @@ -304,10 +316,11 @@ async fn insert_higher_priced_tx_removes_lower_priced_tx() { universe.build_pool(); // Given - let (_, coin_input) = universe.setup_coin(); - let tx1 = universe.build_script_transaction(Some(vec![coin_input.clone()]), None, 10); + let common_coin = universe.setup_coin().1; + let tx1 = + universe.build_script_transaction(Some(vec![common_coin.clone()]), None, 10); let tx_id = tx1.id(&ChainId::default()); - let tx2 = universe.build_script_transaction(Some(vec![coin_input]), None, 20); + let tx2 = universe.build_script_transaction(Some(vec![common_coin]), None, 20); // When let result1 = universe.verify_and_insert(tx1).await.unwrap(); @@ -317,1150 +330,856 @@ async fn insert_higher_priced_tx_removes_lower_priced_tx() { assert_eq!(result2[0].id(), tx_id); } -// #[tokio::test] -// async fn underpriced_tx1_not_included_coin_collision() { -// let mut context = PoolContext::default(); - -// let (_, gas_coin) = context.setup_coin(); -// let (output, unset_input) = context.create_output_and_input(20); -// let tx1 = TransactionBuilder::script(vec![], vec![]) -// .tip(20) -// .max_fee_limit(20) -// .script_gas_limit(GAS_LIMIT) -// .add_input(gas_coin) -// .add_output(output) -// .finalize_as_transaction(); - -// let input = unset_input.into_input(UtxoId::new(tx1.id(&Default::default()), 0)); - -// let tx2 = TransactionBuilder::script(vec![], vec![]) -// .tip(20) -// .max_fee_limit(20) -// .script_gas_limit(GAS_LIMIT) -// .add_input(input.clone()) -// .finalize_as_transaction(); - -// let tx3 = TransactionBuilder::script(vec![], vec![]) -// .tip(10) -// .max_fee_limit(10) -// .script_gas_limit(GAS_LIMIT) -// .add_input(input) -// .finalize_as_transaction(); - -// let mut txpool = context.build(); -// let tx1_checked = check_unwrap_tx(tx1.clone(), &txpool.config).await; -// let tx2_checked = check_unwrap_tx(tx2.clone(), &txpool.config).await; -// let tx3_checked = check_unwrap_tx(tx3, &txpool.config).await; - -// let results = txpool -// .insert(vec![ -// check_tx_to_pool(tx1_checked), -// check_tx_to_pool(tx2_checked), -// check_tx_to_pool(tx3_checked), -// ]) -// .unwrap(); -// assert_eq!(results.len(), 3); -// assert!(results[0].is_ok()); -// assert!(results[1].is_ok()); -// let err = results[2].as_ref().expect_err("Tx3 should be Err, got Ok"); - -// assert!(matches!(err, Error::Collided(_))); -// } - -// #[tokio::test] -// async fn dependent_contract_input_inserted() { -// let mut context = PoolContext::default(); - -// let contract_id = Contract::EMPTY_CONTRACT_ID; -// let (_, gas_funds) = context.setup_coin(); -// let tx1 = TransactionBuilder::create( -// Default::default(), -// Default::default(), -// Default::default(), -// ) -// .tip(10) -// .max_fee_limit(10) -// .add_input(gas_funds) -// .add_output(create_contract_output(contract_id)) -// .finalize_as_transaction(); - -// let (_, gas_funds) = context.setup_coin(); -// let tx2 = TransactionBuilder::script(vec![], vec![]) -// .tip(10) -// .max_fee_limit(10) -// .script_gas_limit(GAS_LIMIT) -// .add_input(gas_funds) -// .add_input(create_contract_input( -// Default::default(), -// Default::default(), -// contract_id, -// )) -// .add_output(Output::contract(1, Default::default(), Default::default())) -// .finalize_as_transaction(); - -// let mut txpool = context.build(); -// let tx1 = check_unwrap_tx(tx1, &txpool.config).await; -// let tx2 = check_unwrap_tx(tx2, &txpool.config).await; - -// let results = txpool -// .insert(vec![check_tx_to_pool(tx1), check_tx_to_pool(tx2)]) -// .unwrap(); -// assert_eq!(results.len(), 2); -// assert!(results[0].is_ok()); -// assert!(results[1].is_ok()); -// } - -// #[tokio::test] -// async fn more_priced_tx3_removes_tx1_and_dependent_tx2() { -// let mut context = PoolContext::default(); - -// let (_, gas_coin) = context.setup_coin(); - -// let (output, unset_input) = context.create_output_and_input(10); -// let tx1 = TransactionBuilder::script(vec![], vec![]) -// .tip(10) -// .max_fee_limit(10) -// .script_gas_limit(GAS_LIMIT) -// .add_input(gas_coin.clone()) -// .add_output(output) -// .finalize_as_transaction(); - -// let input = unset_input.into_input(UtxoId::new(tx1.id(&Default::default()), 0)); - -// let tx2 = TransactionBuilder::script(vec![], vec![]) -// .tip(9) -// .max_fee_limit(9) -// .script_gas_limit(GAS_LIMIT) -// .add_input(input) -// .finalize_as_transaction(); - -// let tx3 = TransactionBuilder::script(vec![], vec![]) -// .tip(20) -// .max_fee_limit(20) -// .script_gas_limit(GAS_LIMIT) -// .add_input(gas_coin) -// .finalize_as_transaction(); - -// let tx1_id = tx1.id(&ChainId::default()); -// let tx2_id = tx2.id(&ChainId::default()); -// let mut txpool = context.build(); -// let tx1 = check_unwrap_tx(tx1, &txpool.config).await; -// let tx2 = check_unwrap_tx(tx2, &txpool.config).await; -// let tx3 = check_unwrap_tx(tx3, &txpool.config).await; - -// let results = txpool -// .insert(vec![ -// check_tx_to_pool(tx1.clone()), -// check_tx_to_pool(tx2.clone()), -// check_tx_to_pool(tx3.clone()), -// ]) -// .unwrap(); -// assert_eq!(results.len(), 3); -// assert!(results[0].is_ok()); -// assert!(results[1].is_ok()); -// let removed_transactions = results[2].as_ref().unwrap(); -// assert_eq!(removed_transactions.len(), 2); -// assert_eq!( -// removed_transactions[0].id(), -// tx1_id, -// "Tx1 id should be removed" -// ); -// assert_eq!( -// removed_transactions[1].id(), -// tx2_id, -// "Tx2 id should be removed" -// ); -// } - -// #[tokio::test] -// async fn more_priced_tx2_removes_tx1_and_more_priced_tx3_removes_tx2() { -// let mut context = PoolContext::default(); - -// let (_, gas_coin) = context.setup_coin(); - -// let tx1 = TransactionBuilder::script(vec![], vec![]) -// .tip(10) -// .max_fee_limit(10) -// .script_gas_limit(GAS_LIMIT) -// .add_input(gas_coin.clone()) -// .finalize_as_transaction(); - -// let tx2 = TransactionBuilder::script(vec![], vec![]) -// .tip(11) -// .max_fee_limit(11) -// .script_gas_limit(GAS_LIMIT) -// .add_input(gas_coin.clone()) -// .finalize_as_transaction(); - -// let tx3 = TransactionBuilder::script(vec![], vec![]) -// .tip(12) -// .max_fee_limit(12) -// .script_gas_limit(GAS_LIMIT) -// .add_input(gas_coin) -// .finalize_as_transaction(); - -// let mut txpool = context.build(); -// let tx1 = check_unwrap_tx(tx1, &txpool.config).await; -// let tx2 = check_unwrap_tx(tx2, &txpool.config).await; -// let tx3 = check_unwrap_tx(tx3, &txpool.config).await; - -// let results = txpool -// .insert(vec![ -// check_tx_to_pool(tx1.clone()), -// check_tx_to_pool(tx2.clone()), -// check_tx_to_pool(tx3.clone()), -// ]) -// .unwrap(); -// assert_eq!(results.len(), 3); -// assert!(results[0].is_ok()); -// let removed_transactions = results[1].as_ref().unwrap(); -// assert_eq!(removed_transactions.len(), 1); -// let removed_transactions = results[2].as_ref().unwrap(); -// assert_eq!(removed_transactions.len(), 1); -// } - -// #[tokio::test] -// async fn tx_limit_hit() { -// let mut context = PoolContext::default().config(Config { -// max_txs: 1, -// ..Default::default() -// }); - -// let (_, gas_coin) = context.setup_coin(); -// let tx1 = TransactionBuilder::script(vec![], vec![]) -// .script_gas_limit(GAS_LIMIT) -// .add_input(gas_coin) -// .add_output(create_coin_output()) -// .finalize_as_transaction(); - -// let (_, gas_coin) = context.setup_coin(); -// let tx2 = TransactionBuilder::script(vec![], vec![]) -// .script_gas_limit(GAS_LIMIT) -// .add_input(gas_coin) -// .finalize_as_transaction(); - -// let mut txpool = context.build(); -// let tx1 = check_unwrap_tx(tx1, &txpool.config).await; -// let tx2 = check_unwrap_tx(tx2, &txpool.config).await; -// let results = txpool -// .insert(vec![check_tx_to_pool(tx1), check_tx_to_pool(tx2)]) -// .unwrap(); -// assert_eq!(results.len(), 2); -// assert!(results[0].is_ok()); -// let err = results[1].as_ref().expect_err("Tx2 should be Err, got Ok"); -// assert!(matches!(err, Error::NotInsertedLimitHit)); -// } - -// #[tokio::test] -// async fn tx_chain_length_hit() { -// let mut context = PoolContext::default().config(Config { -// max_dependent_txn_count: 2, -// ..Default::default() -// }); - -// let (_, gas_coin) = context.setup_coin(); -// let (output, unset_input) = context.create_output_and_input(10_000); -// let tx1 = TransactionBuilder::script(vec![], vec![]) -// .script_gas_limit(GAS_LIMIT) -// .add_input(gas_coin) -// .add_output(output) -// .finalize_as_transaction(); - -// let input = unset_input.into_input(UtxoId::new(tx1.id(&Default::default()), 0)); -// let (output, unset_input) = context.create_output_and_input(5_000); -// let tx2 = TransactionBuilder::script(vec![], vec![]) -// .script_gas_limit(GAS_LIMIT) -// .add_input(input) -// .add_output(output) -// .finalize_as_transaction(); - -// let input = unset_input.into_input(UtxoId::new(tx2.id(&Default::default()), 0)); -// let tx3 = TransactionBuilder::script(vec![], vec![]) -// .script_gas_limit(GAS_LIMIT) -// .add_input(input) -// .finalize_as_transaction(); - -// let mut txpool = context.build(); -// let tx1 = check_unwrap_tx(tx1, &txpool.config).await; -// let tx2 = check_unwrap_tx(tx2, &txpool.config).await; -// let tx3 = check_unwrap_tx(tx3, &txpool.config).await; - -// let results = txpool -// .insert(vec![ -// check_tx_to_pool(tx1.clone()), -// check_tx_to_pool(tx2.clone()), -// check_tx_to_pool(tx3.clone()), -// ]) -// .unwrap(); -// assert_eq!(results.len(), 3); -// assert!(results[0].is_ok()); -// assert!(results[1].is_ok()); -// let err = results[2].as_ref().expect_err("Tx3 should be Err, got Ok"); -// assert!(matches!(err, Error::NotInsertedChainDependencyTooBig)); -// } - -// #[tokio::test] -// async fn sorted_out_tx1_2_3() { -// let mut context = PoolContext::default(); - -// let (_, gas_coin) = context.setup_coin(); -// let tx1 = TransactionBuilder::script(vec![], vec![]) -// .tip(10) -// .max_fee_limit(10) -// .script_gas_limit(GAS_LIMIT) -// .add_input(gas_coin) -// .finalize_as_transaction(); - -// let (_, gas_coin) = context.setup_coin(); -// let tx2 = TransactionBuilder::script(vec![], vec![]) -// .tip(9) -// .max_fee_limit(9) -// .script_gas_limit(GAS_LIMIT) -// .add_input(gas_coin) -// .finalize_as_transaction(); - -// let (_, gas_coin) = context.setup_coin(); -// let tx3 = TransactionBuilder::script(vec![], vec![]) -// .tip(20) -// .max_fee_limit(20) -// .script_gas_limit(GAS_LIMIT) -// .add_input(gas_coin) -// .finalize_as_transaction(); - -// let tx1_id = tx1.id(&ChainId::default()); -// let tx2_id = tx2.id(&ChainId::default()); -// let tx3_id = tx3.id(&ChainId::default()); - -// let mut txpool = context.build(); -// let tx1 = check_unwrap_tx(tx1, &txpool.config).await; -// let tx2 = check_unwrap_tx(tx2, &txpool.config).await; -// let tx3 = check_unwrap_tx(tx3, &txpool.config).await; - -// let results = txpool -// .insert(vec![ -// check_tx_to_pool(tx1.clone()), -// check_tx_to_pool(tx2.clone()), -// check_tx_to_pool(tx3.clone()), -// ]) -// .unwrap(); -// assert_eq!(results.len(), 3); -// assert!(results[0].is_ok()); -// assert!(results[1].is_ok()); -// assert!(results[2].is_ok()); - -// let txs = txpool.extract_transactions_for_block().unwrap(); - -// assert_eq!(txs.len(), 3, "Should have 3 txs"); -// assert_eq!(txs[0].id(), tx3_id, "First should be tx3"); -// assert_eq!(txs[1].id(), tx1_id, "Second should be tx1"); -// assert_eq!(txs[2].id(), tx2_id, "Third should be tx2"); -// } - -// #[tokio::test] -// async fn sorted_out_tx_same_tips() { -// let mut context = PoolContext::default(); - -// let (_, gas_coin) = context.setup_coin(); -// let tx1 = TransactionBuilder::script(vec![], vec![]) -// .tip(10) -// .max_fee_limit(10) -// .script_gas_limit(GAS_LIMIT) -// .add_input(gas_coin) -// .finalize_as_transaction(); - -// let (_, gas_coin) = context.setup_coin(); -// let tx2 = TransactionBuilder::script(vec![], vec![]) -// .tip(10) -// .max_fee_limit(10) -// .script_gas_limit(GAS_LIMIT / 2) -// .add_input(gas_coin) -// .finalize_as_transaction(); - -// let (_, gas_coin) = context.setup_coin(); -// let tx3 = TransactionBuilder::script(vec![], vec![]) -// .tip(10) -// .max_fee_limit(10) -// .script_gas_limit(GAS_LIMIT / 4) -// .add_input(gas_coin) -// .finalize_as_transaction(); - -// let tx1_id = tx1.id(&ChainId::default()); -// let tx2_id = tx2.id(&ChainId::default()); -// let tx3_id = tx3.id(&ChainId::default()); - -// let mut txpool = context.build(); -// let tx1 = check_unwrap_tx(tx1, &txpool.config).await; -// let tx2 = check_unwrap_tx(tx2, &txpool.config).await; -// let tx3 = check_unwrap_tx(tx3, &txpool.config).await; - -// let results = txpool -// .insert(vec![ -// check_tx_to_pool(tx1.clone()), -// check_tx_to_pool(tx2.clone()), -// check_tx_to_pool(tx3.clone()), -// ]) -// .unwrap(); -// assert_eq!(results.len(), 3); -// assert!(results[0].is_ok()); -// assert!(results[1].is_ok()); -// assert!(results[2].is_ok()); - -// let txs = txpool.extract_transactions_for_block().unwrap(); -// assert_eq!(txs.len(), 3, "Should have 3 txs"); - -// assert_eq!(txs[0].id(), tx3_id, "First should be tx3"); -// assert_eq!(txs[1].id(), tx2_id, "Second should be tx2"); -// assert_eq!(txs[2].id(), tx1_id, "Third should be tx1"); -// } - -// #[tokio::test] -// async fn sorted_out_tx_profitable_ratios() { -// let mut context = PoolContext::default(); - -// let (_, gas_coin) = context.setup_coin(); -// let tx1 = TransactionBuilder::script(vec![], vec![]) -// .tip(4) -// .max_fee_limit(4) -// .script_gas_limit(GAS_LIMIT) -// .add_input(gas_coin) -// .finalize_as_transaction(); - -// let (_, gas_coin) = context.setup_coin(); -// let tx2 = TransactionBuilder::script(vec![], vec![]) -// .tip(2) -// .max_fee_limit(2) -// .script_gas_limit(GAS_LIMIT / 10) -// .add_input(gas_coin) -// .finalize_as_transaction(); - -// let (_, gas_coin) = context.setup_coin(); -// let tx3 = TransactionBuilder::script(vec![], vec![]) -// .tip(1) -// .max_fee_limit(1) -// .script_gas_limit(GAS_LIMIT / 100) -// .add_input(gas_coin) -// .finalize_as_transaction(); - -// let tx1_id = tx1.id(&ChainId::default()); -// let tx2_id = tx2.id(&ChainId::default()); -// let tx3_id = tx3.id(&ChainId::default()); - -// let mut txpool = context.build(); -// let tx1 = check_unwrap_tx(tx1, &txpool.config).await; -// let tx2 = check_unwrap_tx(tx2, &txpool.config).await; -// let tx3 = check_unwrap_tx(tx3, &txpool.config).await; - -// let results = txpool -// .insert(vec![ -// check_tx_to_pool(tx1.clone()), -// check_tx_to_pool(tx2.clone()), -// check_tx_to_pool(tx3.clone()), -// ]) -// .unwrap(); -// assert_eq!(results.len(), 3); -// assert!(results[0].is_ok()); -// assert!(results[1].is_ok()); -// assert!(results[2].is_ok()); - -// let txs = txpool.extract_transactions_for_block().unwrap(); -// assert_eq!(txs.len(), 3, "Should have 3 txs"); - -// assert_eq!(txs[0].id(), tx3_id, "First should be tx3"); -// assert_eq!(txs[1].id(), tx2_id, "Second should be tx2"); -// assert_eq!(txs[2].id(), tx1_id, "Third should be tx1"); -// } - -// #[tokio::test] -// async fn sorted_out_tx_by_creation_instant() { -// let mut context = PoolContext::default(); - -// let (_, gas_coin) = context.setup_coin(); -// let tx1 = TransactionBuilder::script(vec![], vec![]) -// .tip(4) -// .max_fee_limit(4) -// .script_gas_limit(GAS_LIMIT) -// .add_input(gas_coin) -// .finalize_as_transaction(); - -// let (_, gas_coin) = context.setup_coin(); -// let tx2 = TransactionBuilder::script(vec![], vec![]) -// .tip(4) -// .max_fee_limit(4) -// .script_gas_limit(GAS_LIMIT) -// .add_input(gas_coin) -// .finalize_as_transaction(); - -// let (_, gas_coin) = context.setup_coin(); -// let tx3 = TransactionBuilder::script(vec![], vec![]) -// .tip(4) -// .max_fee_limit(4) -// .script_gas_limit(GAS_LIMIT) -// .add_input(gas_coin) -// .finalize_as_transaction(); - -// let (_, gas_coin) = context.setup_coin(); -// let tx4 = TransactionBuilder::script(vec![], vec![]) -// .tip(4) -// .max_fee_limit(4) -// .script_gas_limit(GAS_LIMIT) -// .add_input(gas_coin) -// .finalize_as_transaction(); - -// let tx1_id = tx1.id(&ChainId::default()); -// let tx2_id = tx2.id(&ChainId::default()); -// let tx3_id = tx3.id(&ChainId::default()); -// let tx4_id = tx4.id(&ChainId::default()); - -// let mut txpool = context.build(); -// let tx1 = check_unwrap_tx(tx1, &txpool.config).await; -// let tx2 = check_unwrap_tx(tx2, &txpool.config).await; -// let tx3 = check_unwrap_tx(tx3, &txpool.config).await; -// let tx4 = check_unwrap_tx(tx4, &txpool.config).await; - -// let results = txpool -// .insert(vec![ -// check_tx_to_pool(tx1.clone()), -// check_tx_to_pool(tx2.clone()), -// check_tx_to_pool(tx3.clone()), -// check_tx_to_pool(tx4.clone()), -// ]) -// .unwrap(); -// assert_eq!(results.len(), 4); -// assert!(results[0].is_ok()); -// assert!(results[1].is_ok()); -// assert!(results[2].is_ok()); -// assert!(results[3].is_ok()); - -// let txs = txpool.extract_transactions_for_block().unwrap(); - -// // This order doesn't match the lexicographical order of the tx ids -// // and so it verifies that the txs are sorted by creation instant -// // The newest tx should be first -// assert_eq!(txs.len(), 4, "Should have 4 txs"); -// assert_eq!(txs[0].id(), tx1_id, "First should be tx1"); -// assert_eq!(txs[1].id(), tx2_id, "Second should be tx2"); -// assert_eq!(txs[2].id(), tx3_id, "Third should be tx3"); -// assert_eq!(txs[3].id(), tx4_id, "Fourth should be tx4"); -// } - -// #[tokio::test] -// async fn tx_at_least_min_gas_price_is_insertable() { -// let gas_price = 10; -// let mut context = PoolContext::default().config(Config { -// ..Default::default() -// }); - -// let (_, gas_coin) = context.setup_coin(); -// let tx = TransactionBuilder::script(vec![], vec![]) -// .tip(10) -// .max_fee_limit(1000) -// .script_gas_limit(GAS_LIMIT) -// .add_input(gas_coin) -// .finalize_as_transaction(); - -// let mut txpool = context.build(); -// let tx = check_unwrap_tx_with_gas_price(tx, &txpool.config, gas_price).await; - -// let results = txpool.insert(vec![check_tx_to_pool(tx)]).unwrap(); -// assert_eq!(results.len(), 1); -// assert!(results[0].is_ok()); -// } - -// #[tokio::test] -// async fn tx_below_min_gas_price_is_not_insertable() { -// let mut context = PoolContext::default(); - -// let gas_coin = context.random_predicate(AssetId::BASE, TEST_COIN_AMOUNT, None); -// let tx = TransactionBuilder::script(vec![], vec![]) -// .tip(10) -// .max_fee_limit(10) -// .script_gas_limit(GAS_LIMIT) -// .add_input(gas_coin) -// .finalize_as_transaction(); -// let gas_price = 11; - -// let err = check_tx_with_gas_price( -// tx, -// &Config { -// ..Default::default() -// }, -// gas_price, -// ) -// .await -// .expect_err("expected insertion failure"); - -// assert!(matches!( -// err, -// Error::ConsensusValidity(CheckError::InsufficientMaxFee { .. }) -// )); -// } - -// #[tokio::test] -// async fn tx_inserted_into_pool_when_input_message_id_exists_in_db() { -// let mut context = PoolContext::default(); -// let (message, input) = create_message_predicate_from_message(5000, 0); - -// let tx = TransactionBuilder::script(vec![], vec![]) -// .script_gas_limit(GAS_LIMIT) -// .add_input(input) -// .finalize_as_transaction(); - -// context.database_mut().insert_message(message); - -// let tx1_id = tx.id(&ChainId::default()); -// let mut txpool = context.build(); - -// let tx = check_unwrap_tx(tx, &txpool.config).await; - -// let results = txpool.insert(vec![check_tx_to_pool(tx)]).unwrap(); -// assert_eq!(results.len(), 1); -// assert!(results[0].is_ok()); - -// let tx = txpool.find_one(&tx1_id).unwrap(); -// assert_eq!(tx.id(), tx1_id); -// } - -// #[tokio::test] -// async fn tx_rejected_from_pool_when_input_message_id_does_not_exist_in_db() { -// let context = PoolContext::default(); -// let (message, input) = create_message_predicate_from_message(5000, 0); -// let tx = TransactionBuilder::script(vec![], vec![]) -// .script_gas_limit(GAS_LIMIT) -// .add_input(input) -// .finalize_as_transaction(); - -// // Do not insert any messages into the DB to ensure there is no matching message for the -// // tx. -// let mut txpool = context.build(); -// let tx = check_unwrap_tx(tx, &txpool.config).await; - -// let results = txpool.insert(vec![check_tx_to_pool(tx)]).unwrap(); -// assert_eq!(results.len(), 1); - -// let err = results[0].as_ref().expect_err("Tx should be Err, got Ok"); -// // check error -// assert!(matches!( -// err, -// Error::NotInsertedInputMessageUnknown(msg_id) if msg_id == message.id() -// )); -// } - -// #[tokio::test] -// async fn tx_rejected_from_pool_when_gas_price_is_lower_than_another_tx_with_same_message_id( -// ) { -// let mut context = PoolContext::default(); -// let message_amount = 10_000; -// let max_fee_limit = 10u64; -// let gas_price_high = 2u64; -// let gas_price_low = 1u64; -// let (message, conflicting_message_input) = -// create_message_predicate_from_message(message_amount, 0); - -// let tx_high = TransactionBuilder::script(vec![], vec![]) -// .tip(gas_price_high) -// .max_fee_limit(max_fee_limit) -// .script_gas_limit(GAS_LIMIT) -// .add_input(conflicting_message_input.clone()) -// .finalize_as_transaction(); - -// let tx_low = TransactionBuilder::script(vec![], vec![]) -// .tip(gas_price_low) -// .max_fee_limit(max_fee_limit) -// .script_gas_limit(GAS_LIMIT) -// .add_input(conflicting_message_input) -// .finalize_as_transaction(); - -// context.database_mut().insert_message(message.clone()); - -// let mut txpool = context.build(); - -// let _tx_high_id = tx_high.id(&ChainId::default()); -// let tx_high = -// check_unwrap_tx_with_gas_price(tx_high, &txpool.config, gas_price_high).await; - -// // Insert a tx for the message id with a high gas amount -// let results = txpool.insert(vec![check_tx_to_pool(tx_high)]).unwrap(); -// assert_eq!(results.len(), 1); -// assert!(results[0].is_ok()); - -// let tx_low = -// check_unwrap_tx_with_gas_price(tx_low, &txpool.config, gas_price_low).await; -// // Insert a tx for the message id with a low gas amount -// // Because the new transaction's id matches an existing transaction, we compare the gas -// // prices of both the new and existing transactions. Since the existing transaction's gas -// // price is higher, we must now reject the new transaction. -// let results = txpool.insert(vec![check_tx_to_pool(tx_low)]).unwrap(); -// assert_eq!(results.len(), 1); -// let err = results[0].as_ref().expect_err("Tx should be Err, got Ok"); - -// // check error -// assert!(matches!(err, Error::Collided(_))); -// } - -// #[tokio::test] -// async fn higher_priced_tx_squeezes_out_lower_priced_tx_with_same_message_id() { -// let mut context = PoolContext::default(); -// let message_amount = 10_000; -// let gas_price_high = 2u64; -// let max_fee_limit = 10u64; -// let gas_price_low = 1u64; -// let (message, conflicting_message_input) = -// create_message_predicate_from_message(message_amount, 0); - -// // Insert a tx for the message id with a low gas amount -// let tx_low = TransactionBuilder::script(vec![], vec![]) -// .tip(gas_price_low) -// .max_fee_limit(max_fee_limit) -// .script_gas_limit(GAS_LIMIT) -// .add_input(conflicting_message_input.clone()) -// .finalize_as_transaction(); - -// context.database_mut().insert_message(message); - -// let mut txpool = context.build(); -// let tx_low_id = tx_low.id(&ChainId::default()); -// let tx_low = -// check_unwrap_tx_with_gas_price(tx_low, &txpool.config, gas_price_low).await; -// let results = txpool.insert(vec![check_tx_to_pool(tx_low)]).unwrap(); -// assert_eq!(results.len(), 1); -// assert!(results[0].is_ok()); - -// // Insert a tx for the message id with a high gas amount -// // Because the new transaction's id matches an existing transaction, we compare the gas -// // prices of both the new and existing transactions. Since the existing transaction's gas -// // price is lower, we accept the new transaction and squeeze out the old transaction. -// let tx_high = TransactionBuilder::script(vec![], vec![]) -// .tip(gas_price_high) -// .max_fee_limit(max_fee_limit) -// .script_gas_limit(GAS_LIMIT) -// .add_input(conflicting_message_input) -// .finalize_as_transaction(); -// let tx_high = -// check_unwrap_tx_with_gas_price(tx_high, &txpool.config, gas_price_high).await; -// let results = txpool.insert(vec![check_tx_to_pool(tx_high)]).unwrap(); -// assert_eq!(results.len(), 1); -// assert!(results[0].is_ok()); -// let squeezed_out_txs = results[0].as_ref().unwrap(); - -// assert_eq!(squeezed_out_txs.len(), 1); -// assert_eq!(squeezed_out_txs[0].id(), tx_low_id,); -// } - -// #[tokio::test] -// async fn message_of_squeezed_out_tx_can_be_resubmitted_at_lower_gas_price() { -// // tx1 (message 1, message 2) gas_price 2 -// // tx2 (message 1) gas_price 3 -// // squeezes tx1 with higher gas price -// // tx3 (message 2) gas_price 1 -// // works since tx1 is no longer part of txpool state even though gas price is less - -// let mut context = PoolContext::default(); -// let (message_1, message_input_1) = create_message_predicate_from_message(10_000, 0); -// let (message_2, message_input_2) = create_message_predicate_from_message(20_000, 1); - -// // Insert a tx for the message id with a low gas amount -// let tx1 = TransactionBuilder::script(vec![], vec![]) -// .tip(2) -// .max_fee_limit(2) -// .script_gas_limit(GAS_LIMIT) -// .add_input(message_input_1.clone()) -// .add_input(message_input_2.clone()) -// .finalize_as_transaction(); - -// let tx2 = TransactionBuilder::script(vec![], vec![]) -// .tip(3) -// .max_fee_limit(3) -// .script_gas_limit(GAS_LIMIT) -// .add_input(message_input_1) -// .finalize_as_transaction(); - -// let tx3 = TransactionBuilder::script(vec![], vec![]) -// .tip(1) -// .max_fee_limit(1) -// .script_gas_limit(GAS_LIMIT) -// .add_input(message_input_2) -// .finalize_as_transaction(); - -// context.database_mut().insert_message(message_1); -// context.database_mut().insert_message(message_2); -// let mut txpool = context.build(); - -// let tx1 = check_unwrap_tx(tx1, &txpool.config).await; -// let tx2 = check_unwrap_tx(tx2, &txpool.config).await; -// let tx3 = check_unwrap_tx(tx3, &txpool.config).await; - -// let results = txpool -// .insert(vec![ -// check_tx_to_pool(tx1.clone()), -// check_tx_to_pool(tx2.clone()), -// check_tx_to_pool(tx3.clone()), -// ]) -// .unwrap(); -// assert_eq!(results.len(), 3); -// assert!(results[0].is_ok()); -// assert!(results[1].is_ok()); -// assert!(results[2].is_ok()); -// } - -// #[tokio::test] -// async fn predicates_with_incorrect_owner_fails() { -// let mut context = PoolContext::default(); -// let mut coin = context.random_predicate(AssetId::BASE, TEST_COIN_AMOUNT, None); -// if let Input::CoinPredicate(CoinPredicate { owner, .. }) = &mut coin { -// *owner = Address::zeroed(); -// } - -// let tx = TransactionBuilder::script(vec![], vec![]) -// .script_gas_limit(GAS_LIMIT) -// .add_input(coin) -// .finalize_as_transaction(); - -// let err = check_tx(tx, &Default::default()) -// .await -// .expect_err("Transaction should be err, got ok"); - -// assert!( -// format!("{err:?}").contains("InputPredicateOwner"), -// "unexpected error: {err:?}", -// ) -// } - -// #[tokio::test] -// async fn predicate_without_enough_gas_returns_out_of_gas() { -// let mut context = PoolContext::default(); - -// let gas_limit = 10000; - -// let mut consensus_parameters = ConsensusParameters::default(); -// consensus_parameters -// .set_tx_params(TxParameters::default().with_max_gas_per_tx(gas_limit)); -// consensus_parameters.set_predicate_params( -// PredicateParameters::default().with_max_gas_per_predicate(gas_limit), -// ); - -// let coin = context -// .custom_predicate( -// AssetId::BASE, -// TEST_COIN_AMOUNT, -// // forever loop -// vec![op::jmp(RegId::ZERO)].into_iter().collect(), -// None, -// ) -// .into_estimated(&consensus_parameters); - -// let tx = TransactionBuilder::script(vec![], vec![]) -// .script_gas_limit(GAS_LIMIT) -// .add_input(coin) -// .finalize_as_transaction(); - -// let err = check_tx(tx, &Default::default()) -// .await -// .expect_err("Transaction should be err, got ok"); - -// assert!( -// err.to_string() -// .contains("PredicateVerificationFailed(OutOfGas)"), -// "unexpected error: {err}", -// ) -// } - -// #[tokio::test] -// async fn predicate_that_returns_false_is_invalid() { -// let mut context = PoolContext::default(); -// let coin = context -// .custom_predicate( -// AssetId::BASE, -// TEST_COIN_AMOUNT, -// // forever loop -// vec![op::ret(RegId::ZERO)].into_iter().collect(), -// None, -// ) -// .into_default_estimated(); - -// let tx = TransactionBuilder::script(vec![], vec![]) -// .script_gas_limit(GAS_LIMIT) -// .add_input(coin) -// .finalize_as_transaction(); - -// let err = check_tx(tx, &Default::default()) -// .await -// .expect_err("Transaction should be err, got ok"); - -// assert!( -// err.to_string().contains("PredicateVerificationFailed"), -// "unexpected error: {err}", -// ) -// } - -// #[tokio::test] -// async fn insert_single__blob_tx_works() { -// let program = vec![123; 123]; -// let tx = TransactionBuilder::blob(BlobBody { -// id: BlobId::compute(program.as_slice()), -// witness_index: 0, -// }) -// .add_witness(program.into()) -// .add_random_fee_input() -// .finalize_as_transaction(); - -// let config = Config { -// utxo_validation: false, -// ..Default::default() -// }; -// let context = PoolContext::default().config(config); -// let mut txpool = context.build(); - -// // Given -// let tx = check_unwrap_tx(tx, &txpool.config).await; -// let id = tx.id(); - -// // When -// let results = txpool.insert(vec![check_tx_to_pool(tx)]).unwrap(); -// assert_eq!(results.len(), 1); -// assert!(results[0].is_ok()); - -// assert!(txpool.find_one(&id).is_some(), "Should find tx in pool"); -// } - -// #[tokio::test] -// async fn insert_single__blob_tx_fails_if_blob_already_inserted_and_lower_tip() { -// let program = vec![123; 123]; -// let blob_id = BlobId::compute(program.as_slice()); -// let tx = TransactionBuilder::blob(BlobBody { -// id: blob_id, -// witness_index: 0, -// }) -// .add_witness(program.clone().into()) -// .add_random_fee_input() -// .finalize_as_transaction(); - -// let config = Config { -// utxo_validation: false, -// ..Default::default() -// }; -// let context = PoolContext::default().config(config); -// let mut txpool = context.build(); -// let tx = check_unwrap_tx(tx, &txpool.config).await; - -// // Given -// let results = txpool.insert(vec![check_tx_to_pool(tx.clone())]).unwrap(); -// assert_eq!(results.len(), 1); -// assert!(results[0].is_ok()); - -// let same_blob_tx = TransactionBuilder::blob(BlobBody { -// id: blob_id, -// witness_index: 1, -// }) -// .add_random_fee_input() -// .add_witness(program.into()) -// .finalize_as_transaction(); -// let same_blob_tx = check_unwrap_tx(same_blob_tx, &txpool.config).await; - -// // When -// let results = txpool -// .insert(vec![check_tx_to_pool(same_blob_tx.clone())]) -// .unwrap(); -// assert_eq!(results.len(), 1); -// let err = results[0].as_ref().expect_err("Tx should be Err, got Ok"); - -// // Then -// assert!(matches!(err, Error::Collided(_))); -// } - -// #[tokio::test] -// async fn insert_single__blob_tx_succeeds_if_blob_already_inserted_but_higher_tip() { -// let program = vec![123; 123]; -// let blob_id = BlobId::compute(program.as_slice()); -// let tx = TransactionBuilder::blob(BlobBody { -// id: blob_id, -// witness_index: 0, -// }) -// .add_witness(program.clone().into()) -// .add_random_fee_input() -// .finalize_as_transaction(); - -// let config = Config { -// utxo_validation: false, -// ..Default::default() -// }; -// let context = PoolContext::default().config(config); -// let mut txpool = context.build(); -// let tx = check_unwrap_tx(tx, &txpool.config).await; - -// // Given -// let results = txpool.insert(vec![check_tx_to_pool(tx.clone())]).unwrap(); -// assert_eq!(results.len(), 1); -// assert!(results[0].is_ok()); - -// let same_blob_tx = TransactionBuilder::blob(BlobBody { -// id: blob_id, -// witness_index: 1, -// }) -// .add_random_fee_input() -// .add_witness(program.into()) -// .tip(100) -// .max_fee_limit(100) -// .finalize_as_transaction(); -// let same_blob_tx = check_unwrap_tx(same_blob_tx, &txpool.config).await; - -// // When -// let results = txpool -// .insert(vec![check_tx_to_pool(same_blob_tx.clone())]) -// .unwrap(); -// assert_eq!(results.len(), 1); - -// // Then -// assert!(results[0].is_ok()); -// } - -// #[tokio::test] -// async fn insert_single__blob_tx_fails_if_blob_already_exists_in_database() { -// let program = vec![123; 123]; -// let blob_id = BlobId::compute(program.as_slice()); -// let tx = TransactionBuilder::blob(BlobBody { -// id: blob_id, -// witness_index: 0, -// }) -// .add_witness(program.clone().into()) -// .add_random_fee_input() -// .finalize_as_transaction(); - -// let config = Config { -// utxo_validation: false, -// ..Default::default() -// }; -// let mut context = PoolContext::default().config(config); -// // Given -// context.database_mut().insert_dummy_blob(blob_id); -// let mut txpool = context.build(); -// let tx = check_unwrap_tx(tx, &txpool.config).await; - -// // When -// let result = txpool.insert(vec![check_tx_to_pool(tx.clone())]).unwrap(); -// assert_eq!(result.len(), 1); -// let err = result[0].as_ref().expect_err("Tx should be Err, got Ok"); - -// // Then -// assert!(matches!( -// err, -// Error::NotInsertedBlobIdAlreadyTaken(b) if *b == blob_id -// )); -// } - -// #[tokio::test] -// async fn insert__if_tx3_depends_and_collides_witg_tx2() { -// let mut context = PoolContext::default(); - -// // tx1 {inputs: {}, outputs: {coinA}, tip: 1} -// let (_, gas_coin) = context.setup_coin(); -// let (output_a, unset_input) = context.create_output_and_input(1); -// let tx1 = TransactionBuilder::script(vec![], vec![]) -// .tip(1) -// .max_fee_limit(1) -// .script_gas_limit(GAS_LIMIT) -// .add_input(gas_coin) -// .add_output(output_a) -// .finalize_as_transaction(); - -// // tx2 {inputs: {coinA}, outputs: {coinB}, tip: 1} -// let (_, gas_coin) = context.setup_coin(); -// let input_a = unset_input.into_input(UtxoId::new(tx1.id(&Default::default()), 0)); -// let (output_b, unset_input) = context.create_output_and_input(1); -// let tx2 = TransactionBuilder::script(vec![], vec![]) -// .tip(1) -// .max_fee_limit(1) -// .script_gas_limit(GAS_LIMIT) -// .add_input(input_a.clone()) -// .add_input(gas_coin) -// .add_output(output_b) -// .finalize_as_transaction(); - -// // Given -// // tx3 {inputs: {coinA, coinB}, outputs:{}, tip: 20} -// let (_, gas_coin) = context.setup_coin(); -// let input_b = unset_input.into_input(UtxoId::new(tx2.id(&Default::default()), 0)); -// let tx3 = TransactionBuilder::script(vec![], vec![]) -// .tip(20) -// .max_fee_limit(20) -// .script_gas_limit(GAS_LIMIT) -// .add_input(input_a) -// .add_input(input_b.clone()) -// .add_input(gas_coin) -// .finalize_as_transaction(); - -// let (_, gas_coin) = context.setup_coin(); - -// let mut txpool = context.build(); -// let tx1 = check_unwrap_tx(tx1, &txpool.config).await; -// let tx2 = check_unwrap_tx(tx2, &txpool.config).await; -// let tx3 = check_unwrap_tx(tx3, &txpool.config).await; - -// // When -// let results = txpool -// .insert(vec![ -// check_tx_to_pool(tx1.clone()), -// check_tx_to_pool(tx2.clone()), -// check_tx_to_pool(tx3.clone()), -// ]) -// .unwrap(); -// assert_eq!(results.len(), 3); -// assert!(results[0].is_ok()); -// assert!(results[1].is_ok()); - -// // Then -// let err = results[2].as_ref().expect_err("Tx3 should be Err, got Ok"); -// assert!(matches!(err, Error::Storage(_))); -// } - -// // TODO: Reinstantiatte when https://github.com/FuelLabs/fuel-core/issues/2186 is implemented -// // #[tokio::test] -// // async fn insert_inner__rejects_upgrade_tx_with_invalid_wasm() { -// // let predicate = vec![op::ret(1)].into_iter().collect::>(); -// // let privileged_address = Input::predicate_owner(predicate.clone()); - -// // let config = Config { -// // utxo_validation: false, -// // ..Default::default() -// // }; -// // let context = PoolContext::default() -// // .config(config) -// // .wasm_checker(MockWasmChecker { -// // result: Err(WasmValidityError::NotValid), -// // }); -// // let mut txpool = context.build(); -// // let gas_price_provider = MockTxPoolGasPrice::new(0); - -// // // Given -// // let tx = TransactionBuilder::upgrade(UpgradePurpose::StateTransition { -// // root: Bytes32::new([1; 32]), -// // }) -// // .add_input(Input::coin_predicate( -// // UtxoId::new(Bytes32::new([1; 32]), 0), -// // privileged_address, -// // 1_000_000_000, -// // AssetId::BASE, -// // Default::default(), -// // Default::default(), -// // predicate, -// // vec![], -// // )) -// // .finalize_as_transaction(); -// // let mut params = ConsensusParameters::default(); -// // params.set_privileged_address(privileged_address); -// // let tx = check_single_tx( -// // tx, -// // Default::default(), -// // false, -// // ¶ms, -// // &gas_price_provider, -// // MemoryInstance::new(), -// // ) -// // .await -// // .expect("Transaction should be checked"); - -// // // When -// // let result = txpool.insert_single(tx); - -// // // Then -// // assert_eq!(result, Err(Error::NotInsertedInvalidWasm)); -// // } +#[tokio::test] +async fn insert__colliding_dependent_underpriced() { + let mut universe = TestPoolUniverse::default(); + universe.build_pool(); + + let (output, unset_input) = universe.create_output_and_input(20); + let tx1 = universe.build_script_transaction(None, Some(vec![output]), 20); + let input = unset_input.into_input(UtxoId::new(tx1.id(&ChainId::default()), 0)); + + // Given + let tx2 = universe.build_script_transaction(Some(vec![input.clone()]), None, 20); + let tx3 = universe.build_script_transaction(Some(vec![input]), None, 10); + + // When + let result1 = universe.verify_and_insert(tx1).await; + let result2 = universe.verify_and_insert(tx2).await; + let result3 = universe.verify_and_insert(tx3).await; + + // Then + assert!(result1.is_ok()); + assert!(result2.is_ok()); + let err = result3.unwrap_err(); + assert!(matches!(err, Error::Collided(_))); +} + +#[tokio::test] +async fn insert_dependent_contract_creation() { + let mut universe = TestPoolUniverse::default(); + universe.build_pool(); + let contract_id = Contract::EMPTY_CONTRACT_ID; + + // Given + let (_, gas_funds) = universe.setup_coin(); + let tx1 = TransactionBuilder::create( + Default::default(), + Default::default(), + Default::default(), + ) + .tip(10) + .max_fee_limit(10) + .add_input(gas_funds) + .add_output(create_contract_output(contract_id)) + .finalize_as_transaction(); + + let tx2 = universe.build_script_transaction( + Some(vec![create_contract_input( + Default::default(), + Default::default(), + contract_id, + )]), + Some(vec![Output::contract( + 0, + Default::default(), + Default::default(), + )]), + 10, + ); + + // When + let result1 = universe.verify_and_insert(tx1).await; + let result2 = universe.verify_and_insert(tx2).await; + + // Then + assert!(result1.is_ok()); + assert!(result2.is_ok()); +} + +#[tokio::test] +async fn insert_more_priced_tx3_removes_tx1_and_dependent_tx2() { + let mut universe = TestPoolUniverse::default(); + universe.build_pool(); + + // Given + let common_coin = universe.setup_coin().1; + let (output, unset_input) = universe.create_output_and_input(10); + + let tx1 = universe.build_script_transaction( + Some(vec![common_coin.clone()]), + Some(vec![output]), + 10, + ); + let tx1_id = tx1.id(&ChainId::default()); + let input = unset_input.into_input(UtxoId::new(tx1_id, 0)); + + let tx2 = universe.build_script_transaction(Some(vec![input.clone()]), None, 10); + let tx2_id = tx2.id(&ChainId::default()); + + let tx3 = universe.build_script_transaction(Some(vec![common_coin]), None, 20); + + // When + let result1 = universe.verify_and_insert(tx1).await; + let result2 = universe.verify_and_insert(tx2).await; + let result3 = universe.verify_and_insert(tx3).await; + + // Then + assert!(result1.is_ok()); + assert!(result2.is_ok()); + assert!(result3.is_ok()); + let removed_txs = result3.unwrap(); + assert_eq!(removed_txs.len(), 2); + assert_eq!(removed_txs[0].id(), tx1_id); + assert_eq!(removed_txs[1].id(), tx2_id); +} + +#[tokio::test] +async fn insert_more_priced_tx2_removes_tx1_and_more_priced_tx3_removes_tx2() { + let mut universe = TestPoolUniverse::default(); + universe.build_pool(); + + // Given + let common_coin = universe.setup_coin().1; + + let tx1 = + universe.build_script_transaction(Some(vec![common_coin.clone()]), None, 10); + let tx1_id = tx1.id(&ChainId::default()); + + let tx2 = + universe.build_script_transaction(Some(vec![common_coin.clone()]), None, 11); + let tx2_id = tx2.id(&ChainId::default()); + + let tx3 = universe.build_script_transaction(Some(vec![common_coin]), None, 12); + + // When + let result1 = universe.verify_and_insert(tx1).await; + let result2 = universe.verify_and_insert(tx2).await; + let result3 = universe.verify_and_insert(tx3).await; + + // Then + assert!(result1.is_ok()); + assert!(result2.is_ok()); + let removed_txs = result2.unwrap(); + assert_eq!(removed_txs.len(), 1); + assert_eq!(removed_txs[0].id(), tx1_id); + assert!(result3.is_ok()); + let removed_txs = result3.unwrap(); + assert_eq!(removed_txs.len(), 1); + assert_eq!(removed_txs[0].id(), tx2_id); +} + +#[tokio::test] +async fn insert__tx_limit_hit() { + let mut universe = TestPoolUniverse::default().config(Config { + max_txs: 1, + ..Default::default() + }); + universe.build_pool(); + + // Given + let tx1 = universe.build_script_transaction(None, None, 0); + let tx2 = universe.build_script_transaction(None, None, 0); + + // When + let result1 = universe.verify_and_insert(tx1).await; + let result2 = universe.verify_and_insert(tx2).await; + + // Then + assert!(result1.is_ok()); + let err = result2.unwrap_err(); + assert!(matches!(err, Error::NotInsertedLimitHit)); +} + +#[tokio::test] +async fn insert__dependency_chain_length_hit() { + let mut universe = TestPoolUniverse::default().config(Config { + max_dependent_txn_count: 2, + ..Default::default() + }); + universe.build_pool(); + + // Given + let (output, unset_input) = universe.create_output_and_input(10_000); + let tx1 = universe.build_script_transaction(None, Some(vec![output]), 0); + let input = unset_input.into_input(UtxoId::new(tx1.id(&Default::default()), 0)); + + let (output, unset_input) = universe.create_output_and_input(5_000); + let tx2 = universe.build_script_transaction(Some(vec![input]), Some(vec![output]), 0); + let input = unset_input.into_input(UtxoId::new(tx2.id(&Default::default()), 0)); + + let tx3 = universe.build_script_transaction(Some(vec![input]), None, 0); + + // When + let result1 = universe.verify_and_insert(tx1).await; + let result2 = universe.verify_and_insert(tx2).await; + let result3 = universe.verify_and_insert(tx3).await; + + // Then + assert!(result1.is_ok()); + assert!(result2.is_ok()); + let err = result3.unwrap_err(); + assert!(matches!(err, Error::NotInsertedChainDependencyTooBig)); +} + +#[tokio::test] +async fn get_sorted_out_tx1_2_3() { + let mut universe = TestPoolUniverse::default(); + universe.build_pool(); + + // Given + let tx1 = universe.build_script_transaction(None, None, 10); + let tx2 = universe.build_script_transaction(None, None, 9); + let tx3 = universe.build_script_transaction(None, None, 20); + + let tx1_id = tx1.id(&ChainId::default()); + let tx2_id = tx2.id(&ChainId::default()); + let tx3_id = tx3.id(&ChainId::default()); + + universe.verify_and_insert(tx1).await.unwrap(); + universe.verify_and_insert(tx2).await.unwrap(); + universe.verify_and_insert(tx3).await.unwrap(); + + // When + let txs = universe + .get_pool() + .write() + .extract_transactions_for_block() + .unwrap(); + + // Then + assert_eq!(txs.len(), 3, "Should have 3 txs"); + assert_eq!(txs[0].id(), tx3_id, "First should be tx3"); + assert_eq!(txs[1].id(), tx1_id, "Second should be tx1"); + assert_eq!(txs[2].id(), tx2_id, "Third should be tx2"); +} + +#[tokio::test] +async fn get_sorted_out_tx_same_tips() { + let mut universe = TestPoolUniverse::default(); + universe.build_pool(); + + // Given + let gas_coin = universe.setup_coin().1; + let tx1 = TransactionBuilder::script(vec![], vec![]) + .tip(10) + .max_fee_limit(10) + .script_gas_limit(GAS_LIMIT) + .add_input(gas_coin) + .finalize_as_transaction(); + + let (_, gas_coin) = universe.setup_coin(); + let tx2 = TransactionBuilder::script(vec![], vec![]) + .tip(10) + .max_fee_limit(10) + .script_gas_limit(GAS_LIMIT / 2) + .add_input(gas_coin) + .finalize_as_transaction(); + + let (_, gas_coin) = universe.setup_coin(); + let tx3 = TransactionBuilder::script(vec![], vec![]) + .tip(10) + .max_fee_limit(10) + .script_gas_limit(GAS_LIMIT / 4) + .add_input(gas_coin) + .finalize_as_transaction(); + + let tx1_id = tx1.id(&ChainId::default()); + let tx2_id = tx2.id(&ChainId::default()); + let tx3_id = tx3.id(&ChainId::default()); + + universe.verify_and_insert(tx1).await.unwrap(); + universe.verify_and_insert(tx2).await.unwrap(); + universe.verify_and_insert(tx3).await.unwrap(); + + // When + let txs = universe + .get_pool() + .write() + .extract_transactions_for_block() + .unwrap(); + + // Then + assert_eq!(txs.len(), 3, "Should have 3 txs"); + assert_eq!(txs[0].id(), tx3_id, "First should be tx3"); + assert_eq!(txs[1].id(), tx2_id, "Second should be tx2"); + assert_eq!(txs[2].id(), tx1_id, "Third should be tx1"); +} + +#[tokio::test] +async fn get_sorted_out_tx_profitable_ratios() { + let mut universe = TestPoolUniverse::default(); + universe.build_pool(); + + // Given + let gas_coin = universe.setup_coin().1; + let tx1 = TransactionBuilder::script(vec![], vec![]) + .tip(4) + .max_fee_limit(4) + .script_gas_limit(GAS_LIMIT) + .add_input(gas_coin) + .finalize_as_transaction(); + + let (_, gas_coin) = universe.setup_coin(); + let tx2 = TransactionBuilder::script(vec![], vec![]) + .tip(2) + .max_fee_limit(2) + .script_gas_limit(GAS_LIMIT / 10) + .add_input(gas_coin) + .finalize_as_transaction(); + + let (_, gas_coin) = universe.setup_coin(); + let tx3 = TransactionBuilder::script(vec![], vec![]) + .tip(1) + .max_fee_limit(1) + .script_gas_limit(GAS_LIMIT / 100) + .add_input(gas_coin) + .finalize_as_transaction(); + + let tx1_id = tx1.id(&ChainId::default()); + let tx2_id = tx2.id(&ChainId::default()); + let tx3_id = tx3.id(&ChainId::default()); + + universe.verify_and_insert(tx1).await.unwrap(); + universe.verify_and_insert(tx2).await.unwrap(); + universe.verify_and_insert(tx3).await.unwrap(); + + // When + let txs = universe + .get_pool() + .write() + .extract_transactions_for_block() + .unwrap(); + + // Then + assert_eq!(txs.len(), 3, "Should have 3 txs"); + assert_eq!(txs[0].id(), tx3_id, "First should be tx3"); + assert_eq!(txs[1].id(), tx2_id, "Second should be tx2"); + assert_eq!(txs[2].id(), tx1_id, "Third should be tx1"); +} + +#[tokio::test] +async fn get_sorted_out_tx_by_creation_instant() { + let mut universe = TestPoolUniverse::default(); + universe.build_pool(); + + // Given + let tx1 = universe.build_script_transaction(None, None, 0); + let tx2 = universe.build_script_transaction(None, None, 0); + let tx3 = universe.build_script_transaction(None, None, 0); + let tx4 = universe.build_script_transaction(None, None, 0); + let tx1_id = tx1.id(&ChainId::default()); + let tx2_id = tx2.id(&ChainId::default()); + let tx3_id = tx3.id(&ChainId::default()); + let tx4_id = tx4.id(&ChainId::default()); + + universe.verify_and_insert(tx1).await.unwrap(); + universe.verify_and_insert(tx2).await.unwrap(); + universe.verify_and_insert(tx3).await.unwrap(); + universe.verify_and_insert(tx4).await.unwrap(); + + // When + let txs = universe + .get_pool() + .write() + .extract_transactions_for_block() + .unwrap(); + + // Then + // This order doesn't match the lexicographical order of the tx ids + // and so it verifies that the txs are sorted by creation instant + // The newest tx should be first + assert_eq!(txs.len(), 4, "Should have 4 txs"); + assert_eq!(txs[0].id(), tx1_id, "First should be tx1"); + assert_eq!(txs[1].id(), tx2_id, "Second should be tx2"); + assert_eq!(txs[2].id(), tx3_id, "Third should be tx3"); + assert_eq!(txs[3].id(), tx4_id, "Fourth should be tx4"); +} + +#[tokio::test] +async fn insert_tx_at_least_min_gas_price() { + // Given + let gas_price = 10; + let mut universe = TestPoolUniverse::default().config(Config { + ..Default::default() + }); + universe.build_pool(); + + let tx = universe.build_script_transaction(None, None, gas_price); + // When + universe.verify_and_insert_with_gas_price(tx, gas_price).await + // Then + .unwrap(); +} + +#[tokio::test] +async fn insert__tx_below_min_gas_price() { + // Given + let gas_price = 11; + let mut universe = TestPoolUniverse::default(); + universe.build_pool(); + + let gas_coin = universe.setup_coin().1; + let tx = TransactionBuilder::script(vec![], vec![]) + .tip(10) + .max_fee_limit(10) + .script_gas_limit(GAS_LIMIT) + .add_input(gas_coin) + .finalize_as_transaction(); + + // When + let err = universe + .verify_and_insert_with_gas_price(tx, gas_price) + .await + .unwrap_err(); + + // Then + assert!(matches!( + err, + Error::ConsensusValidity(CheckError::InsufficientMaxFee { .. }) + )); +} + +#[tokio::test] +async fn insert_tx_when_input_message_id_exists_in_db() { + let mut universe = TestPoolUniverse::default(); + universe.build_pool(); + + // Given + let (message, input) = create_message_predicate_from_message(5000, 0); + universe.database_mut().insert_message(message); + let tx = universe.build_script_transaction(Some(vec![input]), None, 0); + + // When + universe.verify_and_insert(tx).await + // Then + .unwrap(); +} + +#[tokio::test] +async fn insert__tx_when_input_message_id_do_not_exists_in_db() { + let mut universe = TestPoolUniverse::default(); + universe.build_pool(); + + // Given + let (message, input) = create_message_predicate_from_message(5000, 0); + let tx = universe.build_script_transaction(Some(vec![input]), None, 0); + + // When + let err = universe.verify_and_insert(tx).await.unwrap_err(); + + // Then + assert!(matches!( + err, + Error::NotInsertedInputMessageUnknown(msg_id) if msg_id == *message.id() + )); +} + +#[tokio::test] +async fn insert__tx_tip_lower_than_another_tx_with_same_message_id() { + let mut universe = TestPoolUniverse::default(); + universe.build_pool(); + + let tip_high = 2u64; + let tip_low = 1u64; + let (message, conflicting_message_input) = + create_message_predicate_from_message(10_000, 0); + universe.database_mut().insert_message(message.clone()); + + // Given + let tx_high = universe.build_script_transaction( + Some(vec![conflicting_message_input.clone()]), + None, + tip_high, + ); + let tx_low = universe.build_script_transaction( + Some(vec![conflicting_message_input]), + None, + tip_low, + ); + + // When + universe.verify_and_insert(tx_high).await.unwrap(); + let err = universe.verify_and_insert(tx_low).await.unwrap_err(); + + // Then + assert!(matches!(err, Error::Collided(_))); +} + +#[tokio::test] +async fn insert_tx_tip_higher_than_another_tx_with_same_message_id() { + let mut universe = TestPoolUniverse::default(); + universe.build_pool(); + + let tip_high = 2u64; + let tip_low = 1u64; + let (message, conflicting_message_input) = + create_message_predicate_from_message(10_000, 0); + universe.database_mut().insert_message(message.clone()); + + // Given + let tx_high = universe.build_script_transaction( + Some(vec![conflicting_message_input.clone()]), + None, + tip_low, + ); + let tx_high_id = tx_high.id(&ChainId::default()); + let tx_low = universe.build_script_transaction( + Some(vec![conflicting_message_input]), + None, + tip_high, + ); + + // When + let result1 = universe.verify_and_insert(tx_high).await; + let result2 = universe.verify_and_insert(tx_low).await; + + // Then + assert!(result1.is_ok()); + assert!(result2.is_ok()); + let removed_txs = result2.unwrap(); + assert_eq!(removed_txs.len(), 1); + assert_eq!(removed_txs[0].id(), tx_high_id); +} + +#[tokio::test] +async fn insert_again_message_after_squeeze_with_even_lower_tip() { + // tx1 (message 1, message 2) tip 2 + // tx2 (message 1) tip 3 + // squeezes tx1 with higher tip + // tx3 (message 2) tip 1 + // works since tx1 is no longer part of txpool state even though tip is less + + let mut universe = TestPoolUniverse::default(); + universe.build_pool(); + let (message_1, message_input_1) = create_message_predicate_from_message(10_000, 0); + let (message_2, message_input_2) = create_message_predicate_from_message(20_000, 1); + universe.database_mut().insert_message(message_1.clone()); + universe.database_mut().insert_message(message_2.clone()); + + // Given + let tx1 = universe.build_script_transaction( + Some(vec![message_input_1.clone(), message_input_2.clone()]), + None, + 2, + ); + let tx2 = universe.build_script_transaction(Some(vec![message_input_1]), None, 3); + let tx3 = universe.build_script_transaction(Some(vec![message_input_2]), None, 1); + + // When + let result1 = universe.verify_and_insert(tx1).await; + let result2 = universe.verify_and_insert(tx2).await; + let result3 = universe.verify_and_insert(tx3).await; + + // Then + assert!(result1.is_ok()); + assert!(result2.is_ok()); + assert!(result3.is_ok()); +} + +#[tokio::test] +async fn insert__tx_with_predicates_incorrect_owner() { + let mut universe = TestPoolUniverse::default().config(Config { + utxo_validation: false, + ..Default::default() + }); + universe.build_pool(); + + // Given + let mut coin = universe.random_predicate(AssetId::BASE, TEST_COIN_AMOUNT, None); + if let Input::CoinPredicate(CoinPredicate { owner, .. }) = &mut coin { + *owner = Address::zeroed(); + } + + let tx = universe.build_script_transaction(Some(vec![coin]), None, 0); + + // When + let err = universe.verify_and_insert(tx).await.unwrap_err(); + + // Then + assert!(matches!( + err, + Error::ConsensusValidity(CheckError::PredicateVerificationFailed( + PredicateVerificationFailed::InvalidOwner + )) + )); +} + +#[tokio::test] +async fn insert__tx_with_predicate_without_enough_gas() { + let mut universe = TestPoolUniverse::default().config(Config { + utxo_validation: false, + ..Default::default() + }); + universe.build_pool(); + + let gas_limit = 10000; + + // Given + let mut consensus_parameters = ConsensusParameters::default(); + consensus_parameters + .set_tx_params(TxParameters::default().with_max_gas_per_tx(gas_limit)); + consensus_parameters.set_predicate_params( + PredicateParameters::default().with_max_gas_per_predicate(gas_limit), + ); + + let coin = universe + .custom_predicate( + AssetId::BASE, + TEST_COIN_AMOUNT, + // forever loop + vec![op::jmp(RegId::ZERO)].into_iter().collect(), + None, + ) + .into_estimated(&consensus_parameters); + + let tx = universe.build_script_transaction(Some(vec![coin]), None, 0); + + // When + let err = universe.verify_and_insert(tx).await.unwrap_err(); + + // Then + assert!(matches!( + err, + Error::ConsensusValidity(CheckError::PredicateVerificationFailed( + PredicateVerificationFailed::OutOfGas + )) + )); +} + +#[tokio::test] +async fn insert__tx_with_predicate_that_returns_false() { + let mut universe = TestPoolUniverse::default().config(Config { + utxo_validation: false, + ..Default::default() + }); + universe.build_pool(); + + // Given + let coin = universe + .custom_predicate( + AssetId::BASE, + TEST_COIN_AMOUNT, + // forever loop + vec![op::ret(RegId::ZERO)].into_iter().collect(), + None, + ) + .into_default_estimated(); + + let tx = universe.build_script_transaction(Some(vec![coin]), None, 0); + + // When + let err = universe.verify_and_insert(tx).await.unwrap_err(); + + // Then + assert!(matches!( + err, + Error::ConsensusValidity(CheckError::PredicateVerificationFailed( + PredicateVerificationFailed::Panic(PanicReason::PredicateReturnedNonOne) + )) + )); +} + +#[tokio::test] +async fn insert_tx_with_blob() { + let mut universe = TestPoolUniverse::default().config(Config { + utxo_validation: false, + ..Default::default() + }); + universe.build_pool(); + + // Given + let program = vec![123; 123]; + let tx = TransactionBuilder::blob(BlobBody { + id: BlobId::compute(program.as_slice()), + witness_index: 0, + }) + .add_witness(program.into()) + .add_random_fee_input() + .finalize_as_transaction(); + + // When + universe.verify_and_insert(tx).await + // Then + .unwrap(); +} + +#[tokio::test] +async fn insert__tx_with_blob_already_inserted_at_higher_tip() { + let mut universe = TestPoolUniverse::default().config(Config { + utxo_validation: false, + ..Default::default() + }); + universe.build_pool(); + + // Given + let program = vec![123; 123]; + let blob_id = BlobId::compute(program.as_slice()); + let tx = TransactionBuilder::blob(BlobBody { + id: blob_id, + witness_index: 0, + }) + .add_witness(program.clone().into()) + .add_random_fee_input() + .finalize_as_transaction(); + + universe.verify_and_insert(tx).await.unwrap(); + + let same_blob_tx = TransactionBuilder::blob(BlobBody { + id: blob_id, + witness_index: 1, + }) + .add_random_fee_input() + .add_witness(program.into()) + .finalize_as_transaction(); + + // When + let err = universe.verify_and_insert(same_blob_tx).await.unwrap_err(); + + // Then + assert!(matches!(err, Error::Collided(_))); +} + +#[tokio::test] +async fn insert_tx_with_blob_already_insert_at_lower_tip() { + let mut universe = TestPoolUniverse::default().config(Config { + utxo_validation: false, + ..Default::default() + }); + universe.build_pool(); + + // Given + let program = vec![123; 123]; + let blob_id = BlobId::compute(program.as_slice()); + let tx = TransactionBuilder::blob(BlobBody { + id: blob_id, + witness_index: 0, + }) + .add_witness(program.clone().into()) + .add_random_fee_input() + .finalize_as_transaction(); + + universe.verify_and_insert(tx).await.unwrap(); + + let same_blob_tx = TransactionBuilder::blob(BlobBody { + id: blob_id, + witness_index: 1, + }) + .add_random_fee_input() + .add_witness(program.into()) + .tip(100) + .max_fee_limit(100) + .finalize_as_transaction(); + + // When + let result = universe.verify_and_insert(same_blob_tx).await; + + // Then + assert!(result.is_ok()); +} + +#[tokio::test] +async fn insert__tx_blob_already_in_db() { + let mut universe = TestPoolUniverse::default().config(Config { + utxo_validation: false, + ..Default::default() + }); + universe.build_pool(); + let program = vec![123; 123]; + let blob_id = BlobId::compute(program.as_slice()); + let tx = TransactionBuilder::blob(BlobBody { + id: blob_id, + witness_index: 0, + }) + .add_witness(program.clone().into()) + .add_random_fee_input() + .finalize_as_transaction(); + + // Given + universe.database_mut().insert_dummy_blob(blob_id); + + // When + let err = universe.verify_and_insert(tx).await.unwrap_err(); + + // Then + assert!(matches!( + err, + Error::NotInsertedBlobIdAlreadyTaken(b) if b == blob_id + )); +} + +#[tokio::test] +async fn insert__if_tx3_depends_and_collides_with_tx2() { + let mut universe = TestPoolUniverse::default(); + universe.build_pool(); + + // tx1 {inputs: {}, outputs: {coinA}, tip: 1} + let (output_a, unset_input) = universe.create_output_and_input(1); + let tx1 = universe.build_script_transaction(None, Some(vec![output_a]), 1); + // tx2 {inputs: {coinA}, outputs: {coinB}, tip: 1} + let input_a = unset_input.into_input(UtxoId::new(tx1.id(&Default::default()), 0)); + let (output_b, unset_input) = universe.create_output_and_input(1); + let tx2 = universe.build_script_transaction( + Some(vec![input_a.clone()]), + Some(vec![output_b]), + 1, + ); + // Given + // tx3 {inputs: {coinA, coinB}, outputs:{}, tip: 20} + let input_b = unset_input.into_input(UtxoId::new(tx2.id(&Default::default()), 0)); + let tx3 = universe.build_script_transaction(Some(vec![input_a, input_b]), None, 20); + + // When + universe.verify_and_insert(tx1).await.unwrap(); + universe.verify_and_insert(tx2).await.unwrap(); + let err = universe.verify_and_insert(tx3).await.unwrap_err(); + + // Then + assert!(matches!(err, Error::Storage(_))); +} + +#[tokio::test] +async fn insert__tx_upgrade_with_invalid_wasm() { + let mut universe = TestPoolUniverse::default().config(Config { + utxo_validation: false, + ..Default::default() + }); + universe.build_pool(); + + let predicate = vec![op::ret(1)].into_iter().collect::>(); + let privileged_address = Input::predicate_owner(predicate.clone()); + + // Given + let tx = TransactionBuilder::upgrade(UpgradePurpose::StateTransition { + root: Bytes32::new([1; 32]), + }) + .add_input(Input::coin_predicate( + UtxoId::new(Bytes32::new([1; 32]), 0), + privileged_address, + 1_000_000_000, + AssetId::BASE, + Default::default(), + Default::default(), + predicate, + vec![], + )) + .finalize_as_transaction(); + let mut params = ConsensusParameters::default(); + params.set_privileged_address(privileged_address); + + // When + let result = universe + .verify_and_insert_with_consensus_params_wasm_checker( + tx, + params, + MockWasmChecker::new(Err(WasmValidityError::NotEnabled)), + ) + .await + .unwrap_err(); + + // Then + assert!(matches!( + result, + Error::WasmValidity(WasmValidityError::NotEnabled) + )); +} From cb635e892d7de60c29f2ea39b17cbc84694fcc9f Mon Sep 17 00:00:00 2001 From: AurelienFT Date: Wed, 18 Sep 2024 09:52:41 +0200 Subject: [PATCH 09/28] Add heavy async work for insert --- Cargo.lock | 2 + crates/services/txpool_v2/Cargo.toml | 2 + crates/services/txpool_v2/src/config.rs | 14 ++ .../txpool_v2/src/heavy_async_processing.rs | 195 ++++++++++++++++++ crates/services/txpool_v2/src/lib.rs | 1 + crates/services/txpool_v2/src/service.rs | 63 ++++-- 6 files changed, 256 insertions(+), 21 deletions(-) create mode 100644 crates/services/txpool_v2/src/heavy_async_processing.rs diff --git a/Cargo.lock b/Cargo.lock index fc36374bd1e..802eabd062d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3738,10 +3738,12 @@ dependencies = [ "fuel-core-services", "fuel-core-storage", "fuel-core-types", + "futures", "mockall", "num-rational", "parking_lot", "petgraph", + "rayon", "tokio", "tokio-rayon", "tracing", diff --git a/crates/services/txpool_v2/Cargo.toml b/crates/services/txpool_v2/Cargo.toml index 2f080772e1f..632a8a16fa2 100644 --- a/crates/services/txpool_v2/Cargo.toml +++ b/crates/services/txpool_v2/Cargo.toml @@ -18,10 +18,12 @@ derive_more = { workspace = true } fuel-core-services = { workspace = true } fuel-core-storage = { workspace = true, features = ["std"] } fuel-core-types = { workspace = true, features = ["test-helpers"] } +futures = { workspace = true } mockall = { workspace = true, optional = true } num-rational = { workspace = true } parking_lot = { workspace = true } petgraph = "0.6.5" +rayon = { workspace = true } tokio = { workspace = true, default-features = false, features = ["sync"] } tokio-rayon = { workspace = true } tracing = { workspace = true } diff --git a/crates/services/txpool_v2/src/config.rs b/crates/services/txpool_v2/src/config.rs index ff43bc386e9..c39daf04374 100644 --- a/crates/services/txpool_v2/src/config.rs +++ b/crates/services/txpool_v2/src/config.rs @@ -129,10 +129,20 @@ pub struct Config { pub max_txs: u64, /// Maximum transaction time to live. pub max_txs_ttl: Duration, + /// Heavy async processing configuration. + pub heavy_work: HeavyWorkConfig, /// Blacklist. Transactions with blacklisted inputs will not be accepted. pub black_list: BlackList, } +#[derive(Clone)] +pub struct HeavyWorkConfig { + /// Maximum of threads for managing verifications/insertions. + pub number_threads_verif_insert_transactions: usize, + /// Maximum of tasks in the heavy async processing queue. + pub number_pending_tasks_threads_verif_insert_transactions: usize, +} + #[cfg(test)] impl Default for Config { fn default() -> Self { @@ -144,6 +154,10 @@ impl Default for Config { max_txs: 10000, max_txs_ttl: Duration::from_secs(60 * 10), black_list: BlackList::default(), + heavy_work: HeavyWorkConfig { + number_threads_verif_insert_transactions: 4, + number_pending_tasks_threads_verif_insert_transactions: 100, + }, } } } diff --git a/crates/services/txpool_v2/src/heavy_async_processing.rs b/crates/services/txpool_v2/src/heavy_async_processing.rs new file mode 100644 index 00000000000..94fabad1fc7 --- /dev/null +++ b/crates/services/txpool_v2/src/heavy_async_processing.rs @@ -0,0 +1,195 @@ +use std::{ + future::Future, + sync::Arc, +}; +use tokio::sync::Semaphore; + +pub struct HeavyAsyncProcessor { + rayon_thread_pool: rayon::ThreadPool, + semaphore: Arc, +} + +#[derive(Debug, PartialEq, Eq)] +pub struct OutOfCapacity; + +impl HeavyAsyncProcessor { + pub fn new( + number_of_threads: usize, + number_of_pending_tasks: usize, + ) -> anyhow::Result { + let rayon_thread_pool = rayon::ThreadPoolBuilder::new() + .num_threads(number_of_threads) + .build() + .map_err(|e| anyhow::anyhow!("Failed to create a rayon pool: {}", e))?; + let semaphore = Arc::new(Semaphore::new(number_of_pending_tasks)); + Ok(Self { + rayon_thread_pool, + semaphore, + }) + } + + pub fn spawn(&self, future: F) -> Result<(), OutOfCapacity> + where + F: Future + Send + 'static, + { + let permit = self.semaphore.clone().try_acquire_owned(); + + if let Ok(permit) = permit { + self.rayon_thread_pool.spawn_fifo(move || { + let _drop = permit; + futures::executor::block_on(future); + }); + Ok(()) + } else { + Err(OutOfCapacity) + } + } +} + +#[cfg(test)] +#[allow(clippy::bool_assert_comparison)] +mod tests { + use super::*; + use std::{ + thread::sleep, + time::Duration, + }; + use tokio::time::Instant; + + #[tokio::test] + async fn one_spawn_single_tasks_works() { + // Given + let number_of_pending_tasks = 1; + let heavy_task_processor = + HeavyAsyncProcessor::new(1, number_of_pending_tasks).unwrap(); + + // When + let (sender, receiver) = tokio::sync::oneshot::channel(); + let result = heavy_task_processor.spawn(async move { + sender.send(()).unwrap(); + }); + + // Then + assert_eq!(result, Ok(())); + let timeout = tokio::time::timeout(Duration::from_secs(1), receiver).await; + timeout + .expect("Shouldn't timeout") + .expect("Should receive a message"); + } + + #[tokio::test] + async fn second_spawn_fails_when_limit_is_one_and_first_in_progress() { + // Given + let number_of_pending_tasks = 1; + let heavy_task_processor = + HeavyAsyncProcessor::new(1, number_of_pending_tasks).unwrap(); + let first_spawn_result = heavy_task_processor.spawn(async move { + sleep(Duration::from_secs(1)); + }); + assert_eq!(first_spawn_result, Ok(())); + + // When + let second_spawn_result = heavy_task_processor.spawn(async move { + sleep(Duration::from_secs(1)); + }); + + // Then + assert_eq!(second_spawn_result, Err(OutOfCapacity)); + } + + #[tokio::test] + async fn second_spawn_works_when_first_is_finished() { + let number_of_pending_tasks = 1; + let heavy_task_processor = + HeavyAsyncProcessor::new(1, number_of_pending_tasks).unwrap(); + + // Given + let (sender, receiver) = tokio::sync::oneshot::channel(); + let first_spawn = heavy_task_processor.spawn(async move { + sleep(Duration::from_secs(1)); + sender.send(()).unwrap(); + }); + assert_eq!(first_spawn, Ok(())); + receiver.await.unwrap(); + + // When + let second_spawn = heavy_task_processor.spawn(async move { + sleep(Duration::from_secs(1)); + }); + + // Then + assert_eq!(second_spawn, Ok(())); + } + + #[tokio::test] + async fn can_spawn_10_tasks_when_limit_is_10() { + // Given + let number_of_pending_tasks = 10; + let heavy_task_processor = + HeavyAsyncProcessor::new(1, number_of_pending_tasks).unwrap(); + + for _ in 0..number_of_pending_tasks { + // When + let result = heavy_task_processor.spawn(async move { + sleep(Duration::from_secs(1)); + }); + + // Then + assert_eq!(result, Ok(())); + } + } + + #[tokio::test] + async fn executes_10_tasks_for_10_seconds_with_one_thread() { + // Given + let number_of_pending_tasks = 10; + let number_of_threads = 1; + let heavy_task_processor = + HeavyAsyncProcessor::new(number_of_threads, number_of_pending_tasks).unwrap(); + + // When + let (broadcast_sender, mut broadcast_receiver) = + tokio::sync::broadcast::channel(1024); + let instant = Instant::now(); + for _ in 0..number_of_pending_tasks { + let broadcast_sender = broadcast_sender.clone(); + let result = heavy_task_processor.spawn(async move { + sleep(Duration::from_secs(1)); + broadcast_sender.send(()).unwrap(); + }); + assert_eq!(result, Ok(())); + } + drop(broadcast_sender); + + // Then + while broadcast_receiver.recv().await.is_ok() {} + assert!(instant.elapsed() >= Duration::from_secs(10)); + } + + #[tokio::test] + async fn executes_10_tasks_for_2_seconds_with_10_thread() { + // Given + let number_of_pending_tasks = 10; + let number_of_threads = 10; + let heavy_task_processor = + HeavyAsyncProcessor::new(number_of_threads, number_of_pending_tasks).unwrap(); + + // When + let (broadcast_sender, mut broadcast_receiver) = + tokio::sync::broadcast::channel(1024); + let instant = Instant::now(); + for _ in 0..number_of_pending_tasks { + let broadcast_sender = broadcast_sender.clone(); + let result = heavy_task_processor.spawn(async move { + sleep(Duration::from_secs(1)); + broadcast_sender.send(()).unwrap(); + }); + assert_eq!(result, Ok(())); + } + drop(broadcast_sender); + + // Then + while broadcast_receiver.recv().await.is_ok() {} + assert!(instant.elapsed() <= Duration::from_secs(2)); + } +} diff --git a/crates/services/txpool_v2/src/lib.rs b/crates/services/txpool_v2/src/lib.rs index d79b329d46a..20066d59a63 100644 --- a/crates/services/txpool_v2/src/lib.rs +++ b/crates/services/txpool_v2/src/lib.rs @@ -4,6 +4,7 @@ mod collision_manager; mod config; mod error; +mod heavy_async_processing; mod pool; mod ports; mod selection_algorithms; diff --git a/crates/services/txpool_v2/src/service.rs b/crates/services/txpool_v2/src/service.rs index b29f7c269d9..f3dad458dd0 100644 --- a/crates/services/txpool_v2/src/service.rs +++ b/crates/services/txpool_v2/src/service.rs @@ -23,6 +23,7 @@ use crate::{ collision_manager::basic::BasicCollisionManager, config::Config, error::Error, + heavy_async_processing::HeavyAsyncProcessor, pool::Pool, ports::{ AtomicView, @@ -68,6 +69,7 @@ pub struct SharedState< gas_price_provider: Arc, wasm_checker: Arc, memory: Arc, + heavy_async_processor: Arc, utxo_validation: bool, } @@ -88,6 +90,7 @@ impl where - PSProvider: AtomicView, + PSProvider: AtomicView + 'static, PSView: TxPoolPersistentStorage, - ConsensusParamsProvider: ConsensusParametersProvider, - GasPriceProvider: GasPriceProviderTrait + Send + Sync, - WasmChecker: WasmCheckerTrait + Send + Sync, - MemoryPool: MemoryPoolTrait + Send + Sync, + ConsensusParamsProvider: ConsensusParametersProvider + 'static, + GasPriceProvider: GasPriceProviderTrait + Send + Sync + 'static, + WasmChecker: WasmCheckerTrait + Send + Sync + 'static, + MemoryPool: MemoryPoolTrait + Send + Sync + 'static, { async fn insert( - &mut self, + &self, transactions: Vec, ) -> Result, Error> { let current_height = *self.current_height.read(); @@ -126,21 +129,30 @@ where .latest_consensus_parameters(); let mut results = vec![]; for transaction in transactions { - let checked_tx = perform_all_verifications( - transaction, - self.pool.clone(), - current_height, - ¶ms, - version, - self.gas_price_provider.as_ref(), - self.wasm_checker.as_ref(), - self.memory.get_memory().await, - ) - .await?; - let result = { - let mut pool = self.pool.write(); - pool.insert(checked_tx) - }; + self.heavy_async_processor.spawn({ + let shared_state = self.clone(); + let params = params.clone(); + async move { + // TODO: Return the error in the status update channel (see: https://github.com/FuelLabs/fuel-core/issues/2185) + let checked_tx = perform_all_verifications( + transaction, + shared_state.pool.clone(), + current_height, + ¶ms, + version, + shared_state.gas_price_provider.as_ref(), + shared_state.wasm_checker.as_ref(), + shared_state.memory.get_memory().await, + ) + .await + .unwrap(); + let result = { + let mut pool = shared_state.pool.write(); + // TODO: Return the result of the insertion (see: https://github.com/FuelLabs/fuel-core/issues/2185) + pool.insert(checked_tx) + }; + } + }); } Ok(results) } @@ -300,6 +312,15 @@ where memory: Arc::new(memory_pool), current_height: Arc::new(RwLock::new(current_height)), utxo_validation: config.utxo_validation, + heavy_async_processor: Arc::new( + HeavyAsyncProcessor::new( + config + .heavy_work + .number_pending_tasks_threads_verif_insert_transactions, + config.heavy_work.number_threads_verif_insert_transactions, + ) + .unwrap(), + ), pool: Arc::new(RwLock::new(Pool::new( ps_provider, GraphStorage::new(GraphConfig { From e7dc92491be1f25b25da5bee151eedf3011e27bf Mon Sep 17 00:00:00 2001 From: AurelienFT Date: Wed, 18 Sep 2024 10:59:11 +0200 Subject: [PATCH 10/28] Clean up of the code --- Cargo.lock | 2 +- crates/services/txpool_v2/src/service.rs | 1 - .../services/txpool_v2/src/storage/graph.rs | 2 - .../services/txpool_v2/src/tests/context.rs | 120 ++++++++---------- crates/services/txpool_v2/src/tests/pool.rs | 44 ++----- .../services/txpool_v2/src/verifications.rs | 6 +- 6 files changed, 67 insertions(+), 108 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 6aa4dfc13ac..0d88e9f8815 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3744,7 +3744,7 @@ dependencies = [ "derive_more", "fuel-core-services", "fuel-core-storage", - "fuel-core-types", + "fuel-core-types 0.36.0", "futures", "mockall", "num-rational", diff --git a/crates/services/txpool_v2/src/service.rs b/crates/services/txpool_v2/src/service.rs index f3dad458dd0..42dd7ab2e84 100644 --- a/crates/services/txpool_v2/src/service.rs +++ b/crates/services/txpool_v2/src/service.rs @@ -37,7 +37,6 @@ use crate::{ storage::graph::{ GraphConfig, GraphStorage, - GraphStorageIndex, }, verifications::perform_all_verifications, }; diff --git a/crates/services/txpool_v2/src/storage/graph.rs b/crates/services/txpool_v2/src/storage/graph.rs index 1759b68a465..4b533866475 100644 --- a/crates/services/txpool_v2/src/storage/graph.rs +++ b/crates/services/txpool_v2/src/storage/graph.rs @@ -47,8 +47,6 @@ use super::{ StorageData, }; -pub type GraphStorageIndex = NodeIndex; - pub struct GraphStorage { /// The configuration of the graph config: GraphConfig, diff --git a/crates/services/txpool_v2/src/tests/context.rs b/crates/services/txpool_v2/src/tests/context.rs index c477748a763..6a7028e454f 100644 --- a/crates/services/txpool_v2/src/tests/context.rs +++ b/crates/services/txpool_v2/src/tests/context.rs @@ -1,6 +1,5 @@ use std::sync::Arc; -use fuel_core_storage::transactional::AtomicView; use fuel_core_types::{ entities::{ coins::coin::{ @@ -28,7 +27,6 @@ use fuel_core_types::{ contract::Contract as ContractInput, Input, }, - Bytes32, ConsensusParameters, Contract, ContractId, @@ -44,27 +42,17 @@ use fuel_core_types::{ Word, }, fuel_vm::{ - checked_transaction::{ - Checked, - EstimatePredicates, - }, + checked_transaction::EstimatePredicates, interpreter::MemoryInstance, }, - services::txpool::PoolTransaction, }; use parking_lot::RwLock; -use petgraph::graph::NodeIndex; use crate::{ collision_manager::basic::BasicCollisionManager, config::Config, error::Error, pool::Pool, - ports::{ - GasPriceProvider, - WasmChecker, - WasmValidityError, - }, selection_algorithms::ratio_tip_gas::RatioTipGasSelection, service::{ RemovedTransactions, @@ -86,58 +74,6 @@ use super::mocks::{ MockTxPoolGasPrice, MockWasmChecker, }; -// TDOO: Reorganize this file - -pub fn create_message_predicate_from_message( - amount: Word, - nonce: u64, -) -> (Message, Input) { - let predicate = vec![op::ret(1)].into_iter().collect::>(); - let message = MessageV1 { - sender: Default::default(), - recipient: Input::predicate_owner(&predicate), - nonce: nonce.into(), - amount, - data: vec![], - da_height: Default::default(), - }; - - ( - message.clone().into(), - Input::message_coin_predicate( - message.sender, - Input::predicate_owner(&predicate), - message.amount, - message.nonce, - Default::default(), - predicate, - Default::default(), - ) - .into_default_estimated(), - ) -} - -pub fn create_coin_output() -> Output { - Output::coin(Default::default(), Default::default(), Default::default()) -} - -pub fn create_contract_input( - tx_id: TxId, - output_index: u16, - contract_id: ContractId, -) -> Input { - Input::contract( - UtxoId::new(tx_id, output_index), - Default::default(), - Default::default(), - Default::default(), - contract_id, - ) -} - -pub fn create_contract_output(contract_id: ContractId) -> Output { - Output::contract_created(contract_id, Contract::default_state_root()) -} // use some arbitrary large amount, this shouldn't affect the txpool logic except for covering // the byte and gas price fees. @@ -302,10 +238,9 @@ impl TestPoolUniverse { (coin.uncompress(utxo_id), input) } - // TODO: Change this - pub fn create_output_and_input(&mut self, amount: Word) -> (Output, UnsetInput) { - let input = self.random_predicate(AssetId::BASE, amount, None); - let output = Output::coin(*input.input_owner().unwrap(), amount, AssetId::BASE); + pub fn create_output_and_input(&mut self) -> (Output, UnsetInput) { + let input = self.random_predicate(AssetId::BASE, 1, None); + let output = Output::coin(*input.input_owner().unwrap(), 1, AssetId::BASE); (output, UnsetInput(input)) } @@ -354,6 +289,53 @@ impl TestPoolUniverse { } } +pub fn create_message_predicate_from_message( + amount: Word, + nonce: u64, +) -> (Message, Input) { + let predicate = vec![op::ret(1)].into_iter().collect::>(); + let message = MessageV1 { + sender: Default::default(), + recipient: Input::predicate_owner(&predicate), + nonce: nonce.into(), + amount, + data: vec![], + da_height: Default::default(), + }; + + ( + message.clone().into(), + Input::message_coin_predicate( + message.sender, + Input::predicate_owner(&predicate), + message.amount, + message.nonce, + Default::default(), + predicate, + Default::default(), + ) + .into_default_estimated(), + ) +} + +pub fn create_contract_input( + tx_id: TxId, + output_index: u16, + contract_id: ContractId, +) -> Input { + Input::contract( + UtxoId::new(tx_id, output_index), + Default::default(), + Default::default(), + Default::default(), + contract_id, + ) +} + +pub fn create_contract_output(contract_id: ContractId) -> Output { + Output::contract_created(contract_id, Contract::default_state_root()) +} + pub struct UnsetInput(Input); impl UnsetInput { diff --git a/crates/services/txpool_v2/src/tests/pool.rs b/crates/services/txpool_v2/src/tests/pool.rs index 9483b15874e..efe49c34db1 100644 --- a/crates/services/txpool_v2/src/tests/pool.rs +++ b/crates/services/txpool_v2/src/tests/pool.rs @@ -6,7 +6,6 @@ use crate::{ ports::WasmValidityError, tests::{ context::{ - create_coin_output, create_contract_input, create_contract_output, create_message_predicate_from_message, @@ -19,15 +18,11 @@ use crate::{ }, }; use fuel_core_types::{ - blockchain::header::ConsensusParametersVersion, fuel_asm::{ op, RegId, - Word, }, - fuel_merkle::common, fuel_tx::{ - consensus_parameters::gas, input::coin::CoinPredicate, Address, AssetId, @@ -37,12 +32,10 @@ use fuel_core_types::{ Bytes32, ConsensusParameters, Contract, - Finalizable, Input, Output, PanicReason, PredicateParameters, - Transaction, TransactionBuilder, TxParameters, UniqueIdentifier, @@ -51,18 +44,9 @@ use fuel_core_types::{ }, fuel_types::ChainId, fuel_vm::{ - checked_transaction::{ - CheckError, - Checked, - CheckedTransaction, - IntoChecked, - }, + checked_transaction::CheckError, PredicateVerificationFailed, }, - services::txpool::{ - self, - PoolTransaction, - }, }; use std::vec; @@ -167,10 +151,10 @@ async fn insert__tx_with_blacklisted_message() { #[tokio::test] async fn insert_tx2_dependent_tx1() { let mut universe = TestPoolUniverse::default(); - let mut txpool = universe.build_pool(); + universe.build_pool(); // Given - let (output, unset_input) = universe.create_output_and_input(1); + let (output, unset_input) = universe.create_output_and_input(); let tx1 = universe.build_script_transaction(None, Some(vec![output]), 0); let input = unset_input.into_input(UtxoId::new(tx1.id(&ChainId::default()), 0)); @@ -194,7 +178,7 @@ async fn insert__tx2_collided_on_contract_id() { // contract creation tx let (_, gas_coin) = universe.setup_coin(); - let (output, unset_input) = universe.create_output_and_input(10); + let (output, unset_input) = universe.create_output_and_input(); let tx = TransactionBuilder::create( Default::default(), Default::default(), @@ -241,7 +225,7 @@ async fn insert__tx_with_dependency_on_invalid_utxo_type() { universe.build_pool(); let contract_id = Contract::EMPTY_CONTRACT_ID; - let (_, gas_coin) = universe.setup_coin(); + let gas_coin = universe.setup_coin().1; let tx = TransactionBuilder::create( Default::default(), Default::default(), @@ -323,11 +307,11 @@ async fn insert_higher_priced_tx_removes_lower_priced_tx() { let tx2 = universe.build_script_transaction(Some(vec![common_coin]), None, 20); // When - let result1 = universe.verify_and_insert(tx1).await.unwrap(); - let result2 = universe.verify_and_insert(tx2).await.unwrap(); + universe.verify_and_insert(tx1).await.unwrap(); + let result = universe.verify_and_insert(tx2).await.unwrap(); // Then - assert_eq!(result2[0].id(), tx_id); + assert_eq!(result[0].id(), tx_id); } #[tokio::test] @@ -335,7 +319,7 @@ async fn insert__colliding_dependent_underpriced() { let mut universe = TestPoolUniverse::default(); universe.build_pool(); - let (output, unset_input) = universe.create_output_and_input(20); + let (output, unset_input) = universe.create_output_and_input(); let tx1 = universe.build_script_transaction(None, Some(vec![output]), 20); let input = unset_input.into_input(UtxoId::new(tx1.id(&ChainId::default()), 0)); @@ -404,7 +388,7 @@ async fn insert_more_priced_tx3_removes_tx1_and_dependent_tx2() { // Given let common_coin = universe.setup_coin().1; - let (output, unset_input) = universe.create_output_and_input(10); + let (output, unset_input) = universe.create_output_and_input(); let tx1 = universe.build_script_transaction( Some(vec![common_coin.clone()]), @@ -500,11 +484,11 @@ async fn insert__dependency_chain_length_hit() { universe.build_pool(); // Given - let (output, unset_input) = universe.create_output_and_input(10_000); + let (output, unset_input) = universe.create_output_and_input(); let tx1 = universe.build_script_transaction(None, Some(vec![output]), 0); let input = unset_input.into_input(UtxoId::new(tx1.id(&Default::default()), 0)); - let (output, unset_input) = universe.create_output_and_input(5_000); + let (output, unset_input) = universe.create_output_and_input(); let tx2 = universe.build_script_transaction(Some(vec![input]), Some(vec![output]), 0); let input = unset_input.into_input(UtxoId::new(tx2.id(&Default::default()), 0)); @@ -1114,11 +1098,11 @@ async fn insert__if_tx3_depends_and_collides_with_tx2() { universe.build_pool(); // tx1 {inputs: {}, outputs: {coinA}, tip: 1} - let (output_a, unset_input) = universe.create_output_and_input(1); + let (output_a, unset_input) = universe.create_output_and_input(); let tx1 = universe.build_script_transaction(None, Some(vec![output_a]), 1); // tx2 {inputs: {coinA}, outputs: {coinB}, tip: 1} let input_a = unset_input.into_input(UtxoId::new(tx1.id(&Default::default()), 0)); - let (output_b, unset_input) = universe.create_output_and_input(1); + let (output_b, unset_input) = universe.create_output_and_input(); let tx2 = universe.build_script_transaction( Some(vec![input_a.clone()]), Some(vec![output_b]), diff --git a/crates/services/txpool_v2/src/verifications.rs b/crates/services/txpool_v2/src/verifications.rs index c38621fa855..4ceed3a68ee 100644 --- a/crates/services/txpool_v2/src/verifications.rs +++ b/crates/services/txpool_v2/src/verifications.rs @@ -1,8 +1,5 @@ use fuel_core_types::{ - blockchain::{ - consensus, - header::ConsensusParametersVersion, - }, + blockchain::header::ConsensusParametersVersion, fuel_tx::{ field::UpgradePurpose as _, ConsensusParameters, @@ -26,7 +23,6 @@ use fuel_core_types::{ use crate::{ error::Error, - pool::Pool, ports::{ AtomicView, GasPriceProvider, From c12ee381fb0629301f8f602574bdd91c123aaef3 Mon Sep 17 00:00:00 2001 From: AurelienFT Date: Wed, 18 Sep 2024 11:15:17 +0200 Subject: [PATCH 11/28] Update changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 64ac52ee04b..ae907e415aa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/). - [2163](https://github.com/FuelLabs/fuel-core/pull/2163): Added runnable task for fetching block committer data. - [2204](https://github.com/FuelLabs/fuel-core/pull/2204): Added `dnsaddr` resolution for TLD without suffixes. - [2162](https://github.com/FuelLabs/fuel-core/pull/2162): Pool structure with dependencies, etc.. for the next transaction pool module. +- [2193](https://github.com/FuelLabs/fuel-core/pull/2193): Insertion in PoolV2 and tests refactoring ### Changed From 4f87fbda8f5c3f535462514db3f03833c9a84d8e Mon Sep 17 00:00:00 2001 From: AurelienFT Date: Wed, 18 Sep 2024 14:16:22 +0200 Subject: [PATCH 12/28] fix clippy --- crates/services/txpool_v2/src/tests/context.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/services/txpool_v2/src/tests/context.rs b/crates/services/txpool_v2/src/tests/context.rs index 6a7028e454f..b9c25553465 100644 --- a/crates/services/txpool_v2/src/tests/context.rs +++ b/crates/services/txpool_v2/src/tests/context.rs @@ -132,7 +132,7 @@ impl TestPoolUniverse { ) -> Transaction { let mut inputs = inputs.unwrap_or_default(); let (_, gas_coin) = self.setup_coin(); - inputs.push(gas_coin.into()); + inputs.push(gas_coin); let outputs = outputs.unwrap_or_default(); let mut tx_builder = TransactionBuilder::script(vec![], vec![]); tx_builder.script_gas_limit(GAS_LIMIT); From cf9e9eb54807655c1dafb696731bbe724ce8364c Mon Sep 17 00:00:00 2001 From: AurelienFT Date: Mon, 23 Sep 2024 16:47:24 +0200 Subject: [PATCH 13/28] Change insertion checks to be simplified --- .../txpool_v2/src/collision_manager/basic.rs | 103 ++++++++-------- .../txpool_v2/src/collision_manager/mod.rs | 46 +------- crates/services/txpool_v2/src/config.rs | 20 +++- crates/services/txpool_v2/src/error.rs | 2 + crates/services/txpool_v2/src/pool.rs | 111 +++++++++++++++--- .../services/txpool_v2/src/storage/graph.rs | 56 +++++---- crates/services/txpool_v2/src/storage/mod.rs | 29 ++++- crates/services/txpool_v2/src/tests/pool.rs | 11 +- 8 files changed, 240 insertions(+), 138 deletions(-) diff --git a/crates/services/txpool_v2/src/collision_manager/basic.rs b/crates/services/txpool_v2/src/collision_manager/basic.rs index 63bff4fd7fc..64cf222123e 100644 --- a/crates/services/txpool_v2/src/collision_manager/basic.rs +++ b/crates/services/txpool_v2/src/collision_manager/basic.rs @@ -34,11 +34,7 @@ use crate::{ storage::StorageData, }; -use super::{ - CollisionManager, - CollisionReason, - Collisions, -}; +use super::CollisionManager; pub trait BasicCollisionManagerStorage { type StorageIndex: Copy + Debug; @@ -75,16 +71,21 @@ impl Default for BasicCollisionManager { } impl BasicCollisionManager { - fn gather_colliding_txs( + fn gather_colliding_tx( &self, tx: &PoolTransaction, - ) -> Result, Error> { - let mut collisions = Collisions::new(); + ) -> Result, Error> { + let mut collision: Option = None; if let PoolTransaction::Blob(checked_tx, _) = tx { let blob_id = checked_tx.transaction().blob_id(); if let Some(state) = self.blobs_users.get(blob_id) { - collisions.reasons.insert(CollisionReason::Blob(*blob_id)); - collisions.colliding_txs.push(*state); + if collision.is_some() { + return Err(Error::Collided(format!( + "Transaction collides with other transactions: {:?}", + collision + ))); + } + collision = Some(*state); } } for input in tx.inputs() { @@ -93,8 +94,13 @@ impl BasicCollisionManager { | Input::CoinPredicate(CoinPredicate { utxo_id, .. }) => { // Check if the utxo is already spent by another transaction in the pool if let Some(tx_id) = self.coins_spenders.get(utxo_id) { - collisions.reasons.insert(CollisionReason::Coin(*utxo_id)); - collisions.colliding_txs.push(*tx_id); + if collision.is_some() { + return Err(Error::Collided(format!( + "Transaction collides with other transactions: {:?}", + collision + ))); + } + collision = Some(*tx_id); } } Input::MessageCoinSigned(MessageCoinSigned { nonce, .. }) @@ -103,8 +109,13 @@ impl BasicCollisionManager { | Input::MessageDataPredicate(MessageDataPredicate { nonce, .. }) => { // Check if the message is already spent by another transaction in the pool if let Some(tx_id) = self.messages_spenders.get(nonce) { - collisions.reasons.insert(CollisionReason::Message(*nonce)); - collisions.colliding_txs.push(*tx_id); + if collision.is_some() { + return Err(Error::Collided(format!( + "Transaction collides with other transactions: {:?}", + collision + ))); + } + collision = Some(*tx_id); } } // No collision for contract inputs @@ -116,40 +127,34 @@ impl BasicCollisionManager { if let Output::ContractCreated { contract_id, .. } = output { // Check if the contract is already created by another transaction in the pool if let Some(tx_id) = self.contracts_creators.get(contract_id) { - collisions - .reasons - .insert(CollisionReason::ContractCreation(*contract_id)); - collisions.colliding_txs.push(*tx_id); + if collision.is_some() { + return Err(Error::Collided(format!( + "Transaction collides with other transactions: {:?}", + collision + ))); + } + collision = Some(*tx_id); } } } - Ok(collisions) + Ok(collision) } - fn is_better_than_collisions( + fn is_better_than_collision( &self, tx: &PoolTransaction, - collisions: &Collisions, + collision: S::StorageIndex, storage: &S, ) -> bool { let new_tx_ratio = Ratio::new(tx.tip(), tx.max_gas()); - let (total_tip, total_gas) = collisions.colliding_txs.iter().fold( - (0u64, 0u64), - |(total_tip, total_gas), node_id| { - let dependent_tx = storage - .get(node_id) - .expect("Transaction always should exist in storage"); - let total_tip = - total_tip.saturating_add(dependent_tx.dependents_cumulative_tip); - let total_gas = - total_gas.saturating_add(dependent_tx.dependents_cumulative_gas); - (total_tip, total_gas) - }, + let colliding_tx = storage + .get(&collision) + .expect("Transaction always should exist in storage"); + let colliding_tx_ratio = Ratio::new( + colliding_tx.dependents_cumulative_gas, + colliding_tx.dependents_cumulative_tip, ); - - let collision_tx_ratio = Ratio::new(total_tip, total_gas); - - new_tx_ratio > collision_tx_ratio + new_tx_ratio > colliding_tx_ratio } } @@ -157,21 +162,23 @@ impl CollisionManager for BasicCollisionManager type Storage = S; type StorageIndex = S::StorageIndex; - fn collect_colliding_transactions( + fn collect_colliding_transaction( &self, transaction: &PoolTransaction, storage: &S, - ) -> Result, Error> { - let collisions = self.gather_colliding_txs(transaction)?; - if collisions.colliding_txs.is_empty() { - Ok(Collisions::new()) - } else if self.is_better_than_collisions(transaction, &collisions, storage) { - Ok(collisions) + ) -> Result, Error> { + let collision = self.gather_colliding_tx(transaction)?; + if let Some(collision) = collision { + if self.is_better_than_collision(transaction, collision, storage) { + Ok(Some(collision)) + } else { + Err(Error::Collided(format!( + "Transaction collides with other transactions: {:?}", + collision + ))) + } } else { - Err(Error::Collided(format!( - "Transaction collides with other transactions: {:?}", - collisions.colliding_txs - ))) + Ok(None) } } diff --git a/crates/services/txpool_v2/src/collision_manager/mod.rs b/crates/services/txpool_v2/src/collision_manager/mod.rs index 95433bb286a..b8a81b702ea 100644 --- a/crates/services/txpool_v2/src/collision_manager/mod.rs +++ b/crates/services/txpool_v2/src/collision_manager/mod.rs @@ -17,57 +17,21 @@ use crate::error::Error; pub mod basic; -/// The reason why a transaction collides with another. -/// It also contains additional information about the collision. -#[derive(Debug, Copy, Clone, Ord, PartialOrd, Eq, PartialEq, Hash)] -pub enum CollisionReason { - Coin(UtxoId), - Blob(BlobId), - Message(Nonce), - ContractCreation(ContractId), -} - -/// Contains all the information about the collisions of a transaction. -#[derive(Default, Debug)] -pub struct Collisions { - reasons: HashSet, - colliding_txs: Vec, -} - -impl Collisions { - /// Create a new empty collision information. - pub fn new() -> Self { - Self { - reasons: HashSet::default(), - colliding_txs: vec![], - } - } - - /// Get the reasons of the collisions. - pub fn reasons(&self) -> &HashSet { - &self.reasons - } - - /// Get the transactions that collide with the transaction. - pub fn colliding_txs(&self) -> &[Idx] { - &self.colliding_txs - } -} - pub trait CollisionManager { /// Storage type of the collision manager. type Storage; /// Index that identifies a transaction in the storage. type StorageIndex; - /// Collect all the transactions that collide with the given transaction. + /// Collect the transaction that collide with the given transaction. /// It returns an error if the transaction is less worthy than the colliding transactions. - /// It returns the information about the collisions. - fn collect_colliding_transactions( + /// It returns an error if the transaction collide with two transactions. + /// It returns the information about the collision if exists, none otherwise. + fn collect_colliding_transaction( &self, transaction: &PoolTransaction, storage: &Self::Storage, - ) -> Result, Error>; + ) -> Result, Error>; /// Inform the collision manager that a transaction was stored. fn on_stored_transaction( diff --git a/crates/services/txpool_v2/src/config.rs b/crates/services/txpool_v2/src/config.rs index 6998f2acec3..592b11a054f 100644 --- a/crates/services/txpool_v2/src/config.rs +++ b/crates/services/txpool_v2/src/config.rs @@ -125,8 +125,8 @@ pub struct Config { pub max_block_gas: u64, /// Maximum transactions per dependencies chain. pub max_txs_chain_count: usize, - /// Maximum transactions in the pool. - pub max_txs: usize, + /// Pool limits + pub pool_limits: PoolLimits, /// Maximum transaction time to live. pub max_txs_ttl: Duration, /// Heavy async processing configuration. @@ -135,6 +135,16 @@ pub struct Config { pub black_list: BlackList, } +#[derive(Clone)] +pub struct PoolLimits { + /// Maximum number of transactions in the pool. + pub max_txs: usize, + /// Maximum number of gas in the pool. + pub max_gas: u64, + /// Maximum number of bytes in the pool. + pub max_bytes_size: usize, +} + #[derive(Clone)] pub struct HeavyWorkConfig { /// Maximum of threads for managing verifications/insertions. @@ -151,7 +161,11 @@ impl Default for Config { max_block_gas: 100000000, max_block_size: 1000000000, max_txs_chain_count: 1000, - max_txs: 10000, + pool_limits: PoolLimits { + max_txs: 10000, + max_gas: 100_000_000_000, + max_bytes_size: 10_000_000_000, + }, max_txs_ttl: Duration::from_secs(60 * 10), black_list: BlackList::default(), heavy_work: HeavyWorkConfig { diff --git a/crates/services/txpool_v2/src/error.rs b/crates/services/txpool_v2/src/error.rs index 011afb8efee..14e6d0a6fa3 100644 --- a/crates/services/txpool_v2/src/error.rs +++ b/crates/services/txpool_v2/src/error.rs @@ -32,6 +32,8 @@ pub enum Error { // TODO: Make more specific errors: https://github.com/FuelLabs/fuel-core/issues/2185 #[display(fmt = "Transaction collided: {_0}")] Collided(String), + #[display(fmt = "Transaction is not inserted. Collision is also a dependency")] + NotInsertedCollisionIsDependency, #[display(fmt = "Utxo not found: {_0}")] UtxoNotFound(UtxoId), #[display(fmt = "The UTXO `{_0}` is blacklisted")] diff --git a/crates/services/txpool_v2/src/pool.rs b/crates/services/txpool_v2/src/pool.rs index 60bede63eae..2907d25e7d6 100644 --- a/crates/services/txpool_v2/src/pool.rs +++ b/crates/services/txpool_v2/src/pool.rs @@ -23,7 +23,10 @@ use crate::{ Constraints, SelectionAlgorithm, }, - storage::Storage, + storage::{ + RemovedTransactions, + Storage, + }, verifications::FullyVerifiedTx, }; @@ -42,6 +45,10 @@ pub struct Pool { persistent_storage_provider: PSProvider, /// Mapping from tx_id to storage_id. tx_id_to_storage_id: HashMap, + /// Current pool gas stored. + current_gas: u64, + /// Current pool size in bytes. + current_bytes_size: usize, } impl Pool { @@ -60,6 +67,8 @@ impl Pool { persistent_storage_provider, config, tx_id_to_storage_id: HashMap::new(), + current_gas: 0, + current_bytes_size: 0, } } } @@ -83,21 +92,31 @@ where .latest_view() .map_err(|e| Error::Database(format!("{:?}", e)))?; let tx_id = tx.id(); - self.check_pool_is_not_full()?; self.config.black_list.check_blacklisting(&tx)?; Self::check_blob_does_not_exist(&tx, &latest_view)?; - let collisions = self - .collision_manager - .collect_colliding_transactions(&tx, &self.storage)?; self.storage .validate_inputs(&tx, &latest_view, self.config.utxo_validation)?; + let collision = self + .collision_manager + .collect_colliding_transaction(&tx, &self.storage)?; let dependencies = self.storage.collect_transaction_dependencies(&tx)?; + if let Some(collision) = collision { + if self + .storage + .is_in_dependencies_subtrees(collision, &dependencies)? + { + return Err(Error::NotInsertedCollisionIsDependency); + } + } + let transactions_to_remove = + self.check_pool_size_available(&tx, &collision, &dependencies)?; + let mut removed_transactions = vec![]; + for tx in transactions_to_remove { + let removed = self.storage.remove_transaction_and_dependents_subtree(tx)?; + removed_transactions.extend(removed); + } let has_dependencies = !dependencies.is_empty(); - let (storage_id, removed_transactions) = self.storage.store_transaction( - tx, - dependencies, - collisions.colliding_txs(), - )?; + let storage_id = self.storage.store_transaction(tx, dependencies)?; self.tx_id_to_storage_id.insert(tx_id, storage_id); // No dependencies directly in the graph and the sorted transactions if !has_dependencies { @@ -117,20 +136,19 @@ where .persistent_storage_provider .latest_view() .map_err(|e| Error::Database(format!("{:?}", e)))?; - self.check_pool_is_not_full()?; self.config.black_list.check_blacklisting(tx)?; Self::check_blob_does_not_exist(tx, &persistent_storage)?; - let collisions = self + let collision = self .collision_manager - .collect_colliding_transactions(tx, &self.storage)?; + .collect_colliding_transaction(tx, &self.storage)?; self.storage.validate_inputs( tx, &persistent_storage, self.config.utxo_validation, )?; let dependencies = self.storage.collect_transaction_dependencies(tx)?; - self.storage - .can_store_transaction(tx, &dependencies, collisions.colliding_txs()); + self.check_pool_size_available(&tx, &collision, &dependencies)?; + self.storage.can_store_transaction(tx, &dependencies); Ok(()) } @@ -175,11 +193,68 @@ where .ok() } - fn check_pool_is_not_full(&self) -> Result<(), Error> { - if self.storage.count() >= self.config.max_txs { + /// Check if the pool has enough space to store a transaction. + /// It will try to see if we can free some space dependending on defined rules + /// Returns an error if the pool is full. + /// If the pool is not full, it will return the list of transactions that must be removed from the pool along all of their dependent subtree + fn check_pool_size_available( + &self, + tx: &PoolTransaction, + collision: &Option, + dependencies: &Vec, + ) -> Result, Error> { + if self.current_gas < self.config.pool_limits.max_gas + && self.current_bytes_size < self.config.pool_limits.max_bytes_size + && self.storage.count() < self.config.pool_limits.max_txs + { + return Ok(vec![]); + } + // If the transaction has a collision verify that by removing the transaction we can free enough space + // otherwise return an error + if let Some(collision) = collision { + let collision_data = self.storage.get(collision)?; + let new_current_gas = self + .current_gas + .saturating_sub(collision_data.dependents_cumulative_gas); + let new_current_bytes_size = self + .current_bytes_size + .saturating_sub(collision_data.dependents_cumulative_bytes_size); + if new_current_gas < self.config.pool_limits.max_gas + && new_current_bytes_size < self.config.pool_limits.max_bytes_size + && self.storage.count().saturating_sub(1) + < self.config.pool_limits.max_txs + { + return Ok(vec![*collision]); + } else { + return Err(Error::NotInsertedLimitHit); + } + } + + // If the transaction has a dependency and the pool is full, we refuse it + if !dependencies.is_empty() { return Err(Error::NotInsertedLimitHit); } - Ok(()) + + // Here the transaction has no dependencies and no collision which means that it's an executable transaction + // and we want to make space for it + let mut removed_transactions = vec![]; + let mut gas_left = self.current_gas; + let mut bytes_left = self.current_bytes_size; + let mut txs_left = self.storage.count(); + let mut sorted_txs = self.storage.get_worst_ratio_tip_gas_subtree_roots()?; + while gas_left > self.config.pool_limits.max_gas + || bytes_left > self.config.pool_limits.max_bytes_size + || txs_left > self.config.pool_limits.max_txs + { + let storage_id = sorted_txs.next().ok_or(Error::NotInsertedLimitHit)?; + let storage_data = self.storage.get(&storage_id)?; + gas_left = gas_left.saturating_sub(storage_data.dependents_cumulative_gas); + bytes_left = + bytes_left.saturating_sub(storage_data.dependents_cumulative_bytes_size); + txs_left = txs_left.saturating_sub(1); + removed_transactions.push(storage_id); + } + Ok(removed_transactions) } fn check_blob_does_not_exist( diff --git a/crates/services/txpool_v2/src/storage/graph.rs b/crates/services/txpool_v2/src/storage/graph.rs index adfbdfa96ed..1ef35b906be 100644 --- a/crates/services/txpool_v2/src/storage/graph.rs +++ b/crates/services/txpool_v2/src/storage/graph.rs @@ -1,5 +1,6 @@ use std::{ collections::{ + BTreeMap, HashMap, HashSet, }, @@ -29,16 +30,14 @@ use fuel_core_types::{ }, services::txpool::PoolTransaction, }; +use num_rational::Ratio; use petgraph::{ graph::NodeIndex, prelude::StableDiGraph, }; use crate::{ - collision_manager::{ - basic::BasicCollisionManagerStorage, - CollisionReason, - }, + collision_manager::basic::BasicCollisionManagerStorage, error::Error, ports::TxPoolPersistentStorage, selection_algorithms::ratio_tip_gas::RatioTipGasSelectionAlgorithmStorage, @@ -321,24 +320,19 @@ impl Storage for GraphStorage { &mut self, transaction: PoolTransaction, dependencies: Vec, - collided_transactions: &[Self::StorageIndex], - ) -> Result<(Self::StorageIndex, RemovedTransactions), Error> { + ) -> Result { let tx_id = transaction.id(); // Add the new transaction to the graph and update the others in consequence let tip = transaction.tip(); let gas = transaction.max_gas(); + let size = transaction.metered_bytes_size(); let outputs = transaction.outputs().clone(); // Check if the dependency chain is too big let mut all_dependencies_recursively = HashSet::new(); let mut to_check = dependencies.clone(); while let Some(node_id) = to_check.pop() { - if collided_transactions.contains(&node_id) { - return Err(Error::Collided( - "Use a collided transaction as a dependency".to_string(), - )); - } // Already checked node if all_dependencies_recursively.contains(&node_id) { continue; @@ -360,16 +354,10 @@ impl Storage for GraphStorage { return Err(Error::NotInsertedChainDependencyTooBig); } - // Remove collisions and their dependencies from the graph - let mut removed_transactions = vec![]; - for collision in collided_transactions { - removed_transactions - .extend(self.remove_node_and_dependent_sub_graph(*collision)?); - } - let node = StorageData { dependents_cumulative_tip: tip, dependents_cumulative_gas: gas, + dependents_cumulative_bytes_size: size, transaction, number_txs_in_chain: all_dependencies_recursively.len().saturating_add(1), }; @@ -394,15 +382,16 @@ impl Storage for GraphStorage { node.dependents_cumulative_tip.saturating_add(tip); node.dependents_cumulative_gas = node.dependents_cumulative_gas.saturating_add(gas); + node.dependents_cumulative_bytes_size = + node.dependents_cumulative_bytes_size.saturating_add(size); } - Ok((node_id, removed_transactions)) + Ok(node_id) } fn can_store_transaction( &self, transaction: &PoolTransaction, dependencies: &[Self::StorageIndex], - collided_transactions: &[Self::StorageIndex], ) -> Result<(), Error> { for node_id in dependencies.iter() { let Some(dependency_node) = self.graph.node_weight(*node_id) else { @@ -438,6 +427,26 @@ impl Storage for GraphStorage { self.get_dependents_inner(index) } + fn is_in_dependencies_subtrees( + &self, + index: Self::StorageIndex, + transactions: &[Self::StorageIndex], + ) -> Result { + let mut already_visited = HashSet::new(); + let mut to_check = transactions.to_vec(); + while let Some(node_id) = to_check.pop() { + if already_visited.contains(&node_id) { + continue; + } + if node_id == index { + return Ok(true); + } + already_visited.insert(node_id); + to_check.extend(self.get_dependencies(node_id)?); + } + Ok(false) + } + fn validate_inputs( &self, transaction: &PoolTransaction, @@ -559,6 +568,13 @@ impl Storage for GraphStorage { }) } + fn remove_transaction_and_dependents_subtree( + &mut self, + index: Self::StorageIndex, + ) -> Result { + self.remove_node_and_dependent_sub_graph(index) + } + fn count(&self) -> usize { self.graph.node_count() } diff --git a/crates/services/txpool_v2/src/storage/mod.rs b/crates/services/txpool_v2/src/storage/mod.rs index 8987cd4a62c..2bab7c56aea 100644 --- a/crates/services/txpool_v2/src/storage/mod.rs +++ b/crates/services/txpool_v2/src/storage/mod.rs @@ -5,7 +5,6 @@ use std::{ }; use crate::{ - collision_manager::CollisionReason, error::Error, ports::TxPoolPersistentStorage, }; @@ -21,6 +20,8 @@ pub struct StorageData { pub dependents_cumulative_tip: u64, /// The cumulative gas of a transaction and all of its children. pub dependents_cumulative_gas: u64, + /// The cumulative of space used by a transaction and all of its children. + pub dependents_cumulative_bytes_size: usize, /// Number of dependents pub number_txs_in_chain: usize, } @@ -33,14 +34,13 @@ pub trait Storage { /// The index type used in the storage and allow other components to reference transactions. type StorageIndex: Copy + Debug; - /// Store a transaction in the storage according to the dependencies and collisions. - /// Returns the index of the stored transaction and the transactions that were removed from the storage in favor of the new transaction. + /// Store a transaction in the storage according to the dependencies. + /// Returns the index of the stored transaction. fn store_transaction( &mut self, transaction: PoolTransaction, dependencies: Vec, - collided_transactions: &[Self::StorageIndex], - ) -> Result<(Self::StorageIndex, RemovedTransactions), Error>; + ) -> Result; /// Check if a transaction could be stored in the storage. This shouldn't be expected to be called before store_transaction. /// Its just a way to perform some checks without storing the transaction. @@ -48,7 +48,6 @@ pub trait Storage { &self, transaction: &PoolTransaction, dependencies: &[Self::StorageIndex], - collided_transactions: &[Self::StorageIndex], ) -> Result<(), Error>; /// Get the storage data by its index. @@ -66,6 +65,18 @@ pub trait Storage { index: Self::StorageIndex, ) -> Result, Error>; + /// Get less worth subtree roots. + fn get_worst_ratio_tip_gas_subtree_roots( + &self, + ) -> Result, Error>; + + /// Verify if an id is in the dependencies subtree of another ids + fn is_in_dependencies_subtrees( + &self, + index: Self::StorageIndex, + transactions: &[Self::StorageIndex], + ) -> Result; + /// Validate inputs of a transaction. fn validate_inputs( &self, @@ -88,6 +99,12 @@ pub trait Storage { index: Self::StorageIndex, ) -> Result; + /// Remove a transaction along with its dependents subtree. + fn remove_transaction_and_dependents_subtree( + &mut self, + index: Self::StorageIndex, + ) -> Result; + /// Count the number of transactions in the storage. fn count(&self) -> usize; } diff --git a/crates/services/txpool_v2/src/tests/pool.rs b/crates/services/txpool_v2/src/tests/pool.rs index 410244362ca..10121cc8964 100644 --- a/crates/services/txpool_v2/src/tests/pool.rs +++ b/crates/services/txpool_v2/src/tests/pool.rs @@ -1,7 +1,10 @@ #![allow(non_snake_case)] use crate::{ - config::Config, + config::{ + Config, + PoolLimits, + }, error::Error, ports::WasmValidityError, tests::{ @@ -456,7 +459,11 @@ async fn insert_more_priced_tx2_removes_tx1_and_more_priced_tx3_removes_tx2() { #[tokio::test] async fn insert__tx_limit_hit() { let mut universe = TestPoolUniverse::default().config(Config { - max_txs: 1, + pool_limits: PoolLimits { + max_txs: 1, + max_bytes_size: 1000000000, + max_gas: 100_000_000_000, + }, ..Default::default() }); universe.build_pool(); From 1c59fc760edbd6dada1a99b1d13c0927a13d95d7 Mon Sep 17 00:00:00 2001 From: AurelienFT Date: Mon, 23 Sep 2024 16:53:14 +0200 Subject: [PATCH 14/28] Simplified insert --- crates/services/txpool_v2/src/pool.rs | 5 +++- .../services/txpool_v2/src/storage/graph.rs | 25 ++++++++++++++++++- crates/services/txpool_v2/src/storage/mod.rs | 2 +- 3 files changed, 29 insertions(+), 3 deletions(-) diff --git a/crates/services/txpool_v2/src/pool.rs b/crates/services/txpool_v2/src/pool.rs index 2907d25e7d6..8ffa4bf0840 100644 --- a/crates/services/txpool_v2/src/pool.rs +++ b/crates/services/txpool_v2/src/pool.rs @@ -241,7 +241,10 @@ where let mut gas_left = self.current_gas; let mut bytes_left = self.current_bytes_size; let mut txs_left = self.storage.count(); - let mut sorted_txs = self.storage.get_worst_ratio_tip_gas_subtree_roots()?; + let mut sorted_txs = self + .storage + .get_worst_ratio_tip_gas_subtree_roots()? + .into_iter(); while gas_left > self.config.pool_limits.max_gas || bytes_left > self.config.pool_limits.max_bytes_size || txs_left > self.config.pool_limits.max_txs diff --git a/crates/services/txpool_v2/src/storage/graph.rs b/crates/services/txpool_v2/src/storage/graph.rs index 1ef35b906be..1a95d3a642a 100644 --- a/crates/services/txpool_v2/src/storage/graph.rs +++ b/crates/services/txpool_v2/src/storage/graph.rs @@ -1,6 +1,6 @@ use std::{ collections::{ - BTreeMap, + BTreeSet, HashMap, HashSet, }, @@ -427,6 +427,29 @@ impl Storage for GraphStorage { self.get_dependents_inner(index) } + // Maybe change in the future as it can be very costly. + fn get_worst_ratio_tip_gas_subtree_roots( + &self, + ) -> Result, Error> { + let mut sorted_nodes: BTreeSet<(Ratio, NodeIndex)> = BTreeSet::new(); + for node_id in self.graph.node_indices() { + let Some(node) = self.graph.node_weight(node_id) else { + return Err(Error::Storage(format!( + "Node with id {:?} not found", + node_id + ))); + }; + sorted_nodes.insert(( + Ratio::new( + node.dependents_cumulative_tip, + node.dependents_cumulative_gas, + ), + node_id, + )); + } + Ok(sorted_nodes.iter().map(|(_, node_id)| *node_id).collect()) + } + fn is_in_dependencies_subtrees( &self, index: Self::StorageIndex, diff --git a/crates/services/txpool_v2/src/storage/mod.rs b/crates/services/txpool_v2/src/storage/mod.rs index 2bab7c56aea..9d9b5c40f62 100644 --- a/crates/services/txpool_v2/src/storage/mod.rs +++ b/crates/services/txpool_v2/src/storage/mod.rs @@ -68,7 +68,7 @@ pub trait Storage { /// Get less worth subtree roots. fn get_worst_ratio_tip_gas_subtree_roots( &self, - ) -> Result, Error>; + ) -> Result, Error>; /// Verify if an id is in the dependencies subtree of another ids fn is_in_dependencies_subtrees( From e859b60656715008d2e337e1af031d1ea9cc6e79 Mon Sep 17 00:00:00 2001 From: AurelienFT Date: Mon, 23 Sep 2024 16:58:17 +0200 Subject: [PATCH 15/28] Fix collision system to allow multiple collision to the same tx --- .../txpool_v2/src/collision_manager/basic.rs | 50 +++++++++++-------- 1 file changed, 29 insertions(+), 21 deletions(-) diff --git a/crates/services/txpool_v2/src/collision_manager/basic.rs b/crates/services/txpool_v2/src/collision_manager/basic.rs index 64cf222123e..592845a79a6 100644 --- a/crates/services/txpool_v2/src/collision_manager/basic.rs +++ b/crates/services/txpool_v2/src/collision_manager/basic.rs @@ -37,7 +37,7 @@ use crate::{ use super::CollisionManager; pub trait BasicCollisionManagerStorage { - type StorageIndex: Copy + Debug; + type StorageIndex: Copy + Debug + PartialEq + Eq; fn get(&self, index: &Self::StorageIndex) -> Result<&StorageData, Error>; } @@ -79,11 +79,13 @@ impl BasicCollisionManager { if let PoolTransaction::Blob(checked_tx, _) = tx { let blob_id = checked_tx.transaction().blob_id(); if let Some(state) = self.blobs_users.get(blob_id) { - if collision.is_some() { - return Err(Error::Collided(format!( - "Transaction collides with other transactions: {:?}", - collision - ))); + if let Some(collision) = collision { + if state != &collision { + return Err(Error::Collided(format!( + "Transaction collides with other transactions: {:?}", + collision + ))); + } } collision = Some(*state); } @@ -94,11 +96,13 @@ impl BasicCollisionManager { | Input::CoinPredicate(CoinPredicate { utxo_id, .. }) => { // Check if the utxo is already spent by another transaction in the pool if let Some(tx_id) = self.coins_spenders.get(utxo_id) { - if collision.is_some() { - return Err(Error::Collided(format!( - "Transaction collides with other transactions: {:?}", - collision - ))); + if let Some(collision) = collision { + if tx_id != &collision { + return Err(Error::Collided(format!( + "Transaction collides with other transactions: {:?}", + collision + ))); + } } collision = Some(*tx_id); } @@ -109,11 +113,13 @@ impl BasicCollisionManager { | Input::MessageDataPredicate(MessageDataPredicate { nonce, .. }) => { // Check if the message is already spent by another transaction in the pool if let Some(tx_id) = self.messages_spenders.get(nonce) { - if collision.is_some() { - return Err(Error::Collided(format!( - "Transaction collides with other transactions: {:?}", - collision - ))); + if let Some(collision) = collision { + if tx_id != &collision { + return Err(Error::Collided(format!( + "Transaction collides with other transactions: {:?}", + collision + ))); + } } collision = Some(*tx_id); } @@ -127,11 +133,13 @@ impl BasicCollisionManager { if let Output::ContractCreated { contract_id, .. } = output { // Check if the contract is already created by another transaction in the pool if let Some(tx_id) = self.contracts_creators.get(contract_id) { - if collision.is_some() { - return Err(Error::Collided(format!( - "Transaction collides with other transactions: {:?}", - collision - ))); + if let Some(collision) = collision { + if tx_id != &collision { + return Err(Error::Collided(format!( + "Transaction collides with other transactions: {:?}", + collision + ))); + } } collision = Some(*tx_id); } From 1f8aaf883a72a1cafa4e9d71758d6583b33490d0 Mon Sep 17 00:00:00 2001 From: AurelienFT Date: Mon, 23 Sep 2024 17:17:58 +0200 Subject: [PATCH 16/28] Fix some node that wasn't removed. --- .../txpool_v2/src/collision_manager/basic.rs | 2 +- crates/services/txpool_v2/src/pool.rs | 15 +++++++--- .../services/txpool_v2/src/storage/graph.rs | 6 +++- crates/services/txpool_v2/src/tests/pool.rs | 30 ++----------------- 4 files changed, 19 insertions(+), 34 deletions(-) diff --git a/crates/services/txpool_v2/src/collision_manager/basic.rs b/crates/services/txpool_v2/src/collision_manager/basic.rs index 592845a79a6..fe60ee8f621 100644 --- a/crates/services/txpool_v2/src/collision_manager/basic.rs +++ b/crates/services/txpool_v2/src/collision_manager/basic.rs @@ -159,8 +159,8 @@ impl BasicCollisionManager { .get(&collision) .expect("Transaction always should exist in storage"); let colliding_tx_ratio = Ratio::new( - colliding_tx.dependents_cumulative_gas, colliding_tx.dependents_cumulative_tip, + colliding_tx.dependents_cumulative_gas, ); new_tx_ratio > colliding_tx_ratio } diff --git a/crates/services/txpool_v2/src/pool.rs b/crates/services/txpool_v2/src/pool.rs index 8ffa4bf0840..8ab0ad6e0a0 100644 --- a/crates/services/txpool_v2/src/pool.rs +++ b/crates/services/txpool_v2/src/pool.rs @@ -115,6 +115,12 @@ where let removed = self.storage.remove_transaction_and_dependents_subtree(tx)?; removed_transactions.extend(removed); } + if let Some(collision) = collision { + removed_transactions.extend( + self.storage + .remove_transaction_and_dependents_subtree(collision)? + ); + } let has_dependencies = !dependencies.is_empty(); let storage_id = self.storage.store_transaction(tx, dependencies)?; self.tx_id_to_storage_id.insert(tx_id, storage_id); @@ -147,7 +153,7 @@ where self.config.utxo_validation, )?; let dependencies = self.storage.collect_transaction_dependencies(tx)?; - self.check_pool_size_available(&tx, &collision, &dependencies)?; + self.check_pool_size_available(tx, &collision, &dependencies)?; self.storage.can_store_transaction(tx, &dependencies); Ok(()) } @@ -195,13 +201,14 @@ where /// Check if the pool has enough space to store a transaction. /// It will try to see if we can free some space dependending on defined rules - /// Returns an error if the pool is full. - /// If the pool is not full, it will return the list of transactions that must be removed from the pool along all of their dependent subtree + /// If the pool is not full, it will return an empty list + /// If the pool is full, it will return the list of transactions that must be removed from the pool along all of their dependent subtree + /// If the pool is full and we can't make enough space by removing transactions, it will return an error fn check_pool_size_available( &self, tx: &PoolTransaction, collision: &Option, - dependencies: &Vec, + dependencies: &[S::StorageIndex], ) -> Result, Error> { if self.current_gas < self.config.pool_limits.max_gas && self.current_bytes_size < self.config.pool_limits.max_bytes_size diff --git a/crates/services/txpool_v2/src/storage/graph.rs b/crates/services/txpool_v2/src/storage/graph.rs index 1a95d3a642a..2e7e5c11120 100644 --- a/crates/services/txpool_v2/src/storage/graph.rs +++ b/crates/services/txpool_v2/src/storage/graph.rs @@ -91,7 +91,7 @@ impl GraphStorage { let gas_removed = root.dependents_cumulative_gas; let tip_removed = root.dependents_cumulative_tip; let dependencies: Vec = self.get_dependencies(root_id)?.collect(); - let removed = self.remove_dependent_sub_graph(root_id)?; + let mut removed = self.remove_dependent_sub_graph(root_id)?; let mut already_visited = HashSet::new(); for dependency in dependencies { if already_visited.contains(&dependency) { @@ -105,6 +105,10 @@ impl GraphStorage { &mut already_visited, )?; } + if let Some(removed_tx) = self.graph.remove_node(root_id) { + self.clear_cache(&removed_tx.transaction.outputs(), &removed_tx.transaction.id())?; + removed.push(removed_tx.transaction); + } Ok(removed) } fn reduce_dependencies_cumulative_gas_tip_and_chain_count( diff --git a/crates/services/txpool_v2/src/tests/pool.rs b/crates/services/txpool_v2/src/tests/pool.rs index 10121cc8964..294cafac9fd 100644 --- a/crates/services/txpool_v2/src/tests/pool.rs +++ b/crates/services/txpool_v2/src/tests/pool.rs @@ -456,32 +456,6 @@ async fn insert_more_priced_tx2_removes_tx1_and_more_priced_tx3_removes_tx2() { assert_eq!(removed_txs[0].id(), tx2_id); } -#[tokio::test] -async fn insert__tx_limit_hit() { - let mut universe = TestPoolUniverse::default().config(Config { - pool_limits: PoolLimits { - max_txs: 1, - max_bytes_size: 1000000000, - max_gas: 100_000_000_000, - }, - ..Default::default() - }); - universe.build_pool(); - - // Given - let tx1 = universe.build_script_transaction(None, None, 0); - let tx2 = universe.build_script_transaction(None, None, 0); - - // When - let result1 = universe.verify_and_insert(tx1).await; - let result2 = universe.verify_and_insert(tx2).await; - - // Then - assert!(result1.is_ok()); - let err = result2.unwrap_err(); - assert!(matches!(err, Error::NotInsertedLimitHit)); -} - #[tokio::test] async fn insert__dependency_chain_length_hit() { let mut universe = TestPoolUniverse::default().config(Config { @@ -802,8 +776,8 @@ async fn insert_tx_tip_higher_than_another_tx_with_same_message_id() { let mut universe = TestPoolUniverse::default(); universe.build_pool(); - let tip_high = 2u64; let tip_low = 1u64; + let tip_high = 2u64; let (message, conflicting_message_input) = create_message_predicate_from_message(10_000, 0); universe.database_mut().insert_message(message.clone()); @@ -1126,7 +1100,7 @@ async fn insert__if_tx3_depends_and_collides_with_tx2() { let err = universe.verify_and_insert(tx3).await.unwrap_err(); // Then - assert!(matches!(err, Error::Collided(_))); + assert!(matches!(err, Error::NotInsertedCollisionIsDependency)); } #[tokio::test] From c7b3a298554e38f04658253537d3d3ac59f985ee Mon Sep 17 00:00:00 2001 From: AurelienFT Date: Mon, 23 Sep 2024 23:45:15 +0200 Subject: [PATCH 17/28] format --- crates/services/txpool_v2/src/pool.rs | 2 +- crates/services/txpool_v2/src/storage/graph.rs | 5 ++++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/crates/services/txpool_v2/src/pool.rs b/crates/services/txpool_v2/src/pool.rs index 8ab0ad6e0a0..427a39a7d88 100644 --- a/crates/services/txpool_v2/src/pool.rs +++ b/crates/services/txpool_v2/src/pool.rs @@ -118,7 +118,7 @@ where if let Some(collision) = collision { removed_transactions.extend( self.storage - .remove_transaction_and_dependents_subtree(collision)? + .remove_transaction_and_dependents_subtree(collision)?, ); } let has_dependencies = !dependencies.is_empty(); diff --git a/crates/services/txpool_v2/src/storage/graph.rs b/crates/services/txpool_v2/src/storage/graph.rs index 2e7e5c11120..97ba1828a0c 100644 --- a/crates/services/txpool_v2/src/storage/graph.rs +++ b/crates/services/txpool_v2/src/storage/graph.rs @@ -106,7 +106,10 @@ impl GraphStorage { )?; } if let Some(removed_tx) = self.graph.remove_node(root_id) { - self.clear_cache(&removed_tx.transaction.outputs(), &removed_tx.transaction.id())?; + self.clear_cache( + &removed_tx.transaction.outputs(), + &removed_tx.transaction.id(), + )?; removed.push(removed_tx.transaction); } Ok(removed) From 0ebfc5c9b6406fc65104cfa9850b2964e3f798b4 Mon Sep 17 00:00:00 2001 From: AurelienFT Date: Tue, 24 Sep 2024 00:20:15 +0200 Subject: [PATCH 18/28] fix max size algo and add tests --- crates/services/txpool_v2/src/pool.rs | 40 +++++-- .../services/txpool_v2/src/storage/graph.rs | 2 +- crates/services/txpool_v2/src/tests/pool.rs | 103 +++++++++++++++++- 3 files changed, 136 insertions(+), 9 deletions(-) diff --git a/crates/services/txpool_v2/src/pool.rs b/crates/services/txpool_v2/src/pool.rs index 427a39a7d88..f307ebe6c8c 100644 --- a/crates/services/txpool_v2/src/pool.rs +++ b/crates/services/txpool_v2/src/pool.rs @@ -9,6 +9,7 @@ use fuel_core_types::{ fuel_vm::checked_transaction::Checked, services::txpool::PoolTransaction, }; +use num_rational::Ratio; use tracing::instrument; use crate::{ @@ -92,6 +93,8 @@ where .latest_view() .map_err(|e| Error::Database(format!("{:?}", e)))?; let tx_id = tx.id(); + let gas = tx.max_gas(); + let bytes_size = tx.metered_bytes_size(); self.config.black_list.check_blacklisting(&tx)?; Self::check_blob_does_not_exist(&tx, &latest_view)?; self.storage @@ -124,6 +127,8 @@ where let has_dependencies = !dependencies.is_empty(); let storage_id = self.storage.store_transaction(tx, dependencies)?; self.tx_id_to_storage_id.insert(tx_id, storage_id); + self.current_gas = self.current_gas.saturating_add(gas); + self.current_bytes_size = self.current_bytes_size.saturating_add(bytes_size); // No dependencies directly in the graph and the sorted transactions if !has_dependencies { self.selection_algorithm @@ -200,7 +205,7 @@ where } /// Check if the pool has enough space to store a transaction. - /// It will try to see if we can free some space dependending on defined rules + /// It will try to see if we can free some space depending on defined rules /// If the pool is not full, it will return an empty list /// If the pool is full, it will return the list of transactions that must be removed from the pool along all of their dependent subtree /// If the pool is full and we can't make enough space by removing transactions, it will return an error @@ -210,9 +215,12 @@ where collision: &Option, dependencies: &[S::StorageIndex], ) -> Result, Error> { - if self.current_gas < self.config.pool_limits.max_gas - && self.current_bytes_size < self.config.pool_limits.max_bytes_size - && self.storage.count() < self.config.pool_limits.max_txs + let tx_gas = tx.max_gas(); + let bytes_size = tx.metered_bytes_size(); + if self.current_gas.saturating_add(tx_gas) <= self.config.pool_limits.max_gas + && self.current_bytes_size.saturating_add(bytes_size) + <= self.config.pool_limits.max_bytes_size + && self.storage.count().saturating_add(1) <= self.config.pool_limits.max_txs { return Ok(vec![]); } @@ -245,9 +253,10 @@ where // Here the transaction has no dependencies and no collision which means that it's an executable transaction // and we want to make space for it let mut removed_transactions = vec![]; - let mut gas_left = self.current_gas; - let mut bytes_left = self.current_bytes_size; - let mut txs_left = self.storage.count(); + let mut gas_left = self.current_gas.saturating_add(tx_gas); + let mut bytes_left = self.current_bytes_size.saturating_add(bytes_size); + let mut txs_left = self.storage.count().saturating_add(1); + let current_ratio = Ratio::new(tx.tip(), tx_gas); let mut sorted_txs = self .storage .get_worst_ratio_tip_gas_subtree_roots()? @@ -258,6 +267,19 @@ where { let storage_id = sorted_txs.next().ok_or(Error::NotInsertedLimitHit)?; let storage_data = self.storage.get(&storage_id)?; + let mut dependencies = self.storage.get_dependencies(storage_id)?; + match dependencies.next() { + Some(_) => {} + None => { + let stored_ratio = Ratio::new( + storage_data.transaction.tip(), + storage_data.transaction.max_gas(), + ); + if stored_ratio >= current_ratio { + return Err(Error::NotInsertedLimitHit); + } + } + } gas_left = gas_left.saturating_sub(storage_data.dependents_cumulative_gas); bytes_left = bytes_left.saturating_sub(storage_data.dependents_cumulative_bytes_size); @@ -291,6 +313,10 @@ where self.collision_manager.on_removed_transaction(tx)?; self.selection_algorithm.on_removed_transaction(tx)?; self.tx_id_to_storage_id.remove(&tx.id()); + self.current_gas = self.current_gas.saturating_sub(tx.max_gas()); + self.current_bytes_size = self + .current_bytes_size + .saturating_sub(tx.metered_bytes_size()); } Ok(()) } diff --git a/crates/services/txpool_v2/src/storage/graph.rs b/crates/services/txpool_v2/src/storage/graph.rs index 97ba1828a0c..fdc73bf73bf 100644 --- a/crates/services/txpool_v2/src/storage/graph.rs +++ b/crates/services/txpool_v2/src/storage/graph.rs @@ -107,7 +107,7 @@ impl GraphStorage { } if let Some(removed_tx) = self.graph.remove_node(root_id) { self.clear_cache( - &removed_tx.transaction.outputs(), + removed_tx.transaction.outputs(), &removed_tx.transaction.id(), )?; removed.push(removed_tx.transaction); diff --git a/crates/services/txpool_v2/src/tests/pool.rs b/crates/services/txpool_v2/src/tests/pool.rs index 294cafac9fd..68841e21def 100644 --- a/crates/services/txpool_v2/src/tests/pool.rs +++ b/crates/services/txpool_v2/src/tests/pool.rs @@ -33,6 +33,7 @@ use fuel_core_types::{ BlobId, BlobIdExt, Bytes32, + Chargeable, ConsensusParameters, Contract, Input, @@ -47,7 +48,11 @@ use fuel_core_types::{ }, fuel_types::ChainId, fuel_vm::{ - checked_transaction::CheckError, + checked_transaction::{ + CheckError, + CheckedTransaction, + IntoChecked, + }, PredicateVerificationFailed, }, }; @@ -456,6 +461,102 @@ async fn insert_more_priced_tx2_removes_tx1_and_more_priced_tx3_removes_tx2() { assert_eq!(removed_txs[0].id(), tx2_id); } +#[tokio::test] +async fn insert__tx_limit_hit() { + let mut universe = TestPoolUniverse::default().config(Config { + pool_limits: PoolLimits { + max_txs: 1, + max_bytes_size: 1000000000, + max_gas: 100_000_000_000, + }, + ..Default::default() + }); + universe.build_pool(); + + // Given + let tx1 = universe.build_script_transaction(None, None, 10); + let tx2 = universe.build_script_transaction(None, None, 0); + + // When + let result1 = universe.verify_and_insert(tx1).await; + let result2 = universe.verify_and_insert(tx2).await; + + // Then + assert!(result1.is_ok()); + let err = result2.unwrap_err(); + assert!(matches!(err, Error::NotInsertedLimitHit)); +} + +#[tokio::test] +async fn insert__tx_gas_limit() { + // Given + let mut universe = TestPoolUniverse::default(); + let tx1 = universe.build_script_transaction(None, None, 10); + let checked_tx: CheckedTransaction = tx1 + .clone() + .into_checked_basic(Default::default(), &ConsensusParameters::default()) + .unwrap() + .into(); + let max_gas = match checked_tx { + CheckedTransaction::Script(tx) => tx.metadata().max_gas, + _ => panic!("Expected script transaction"), + }; + let tx2 = universe.build_script_transaction(None, None, 0); + universe = universe.config(Config { + pool_limits: PoolLimits { + max_txs: 10000, + max_bytes_size: 1000000000, + max_gas: max_gas + 10, + }, + ..Default::default() + }); + universe.build_pool(); + + // When + let result1 = universe.verify_and_insert(tx1).await; + let result2 = universe.verify_and_insert(tx2).await; + + // Then + assert!(result1.is_ok()); + let err = result2.unwrap_err(); + assert!(matches!(err, Error::NotInsertedLimitHit)); +} + +#[tokio::test] +async fn insert__tx_bytes_limit() { + // Given + let mut universe = TestPoolUniverse::default(); + let tx1 = universe.build_script_transaction(None, None, 0); + let checked_tx: CheckedTransaction = tx1 + .clone() + .into_checked_basic(Default::default(), &ConsensusParameters::default()) + .unwrap() + .into(); + let max_bytes = match checked_tx { + CheckedTransaction::Script(tx) => tx.transaction().metered_bytes_size(), + _ => panic!("Expected script transaction"), + }; + let tx2 = universe.build_script_transaction(None, None, 0); + universe = universe.config(Config { + pool_limits: PoolLimits { + max_txs: 10000, + max_bytes_size: max_bytes + 10, + max_gas: 100_000_000_000, + }, + ..Default::default() + }); + universe.build_pool(); + + // When + let result1 = universe.verify_and_insert(tx1).await; + let result2 = universe.verify_and_insert(tx2).await; + + // Then + assert!(result1.is_ok()); + let err = result2.unwrap_err(); + assert!(matches!(err, Error::NotInsertedLimitHit)); +} + #[tokio::test] async fn insert__dependency_chain_length_hit() { let mut universe = TestPoolUniverse::default().config(Config { From 82c9dd97c82ecdd6af1efd6b9cc6e5761f8c2dda Mon Sep 17 00:00:00 2001 From: AurelienFT <32803821+AurelienFT@users.noreply.github.com> Date: Thu, 26 Sep 2024 00:14:45 +0200 Subject: [PATCH 19/28] Update crates/services/txpool_v2/src/config.rs Co-authored-by: Green Baneling --- crates/services/txpool_v2/src/config.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/services/txpool_v2/src/config.rs b/crates/services/txpool_v2/src/config.rs index 592b11a054f..913e61a08cc 100644 --- a/crates/services/txpool_v2/src/config.rs +++ b/crates/services/txpool_v2/src/config.rs @@ -148,7 +148,7 @@ pub struct PoolLimits { #[derive(Clone)] pub struct HeavyWorkConfig { /// Maximum of threads for managing verifications/insertions. - pub number_threads_verif_insert_transactions: usize, + pub number_threads_to_verify_transactions: usize, /// Maximum of tasks in the heavy async processing queue. pub number_pending_tasks_threads_verif_insert_transactions: usize, } From 70a45e926765b4f1a393795f2e87d6a4fd1388a4 Mon Sep 17 00:00:00 2001 From: AurelienFT <32803821+AurelienFT@users.noreply.github.com> Date: Thu, 26 Sep 2024 00:14:58 +0200 Subject: [PATCH 20/28] Update crates/services/txpool_v2/src/config.rs Co-authored-by: Green Baneling --- crates/services/txpool_v2/src/config.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/services/txpool_v2/src/config.rs b/crates/services/txpool_v2/src/config.rs index 913e61a08cc..24e12894455 100644 --- a/crates/services/txpool_v2/src/config.rs +++ b/crates/services/txpool_v2/src/config.rs @@ -150,7 +150,7 @@ pub struct HeavyWorkConfig { /// Maximum of threads for managing verifications/insertions. pub number_threads_to_verify_transactions: usize, /// Maximum of tasks in the heavy async processing queue. - pub number_pending_tasks_threads_verif_insert_transactions: usize, + pub size_of_verification_queue: usize, } #[cfg(test)] From 677a130388ace73011321265f8288b24f668cd43 Mon Sep 17 00:00:00 2001 From: AurelienFT Date: Thu, 26 Sep 2024 01:27:59 +0200 Subject: [PATCH 21/28] Change collision manager and add more info in error --- .../txpool_v2/src/collision_manager/basic.rs | 160 +++++++++--------- .../txpool_v2/src/collision_manager/mod.rs | 27 ++- crates/services/txpool_v2/src/config.rs | 4 +- crates/services/txpool_v2/src/error.rs | 29 +++- crates/services/txpool_v2/src/pool.rs | 84 +++++---- crates/services/txpool_v2/src/service.rs | 6 +- crates/services/txpool_v2/src/tests/pool.rs | 24 ++- 7 files changed, 196 insertions(+), 138 deletions(-) diff --git a/crates/services/txpool_v2/src/collision_manager/basic.rs b/crates/services/txpool_v2/src/collision_manager/basic.rs index fe60ee8f621..1b020f5fbeb 100644 --- a/crates/services/txpool_v2/src/collision_manager/basic.rs +++ b/crates/services/txpool_v2/src/collision_manager/basic.rs @@ -1,6 +1,7 @@ use std::{ collections::HashMap, fmt::Debug, + hash::Hash, }; use fuel_core_types::{ @@ -30,14 +31,17 @@ use fuel_core_types::{ use num_rational::Ratio; use crate::{ - error::Error, + error::{ + CollisionReason, + Error, + }, storage::StorageData, }; use super::CollisionManager; pub trait BasicCollisionManagerStorage { - type StorageIndex: Copy + Debug + PartialEq + Eq; + type StorageIndex: Copy + Debug + PartialEq + Eq + Hash; fn get(&self, index: &Self::StorageIndex) -> Result<&StorageData, Error>; } @@ -71,40 +75,47 @@ impl Default for BasicCollisionManager { } impl BasicCollisionManager { - fn gather_colliding_tx( + fn is_better_than_collision( &self, tx: &PoolTransaction, - ) -> Result, Error> { - let mut collision: Option = None; - if let PoolTransaction::Blob(checked_tx, _) = tx { + collision: S::StorageIndex, + storage: &S, + ) -> bool { + let new_tx_ratio = Ratio::new(tx.tip(), tx.max_gas()); + let colliding_tx = storage + .get(&collision) + .expect("Transaction always should exist in storage"); + let colliding_tx_ratio = Ratio::new( + colliding_tx.dependents_cumulative_tip, + colliding_tx.dependents_cumulative_gas, + ); + new_tx_ratio > colliding_tx_ratio + } +} + +impl CollisionManager for BasicCollisionManager { + type Storage = S; + type StorageIndex = S::StorageIndex; + + fn collect_colliding_transactions( + &self, + transaction: &PoolTransaction, + ) -> Result>, Error> { + let mut collisions = HashMap::new(); + if let PoolTransaction::Blob(checked_tx, _) = transaction { let blob_id = checked_tx.transaction().blob_id(); if let Some(state) = self.blobs_users.get(blob_id) { - if let Some(collision) = collision { - if state != &collision { - return Err(Error::Collided(format!( - "Transaction collides with other transactions: {:?}", - collision - ))); - } - } - collision = Some(*state); + collisions.insert(*state, vec![CollisionReason::Blob(*blob_id)]); } } - for input in tx.inputs() { + for input in transaction.inputs() { match input { Input::CoinSigned(CoinSigned { utxo_id, .. }) | Input::CoinPredicate(CoinPredicate { utxo_id, .. }) => { // Check if the utxo is already spent by another transaction in the pool - if let Some(tx_id) = self.coins_spenders.get(utxo_id) { - if let Some(collision) = collision { - if tx_id != &collision { - return Err(Error::Collided(format!( - "Transaction collides with other transactions: {:?}", - collision - ))); - } - } - collision = Some(*tx_id); + if let Some(storage_id) = self.coins_spenders.get(utxo_id) { + let entry = collisions.entry(*storage_id).or_default(); + entry.push(CollisionReason::Utxo(*utxo_id)); } } Input::MessageCoinSigned(MessageCoinSigned { nonce, .. }) @@ -112,16 +123,9 @@ impl BasicCollisionManager { | Input::MessageDataSigned(MessageDataSigned { nonce, .. }) | Input::MessageDataPredicate(MessageDataPredicate { nonce, .. }) => { // Check if the message is already spent by another transaction in the pool - if let Some(tx_id) = self.messages_spenders.get(nonce) { - if let Some(collision) = collision { - if tx_id != &collision { - return Err(Error::Collided(format!( - "Transaction collides with other transactions: {:?}", - collision - ))); - } - } - collision = Some(*tx_id); + if let Some(storage_id) = self.messages_spenders.get(nonce) { + let entry = collisions.entry(*storage_id).or_default(); + entry.push(CollisionReason::Message(*nonce)); } } // No collision for contract inputs @@ -129,65 +133,55 @@ impl BasicCollisionManager { } } - for output in tx.outputs() { + for output in transaction.outputs() { if let Output::ContractCreated { contract_id, .. } = output { // Check if the contract is already created by another transaction in the pool - if let Some(tx_id) = self.contracts_creators.get(contract_id) { - if let Some(collision) = collision { - if tx_id != &collision { - return Err(Error::Collided(format!( - "Transaction collides with other transactions: {:?}", - collision - ))); - } - } - collision = Some(*tx_id); + if let Some(storage_id) = self.contracts_creators.get(contract_id) { + let entry = collisions.entry(*storage_id).or_default(); + entry.push(CollisionReason::ContractCreation(*contract_id)); } } } - Ok(collision) - } - - fn is_better_than_collision( - &self, - tx: &PoolTransaction, - collision: S::StorageIndex, - storage: &S, - ) -> bool { - let new_tx_ratio = Ratio::new(tx.tip(), tx.max_gas()); - let colliding_tx = storage - .get(&collision) - .expect("Transaction always should exist in storage"); - let colliding_tx_ratio = Ratio::new( - colliding_tx.dependents_cumulative_tip, - colliding_tx.dependents_cumulative_gas, - ); - new_tx_ratio > colliding_tx_ratio + Ok(collisions) } -} -impl CollisionManager for BasicCollisionManager { - type Storage = S; - type StorageIndex = S::StorageIndex; - - fn collect_colliding_transaction( + /// Rules: + // A transaction with dependencies can collide only with one other transaction if it is less worth it + // A transaction without dependencies can collide with multiple transaction if they are less worth it + fn can_store_transaction( &self, transaction: &PoolTransaction, - storage: &S, - ) -> Result, Error> { - let collision = self.gather_colliding_tx(transaction)?; - if let Some(collision) = collision { - if self.is_better_than_collision(transaction, collision, storage) { - Ok(Some(collision)) - } else { - Err(Error::Collided(format!( - "Transaction collides with other transactions: {:?}", - collision - ))) + has_dependencies: bool, + colliding_transactions: &HashMap>, + storage: &Self::Storage, + ) -> Result<(), CollisionReason> { + if colliding_transactions.is_empty() { + return Ok(()); + } + if has_dependencies { + if colliding_transactions.len() > 1 { + return Err(CollisionReason::MultipleCollisions); + } + let (collision, reason) = colliding_transactions.iter().next().unwrap(); + if !self.is_better_than_collision(transaction, *collision, storage) { + if let Some(reason) = reason.first() { + return Err(reason.clone()); + } else { + return Err(CollisionReason::Unknown); + } } } else { - Ok(None) + for (collision, reason) in colliding_transactions.iter() { + if !self.is_better_than_collision(transaction, *collision, storage) { + if let Some(reason) = reason.first() { + return Err(reason.clone()); + } else { + return Err(CollisionReason::Unknown); + } + } + } } + Ok(()) } fn on_stored_transaction( diff --git a/crates/services/txpool_v2/src/collision_manager/mod.rs b/crates/services/txpool_v2/src/collision_manager/mod.rs index b8a81b702ea..5fcc5409d63 100644 --- a/crates/services/txpool_v2/src/collision_manager/mod.rs +++ b/crates/services/txpool_v2/src/collision_manager/mod.rs @@ -1,5 +1,8 @@ use std::{ - collections::HashSet, + collections::{ + HashMap, + HashSet, + }, fmt::Debug, }; @@ -13,7 +16,10 @@ use fuel_core_types::{ services::txpool::PoolTransaction, }; -use crate::error::Error; +use crate::error::{ + CollisionReason, + Error, +}; pub mod basic; @@ -24,14 +30,21 @@ pub trait CollisionManager { type StorageIndex; /// Collect the transaction that collide with the given transaction. - /// It returns an error if the transaction is less worthy than the colliding transactions. - /// It returns an error if the transaction collide with two transactions. - /// It returns the information about the collision if exists, none otherwise. - fn collect_colliding_transaction( + /// Returns a list of storage indexes of the colliding transactions and the reason of the collision. + fn collect_colliding_transactions( + &self, + transaction: &PoolTransaction, + ) -> Result>, Error>; + + /// Determine if the collisions allow the transaction to be stored. + /// Returns the reason of the collision if the transaction cannot be stored. + fn can_store_transaction( &self, transaction: &PoolTransaction, + has_dependencies: bool, + colliding_transactions: &HashMap>, storage: &Self::Storage, - ) -> Result, Error>; + ) -> Result<(), CollisionReason>; /// Inform the collision manager that a transaction was stored. fn on_stored_transaction( diff --git a/crates/services/txpool_v2/src/config.rs b/crates/services/txpool_v2/src/config.rs index 24e12894455..52d14d4234b 100644 --- a/crates/services/txpool_v2/src/config.rs +++ b/crates/services/txpool_v2/src/config.rs @@ -169,8 +169,8 @@ impl Default for Config { max_txs_ttl: Duration::from_secs(60 * 10), black_list: BlackList::default(), heavy_work: HeavyWorkConfig { - number_threads_verif_insert_transactions: 4, - number_pending_tasks_threads_verif_insert_transactions: 100, + number_threads_to_verify_transactions: 4, + size_of_verification_queue: 100, }, } } diff --git a/crates/services/txpool_v2/src/error.rs b/crates/services/txpool_v2/src/error.rs index 14e6d0a6fa3..36459d18a24 100644 --- a/crates/services/txpool_v2/src/error.rs +++ b/crates/services/txpool_v2/src/error.rs @@ -29,9 +29,8 @@ pub enum Error { fmt = "Transaction is not inserted. Transaction chain dependency is already too big" )] NotInsertedChainDependencyTooBig, - // TODO: Make more specific errors: https://github.com/FuelLabs/fuel-core/issues/2185 #[display(fmt = "Transaction collided: {_0}")] - Collided(String), + Collided(CollisionReason), #[display(fmt = "Transaction is not inserted. Collision is also a dependency")] NotInsertedCollisionIsDependency, #[display(fmt = "Utxo not found: {_0}")] @@ -92,6 +91,32 @@ pub enum Error { MintIsDisallowed, } +#[derive(Debug, Clone, derive_more::Display)] +pub enum CollisionReason { + #[display( + fmt = "Transaction with the same UTXO (id: {_0}) already exists and is more worth it" + )] + Utxo(UtxoId), + #[display( + fmt = "Transaction that create the same contract (id: {_0}) already exists and is more worth it" + )] + ContractCreation(ContractId), + #[display( + fmt = "Transaction that use the same blob (id: {_0}) already exists and is more worth it" + )] + Blob(BlobId), + #[display( + fmt = "Transaction that use the same message (id: {_0}) already exists and is more worth it" + )] + Message(Nonce), + #[display(fmt = "This transaction have an unknown collision")] + Unknown, + #[display( + fmt = "This transaction have dependencies and is colliding with multiple transactions" + )] + MultipleCollisions, +} + impl From for Error { fn from(e: CheckError) -> Self { Error::ConsensusValidity(e) diff --git a/crates/services/txpool_v2/src/pool.rs b/crates/services/txpool_v2/src/pool.rs index f307ebe6c8c..5679396e7a2 100644 --- a/crates/services/txpool_v2/src/pool.rs +++ b/crates/services/txpool_v2/src/pool.rs @@ -15,7 +15,10 @@ use tracing::instrument; use crate::{ collision_manager::CollisionManager, config::Config, - error::Error, + error::{ + CollisionReason, + Error, + }, ports::{ AtomicView, TxPoolPersistentStorage, @@ -99,32 +102,39 @@ where Self::check_blob_does_not_exist(&tx, &latest_view)?; self.storage .validate_inputs(&tx, &latest_view, self.config.utxo_validation)?; - let collision = self - .collision_manager - .collect_colliding_transaction(&tx, &self.storage)?; + let colliding_transactions = + self.collision_manager.collect_colliding_transactions(&tx)?; let dependencies = self.storage.collect_transaction_dependencies(&tx)?; - if let Some(collision) = collision { + let has_dependencies = !dependencies.is_empty(); + self.collision_manager + .can_store_transaction( + &tx, + has_dependencies, + &colliding_transactions, + &self.storage, + ) + .map_err(|e| Error::Collided(e))?; + for collision in colliding_transactions.keys() { if self .storage - .is_in_dependencies_subtrees(collision, &dependencies)? + .is_in_dependencies_subtrees(*collision, &dependencies)? { return Err(Error::NotInsertedCollisionIsDependency); } } let transactions_to_remove = - self.check_pool_size_available(&tx, &collision, &dependencies)?; + self.check_pool_size_available(&tx, &colliding_transactions, &dependencies)?; let mut removed_transactions = vec![]; for tx in transactions_to_remove { let removed = self.storage.remove_transaction_and_dependents_subtree(tx)?; removed_transactions.extend(removed); } - if let Some(collision) = collision { + for collision in colliding_transactions.keys() { removed_transactions.extend( self.storage - .remove_transaction_and_dependents_subtree(collision)?, + .remove_transaction_and_dependents_subtree(*collision)?, ); } - let has_dependencies = !dependencies.is_empty(); let storage_id = self.storage.store_transaction(tx, dependencies)?; self.tx_id_to_storage_id.insert(tx_id, storage_id); self.current_gas = self.current_gas.saturating_add(gas); @@ -149,16 +159,24 @@ where .map_err(|e| Error::Database(format!("{:?}", e)))?; self.config.black_list.check_blacklisting(tx)?; Self::check_blob_does_not_exist(tx, &persistent_storage)?; - let collision = self - .collision_manager - .collect_colliding_transaction(tx, &self.storage)?; + let colliding_transaction = + self.collision_manager.collect_colliding_transactions(tx)?; self.storage.validate_inputs( tx, &persistent_storage, self.config.utxo_validation, )?; let dependencies = self.storage.collect_transaction_dependencies(tx)?; - self.check_pool_size_available(tx, &collision, &dependencies)?; + let has_dependencies = !dependencies.is_empty(); + self.collision_manager + .can_store_transaction( + tx, + has_dependencies, + &colliding_transaction, + &self.storage, + ) + .map_err(|e| Error::Collided(e))?; + self.check_pool_size_available(tx, &colliding_transaction, &dependencies)?; self.storage.can_store_transaction(tx, &dependencies); Ok(()) } @@ -209,10 +227,14 @@ where /// If the pool is not full, it will return an empty list /// If the pool is full, it will return the list of transactions that must be removed from the pool along all of their dependent subtree /// If the pool is full and we can't make enough space by removing transactions, it will return an error + /// Currently, the rules are: + /// If a transaction is colliding with another verify if deleting the colliding transaction and dependents subtree is enough otherwise refuses the tx + /// If a transaction is dependent and not enough space, don't accept transaction + /// If a transaction is executable, try to free has much space used by less profitable transactions as possible in the pool to include it fn check_pool_size_available( &self, tx: &PoolTransaction, - collision: &Option, + collided_transactions: &HashMap>, dependencies: &[S::StorageIndex], ) -> Result, Error> { let tx_gas = tx.max_gas(); @@ -224,24 +246,24 @@ where { return Ok(vec![]); } + let mut removed_transactions = vec![]; + let mut gas_left = self.current_gas.saturating_add(tx_gas); + let mut bytes_left = self.current_bytes_size.saturating_add(bytes_size); + let mut txs_left = self.storage.count().saturating_add(1); // If the transaction has a collision verify that by removing the transaction we can free enough space // otherwise return an error - if let Some(collision) = collision { + for collision in collided_transactions.keys() { let collision_data = self.storage.get(collision)?; - let new_current_gas = self - .current_gas - .saturating_sub(collision_data.dependents_cumulative_gas); - let new_current_bytes_size = self - .current_bytes_size + gas_left = gas_left.saturating_sub(collision_data.dependents_cumulative_gas); + bytes_left = bytes_left .saturating_sub(collision_data.dependents_cumulative_bytes_size); - if new_current_gas < self.config.pool_limits.max_gas - && new_current_bytes_size < self.config.pool_limits.max_bytes_size - && self.storage.count().saturating_sub(1) - < self.config.pool_limits.max_txs + txs_left = txs_left.saturating_sub(1); + removed_transactions.push(*collision); + if gas_left <= self.config.pool_limits.max_gas + && bytes_left <= self.config.pool_limits.max_bytes_size + && txs_left <= self.config.pool_limits.max_txs { - return Ok(vec![*collision]); - } else { - return Err(Error::NotInsertedLimitHit); + return Ok(removed_transactions); } } @@ -250,12 +272,8 @@ where return Err(Error::NotInsertedLimitHit); } - // Here the transaction has no dependencies and no collision which means that it's an executable transaction + // Here the transaction has no dependencies which means that it's an executable transaction // and we want to make space for it - let mut removed_transactions = vec![]; - let mut gas_left = self.current_gas.saturating_add(tx_gas); - let mut bytes_left = self.current_bytes_size.saturating_add(bytes_size); - let mut txs_left = self.storage.count().saturating_add(1); let current_ratio = Ratio::new(tx.tip(), tx_gas); let mut sorted_txs = self .storage diff --git a/crates/services/txpool_v2/src/service.rs b/crates/services/txpool_v2/src/service.rs index 042bf47b54c..ba6bb2b06e8 100644 --- a/crates/services/txpool_v2/src/service.rs +++ b/crates/services/txpool_v2/src/service.rs @@ -313,10 +313,8 @@ where utxo_validation: config.utxo_validation, heavy_async_processor: Arc::new( HeavyAsyncProcessor::new( - config - .heavy_work - .number_pending_tasks_threads_verif_insert_transactions, - config.heavy_work.number_threads_verif_insert_transactions, + config.heavy_work.number_threads_to_verify_transactions, + config.heavy_work.size_of_verification_queue, ) .unwrap(), ), diff --git a/crates/services/txpool_v2/src/tests/pool.rs b/crates/services/txpool_v2/src/tests/pool.rs index 68841e21def..2a4a4539883 100644 --- a/crates/services/txpool_v2/src/tests/pool.rs +++ b/crates/services/txpool_v2/src/tests/pool.rs @@ -5,7 +5,10 @@ use crate::{ Config, PoolLimits, }, - error::Error, + error::{ + CollisionReason, + Error, + }, ports::WasmValidityError, tests::{ context::{ @@ -223,8 +226,12 @@ async fn insert__tx2_collided_on_contract_id() { let result2 = universe.verify_and_insert(tx_faulty).await; assert!(result1.is_ok()); + + // Then let err = result2.unwrap_err(); - assert!(matches!(err, Error::Collided(_))); + assert!( + matches!(err, Error::Collided(CollisionReason::ContractCreation(id)) if id == contract_id) + ); } #[tokio::test] @@ -281,7 +288,7 @@ async fn insert__already_known_tx() { // Then assert!(result1.is_ok()); let err = result2.unwrap_err(); - assert!(matches!(err, Error::Collided(_))); + assert!(matches!(err, Error::Collided(CollisionReason::Utxo(_)))); } #[tokio::test] @@ -329,7 +336,8 @@ async fn insert__colliding_dependent_underpriced() { let (output, unset_input) = universe.create_output_and_input(); let tx1 = universe.build_script_transaction(None, Some(vec![output]), 20); - let input = unset_input.into_input(UtxoId::new(tx1.id(&ChainId::default()), 0)); + let utxo_id = UtxoId::new(tx1.id(&ChainId::default()), 0); + let input = unset_input.into_input(utxo_id); // Given let tx2 = universe.build_script_transaction(Some(vec![input.clone()]), None, 20); @@ -344,7 +352,7 @@ async fn insert__colliding_dependent_underpriced() { assert!(result1.is_ok()); assert!(result2.is_ok()); let err = result3.unwrap_err(); - assert!(matches!(err, Error::Collided(_))); + assert!(matches!(err, Error::Collided(CollisionReason::Utxo(id)) if id == utxo_id)); } #[tokio::test] @@ -869,7 +877,9 @@ async fn insert__tx_tip_lower_than_another_tx_with_same_message_id() { let err = universe.verify_and_insert(tx_low).await.unwrap_err(); // Then - assert!(matches!(err, Error::Collided(_))); + assert!( + matches!(err, Error::Collided(CollisionReason::Message(msg_id)) if msg_id == *message.id()) + ); } #[tokio::test] @@ -1103,7 +1113,7 @@ async fn insert__tx_with_blob_already_inserted_at_higher_tip() { let err = universe.verify_and_insert(same_blob_tx).await.unwrap_err(); // Then - assert!(matches!(err, Error::Collided(_))); + assert!(matches!(err, Error::Collided(CollisionReason::Blob(b)) if b == blob_id)); } #[tokio::test] From 26871c8a214a966998ff4bf6a8e35c4a771dc913 Mon Sep 17 00:00:00 2001 From: AurelienFT Date: Thu, 26 Sep 2024 01:43:52 +0200 Subject: [PATCH 22/28] Update pool check size --- crates/services/txpool_v2/src/pool.rs | 32 +++++++++------------ crates/services/txpool_v2/src/tests/pool.rs | 2 +- 2 files changed, 14 insertions(+), 20 deletions(-) diff --git a/crates/services/txpool_v2/src/pool.rs b/crates/services/txpool_v2/src/pool.rs index 5679396e7a2..61d9ca39de7 100644 --- a/crates/services/txpool_v2/src/pool.rs +++ b/crates/services/txpool_v2/src/pool.rs @@ -239,17 +239,17 @@ where ) -> Result, Error> { let tx_gas = tx.max_gas(); let bytes_size = tx.metered_bytes_size(); - if self.current_gas.saturating_add(tx_gas) <= self.config.pool_limits.max_gas - && self.current_bytes_size.saturating_add(bytes_size) - <= self.config.pool_limits.max_bytes_size - && self.storage.count().saturating_add(1) <= self.config.pool_limits.max_txs - { - return Ok(vec![]); - } let mut removed_transactions = vec![]; let mut gas_left = self.current_gas.saturating_add(tx_gas); let mut bytes_left = self.current_bytes_size.saturating_add(bytes_size); let mut txs_left = self.storage.count().saturating_add(1); + if gas_left <= self.config.pool_limits.max_gas + && bytes_left <= self.config.pool_limits.max_bytes_size + && txs_left <= self.config.pool_limits.max_txs + { + return Ok(vec![]); + } + // If the transaction has a collision verify that by removing the transaction we can free enough space // otherwise return an error for collision in collided_transactions.keys() { @@ -285,18 +285,12 @@ where { let storage_id = sorted_txs.next().ok_or(Error::NotInsertedLimitHit)?; let storage_data = self.storage.get(&storage_id)?; - let mut dependencies = self.storage.get_dependencies(storage_id)?; - match dependencies.next() { - Some(_) => {} - None => { - let stored_ratio = Ratio::new( - storage_data.transaction.tip(), - storage_data.transaction.max_gas(), - ); - if stored_ratio >= current_ratio { - return Err(Error::NotInsertedLimitHit); - } - } + let ratio = Ratio::new( + storage_data.dependents_cumulative_tip, + storage_data.dependents_cumulative_gas, + ); + if ratio > current_ratio { + return Err(Error::NotInsertedLimitHit); } gas_left = gas_left.saturating_sub(storage_data.dependents_cumulative_gas); bytes_left = diff --git a/crates/services/txpool_v2/src/tests/pool.rs b/crates/services/txpool_v2/src/tests/pool.rs index 2a4a4539883..8e156efd2f0 100644 --- a/crates/services/txpool_v2/src/tests/pool.rs +++ b/crates/services/txpool_v2/src/tests/pool.rs @@ -534,7 +534,7 @@ async fn insert__tx_gas_limit() { async fn insert__tx_bytes_limit() { // Given let mut universe = TestPoolUniverse::default(); - let tx1 = universe.build_script_transaction(None, None, 0); + let tx1 = universe.build_script_transaction(None, None, 10); let checked_tx: CheckedTransaction = tx1 .clone() .into_checked_basic(Default::default(), &ConsensusParameters::default()) From 76b2af49dc1bc223f9fefeb59c6f376eaa02b25d Mon Sep 17 00:00:00 2001 From: AurelienFT Date: Thu, 26 Sep 2024 01:49:45 +0200 Subject: [PATCH 23/28] Move logic from pool to storage --- crates/services/txpool_v2/src/pool.rs | 10 +--- .../services/txpool_v2/src/storage/graph.rs | 50 +++++++++++-------- crates/services/txpool_v2/src/storage/mod.rs | 14 ++---- 3 files changed, 34 insertions(+), 40 deletions(-) diff --git a/crates/services/txpool_v2/src/pool.rs b/crates/services/txpool_v2/src/pool.rs index 61d9ca39de7..e1f17a8353d 100644 --- a/crates/services/txpool_v2/src/pool.rs +++ b/crates/services/txpool_v2/src/pool.rs @@ -114,14 +114,6 @@ where &self.storage, ) .map_err(|e| Error::Collided(e))?; - for collision in colliding_transactions.keys() { - if self - .storage - .is_in_dependencies_subtrees(*collision, &dependencies)? - { - return Err(Error::NotInsertedCollisionIsDependency); - } - } let transactions_to_remove = self.check_pool_size_available(&tx, &colliding_transactions, &dependencies)?; let mut removed_transactions = vec![]; @@ -177,7 +169,7 @@ where ) .map_err(|e| Error::Collided(e))?; self.check_pool_size_available(tx, &colliding_transaction, &dependencies)?; - self.storage.can_store_transaction(tx, &dependencies); + self.storage.can_store_transaction(tx, &dependencies, &colliding_transaction)?; Ok(()) } diff --git a/crates/services/txpool_v2/src/storage/graph.rs b/crates/services/txpool_v2/src/storage/graph.rs index fdc73bf73bf..69abdbe5a3e 100644 --- a/crates/services/txpool_v2/src/storage/graph.rs +++ b/crates/services/txpool_v2/src/storage/graph.rs @@ -38,7 +38,7 @@ use petgraph::{ use crate::{ collision_manager::basic::BasicCollisionManagerStorage, - error::Error, + error::{CollisionReason, Error}, ports::TxPoolPersistentStorage, selection_algorithms::ratio_tip_gas::RatioTipGasSelectionAlgorithmStorage, }; @@ -318,6 +318,26 @@ impl GraphStorage { .graph .neighbors_directed(index, petgraph::Direction::Outgoing)) } + + fn is_in_dependencies_subtrees( + &self, + index: NodeIndex, + transactions: &[NodeIndex], + ) -> Result { + let mut already_visited = HashSet::new(); + let mut to_check = transactions.to_vec(); + while let Some(node_id) = to_check.pop() { + if already_visited.contains(&node_id) { + continue; + } + if node_id == index { + return Ok(true); + } + already_visited.insert(node_id); + to_check.extend(self.get_dependencies(node_id)?); + } + Ok(false) + } } impl Storage for GraphStorage { @@ -399,7 +419,15 @@ impl Storage for GraphStorage { &self, transaction: &PoolTransaction, dependencies: &[Self::StorageIndex], + colliding_transactions: &HashMap>, ) -> Result<(), Error> { + for collision in colliding_transactions.keys() { + if self + .is_in_dependencies_subtrees(*collision, &dependencies)? + { + return Err(Error::NotInsertedCollisionIsDependency); + } + } for node_id in dependencies.iter() { let Some(dependency_node) = self.graph.node_weight(*node_id) else { return Err(Error::Storage(format!( @@ -457,26 +485,6 @@ impl Storage for GraphStorage { Ok(sorted_nodes.iter().map(|(_, node_id)| *node_id).collect()) } - fn is_in_dependencies_subtrees( - &self, - index: Self::StorageIndex, - transactions: &[Self::StorageIndex], - ) -> Result { - let mut already_visited = HashSet::new(); - let mut to_check = transactions.to_vec(); - while let Some(node_id) = to_check.pop() { - if already_visited.contains(&node_id) { - continue; - } - if node_id == index { - return Ok(true); - } - already_visited.insert(node_id); - to_check.extend(self.get_dependencies(node_id)?); - } - Ok(false) - } - fn validate_inputs( &self, transaction: &PoolTransaction, diff --git a/crates/services/txpool_v2/src/storage/mod.rs b/crates/services/txpool_v2/src/storage/mod.rs index 9d9b5c40f62..ef943b19cae 100644 --- a/crates/services/txpool_v2/src/storage/mod.rs +++ b/crates/services/txpool_v2/src/storage/mod.rs @@ -1,11 +1,11 @@ use std::{ - collections::HashSet, + collections::{HashMap, HashSet}, fmt::Debug, time::Instant, }; use crate::{ - error::Error, + error::{CollisionReason, Error}, ports::TxPoolPersistentStorage, }; use fuel_core_types::services::txpool::PoolTransaction; @@ -48,7 +48,8 @@ pub trait Storage { &self, transaction: &PoolTransaction, dependencies: &[Self::StorageIndex], - ) -> Result<(), Error>; + collisions: &HashMap>, + ) -> Result<(), Error>; /// Get the storage data by its index. fn get(&self, index: &Self::StorageIndex) -> Result<&StorageData, Error>; @@ -70,13 +71,6 @@ pub trait Storage { &self, ) -> Result, Error>; - /// Verify if an id is in the dependencies subtree of another ids - fn is_in_dependencies_subtrees( - &self, - index: Self::StorageIndex, - transactions: &[Self::StorageIndex], - ) -> Result; - /// Validate inputs of a transaction. fn validate_inputs( &self, From 13e69904109a2bb483966009a4f8eee582daaa59 Mon Sep 17 00:00:00 2001 From: AurelienFT Date: Thu, 26 Sep 2024 01:51:57 +0200 Subject: [PATCH 24/28] fmt and clippy --- crates/services/txpool_v2/src/pool.rs | 7 ++++--- crates/services/txpool_v2/src/storage/graph.rs | 9 +++++---- crates/services/txpool_v2/src/storage/mod.rs | 12 +++++++++--- 3 files changed, 18 insertions(+), 10 deletions(-) diff --git a/crates/services/txpool_v2/src/pool.rs b/crates/services/txpool_v2/src/pool.rs index e1f17a8353d..a99eb3a9cc3 100644 --- a/crates/services/txpool_v2/src/pool.rs +++ b/crates/services/txpool_v2/src/pool.rs @@ -113,7 +113,7 @@ where &colliding_transactions, &self.storage, ) - .map_err(|e| Error::Collided(e))?; + .map_err(Error::Collided)?; let transactions_to_remove = self.check_pool_size_available(&tx, &colliding_transactions, &dependencies)?; let mut removed_transactions = vec![]; @@ -167,9 +167,10 @@ where &colliding_transaction, &self.storage, ) - .map_err(|e| Error::Collided(e))?; + .map_err(Error::Collided)?; self.check_pool_size_available(tx, &colliding_transaction, &dependencies)?; - self.storage.can_store_transaction(tx, &dependencies, &colliding_transaction)?; + self.storage + .can_store_transaction(tx, &dependencies, &colliding_transaction)?; Ok(()) } diff --git a/crates/services/txpool_v2/src/storage/graph.rs b/crates/services/txpool_v2/src/storage/graph.rs index 69abdbe5a3e..2196c736e52 100644 --- a/crates/services/txpool_v2/src/storage/graph.rs +++ b/crates/services/txpool_v2/src/storage/graph.rs @@ -38,7 +38,10 @@ use petgraph::{ use crate::{ collision_manager::basic::BasicCollisionManagerStorage, - error::{CollisionReason, Error}, + error::{ + CollisionReason, + Error, + }, ports::TxPoolPersistentStorage, selection_algorithms::ratio_tip_gas::RatioTipGasSelectionAlgorithmStorage, }; @@ -422,9 +425,7 @@ impl Storage for GraphStorage { colliding_transactions: &HashMap>, ) -> Result<(), Error> { for collision in colliding_transactions.keys() { - if self - .is_in_dependencies_subtrees(*collision, &dependencies)? - { + if self.is_in_dependencies_subtrees(*collision, dependencies)? { return Err(Error::NotInsertedCollisionIsDependency); } } diff --git a/crates/services/txpool_v2/src/storage/mod.rs b/crates/services/txpool_v2/src/storage/mod.rs index ef943b19cae..7502be00685 100644 --- a/crates/services/txpool_v2/src/storage/mod.rs +++ b/crates/services/txpool_v2/src/storage/mod.rs @@ -1,11 +1,17 @@ use std::{ - collections::{HashMap, HashSet}, + collections::{ + HashMap, + HashSet, + }, fmt::Debug, time::Instant, }; use crate::{ - error::{CollisionReason, Error}, + error::{ + CollisionReason, + Error, + }, ports::TxPoolPersistentStorage, }; use fuel_core_types::services::txpool::PoolTransaction; @@ -49,7 +55,7 @@ pub trait Storage { transaction: &PoolTransaction, dependencies: &[Self::StorageIndex], collisions: &HashMap>, - ) -> Result<(), Error>; + ) -> Result<(), Error>; /// Get the storage data by its index. fn get(&self, index: &Self::StorageIndex) -> Result<&StorageData, Error>; From 6249e5dd53036fa574d4f1cfc3881f0f3bf0f455 Mon Sep 17 00:00:00 2001 From: AurelienFT Date: Thu, 26 Sep 2024 02:12:30 +0200 Subject: [PATCH 25/28] remove unused stream predicate verif --- Cargo.lock | 1 - crates/services/txpool_v2/Cargo.toml | 1 - crates/services/txpool_v2/src/verifications.rs | 13 +++++++------ 3 files changed, 7 insertions(+), 8 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 415d719082f..382f5bebc71 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3754,7 +3754,6 @@ dependencies = [ "petgraph", "rayon", "tokio", - "tokio-rayon", "tracing", ] diff --git a/crates/services/txpool_v2/Cargo.toml b/crates/services/txpool_v2/Cargo.toml index 632a8a16fa2..25fafbeb4e1 100644 --- a/crates/services/txpool_v2/Cargo.toml +++ b/crates/services/txpool_v2/Cargo.toml @@ -25,7 +25,6 @@ parking_lot = { workspace = true } petgraph = "0.6.5" rayon = { workspace = true } tokio = { workspace = true, default-features = false, features = ["sync"] } -tokio-rayon = { workspace = true } tracing = { workspace = true } [features] diff --git a/crates/services/txpool_v2/src/verifications.rs b/crates/services/txpool_v2/src/verifications.rs index 4ceed3a68ee..997f1b35afa 100644 --- a/crates/services/txpool_v2/src/verifications.rs +++ b/crates/services/txpool_v2/src/verifications.rs @@ -94,7 +94,7 @@ impl BasicVerifiedTx { } impl InputDependenciesVerifiedTx { - pub async fn perform_input_computation_verifications( + pub fn perform_input_computation_verifications( self, consensus_params: &ConsensusParameters, wasm_checker: &impl WasmChecker, @@ -112,9 +112,7 @@ impl InputDependenciesVerifiedTx { } let parameters = CheckPredicateParams::from(consensus_params); - let tx = - tokio_rayon::spawn_fifo(move || self.0.check_predicates(¶meters, memory)) - .await?; + let tx = self.0.check_predicates(¶meters, memory)?; Ok(InputComputationVerifiedTx(tx)) } @@ -163,8 +161,11 @@ where let inputs_verified_tx = basically_verified_tx .perform_inputs_verifications(pool, consensus_params_version)?; let input_computation_verified_tx = inputs_verified_tx - .perform_input_computation_verifications(consensus_params, wasm_checker, memory) - .await?; + .perform_input_computation_verifications( + consensus_params, + wasm_checker, + memory, + )?; let fully_verified_tx = input_computation_verified_tx.perform_final_verifications(consensus_params)?; fully_verified_tx.into_pool_transaction(consensus_params_version) From 83c56ae8506c2f51a6c28b060ada695b86bdb78c Mon Sep 17 00:00:00 2001 From: AurelienFT Date: Thu, 26 Sep 2024 15:56:55 +0200 Subject: [PATCH 26/28] Change way to get less worth it transactions --- crates/services/txpool_v2/src/pool.rs | 20 ++++-- .../txpool_v2/src/selection_algorithms/mod.rs | 18 ++++- .../src/selection_algorithms/ratio_tip_gas.rs | 70 ++++++++++++++++--- .../services/txpool_v2/src/storage/graph.rs | 25 +------ crates/services/txpool_v2/src/storage/mod.rs | 8 +-- 5 files changed, 96 insertions(+), 45 deletions(-) diff --git a/crates/services/txpool_v2/src/pool.rs b/crates/services/txpool_v2/src/pool.rs index bafe09360e5..1b2893ae1e7 100644 --- a/crates/services/txpool_v2/src/pool.rs +++ b/crates/services/txpool_v2/src/pool.rs @@ -1,4 +1,7 @@ -use std::collections::HashMap; +use std::{ + collections::HashMap, + time::Instant, +}; use fuel_core_types::{ fuel_tx::{ @@ -97,6 +100,7 @@ where .map_err(|e| Error::Database(format!("{:?}", e)))?; let tx_id = tx.id(); let gas = tx.max_gas(); + let creation_instant = Instant::now(); let bytes_size = tx.metered_bytes_size(); self.config.black_list.check_blacklisting(&tx)?; Self::check_blob_does_not_exist(&tx, &latest_view)?; @@ -127,7 +131,9 @@ where .remove_transaction_and_dependents_subtree(*collision)?, ); } - let storage_id = self.storage.store_transaction(tx, dependencies)?; + let storage_id = + self.storage + .store_transaction(tx, creation_instant, dependencies)?; self.tx_id_to_storage_id.insert(tx_id, storage_id); self.current_gas = self.current_gas.saturating_add(gas); self.current_bytes_size = self.current_bytes_size.saturating_add(bytes_size); @@ -140,6 +146,11 @@ where let tx = Storage::get(&self.storage, &storage_id)?; self.collision_manager .on_stored_transaction(&tx.transaction, storage_id)?; + self.selection_algorithm.on_stored_transaction( + &tx.transaction, + creation_instant, + storage_id, + ); Ok(removed_transactions) } @@ -265,10 +276,7 @@ where // Here the transaction has no dependencies which means that it's an executable transaction // and we want to make space for it let current_ratio = Ratio::new(tx.tip(), tx_gas); - let mut sorted_txs = self - .storage - .get_worst_ratio_tip_gas_subtree_roots()? - .into_iter(); + let mut sorted_txs = self.selection_algorithm.get_less_worth_txs(); while gas_left > self.config.pool_limits.max_gas || bytes_left > self.config.pool_limits.max_bytes_size || txs_left > self.config.pool_limits.max_txs diff --git a/crates/services/txpool_v2/src/selection_algorithms/mod.rs b/crates/services/txpool_v2/src/selection_algorithms/mod.rs index bcbe27c9467..c5bb0ef1a97 100644 --- a/crates/services/txpool_v2/src/selection_algorithms/mod.rs +++ b/crates/services/txpool_v2/src/selection_algorithms/mod.rs @@ -1,6 +1,11 @@ +use std::time::Instant; + use fuel_core_types::services::txpool::PoolTransaction; -use crate::error::Error; +use crate::{ + error::Error, + storage::Storage, +}; pub mod ratio_tip_gas; @@ -29,6 +34,17 @@ pub trait SelectionAlgorithm { storage: &Self::Storage, ) -> Result<(), Error>; + /// Get less worth transactions iterator + fn get_less_worth_txs(&self) -> impl Iterator; + + /// Inform the collision manager that a transaction was stored. + fn on_stored_transaction( + &mut self, + transaction: &PoolTransaction, + creation_instant: Instant, + transaction_id: Self::StorageIndex, + ) -> Result<(), Error>; + /// Inform the selection algorithm that a transaction was removed from the pool. fn on_removed_transaction( &mut self, diff --git a/crates/services/txpool_v2/src/selection_algorithms/ratio_tip_gas.rs b/crates/services/txpool_v2/src/selection_algorithms/ratio_tip_gas.rs index 562fb29bebc..fbf63e3d78a 100644 --- a/crates/services/txpool_v2/src/selection_algorithms/ratio_tip_gas.rs +++ b/crates/services/txpool_v2/src/selection_algorithms/ratio_tip_gas.rs @@ -3,7 +3,10 @@ use std::{ Ordering, Reverse, }, - collections::BTreeMap, + collections::{ + BTreeMap, + HashMap, + }, fmt::Debug, time::Instant, }; @@ -69,13 +72,17 @@ impl PartialOrd for Key { /// The selection algorithm that selects transactions based on the tip/gas ratio. pub struct RatioTipGasSelection { - transactions_sorted_tip_gas_ratio: BTreeMap, S::StorageIndex>, + executable_transactions_sorted_tip_gas_ratio: BTreeMap, S::StorageIndex>, + all_transactions_sorted_tip_gas_ratio: BTreeMap, S::StorageIndex>, + tx_id_to_creation_instant: HashMap, } impl RatioTipGasSelection { pub fn new() -> Self { Self { - transactions_sorted_tip_gas_ratio: BTreeMap::new(), + executable_transactions_sorted_tip_gas_ratio: BTreeMap::new(), + all_transactions_sorted_tip_gas_ratio: BTreeMap::new(), + tx_id_to_creation_instant: HashMap::new(), } } } @@ -102,11 +109,13 @@ impl SelectionAlgorithm // Take the first transaction with the highest tip/gas ratio if it fits in the gas limit // then promote all its dependents to the list of transactions to be executed // and repeat the process until the gas limit is reached - while gas_left > 0 && !self.transactions_sorted_tip_gas_ratio.is_empty() { + while gas_left > 0 + && !self.executable_transactions_sorted_tip_gas_ratio.is_empty() + { let mut new_executables = vec![]; let mut best_transaction = None; - let sorted_iter = self.transactions_sorted_tip_gas_ratio.iter(); + let sorted_iter = self.executable_transactions_sorted_tip_gas_ratio.iter(); for (key, storage_id) in sorted_iter { let enough_gas = { let stored_transaction = storage.get(storage_id)?; @@ -125,7 +134,8 @@ impl SelectionAlgorithm self.new_executable_transactions(new_executables, storage)?; // Remove the best transaction from the sorted list if let Some((key, best_transaction)) = best_transaction { - self.transactions_sorted_tip_gas_ratio.remove(&key); + self.executable_transactions_sorted_tip_gas_ratio + .remove(&key); best_transactions.push(best_transaction); } else { // If no transaction fits in the gas limit, @@ -149,26 +159,66 @@ impl SelectionAlgorithm ); let key = Key { ratio: tip_gas_ratio, - creation_instant: Instant::now(), + creation_instant: stored_transaction.creation_instant, tx_id: stored_transaction.transaction.id(), }; - self.transactions_sorted_tip_gas_ratio + self.executable_transactions_sorted_tip_gas_ratio .insert(Reverse(key), storage_id); + self.tx_id_to_creation_instant.insert( + stored_transaction.transaction.id(), + stored_transaction.creation_instant, + ); } Ok(()) } + fn get_less_worth_txs(&self) -> impl Iterator { + self.all_transactions_sorted_tip_gas_ratio + .iter() + .map(|(_, storage_id)| *storage_id) + } + + fn on_stored_transaction( + &mut self, + transaction: &PoolTransaction, + creation_instant: Instant, + transaction_id: Self::StorageIndex, + ) -> Result<(), Error> { + let tip_gas_ratio = RatioTipGas::new(transaction.tip(), transaction.max_gas()); + let key = Key { + ratio: tip_gas_ratio, + creation_instant, + tx_id: transaction.id(), + }; + self.all_transactions_sorted_tip_gas_ratio + .insert(Reverse(key), transaction_id); + self.tx_id_to_creation_instant + .insert(transaction.id(), creation_instant); + Ok(()) + } + fn on_removed_transaction( &mut self, transaction: &PoolTransaction, ) -> Result<(), Error> { let tip_gas_ratio = RatioTipGas::new(transaction.tip(), transaction.max_gas()); + let creation_instant = *self + .tx_id_to_creation_instant + .get(&transaction.id()) + .ok_or(Error::TransactionNotFound( + "Expected the transaction to be in the tx_id_to_creation_instant map" + .to_string(), + ))?; let key = Key { ratio: tip_gas_ratio, - creation_instant: Instant::now(), + creation_instant, tx_id: transaction.id(), }; - self.transactions_sorted_tip_gas_ratio.remove(&Reverse(key)); + self.executable_transactions_sorted_tip_gas_ratio + .remove(&Reverse(key)); + self.all_transactions_sorted_tip_gas_ratio + .remove(&Reverse(key)); + self.tx_id_to_creation_instant.remove(&transaction.id()); Ok(()) } } diff --git a/crates/services/txpool_v2/src/storage/graph.rs b/crates/services/txpool_v2/src/storage/graph.rs index 428913d5797..ca268147288 100644 --- a/crates/services/txpool_v2/src/storage/graph.rs +++ b/crates/services/txpool_v2/src/storage/graph.rs @@ -338,6 +338,7 @@ impl Storage for GraphStorage { fn store_transaction( &mut self, transaction: PoolTransaction, + creation_instant: Instant, dependencies: Vec, ) -> Result { let tx_id = transaction.id(); @@ -378,6 +379,7 @@ impl Storage for GraphStorage { dependents_cumulative_gas: gas, dependents_cumulative_bytes_size: size, transaction, + creation_instant, number_txs_in_chain: all_dependencies_recursively.len().saturating_add(1), }; @@ -452,29 +454,6 @@ impl Storage for GraphStorage { self.get_dependents_inner(index) } - // Maybe change in the future as it can be very costly. - fn get_worst_ratio_tip_gas_subtree_roots( - &self, - ) -> Result, Error> { - let mut sorted_nodes: BTreeSet<(Ratio, NodeIndex)> = BTreeSet::new(); - for node_id in self.graph.node_indices() { - let Some(node) = self.graph.node_weight(node_id) else { - return Err(Error::Storage(format!( - "Node with id {:?} not found", - node_id - ))); - }; - sorted_nodes.insert(( - Ratio::new( - node.dependents_cumulative_tip, - node.dependents_cumulative_gas, - ), - node_id, - )); - } - Ok(sorted_nodes.iter().map(|(_, node_id)| *node_id).collect()) - } - fn validate_inputs( &self, transaction: &PoolTransaction, diff --git a/crates/services/txpool_v2/src/storage/mod.rs b/crates/services/txpool_v2/src/storage/mod.rs index 7502be00685..8f2774503fe 100644 --- a/crates/services/txpool_v2/src/storage/mod.rs +++ b/crates/services/txpool_v2/src/storage/mod.rs @@ -30,6 +30,8 @@ pub struct StorageData { pub dependents_cumulative_bytes_size: usize, /// Number of dependents pub number_txs_in_chain: usize, + /// The instant when the transaction was added to the pool. + pub creation_instant: Instant, } pub type RemovedTransactions = Vec; @@ -45,6 +47,7 @@ pub trait Storage { fn store_transaction( &mut self, transaction: PoolTransaction, + creation_instant: Instant, dependencies: Vec, ) -> Result; @@ -72,11 +75,6 @@ pub trait Storage { index: Self::StorageIndex, ) -> Result, Error>; - /// Get less worth subtree roots. - fn get_worst_ratio_tip_gas_subtree_roots( - &self, - ) -> Result, Error>; - /// Validate inputs of a transaction. fn validate_inputs( &self, From de44984ac447cff8a9620635f3308e221663ef4a Mon Sep 17 00:00:00 2001 From: AurelienFT Date: Thu, 26 Sep 2024 16:06:08 +0200 Subject: [PATCH 27/28] Fix clippy --- .../txpool_v2/src/selection_algorithms/ratio_tip_gas.rs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/crates/services/txpool_v2/src/selection_algorithms/ratio_tip_gas.rs b/crates/services/txpool_v2/src/selection_algorithms/ratio_tip_gas.rs index fbf63e3d78a..9ae46806b30 100644 --- a/crates/services/txpool_v2/src/selection_algorithms/ratio_tip_gas.rs +++ b/crates/services/txpool_v2/src/selection_algorithms/ratio_tip_gas.rs @@ -173,9 +173,7 @@ impl SelectionAlgorithm } fn get_less_worth_txs(&self) -> impl Iterator { - self.all_transactions_sorted_tip_gas_ratio - .iter() - .map(|(_, storage_id)| *storage_id) + self.all_transactions_sorted_tip_gas_ratio.values().copied() } fn on_stored_transaction( From 4bb55ba146aae0b1d48165c91a974e19655d99c3 Mon Sep 17 00:00:00 2001 From: AurelienFT Date: Fri, 27 Sep 2024 18:02:18 +0200 Subject: [PATCH 28/28] simplify an unused function --- .../services/txpool_v2/src/storage/graph.rs | 26 +++++++------------ 1 file changed, 9 insertions(+), 17 deletions(-) diff --git a/crates/services/txpool_v2/src/storage/graph.rs b/crates/services/txpool_v2/src/storage/graph.rs index 150d5b33ce6..1b53f2f3921 100644 --- a/crates/services/txpool_v2/src/storage/graph.rs +++ b/crates/services/txpool_v2/src/storage/graph.rs @@ -79,21 +79,6 @@ impl GraphStorage { } impl GraphStorage { - /// Remove a node and all its dependent sub-graph. - /// Edit the data of dependencies transactions accordingly. - /// Returns the removed transactions. - fn remove_node_and_dependent_sub_graph( - &mut self, - root_id: NodeIndex, - ) -> Vec { - let Some(root) = self.graph.node_weight(root_id) else { - debug_assert!(false, "Node with id {:?} not found", root_id); - return vec![]; - }; - let gas_reduction = root.dependents_cumulative_gas; - let tip_reduction = root.dependents_cumulative_tip; - self.remove_dependent_sub_graph(root_id) - } fn reduce_dependencies_cumulative_gas_tip_and_chain_count( &mut self, root_id: NodeIndex, @@ -124,7 +109,13 @@ impl GraphStorage { } } - fn remove_dependent_sub_graph(&mut self, root_id: NodeIndex) -> Vec { + /// Remove a node and all its dependent sub-graph. + /// Edit the data of dependencies transactions accordingly. + /// Returns the removed transactions. + fn remove_node_and_dependent_sub_graph( + &mut self, + root_id: NodeIndex, + ) -> Vec { let dependencies: Vec = self.get_dependencies(root_id).collect(); let dependents: Vec<_> = self .graph @@ -145,7 +136,8 @@ impl GraphStorage { self.clear_cache(root.transaction.outputs(), &root.transaction.id()); let mut removed_transactions = vec![root.transaction]; for dependent in dependents { - removed_transactions.extend(self.remove_dependent_sub_graph(dependent)); + removed_transactions + .extend(self.remove_node_and_dependent_sub_graph(dependent)); } removed_transactions }