diff --git a/zebra-consensus/src/block.rs b/zebra-consensus/src/block.rs index 611aea2ceba..fe8499abfba 100644 --- a/zebra-consensus/src/block.rs +++ b/zebra-consensus/src/block.rs @@ -8,6 +8,7 @@ //! verification, where it may be accepted or rejected. use std::{ + collections::HashSet, future::Future, pin::Pin, sync::Arc, @@ -25,7 +26,7 @@ use zebra_chain::{ amount::Amount, block, parameters::{subsidy::FundingStreamReceiver, Network}, - transparent, + transaction, transparent, work::equihash, }; use zebra_state as zs; @@ -232,13 +233,21 @@ where &block, &transaction_hashes, )); - for transaction in &block.transactions { + + let known_outpoint_hashes: Arc> = + Arc::new(known_utxos.keys().map(|outpoint| outpoint.hash).collect()); + + for (&transaction_hash, transaction) in + transaction_hashes.iter().zip(block.transactions.iter()) + { let rsp = transaction_verifier .ready() .await .expect("transaction verifier is always ready") .call(tx::Request::Block { + transaction_hash, transaction: transaction.clone(), + known_outpoint_hashes: known_outpoint_hashes.clone(), known_utxos: known_utxos.clone(), height, time: block.header.time, diff --git a/zebra-consensus/src/transaction.rs b/zebra-consensus/src/transaction.rs index aac77a055d6..e25b3b8d28e 100644 --- a/zebra-consensus/src/transaction.rs +++ b/zebra-consensus/src/transaction.rs @@ -1,7 +1,7 @@ //! Asynchronous verification of transactions. use std::{ - collections::HashMap, + collections::{HashMap, HashSet}, future::Future, pin::Pin, sync::Arc, @@ -146,8 +146,12 @@ where pub enum Request { /// Verify the supplied transaction as part of a block. Block { + /// The transaction hash. + transaction_hash: transaction::Hash, /// The transaction itself. transaction: Arc, + /// Set of transaction hashes that create new transparent outputs. + known_outpoint_hashes: Arc>, /// Additional UTXOs which are known at the time of verification. known_utxos: Arc>, /// The height of the block containing this transaction. @@ -259,6 +263,16 @@ impl Request { } } + /// The mined transaction ID for the transaction in this request. + pub fn tx_mined_id(&self) -> transaction::Hash { + match self { + Request::Block { + transaction_hash, .. + } => *transaction_hash, + Request::Mempool { transaction, .. } => transaction.id.mined_id(), + } + } + /// The set of additional known unspent transaction outputs that's in this request. pub fn known_utxos(&self) -> Arc> { match self { @@ -267,6 +281,17 @@ impl Request { } } + /// The set of additional known [`transparent::OutPoint`]s of unspent transaction outputs that's in this request. + pub fn known_outpoint_hashes(&self) -> Arc> { + match self { + Request::Block { + known_outpoint_hashes, + .. + } => known_outpoint_hashes.clone(), + Request::Mempool { .. } => HashSet::new().into(), + } + } + /// The height used to select the consensus rules for verifying this transaction. pub fn height(&self) -> block::Height { match self { @@ -377,6 +402,16 @@ where async move { tracing::trace!(?tx_id, ?req, "got tx verify request"); + if let Some(result) = Self::try_find_verified_unmined_tx(&req, mempool.clone()).await { + let verified_tx = result?; + + return Ok(Response::Block { + tx_id: verified_tx.transaction.id, + miner_fee: Some(verified_tx.miner_fee), + legacy_sigop_count: verified_tx.legacy_sigop_count + }); + } + // Do quick checks first check::has_inputs_and_outputs(&tx)?; check::has_enough_orchard_flags(&tx)?; @@ -608,8 +643,52 @@ where } } - /// Waits for the UTXOs that are being spent by the given transaction to arrive in - /// the state for [`Block`](Request::Block) requests. + /// Attempts to find a transaction in the mempool by its transaction hash and checks + /// that all of its dependencies are available in the block. + /// + /// Returns [`Some(Ok(VerifiedUnminedTx))`](VerifiedUnminedTx) if successful, + /// None if the transaction id was not found in the mempool, + /// or `Some(Err(TransparentInputNotFound))` if the transaction was found, but some of its + /// dependencies are missing in the block. + async fn try_find_verified_unmined_tx( + req: &Request, + mempool: Option>, + ) -> Option> { + if req.is_mempool() || req.transaction().is_coinbase() { + return None; + } + + let mempool = mempool?; + let known_outpoint_hashes = req.known_outpoint_hashes(); + let tx_id = req.tx_mined_id(); + + let mempool::Response::TransactionWithDeps { + transaction, + dependencies, + } = mempool + .oneshot(mempool::Request::TransactionWithDepsByMinedId(tx_id)) + .await + .ok()? + else { + panic!("unexpected response to TransactionWithDepsByMinedId request"); + }; + + // Note: This does not verify that the spends are in order, this should be + // done during contextual validation in zebra-state. + let has_all_tx_deps = dependencies + .into_iter() + .all(|dependency_id| known_outpoint_hashes.contains(&dependency_id)); + + let result = if has_all_tx_deps { + Ok(transaction) + } else { + Err(TransactionError::TransparentInputNotFound) + }; + + Some(result) + } + + /// Wait for the UTXOs that are being spent by the given transaction. /// /// Looks up UTXOs that are being spent by the given transaction in the state or waits /// for them to be added to the mempool for [`Mempool`](Request::Mempool) requests. diff --git a/zebra-consensus/src/transaction/tests.rs b/zebra-consensus/src/transaction/tests.rs index d42bbb8594c..b323cd7f555 100644 --- a/zebra-consensus/src/transaction/tests.rs +++ b/zebra-consensus/src/transaction/tests.rs @@ -2,7 +2,10 @@ // // TODO: split fixed test vectors into a `vectors` module? -use std::{collections::HashMap, sync::Arc}; +use std::{ + collections::{HashMap, HashSet}, + sync::Arc, +}; use chrono::{DateTime, TimeZone, Utc}; use color_eyre::eyre::Report; @@ -982,8 +985,10 @@ async fn v5_transaction_is_rejected_before_nu5_activation() { let result = verifier .oneshot(Request::Block { + transaction_hash: transaction.hash(), transaction: Arc::new(transaction), known_utxos: Arc::new(HashMap::new()), + known_outpoint_hashes: Arc::new(HashSet::new()), height: canopy .activation_height(&network) .expect("Canopy activation height is specified"), @@ -1042,8 +1047,10 @@ fn v5_transaction_is_accepted_after_nu5_activation_for_network(network: Network) let result = verifier .oneshot(Request::Block { + transaction_hash: transaction.hash(), transaction: Arc::new(transaction), known_utxos: Arc::new(HashMap::new()), + known_outpoint_hashes: Arc::new(HashSet::new()), height: expiry_height, time: DateTime::::MAX_UTC, }) @@ -1097,8 +1104,10 @@ async fn v4_transaction_with_transparent_transfer_is_accepted() { let result = verifier .oneshot(Request::Block { + transaction_hash: transaction.hash(), transaction: Arc::new(transaction), known_utxos: Arc::new(known_utxos), + known_outpoint_hashes: Arc::new(HashSet::new()), height: transaction_block_height, time: DateTime::::MAX_UTC, }) @@ -1141,8 +1150,10 @@ async fn v4_transaction_with_last_valid_expiry_height() { let result = verifier .oneshot(Request::Block { + transaction_hash: transaction.hash(), transaction: Arc::new(transaction.clone()), known_utxos: Arc::new(known_utxos), + known_outpoint_hashes: Arc::new(HashSet::new()), height: block_height, time: DateTime::::MAX_UTC, }) @@ -1186,8 +1197,10 @@ async fn v4_coinbase_transaction_with_low_expiry_height() { let result = verifier .oneshot(Request::Block { + transaction_hash: transaction.hash(), transaction: Arc::new(transaction.clone()), known_utxos: Arc::new(HashMap::new()), + known_outpoint_hashes: Arc::new(HashSet::new()), height: block_height, time: DateTime::::MAX_UTC, }) @@ -1233,8 +1246,10 @@ async fn v4_transaction_with_too_low_expiry_height() { let result = verifier .oneshot(Request::Block { + transaction_hash: transaction.hash(), transaction: Arc::new(transaction.clone()), known_utxos: Arc::new(known_utxos), + known_outpoint_hashes: Arc::new(HashSet::new()), height: block_height, time: DateTime::::MAX_UTC, }) @@ -1283,8 +1298,10 @@ async fn v4_transaction_with_exceeding_expiry_height() { let result = verifier .oneshot(Request::Block { + transaction_hash: transaction.hash(), transaction: Arc::new(transaction.clone()), known_utxos: Arc::new(known_utxos), + known_outpoint_hashes: Arc::new(HashSet::new()), height: block_height, time: DateTime::::MAX_UTC, }) @@ -1336,8 +1353,10 @@ async fn v4_coinbase_transaction_with_exceeding_expiry_height() { let result = verifier .oneshot(Request::Block { + transaction_hash: transaction.hash(), transaction: Arc::new(transaction.clone()), known_utxos: Arc::new(HashMap::new()), + known_outpoint_hashes: Arc::new(HashSet::new()), height: block_height, time: DateTime::::MAX_UTC, }) @@ -1387,8 +1406,10 @@ async fn v4_coinbase_transaction_is_accepted() { let result = verifier .oneshot(Request::Block { + transaction_hash: transaction.hash(), transaction: Arc::new(transaction), known_utxos: Arc::new(HashMap::new()), + known_outpoint_hashes: Arc::new(HashSet::new()), height: transaction_block_height, time: DateTime::::MAX_UTC, }) @@ -1442,8 +1463,10 @@ async fn v4_transaction_with_transparent_transfer_is_rejected_by_the_script() { let result = verifier .oneshot(Request::Block { + transaction_hash: transaction.hash(), transaction: Arc::new(transaction), known_utxos: Arc::new(known_utxos), + known_outpoint_hashes: Arc::new(HashSet::new()), height: transaction_block_height, time: DateTime::::MAX_UTC, }) @@ -1497,8 +1520,10 @@ async fn v4_transaction_with_conflicting_transparent_spend_is_rejected() { let result = verifier .oneshot(Request::Block { + transaction_hash: transaction.hash(), transaction: Arc::new(transaction), known_utxos: Arc::new(known_utxos), + known_outpoint_hashes: Arc::new(HashSet::new()), height: transaction_block_height, time: DateTime::::MAX_UTC, }) @@ -1568,8 +1593,10 @@ fn v4_transaction_with_conflicting_sprout_nullifier_inside_joinsplit_is_rejected let result = verifier .oneshot(Request::Block { + transaction_hash: transaction.hash(), transaction: Arc::new(transaction), known_utxos: Arc::new(HashMap::new()), + known_outpoint_hashes: Arc::new(HashSet::new()), height: transaction_block_height, time: DateTime::::MAX_UTC, }) @@ -1644,8 +1671,10 @@ fn v4_transaction_with_conflicting_sprout_nullifier_across_joinsplits_is_rejecte let result = verifier .oneshot(Request::Block { + transaction_hash: transaction.hash(), transaction: Arc::new(transaction), known_utxos: Arc::new(HashMap::new()), + known_outpoint_hashes: Arc::new(HashSet::new()), height: transaction_block_height, time: DateTime::::MAX_UTC, }) @@ -1703,8 +1732,10 @@ async fn v5_transaction_with_transparent_transfer_is_accepted() { let result = verifier .oneshot(Request::Block { + transaction_hash: transaction.hash(), transaction: Arc::new(transaction), known_utxos: Arc::new(known_utxos), + known_outpoint_hashes: Arc::new(HashSet::new()), height: transaction_block_height, time: DateTime::::MAX_UTC, }) @@ -1749,8 +1780,10 @@ async fn v5_transaction_with_last_valid_expiry_height() { let result = verifier .oneshot(Request::Block { + transaction_hash: transaction.hash(), transaction: Arc::new(transaction.clone()), known_utxos: Arc::new(known_utxos), + known_outpoint_hashes: Arc::new(HashSet::new()), height: block_height, time: DateTime::::MAX_UTC, }) @@ -1794,8 +1827,10 @@ async fn v5_coinbase_transaction_expiry_height() { let result = verifier .clone() .oneshot(Request::Block { + transaction_hash: transaction.hash(), transaction: Arc::new(transaction.clone()), known_utxos: Arc::new(HashMap::new()), + known_outpoint_hashes: Arc::new(HashSet::new()), height: block_height, time: DateTime::::MAX_UTC, }) @@ -1815,8 +1850,10 @@ async fn v5_coinbase_transaction_expiry_height() { let result = verifier .clone() .oneshot(Request::Block { + transaction_hash: transaction.hash(), transaction: Arc::new(new_transaction.clone()), known_utxos: Arc::new(HashMap::new()), + known_outpoint_hashes: Arc::new(HashSet::new()), height: block_height, time: DateTime::::MAX_UTC, }) @@ -1844,8 +1881,10 @@ async fn v5_coinbase_transaction_expiry_height() { let result = verifier .clone() .oneshot(Request::Block { + transaction_hash: transaction.hash(), transaction: Arc::new(new_transaction.clone()), known_utxos: Arc::new(HashMap::new()), + known_outpoint_hashes: Arc::new(HashSet::new()), height: block_height, time: DateTime::::MAX_UTC, }) @@ -1875,8 +1914,10 @@ async fn v5_coinbase_transaction_expiry_height() { let result = verifier .clone() .oneshot(Request::Block { + transaction_hash: transaction.hash(), transaction: Arc::new(new_transaction.clone()), known_utxos: Arc::new(HashMap::new()), + known_outpoint_hashes: Arc::new(HashSet::new()), height: new_expiry_height, time: DateTime::::MAX_UTC, }) @@ -1924,8 +1965,10 @@ async fn v5_transaction_with_too_low_expiry_height() { let result = verifier .oneshot(Request::Block { + transaction_hash: transaction.hash(), transaction: Arc::new(transaction.clone()), known_utxos: Arc::new(known_utxos), + known_outpoint_hashes: Arc::new(HashSet::new()), height: block_height, time: DateTime::::MAX_UTC, }) @@ -1975,8 +2018,10 @@ async fn v5_transaction_with_exceeding_expiry_height() { let result = verifier .oneshot(Request::Block { + transaction_hash: transaction.hash(), transaction: Arc::new(transaction.clone()), known_utxos: Arc::new(known_utxos), + known_outpoint_hashes: Arc::new(HashSet::new()), height: block_height, time: DateTime::::MAX_UTC, }) @@ -2029,8 +2074,10 @@ async fn v5_coinbase_transaction_is_accepted() { let result = verifier .oneshot(Request::Block { + transaction_hash: transaction.hash(), transaction: Arc::new(transaction), known_utxos: Arc::new(known_utxos), + known_outpoint_hashes: Arc::new(HashSet::new()), height: transaction_block_height, time: DateTime::::MAX_UTC, }) @@ -2086,8 +2133,10 @@ async fn v5_transaction_with_transparent_transfer_is_rejected_by_the_script() { let result = verifier .oneshot(Request::Block { + transaction_hash: transaction.hash(), transaction: Arc::new(transaction), known_utxos: Arc::new(known_utxos), + known_outpoint_hashes: Arc::new(HashSet::new()), height: transaction_block_height, time: DateTime::::MAX_UTC, }) @@ -2143,8 +2192,10 @@ async fn v5_transaction_with_conflicting_transparent_spend_is_rejected() { let result = verifier .oneshot(Request::Block { + transaction_hash: transaction.hash(), transaction: Arc::new(transaction), known_utxos: Arc::new(known_utxos), + known_outpoint_hashes: Arc::new(HashSet::new()), height: transaction_block_height, time: DateTime::::MAX_UTC, }) @@ -2187,8 +2238,10 @@ fn v4_with_signed_sprout_transfer_is_accepted() { // Test the transaction verifier let result = verifier .oneshot(Request::Block { + transaction_hash: transaction.hash(), transaction, known_utxos: Arc::new(HashMap::new()), + known_outpoint_hashes: Arc::new(HashSet::new()), height, time: DateTime::::MAX_UTC, }) @@ -2276,8 +2329,10 @@ async fn v4_with_joinsplit_is_rejected_for_modification( let result = verifier .clone() .oneshot(Request::Block { + transaction_hash: transaction.hash(), transaction: transaction.clone(), known_utxos: Arc::new(HashMap::new()), + known_outpoint_hashes: Arc::new(HashSet::new()), height, time: DateTime::::MAX_UTC, }) @@ -2322,8 +2377,10 @@ fn v4_with_sapling_spends() { // Test the transaction verifier let result = verifier .oneshot(Request::Block { + transaction_hash: transaction.hash(), transaction, known_utxos: Arc::new(HashMap::new()), + known_outpoint_hashes: Arc::new(HashSet::new()), height, time: DateTime::::MAX_UTC, }) @@ -2364,8 +2421,10 @@ fn v4_with_duplicate_sapling_spends() { // Test the transaction verifier let result = verifier .oneshot(Request::Block { + transaction_hash: transaction.hash(), transaction, known_utxos: Arc::new(HashMap::new()), + known_outpoint_hashes: Arc::new(HashSet::new()), height, time: DateTime::::MAX_UTC, }) @@ -2408,8 +2467,10 @@ fn v4_with_sapling_outputs_and_no_spends() { // Test the transaction verifier let result = verifier .oneshot(Request::Block { + transaction_hash: transaction.hash(), transaction, known_utxos: Arc::new(HashMap::new()), + known_outpoint_hashes: Arc::new(HashSet::new()), height, time: DateTime::::MAX_UTC, }) @@ -2456,8 +2517,10 @@ fn v5_with_sapling_spends() { // Test the transaction verifier let result = verifier .oneshot(Request::Block { + transaction_hash: transaction.hash(), transaction: Arc::new(transaction), known_utxos: Arc::new(HashMap::new()), + known_outpoint_hashes: Arc::new(HashSet::new()), height, time: DateTime::::MAX_UTC, }) @@ -2499,8 +2562,10 @@ fn v5_with_duplicate_sapling_spends() { // Test the transaction verifier let result = verifier .oneshot(Request::Block { + transaction_hash: transaction.hash(), transaction: Arc::new(transaction), known_utxos: Arc::new(HashMap::new()), + known_outpoint_hashes: Arc::new(HashSet::new()), height, time: DateTime::::MAX_UTC, }) @@ -2561,8 +2626,10 @@ fn v5_with_duplicate_orchard_action() { // Test the transaction verifier let result = verifier .oneshot(Request::Block { + transaction_hash: transaction.hash(), transaction: Arc::new(transaction), known_utxos: Arc::new(HashMap::new()), + known_outpoint_hashes: Arc::new(HashSet::new()), height, time: DateTime::::MAX_UTC, }) diff --git a/zebra-consensus/src/transaction/tests/prop.rs b/zebra-consensus/src/transaction/tests/prop.rs index 856742e5d74..8fea9cf3433 100644 --- a/zebra-consensus/src/transaction/tests/prop.rs +++ b/zebra-consensus/src/transaction/tests/prop.rs @@ -1,6 +1,9 @@ //! Randomised property tests for transaction verification. -use std::{collections::HashMap, sync::Arc}; +use std::{ + collections::{HashMap, HashSet}, + sync::Arc, +}; use chrono::{DateTime, Duration, Utc}; use proptest::{collection::vec, prelude::*}; @@ -452,13 +455,16 @@ fn validate( tower::service_fn(|_| async { unreachable!("State service should not be called") }); let verifier = transaction::Verifier::new_for_tests(&network, state_service); let verifier = Buffer::new(verifier, 10); + let transaction_hash = transaction.hash(); // Test the transaction verifier verifier .clone() .oneshot(transaction::Request::Block { + transaction_hash, transaction: Arc::new(transaction), known_utxos: Arc::new(known_utxos), + known_outpoint_hashes: Arc::new(HashSet::new()), height, time: block_time, }) diff --git a/zebra-node-services/src/mempool.rs b/zebra-node-services/src/mempool.rs index 10f51cf4a30..793e5b1fe3b 100644 --- a/zebra-node-services/src/mempool.rs +++ b/zebra-node-services/src/mempool.rs @@ -61,6 +61,9 @@ pub enum Request { /// Outdated requests are pruned on a regular basis. AwaitOutput(transparent::OutPoint), + /// Request a [`VerifiedUnminedTx`] and its dependencies by its mined id. + TransactionWithDepsByMinedId(transaction::Hash), + /// Get all the [`VerifiedUnminedTx`] in the mempool. /// /// Equivalent to `TransactionsById(TransactionIds)`, @@ -124,6 +127,14 @@ pub enum Response { /// Response to [`Request::AwaitOutput`] with the transparent output UnspentOutput(transparent::Output), + /// Response to [`Request::TransactionWithDepsByMinedId`]. + TransactionWithDeps { + /// The queried transaction + transaction: VerifiedUnminedTx, + /// A list of dependencies of the queried transaction. + dependencies: HashSet, + }, + /// Returns all [`VerifiedUnminedTx`] in the mempool. // // TODO: make the Transactions response return VerifiedUnminedTx, diff --git a/zebrad/src/components/mempool.rs b/zebrad/src/components/mempool.rs index b94ad0b09b8..bc5796df615 100644 --- a/zebrad/src/components/mempool.rs +++ b/zebrad/src/components/mempool.rs @@ -733,6 +733,24 @@ impl Service for Mempool { async move { Ok(Response::Transactions(res)) }.boxed() } + Request::TransactionWithDepsByMinedId(tx_id) => { + trace!(?req, "got mempool request"); + + let res = if let Some((transaction, dependencies)) = + storage.transaction_with_deps(tx_id) + { + Ok(Response::TransactionWithDeps { + transaction, + dependencies, + }) + } else { + Err("transaction not found in mempool".into()) + }; + + trace!(?req, ?res, "answered mempool request"); + + async move { res }.boxed() + } Request::AwaitOutput(outpoint) => { trace!(?req, "got mempool request"); @@ -828,7 +846,7 @@ impl Service for Mempool { Request::TransactionsById(_) => Response::Transactions(Default::default()), Request::TransactionsByMinedId(_) => Response::Transactions(Default::default()), - Request::AwaitOutput(_) => { + Request::TransactionWithDepsByMinedId(_) | Request::AwaitOutput(_) => { return async move { Err("mempool is not active: wait for Zebra to sync to the tip".into()) } diff --git a/zebrad/src/components/mempool/storage.rs b/zebrad/src/components/mempool/storage.rs index ce6f09cf1d6..ef3733b128e 100644 --- a/zebrad/src/components/mempool/storage.rs +++ b/zebrad/src/components/mempool/storage.rs @@ -513,6 +513,23 @@ impl Storage { .map(|(_, tx)| &tx.transaction) } + /// Returns a transaction and the transaction ids of its dependencies, if it is in the verified set. + pub fn transaction_with_deps( + &self, + tx_id: transaction::Hash, + ) -> Option<(VerifiedUnminedTx, HashSet)> { + let tx = self.verified.transactions().get(&tx_id).cloned()?; + let deps = self + .verified + .transaction_dependencies() + .dependencies() + .get(&tx_id) + .cloned() + .unwrap_or_default(); + + Some((tx, deps)) + } + /// Returns `true` if a transaction exactly matching an [`UnminedTxId`] is in /// the mempool. ///