diff --git a/.changes/added/2848.md b/.changes/added/2848.md new file mode 100644 index 00000000000..e0a2667cf55 --- /dev/null +++ b/.changes/added/2848.md @@ -0,0 +1 @@ +Link all components of preconfirmations and add E2E tests. \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index 31816412a54..c614a10ecfb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3480,6 +3480,7 @@ dependencies = [ "fuel-core", "fuel-core-chain-config", "fuel-core-metrics", + "fuel-core-poa", "fuel-core-shared-sequencer", "fuel-core-storage", "fuel-core-types 0.42.0", @@ -4094,6 +4095,7 @@ dependencies = [ "fuel-core-types 0.42.0", "fuel-vm 0.60.0", "k256", + "postcard", "rand 0.8.5", "secrecy", "serde", diff --git a/bin/fuel-core/Cargo.toml b/bin/fuel-core/Cargo.toml index deaf51fb2e8..3026651d68e 100644 --- a/bin/fuel-core/Cargo.toml +++ b/bin/fuel-core/Cargo.toml @@ -28,6 +28,7 @@ dotenvy = { version = "0.15", optional = true } fuel-core = { workspace = true, features = ["wasm-executor"] } fuel-core-chain-config = { workspace = true } fuel-core-metrics = { workspace = true } +fuel-core-poa = { workspace = true, optional = true } fuel-core-shared-sequencer = { workspace = true, optional = true } fuel-core-types = { workspace = true, features = ["std"] } hex = { workspace = true } @@ -62,7 +63,7 @@ test-case = { workspace = true } default = ["env", "relayer", "rocksdb"] aws-kms = ["dep:aws-config", "dep:aws-sdk-kms", "fuel-core-types/aws-kms"] env = ["dep:dotenvy"] -p2p = ["fuel-core/p2p", "const_format"] +p2p = ["fuel-core/p2p", "const_format", "dep:fuel-core-poa"] shared-sequencer = [ "dep:fuel-core-shared-sequencer", "fuel-core/shared-sequencer", diff --git a/bin/fuel-core/src/cli/run.rs b/bin/fuel-core/src/cli/run.rs index ab7ac35e499..d1ab672c036 100644 --- a/bin/fuel-core/src/cli/run.rs +++ b/bin/fuel-core/src/cli/run.rs @@ -110,6 +110,8 @@ mod shared_sequencer; mod consensus; mod gas_price; mod graphql; +#[cfg(feature = "p2p")] +mod preconfirmation_signature_service; mod profiling; #[cfg(feature = "relayer")] mod relayer; @@ -274,6 +276,11 @@ pub struct Command { #[cfg(feature = "p2p")] pub sync_args: p2p::SyncArgs, + #[cfg_attr(feature = "p2p", clap(flatten))] + #[cfg(feature = "p2p")] + pub pre_confirmation_signature_service_args: + preconfirmation_signature_service::PreconfirmationArgs, + #[cfg_attr(feature = "shared-sequencer", clap(flatten))] #[cfg(feature = "shared-sequencer")] pub shared_sequencer_args: shared_sequencer::Args, @@ -343,6 +350,8 @@ impl Command { p2p_args, #[cfg(feature = "p2p")] sync_args, + #[cfg(feature = "p2p")] + pre_confirmation_signature_service_args, #[cfg(feature = "shared-sequencer")] shared_sequencer_args, metrics, @@ -406,6 +415,17 @@ impl Command { metrics.is_enabled(Module::P2P), )?; + #[cfg(feature = "p2p")] + let preconfirmation_signature_service_config = + fuel_core_poa::pre_confirmation_signature_service::config::Config { + key_rotation_interval: *pre_confirmation_signature_service_args + .key_rotation_interval, + key_expiration_interval: *pre_confirmation_signature_service_args + .key_expiration_interval, + echo_delegation_interval: *pre_confirmation_signature_service_args + .echo_delegation_interval, + }; + let trigger: Trigger = poa_trigger.into(); if trigger != Trigger::Never { @@ -703,6 +723,8 @@ impl Command { p2p: p2p_cfg, #[cfg(feature = "p2p")] sync: sync_args.into(), + #[cfg(feature = "p2p")] + pre_confirmation_signature_service: preconfirmation_signature_service_config, #[cfg(feature = "shared-sequencer")] shared_sequencer: shared_sequencer_args.try_into()?, consensus_signer, diff --git a/bin/fuel-core/src/cli/run/preconfirmation_signature_service.rs b/bin/fuel-core/src/cli/run/preconfirmation_signature_service.rs new file mode 100644 index 00000000000..528149ac03d --- /dev/null +++ b/bin/fuel-core/src/cli/run/preconfirmation_signature_service.rs @@ -0,0 +1,24 @@ +#[derive(Debug, Clone, clap::Args)] +pub struct PreconfirmationArgs { + /// The frequency at which we rotate the preconfirmation sub-key + #[clap( + long = "preconfirmation-key-rotation-frequency", + env, + default_value = "10m" + )] + pub key_rotation_interval: humantime::Duration, + /// The frequency at which the preconfirmation sub-key expires + #[clap( + long = "preconfirmation-key-expiration-frequency", + env, + default_value = "20m" + )] + pub key_expiration_interval: humantime::Duration, + /// The frequency at which the preconfirmation sub-key is echoed to the network + #[clap( + long = "preconfirmation-echo-delegation-frequency", + env, + default_value = "10s" + )] + pub echo_delegation_interval: humantime::Duration, +} diff --git a/bin/fuel-core/src/cli/snapshot.rs b/bin/fuel-core/src/cli/snapshot.rs index d009f9d6a21..02a5b3b39ea 100644 --- a/bin/fuel-core/src/cli/snapshot.rs +++ b/bin/fuel-core/src/cli/snapshot.rs @@ -290,7 +290,7 @@ mod tests { UtxoId, }, fuel_types::ChainId, - services::txpool::TransactionExecutionStatus, + services::transaction_status::TransactionExecutionStatus, tai64::Tai64, }; use itertools::Itertools; diff --git a/crates/fuel-core/Cargo.toml b/crates/fuel-core/Cargo.toml index 001ddd76e7c..1d0f957422a 100644 --- a/crates/fuel-core/Cargo.toml +++ b/crates/fuel-core/Cargo.toml @@ -114,6 +114,7 @@ test-helpers = [ "fuel-core-services/test-helpers", "fuel-core-shared-sequencer?/test-helpers", "fuel-core-importer/test-helpers", + "fuel-core-poa/test-helpers", "dep:mockall", ] # features to enable in production, but increase build times diff --git a/crates/fuel-core/src/database/transactions.rs b/crates/fuel-core/src/database/transactions.rs index 24f630c2a56..8eed97ddff8 100644 --- a/crates/fuel-core/src/database/transactions.rs +++ b/crates/fuel-core/src/database/transactions.rs @@ -21,7 +21,7 @@ use fuel_core_types::{ TxPointer, }, fuel_types::Address, - services::txpool::TransactionExecutionStatus, + services::transaction_status::TransactionExecutionStatus, }; #[cfg(feature = "test-helpers")] diff --git a/crates/fuel-core/src/executor.rs b/crates/fuel-core/src/executor.rs index 80555fbd934..2da3806cc06 100644 --- a/crates/fuel-core/src/executor.rs +++ b/crates/fuel-core/src/executor.rs @@ -17,7 +17,10 @@ mod tests { }; #[cfg(not(feature = "wasm-executor"))] - use fuel_core_types::services::preconfirmation::PreconfirmationStatus; + use fuel_core_types::services::preconfirmation::{ + Preconfirmation, + PreconfirmationStatus, + }; use crate as fuel_core; use fuel_core::database::Database; @@ -3117,19 +3120,19 @@ mod tests { async fn execute_block__send_preconfirmations() { // Given struct MockPreconfirmationsSender { - sender: tokio::sync::mpsc::Sender>, + sender: tokio::sync::mpsc::Sender>, } impl PreconfirmationSenderPort for MockPreconfirmationsSender { fn try_send( &self, - preconfirmations: Vec, - ) -> Vec { + preconfirmations: Vec, + ) -> Vec { preconfirmations } /// Send a batch of pre-confirmations, awaiting for the send to be successful. - async fn send(&self, preconfirmations: Vec) { + async fn send(&self, preconfirmations: Vec) { self.sender.send(preconfirmations).await.unwrap(); } } @@ -3173,7 +3176,7 @@ mod tests { let preconfirmations = receiver.recv().await.unwrap(); assert_eq!(preconfirmations.len(), 1); assert!(matches!( - preconfirmations[0], + preconfirmations[0].status, PreconfirmationStatus::Success { .. } )); assert_eq!(res.skipped_transactions.len(), 0); diff --git a/crates/fuel-core/src/graphql_api/database.rs b/crates/fuel-core/src/graphql_api/database.rs index 983fac12adb..d07f266c2c5 100644 --- a/crates/fuel-core/src/graphql_api/database.rs +++ b/crates/fuel-core/src/graphql_api/database.rs @@ -64,7 +64,7 @@ use fuel_core_types::{ fuel_vm::BlobData, services::{ graphql_api::ContractBalance, - txpool::TransactionExecutionStatus, + transaction_status::TransactionExecutionStatus, }, }; use futures::Stream; diff --git a/crates/fuel-core/src/graphql_api/ports.rs b/crates/fuel-core/src/graphql_api/ports.rs index 58b80f16964..ffb4cd62201 100644 --- a/crates/fuel-core/src/graphql_api/ports.rs +++ b/crates/fuel-core/src/graphql_api/ports.rs @@ -70,7 +70,7 @@ use fuel_core_types::{ }, graphql_api::ContractBalance, p2p::PeerInfo, - txpool::{ + transaction_status::{ self, TransactionStatus, }, @@ -90,7 +90,7 @@ pub trait OffChainDatabase: Send + Sync { fn tx_status( &self, tx_id: &TxId, - ) -> StorageResult; + ) -> StorageResult; fn balance( &self, @@ -363,7 +363,7 @@ pub mod worker { fuel_types::BlockHeight, services::{ block_importer::SharedImportResult, - txpool::{ + transaction_status::{ self, TransactionStatus, }, @@ -431,8 +431,8 @@ pub mod worker { fn update_tx_status( &mut self, id: &Bytes32, - status: txpool::TransactionExecutionStatus, - ) -> StorageResult>; + status: transaction_status::TransactionExecutionStatus, + ) -> StorageResult>; /// Update metadata about the total number of transactions on the chain. /// Returns the total count after the update. diff --git a/crates/fuel-core/src/graphql_api/storage.rs b/crates/fuel-core/src/graphql_api/storage.rs index 25b89ba68dd..420782a5aa8 100644 --- a/crates/fuel-core/src/graphql_api/storage.rs +++ b/crates/fuel-core/src/graphql_api/storage.rs @@ -32,7 +32,7 @@ use fuel_core_types::{ Bytes32, }, fuel_types::BlockHeight, - services::txpool::TransactionExecutionStatus, + services::transaction_status::TransactionExecutionStatus, }; use statistic::StatisticTable; diff --git a/crates/fuel-core/src/graphql_api/storage/transactions.rs b/crates/fuel-core/src/graphql_api/storage/transactions.rs index fb1cf1c2cda..56e8000c1d3 100644 --- a/crates/fuel-core/src/graphql_api/storage/transactions.rs +++ b/crates/fuel-core/src/graphql_api/storage/transactions.rs @@ -23,7 +23,7 @@ use fuel_core_types::{ Bytes32, }, fuel_types::BlockHeight, - services::txpool::TransactionExecutionStatus, + services::transaction_status::TransactionExecutionStatus, }; use std::{ array::TryFromSliceError, diff --git a/crates/fuel-core/src/graphql_api/worker_service/tests.rs b/crates/fuel-core/src/graphql_api/worker_service/tests.rs index 38c6b7e2a6f..10d9bac8f07 100644 --- a/crates/fuel-core/src/graphql_api/worker_service/tests.rs +++ b/crates/fuel-core/src/graphql_api/worker_service/tests.rs @@ -10,7 +10,7 @@ use fuel_core_storage::StorageAsRef; use fuel_core_types::{ fuel_tx::Bytes32, fuel_types::BlockHeight, - services::txpool::TransactionStatus, + services::transaction_status::TransactionStatus, }; use std::sync::Arc; diff --git a/crates/fuel-core/src/p2p_test_helpers.rs b/crates/fuel-core/src/p2p_test_helpers.rs index dd58beb1444..924fdfbd76f 100644 --- a/crates/fuel-core/src/p2p_test_helpers.rs +++ b/crates/fuel-core/src/p2p_test_helpers.rs @@ -364,7 +364,6 @@ pub async fn make_nodes( ); let mut test_txs = Vec::with_capacity(0); - node_config.block_production = Trigger::Instant; if let Some(( ProducerSetup { @@ -396,6 +395,7 @@ pub async fn make_nodes( } node_config.utxo_validation = utxo_validation; + node_config.txpool.utxo_validation = utxo_validation; update_signing_key(&mut node_config, Input::owner(&secret.public_key())); node_config.consensus_signer = SignMode::Key(Secret::new(secret.into())); @@ -422,6 +422,7 @@ pub async fn make_nodes( overrides, ); node_config.block_production = Trigger::Never; + node_config.consensus_signer = SignMode::Unavailable; if let Some(ValidatorSetup { pub_key, diff --git a/crates/fuel-core/src/query/message.rs b/crates/fuel-core/src/query/message.rs index a7868d57cb9..b95c6447006 100644 --- a/crates/fuel-core/src/query/message.rs +++ b/crates/fuel-core/src/query/message.rs @@ -31,7 +31,7 @@ use fuel_core_types::{ MessageId, Nonce, }, - services::txpool::TransactionExecutionStatus, + services::transaction_status::TransactionExecutionStatus, }; use futures::{ Stream, @@ -354,7 +354,7 @@ mod tests { BlockHeight, Nonce, }, - services::txpool::TransactionExecutionStatus, + services::transaction_status::TransactionExecutionStatus, tai64::Tai64, }; diff --git a/crates/fuel-core/src/query/subscriptions.rs b/crates/fuel-core/src/query/subscriptions.rs index 9904e37612b..4c94c0d243e 100644 --- a/crates/fuel-core/src/query/subscriptions.rs +++ b/crates/fuel-core/src/query/subscriptions.rs @@ -3,7 +3,7 @@ use fuel_core_storage::Result as StorageResult; use fuel_core_tx_status_manager::TxStatusMessage; use fuel_core_types::{ fuel_types::Bytes32, - services::txpool::TransactionStatus, + services::transaction_status::TransactionStatus, }; use futures::{ stream::BoxStream, diff --git a/crates/fuel-core/src/query/subscriptions/test.rs b/crates/fuel-core/src/query/subscriptions/test.rs index 9a3046eec06..eb033d69091 100644 --- a/crates/fuel-core/src/query/subscriptions/test.rs +++ b/crates/fuel-core/src/query/subscriptions/test.rs @@ -24,7 +24,7 @@ use fuel_core_tx_status_manager::TxStatusMessage; use fuel_core_txpool::error::RemovedReason; use fuel_core_types::{ fuel_types::Bytes32, - services::txpool::TransactionStatus, + services::transaction_status::TransactionStatus, tai64::Tai64, }; use futures::StreamExt; diff --git a/crates/fuel-core/src/query/tx.rs b/crates/fuel-core/src/query/tx.rs index de76ca0b288..1661c2065c8 100644 --- a/crates/fuel-core/src/query/tx.rs +++ b/crates/fuel-core/src/query/tx.rs @@ -14,7 +14,7 @@ use fuel_core_types::{ TxPointer, }, fuel_types::Address, - services::txpool::TransactionExecutionStatus, + services::transaction_status::TransactionExecutionStatus, }; use futures::{ Stream, diff --git a/crates/fuel-core/src/schema/tx.rs b/crates/fuel-core/src/schema/tx.rs index a108e9956ed..55999a03a27 100644 --- a/crates/fuel-core/src/schema/tx.rs +++ b/crates/fuel-core/src/schema/tx.rs @@ -84,7 +84,7 @@ use fuel_core_types::{ CheckPredicateParams, EstimatePredicates, }, - services::txpool, + services::transaction_status, }; use futures::{ Stream, @@ -712,7 +712,7 @@ async fn submit_and_await_status<'a>( Err(anyhow::anyhow!("Failed to get transaction status").into()) } }) - .take(2)) + .take(3)) } struct StatusChangeState<'a> { @@ -724,7 +724,7 @@ impl<'a> TxnStatusChangeState for StatusChangeState<'a> { async fn get_tx_status( &self, id: Bytes32, - ) -> StorageResult> { + ) -> StorageResult> { match self.query.tx_status(&id) { Ok(status) => Ok(Some(status.into())), Err(StorageError::NotFound(_, _)) => { diff --git a/crates/fuel-core/src/schema/tx/types.rs b/crates/fuel-core/src/schema/tx/types.rs index c88a30b2f65..40b78c2a851 100644 --- a/crates/fuel-core/src/schema/tx/types.rs +++ b/crates/fuel-core/src/schema/tx/types.rs @@ -83,7 +83,7 @@ use fuel_core_types::{ TransactionExecutionResult, TransactionExecutionStatus, }, - txpool::{ + transaction_status::{ self, TransactionStatus as TxStatus, }, @@ -163,7 +163,7 @@ impl SubmittedStatus { #[derive(Debug)] pub struct SuccessStatus { tx_id: TxId, - status: Arc, + status: Arc, } #[Object] @@ -214,7 +214,7 @@ impl SuccessStatus { #[derive(Debug)] pub struct PreconfirmationSuccessStatus { pub tx_id: TxId, - pub status: Arc, + pub status: Arc, } #[Object] @@ -268,7 +268,7 @@ impl PreconfirmationSuccessStatus { #[derive(Debug)] pub struct FailureStatus { tx_id: TxId, - status: Arc, + status: Arc, } #[Object] @@ -326,7 +326,7 @@ impl FailureStatus { #[derive(Debug)] pub struct PreconfirmationFailureStatus { pub tx_id: TxId, - pub status: Arc, + pub status: Arc, } #[Object] @@ -384,7 +384,7 @@ impl PreconfirmationFailureStatus { #[derive(Debug)] pub struct SqueezedOutStatus { pub tx_id: TxId, - pub status: Arc, + pub status: Arc, } #[Object] @@ -1095,7 +1095,7 @@ pub(crate) async fn get_tx_status( ) -> Result, StorageError> { let api_result = query .tx_status(&id) - .into_api_result::()?; + .into_api_result::()?; match api_result { Some(status) => { let status = TransactionStatus::new(id, status); diff --git a/crates/fuel-core/src/service.rs b/crates/fuel-core/src/service.rs index b18a1dc311e..b6d3c6d2a5e 100644 --- a/crates/fuel-core/src/service.rs +++ b/crates/fuel-core/src/service.rs @@ -556,8 +556,8 @@ mod tests { // } #[cfg(feature = "p2p")] { - // p2p & sync - expected_services += 2; + // p2p & sync & preconfirmation signature service + expected_services += 3; } #[cfg(feature = "shared-sequencer")] { diff --git a/crates/fuel-core/src/service/adapters.rs b/crates/fuel-core/src/service/adapters.rs index 5ea99db0533..02de422f3c2 100644 --- a/crates/fuel-core/src/service/adapters.rs +++ b/crates/fuel-core/src/service/adapters.rs @@ -1,4 +1,11 @@ -use std::sync::Arc; +use std::{ + ops::Deref, + sync::Arc, +}; +use tokio::sync::{ + mpsc, + watch, +}; use fuel_core_consensus_module::{ block_verifier::Verifier, @@ -38,7 +45,7 @@ use fuel_core_types::{ Result as ExecutorResult, UncommittedResult, }, - preconfirmation::PreconfirmationStatus, + preconfirmation::Preconfirmation, }, signer::SignMode, tai64::Tai64, @@ -329,31 +336,38 @@ impl TransactionsSource { } pub struct NewTxWaiter { - pub receiver: tokio::sync::watch::Receiver<()>, + pub receiver: watch::Receiver<()>, pub timeout: Instant, } impl NewTxWaiter { - pub fn new(receiver: tokio::sync::watch::Receiver<()>, timeout: Instant) -> Self { + pub fn new(receiver: watch::Receiver<()>, timeout: Instant) -> Self { Self { receiver, timeout } } } #[derive(Clone)] pub struct PreconfirmationSender { - pub sender: tokio::sync::mpsc::Sender>, + pub sender_signature_service: mpsc::Sender>, + pub tx_status_manager_adapter: TxStatusManagerAdapter, } impl PreconfirmationSender { - pub fn new(sender: tokio::sync::mpsc::Sender>) -> Self { - Self { sender } + pub fn new( + sender_signature_service: mpsc::Sender>, + tx_status_manager_adapter: TxStatusManagerAdapter, + ) -> Self { + Self { + sender_signature_service, + tx_status_manager_adapter, + } } } #[derive(Clone)] pub struct ExecutorAdapter { pub(crate) executor: Arc>>, - pub new_txs_watcher: tokio::sync::watch::Receiver<()>, + pub new_txs_watcher: watch::Receiver<()>, pub preconfirmation_sender: PreconfirmationSender, } @@ -362,11 +376,10 @@ impl ExecutorAdapter { database: Database, relayer_database: Database, config: fuel_core_upgradable_executor::config::Config, - new_txs_watcher: tokio::sync::watch::Receiver<()>, - preconfirmation_sender: tokio::sync::mpsc::Sender>, + new_txs_watcher: watch::Receiver<()>, + preconfirmation_sender: PreconfirmationSender, ) -> Self { let executor = Executor::new(database, relayer_database, config); - let preconfirmation_sender = PreconfirmationSender::new(preconfirmation_sender); Self { executor: Arc::new(executor), new_txs_watcher, @@ -462,6 +475,14 @@ pub struct TxStatusManagerAdapter { tx_status_manager_shared_data: SharedData, } +impl Deref for TxStatusManagerAdapter { + type Target = SharedData; + + fn deref(&self) -> &Self::Target { + &self.tx_status_manager_shared_data + } +} + impl TxStatusManagerAdapter { pub fn new(tx_status_manager_shared_data: SharedData) -> Self { Self { @@ -478,6 +499,7 @@ impl TxStatusManagerAdapter { } } +#[derive(Clone)] pub struct FuelBlockSigner { mode: SignMode, } diff --git a/crates/fuel-core/src/service/adapters/consensus_module/poa.rs b/crates/fuel-core/src/service/adapters/consensus_module/poa.rs index ab7d65ade36..dcc81a28f40 100644 --- a/crates/fuel-core/src/service/adapters/consensus_module/poa.rs +++ b/crates/fuel-core/src/service/adapters/consensus_module/poa.rs @@ -89,7 +89,7 @@ impl TransactionPool for TxPoolAdapter { self.service.get_new_executable_txs_notifier() } - fn notify_skipped_txs(&self, tx_ids_and_reasons: Vec<(Bytes32, String)>) { + fn notify_skipped_txs(&self, tx_ids_and_reasons: Vec) { self.service.notify_skipped_txs(tx_ids_and_reasons) } } diff --git a/crates/fuel-core/src/service/adapters/consensus_module/poa/pre_confirmation_signature/broadcast.rs b/crates/fuel-core/src/service/adapters/consensus_module/poa/pre_confirmation_signature/broadcast.rs index 5ed1340e7a8..c989f1d3a90 100644 --- a/crates/fuel-core/src/service/adapters/consensus_module/poa/pre_confirmation_signature/broadcast.rs +++ b/crates/fuel-core/src/service/adapters/consensus_module/poa/pre_confirmation_signature/broadcast.rs @@ -1,8 +1,6 @@ use crate::service::adapters::{ - consensus_module::poa::pre_confirmation_signature::{ - key_generator::Ed25519Key, - parent_signature::FuelParentSigner, - }, + consensus_module::poa::pre_confirmation_signature::key_generator::Ed25519Key, + FuelBlockSigner, P2PAdapter, }; use fuel_core_poa::pre_confirmation_signature_service::{ @@ -26,35 +24,29 @@ use fuel_core_types::{ SignedByBlockProducerDelegation, SignedPreconfirmationByDelegate, }, - preconfirmation::{ - Preconfirmation, - Preconfirmations, - }, + preconfirmation::Preconfirmations, }, - tai64::Tai64, }; use std::sync::Arc; impl Broadcast for P2PAdapter { - type ParentKey = FuelParentSigner; + type ParentKey = FuelBlockSigner; type DelegateKey = Ed25519Key; - type Preconfirmations = Vec; + type Preconfirmations = Preconfirmations; async fn broadcast_preconfirmations( &mut self, preconfirmations: Self::Preconfirmations, signature: ::Signature, - expiration: Tai64, ) -> PreConfServiceResult<()> { if let Some(p2p) = &self.service { - let entity = Preconfirmations { - expiration, - preconfirmations, - }; let signature_bytes = signature.to_bytes(); let signature = Bytes64::new(signature_bytes); let preconfirmations = Arc::new(PreConfirmationMessage::Preconfirmations( - SignedPreconfirmationByDelegate { entity, signature }, + SignedPreconfirmationByDelegate { + entity: preconfirmations, + signature, + }, )); p2p.broadcast_preconfirmations(preconfirmations) .map_err(|e| PreConfServiceError::Broadcast(format!("{e:?}")))?; @@ -66,14 +58,15 @@ impl Broadcast for P2PAdapter { async fn broadcast_delegate_key( &mut self, delegate: DelegatePreConfirmationKey>, + nonce: u64, signature: ::Signature, ) -> PreConfServiceResult<()> { if let Some(p2p) = &self.service { - let sealed = SignedByBlockProducerDelegation { + let seal = SignedByBlockProducerDelegation { entity: delegate, signature, }; - let delegate_key = Arc::new(PreConfirmationMessage::Delegate(sealed)); + let delegate_key = Arc::new(PreConfirmationMessage::Delegate { seal, nonce }); p2p.broadcast_preconfirmations(delegate_key) .map_err(|e| PreConfServiceError::Broadcast(format!("{e:?}")))?; } @@ -102,8 +95,12 @@ mod tests { ed25519_dalek::VerifyingKey, services::{ p2p::ProtocolSignature, - preconfirmation::PreconfirmationStatus, + preconfirmation::{ + Preconfirmation, + PreconfirmationStatus, + }, }, + tai64::Tai64, }; #[tokio::test] @@ -114,22 +111,24 @@ mod tests { let peer_report_config = PeerReportConfig::default(); let service = Some(shared_state); let mut adapter = P2PAdapter::new(service, peer_report_config); - let preconfirmations = vec![Preconfirmation { - tx_id: Default::default(), - status: PreconfirmationStatus::Failure { - tx_pointer: Default::default(), - total_gas: 0, - total_fee: 0, - receipts: vec![], - outputs: vec![], - }, - }]; + let preconfirmations = Preconfirmations { + preconfirmations: vec![Preconfirmation { + tx_id: Default::default(), + status: PreconfirmationStatus::Failure { + tx_pointer: Default::default(), + total_gas: 0, + total_fee: 0, + receipts: vec![], + outputs: vec![], + }, + }], + expiration: Tai64::UNIX_EPOCH, + }; let signature = ed25519::Signature::from_bytes(&[5u8; 64]); - let expiration = Tai64::UNIX_EPOCH; // when adapter - .broadcast_preconfirmations(preconfirmations.clone(), signature, expiration) + .broadcast_preconfirmations(preconfirmations.clone(), signature) .await .unwrap(); @@ -140,9 +139,9 @@ mod tests { TaskRequest::BroadcastPreConfirmations(inner) if pre_conf_matches_expected_values( &inner, - &preconfirmations, + &preconfirmations.preconfirmations, &Bytes64::new(signature.to_bytes()), - &expiration, + preconfirmations.expiration, ) )); } @@ -151,15 +150,12 @@ mod tests { inner: &Arc, preconfirmations: &[Preconfirmation], signature: &Bytes64, - expiration: &Tai64, + expiration: Tai64, ) -> bool { - let entity = Preconfirmations { - expiration: *expiration, - preconfirmations: preconfirmations.to_vec(), - }; match &**inner { PreConfirmationMessage::Preconfirmations(signed_preconfirmation) => { - signed_preconfirmation.entity == entity + signed_preconfirmation.entity.preconfirmations == preconfirmations + && signed_preconfirmation.entity.expiration == expiration && signed_preconfirmation.signature == *signature } _ => false, @@ -183,7 +179,7 @@ mod tests { // when adapter - .broadcast_delegate_key(delegate.clone(), signature) + .broadcast_delegate_key(delegate.clone(), 0, signature) .await .unwrap(); @@ -212,7 +208,10 @@ mod tests { expiration, }; match &**inner { - PreConfirmationMessage::Delegate(signed_by_block_producer_delegation) => { + PreConfirmationMessage::Delegate { + seal: signed_by_block_producer_delegation, + .. + } => { signed_by_block_producer_delegation.entity == entity && signed_by_block_producer_delegation.signature == *signature } diff --git a/crates/fuel-core/src/service/adapters/consensus_module/poa/pre_confirmation_signature/parent_signature.rs b/crates/fuel-core/src/service/adapters/consensus_module/poa/pre_confirmation_signature/parent_signature.rs index 9da3b2425e0..551fce0805f 100644 --- a/crates/fuel-core/src/service/adapters/consensus_module/poa/pre_confirmation_signature/parent_signature.rs +++ b/crates/fuel-core/src/service/adapters/consensus_module/poa/pre_confirmation_signature/parent_signature.rs @@ -8,15 +8,12 @@ use fuel_core_poa::pre_confirmation_signature_service::{ use fuel_core_types::{ fuel_crypto, fuel_vm::Signature, - signer::SignMode, }; use serde::Serialize; -pub struct FuelParentSigner { - mode: SignMode, -} +use crate::service::adapters::FuelBlockSigner; -impl ParentSignature for FuelParentSigner { +impl ParentSignature for FuelBlockSigner { type Signature = Signature; async fn sign(&self, data: &T) -> PoAResult diff --git a/crates/fuel-core/src/service/adapters/consensus_module/poa/pre_confirmation_signature/trigger.rs b/crates/fuel-core/src/service/adapters/consensus_module/poa/pre_confirmation_signature/trigger.rs index 7d5e8f62598..03412799376 100644 --- a/crates/fuel-core/src/service/adapters/consensus_module/poa/pre_confirmation_signature/trigger.rs +++ b/crates/fuel-core/src/service/adapters/consensus_module/poa/pre_confirmation_signature/trigger.rs @@ -17,9 +17,11 @@ pub struct TimeBasedTrigger { impl TimeBasedTrigger { pub fn new(time: T, rotation_interval: Duration, expiration: Duration) -> Self { + let mut interval = tokio::time::interval(rotation_interval); + interval.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Skip); Self { time, - interval: tokio::time::interval(rotation_interval), + interval, expiration, } } diff --git a/crates/fuel-core/src/service/adapters/executor.rs b/crates/fuel-core/src/service/adapters/executor.rs index f8e8755441a..803eebb9b1d 100644 --- a/crates/fuel-core/src/service/adapters/executor.rs +++ b/crates/fuel-core/src/service/adapters/executor.rs @@ -17,7 +17,7 @@ use fuel_core_txpool::Constraints; use fuel_core_types::{ blockchain::primitives::DaBlockHeight, services::{ - preconfirmation::PreconfirmationStatus, + preconfirmation::Preconfirmation, relayer::Event, }, }; @@ -103,22 +103,36 @@ impl NewTxWaiterPort for NewTxWaiter { } impl PreconfirmationSenderPort for PreconfirmationSender { - async fn send(&self, preconfirmations: Vec) { + async fn send(&self, preconfirmations: Vec) { + // TODO: Avoid cloning of the `preconfirmations` + self.tx_status_manager_adapter + .tx_status_manager_shared_data + .update_preconfirmations(preconfirmations.clone()); + // If the receiver is closed, it means no one is listening to the preconfirmations and so we can drop them. // We don't consider this an error. - let _ = self.sender.send(preconfirmations).await; + let _ = self.sender_signature_service.send(preconfirmations).await; } - fn try_send( - &self, - preconfirmations: Vec, - ) -> Vec { - match self.sender.try_send(preconfirmations) { - Ok(()) => vec![], + fn try_send(&self, preconfirmations: Vec) -> Vec { + match self.sender_signature_service.try_reserve() { + Ok(permit) => { + // TODO: Avoid cloning of the `preconfirmations` + self.tx_status_manager_adapter + .tx_status_manager_shared_data + .update_preconfirmations(preconfirmations.clone()); + permit.send(preconfirmations); + vec![] + } // If the receiver is closed, it means no one is listening to the preconfirmations and so we can drop them. // We don't consider this an error. - Err(TrySendError::Closed(_)) => vec![], - Err(TrySendError::Full(preconfirmations)) => preconfirmations, + Err(TrySendError::Closed(_)) => { + self.tx_status_manager_adapter + .tx_status_manager_shared_data + .update_preconfirmations(preconfirmations); + vec![] + } + Err(TrySendError::Full(_)) => preconfirmations, } } } diff --git a/crates/fuel-core/src/service/adapters/graphql_api.rs b/crates/fuel-core/src/service/adapters/graphql_api.rs index 771c4a4e123..1724127410c 100644 --- a/crates/fuel-core/src/service/adapters/graphql_api.rs +++ b/crates/fuel-core/src/service/adapters/graphql_api.rs @@ -71,7 +71,7 @@ use fuel_core_types::{ TransactionExecutionStatus, }, p2p::PeerInfo, - txpool::TransactionStatus, + transaction_status::TransactionStatus, }, tai64::Tai64, }; diff --git a/crates/fuel-core/src/service/adapters/graphql_api/off_chain.rs b/crates/fuel-core/src/service/adapters/graphql_api/off_chain.rs index d1d57bc1664..f69bef7c721 100644 --- a/crates/fuel-core/src/service/adapters/graphql_api/off_chain.rs +++ b/crates/fuel-core/src/service/adapters/graphql_api/off_chain.rs @@ -82,7 +82,7 @@ use fuel_core_types::{ BlockHeight, Nonce, }, - services::txpool, + services::transaction_status, }; use std::iter; @@ -95,7 +95,7 @@ impl OffChainDatabase for OffChainIterableKeyValueView { fn tx_status( &self, tx_id: &TxId, - ) -> StorageResult { + ) -> StorageResult { self.get_tx_status(tx_id) .transpose() .ok_or(not_found!("TransactionId"))? diff --git a/crates/fuel-core/src/service/adapters/txpool.rs b/crates/fuel-core/src/service/adapters/txpool.rs index e133ad7143e..d64931a028b 100644 --- a/crates/fuel-core/src/service/adapters/txpool.rs +++ b/crates/fuel-core/src/service/adapters/txpool.rs @@ -4,8 +4,8 @@ use crate::{ BlockImporterAdapter, ChainStateInfoProvider, P2PAdapter, + PreconfirmationSender, StaticGasPrice, - TxStatusManagerAdapter, }, }; use fuel_core_services::stream::BoxStream; @@ -50,7 +50,14 @@ use fuel_core_types::{ PeerId, TransactionGossipData, }, - txpool::TransactionStatus, + preconfirmation::{ + Preconfirmation, + PreconfirmationStatus, + }, + transaction_status::{ + statuses, + TransactionStatus, + }, }, }; use std::sync::Arc; @@ -231,9 +238,40 @@ impl ChainStateInfoProviderTrait for ChainStateInfoProvider { } } -impl TxStatusManager for TxStatusManagerAdapter { +impl TxStatusManager for PreconfirmationSender { fn status_update(&self, tx_id: TxId, tx_status: TransactionStatus) { - self.tx_status_manager_shared_data + let permit = self.sender_signature_service.try_reserve(); + + if let Ok(permit) = permit { + if let TransactionStatus::SqueezedOut(status) = &tx_status { + let preconfirmation = Preconfirmation { + tx_id, + status: PreconfirmationStatus::SqueezedOut { + reason: status.reason.clone(), + }, + }; + permit.send(vec![preconfirmation]); + } + } + + self.tx_status_manager_adapter .update_status(tx_id, tx_status); } + + fn squeezed_out_txs(&self, statuses: Vec<(TxId, statuses::SqueezedOut)>) { + let permit = self.sender_signature_service.try_reserve(); + if let Ok(permit) = permit { + let preconfirmations = statuses + .iter() + .map(|(tx_id, status)| Preconfirmation { + tx_id: *tx_id, + status: PreconfirmationStatus::SqueezedOut { + reason: status.reason.clone(), + }, + }) + .collect(); + permit.send(preconfirmations); + } + self.tx_status_manager_adapter.update_statuses(statuses); + } } diff --git a/crates/fuel-core/src/service/config.rs b/crates/fuel-core/src/service/config.rs index bfe051ad498..9cc4d85cff8 100644 --- a/crates/fuel-core/src/service/config.rs +++ b/crates/fuel-core/src/service/config.rs @@ -77,6 +77,9 @@ pub struct Config { pub p2p: Option>, #[cfg(feature = "p2p")] pub sync: fuel_core_sync::Config, + #[cfg(feature = "p2p")] + pub pre_confirmation_signature_service: + fuel_core_poa::pre_confirmation_signature_service::config::Config, #[cfg(feature = "shared-sequencer")] pub shared_sequencer: fuel_core_shared_sequencer::Config, pub consensus_signer: SignMode, @@ -201,6 +204,10 @@ impl Config { p2p: Some(P2PConfig::::default(network_name.as_str())), #[cfg(feature = "p2p")] sync: fuel_core_sync::Config::default(), + #[cfg(feature = "p2p")] + pre_confirmation_signature_service: + fuel_core_poa::pre_confirmation_signature_service::config::Config::default( + ), #[cfg(feature = "shared-sequencer")] shared_sequencer: fuel_core_shared_sequencer::Config::local_node(), consensus_signer: SignMode::Key(fuel_core_types::secrecy::Secret::new( @@ -259,6 +266,23 @@ impl From<&Config> for fuel_core_poa::Config { } } +#[cfg(feature = "p2p")] +impl From<&Config> for fuel_core_poa::pre_confirmation_signature_service::config::Config { + fn from(value: &Config) -> Self { + fuel_core_poa::pre_confirmation_signature_service::config::Config { + echo_delegation_interval: value + .pre_confirmation_signature_service + .echo_delegation_interval, + key_expiration_interval: value + .pre_confirmation_signature_service + .key_expiration_interval, + key_rotation_interval: value + .pre_confirmation_signature_service + .key_rotation_interval, + } + } +} + #[derive( Clone, Copy, Debug, Display, Eq, PartialEq, EnumString, EnumVariantNames, ValueEnum, )] diff --git a/crates/fuel-core/src/service/query.rs b/crates/fuel-core/src/service/query.rs index ccf0cea4063..2aa3a64a402 100644 --- a/crates/fuel-core/src/service/query.rs +++ b/crates/fuel-core/src/service/query.rs @@ -7,7 +7,7 @@ use fuel_core_types::{ UniqueIdentifier, }, fuel_types::Bytes32, - services::txpool::TransactionStatus as TxPoolTxStatus, + services::transaction_status::TransactionStatus as TxPoolTxStatus, }; use futures::{ Stream, diff --git a/crates/fuel-core/src/service/sub_services.rs b/crates/fuel-core/src/service/sub_services.rs index 5e94db342ab..054cef19d94 100644 --- a/crates/fuel-core/src/service/sub_services.rs +++ b/crates/fuel-core/src/service/sub_services.rs @@ -14,11 +14,7 @@ use fuel_core_gas_price_service::v1::{ uninitialized_task::new_gas_price_service_v1, }; -#[cfg(feature = "p2p")] -use fuel_core_poa::pre_confirmation_signature_service::PreConfirmationSignatureTask; use fuel_core_poa::Trigger; -#[cfg(feature = "p2p")] -use fuel_core_services::ServiceRunner; use fuel_core_storage::{ self, transactional::AtomicView, @@ -34,11 +30,7 @@ use crate::relayer::Config as RelayerConfig; #[cfg(feature = "p2p")] use crate::service::adapters::consensus_module::poa::pre_confirmation_signature::{ - key_generator::{ - Ed25519Key, - Ed25519KeyGenerator, - }, - parent_signature::FuelParentSigner, + key_generator::Ed25519KeyGenerator, trigger::TimeBasedTrigger, tx_receiver::PreconfirmationsReceiver, }; @@ -80,6 +72,7 @@ use crate::{ ExecutorAdapter, MaybeRelayerAdapter, PoAAdapter, + PreconfirmationSender, SharedMemoryPool, SystemTime, TxPoolAdapter, @@ -127,8 +120,11 @@ pub fn init_sub_services( let chain_name = chain_config.chain_name.clone(); let on_chain_view = database.on_chain().latest_view()?; let (new_txs_updater, new_txs_watcher) = tokio::sync::watch::channel(()); - let (preconfirmation_sender, _preconfirmation_receiver) = + #[cfg(feature = "p2p")] + let (preconfirmation_sender, preconfirmation_receiver) = tokio::sync::mpsc::channel(1024); + #[cfg(not(feature = "p2p"))] + let (preconfirmation_sender, _) = tokio::sync::mpsc::channel(1024); let genesis_block = on_chain_view .genesis_block()? @@ -148,6 +144,49 @@ pub fn init_sub_services( )); } + #[cfg(feature = "p2p")] + let p2p_externals = config + .p2p + .clone() + .map(fuel_core_p2p::service::build_shared_state); + + #[cfg(feature = "p2p")] + let p2p_adapter = { + use crate::service::adapters::PeerReportConfig; + + // Hardcoded for now, but left here to be configurable in the future. + // TODO: https://github.com/FuelLabs/fuel-core/issues/1340 + let peer_report_config = PeerReportConfig { + successful_block_import: 5., + missing_block_headers: -100., + bad_block_header: -100., + missing_transactions: -100., + invalid_transactions: -100., + }; + P2PAdapter::new( + p2p_externals.as_ref().map(|ext| ext.0.clone()), + peer_report_config, + ) + }; + + #[cfg(not(feature = "p2p"))] + let p2p_adapter = P2PAdapter::new(); + + let protocol_pubkey = + ConsensusConfigProtocolPublicKey::new(chain_config.consensus.clone()); + + let tx_status_manager = fuel_core_tx_status_manager::new_service( + p2p_adapter.clone(), + config.tx_status_manager.clone(), + protocol_pubkey, + ); + let tx_status_manager_adapter = + TxStatusManagerAdapter::new(tx_status_manager.shared.clone()); + let preconfirmation_sender = PreconfirmationSender::new( + preconfirmation_sender, + tx_status_manager_adapter.clone(), + ); + let upgradable_executor_config = fuel_core_upgradable_executor::config::Config { forbid_fake_coins_default: config.utxo_validation, native_executor_version: config.native_executor_version, @@ -158,7 +197,7 @@ pub fn init_sub_services( database.relayer().clone(), upgradable_executor_config, new_txs_watcher, - preconfirmation_sender, + preconfirmation_sender.clone(), ); let import_result_provider = ImportResultProvider::new(database.on_chain().clone(), executor.clone()); @@ -206,34 +245,6 @@ pub fn init_sub_services( ), }; - #[cfg(feature = "p2p")] - let p2p_externals = config - .p2p - .clone() - .map(fuel_core_p2p::service::build_shared_state); - - #[cfg(feature = "p2p")] - let p2p_adapter = { - use crate::service::adapters::PeerReportConfig; - - // Hardcoded for now, but left here to be configurable in the future. - // TODO: https://github.com/FuelLabs/fuel-core/issues/1340 - let peer_report_config = PeerReportConfig { - successful_block_import: 5., - missing_block_headers: -100., - bad_block_header: -100., - missing_transactions: -100., - invalid_transactions: -100., - }; - P2PAdapter::new( - p2p_externals.as_ref().map(|ext| ext.0.clone()), - peer_report_config, - ) - }; - - #[cfg(not(feature = "p2p"))] - let p2p_adapter = P2PAdapter::new(); - let genesis_block_height = *genesis_block.header().height(); let settings = chain_state_info_provider.clone(); let block_stream = importer_adapter.events_shared_result(); @@ -263,17 +274,6 @@ pub fn init_sub_services( universal_gas_price_provider.clone(), ); - let protocol_pubkey = - ConsensusConfigProtocolPublicKey::new(chain_config.consensus.clone()); - - let tx_status_manager = fuel_core_tx_status_manager::new_service( - p2p_adapter.clone(), - config.tx_status_manager.clone(), - protocol_pubkey, - ); - let tx_status_manager_adapter = - TxStatusManagerAdapter::new(tx_status_manager.shared.clone()); - let txpool = fuel_core_txpool::new_service( chain_id, config.txpool.clone(), @@ -285,7 +285,7 @@ pub fn init_sub_services( universal_gas_price_provider.clone(), executor.clone(), new_txs_updater, - tx_status_manager_adapter.clone(), + preconfirmation_sender, ); let tx_pool_adapter = TxPoolAdapter::new(txpool.shared.clone()); @@ -320,12 +320,15 @@ pub fn init_sub_services( let poa_config: fuel_core_poa::Config = config.into(); let mut production_enabled = !matches!(poa_config.trigger, Trigger::Never); - if !production_enabled && config.debug { + if !production_enabled + && config.debug + && !matches!(poa_config.signer, SignMode::Unavailable) + { production_enabled = true; tracing::info!("Enabled manual block production because of `debug` flag"); } - let signer = Arc::new(FuelBlockSigner::new(config.consensus_signer.clone())); + let signer = FuelBlockSigner::new(config.consensus_signer.clone()); #[cfg(feature = "shared-sequencer")] let shared_sequencer = { @@ -334,7 +337,7 @@ pub fn init_sub_services( fuel_core_shared_sequencer::service::new_service( importer_adapter.clone(), config, - signer.clone(), + Arc::new(signer.clone()), )? }; @@ -342,16 +345,26 @@ pub fn init_sub_services( InDirectoryPredefinedBlocks::new(config.predefined_blocks_path.clone()); #[cfg(feature = "p2p")] - let _pre_confirmation_service: ServiceRunner< - PreConfirmationSignatureTask< - PreconfirmationsReceiver, - P2PAdapter, - FuelParentSigner, - Ed25519KeyGenerator, - Ed25519Key, - TimeBasedTrigger, - >, - >; + let config_preconfirmation: fuel_core_poa::pre_confirmation_signature_service::config::Config = + config.into(); + + #[cfg(feature = "p2p")] + let pre_confirmation_service = production_enabled + .then(|| { + fuel_core_poa::pre_confirmation_signature_service::new_service( + config_preconfirmation.clone(), + PreconfirmationsReceiver::new(preconfirmation_receiver), + p2p_adapter.clone(), + signer.clone(), + Ed25519KeyGenerator, + TimeBasedTrigger::new( + SystemTime, + config_preconfirmation.key_rotation_interval, + config_preconfirmation.key_expiration_interval, + ), + ) + }) + .transpose()?; let poa = production_enabled.then(|| { fuel_core_poa::new_service( @@ -362,7 +375,7 @@ pub fn init_sub_services( producer_adapter.clone(), importer_adapter.clone(), p2p_adapter.clone(), - signer, + Arc::new(signer), predefined_blocks, SystemTime, block_production_ready_signal, @@ -482,6 +495,9 @@ pub fn init_sub_services( if let Some(network) = network.take() { services.push(Box::new(network)); services.push(Box::new(sync)); + if let Some(pre_confirmation_service) = pre_confirmation_service { + services.push(Box::new(pre_confirmation_service)); + } } } #[cfg(feature = "shared-sequencer")] diff --git a/crates/services/consensus_module/poa/src/ports.rs b/crates/services/consensus_module/poa/src/ports.rs index a53eec10139..1165ba81ae0 100644 --- a/crates/services/consensus_module/poa/src/ports.rs +++ b/crates/services/consensus_module/poa/src/ports.rs @@ -31,7 +31,7 @@ use tokio::time::Instant; pub trait TransactionPool: Send + Sync { fn new_txs_watcher(&self) -> tokio::sync::watch::Receiver<()>; - fn notify_skipped_txs(&self, tx_ids_and_reasons: Vec<(Bytes32, String)>); + fn notify_skipped_txs(&self, tx_ids_and_reasons: Vec); } #[cfg_attr(test, mockall::automock)] diff --git a/crates/services/consensus_module/poa/src/pre_confirmation_signature_service.rs b/crates/services/consensus_module/poa/src/pre_confirmation_signature_service.rs index 09a2ee7f946..990ac304166 100644 --- a/crates/services/consensus_module/poa/src/pre_confirmation_signature_service.rs +++ b/crates/services/consensus_module/poa/src/pre_confirmation_signature_service.rs @@ -5,13 +5,20 @@ use fuel_core_services::{ EmptyShared, RunnableService, RunnableTask, + ServiceRunner, StateWatcher, TaskNextAction, }; use fuel_core_types::{ - services::p2p::{ - DelegatePreConfirmationKey, - Sealed, + services::{ + p2p::{ + DelegatePreConfirmationKey, + Sealed, + }, + preconfirmation::{ + Preconfirmation, + Preconfirmations, + }, }, tai64::Tai64, }; @@ -31,6 +38,7 @@ use crate::pre_confirmation_signature_service::{ }; pub mod broadcast; +pub mod config; pub mod error; pub mod key_generator; pub mod parent_signature; @@ -41,6 +49,23 @@ pub mod tx_receiver; #[cfg(test)] pub mod tests; +pub struct UninitializedPreConfirmationSignatureTask< + TxReceiver, + Broadcast, + Parent, + KeyGenerator, + KeyRotationTrigger, +> where + Parent: ParentSignature, +{ + tx_receiver: TxReceiver, + broadcast: Broadcast, + parent_signature: Parent, + key_generator: KeyGenerator, + key_rotation_trigger: KeyRotationTrigger, + echo_delegation_trigger: Interval, +} + pub struct PreConfirmationSignatureTask< TxReceiver, Broadcast, @@ -59,15 +84,16 @@ pub struct PreConfirmationSignatureTask< current_delegate_key: ExpiringKey, sealed_delegate_message: Sealed, Parent::Signature>, + nonce: u64, key_rotation_trigger: KeyRotationTrigger, echo_delegation_trigger: Interval, } #[async_trait::async_trait] -impl RunnableService - for PreConfirmationSignatureTask +impl RunnableService + for UninitializedPreConfirmationSignatureTask where - TxRcv: TxReceiver, + TxRcv: TxReceiver>, Brdcst: Broadcast< DelegateKey = DelegateKey, ParentKey = Parent, @@ -77,11 +103,11 @@ where Trigger: KeyRotationTrigger, DelegateKey: SigningKey, Parent: ParentSignature, - Preconfirmations: serde::Serialize + Send, { const NAME: &'static str = "PreconfirmationSignatureTask"; type SharedData = EmptyShared; - type Task = Self; + type Task = + PreConfirmationSignatureTask; type TaskParams = (); fn shared_data(&self) -> Self::SharedData { @@ -93,7 +119,46 @@ where _: &StateWatcher, _: Self::TaskParams, ) -> anyhow::Result { - Ok(self) + let Self { + tx_receiver, + mut broadcast, + parent_signature, + mut key_generator, + mut key_rotation_trigger, + echo_delegation_trigger, + } = self; + + // The first key rotation is triggered immediately + let expiration = key_rotation_trigger.next_rotation().await?; + let (new_delegate_key, sealed) = + create_delegate_key(&mut key_generator, &parent_signature, expiration) + .await + .map_err(|e| anyhow::anyhow!(e))?; + + let mut nonce = 0; + if let Err(e) = broadcast + .broadcast_delegate_key( + sealed.entity.clone(), + nonce, + sealed.signature.clone(), + ) + .await + { + tracing::error!("Failed to broadcast delegate key: {:?}", e); + } + nonce = nonce.saturating_add(1); + let initialized_task = PreConfirmationSignatureTask { + tx_receiver, + broadcast, + parent_signature, + key_generator, + current_delegate_key: new_delegate_key, + sealed_delegate_message: sealed, + nonce, + key_rotation_trigger, + echo_delegation_trigger, + }; + Ok(initialized_task) } } @@ -151,10 +216,10 @@ where Ok((new_delegate_key, sealed)) } -impl RunnableTask +impl RunnableTask for PreConfirmationSignatureTask where - TxRcv: TxReceiver, + TxRcv: TxReceiver>, Brdcst: Broadcast< DelegateKey = DelegateKey, ParentKey = Parent, @@ -164,7 +229,6 @@ where Trigger: KeyRotationTrigger, DelegateKey: SigningKey, Parent: ParentSignature, - Preconfirmations: serde::Serialize + Send, { async fn run(&mut self, watcher: &mut StateWatcher) -> TaskNextAction { tracing::debug!("Running pre-confirmation task"); @@ -174,11 +238,15 @@ where } res = self.tx_receiver.receive() => { tracing::debug!("Received transactions"); - let pre_confirmations = try_or_stop!(res); - let signature = try_or_stop!(self.current_delegate_key.sign(&pre_confirmations)); + let preconfirmations = try_or_stop!(res); let expiration = self.current_delegate_key.expiration(); + let pre_confirmations = Preconfirmations { + expiration, + preconfirmations, + }; + let signature = try_or_stop!(self.current_delegate_key.sign(&pre_confirmations)); try_or_continue!( - self.broadcast.broadcast_preconfirmations(pre_confirmations, signature, expiration).await, + self.broadcast.broadcast_preconfirmations(pre_confirmations, signature).await, |err| tracing::error!("Failed to broadcast pre-confirmations: {:?}", err) ); TaskNextAction::Continue @@ -195,11 +263,13 @@ where self.current_delegate_key = new_delegate_key; self.sealed_delegate_message = sealed.clone(); + self.nonce = 0; try_or_continue!( - self.broadcast.broadcast_delegate_key(sealed.entity, sealed.signature).await, + self.broadcast.broadcast_delegate_key(sealed.entity, self.nonce, sealed.signature).await, |err| tracing::error!("Failed to broadcast newly generated delegate key: {:?}", err) ); + self.nonce = self.nonce.saturating_add(1); TaskNextAction::Continue } _ = self.echo_delegation_trigger.tick() => { @@ -207,9 +277,10 @@ where let sealed = self.sealed_delegate_message.clone(); try_or_continue!( - self.broadcast.broadcast_delegate_key(sealed.entity, sealed.signature).await, + self.broadcast.broadcast_delegate_key(sealed.entity, self.nonce, sealed.signature).await, |err| tracing::error!("Failed to re-broadcast delegate key: {:?}", err) ); + self.nonce = self.nonce.saturating_add(1); TaskNextAction::Continue } } @@ -219,3 +290,37 @@ where Ok(()) } } + +pub type Service = ServiceRunner< + UninitializedPreConfirmationSignatureTask, +>; + +pub fn new_service( + config: config::Config, + preconfirmation_receiver: TxRcv, + p2p_adapter: Brdcst, + parent_signature: Parent, + key_generator: Gen, + key_rotation_trigger: Trigger, +) -> anyhow::Result> +where + TxRcv: TxReceiver>, + Brdcst: Broadcast< + DelegateKey = DelegateKey, + ParentKey = Parent, + Preconfirmations = Preconfirmations, + >, + Gen: KeyGenerator, + Trigger: KeyRotationTrigger, + DelegateKey: SigningKey, + Parent: ParentSignature, +{ + Ok(Service::new(UninitializedPreConfirmationSignatureTask { + tx_receiver: preconfirmation_receiver, + broadcast: p2p_adapter, + parent_signature, + key_generator, + key_rotation_trigger, + echo_delegation_trigger: tokio::time::interval(config.echo_delegation_interval), + })) +} diff --git a/crates/services/consensus_module/poa/src/pre_confirmation_signature_service/broadcast.rs b/crates/services/consensus_module/poa/src/pre_confirmation_signature_service/broadcast.rs index 6a605460bed..770dfd59ca0 100644 --- a/crates/services/consensus_module/poa/src/pre_confirmation_signature_service/broadcast.rs +++ b/crates/services/consensus_module/poa/src/pre_confirmation_signature_service/broadcast.rs @@ -4,10 +4,7 @@ use crate::pre_confirmation_signature_service::{ signing_key::SigningKey, }; use core::future::Future; -use fuel_core_types::{ - services::p2p::DelegatePreConfirmationKey, - tai64::Tai64, -}; +use fuel_core_types::services::p2p::DelegatePreConfirmationKey; #[allow(type_alias_bounds)] pub type PublicKey @@ -31,12 +28,12 @@ pub trait Broadcast: Send { &mut self, message: Self::Preconfirmations, signature: Signature, - expiration: Tai64, ) -> impl Future> + Send; fn broadcast_delegate_key( &mut self, delegate: DelegatePreConfirmationKey>, + nonce: u64, signature: ::Signature, ) -> impl Future> + Send; } diff --git a/crates/services/consensus_module/poa/src/pre_confirmation_signature_service/config.rs b/crates/services/consensus_module/poa/src/pre_confirmation_signature_service/config.rs new file mode 100644 index 00000000000..eb5e3dcb9a0 --- /dev/null +++ b/crates/services/consensus_module/poa/src/pre_confirmation_signature_service/config.rs @@ -0,0 +1,19 @@ +use std::time::Duration; + +#[derive(Debug, Clone)] +pub struct Config { + pub key_rotation_interval: Duration, + pub key_expiration_interval: Duration, + pub echo_delegation_interval: Duration, +} + +#[cfg(any(test, feature = "test-helpers"))] +impl Default for Config { + fn default() -> Self { + Self { + key_rotation_interval: Duration::from_secs(10), + key_expiration_interval: Duration::from_secs(30), + echo_delegation_interval: Duration::from_secs(5), + } + } +} diff --git a/crates/services/consensus_module/poa/src/pre_confirmation_signature_service/tests.rs b/crates/services/consensus_module/poa/src/pre_confirmation_signature_service/tests.rs index 9aef6df5f3a..ab79eadf4a1 100644 --- a/crates/services/consensus_module/poa/src/pre_confirmation_signature_service/tests.rs +++ b/crates/services/consensus_module/poa/src/pre_confirmation_signature_service/tests.rs @@ -12,9 +12,10 @@ use fuel_core_services::StateWatcher; use fuel_core_types::{ fuel_tx::{ Bytes32, - Transaction, + TxId, }, fuel_types::BlockHeight, + services::preconfirmation::PreconfirmationStatus, }; use std::time::Duration; @@ -26,13 +27,13 @@ pub enum Status { } pub struct FakeTxReceiver { - recv: tokio::sync::mpsc::Receiver>, + recv: tokio::sync::mpsc::Receiver>, } impl TxReceiver for FakeTxReceiver { - type Txs = Vec<(Transaction, Status)>; + type Txs = Vec; - async fn receive(&mut self) -> Result> { + async fn receive(&mut self) -> Result> { let txs = self.recv.recv().await; match txs { Some(txs) => Ok(txs), @@ -45,25 +46,22 @@ pub struct FakeBroadcast { delegation_key_sender: tokio::sync::mpsc::Sender< FakeParentSignedData>, >, - tx_sender: - tokio::sync::mpsc::Sender>>, + tx_sender: tokio::sync::mpsc::Sender>, } impl Broadcast for FakeBroadcast { type DelegateKey = FakeSigningKey; type ParentKey = FakeParentSignature; - type Preconfirmations = Vec<(Transaction, Status)>; + type Preconfirmations = Preconfirmations; async fn broadcast_preconfirmations( &mut self, message: Self::Preconfirmations, signature: Signature, - expiration: Tai64, ) -> Result<()> { let txs = FakeDelegateSignedData { data: message, dummy_signature: signature, - key_expiration: expiration, }; self.tx_sender.send(txs).await.map_err(|error| { Error::Broadcast(format!("Could not send {:?} over channel", error)) @@ -74,6 +72,7 @@ impl Broadcast for FakeBroadcast { async fn broadcast_delegate_key( &mut self, data: DelegatePreConfirmationKey>, + _: u64, signature: ::Signature, ) -> Result<()> { let delegate_key = FakeParentSignedData { @@ -100,7 +99,6 @@ pub struct FakeParentSignature { pub struct FakeDelegateSignedData { data: T, dummy_signature: String, - key_expiration: Tai64, } #[derive(Debug, Clone, Eq, PartialEq)] @@ -192,8 +190,8 @@ pub struct TestImplHandles { FakeParentSignedData>, >, pub broadcast_tx_handle: - tokio::sync::mpsc::Receiver>>, - pub tx_sender_handle: tokio::sync::mpsc::Sender>, + tokio::sync::mpsc::Receiver>, + pub tx_sender_handle: tokio::sync::mpsc::Sender>, } #[derive(Default)] @@ -242,6 +240,7 @@ impl TaskBuilder { key_generator, current_delegate_key, sealed_delegate_message: Sealed { entity, signature }, + nonce: 0, key_rotation_trigger, echo_delegation_trigger: tokio::time::interval(period), }; @@ -291,7 +290,7 @@ impl TaskBuilder { tokio::sync::mpsc::Receiver< FakeParentSignedData>, >, - tokio::sync::mpsc::Receiver>>, + tokio::sync::mpsc::Receiver>, ) { let (delegation_key_sender, delegation_key_receiver) = tokio::sync::mpsc::channel(10); @@ -307,7 +306,7 @@ impl TaskBuilder { &self, ) -> ( FakeTxReceiver, - tokio::sync::mpsc::Sender>, + tokio::sync::mpsc::Sender>, ) { let (tx_sender, tx_receiver) = tokio::sync::mpsc::channel(1); let tx_receiver = FakeTxReceiver { recv: tx_receiver }; @@ -424,7 +423,9 @@ async fn run__will_rebroadcast_generated_key_with_correct_signature_after_1_seco dummy_signature: dummy_signature.into(), }; - assert_eq!(actual_1, actual_2); + assert_eq!(actual_1.data.expiration, actual_2.data.expiration); + assert_eq!(actual_1.data.public_key, actual_2.data.public_key); + assert_eq!(actual_1.dummy_signature, actual_2.dummy_signature); assert_eq!(expected, actual_1); } @@ -463,18 +464,18 @@ async fn run__received_tx_will_be_broadcast_with_current_delegate_key_signature( // when let txs = vec![ - ( - Transaction::default(), - Status::Success { - height: BlockHeight::from(1), + Preconfirmation { + tx_id: TxId::zeroed(), + status: PreconfirmationStatus::SqueezedOut { + reason: "foo".into(), }, - ), - ( - Transaction::default(), - Status::Fail { - height: BlockHeight::from(2), + }, + Preconfirmation { + tx_id: TxId::zeroed(), + status: PreconfirmationStatus::SqueezedOut { + reason: "bar".into(), }, - ), + }, ]; tokio::task::spawn(async move { let _ = task.run(&mut state_watcher).await; @@ -491,9 +492,11 @@ async fn run__received_tx_will_be_broadcast_with_current_delegate_key_signature( let actual = handles.broadcast_tx_handle.try_recv().unwrap(); let dummy_signature = current_delegate_key.to_string(); let expected = FakeDelegateSignedData { - data: txs, + data: Preconfirmations { + expiration: Tai64::UNIX_EPOCH, + preconfirmations: txs, + }, dummy_signature, - key_expiration: Tai64::UNIX_EPOCH, }; assert_eq!(expected, actual); } diff --git a/crates/services/consensus_module/poa/src/service.rs b/crates/services/consensus_module/poa/src/service.rs index 375ef6c1865..dd964f52a2c 100644 --- a/crates/services/consensus_module/poa/src/service.rs +++ b/crates/services/consensus_module/poa/src/service.rs @@ -252,10 +252,23 @@ where }, RequestType::Trigger => { let now = self.clock.now(); - if now > self.last_timestamp { - Ok(now) - } else { - self.next_time(RequestType::Manual) + match self.trigger { + Trigger::Open { period } => { + let expected_timestamp = + increase_time(self.last_timestamp, period)?; + if now > expected_timestamp { + Ok(now) + } else { + Ok(expected_timestamp) + } + } + _ => { + if now > self.last_timestamp { + Ok(now) + } else { + self.next_time(RequestType::Manual) + } + } } } } @@ -393,7 +406,9 @@ where ); tx_ids_to_remove.push((tx_id, err.to_string())); } - self.txpool.notify_skipped_txs(tx_ids_to_remove.clone()); + self.txpool.notify_skipped_txs( + tx_ids_to_remove.iter().map(|(tx_id, _)| *tx_id).collect(), + ); self.tx_status_manager.notify_skipped_txs(tx_ids_to_remove); } @@ -415,7 +430,10 @@ where // Update last block time self.last_height = height; self.last_timestamp = block_time; - self.last_block_created = last_block_created; + self.last_block_created = match self.trigger { + Trigger::Open { .. } => deadline.max(last_block_created), + _ => last_block_created, + }; Ok(()) } diff --git a/crates/services/consensus_module/poa/src/service_test.rs b/crates/services/consensus_module/poa/src/service_test.rs index cc0a9f04587..ec08d5bdf7f 100644 --- a/crates/services/consensus_module/poa/src/service_test.rs +++ b/crates/services/consensus_module/poa/src/service_test.rs @@ -369,7 +369,6 @@ async fn remove_skipped_transactions() { txpool .expect_notify_skipped_txs() .returning(move |skipped_ids| { - let skipped_ids: Vec<_> = skipped_ids.into_iter().map(|(id, _)| id).collect(); // Transform transactions into ids. let skipped_transactions: Vec<_> = skipped_transactions .iter() diff --git a/crates/services/consensus_module/poa/src/service_test/trigger_tests.rs b/crates/services/consensus_module/poa/src/service_test/trigger_tests.rs index eed104810dc..810ef87a2a2 100644 --- a/crates/services/consensus_module/poa/src/service_test/trigger_tests.rs +++ b/crates/services/consensus_module/poa/src/service_test/trigger_tests.rs @@ -506,3 +506,80 @@ async fn interval_trigger_even_if_queued_tx_events() { }); block_creation_waiter.notified().await; } + +#[tokio::test] +async fn open_trigger__produce_blocks_in_time() { + // Given + let open_time = Duration::from_secs(10); + let quarter_of_open_time = open_time / 4; + let offset = Duration::from_secs(1); + let mut ctx = DefaultContext::new(Config { + trigger: Trigger::Open { period: open_time }, + signer: SignMode::Key(test_signing_key()), + metrics: false, + ..Default::default() + }) + .await; + time::sleep(offset).await; + + for _ in 0..10 { + // When + ctx.advance_time_with_tokio(); + time::sleep(quarter_of_open_time).await; + let first_quarter = ctx.block_import.try_recv(); + + ctx.advance_time_with_tokio(); + time::sleep(quarter_of_open_time).await; + let second_quarter = ctx.block_import.try_recv(); + + ctx.advance_time_with_tokio(); + time::sleep(quarter_of_open_time).await; + let third_quarter = ctx.block_import.try_recv(); + + ctx.advance_time_with_tokio(); + time::sleep(quarter_of_open_time).await; + let forth_quarter = ctx.block_import.try_recv(); + + // Then + assert!(first_quarter.is_err()); + assert!(second_quarter.is_err()); + assert!(third_quarter.is_err()); + assert!(forth_quarter.is_ok()); + } +} + +#[tokio::test] +async fn open_trigger__produce_blocks_with_correct_time() { + // Given + let open_time = Duration::from_secs(10); + let offset = Duration::from_secs(1); + let mut ctx = DefaultContext::new(Config { + trigger: Trigger::Open { period: open_time }, + signer: SignMode::Key(test_signing_key()), + metrics: false, + ..Default::default() + }) + .await; + let expected_first_block_time = ctx.now().0.checked_add(open_time.as_secs()).unwrap(); + let expected_second_block_time = expected_first_block_time + .checked_add(open_time.as_secs()) + .unwrap(); + + // When + time::sleep(offset).await; + ctx.advance_time_with_tokio(); + time::sleep(open_time).await; + let first_block = ctx.block_import.try_recv(); + + ctx.advance_time_with_tokio(); + time::sleep(open_time).await; + let second_block = ctx.block_import.try_recv(); + + // Then + assert!(first_block.is_ok()); + assert!(second_block.is_ok()); + let first_block_time = first_block.unwrap().entity.header().time(); + let second_block_time = second_block.unwrap().entity.header().time(); + assert_eq!(first_block_time.0, expected_first_block_time); + assert_eq!(second_block_time.0, expected_second_block_time); +} diff --git a/crates/services/executor/src/executor.rs b/crates/services/executor/src/executor.rs index a0a32d87fca..9a6499418b9 100644 --- a/crates/services/executor/src/executor.rs +++ b/crates/services/executor/src/executor.rs @@ -53,7 +53,11 @@ use fuel_core_types::{ contract::ContractUtxoInfo, RelayedTransaction, }, - fuel_asm::Word, + fuel_asm::{ + op, + PanicInstruction, + Word, + }, fuel_merkle::binary::root_calculator::MerkleRootCalculator, fuel_tx::{ field::{ @@ -88,6 +92,7 @@ use fuel_core_types::{ Input, Mint, Output, + PanicReason, Receipt, Transaction, TxId, @@ -116,8 +121,10 @@ use fuel_core_types::{ ExecutableTransaction, InterpreterParams, MemoryInstance, + NotSupportedEcal, }, state::StateTransition, + verification, Interpreter, ProgramState, }, @@ -136,7 +143,10 @@ use fuel_core_types::{ UncommittedValidationResult, ValidationResult, }, - preconfirmation::PreconfirmationStatus, + preconfirmation::{ + Preconfirmation, + PreconfirmationStatus, + }, relayer::Event, }, }; @@ -159,17 +169,6 @@ use alloc::{ vec, vec::Vec, }; -use fuel_core_types::{ - fuel_asm::{ - op, - PanicInstruction, - }, - fuel_tx::PanicReason, - fuel_vm::{ - interpreter::NotSupportedEcal, - verification, - }, -}; /// The maximum amount of transactions that can be included in a block, /// excluding the mint transaction. @@ -239,19 +238,20 @@ impl NewTxWaiterPort for TimeoutOnlyTxWaiter { pub struct TransparentPreconfirmationSender; impl PreconfirmationSenderPort for TransparentPreconfirmationSender { - fn try_send(&self, _: Vec) -> Vec { + fn try_send(&self, _: Vec) -> Vec { vec![] } - async fn send(&self, _: Vec) {} + async fn send(&self, _: Vec) {} } fn convert_tx_execution_result_to_preconfirmation( tx: &Transaction, + tx_id: TxId, tx_exec_result: &TransactionExecutionResult, block_height: BlockHeight, tx_index: u16, -) -> PreconfirmationStatus { +) -> Preconfirmation { let tx_pointer = TxPointer::new(block_height, tx_index); let dynamic_outputs = tx .outputs() @@ -265,7 +265,7 @@ fn convert_tx_execution_result_to_preconfirmation( }) .collect(); - match tx_exec_result { + let status = match tx_exec_result { TransactionExecutionResult::Success { receipts, total_gas, @@ -290,7 +290,8 @@ fn convert_tx_execution_result_to_preconfirmation( receipts: receipts.clone(), outputs: dynamic_outputs, }, - } + }; + Preconfirmation { tx_id, status } } /// Data that is generated after executing all transactions. @@ -808,6 +809,7 @@ where let preconfirmation_status = convert_tx_execution_result_to_preconfirmation( tx, + tx_id, &latest_executed_tx.result, *block.header.height(), data.tx_count, @@ -815,8 +817,11 @@ where status.push(preconfirmation_status); } Err(err) => { - status.push(PreconfirmationStatus::SqueezedOut { - reason: err.to_string(), + status.push(Preconfirmation { + tx_id, + status: PreconfirmationStatus::SqueezedOut { + reason: err.to_string(), + }, }); data.skipped_transactions.push((tx_id, err)); } diff --git a/crates/services/executor/src/ports.rs b/crates/services/executor/src/ports.rs index 4d18fc51752..0534f40f2f7 100644 --- a/crates/services/executor/src/ports.rs +++ b/crates/services/executor/src/ports.rs @@ -21,7 +21,7 @@ use fuel_core_types::{ fuel_vm::checked_transaction::CheckedTransaction, services::{ executor::Result as ExecutorResult, - preconfirmation::PreconfirmationStatus, + preconfirmation::Preconfirmation, relayer::Event, }, }; @@ -181,14 +181,11 @@ pub trait NewTxWaiterPort: Send { pub trait PreconfirmationSenderPort { /// Try to send a batch of pre-confirmations. Will succeed only if /// it can be directly sent. Otherwise it will return the batch. - fn try_send( - &self, - preconfirmations: Vec, - ) -> Vec; + fn try_send(&self, preconfirmations: Vec) -> Vec; /// Send a batch of pre-confirmations, awaiting for the send to be successful. fn send( &self, - preconfirmations: Vec, + preconfirmations: Vec, ) -> impl Future + Send; } diff --git a/crates/services/p2p/src/codecs/postcard.rs b/crates/services/p2p/src/codecs/postcard.rs index 296deb34590..07070d2dacc 100644 --- a/crates/services/p2p/src/codecs/postcard.rs +++ b/crates/services/p2p/src/codecs/postcard.rs @@ -63,7 +63,11 @@ where #[cfg(test)] #[allow(non_snake_case)] mod tests { - use fuel_core_types::blockchain::SealedBlockHeader; + use fuel_core_types::{ + blockchain::SealedBlockHeader, + fuel_tx::Transaction, + services::p2p::NetworkableTransactionPool, + }; use libp2p::request_response::Codec; use super::*; @@ -92,7 +96,7 @@ mod tests { } #[tokio::test] - async fn codec__serialization_roundtrip_using_v2_on_successful_response_returns_original_value( + async fn codec__serialization_roundtrip_using_v2_on_successful_response_returns_original_value__sealed_headers( ) { // Given let sealed_block_headers = vec![SealedBlockHeader::default()]; @@ -119,6 +123,37 @@ mod tests { )); } + #[tokio::test] + async fn codec__serialization_roundtrip_using_v2_on_successful_response_returns_original_value__full_transactions( + ) { + // Given + let full_transactions = vec![Some(NetworkableTransactionPool::Transaction( + Transaction::default_test_tx(), + ))]; + let response = + V2ResponseMessage::TxPoolFullTransactions(Ok(full_transactions.clone())); + let mut codec: RequestResponseMessageHandler = + RequestResponseMessageHandler::new(MAX_REQUEST_SIZE); + let mut buf = Vec::with_capacity(1024); + + // When + codec + .write_response(&RequestResponseProtocol::V2, &mut buf, response) + .await + .expect("Valid full transactions should be serialized using v2"); + + let deserialized = codec + .read_response(&RequestResponseProtocol::V2, &mut buf.as_slice()) + .await + .expect("Valid full transactions should be deserialized using v2"); + + // Then + assert!(matches!( + deserialized, + V2ResponseMessage::TxPoolFullTransactions(Ok(actual)) if actual == full_transactions + )); + } + #[tokio::test] async fn codec__serialization_roundtrip_using_v1_on_successful_response_returns_original_value( ) { diff --git a/crates/services/p2p/src/config.rs b/crates/services/p2p/src/config.rs index 6492c3c1fb2..97c213c8dd2 100644 --- a/crates/services/p2p/src/config.rs +++ b/crates/services/p2p/src/config.rs @@ -253,7 +253,7 @@ impl Config { database_read_threads: 0, tx_pool_threads: 0, state: NotInitialized, - subscribe_to_pre_confirmations: false, + subscribe_to_pre_confirmations: true, } } } diff --git a/crates/services/sync/src/import/tests.rs b/crates/services/sync/src/import/tests.rs index 6cd2c0f0e04..a32ee67d691 100644 --- a/crates/services/sync/src/import/tests.rs +++ b/crates/services/sync/src/import/tests.rs @@ -1495,12 +1495,6 @@ impl PeerReportTestBuilder { } } - #[allow(dead_code)] - pub fn debug(mut self) -> Self { - self.debug = true; - self - } - pub fn with_get_sealed_block_headers( mut self, get_headers: Option>, diff --git a/crates/services/tx_status_manager/src/manager.rs b/crates/services/tx_status_manager/src/manager.rs index fa11aa8a929..2f8a3e6b3d0 100644 --- a/crates/services/tx_status_manager/src/manager.rs +++ b/crates/services/tx_status_manager/src/manager.rs @@ -14,7 +14,7 @@ use fuel_core_types::{ Bytes32, TxId, }, - services::txpool::TransactionStatus, + services::transaction_status::TransactionStatus, }; use crate::{ @@ -212,7 +212,7 @@ mod tests { use test_case::test_case; use fuel_core_types::{ - services::txpool::TransactionStatus, + services::transaction_status::TransactionStatus, tai64::Tai64, }; diff --git a/crates/services/tx_status_manager/src/service.rs b/crates/services/tx_status_manager/src/service.rs index af1d8724581..86f3a6b4b99 100644 --- a/crates/services/tx_status_manager/src/service.rs +++ b/crates/services/tx_status_manager/src/service.rs @@ -41,7 +41,10 @@ use fuel_core_types::{ Preconfirmation, Preconfirmations, }, - txpool::TransactionStatus, + transaction_status::{ + statuses, + TransactionStatus, + }, }, tai64::Tai64, }; @@ -68,6 +71,12 @@ enum WriteRequest { tx_id: TxId, status: TransactionStatus, }, + UpdateStatuses { + statuses: Vec<(TxId, statuses::SqueezedOut)>, + }, + UpdatePreconfirmations { + preconfirmations: Vec, + }, NotifySkipped { tx_ids_and_reason: Vec<(Bytes32, String)>, }, @@ -102,9 +111,23 @@ impl SharedData { let _ = self.write_requests_sender.send(request); } + pub fn update_statuses(&self, statuses: Vec<(TxId, statuses::SqueezedOut)>) { + let request = WriteRequest::UpdateStatuses { statuses }; + let _ = self.write_requests_sender.send(request); + } + + pub fn update_preconfirmations(&self, preconfirmations: Vec) { + let request = WriteRequest::UpdatePreconfirmations { preconfirmations }; + if let Err(e) = self.write_requests_sender.send(request) { + tracing::error!("Failed to send preconfirmations: {:?}", e); + } + } + pub fn notify_skipped(&self, tx_ids_and_reason: Vec<(Bytes32, String)>) { let request = WriteRequest::NotifySkipped { tx_ids_and_reason }; - let _ = self.write_requests_sender.send(request); + if let Err(e) = self.write_requests_sender.send(request) { + tracing::error!("Failed to send skipped txs: {:?}", e); + } } } @@ -217,14 +240,14 @@ impl Task { preconfirmations: P2PPreConfirmationMessage, ) { match preconfirmations { - PreConfirmationMessage::Delegate(sealed) => { + PreConfirmationMessage::Delegate { seal, .. } => { tracing::debug!( "Received new delegate signature from peer: {:?}", - sealed.entity.public_key + seal.entity.public_key ); // TODO: Report peer for sending invalid delegation // https://github.com/FuelLabs/fuel-core/issues/2872 - let _ = self.signature_verification.add_new_delegate(&sealed); + let _ = self.signature_verification.add_new_delegate(&seal); } PreConfirmationMessage::Preconfirmations(sealed) => { tracing::debug!("Received new preconfirmations from peer"); @@ -292,6 +315,18 @@ impl RunnableTask for Task { self.manager.status_update(tx_id, status); TaskNextAction::Continue } + Some(WriteRequest::UpdateStatuses { statuses }) => { + for (tx_id, status) in statuses { + self.manager.status_update(tx_id, status.into()); + } + TaskNextAction::Continue + } + Some(WriteRequest::UpdatePreconfirmations { preconfirmations }) => { + for preconfirmation in preconfirmations { + self.manager.status_update(preconfirmation.tx_id, preconfirmation.status.into()); + } + TaskNextAction::Continue + } Some(WriteRequest::NotifySkipped { tx_ids_and_reason }) => { // TODO[RC]: This part not tested by TxStatusManager service tests yet. self.manager.notify_skipped_txs(tx_ids_and_reason); @@ -406,7 +441,7 @@ mod tests { Preconfirmation, Preconfirmations, }, - txpool::TransactionStatus, + transaction_status::TransactionStatus, }, tai64::Tai64, }; @@ -472,6 +507,7 @@ mod tests { pub(super) mod status { pub(super) mod preconfirmation { + use fuel_core_types::services::preconfirmation::PreconfirmationStatus; pub fn success() -> PreconfirmationStatus { @@ -479,8 +515,8 @@ mod tests { tx_pointer: Default::default(), total_gas: Default::default(), total_fee: Default::default(), - receipts: Default::default(), - outputs: Default::default(), + receipts: vec![], + outputs: vec![], } } } @@ -493,7 +529,7 @@ mod tests { rngs::StdRng, seq::SliceRandom, }, - services::txpool::{ + services::transaction_status::{ statuses::{ PreConfirmationFailure, PreConfirmationSuccess, @@ -657,8 +693,8 @@ mod tests { let bytes = postcard::to_allocvec(&entity).unwrap(); let message = Message::new(&bytes); let signature = Signature::sign(&protocol_secret_key, &message); - let sealed = Sealed { entity, signature }; - let inner = P2PPreConfirmationMessage::Delegate(sealed); + let seal = Sealed { entity, signature }; + let inner = P2PPreConfirmationMessage::Delegate { seal, nonce: 0 }; GossipData { data: Some(inner), peer_id: Default::default(), diff --git a/crates/services/tx_status_manager/src/tests/tests_e2e.rs b/crates/services/tx_status_manager/src/tests/tests_e2e.rs index 11499abddbe..e1d72339b54 100644 --- a/crates/services/tx_status_manager/src/tests/tests_e2e.rs +++ b/crates/services/tx_status_manager/src/tests/tests_e2e.rs @@ -6,7 +6,7 @@ use fuel_core_types::{ fuel_tx::Bytes32, - services::txpool::TransactionStatus, + services::transaction_status::TransactionStatus, }; use proptest::prelude::*; use test_strategy::{ diff --git a/crates/services/tx_status_manager/src/tests/tests_sending.rs b/crates/services/tx_status_manager/src/tests/tests_sending.rs index e8982184001..3a8d93d3345 100644 --- a/crates/services/tx_status_manager/src/tests/tests_sending.rs +++ b/crates/services/tx_status_manager/src/tests/tests_sending.rs @@ -6,7 +6,7 @@ use std::{ use fuel_core_types::{ fuel_tx::Bytes32, - services::txpool::TransactionStatus, + services::transaction_status::TransactionStatus, }; use parking_lot::lock_api::Mutex; use test_strategy::proptest; diff --git a/crates/services/tx_status_manager/src/tests/utils.rs b/crates/services/tx_status_manager/src/tests/utils.rs index f5580be54f4..08cb59ac161 100644 --- a/crates/services/tx_status_manager/src/tests/utils.rs +++ b/crates/services/tx_status_manager/src/tests/utils.rs @@ -5,7 +5,7 @@ use std::{ use fuel_core_types::{ fuel_tx::Bytes32, - services::txpool::TransactionStatus, + services::transaction_status::TransactionStatus, }; use proptest::{ prelude::*, diff --git a/crates/services/tx_status_manager/src/tx_status_stream.rs b/crates/services/tx_status_manager/src/tx_status_stream.rs index d9d6682edcb..d85243ac9ad 100644 --- a/crates/services/tx_status_manager/src/tx_status_stream.rs +++ b/crates/services/tx_status_manager/src/tx_status_stream.rs @@ -1,6 +1,6 @@ use fuel_core_types::{ fuel_tx::Bytes32, - services::txpool::TransactionStatus, + services::transaction_status::TransactionStatus, }; use std::pin::Pin; use tokio_stream::Stream; diff --git a/crates/services/tx_status_manager/src/update_sender.rs b/crates/services/tx_status_manager/src/update_sender.rs index 009980ff63d..8878e0de08d 100644 --- a/crates/services/tx_status_manager/src/update_sender.rs +++ b/crates/services/tx_status_manager/src/update_sender.rs @@ -27,9 +27,9 @@ use crate::tx_status_stream::{ }; /// Subscriber channel buffer size. -/// Subscribers will only ever get at most a submitted -/// and final transaction status update. -const BUFFER_SIZE: usize = 2; +/// Subscribers will only ever get at most a submitted, +/// a preconfirmation and final transaction status update. +const BUFFER_SIZE: usize = 3; #[derive(Clone)] pub struct TxStatusChange { diff --git a/crates/services/tx_status_manager/src/utils.rs b/crates/services/tx_status_manager/src/utils.rs index e0939e24b7d..3e301be460c 100644 --- a/crates/services/tx_status_manager/src/utils.rs +++ b/crates/services/tx_status_manager/src/utils.rs @@ -2,7 +2,7 @@ use fuel_core_types::{ blockchain::block::Block, services::{ executor::TransactionExecutionResult, - txpool::TransactionExecutionStatus, + transaction_status::TransactionExecutionStatus, }, }; diff --git a/crates/services/txpool_v2/src/error.rs b/crates/services/txpool_v2/src/error.rs index c731d1a3c04..9eecce52939 100644 --- a/crates/services/txpool_v2/src/error.rs +++ b/crates/services/txpool_v2/src/error.rs @@ -61,6 +61,15 @@ pub enum Error { }, } +impl Error { + pub fn is_duplicate_tx(&self) -> bool { + matches!( + self, + Error::InputValidation(InputValidationError::DuplicateTxId(_)) + ) + } +} + #[derive(Clone, Debug, derive_more::Display)] pub enum RemovedReason { #[display( diff --git a/crates/services/txpool_v2/src/pool.rs b/crates/services/txpool_v2/src/pool.rs index 43f5be5d346..708e27e017e 100644 --- a/crates/services/txpool_v2/src/pool.rs +++ b/crates/services/txpool_v2/src/pool.rs @@ -3,6 +3,7 @@ mod collisions; use std::{ collections::HashMap, iter, + sync::Arc, time::{ Instant, SystemTime, @@ -16,10 +17,14 @@ use fuel_core_types::{ field::BlobId, TxId, }, - services::txpool::{ - ArcPoolTx, - PoolTransaction, + services::{ + transaction_status::TransactionStatus, + txpool::{ + ArcPoolTx, + PoolTransaction, + }, }, + tai64::Tai64, }; use num_rational::Ratio; @@ -36,7 +41,10 @@ use crate::{ InsertionErrorType, }, extracted_outputs::ExtractedOutputs, - ports::TxPoolPersistentStorage, + ports::{ + TxPoolPersistentStorage, + TxStatusManager as TxStatusManagerTrait, + }, selection_algorithms::{ Constraints, SelectionAlgorithm, @@ -48,6 +56,8 @@ use crate::{ }, }; +use crate::error::RemovedReason; +use fuel_core_types::services::transaction_status::statuses; #[cfg(test)] use std::collections::HashSet; @@ -60,7 +70,7 @@ pub struct TxPoolStats { /// The pool is the main component of the txpool service. It is responsible for storing transactions /// and allowing the selection of transactions for inclusion in a block. -pub struct Pool { +pub struct Pool { /// Configuration of the pool. pub(crate) config: Config, /// The storage of the pool. @@ -81,9 +91,11 @@ pub struct Pool { pub(crate) pool_stats_sender: tokio::sync::watch::Sender, /// New executable transactions notifier. pub(crate) new_executable_txs_notifier: tokio::sync::watch::Sender<()>, + /// Transaction status manager. + pub(crate) tx_status_manager: Arc, } -impl Pool { +impl Pool { /// Create a new pool. pub fn new( storage: S, @@ -92,6 +104,7 @@ impl Pool { config: Config, pool_stats_sender: tokio::sync::watch::Sender, new_executable_txs_notifier: tokio::sync::watch::Sender<()>, + tx_status_manager: Arc, ) -> Self { Pool { storage, @@ -104,6 +117,7 @@ impl Pool { current_bytes_size: 0, pool_stats_sender, new_executable_txs_notifier, + tx_status_manager, } } @@ -113,21 +127,21 @@ impl Pool { } } -impl Pool +impl + Pool where S: Storage, CM: CollisionManager, SA: SelectionAlgorithm, + TxStatusManager: TxStatusManagerTrait, { /// Insert transactions into the pool. /// Returns a list of results for each transaction. - /// Each result is a list of transactions that were removed from the pool - /// because of the insertion of the new transaction. pub fn insert( &mut self, tx: ArcPoolTx, persistent_storage: &impl TxPoolPersistentStorage, - ) -> Result, InsertionErrorType> { + ) -> Result<(), InsertionErrorType> { let insertion_result = self.insert_inner(tx, persistent_storage); self.register_transaction_counts(); insertion_result @@ -135,9 +149,9 @@ where fn insert_inner( &mut self, - tx: std::sync::Arc, + tx: Arc, persistent_storage: &impl TxPoolPersistentStorage, - ) -> Result>, InsertionErrorType> { + ) -> Result<(), InsertionErrorType> { let CanStoreTransaction { checked_transaction, transactions_to_remove, @@ -186,6 +200,17 @@ where Storage::get(&self.storage, &storage_id).expect("Transaction is set above"); self.collision_manager.on_stored_transaction(storage_id, tx); + let duration = i64::try_from( + creation_instant + .duration_since(SystemTime::UNIX_EPOCH) + .expect("Time can't be less than UNIX EPOCH") + .as_secs(), + ) + .expect("Duration is less than i64::MAX"); + self.tx_status_manager.status_update( + tx_id, + TransactionStatus::submitted(Tai64::from_unix(duration)), + ); // No dependencies directly in the graph and the sorted transactions if !has_dependencies { self.selection_algorithm @@ -193,12 +218,23 @@ where self.new_executable_txs_notifier.send_replace(()); } + let status = statuses::SqueezedOut { + reason: Error::Removed(RemovedReason::LessWorth(tx_id)).to_string(), + }; + let removed_transactions = removed_transactions .into_iter() - .map(|data| data.transaction) + .map(|data| { + let removed_tx_id = data.transaction.id(); + + (removed_tx_id, status.clone()) + }) .collect::>(); + self.tx_status_manager + .squeezed_out_txs(removed_transactions); + self.update_stats(); - Ok(removed_transactions) + Ok(()) } fn update_stats(&self) { @@ -551,7 +587,8 @@ where pub fn remove_transaction_and_dependents( &mut self, tx_ids: Vec, - ) -> Vec { + tx_status: statuses::SqueezedOut, + ) { let mut removed_transactions = vec![]; for tx_id in tx_ids { if let Some(storage_id) = self.tx_id_to_storage_id.remove(&tx_id) { @@ -559,31 +596,30 @@ where .storage .remove_transaction_and_dependents_subtree(storage_id); self.update_components_and_caches_on_removal(removed.iter()); - removed_transactions - .extend(removed.into_iter().map(|data| data.transaction)); + removed_transactions.extend(removed.into_iter().map(|data| { + let tx_id = data.transaction.id(); + + (tx_id, tx_status.clone()) + })); } } + self.tx_status_manager + .squeezed_out_txs(removed_transactions); self.update_stats(); - - removed_transactions } - pub fn remove_skipped_transaction(&mut self, tx_id: TxId) -> Vec { + pub fn remove_skipped_transaction(&mut self, tx_id: TxId) { self.extracted_outputs.new_skipped_transaction(&tx_id); - let mut txs_removed = vec![]; let coin_dependents = self.collision_manager.get_coins_spenders(&tx_id); for dependent in coin_dependents { let removed = self .storage .remove_transaction_and_dependents_subtree(dependent); self.update_components_and_caches_on_removal(removed.iter()); - txs_removed.extend(removed.into_iter().map(|data| data.transaction)); } self.update_stats(); - - txs_removed } fn check_blob_does_not_exist( diff --git a/crates/services/txpool_v2/src/pool_worker.rs b/crates/services/txpool_v2/src/pool_worker.rs index a0fc9bffe25..7ef51c07383 100644 --- a/crates/services/txpool_v2/src/pool_worker.rs +++ b/crates/services/txpool_v2/src/pool_worker.rs @@ -8,6 +8,7 @@ use fuel_core_types::{ services::{ block_importer::SharedImportResult, p2p::GossipsubMessageInfo, + transaction_status::statuses, txpool::ArcPoolTx, }, }; @@ -36,10 +37,12 @@ use crate::{ error::{ Error, InsertionErrorType, - RemovedReason, }, pending_pool::PendingPool, - ports::TxPoolPersistentStorage, + ports::{ + TxPoolPersistentStorage, + TxStatusManager as TxStatusManagerTrait, + }, service::{ TxInfo, TxPool, @@ -66,13 +69,14 @@ pub(super) struct PoolWorkerInterface { } impl PoolWorkerInterface { - pub fn new( - tx_pool: TxPool, + pub fn new( + tx_pool: TxPool, view_provider: Arc>, limits: &ServiceChannelLimits, ) -> Self where View: TxPoolPersistentStorage, + TxStatusManager: TxStatusManagerTrait, { let (request_read_sender, request_read_receiver) = mpsc::channel(limits.max_pending_read_pool_requests); @@ -202,7 +206,7 @@ pub(super) enum PoolRemoveRequest { block_result: SharedImportResult, }, SkippedTransactions { - dependents_ids: Vec<(TxId, Error)>, + dependents_ids: Vec, }, TxAndCoinDependents { tx_and_dependents_ids: (Vec, Error), @@ -246,28 +250,25 @@ pub(super) enum PoolNotification { error: Error, source: InsertionSource, }, - Removed { - tx_id: TxId, - error: Error, - }, } -pub(super) struct PoolWorker { +pub(super) struct PoolWorker { tx_insert_from_pending_sender: Sender, thread_management_receiver: Receiver, request_remove_receiver: Receiver, request_read_receiver: Receiver, extract_block_transactions_receiver: Receiver, request_insert_receiver: Receiver, - pool: TxPool, + pool: TxPool, pending_pool: PendingPool, view_provider: Arc>, notification_sender: Sender, } -impl PoolWorker +impl PoolWorker where View: TxPoolPersistentStorage, + TxStatusManager: TxStatusManagerTrait, { pub async fn run(&mut self, pending_pool_expiration_check: &mut Interval) -> bool { let mut remove_buffer = vec![]; @@ -373,7 +374,7 @@ where let res = self.pool.insert(tx.clone(), &view); match res { - Ok(removed_txs) => { + Ok(()) => { let extended_source = match source { InsertionSource::P2P { from_peer_info } => { ExtendedInsertionSource::P2P { from_peer_info } @@ -404,19 +405,6 @@ where { tracing::error!("Failed to send inserted notification: {}", e); } - - for tx in removed_txs { - let removed_tx_id = tx.id(); - if let Err(e) = - self.notification_sender - .try_send(PoolNotification::Removed { - tx_id: removed_tx_id, - error: Error::Removed(RemovedReason::LessWorth(tx_id)), - }) - { - tracing::error!("Failed to send removed notification: {}", e); - } - } let resolved_txs = self.pending_pool.new_known_tx(tx); for (tx, source) in resolved_txs { @@ -499,38 +487,19 @@ where } } - fn remove_skipped_transactions(&mut self, parent_txs: Vec<(TxId, Error)>) { - for (tx_id, reason) in parent_txs { - let removed = self.pool.remove_skipped_transaction(tx_id); - for tx in removed { - let tx_id = tx.id(); - if let Err(e) = self.notification_sender.try_send(PoolNotification::Removed { - tx_id, - error: Error::SkippedTransaction( - format!("Parent transaction with id: {tx_id}, was removed because of: {reason}") - ) - }) { - tracing::error!("Failed to send removed notification: {}", e); - } - } + fn remove_skipped_transactions(&mut self, parent_txs: Vec) { + for tx_id in parent_txs { + self.pool.remove_skipped_transaction(tx_id); } } fn remove_and_coin_dependents(&mut self, tx_ids: (Vec, Error)) { let (tx_ids, error) = tx_ids; - let removed = self.pool.remove_transaction_and_dependents(tx_ids); - for tx in removed { - let tx_id = tx.id(); - if let Err(e) = self - .notification_sender - .try_send(PoolNotification::Removed { - tx_id, - error: error.clone(), - }) - { - tracing::error!("Failed to send removed notification: {}", e); - } - } + let tx_status = statuses::SqueezedOut { + reason: error.to_string(), + }; + self.pool + .remove_transaction_and_dependents(tx_ids, tx_status); } fn get_tx_ids(&mut self, max_txs: usize, tx_ids_sender: oneshot::Sender>) { diff --git a/crates/services/txpool_v2/src/ports.rs b/crates/services/txpool_v2/src/ports.rs index dc992e920bf..9eec852436f 100644 --- a/crates/services/txpool_v2/src/ports.rs +++ b/crates/services/txpool_v2/src/ports.rs @@ -29,16 +29,19 @@ use fuel_core_types::{ NetworkData, PeerId, }, - txpool::TransactionStatus, + transaction_status::TransactionStatus, }, }; use crate::GasPrice; pub use fuel_core_storage::transactional::AtomicView; +use fuel_core_types::services::transaction_status::statuses; pub trait TxStatusManager: Send + Sync + 'static { fn status_update(&self, tx_id: TxId, tx_status: TransactionStatus); + + fn squeezed_out_txs(&self, statuses: Vec<(TxId, statuses::SqueezedOut)>); } pub trait BlockImporter { diff --git a/crates/services/txpool_v2/src/service.rs b/crates/services/txpool_v2/src/service.rs index 14b83f5347f..b94229546d1 100644 --- a/crates/services/txpool_v2/src/service.rs +++ b/crates/services/txpool_v2/src/service.rs @@ -81,10 +81,8 @@ use fuel_core_types::{ PeerId, TransactionGossipData, }, - txpool::{ - ArcPoolTx, - TransactionStatus, - }, + transaction_status::TransactionStatus, + txpool::ArcPoolTx, }, tai64::Tai64, }; @@ -116,11 +114,12 @@ mod pruner; mod subscriptions; pub(crate) mod verifications; -pub type TxPool = Pool< +pub type TxPool = Pool< GraphStorage, ::StorageIndex, BasicCollisionManager<::StorageIndex>, RatioTipGasSelection, + TxStatusManager, >; pub(crate) type Shared = Arc>; @@ -161,6 +160,7 @@ impl TryFrom for TransactionStatus { } } +#[allow(clippy::enum_variant_names)] pub enum WritePoolRequest { InsertTxs { transactions: Vec>, @@ -372,15 +372,6 @@ where expiration, source, } => { - let duration = time - .duration_since(SystemTime::UNIX_EPOCH) - .expect("Time can't be less than UNIX EPOCH"); - // We do it at the top of the function to avoid any inconsistency in case of error - let Ok(duration) = i64::try_from(duration.as_secs()) else { - tracing::error!("Failed to convert the duration to i64"); - return - }; - match source { ExtendedInsertionSource::P2P { from_peer_info } => { let _ = self.p2p.notify_gossip_transaction_validity( @@ -404,10 +395,6 @@ where } self.pruner.time_txs_submitted.push_front((time, tx_id)); - self.tx_status_manager.status_update( - tx_id, - TransactionStatus::submitted(Tai64::from_unix(duration)), - ); if expiration < u32::MAX.into() { let block_height_expiration = self @@ -423,6 +410,8 @@ where error, source, } => { + let is_duplicate = error.is_duplicate_tx(); + let tx_status = TransactionStatus::squeezed_out(error.to_string()); match source { InsertionSource::P2P { from_peer_info } => { let _ = self.p2p.notify_gossip_transaction_validity( @@ -432,21 +421,19 @@ where } InsertionSource::RPC { response_channel } => { if let Some(channel) = response_channel { - let _ = channel.send(Err(error.clone())); + let _ = channel.send(Err(error)); } } } - self.tx_status_manager.status_update( - tx_id, - TransactionStatus::squeezed_out(error.to_string()), - ); - } - PoolNotification::Removed { tx_id, error } => { - self.tx_status_manager.status_update( - tx_id, - TransactionStatus::squeezed_out(error.to_string()), - ); + // If the transaction is a duplicate, we don't want to update + // the status with useless information for the end user. + // Transaction already is inserted into the pool, + // so main goal of the user is already achieved - + // his transaction is accepted by the pool. + if !is_duplicate { + self.tx_status_manager.status_update(tx_id, tx_status); + } } } } @@ -803,6 +790,7 @@ where let service_channel_limits = config.service_channel_limits; let utxo_validation = config.utxo_validation; + let tx_status_manager = Arc::new(tx_status_manager); let txpool = Pool::new( GraphStorage::new(GraphConfig { max_txs_chain_count: config.max_txs_chain_count, @@ -812,6 +800,7 @@ where config, pool_stats_sender, new_txs_notifier.clone(), + tx_status_manager.clone(), ); // BlockHeight is < 64 bytes, so we can use SeqLock @@ -847,6 +836,6 @@ where shared_state, metrics, tx_sync_history: Default::default(), - tx_status_manager: Arc::new(tx_status_manager), + tx_status_manager, }) } diff --git a/crates/services/txpool_v2/src/shared_state.rs b/crates/services/txpool_v2/src/shared_state.rs index 1130d630e83..25f669c57cc 100644 --- a/crates/services/txpool_v2/src/shared_state.rs +++ b/crates/services/txpool_v2/src/shared_state.rs @@ -147,15 +147,7 @@ impl SharedState { /// Notify the txpool that some transactions were skipped during block production. /// This is used to update the status of the skipped transactions internally and in subscriptions - pub fn notify_skipped_txs(&self, tx_ids_and_reason: Vec<(Bytes32, String)>) { - let dependents_ids = tx_ids_and_reason - .into_iter() - .map(|(tx_id, reason)| { - let error = Error::SkippedTransaction(reason); - (tx_id, error) - }) - .collect(); - + pub fn notify_skipped_txs(&self, dependents_ids: Vec) { if let Err(e) = self .request_remove_sender .try_send(PoolRemoveRequest::SkippedTransactions { dependents_ids }) diff --git a/crates/services/txpool_v2/src/tests/mocks.rs b/crates/services/txpool_v2/src/tests/mocks.rs index 18872c089a7..790e2d7da28 100644 --- a/crates/services/txpool_v2/src/tests/mocks.rs +++ b/crates/services/txpool_v2/src/tests/mocks.rs @@ -58,7 +58,10 @@ use fuel_core_types::{ GossipsubMessageInfo, PeerId, }, - txpool::TransactionStatus, + transaction_status::{ + statuses, + TransactionStatus, + }, }, }; use std::{ @@ -97,9 +100,13 @@ impl MockTxStatusManager { impl ports::TxStatusManager for MockTxStatusManager { fn status_update(&self, tx_id: TxId, tx_status: TransactionStatus) { let tx = self.tx.clone(); - tokio::spawn(async move { - tx.send((tx_id, tx_status)).await.unwrap(); - }); + tx.try_send((tx_id, tx_status)).unwrap(); + } + + fn squeezed_out_txs(&self, statuses: Vec<(TxId, statuses::SqueezedOut)>) { + for (tx_id, tx_status) in statuses { + self.status_update(tx_id, tx_status.into()); + } } } diff --git a/crates/services/txpool_v2/src/tests/tests_pending_pool.rs b/crates/services/txpool_v2/src/tests/tests_pending_pool.rs index 43d953bf791..f5ca849cac7 100644 --- a/crates/services/txpool_v2/src/tests/tests_pending_pool.rs +++ b/crates/services/txpool_v2/src/tests/tests_pending_pool.rs @@ -4,7 +4,7 @@ use fuel_core_types::{ UniqueIdentifier, UtxoId, }, - services::txpool::TransactionStatus, + services::transaction_status::TransactionStatus, }; use crate::tests::universe::TestPoolUniverse; diff --git a/crates/services/txpool_v2/src/tests/tests_pool.rs b/crates/services/txpool_v2/src/tests/tests_pool.rs index fc8c1032644..f2580b00b63 100644 --- a/crates/services/txpool_v2/src/tests/tests_pool.rs +++ b/crates/services/txpool_v2/src/tests/tests_pool.rs @@ -76,7 +76,7 @@ fn insert_one_tx_succeeds() { // Then assert!(result.is_ok()); - let tx = result.unwrap().0; + let tx = result.unwrap(); universe.assert_pool_integrity(&[tx]); } @@ -194,7 +194,7 @@ fn insert__tx2_succeeds_after_dependent_tx1() { // Then assert!(result1.is_ok()); assert!(result2.is_ok()); - universe.assert_pool_integrity(&[result1.unwrap().0, result2.unwrap().0]); + universe.assert_pool_integrity(&[result1.unwrap(), result2.unwrap()]); } #[test] @@ -232,7 +232,7 @@ fn insert__tx2_collided_on_contract_id() { .add_input(gas_coin) .add_output(create_contract_output(contract_id)) .finalize_as_transaction(); - let tx = universe.verify_and_insert(tx).unwrap().0; + let tx = universe.verify_and_insert(tx).unwrap(); // When let result2 = universe.verify_and_insert(tx_faulty); @@ -269,7 +269,7 @@ fn insert__tx_with_dependency_on_invalid_utxo_type() { universe.random_predicate(AssetId::BASE, TEST_COIN_AMOUNT, Some(utxo_id)); let tx_faulty = universe.build_script_transaction(Some(vec![random_predicate]), None, 0); - let tx = universe.verify_and_insert(tx).unwrap().0; + let tx = universe.verify_and_insert(tx).unwrap(); // When let result2 = universe.verify_and_insert(tx_faulty); @@ -293,7 +293,7 @@ fn insert__already_known_tx_returns_error() { // Given let tx = universe.build_script_transaction(None, None, 0); - let pool_tx = universe.verify_and_insert(tx.clone()).unwrap().0; + let pool_tx = universe.verify_and_insert(tx.clone()).unwrap(); // When let result2 = universe.verify_and_insert(tx.clone()); @@ -327,8 +327,8 @@ fn insert__unknown_utxo_returns_error() { universe.assert_pool_integrity(&[]); } -#[test] -fn insert__higher_priced_tx_removes_lower_priced_tx() { +#[tokio::test] +async fn insert__higher_priced_tx_removes_lower_priced_tx() { let mut universe = TestPoolUniverse::default(); universe.build_pool(); @@ -344,8 +344,10 @@ fn insert__higher_priced_tx_removes_lower_priced_tx() { let result = universe.verify_and_insert(tx2).unwrap(); // Then - assert_eq!(result.1[0].id(), tx_id); - universe.assert_pool_integrity(&[result.0]); + universe + .await_expected_tx_statuses_squeeze_out(vec![tx_id]) + .await; + universe.assert_pool_integrity(&[result]); } #[test] @@ -361,8 +363,8 @@ fn insert__colliding_dependent_and_underpriced_returns_error() { // Given let tx2 = universe.build_script_transaction(Some(vec![input.clone()]), None, 20); let tx3 = universe.build_script_transaction(Some(vec![input]), None, 10); - let tx1 = universe.verify_and_insert(tx1).unwrap().0; - let tx2 = universe.verify_and_insert(tx2).unwrap().0; + let tx1 = universe.verify_and_insert(tx1).unwrap(); + let tx2 = universe.verify_and_insert(tx2).unwrap(); // When let result3 = universe.verify_and_insert(tx3); @@ -413,11 +415,11 @@ fn insert_dependent_contract_creation() { // Then assert!(result1.is_ok()); assert!(result2.is_ok()); - universe.assert_pool_integrity(&[result1.unwrap().0, result2.unwrap().0]); + universe.assert_pool_integrity(&[result1.unwrap(), result2.unwrap()]); } -#[test] -fn insert_more_priced_tx3_removes_tx1_and_dependent_tx2() { +#[tokio::test] +async fn insert_more_priced_tx3_removes_tx1_and_dependent_tx2() { let mut universe = TestPoolUniverse::default(); universe.build_pool(); @@ -444,15 +446,15 @@ fn insert_more_priced_tx3_removes_tx1_and_dependent_tx2() { let result3 = universe.verify_and_insert(tx3); // Then - let (pool_tx, 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); + let pool_tx = result3.unwrap(); + universe + .await_expected_tx_statuses_squeeze_out(vec![tx1_id, tx2_id]) + .await; universe.assert_pool_integrity(&[pool_tx]); } -#[test] -fn insert_more_priced_tx2_removes_tx1_and_more_priced_tx3_removes_tx2() { +#[tokio::test] +async fn insert_more_priced_tx2_removes_tx1_and_more_priced_tx3_removes_tx2() { let mut universe = TestPoolUniverse::default(); universe.build_pool(); @@ -477,13 +479,11 @@ fn insert_more_priced_tx2_removes_tx1_and_more_priced_tx3_removes_tx2() { // Then assert!(result2.is_ok()); - let removed_txs = result2.unwrap().1; - assert_eq!(removed_txs.len(), 1); - assert_eq!(removed_txs[0].id(), tx1_id); assert!(result3.is_ok()); - let (pool_tx, removed_txs) = result3.unwrap(); - assert_eq!(removed_txs.len(), 1); - assert_eq!(removed_txs[0].id(), tx2_id); + universe + .await_expected_tx_statuses_squeeze_out(vec![tx1_id, tx2_id]) + .await; + let pool_tx = result3.unwrap(); universe.assert_pool_integrity(&[pool_tx]); } @@ -502,7 +502,7 @@ fn insert__tx_limit_hit() { // Given let tx1 = universe.build_script_transaction(None, None, 10); let tx2 = universe.build_script_transaction(None, None, 0); - let pool_tx = universe.verify_and_insert(tx1).unwrap().0; + let pool_tx = universe.verify_and_insert(tx1).unwrap(); // When let result2 = universe.verify_and_insert(tx2); @@ -537,7 +537,7 @@ fn insert__tx_gas_limit() { ..Default::default() }); universe.build_pool(); - let pool_tx = universe.verify_and_insert(tx1).unwrap().0; + let pool_tx = universe.verify_and_insert(tx1).unwrap(); // When let result2 = universe.verify_and_insert(tx2); @@ -572,7 +572,7 @@ fn insert__tx_bytes_limit() { ..Default::default() }); universe.build_pool(); - let pool_tx = universe.verify_and_insert(tx1).unwrap().0; + let pool_tx = universe.verify_and_insert(tx1).unwrap(); // When let result2 = universe.verify_and_insert(tx2); @@ -601,8 +601,8 @@ fn insert__dependency_chain_length_hit() { let input = unset_input.into_input(UtxoId::new(tx2.id(&Default::default()), 0)); let tx3 = universe.build_script_transaction(Some(vec![input]), None, 0); - let tx1 = universe.verify_and_insert(tx1).unwrap().0; - let tx2 = universe.verify_and_insert(tx2).unwrap().0; + let tx1 = universe.verify_and_insert(tx1).unwrap(); + let tx2 = universe.verify_and_insert(tx2).unwrap(); // When let result3 = universe.verify_and_insert(tx3); @@ -921,7 +921,7 @@ fn insert_tx_when_input_message_id_exists_in_db() { // When let pool_tx = universe.verify_and_insert(tx) // Then - .unwrap().0; + .unwrap(); universe.assert_pool_integrity(&[pool_tx]); } @@ -969,7 +969,7 @@ fn insert__tx_tip_lower_than_another_tx_with_same_message_id() { ); // When - let pool_tx = universe.verify_and_insert(tx_high).unwrap().0; + let pool_tx = universe.verify_and_insert(tx_high).unwrap(); let err = universe.verify_and_insert(tx_low).unwrap_err(); // Then @@ -979,8 +979,8 @@ fn insert__tx_tip_lower_than_another_tx_with_same_message_id() { universe.assert_pool_integrity(&[pool_tx]); } -#[test] -fn insert_tx_tip_higher_than_another_tx_with_same_message_id() { +#[tokio::test] +async fn insert_tx_tip_higher_than_another_tx_with_same_message_id() { let mut universe = TestPoolUniverse::default(); universe.build_pool(); @@ -1010,9 +1010,10 @@ fn insert_tx_tip_higher_than_another_tx_with_same_message_id() { // Then assert!(result1.is_ok()); assert!(result2.is_ok()); - let (pool_tx, removed_txs) = result2.unwrap(); - assert_eq!(removed_txs.len(), 1); - assert_eq!(removed_txs[0].id(), tx_high_id); + let pool_tx = result2.unwrap(); + universe + .await_expected_tx_statuses_squeeze_out(vec![tx_high_id]) + .await; universe.assert_pool_integrity(&[pool_tx]); } @@ -1049,7 +1050,7 @@ fn insert_again_message_after_squeeze_with_even_lower_tip() { assert!(result1.is_ok()); assert!(result2.is_ok()); assert!(result3.is_ok()); - universe.assert_pool_integrity(&[result2.unwrap().0, result3.unwrap().0]); + universe.assert_pool_integrity(&[result2.unwrap(), result3.unwrap()]); } #[test] @@ -1179,7 +1180,7 @@ fn insert_tx_with_blob() { // When let pool_tx = universe.verify_and_insert(tx) // Then - .unwrap().0; + .unwrap(); universe.assert_pool_integrity(&[pool_tx]); } @@ -1202,7 +1203,7 @@ fn insert__tx_with_blob_already_inserted_at_higher_tip() { .add_fee_input() .finalize_as_transaction(); - let pool_tx = universe.verify_and_insert(tx).unwrap().0; + let pool_tx = universe.verify_and_insert(tx).unwrap(); let same_blob_tx = TransactionBuilder::blob(BlobBody { id: blob_id, @@ -1256,7 +1257,7 @@ fn insert_tx_with_blob_already_insert_at_lower_tip() { // Then assert!(result.is_ok()); - universe.assert_pool_integrity(&[result.unwrap().0]); + universe.assert_pool_integrity(&[result.unwrap()]); } #[test] @@ -1269,7 +1270,7 @@ fn verify_and_insert__when_dependent_tx_is_extracted_new_tx_still_accepted() { let (output_a, unset_input) = universe.create_output_and_input(); let dependency_tx = universe.build_script_transaction(inputs.clone(), Some(vec![output_a]), 1); - let mut pool_dependency_tx = universe.verify_and_insert(dependency_tx).unwrap().0; + let mut pool_dependency_tx = universe.verify_and_insert(dependency_tx).unwrap(); inputs = Some(vec![ unset_input.into_input(UtxoId::new(pool_dependency_tx.id(), 0)) ]); @@ -1293,7 +1294,7 @@ fn verify_and_insert__when_dependent_tx_is_extracted_new_tx_still_accepted() { assert_eq!(pool_dependency_tx.id(), txs[0].id()); // Then - pool_dependency_tx = universe.verify_and_insert(dependent_tx).unwrap().0; + pool_dependency_tx = universe.verify_and_insert(dependent_tx).unwrap(); let input_a = new_unset_input.into_input(UtxoId::new(pool_dependency_tx.id(), 0)); inputs = Some(vec![input_a.clone()]); } @@ -1349,8 +1350,8 @@ fn insert__if_tx3_depends_and_collides_with_tx2() { // Given // tx3 {inputs: {coinA, coinB}, outputs:{}, tip: 20} let input_b = unset_input.into_input(UtxoId::new(tx2.id(&Default::default()), 0)); - let tx1 = universe.verify_and_insert(tx1).unwrap().0; - let tx2 = universe.verify_and_insert(tx2).unwrap().0; + let tx1 = universe.verify_and_insert(tx1).unwrap(); + let tx2 = universe.verify_and_insert(tx2).unwrap(); let tx3 = universe.build_script_transaction(Some(vec![input_a, input_b]), None, 20); diff --git a/crates/services/txpool_v2/src/tests/tests_service.rs b/crates/services/txpool_v2/src/tests/tests_service.rs index f2d8d5ae8db..459caa8256f 100644 --- a/crates/services/txpool_v2/src/tests/tests_service.rs +++ b/crates/services/txpool_v2/src/tests/tests_service.rs @@ -11,7 +11,7 @@ use fuel_core_types::{ fuel_types::ChainId, services::{ block_importer::ImportResult, - txpool::TransactionStatus, + transaction_status::TransactionStatus, }, }; use std::{ diff --git a/crates/services/txpool_v2/src/tests/universe.rs b/crates/services/txpool_v2/src/tests/universe.rs index 702615a30ec..34030a0a9f2 100644 --- a/crates/services/txpool_v2/src/tests/universe.rs +++ b/crates/services/txpool_v2/src/tests/universe.rs @@ -54,9 +54,9 @@ use fuel_core_types::{ interpreter::MemoryInstance, predicate::EmptyStorage, }, - services::txpool::{ - ArcPoolTx, - TransactionStatus, + services::{ + transaction_status::TransactionStatus, + txpool::ArcPoolTx, }, }; use parking_lot::RwLock; @@ -127,7 +127,7 @@ pub struct TestPoolUniverse { mock_db: MockDb, rng: StdRng, pub config: Config, - pool: Option>, + pool: Option>>, mock_tx_status_manager: MockTxStatusManager, tx_status_manager_receiver: mpsc::Receiver<(TxId, TransactionStatus)>, stats_receiver: Option>, @@ -175,6 +175,7 @@ impl TestPoolUniverse { pub fn build_pool(&mut self) { let (tx_new_executable_txs, _) = tokio::sync::watch::channel(()); + let (status_sender, status_receiver) = mpsc::channel(1_000_000); let (tx, rx) = tokio::sync::watch::channel(TxPoolStats::default()); let pool = Arc::new(RwLock::new(Pool::new( GraphStorage::new(GraphConfig { @@ -185,8 +186,10 @@ impl TestPoolUniverse { self.config.clone(), tx, tx_new_executable_txs, + Arc::new(MockTxStatusManager::new(status_sender)), ))); self.stats_receiver = Some(rx); + self.tx_status_manager_receiver = status_receiver; self.pool = Some(pool.clone()); } @@ -254,10 +257,7 @@ impl TestPoolUniverse { } // Returns the added transaction and the list of transactions that were removed from the pool - pub fn verify_and_insert( - &mut self, - tx: Transaction, - ) -> Result<(ArcPoolTx, Vec), Error> { + pub fn verify_and_insert(&mut self, tx: Transaction) -> Result { if let Some(pool) = &self.pool { let mut mock_chain_state_info_provider = MockChainStateInfoProvider::default(); @@ -277,15 +277,14 @@ impl TestPoolUniverse { let tx = verification.perform_all_verifications(tx, Default::default(), true)?; let tx = Arc::new(tx); - Ok(( - tx.clone(), - pool.write() - .insert(tx, &self.mock_db) - .map_err(|e| match e { - InsertionErrorType::Error(e) => e, - InsertionErrorType::MissingInputs(e) => e.first().unwrap().into(), - })?, - )) + pool.write() + .insert(tx.clone(), &self.mock_db) + .map_err(|e| match e { + InsertionErrorType::Error(e) => e, + InsertionErrorType::MissingInputs(e) => e.first().unwrap().into(), + })?; + + Ok(tx) } else { panic!("Pool needs to be built first"); } @@ -295,7 +294,7 @@ impl TestPoolUniverse { &mut self, tx: Transaction, gas_price: GasPrice, - ) -> Result, Error> { + ) -> Result<(), Error> { if let Some(pool) = &self.pool { let mut mock_chain_state_info = MockChainStateInfoProvider::default(); mock_chain_state_info @@ -329,7 +328,7 @@ impl TestPoolUniverse { tx: Transaction, consensus_params: ConsensusParameters, wasm_checker: MockWasmChecker, - ) -> Result, Error> { + ) -> Result<(), Error> { if let Some(pool) = &self.pool { let mut mock_chain_state_info_provider = MockChainStateInfoProvider::default(); @@ -393,7 +392,7 @@ impl TestPoolUniverse { pool.assert_integrity(txs); } - pub fn get_pool(&self) -> Shared { + pub fn get_pool(&self) -> Shared> { self.pool.clone().unwrap() } @@ -476,6 +475,17 @@ impl TestPoolUniverse { .unwrap(); } + pub(crate) async fn await_expected_tx_statuses_squeeze_out( + &mut self, + tx_ids: Vec, + ) { + self.await_expected_tx_statuses(tx_ids, |status| { + matches!(status, TransactionStatus::SqueezedOut { .. }) + }) + .await + .unwrap(); + } + pub(crate) async fn await_expected_tx_statuses( &mut self, tx_ids: Vec, diff --git a/crates/services/upgradable-executor/wasm-executor/src/preconfirmation_sender.rs b/crates/services/upgradable-executor/wasm-executor/src/preconfirmation_sender.rs index 95755eb56c5..2940f3edd05 100644 --- a/crates/services/upgradable-executor/wasm-executor/src/preconfirmation_sender.rs +++ b/crates/services/upgradable-executor/wasm-executor/src/preconfirmation_sender.rs @@ -1,12 +1,12 @@ use fuel_core_executor::ports::PreconfirmationSenderPort; -use fuel_core_types::services::preconfirmation::PreconfirmationStatus; +use fuel_core_types::services::preconfirmation::Preconfirmation; pub struct PreconfirmationSender; impl PreconfirmationSenderPort for PreconfirmationSender { - fn try_send(&self, _: Vec) -> Vec { + fn try_send(&self, _: Vec) -> Vec { vec![] } - async fn send(&self, _: Vec) {} + async fn send(&self, _: Vec) {} } diff --git a/crates/types/Cargo.toml b/crates/types/Cargo.toml index 19ece14167a..547a1002a98 100644 --- a/crates/types/Cargo.toml +++ b/crates/types/Cargo.toml @@ -38,7 +38,8 @@ zeroize = "1.5" [dev-dependencies] aws-config = { version = "1.1.7", features = ["behavior-version-latest"] } -fuel-core-types = { path = ".", features = ["test-helpers"] } +fuel-core-types = { path = ".", features = ["test-helpers", "serde"] } +postcard = { workspace = true } tokio = { workspace = true, features = ["macros"] } [features] diff --git a/crates/types/src/services.rs b/crates/types/src/services.rs index 0a6e3f2e787..1fcfd7295fd 100644 --- a/crates/types/src/services.rs +++ b/crates/types/src/services.rs @@ -9,6 +9,7 @@ pub mod p2p; pub mod preconfirmation; pub mod relayer; pub mod shared_sequencer; +pub mod transaction_status; #[cfg(feature = "std")] pub mod txpool; diff --git a/crates/types/src/services/executor.rs b/crates/types/src/services/executor.rs index 2eafbb1fb36..f05a4a3ac1d 100644 --- a/crates/types/src/services/executor.rs +++ b/crates/types/src/services/executor.rs @@ -242,9 +242,10 @@ impl TransactionExecutionResult { } } - #[cfg(feature = "std")] /// Get the reason of the failed transaction execution. pub fn reason(receipts: &[Receipt], state: &Option) -> String { + use alloc::format; + receipts .iter() .find_map(|receipt| match receipt { diff --git a/crates/types/src/services/p2p.rs b/crates/types/src/services/p2p.rs index f1f884f479c..fc17a862e96 100644 --- a/crates/types/src/services/p2p.rs +++ b/crates/types/src/services/p2p.rs @@ -116,7 +116,12 @@ pub type SignedPreconfirmationByDelegate = Sealed; #[derive(Debug, Clone, PartialEq, Eq)] pub enum PreConfirmationMessage { /// Notification of key delegation - Delegate(SignedByBlockProducerDelegation), + Delegate { + /// The sealed key delegation. + seal: SignedByBlockProducerDelegation, + /// The nonce of the p2p message to make it unique. + nonce: u64, + }, /// Notification of pre-confirmations Preconfirmations(SignedPreconfirmationByDelegate), } @@ -289,22 +294,57 @@ impl Serialize for NetworkableTransactionPool { where S: serde::Serializer, { + use crate::fuel_tx::{ + Blob, + Create, + Script, + Upgrade, + Upload, + }; + + #[cfg(all(debug_assertions, feature = "test-helpers"))] + // When a new variant is added to `Transaction`, the `TransactionRef` + // must also be updated to match the new variant. + // This match statement will trigger a compilation error if a new variant is added. + // Don't add a `_` wildcard to this match statement, instead handle new variant. + { + match Transaction::default_test_tx() { + Transaction::Script(_) => {} + Transaction::Create(_) => {} + Transaction::Mint(_) => {} + Transaction::Upgrade(_) => {} + Transaction::Upload(_) => {} + Transaction::Blob(_) => {} + } + } + + #[derive(serde::Serialize)] + enum TransactionRef<'a> { + Script(&'a Script), + Create(&'a Create), + #[allow(dead_code)] + Mint, + Upgrade(&'a Upgrade), + Upload(&'a Upload), + Blob(&'a Blob), + } + match self { NetworkableTransactionPool::PoolTransaction(tx) => match (*tx).as_ref() { - PoolTransaction::Script(script, _) => { - script.transaction().serialize(serializer) + PoolTransaction::Script(tx, _) => { + TransactionRef::Script(tx.transaction()).serialize(serializer) } - PoolTransaction::Create(create, _) => { - create.transaction().serialize(serializer) + PoolTransaction::Create(tx, _) => { + TransactionRef::Create(tx.transaction()).serialize(serializer) } - PoolTransaction::Blob(blob, _) => { - blob.transaction().serialize(serializer) + PoolTransaction::Blob(tx, _) => { + TransactionRef::Blob(tx.transaction()).serialize(serializer) } - PoolTransaction::Upgrade(upgrade, _) => { - upgrade.transaction().serialize(serializer) + PoolTransaction::Upgrade(tx, _) => { + TransactionRef::Upgrade(tx.transaction()).serialize(serializer) } - PoolTransaction::Upload(upload, _) => { - upload.transaction().serialize(serializer) + PoolTransaction::Upload(tx, _) => { + TransactionRef::Upload(tx.transaction()).serialize(serializer) } }, NetworkableTransactionPool::Transaction(tx) => tx.serialize(serializer), @@ -335,3 +375,25 @@ impl TryFrom for Transaction { } } } + +#[cfg(test)] +mod tests { + use crate::{ + fuel_tx::Transaction, + services::p2p::NetworkableTransactionPool, + }; + + #[test] + fn ser_der() { + // Given + let transaction = Transaction::default_test_tx(); + let expected = NetworkableTransactionPool::Transaction(transaction.clone()); + let bytes = postcard::to_allocvec(&expected).unwrap(); + + // When + let actual: NetworkableTransactionPool = postcard::from_bytes(&bytes).unwrap(); + + // Then + assert_eq!(actual, expected); + } +} diff --git a/crates/types/src/services/preconfirmation.rs b/crates/types/src/services/preconfirmation.rs index 6a236eee70d..745c181417f 100644 --- a/crates/types/src/services/preconfirmation.rs +++ b/crates/types/src/services/preconfirmation.rs @@ -13,6 +13,7 @@ use alloc::{ string::String, vec::Vec, }; + /// A collection of pre-confirmations that have been signed by a delegate #[derive(Debug, Clone, PartialEq, Eq)] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] diff --git a/crates/types/src/services/transaction_status.rs b/crates/types/src/services/transaction_status.rs new file mode 100644 index 00000000000..d68eeccf1c4 --- /dev/null +++ b/crates/types/src/services/transaction_status.rs @@ -0,0 +1,422 @@ +//! The status of the transaction during its lifecycle. + +use crate::{ + fuel_tx::{ + Output, + Receipt, + TxPointer, + }, + fuel_vm::ProgramState, + services::{ + executor::TransactionExecutionResult, + preconfirmation::PreconfirmationStatus, + }, +}; +use fuel_vm_private::fuel_types::BlockHeight; +use tai64::Tai64; + +#[cfg(feature = "std")] +use std::sync::Arc; + +#[cfg(not(feature = "std"))] +use alloc::sync::Arc; + +#[cfg(feature = "alloc")] +use alloc::{ + string::{ + String, + ToString, + }, + vec::Vec, +}; + +/// The status of the transaction during its life from the tx pool until the block. +// TODO: This type needs to be updated: https://github.com/FuelLabs/fuel-core/issues/2794 +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum TransactionExecutionStatus { + /// Transaction was submitted into the txpool + Submitted { + /// Timestamp of submission into the txpool + time: Tai64, + }, + /// Transaction was successfully included in a block + Success { + /// Included in this block + block_height: BlockHeight, + /// Time when the block was generated + time: Tai64, + /// Result of executing the transaction for scripts + result: Option, + /// The receipts generated during execution of the transaction. + receipts: Vec, + /// The total gas used by the transaction. + total_gas: u64, + /// The total fee paid by the transaction. + total_fee: u64, + }, + /// Transaction was squeezed of the txpool + SqueezedOut { + /// Why this happened + reason: String, + }, + /// Transaction was included in a block, but the execution was reverted + Failed { + /// Included in this block + block_height: BlockHeight, + /// Time when the block was generated + time: Tai64, + /// Result of executing the transaction for scripts + result: Option, + /// The receipts generated during execution of the transaction. + receipts: Vec, + /// The total gas used by the transaction. + total_gas: u64, + /// The total fee paid by the transaction. + total_fee: u64, + }, +} + +impl From for TransactionStatus { + fn from(value: TransactionExecutionStatus) -> Self { + match value { + TransactionExecutionStatus::Submitted { time } => { + TransactionStatus::Submitted( + statuses::Submitted { timestamp: time }.into(), + ) + } + TransactionExecutionStatus::Success { + block_height, + time, + result, + receipts, + total_gas, + total_fee, + } => TransactionStatus::Success( + statuses::Success { + block_height, + block_timestamp: time, + program_state: result, + receipts, + total_gas, + total_fee, + } + .into(), + ), + // TODO: Removed this variant as part of the + // https://github.com/FuelLabs/fuel-core/issues/2794 + TransactionExecutionStatus::SqueezedOut { reason } => { + TransactionStatus::SqueezedOut(statuses::SqueezedOut { reason }.into()) + } + TransactionExecutionStatus::Failed { + block_height, + time, + result, + receipts, + total_gas, + total_fee, + } => { + #[cfg(feature = "std")] + let reason = TransactionExecutionResult::reason(&receipts, &result); + #[cfg(not(feature = "std"))] + let reason = "Unknown reason".to_string(); + TransactionStatus::Failure( + statuses::Failure { + reason, + block_height, + block_timestamp: time, + program_state: result, + receipts, + total_gas, + total_fee, + } + .into(), + ) + } + } + } +} + +/// The status of the transaction during its life from the TxPool until the inclusion in the block. +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum TransactionStatus { + /// Transaction was submitted into the TxPool + Submitted(Arc), + /// Transaction was successfully included in a block + Success(Arc), + /// Transaction was successfully executed by block producer + PreConfirmationSuccess(Arc), + /// Transaction was squeezed out of the TxPool + SqueezedOut(Arc), + /// Transaction was squeezed out + PreConfirmationSqueezedOut(Arc), + /// Transaction was included in a block, but the execution has failed + Failure(Arc), + /// Transaction was included in a block by block producer, but the execution has failed + PreConfirmationFailure(Arc), +} + +impl TransactionStatus { + /// Returns `true` if the status is considered "final". + pub fn is_final(&self) -> bool { + match self { + TransactionStatus::Success(_) + | TransactionStatus::Failure(_) + | TransactionStatus::SqueezedOut(_) + | TransactionStatus::PreConfirmationSqueezedOut(_) => true, + TransactionStatus::Submitted(_) + | TransactionStatus::PreConfirmationSuccess(_) + | TransactionStatus::PreConfirmationFailure(_) => false, + } + } + + /// Returns `true` if the status is `Submitted`. + pub fn is_submitted(&self) -> bool { + matches!(self, Self::Submitted { .. }) + } + + /// Creates a new `TransactionStatus::Submitted` variant. + pub fn submitted(timestamp: Tai64) -> Self { + Self::Submitted(statuses::Submitted { timestamp }.into()) + } + + /// Creates a new `TransactionStatus::SqueezedOut` variant. + pub fn squeezed_out(reason: String) -> Self { + Self::SqueezedOut(statuses::SqueezedOut { reason }.into()) + } + + /// Creates a new `TransactionStatus::PreConfirmationSqueezedOut` variant. + pub fn preconfirmation_squeezed_out(reason: String) -> Self { + Self::PreConfirmationSqueezedOut( + statuses::PreConfirmationSqueezedOut { reason }.into(), + ) + } +} + +impl From for TransactionStatus { + fn from(value: PreconfirmationStatus) -> Self { + match value { + PreconfirmationStatus::SqueezedOut { reason } => { + TransactionStatus::PreConfirmationSqueezedOut( + statuses::PreConfirmationSqueezedOut { reason }.into(), + ) + } + PreconfirmationStatus::Success { + tx_pointer, + total_gas, + total_fee, + receipts, + outputs, + } => TransactionStatus::PreConfirmationSuccess( + statuses::PreConfirmationSuccess { + tx_pointer, + total_gas, + total_fee, + receipts: Some(receipts), + outputs: Some(outputs), + } + .into(), + ), + PreconfirmationStatus::Failure { + tx_pointer, + total_gas, + total_fee, + receipts, + outputs, + } => { + let reason = TransactionExecutionResult::reason(&receipts, &None); + TransactionStatus::PreConfirmationFailure( + statuses::PreConfirmationFailure { + tx_pointer, + total_gas, + total_fee, + receipts: Some(receipts), + outputs: Some(outputs), + reason, + } + .into(), + ) + } + } + } +} + +/// The status of the transaction during its lifecycle. +pub mod statuses { + use super::*; + + /// Transaction was submitted into the TxPool + #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] + #[derive(Clone, Debug, PartialEq, Eq)] + pub struct Submitted { + /// Timestamp of submission into the TxPool + pub timestamp: Tai64, + } + + impl Default for Submitted { + fn default() -> Self { + Self { + timestamp: Tai64::UNIX_EPOCH, + } + } + } + + /// Transaction was successfully included in a block + #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] + #[derive(Clone, Debug, PartialEq, Eq)] + pub struct Success { + /// Included in this block + pub block_height: BlockHeight, + /// Timestamp of the block + pub block_timestamp: Tai64, + /// Result of executing the transaction for scripts + pub program_state: Option, + /// The receipts generated during execution of the transaction + pub receipts: Vec, + /// The total gas used by the transaction + pub total_gas: u64, + /// The total fee paid by the transaction + pub total_fee: u64, + } + + impl Default for Success { + fn default() -> Self { + Self { + block_height: Default::default(), + block_timestamp: Tai64::UNIX_EPOCH, + program_state: None, + receipts: Vec::new(), + total_gas: 0, + total_fee: 0, + } + } + } + + /// Transaction was successfully executed by block producer + #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] + #[derive(Default, Clone, Debug, PartialEq, Eq)] + pub struct PreConfirmationSuccess { + /// Transaction pointer within the block. + pub tx_pointer: TxPointer, + /// The total gas used by the transaction. + pub total_gas: u64, + /// The total fee paid by the transaction. + pub total_fee: u64, + /// Receipts produced by the transaction during execution. + pub receipts: Option>, + /// Dynamic outputs produced by the transaction during execution. + pub outputs: Option>, + } + + /// Transaction was squeezed out of the TxPool + #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] + #[derive(Clone, Debug, PartialEq, Eq)] + pub struct SqueezedOut { + /// The reason why the transaction was squeezed out + pub reason: String, + } + + impl Default for SqueezedOut { + fn default() -> Self { + Self { + reason: "Default reason".to_string(), + } + } + } + + impl From<&PreConfirmationSqueezedOut> for SqueezedOut { + fn from(value: &PreConfirmationSqueezedOut) -> Self { + Self { + reason: value.reason.clone(), + } + } + } + + impl From for TransactionStatus { + fn from(value: SqueezedOut) -> Self { + TransactionStatus::SqueezedOut(value.into()) + } + } + + /// Transaction was squeezed out from block producer + #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] + #[derive(Clone, Debug, PartialEq, Eq)] + pub struct PreConfirmationSqueezedOut { + /// The reason why the transaction was squeezed out + pub reason: String, + } + + impl Default for PreConfirmationSqueezedOut { + fn default() -> Self { + Self { + reason: "Default reason".to_string(), + } + } + } + + /// Transaction was included in a block, but the execution has failed + #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] + #[derive(Clone, Debug, PartialEq, Eq)] + pub struct Failure { + /// Included in this block + pub block_height: BlockHeight, + /// Timestamp of the block + pub block_timestamp: Tai64, + /// The reason why the transaction has failed + pub reason: String, + /// Result of executing the transaction for scripts + pub program_state: Option, + /// The receipts generated during execution of the transaction + pub receipts: Vec, + /// The total gas used by the transaction + pub total_gas: u64, + /// The total fee paid by the transaction + pub total_fee: u64, + } + + impl Default for Failure { + fn default() -> Self { + Self { + block_height: Default::default(), + block_timestamp: Tai64::UNIX_EPOCH, + reason: "Dummy reason".to_string(), + program_state: None, + receipts: Vec::new(), + total_gas: 0, + total_fee: 0, + } + } + } + + /// Transaction was included in a block by block producer, but the execution has failed + #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] + #[derive(Clone, Debug, PartialEq, Eq)] + pub struct PreConfirmationFailure { + /// Transaction pointer within the block. + pub tx_pointer: TxPointer, + /// The total gas used by the transaction. + pub total_gas: u64, + /// The total fee paid by the transaction. + pub total_fee: u64, + /// Receipts produced by the transaction during execution. + pub receipts: Option>, + /// Dynamic outputs produced by the transaction during execution. + pub outputs: Option>, + /// The reason why the transaction has failed + pub reason: String, + } + + impl Default for PreConfirmationFailure { + fn default() -> Self { + Self { + tx_pointer: Default::default(), + total_gas: 0, + total_fee: 0, + receipts: None, + outputs: None, + reason: "Dummy reason".to_string(), + } + } + } +} diff --git a/crates/types/src/services/txpool.rs b/crates/types/src/services/txpool.rs index 130c1087c65..6217570bf76 100644 --- a/crates/types/src/services/txpool.rs +++ b/crates/types/src/services/txpool.rs @@ -16,19 +16,13 @@ use crate::{ Create, Input, Output, - Receipt, Script, Transaction, TxId, - TxPointer, Upgrade, Upload, }, - fuel_vm::{ - checked_transaction::Checked, - ProgramState, - }, - services::preconfirmation::PreconfirmationStatus, + fuel_vm::checked_transaction::Checked, }; use fuel_vm_private::{ checked_transaction::CheckedTransaction, @@ -36,9 +30,6 @@ use fuel_vm_private::{ prelude::field::Expiration, }; use std::sync::Arc; -use tai64::Tai64; - -use super::executor::TransactionExecutionResult; /// Pool transaction wrapped in an Arc for thread-safe sharing pub type ArcPoolTx = Arc; @@ -329,382 +320,3 @@ impl From<&PoolTransaction> for CheckedTransaction { } } } - -/// The status of the transaction during its life from the tx pool until the block. -// TODO: This type needs to be updated: https://github.com/FuelLabs/fuel-core/issues/2794 -#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] -#[derive(Clone, Debug, PartialEq, Eq)] -pub enum TransactionExecutionStatus { - /// Transaction was submitted into the txpool - Submitted { - /// Timestamp of submission into the txpool - time: Tai64, - }, - /// Transaction was successfully included in a block - Success { - /// Included in this block - block_height: BlockHeight, - /// Time when the block was generated - time: Tai64, - /// Result of executing the transaction for scripts - result: Option, - /// The receipts generated during execution of the transaction. - receipts: Vec, - /// The total gas used by the transaction. - total_gas: u64, - /// The total fee paid by the transaction. - total_fee: u64, - }, - /// Transaction was squeezed of the txpool - SqueezedOut { - /// Why this happened - reason: String, - }, - /// Transaction was included in a block, but the execution was reverted - Failed { - /// Included in this block - block_height: BlockHeight, - /// Time when the block was generated - time: Tai64, - /// Result of executing the transaction for scripts - result: Option, - /// The receipts generated during execution of the transaction. - receipts: Vec, - /// The total gas used by the transaction. - total_gas: u64, - /// The total fee paid by the transaction. - total_fee: u64, - }, -} - -impl From for TransactionStatus { - fn from(value: TransactionExecutionStatus) -> Self { - match value { - TransactionExecutionStatus::Submitted { time } => { - TransactionStatus::Submitted( - statuses::Submitted { timestamp: time }.into(), - ) - } - TransactionExecutionStatus::Success { - block_height, - time, - result, - receipts, - total_gas, - total_fee, - } => TransactionStatus::Success( - statuses::Success { - block_height, - block_timestamp: time, - program_state: result, - receipts, - total_gas, - total_fee, - } - .into(), - ), - // TODO: Removed this variant as part of the - // https://github.com/FuelLabs/fuel-core/issues/2794 - TransactionExecutionStatus::SqueezedOut { reason } => { - TransactionStatus::SqueezedOut(statuses::SqueezedOut { reason }.into()) - } - TransactionExecutionStatus::Failed { - block_height, - time, - result, - receipts, - total_gas, - total_fee, - } => TransactionStatus::Failure( - statuses::Failure { - reason: TransactionExecutionResult::reason(&receipts, &result), - block_height, - block_timestamp: time, - program_state: result, - receipts, - total_gas, - total_fee, - } - .into(), - ), - } - } -} - -/// The status of the transaction during its life from the TxPool until the inclusion in the block. -#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] -#[derive(Clone, Debug, PartialEq, Eq)] -pub enum TransactionStatus { - /// Transaction was submitted into the TxPool - Submitted(Arc), - /// Transaction was successfully included in a block - Success(Arc), - /// Transaction was successfully executed by block producer - PreConfirmationSuccess(Arc), - /// Transaction was squeezed out of the TxPool - SqueezedOut(Arc), - /// Transaction was squeezed out - PreConfirmationSqueezedOut(Arc), - /// Transaction was included in a block, but the execution has failed - Failure(Arc), - /// Transaction was included in a block by block producer, but the execution has failed - PreConfirmationFailure(Arc), -} - -impl TransactionStatus { - /// Returns `true` if the status is considered "final". - pub fn is_final(&self) -> bool { - match self { - TransactionStatus::Success(_) - | TransactionStatus::Failure(_) - | TransactionStatus::SqueezedOut(_) - | TransactionStatus::PreConfirmationSqueezedOut(_) => true, - TransactionStatus::Submitted(_) - | TransactionStatus::PreConfirmationSuccess(_) - | TransactionStatus::PreConfirmationFailure(_) => false, - } - } - - /// Returns `true` if the status is `Submitted`. - pub fn is_submitted(&self) -> bool { - matches!(self, Self::Submitted { .. }) - } - - /// Creates a new `TransactionStatus::Submitted` variant. - pub fn submitted(timestamp: Tai64) -> Self { - Self::Submitted(statuses::Submitted { timestamp }.into()) - } - - /// Creates a new `TransactionStatus::SqueezedOut` variant. - pub fn squeezed_out(reason: String) -> Self { - Self::SqueezedOut(statuses::SqueezedOut { reason }.into()) - } - - /// Creates a new `TransactionStatus::PreConfirmationSqueezedOut` variant. - pub fn preconfirmation_squeezed_out(reason: String) -> Self { - Self::PreConfirmationSqueezedOut( - statuses::PreConfirmationSqueezedOut { reason }.into(), - ) - } -} - -impl From for TransactionStatus { - fn from(value: PreconfirmationStatus) -> Self { - match value { - PreconfirmationStatus::SqueezedOut { reason } => { - TransactionStatus::PreConfirmationSqueezedOut( - statuses::PreConfirmationSqueezedOut { reason }.into(), - ) - } - PreconfirmationStatus::Success { - tx_pointer, - total_gas, - total_fee, - receipts, - outputs, - } => TransactionStatus::PreConfirmationSuccess( - statuses::PreConfirmationSuccess { - tx_pointer, - total_gas, - total_fee, - receipts: Some(receipts), - outputs: Some(outputs), - } - .into(), - ), - PreconfirmationStatus::Failure { - tx_pointer, - total_gas, - total_fee, - receipts, - outputs, - } => { - let reason = TransactionExecutionResult::reason(&receipts, &None); - TransactionStatus::PreConfirmationFailure( - statuses::PreConfirmationFailure { - tx_pointer, - total_gas, - total_fee, - receipts: Some(receipts), - outputs: Some(outputs), - reason, - } - .into(), - ) - } - } - } -} - -/// The status of the transaction during its lifecycle. -pub mod statuses { - use super::*; - - /// Transaction was submitted into the TxPool - #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] - #[derive(Clone, Debug, PartialEq, Eq)] - pub struct Submitted { - /// Timestamp of submission into the TxPool - pub timestamp: Tai64, - } - - impl Default for Submitted { - fn default() -> Self { - Self { - timestamp: Tai64::UNIX_EPOCH, - } - } - } - - /// Transaction was successfully included in a block - #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] - #[derive(Clone, Debug, PartialEq, Eq)] - pub struct Success { - /// Included in this block - pub block_height: BlockHeight, - /// Timestamp of the block - pub block_timestamp: Tai64, - /// Result of executing the transaction for scripts - pub program_state: Option, - /// The receipts generated during execution of the transaction - pub receipts: Vec, - /// The total gas used by the transaction - pub total_gas: u64, - /// The total fee paid by the transaction - pub total_fee: u64, - } - - impl Default for Success { - fn default() -> Self { - Self { - block_height: Default::default(), - block_timestamp: Tai64::UNIX_EPOCH, - program_state: None, - receipts: vec![], - total_gas: 0, - total_fee: 0, - } - } - } - - /// Transaction was successfully executed by block producer - #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] - #[derive(Default, Clone, Debug, PartialEq, Eq)] - pub struct PreConfirmationSuccess { - /// Transaction pointer within the block. - pub tx_pointer: TxPointer, - /// The total gas used by the transaction. - pub total_gas: u64, - /// The total fee paid by the transaction. - pub total_fee: u64, - /// Receipts produced by the transaction during execution. - pub receipts: Option>, - /// Dynamic outputs produced by the transaction during execution. - pub outputs: Option>, - } - - /// Transaction was squeezed out of the TxPool - #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] - #[derive(Clone, Debug, PartialEq, Eq)] - pub struct SqueezedOut { - /// The reason why the transaction was squeezed out - pub reason: String, - } - - impl Default for SqueezedOut { - fn default() -> Self { - Self { - reason: "Default reason".to_string(), - } - } - } - - impl From<&PreConfirmationSqueezedOut> for SqueezedOut { - fn from(value: &PreConfirmationSqueezedOut) -> Self { - Self { - reason: value.reason.clone(), - } - } - } - - /// Transaction was squeezed out from block producer - #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] - #[derive(Clone, Debug, PartialEq, Eq)] - pub struct PreConfirmationSqueezedOut { - /// The reason why the transaction was squeezed out - pub reason: String, - } - - impl Default for PreConfirmationSqueezedOut { - fn default() -> Self { - Self { - reason: "Default reason".to_string(), - } - } - } - - /// Transaction was included in a block, but the execution has failed - #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] - #[derive(Clone, Debug, PartialEq, Eq)] - pub struct Failure { - /// Included in this block - pub block_height: BlockHeight, - /// Timestamp of the block - pub block_timestamp: Tai64, - /// The reason why the transaction has failed - pub reason: String, - /// Result of executing the transaction for scripts - pub program_state: Option, - /// The receipts generated during execution of the transaction - pub receipts: Vec, - /// The total gas used by the transaction - pub total_gas: u64, - /// The total fee paid by the transaction - pub total_fee: u64, - } - - impl Default for Failure { - fn default() -> Self { - Self { - block_height: Default::default(), - block_timestamp: Tai64::UNIX_EPOCH, - reason: "Dummy reason".to_string(), - program_state: None, - receipts: vec![], - total_gas: 0, - total_fee: 0, - } - } - } - - /// Transaction was included in a block by block producer, but the execution has failed - #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] - #[derive(Clone, Debug, PartialEq, Eq)] - pub struct PreConfirmationFailure { - /// Transaction pointer within the block. - pub tx_pointer: TxPointer, - /// The total gas used by the transaction. - pub total_gas: u64, - /// The total fee paid by the transaction. - pub total_fee: u64, - /// Receipts produced by the transaction during execution. - pub receipts: Option>, - /// Dynamic outputs produced by the transaction during execution. - pub outputs: Option>, - /// The reason why the transaction has failed - pub reason: String, - } - - impl Default for PreConfirmationFailure { - fn default() -> Self { - Self { - tx_pointer: Default::default(), - total_gas: 0, - total_fee: 0, - receipts: None, - outputs: None, - reason: "Dummy reason".to_string(), - } - } - } -} diff --git a/tests/test-helpers/src/builder.rs b/tests/test-helpers/src/builder.rs index e653045737b..17754bfd0d1 100644 --- a/tests/test-helpers/src/builder.rs +++ b/tests/test-helpers/src/builder.rs @@ -253,6 +253,7 @@ impl TestSetupBuilder { txpool.heavy_work.size_of_verification_queue = self.max_txs; txpool.heavy_work.number_threads_to_verify_transactions = self.number_threads_pool_verif; + txpool.utxo_validation = self.utxo_validation; let gas_price_config = GasPriceConfig { starting_exec_gas_price: self.starting_gas_price, diff --git a/tests/test-helpers/src/counter_contract.rs b/tests/test-helpers/src/counter_contract.rs index 962d6b47958..10a2b586eb5 100644 --- a/tests/test-helpers/src/counter_contract.rs +++ b/tests/test-helpers/src/counter_contract.rs @@ -84,6 +84,11 @@ pub async fn deploy( intermediate_status, TransactionStatus::Submitted { .. } )); + let preconfirmation_status = status_stream.next().await.unwrap().unwrap(); + match preconfirmation_status { + TransactionStatus::PreconfirmationSuccess { .. } => {} + _ => panic!("Tx wasn't preconfirmed: {:?}", preconfirmation_status), + }; let final_status = status_stream.next().await.unwrap().unwrap(); let TransactionStatus::Success { block_height, .. } = final_status else { panic!("Tx wasn't included in a block: {:?}", final_status); diff --git a/tests/test-helpers/src/lib.rs b/tests/test-helpers/src/lib.rs index d8636fbee17..b19d0f8728b 100644 --- a/tests/test-helpers/src/lib.rs +++ b/tests/test-helpers/src/lib.rs @@ -139,6 +139,7 @@ pub async fn produce_block_with_tx(rng: &mut StdRng, client: &FuelClient) { pub fn config_with_fee() -> Config { let mut config = Config::local_node(); config.utxo_validation = true; + config.txpool.utxo_validation = true; config.gas_price_config.min_exec_gas_price = 1000; config } diff --git a/tests/tests/lib.rs b/tests/tests/lib.rs index 70d0a07eb87..8f738aa718f 100644 --- a/tests/tests/lib.rs +++ b/tests/tests/lib.rs @@ -49,6 +49,8 @@ mod node_info; #[cfg(not(feature = "only-p2p"))] mod poa; #[cfg(not(feature = "only-p2p"))] +mod preconfirmations; +#[cfg(not(feature = "only-p2p"))] mod recovery; #[cfg(not(feature = "only-p2p"))] mod regenesis; @@ -71,6 +73,8 @@ mod tx; #[cfg(not(feature = "only-p2p"))] mod vm_storage; +#[cfg(feature = "only-p2p")] +mod preconfirmations_gossip; #[cfg(feature = "only-p2p")] mod sync; #[cfg(feature = "only-p2p")] diff --git a/tests/tests/preconfirmations.rs b/tests/tests/preconfirmations.rs new file mode 100644 index 00000000000..21f67d9dd6b --- /dev/null +++ b/tests/tests/preconfirmations.rs @@ -0,0 +1,336 @@ +use std::time::Duration; + +use fuel_core::service::Config; +use fuel_core_bin::FuelService; +use fuel_core_client::client::{ + types::TransactionStatus, + FuelClient, +}; +use fuel_core_poa::Trigger; +use fuel_core_types::{ + fuel_asm::{ + op, + RegId, + }, + fuel_tx::{ + Address, + AssetId, + Output, + Receipt, + TransactionBuilder, + TxPointer, + UniqueIdentifier, + }, + fuel_types::BlockHeight, + fuel_vm::SecretKey, +}; +use futures::StreamExt; +use rand::Rng; + +#[tokio::test] +async fn preconfirmation__received_after_successful_execution() { + let mut rng = rand::thread_rng(); + let mut config = Config::local_node(); + config.block_production = Trigger::Never; + let address = Address::new([0; 32]); + let amount = 10; + + let srv = FuelService::new_node(config).await.unwrap(); + let client = FuelClient::from(srv.bound_address); + + let gas_limit = 1_000_000; + let maturity = Default::default(); + + // Given + let script = [ + op::addi(0x10, RegId::ZERO, 0xca), + op::addi(0x11, RegId::ZERO, 0xba), + op::log(0x10, 0x11, RegId::ZERO, RegId::ZERO), + op::ret(RegId::ONE), + ]; + let script: Vec = script + .iter() + .flat_map(|op| u32::from(*op).to_be_bytes()) + .collect(); + + let tx = TransactionBuilder::script(script, vec![]) + .script_gas_limit(gas_limit) + .maturity(maturity) + .add_unsigned_coin_input( + SecretKey::random(&mut rng), + rng.gen(), + amount, + AssetId::default(), + Default::default(), + ) + .add_output(Output::change(address, 0, AssetId::default())) + .finalize_as_transaction(); + + let tx_id = tx.id(&Default::default()); + let mut tx_statuses_subscriber = client.submit_and_await_status(&tx).await.unwrap(); + + // When + assert!(matches!( + tx_statuses_subscriber.next().await.unwrap().unwrap(), + TransactionStatus::Submitted { .. } + )); + client.produce_blocks(1, None).await.unwrap(); + if let TransactionStatus::PreconfirmationSuccess { + tx_pointer, + total_fee, + total_gas: _, + transaction_id, + receipts, + resolved_outputs, + } = tx_statuses_subscriber.next().await.unwrap().unwrap() + { + // Then + assert_eq!(tx_pointer, TxPointer::new(BlockHeight::new(1), 1)); + assert_eq!(total_fee, 0); + assert_eq!(transaction_id, tx_id); + let receipts = receipts.unwrap(); + assert_eq!(receipts.len(), 3); + assert!(matches!(receipts[0], + Receipt::Log { + ra, rb, .. + } if ra == 0xca && rb == 0xba)); + + assert!(matches!(receipts[1], + Receipt::Return { + val, .. + } if val == 1)); + let outputs = resolved_outputs.unwrap(); + assert_eq!(outputs.len(), 1); + assert_eq!( + outputs[0], + Output::Change { + to: address, + amount, + asset_id: AssetId::default() + } + ); + } else { + panic!("Expected preconfirmation status"); + } + assert!(matches!( + tx_statuses_subscriber.next().await.unwrap().unwrap(), + TransactionStatus::Success { .. } + )); +} + +#[tokio::test] +async fn preconfirmation__received_after_failed_execution() { + let mut rng = rand::thread_rng(); + let mut config = Config::local_node(); + config.block_production = Trigger::Never; + let address = Address::new([0; 32]); + let amount = 10; + + let srv = FuelService::new_node(config).await.unwrap(); + let client = FuelClient::from(srv.bound_address); + + let gas_limit = 1_000_000; + let maturity = Default::default(); + + // Given + let script = [ + op::addi(0x10, RegId::ZERO, 0xca), + op::addi(0x11, RegId::ZERO, 0xba), + op::log(0x10, 0x11, RegId::ZERO, RegId::ZERO), + op::rvrt(RegId::ONE), + op::ret(RegId::ONE), + ]; + let script: Vec = script + .iter() + .flat_map(|op| u32::from(*op).to_be_bytes()) + .collect(); + + let tx = TransactionBuilder::script(script, vec![]) + .script_gas_limit(gas_limit) + .maturity(maturity) + .add_unsigned_coin_input( + SecretKey::random(&mut rng), + rng.gen(), + amount, + AssetId::default(), + Default::default(), + ) + .add_output(Output::change(address, 0, AssetId::default())) + .finalize_as_transaction(); + + let tx_id = tx.id(&Default::default()); + let mut tx_statuses_subscriber = client.submit_and_await_status(&tx).await.unwrap(); + + // When + assert!(matches!( + tx_statuses_subscriber.next().await.unwrap().unwrap(), + TransactionStatus::Submitted { .. } + )); + client.produce_blocks(1, None).await.unwrap(); + if let TransactionStatus::PreconfirmationFailure { + tx_pointer, + total_fee, + total_gas: _, + transaction_id, + receipts, + resolved_outputs, + reason: _, + } = tx_statuses_subscriber.next().await.unwrap().unwrap() + { + // Then + assert_eq!(tx_pointer, TxPointer::new(BlockHeight::new(1), 1)); + assert_eq!(total_fee, 0); + assert_eq!(transaction_id, tx_id); + let receipts = receipts.unwrap(); + assert_eq!(receipts.len(), 3); + assert!(matches!(receipts[0], + Receipt::Log { + ra, rb, .. + } if ra == 0xca && rb == 0xba)); + + assert!(matches!(receipts[1], + Receipt::Revert { + ra, .. + } if ra == 1)); + let outputs = resolved_outputs.unwrap(); + assert_eq!(outputs.len(), 1); + assert_eq!( + outputs[0], + Output::Change { + to: address, + amount, + asset_id: AssetId::default() + } + ); + } else { + panic!("Expected preconfirmation status"); + } + + assert!(matches!( + tx_statuses_subscriber.next().await.unwrap().unwrap(), + TransactionStatus::Failure { .. } + )); +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 4)] +async fn preconfirmation__received_tx_inserted_end_block_open_period() { + let mut config = Config::local_node(); + let block_production_period = Duration::from_secs(1); + let address = Address::new([0; 32]); + + config.block_production = Trigger::Open { + period: block_production_period, + }; + let srv = FuelService::new_node(config).await.unwrap(); + let client = FuelClient::from(srv.bound_address); + + // Given + let tx = TransactionBuilder::script( + vec![op::ret(RegId::ONE)].into_iter().collect(), + vec![], + ) + .script_gas_limit(1_000_000) + .add_fee_input() + .add_output(Output::variable(address, 0, AssetId::default())) + .finalize_as_transaction(); + + // When + client + .submit_and_await_status(&tx) + .await + .unwrap() + .enumerate() + .for_each(|(event_idx, r)| async move { + let r = r.unwrap(); + // Then + match (event_idx, r) { + (0, TransactionStatus::Submitted { .. }) => {} + (1, TransactionStatus::PreconfirmationSuccess { .. }) => {} + (2, TransactionStatus::Success { block_height, .. }) => { + assert_eq!(block_height, BlockHeight::new(1)); + } + (_, r) => panic!("Unexpected event: {:?}", r), + } + }) + .await; +} + +#[tokio::test] +async fn preconfirmation__received_after_execution__multiple_txs() { + let mut rng = rand::thread_rng(); + let mut config = Config::local_node(); + config.block_production = Trigger::Never; + + let srv = FuelService::new_node(config).await.unwrap(); + let client = FuelClient::from(srv.bound_address); + + // Given + let tx1 = TransactionBuilder::script( + vec![op::ret(RegId::ONE)].into_iter().collect(), + vec![], + ) + .script_gas_limit(1_000_000) + .add_unsigned_coin_input( + SecretKey::random(&mut rng), + rng.gen(), + 10, + AssetId::default(), + Default::default(), + ) + .add_output(Output::variable( + Address::new([0; 32]), + 0, + AssetId::default(), + )) + .finalize_as_transaction(); + let tx2 = TransactionBuilder::script( + vec![op::ret(RegId::ONE)].into_iter().collect(), + vec![1, 2, 3], + ) + .script_gas_limit(1_000_000) + .add_unsigned_coin_input( + SecretKey::random(&mut rng), + rng.gen(), + 10, + AssetId::default(), + Default::default(), + ) + .add_output(Output::variable( + Address::new([0; 32]), + 0, + AssetId::default(), + )) + .finalize_as_transaction(); + + // Given + let mut tx_statuses_subscriber1 = client.submit_and_await_status(&tx1).await.unwrap(); + let mut tx_statuses_subscriber2 = client.submit_and_await_status(&tx2).await.unwrap(); + + // When + assert!(matches!( + tx_statuses_subscriber1.next().await.unwrap().unwrap(), + TransactionStatus::Submitted { .. } + )); + assert!(matches!( + tx_statuses_subscriber2.next().await.unwrap().unwrap(), + TransactionStatus::Submitted { .. } + )); + client.produce_blocks(1, None).await.unwrap(); + assert!(matches!( + tx_statuses_subscriber1.next().await.unwrap().unwrap(), + TransactionStatus::PreconfirmationSuccess { .. } + )); + assert!(matches!( + tx_statuses_subscriber2.next().await.unwrap().unwrap(), + TransactionStatus::PreconfirmationSuccess { .. } + )); + // Then + assert!(matches!( + tx_statuses_subscriber1.next().await.unwrap().unwrap(), + TransactionStatus::Success { block_height, .. } if block_height == BlockHeight::new(1) + )); + assert!(matches!( + tx_statuses_subscriber2.next().await.unwrap().unwrap(), + TransactionStatus::Success { block_height, .. } if block_height == BlockHeight::new(1) + )); +} diff --git a/tests/tests/preconfirmations_gossip.rs b/tests/tests/preconfirmations_gossip.rs new file mode 100644 index 00000000000..3309a9b5314 --- /dev/null +++ b/tests/tests/preconfirmations_gossip.rs @@ -0,0 +1,457 @@ +use std::{ + hash::{ + DefaultHasher, + Hash, + Hasher, + }, + time::Duration, +}; + +use fuel_core::{ + p2p_test_helpers::{ + make_nodes, + BootstrapSetup, + Nodes, + ProducerSetup, + ValidatorSetup, + }, + service::Config, +}; +use fuel_core_client::client::{ + types::TransactionStatus, + FuelClient, +}; +use fuel_core_poa::Trigger; +use fuel_core_types::{ + fuel_asm::{ + op, + RegId, + }, + fuel_tx::{ + Address, + AssetId, + Input, + Output, + Receipt, + TransactionBuilder, + TxPointer, + UniqueIdentifier, + }, + fuel_types::BlockHeight, + fuel_vm::SecretKey, +}; +use futures::StreamExt; +use rand::{ + rngs::StdRng, + Rng, + SeedableRng, +}; + +fn config_with_preconfirmations(block_production_period: Duration) -> Config { + let mut config = Config::local_node(); + + config.p2p.as_mut().unwrap().subscribe_to_pre_confirmations = true; + config.txpool.pending_pool_tx_ttl = Duration::from_secs(1); + config + .pre_confirmation_signature_service + .echo_delegation_interval = Duration::from_millis(100); + config.block_production = Trigger::Open { + period: block_production_period, + }; + + config +} + +#[tokio::test(flavor = "multi_thread")] +async fn preconfirmation__propagate_p2p_after_successful_execution() { + let mut rng = rand::thread_rng(); + let address = Address::new([0; 32]); + let block_production_period = Duration::from_secs(8); + let gas_limit = 1_000_000; + let amount = 10; + + // Given + let script = [ + op::addi(0x10, RegId::ZERO, 0xca), + op::addi(0x11, RegId::ZERO, 0xba), + op::log(0x10, 0x11, RegId::ZERO, RegId::ZERO), + op::ret(RegId::ONE), + ]; + let script: Vec = script + .iter() + .flat_map(|op| u32::from(*op).to_be_bytes()) + .collect(); + + // Given + let tx = TransactionBuilder::script(script, vec![]) + .script_gas_limit(gas_limit) + .add_unsigned_coin_input( + SecretKey::random(&mut rng), + rng.gen(), + amount, + AssetId::default(), + Default::default(), + ) + .add_output(Output::change(address, 0, AssetId::default())) + .finalize_as_transaction(); + let tx_id = tx.id(&Default::default()); + // Create a random seed based on the test parameters. + let mut hasher = DefaultHasher::new(); + let num_txs = 1; + let num_validators = 1; + let num_partitions = 1; + (num_txs, num_validators, num_partitions, line!()).hash(&mut hasher); + let mut rng = StdRng::seed_from_u64(hasher.finish()); + + // Create a set of key pairs. + let secrets: Vec<_> = (0..1).map(|_| SecretKey::random(&mut rng)).collect(); + let pub_keys: Vec<_> = secrets + .clone() + .into_iter() + .map(|secret| Input::owner(&secret.public_key())) + .collect(); + + // Create a producer for each key pair and a set of validators that share + // the same key pair. + let Nodes { + producers: _producers, + validators, + bootstrap_nodes: _dont_drop, + } = make_nodes( + pub_keys + .iter() + .map(|pub_key| Some(BootstrapSetup::new(*pub_key))), + secrets.clone().into_iter().enumerate().map(|(i, secret)| { + Some( + ProducerSetup::new(secret) + .with_name(format!("{}:producer", pub_keys[i])) + .utxo_validation(false), + ) + }), + pub_keys.iter().flat_map(|pub_key| { + (0..num_validators).map(move |i| { + Some( + ValidatorSetup::new(*pub_key) + .with_name(format!("{pub_key}:{i}")) + .utxo_validation(false), + ) + }) + }), + Some(config_with_preconfirmations(block_production_period)), + ) + .await; + + let sentry = &validators[0]; + + // Sleep to let time for exchange preconfirmations delegate public keys + tokio::time::sleep(Duration::from_secs(1)).await; + + // When + let client_sentry = FuelClient::from(sentry.node.bound_address); + let mut tx_statuses_subscriber = client_sentry + .submit_and_await_status(&tx) + .await + .expect("Should be able to subscribe for events"); + + // Then + assert!(matches!( + tx_statuses_subscriber.next().await.unwrap().unwrap(), + TransactionStatus::Submitted { .. } + )); + + let status = + tokio::time::timeout(Duration::from_secs(1), tx_statuses_subscriber.next()) + .await + .unwrap() + .unwrap() + .unwrap(); + if let TransactionStatus::PreconfirmationSuccess { + tx_pointer, + total_fee, + total_gas: _, + transaction_id, + receipts, + resolved_outputs, + } = status.clone() + { + // Then + assert_eq!(tx_pointer, TxPointer::new(BlockHeight::new(1), 1)); + assert_eq!(total_fee, 0); + assert_eq!(transaction_id, tx_id); + let receipts = receipts.unwrap(); + assert_eq!(receipts.len(), 3); + assert!(matches!(receipts[0], + Receipt::Log { + ra, rb, .. + } if ra == 0xca && rb == 0xba)); + + assert!(matches!(receipts[1], + Receipt::Return { + val, .. + } if val == 1)); + let outputs = resolved_outputs.unwrap(); + assert_eq!(outputs.len(), 1); + assert_eq!( + outputs[0], + Output::Change { + to: address, + amount, + asset_id: AssetId::default() + } + ); + } else { + panic!("Expected preconfirmation status, got {status:?}"); + } + assert!(matches!( + tx_statuses_subscriber.next().await.unwrap().unwrap(), + TransactionStatus::Success { .. } + )); +} + +#[tokio::test(flavor = "multi_thread")] +async fn preconfirmation__propagate_p2p_after_failed_execution() { + let mut rng = rand::thread_rng(); + let address = Address::new([0; 32]); + let block_production_period = Duration::from_secs(8); + let gas_limit = 1_000_000; + let amount = 10; + + let script = [ + op::addi(0x10, RegId::ZERO, 0xca), + op::addi(0x11, RegId::ZERO, 0xba), + op::log(0x10, 0x11, RegId::ZERO, RegId::ZERO), + op::rvrt(RegId::ONE), + op::ret(RegId::ONE), + ]; + let script: Vec = script + .iter() + .flat_map(|op| u32::from(*op).to_be_bytes()) + .collect(); + + // Given + let tx = TransactionBuilder::script(script, vec![]) + .script_gas_limit(gas_limit) + .add_unsigned_coin_input( + SecretKey::random(&mut rng), + rng.gen(), + amount, + AssetId::default(), + Default::default(), + ) + .add_output(Output::change(address, 0, AssetId::default())) + .finalize_as_transaction(); + let tx_id = tx.id(&Default::default()); + // Create a random seed based on the test parameters. + let mut hasher = DefaultHasher::new(); + let num_txs = 1; + let num_validators = 1; + let num_partitions = 1; + (num_txs, num_validators, num_partitions, line!()).hash(&mut hasher); + let mut rng = StdRng::seed_from_u64(hasher.finish()); + + // Create a set of key pairs. + let secrets: Vec<_> = (0..1).map(|_| SecretKey::random(&mut rng)).collect(); + let pub_keys: Vec<_> = secrets + .clone() + .into_iter() + .map(|secret| Input::owner(&secret.public_key())) + .collect(); + + // Create a producer for each key pair and a set of validators that share + // the same key pair. + let Nodes { + producers: _producers, + validators, + bootstrap_nodes: _dont_drop, + } = make_nodes( + pub_keys + .iter() + .map(|pub_key| Some(BootstrapSetup::new(*pub_key))), + secrets.clone().into_iter().enumerate().map(|(i, secret)| { + Some( + ProducerSetup::new(secret) + .with_name(format!("{}:producer", pub_keys[i])) + .utxo_validation(false), + ) + }), + pub_keys.iter().flat_map(|pub_key| { + (0..num_validators).map(move |i| { + Some( + ValidatorSetup::new(*pub_key) + .with_name(format!("{pub_key}:{i}")) + .utxo_validation(false), + ) + }) + }), + Some(config_with_preconfirmations(block_production_period)), + ) + .await; + + let sentry = &validators[0]; + + // Sleep to let time for exchange preconfirmations delegate public keys + tokio::time::sleep(Duration::from_secs(1)).await; + + // When + let client_sentry = FuelClient::from(sentry.node.bound_address); + let mut tx_statuses_subscriber = client_sentry + .submit_and_await_status(&tx) + .await + .expect("Should be able to subscribe for events"); + + assert!(matches!( + tx_statuses_subscriber.next().await.unwrap().unwrap(), + TransactionStatus::Submitted { .. } + )); + + let status = + tokio::time::timeout(Duration::from_secs(1), tx_statuses_subscriber.next()) + .await + .unwrap() + .unwrap() + .unwrap(); + + if let TransactionStatus::PreconfirmationFailure { + tx_pointer, + total_fee, + total_gas: _, + transaction_id, + receipts, + resolved_outputs, + reason: _, + } = status + { + // Then + assert_eq!(tx_pointer, TxPointer::new(BlockHeight::new(1), 1)); + assert_eq!(total_fee, 0); + assert_eq!(transaction_id, tx_id); + let receipts = receipts.unwrap(); + assert_eq!(receipts.len(), 3); + assert!(matches!(receipts[0], + Receipt::Log { + ra, rb, .. + } if ra == 0xca && rb == 0xba)); + + assert!(matches!(receipts[1], + Receipt::Revert { + ra, .. + } if ra == 1)); + let outputs = resolved_outputs.unwrap(); + assert_eq!(outputs.len(), 1); + assert_eq!( + outputs[0], + Output::Change { + to: address, + amount, + asset_id: AssetId::default() + } + ); + } else { + panic!("Expected preconfirmation status"); + } + + assert!(matches!( + tx_statuses_subscriber.next().await.unwrap().unwrap(), + TransactionStatus::Failure { .. } + )); +} + +#[tokio::test(flavor = "multi_thread")] +async fn preconfirmation__propagate_p2p_after_squeezed_out_on_producer() { + let mut rng = rand::thread_rng(); + + let block_production_period = Duration::from_secs(8); + let gas_limit = 1_000_000; + let tx = TransactionBuilder::script( + vec![op::ret(RegId::ONE)].into_iter().collect(), + vec![], + ) + .script_gas_limit(gas_limit) + .add_unsigned_coin_input( + SecretKey::random(&mut rng), + rng.gen(), + 10, + Default::default(), + Default::default(), + ) + .add_output(Output::Change { + to: Default::default(), + amount: 0, + asset_id: Default::default(), + }) + .finalize_as_transaction(); + + // Create a random seed based on the test parameters. + let mut hasher = DefaultHasher::new(); + let num_txs = 1; + let num_validators = 1; + let num_partitions = 1; + (num_txs, num_validators, num_partitions, line!()).hash(&mut hasher); + let mut rng = StdRng::seed_from_u64(hasher.finish()); + + // Create a set of key pairs. + let secrets: Vec<_> = (0..1).map(|_| SecretKey::random(&mut rng)).collect(); + let pub_keys: Vec<_> = secrets + .clone() + .into_iter() + .map(|secret| Input::owner(&secret.public_key())) + .collect(); + + // Create a producer for each key pair and a set of validators that share + // the same key pair. + + // Given + // Disable UTXO validation in TxPool so that the transaction is squeezed out by + // block production + let Nodes { + producers: _producers, + validators, + bootstrap_nodes: _dont_drop, + } = make_nodes( + pub_keys + .iter() + .map(|pub_key| Some(BootstrapSetup::new(*pub_key))), + secrets.clone().into_iter().enumerate().map(|(i, secret)| { + Some( + ProducerSetup::new(secret) + .with_name(format!("{}:producer", pub_keys[i])) + .utxo_validation(true), + ) + }), + pub_keys.iter().flat_map(|pub_key| { + (0..num_validators).map(move |i| { + Some( + ValidatorSetup::new(*pub_key) + .with_name(format!("{pub_key}:{i}")) + .utxo_validation(false), + ) + }) + }), + Some(config_with_preconfirmations(block_production_period)), + ) + .await; + + let sentry = &validators[0]; + + // Sleep to let time for exchange preconfirmations delegate public keys + tokio::time::sleep(Duration::from_secs(1)).await; + + // When + let client_sentry = FuelClient::from(sentry.node.bound_address); + let mut tx_statuses_subscriber = client_sentry + .submit_and_await_status(&tx) + .await + .expect("Should be able to subscribe for events"); + + assert!(matches!( + tx_statuses_subscriber.next().await.unwrap().unwrap(), + TransactionStatus::Submitted { .. } + )); + tracing::info!("Submitted"); + + assert!(matches!( + tx_statuses_subscriber.next().await.unwrap().unwrap(), + TransactionStatus::SqueezedOut { .. } + )); + tracing::info!("SqueezedOut"); +} diff --git a/tests/tests/tx.rs b/tests/tests/tx.rs index 9a46d14c0e8..2bb26922758 100644 --- a/tests/tests/tx.rs +++ b/tests/tests/tx.rs @@ -390,6 +390,11 @@ async fn submit_and_await_status() { intermediate_status, TransactionStatus::Submitted { .. } )); + let preconfirmation_status = status_stream.next().await.unwrap().unwrap(); + assert!(matches!( + preconfirmation_status, + TransactionStatus::PreconfirmationSuccess { .. } + )); let final_status = status_stream.next().await.unwrap().unwrap(); assert!(matches!(final_status, TransactionStatus::Success { .. })); } diff --git a/tests/tests/tx/txn_status_subscription.rs b/tests/tests/tx/txn_status_subscription.rs index e01b60b60de..b7e56b64489 100644 --- a/tests/tests/tx/txn_status_subscription.rs +++ b/tests/tests/tx/txn_status_subscription.rs @@ -150,9 +150,11 @@ async fn subscribe_txn_status() { (0, 0) => assert!(matches!(r, fuel_core_client::client::types::TransactionStatus::Submitted{ .. }), "{r:?}"), (0, 1) => assert!(matches!(r, fuel_core_client::client::types::TransactionStatus::SqueezedOut{ .. }), "{r:?}"), (1, 0) => assert!(matches!(r, fuel_core_client::client::types::TransactionStatus::Submitted{ .. }), "{r:?}"), - (1, 1) => assert!(matches!(r, fuel_core_client::client::types::TransactionStatus::Success{ .. }), "{r:?}"), + (1, 1) => assert!(matches!(r, fuel_core_client::client::types::TransactionStatus::PreconfirmationSuccess{ .. }), "{r:?}"), + (1, 2) => assert!(matches!(r, fuel_core_client::client::types::TransactionStatus::Success{ .. }), "{r:?}"), (2, 0) => assert!(matches!(r, fuel_core_client::client::types::TransactionStatus::Submitted{ .. }), "{r:?}"), - (2, 1) => assert!(matches!(r, fuel_core_client::client::types::TransactionStatus::Failure{ .. }), "{r:?}"), + (2, 1) => assert!(matches!(r, fuel_core_client::client::types::TransactionStatus::PreconfirmationFailure{ .. }), "{r:?}"), + (2, 2) => assert!(matches!(r, fuel_core_client::client::types::TransactionStatus::Failure{ .. }), "{r:?}"), _ => unreachable!("{} {} {:?}", txn_idx, event_idx, r), } })