diff --git a/zebra-consensus/src/block.rs b/zebra-consensus/src/block.rs index 247079c401a..1c959dd284e 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 ef20881bbbf..c3ccb78452c 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, + 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)?; @@ -609,8 +644,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, the spend order + // should be verified 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 8627a578c62..417298cc7f3 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; @@ -694,13 +697,180 @@ async fn mempool_request_with_unmined_output_spends_is_accepted() { ); tokio::time::sleep(POLL_MEMPOOL_DELAY * 2).await; + // polled before AwaitOutput request and after a mempool transaction with transparent outputs + // is successfully verified assert_eq!( mempool.poll_count(), 2, - "the mempool service should have been polled twice, \ - first before being called with an AwaitOutput request, \ - then again shortly after a mempool transaction with transparent outputs \ - is successfully verified" + "the mempool service should have been polled twice" + ); +} + +#[tokio::test] +async fn skips_verification_of_block_transactions_in_mempool() { + let mut state: MockService<_, _, _, _> = MockService::build().for_prop_tests(); + let mempool: MockService<_, _, _, _> = MockService::build().for_prop_tests(); + let (mempool_setup_tx, mempool_setup_rx) = tokio::sync::oneshot::channel(); + let verifier = Verifier::new(&Network::Mainnet, state.clone(), mempool_setup_rx); + let verifier = Buffer::new(verifier, 1); + + mempool_setup_tx + .send(mempool.clone()) + .ok() + .expect("send should succeed"); + + let height = NetworkUpgrade::Nu6 + .activation_height(&Network::Mainnet) + .expect("Canopy activation height is specified"); + let fund_height = (height - 1).expect("fake source fund block height is too small"); + let (input, output, known_utxos) = mock_transparent_transfer( + fund_height, + true, + 0, + Amount::try_from(10001).expect("invalid value"), + ); + + // Create a non-coinbase V4 tx with the last valid expiry height. + let tx = Transaction::V5 { + network_upgrade: NetworkUpgrade::Nu6, + inputs: vec![input], + outputs: vec![output], + lock_time: LockTime::min_lock_time_timestamp(), + expiry_height: height, + sapling_shielded_data: None, + orchard_shielded_data: None, + }; + + let tx_hash = tx.hash(); + let input_outpoint = match tx.inputs()[0] { + transparent::Input::PrevOut { outpoint, .. } => outpoint, + transparent::Input::Coinbase { .. } => panic!("requires a non-coinbase transaction"), + }; + + tokio::spawn(async move { + state + .expect_request(zebra_state::Request::BestChainNextMedianTimePast) + .await + .expect("verifier should call mock state service with correct request") + .respond(zebra_state::Response::BestChainNextMedianTimePast( + DateTime32::MAX, + )); + + state + .expect_request(zebra_state::Request::UnspentBestChainUtxo(input_outpoint)) + .await + .expect("verifier should call mock state service with correct request") + .respond(zebra_state::Response::UnspentBestChainUtxo(None)); + + state + .expect_request_that(|req| { + matches!( + req, + zebra_state::Request::CheckBestChainTipNullifiersAndAnchors(_) + ) + }) + .await + .expect("verifier should call mock state service with correct request") + .respond(zebra_state::Response::ValidBestChainTipNullifiersAndAnchors); + }); + + let mut mempool_clone = mempool.clone(); + tokio::spawn(async move { + mempool_clone + .expect_request(mempool::Request::AwaitOutput(input_outpoint)) + .await + .expect("verifier should call mock state service with correct request") + .respond(mempool::Response::UnspentOutput( + known_utxos + .get(&input_outpoint) + .expect("input outpoint should exist in known_utxos") + .utxo + .output + .clone(), + )); + }); + + let verifier_response = verifier + .clone() + .oneshot(Request::Mempool { + transaction: tx.clone().into(), + height, + }) + .await; + + assert!( + verifier_response.is_ok(), + "expected successful verification, got: {verifier_response:?}" + ); + + let crate::transaction::Response::Mempool { + transaction, + spent_mempool_outpoints, + } = verifier_response.expect("already checked that response is ok") + else { + panic!("unexpected response variant from transaction verifier for Mempool request") + }; + + assert_eq!( + spent_mempool_outpoints, + vec![input_outpoint], + "spent_mempool_outpoints in tx verifier response should match input_outpoint" + ); + + let mut mempool_clone = mempool.clone(); + tokio::spawn(async move { + for _ in 0..2 { + mempool_clone + .expect_request(mempool::Request::TransactionWithDepsByMinedId(tx_hash)) + .await + .expect("verifier should call mock state service with correct request") + .respond(mempool::Response::TransactionWithDeps { + transaction: transaction.clone(), + dependencies: [input_outpoint.hash].into(), + }); + } + }); + + let make_request = |known_outpoint_hashes| Request::Block { + transaction_hash: tx_hash, + transaction: Arc::new(tx), + known_outpoint_hashes, + known_utxos: Arc::new(HashMap::new()), + height, + time: Utc::now(), + }; + + let crate::transaction::Response::Block { .. } = verifier + .clone() + .oneshot(make_request.clone()(Arc::new([input_outpoint.hash].into()))) + .await + .expect("should return Ok without calling state service") + else { + panic!("unexpected response variant from transaction verifier for Block request") + }; + + let verifier_response_err = *verifier + .clone() + .oneshot(make_request(Arc::new(HashSet::new()))) + .await + .expect_err("should return Err without calling state service") + .downcast::() + .expect("tx verifier error type should be TransactionError"); + + assert_eq!( + verifier_response_err, + TransactionError::TransparentInputNotFound, + "should be a transparent input not found error" + ); + + tokio::time::sleep(POLL_MEMPOOL_DELAY * 2).await; + // polled before AwaitOutput request, after a mempool transaction with transparent outputs, + // is successfully verified, and twice more when checking if a transaction in a block is + // already the mempool. + assert_eq!( + mempool.poll_count(), + 4, + "the mempool service should have been polled 4 times" ); } @@ -983,8 +1153,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"), @@ -1029,8 +1201,10 @@ fn v5_transaction_is_accepted_after_nu5_activation() { let verification_result = Verifier::new_for_tests(&network, state) .oneshot(Request::Block { + transaction_hash: tx.hash(), transaction: Arc::new(tx), known_utxos: Arc::new(HashMap::new()), + known_outpoint_hashes: Arc::new(HashSet::new()), height: expiry_height, time: DateTime::::MAX_UTC, }) @@ -1087,8 +1261,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, }) @@ -1131,8 +1307,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, }) @@ -1176,8 +1354,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, }) @@ -1223,8 +1403,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, }) @@ -1273,8 +1455,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, }) @@ -1326,8 +1510,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, }) @@ -1377,8 +1563,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, }) @@ -1432,8 +1620,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, }) @@ -1487,8 +1677,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, }) @@ -1558,8 +1750,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, }) @@ -1634,8 +1828,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, }) @@ -1693,8 +1889,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, }) @@ -1739,8 +1937,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, }) @@ -1784,8 +1984,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, }) @@ -1805,8 +2007,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, }) @@ -1834,8 +2038,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, }) @@ -1871,8 +2077,10 @@ async fn v5_coinbase_transaction_expiry_height() { let verification_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, }) @@ -1922,8 +2130,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, }) @@ -1971,8 +2181,10 @@ async fn v5_transaction_with_exceeding_expiry_height() { let verification_result = Verifier::new_for_tests(&Network::Mainnet, state) .oneshot(Request::Block { - transaction: Arc::new(transaction), + transaction_hash: transaction.hash(), + transaction: Arc::new(transaction.clone()), known_utxos: Arc::new(known_utxos), + known_outpoint_hashes: Arc::new(HashSet::new()), height: height_max, time: DateTime::::MAX_UTC, }) @@ -2025,8 +2237,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, }) @@ -2082,8 +2296,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, }) @@ -2130,8 +2346,10 @@ async fn v5_transaction_with_conflicting_transparent_spend_is_rejected() { let verification_result = Verifier::new_for_tests(&network, state) .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, time: DateTime::::MAX_UTC, }) @@ -2173,8 +2391,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, }) @@ -2262,8 +2482,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, }) @@ -2308,8 +2530,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, }) @@ -2350,8 +2574,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, }) @@ -2394,8 +2620,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, }) @@ -2442,8 +2670,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, }) @@ -2485,8 +2715,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, }) @@ -2547,8 +2779,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, }) @@ -2605,8 +2839,10 @@ async fn v5_consensus_branch_ids() { let block_req = verifier .clone() .oneshot(Request::Block { + transaction_hash: tx.hash(), transaction: Arc::new(tx.clone()), known_utxos: known_utxos.clone(), + known_outpoint_hashes: Arc::new(HashSet::new()), // The consensus branch ID of the tx is outdated for this height. height, time: DateTime::::MAX_UTC, @@ -2633,8 +2869,10 @@ async fn v5_consensus_branch_ids() { let block_req = verifier .clone() .oneshot(Request::Block { + transaction_hash: tx.hash(), transaction: Arc::new(tx.clone()), known_utxos: known_utxos.clone(), + known_outpoint_hashes: Arc::new(HashSet::new()), // The consensus branch ID of the tx is supported by this height. height, time: DateTime::::MAX_UTC, @@ -2690,8 +2928,10 @@ async fn v5_consensus_branch_ids() { let block_req = verifier .clone() .oneshot(Request::Block { + transaction_hash: tx.hash(), transaction: Arc::new(tx.clone()), known_utxos: known_utxos.clone(), + known_outpoint_hashes: Arc::new(HashSet::new()), // The consensus branch ID of the tx is not supported by this height. 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 ad3e28c7eec..6c035e6dc44 100644 --- a/zebra-node-services/src/mempool.rs +++ b/zebra-node-services/src/mempool.rs @@ -6,13 +6,10 @@ use std::collections::HashSet; use tokio::sync::oneshot; use zebra_chain::{ - transaction::{self, UnminedTx, UnminedTxId}, + transaction::{self, UnminedTx, UnminedTxId, VerifiedUnminedTx}, transparent, }; -#[cfg(feature = "getblocktemplate-rpcs")] -use zebra_chain::transaction::VerifiedUnminedTx; - use crate::BoxError; mod gossip; @@ -58,6 +55,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)`, @@ -121,6 +121,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/zebra-rpc/src/methods/tests/snapshot/get_block_template_rpcs.rs b/zebra-rpc/src/methods/tests/snapshot/get_block_template_rpcs.rs index b2e012c7bcd..4949b419c43 100644 --- a/zebra-rpc/src/methods/tests/snapshot/get_block_template_rpcs.rs +++ b/zebra-rpc/src/methods/tests/snapshot/get_block_template_rpcs.rs @@ -105,7 +105,8 @@ pub async fn test_responses( extra_coinbase_data: None, debug_like_zcashd: true, // TODO: Use default field values when optional features are enabled in tests #8183 - ..Default::default() + #[cfg(feature = "internal-miner")] + internal_miner: true, }; // nu5 block height diff --git a/zebra-rpc/src/methods/tests/vectors.rs b/zebra-rpc/src/methods/tests/vectors.rs index 1ecde97c634..01ddb4c3d31 100644 --- a/zebra-rpc/src/methods/tests/vectors.rs +++ b/zebra-rpc/src/methods/tests/vectors.rs @@ -1560,7 +1560,8 @@ async fn rpc_getblocktemplate_mining_address(use_p2pkh: bool) { extra_coinbase_data: None, debug_like_zcashd: true, // TODO: Use default field values when optional features are enabled in tests #8183 - ..Default::default() + #[cfg(feature = "internal-miner")] + internal_miner: true, }; // nu5 block height @@ -2014,7 +2015,8 @@ async fn rpc_getdifficulty() { extra_coinbase_data: None, debug_like_zcashd: true, // TODO: Use default field values when optional features are enabled in tests #8183 - ..Default::default() + #[cfg(feature = "internal-miner")] + internal_miner: true, }; // nu5 block height diff --git a/zebrad/src/components/mempool.rs b/zebrad/src/components/mempool.rs index 6986f601e9c..0d76b778d87 100644 --- a/zebrad/src/components/mempool.rs +++ b/zebrad/src/components/mempool.rs @@ -737,6 +737,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"); @@ -832,7 +850,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 be7cbc9593f..cee0845ba2b 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. ///