Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve TxPool tests and documentation #2327

Merged
merged 18 commits into from
Nov 13, 2024
Merged
Show file tree
Hide file tree
Changes from 10 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,16 @@ and this project adheres to [Semantic Versioning](http://semver.org/).

## [Unreleased]

### Added
- [2327](https://github.com/FuelLabs/fuel-core/pull/2327): Add more services tests and more checks of the pool. Also add an high level documentation for users of the pool and contributors.

=======
## [Version 0.39.0]

### Added
- [2324](https://github.com/FuelLabs/fuel-core/pull/2324): Added metrics for sync, async processor and for all GraphQL queries.
- [2320](https://github.com/FuelLabs/fuel-core/pull/2320): Added new CLI flag `graphql-max-resolver-recursive-depth` to limit recursion within resolver. The default value it "1".


## Fixed
- [2320](https://github.com/FuelLabs/fuel-core/issues/2320): Prevent `/health` and `/v1/health` from being throttled by the concurrency limiter.
- [2322](https://github.com/FuelLabs/fuel-core/issues/2322): Set the salt of genesis contracts to zero on execution.
Expand Down
78 changes: 78 additions & 0 deletions crates/services/txpool_v2/src/collision_manager/basic.rs
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,9 @@ use super::{
Collisions,
};

#[cfg(test)]
use fuel_core_types::services::txpool::ArcPoolTx;

pub struct BasicCollisionManager<StorageIndex> {
/// Message -> Transaction that currently use the Message
messages_spenders: HashMap<Nonce, StorageIndex>,
Expand Down Expand Up @@ -236,4 +239,79 @@ where
};
}
}

#[cfg(test)]
/// Expected transactions are the transactions that must be populate all elements present in the collision manager.
/// This function will check if all elements present in the collision manager are present in the expected transactions and vice versa.
fn assert_integrity(&self, expected_txs: &[ArcPoolTx]) {
AurelienFT marked this conversation as resolved.
Show resolved Hide resolved
use std::ops::Deref;

let mut message_spenders = HashMap::new();
let mut coins_spenders = BTreeMap::new();
let mut contracts_creators = HashMap::new();
let mut blobs_users = HashMap::new();
for tx in expected_txs {
if let PoolTransaction::Blob(checked_tx, _) = tx.deref() {
let blob_id = checked_tx.transaction().blob_id();
blobs_users.insert(*blob_id, tx.id());
}
for input in tx.inputs() {
match input {
Input::CoinSigned(CoinSigned { utxo_id, .. })
| Input::CoinPredicate(CoinPredicate { utxo_id, .. }) => {
coins_spenders.insert(*utxo_id, tx.id());
}
Input::MessageCoinSigned(MessageCoinSigned { nonce, .. })
| Input::MessageCoinPredicate(MessageCoinPredicate {
nonce, ..
})
| Input::MessageDataSigned(MessageDataSigned { nonce, .. })
| Input::MessageDataPredicate(MessageDataPredicate {
nonce, ..
}) => {
message_spenders.insert(*nonce, tx.id());
}
_ => {}
rafal-ch marked this conversation as resolved.
Show resolved Hide resolved
}
}
for output in tx.outputs() {
if let Output::ContractCreated { contract_id, .. } = output {
contracts_creators.insert(*contract_id, tx.id());
}
}
}
for nonce in self.messages_spenders.keys() {
message_spenders.remove(nonce).unwrap_or_else(|| panic!(
"A message ({}) spender is present on the collision manager that shouldn't be there.",
nonce
));
}
assert!(
message_spenders.is_empty(),
"Some message senders are missing from the collision manager: {:?}",
AurelienFT marked this conversation as resolved.
Show resolved Hide resolved
message_spenders
);
for utxo_id in self.coins_spenders.keys() {
coins_spenders.remove(utxo_id).unwrap_or_else(|| panic!(
"A coin ({}) spender is present on the collision manager that shouldn't be there.",
utxo_id
));
}
assert!(
coins_spenders.is_empty(),
"Some coin senders are missing from the collision manager: {:?}",
coins_spenders
);
for contract_id in self.contracts_creators.keys() {
contracts_creators.remove(contract_id).unwrap_or_else(|| panic!(
"A contract ({}) creator is present on the collision manager that shouldn't be there.",
contract_id
));
}
assert!(
contracts_creators.is_empty(),
"Some contract creators are missing from the collision manager: {:?}",
contracts_creators
);
}
}
7 changes: 7 additions & 0 deletions crates/services/txpool_v2/src/collision_manager/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@ use std::collections::HashMap;

use crate::storage::StorageData;

#[cfg(test)]
use fuel_core_types::services::txpool::ArcPoolTx;

pub mod basic;

pub type Collisions<StorageIndex> = HashMap<StorageIndex, Vec<CollisionReason>>;
Expand All @@ -36,4 +39,8 @@ pub trait CollisionManager {

/// Inform the collision manager that a transaction was removed.
fn on_removed_transaction(&mut self, transaction: &PoolTransaction);

#[cfg(test)]
/// Asserts the integrity of the collision manager.
fn assert_integrity(&self, expected_txs: &[ArcPoolTx]);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

does this need to be in the trait?
You are forcing all implementors of a CollisionManager to implement a test-only function to establish its integrity.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pool is only defined with generics based on trait and so traits need to implements this for the pool to use it. Personally, I don't find this strange to have trait having test-only methods

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I prefer to keep traits only with business logic, if possible. In the case of tests, you always know the actual type. The TxPool can expose collision manager in the case of tests, in this case you can call assert_integrity directly on txpool.collision_manager.assert_integrity.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okey I personally find it cool to have test methods on trait also. But ok to change as you prefer.

}
39 changes: 37 additions & 2 deletions crates/services/txpool_v2/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,45 @@
//! This crate manage the verification, storage, organisation and selection of the transactions for the network.
//! A transaction in Fuel has inputs and outputs. Inputs are outputs of previous transactions.
//! In a case where one of the input is an output of a transaction that has not been executed in a committed block (transaction still in the pool),
//! then the new transaction is considered dependent on that transaction.
//!
//! If a transaction has a dependency, it cannot be selected in a block until the dependent transaction has been selected.
//! A transaction can have a dependency per input and this dependency transaction can also have its own dependencies.
//! This creates a dependency tree between transactions inside the pool which can be very costly to compute for insertion/deletion etc...
//! In order to avoid too much cost, the transaction pool only allow a maximum number of transaction inside a dependency chain.
//! There is others limits on the pool that prevent its size to grow too much: maximum gas in the pool, maximum bytes in the pool, maximum number of transactions in the pool.
//! The pool also implements a TTL for the transactions, if a transaction is not selected in a block after a certain time, it is removed from the pool.
//!
//! All the transactions ordered by their ratio of gas/tip to be selected in a block.
//! It's possible that a transaction is not profitable enough to be selected for now and so either it will be selected later or it will be removed from the pool.
//! In order to make a transaction more likely to be selected, it's needed to submit a new colliding transaction (see below) with a higher tip/gas ratio.
//!
//! When a transaction is inserted it's possible that it use same inputs as one or multiple transactions already in the pool: this is what we call a collision.
//! The pool has to choose which transaction to keep and which to remove.
//! The pool will always try to maximize the number of transactions that can be selected in the next block and so
//! during collision resolution it will prioritize transactions without dependencies.
//! In a collision case, the transaction is considered a conflict and can be inserted under certain conditions :
//! - The transaction has dependencies:
//! - Can collide only with one other transaction. So, the user can submit
//! the same transaction with a higher tip but not merge one or more
//! transactions into one.
//! - A new transaction can be accepted if its profitability is higher than
acerone85 marked this conversation as resolved.
Show resolved Hide resolved
//! the cumulative profitability of the colliding transactions, and all
//! the transactions that depend on it.
//! - A transaction doesn't have dependencies:
//! - A new transaction can be accepted if its profitability is higher
//! than the collided subtrees'.
//!
//! The pool provides a way to subscribe for updates on a transaction status.
//! It usually stream one or two messages:
//! - If the insertion of the transaction fails, you can expect only one message with the error.
//! - If the transaction is inserted, you can expect two messages: one with the validation of the insertion and one when the transaction is selected in a block.

#![deny(clippy::arithmetic_side_effects)]
#![deny(clippy::cast_possible_truncation)]
#![deny(unused_crate_dependencies)]
#![deny(warnings)]

// TODO: Rename the folder from `txpool_v2` to `txpool` after the migration is complete.
AurelienFT marked this conversation as resolved.
Show resolved Hide resolved

mod collision_manager;
pub mod config;
pub mod error;
Expand Down
31 changes: 31 additions & 0 deletions crates/services/txpool_v2/src/pool.rs
Original file line number Diff line number Diff line change
Expand Up @@ -493,6 +493,37 @@ where
.on_removed_transaction(storage_entry);
}
}

#[cfg(test)]
pub fn assert_integrity(&self, expected_txs: &[ArcPoolTx]) {
let storage_ids_dependencies = self.storage.assert_integrity(expected_txs);
let txs_without_dependencies = expected_txs
.iter()
.zip(storage_ids_dependencies)
.filter_map(|(tx, (_, has_dependencies))| {
if !has_dependencies {
Some(tx.clone())
} else {
None
}
})
.collect::<Vec<_>>();
self.selection_algorithm
.assert_integrity(&txs_without_dependencies);
self.collision_manager.assert_integrity(expected_txs);
let mut txs: HashMap<TxId, ArcPoolTx> = expected_txs
.iter()
.map(|tx| (tx.id(), tx.clone()))
.collect();
for tx in &self.tx_id_to_storage_id {
txs.remove(tx.0)
.expect("Transaction not found in the expected transactions");
}
assert!(
txs.is_empty(),
"Some transactions are not found in the pool"
);
}
}

pub struct NotEnoughSpace {
Expand Down
7 changes: 7 additions & 0 deletions crates/services/txpool_v2/src/selection_algorithms/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@ use crate::storage::{
StorageData,
};

#[cfg(test)]
use fuel_core_types::services::txpool::ArcPoolTx;

pub mod ratio_tip_gas;

/// Constraints that the selection algorithm has to respect.
Expand Down Expand Up @@ -42,4 +45,8 @@ pub trait SelectionAlgorithm {

/// Inform the selection algorithm that a transaction was removed from the pool.
fn on_removed_transaction(&mut self, storage_entry: &StorageData);

#[cfg(test)]
/// Asserts the integrity of the selection algorithm.
fn assert_integrity(&self, expected_txs: &[ArcPoolTx]);
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,12 @@ use super::{
SelectionAlgorithm,
};

#[cfg(test)]
use fuel_core_types::services::txpool::ArcPoolTx;

#[cfg(test)]
use std::collections::HashMap;

pub trait RatioTipGasSelectionAlgorithmStorage {
type StorageIndex: Debug;

Expand Down Expand Up @@ -252,4 +258,25 @@ where
let key = Self::key(storage_entry);
self.on_removed_transaction_inner(key)
}

#[cfg(test)]
fn assert_integrity(&self, expected_txs: &[ArcPoolTx]) {
let mut expected_txs: HashMap<TxId, ArcPoolTx> = expected_txs
.iter()
.map(|tx| (tx.id(), tx.clone()))
.collect();
for key in self.executable_transactions_sorted_tip_gas_ratio.keys() {
expected_txs.remove(&key.0.tx_id).unwrap_or_else(|| {
panic!(
"Transaction with id {:?} is not in the expected transactions.",
key.0.tx_id
)
});
}
assert!(
expected_txs.is_empty(),
"Some transactions are missing from the selection algorithm: {:?}",
expected_txs.keys().collect::<Vec<_>>()
);
}
}
86 changes: 86 additions & 0 deletions crates/services/txpool_v2/src/storage/graph.rs
Original file line number Diff line number Diff line change
Expand Up @@ -619,6 +619,92 @@ impl Storage for GraphStorage {
self.clear_cache(storage_entry);
})
}

#[cfg(test)]
fn assert_integrity(
&self,
expected_txs: &[ArcPoolTx],
) -> Vec<(Self::StorageIndex, bool)> {
use std::ops::Deref;

let mut txs_map: HashMap<TxId, ArcPoolTx> = expected_txs
.iter()
.map(|tx| (tx.id(), tx.clone()))
.collect();
let mut tx_id_node_id = HashMap::new();
let mut txs_info = Vec::new();

for node_id in self.graph.node_indices() {
let node = self
.graph
.node_weight(node_id)
.expect("A node not expected exists in storage");
let has_dependencies = Storage::has_dependencies(self, &node_id);
let tx_id = node.transaction.id();
let tx = txs_map
.remove(&tx_id)
.expect("A transaction not expected exists in storage");
assert_eq!(tx.deref(), node.transaction.deref());
tx_id_node_id.insert(tx_id, node_id);
txs_info.push((node_id, has_dependencies));
}
assert!(
txs_map.is_empty(),
"Some transactions are missing in storage {:?}",
txs_map.keys()
);

let mut coins_creators = HashMap::new();
let mut contracts_creators = HashMap::new();
for expected_tx in expected_txs {
for (i, output) in expected_tx.outputs().iter().enumerate() {
match output {
Output::Coin { .. } => {
let utxo_id =
UtxoId::new(expected_tx.id(), i.try_into().unwrap());
coins_creators.insert(utxo_id, expected_tx.id());
}
Output::ContractCreated { contract_id, .. } => {
contracts_creators.insert(*contract_id, expected_tx.id());
}
_ => {}
rafal-ch marked this conversation as resolved.
Show resolved Hide resolved
}
}
}
for (utxo_id, node_id) in &self.coins_creators {
let tx_id = coins_creators.remove(utxo_id).unwrap_or_else(|| panic!("A coin creator (coin: {}) is present in the storage that shouldn't be there", utxo_id));
let expected_node_id = tx_id_node_id.get(&tx_id).unwrap_or_else(|| {
panic!("A node id is missing for a transaction (tx_id: {})", tx_id)
});
assert_eq!(
expected_node_id, node_id,
"The node id is different from the expected one"
);
}
assert!(
coins_creators.is_empty(),
"Some contract creators are missing in storage: {:?}",
coins_creators
);

for (contract_id, node_id) in &self.contracts_creators {
let tx_id = contracts_creators.remove(contract_id).unwrap_or_else(|| panic!("A contract creator (contract: {}) is present in the storage that shouldn't be there", contract_id));
let expected_node_id = tx_id_node_id.get(&tx_id).unwrap_or_else(|| {
panic!("A node id is missing for a transaction (tx_id: {})", tx_id)
});
assert_eq!(
expected_node_id, node_id,
"The node id is different from the expected one"
);
}
assert!(
contracts_creators.is_empty(),
"Some contract creators are missing in storage: {:?}",
contracts_creators
);

txs_info
}
}

impl RatioTipGasSelectionAlgorithmStorage for GraphStorage {
Expand Down
8 changes: 8 additions & 0 deletions crates/services/txpool_v2/src/storage/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -102,4 +102,12 @@ pub trait Storage {

/// Remove a transaction from the storage.
fn remove_transaction(&mut self, index: Self::StorageIndex) -> Option<StorageData>;

#[cfg(test)]
/// Asserts the integrity of the storage.
/// Returns the list of the storage indexes and a boolean value indicating whether the transaction has dependencies or not.
fn assert_integrity(
&self,
expected_txs: &[ArcPoolTx],
) -> Vec<(Self::StorageIndex, bool)>;
}
2 changes: 2 additions & 0 deletions crates/services/txpool_v2/src/tests/mod.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
#![allow(non_snake_case)]

mod mocks;
mod stability_test;
mod tests_e2e;
Expand Down
1 change: 0 additions & 1 deletion crates/services/txpool_v2/src/tests/stability_test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
//! correct(not in the unexpected state).
//! It relies on the `debug_assert` which are present in the code.

#![allow(non_snake_case)]
#![allow(clippy::cast_possible_truncation)]
#![allow(clippy::arithmetic_side_effects)]

Expand Down
Loading
Loading