diff --git a/zebra-consensus/src/transaction/tests.rs b/zebra-consensus/src/transaction/tests.rs index 41a5e816e41..7e6f8fe331e 100644 --- a/zebra-consensus/src/transaction/tests.rs +++ b/zebra-consensus/src/transaction/tests.rs @@ -28,6 +28,7 @@ use zebra_chain::{ transparent::{self, CoinbaseData}, }; +use zebra_state::ValidateContextError; use zebra_test::mock_service::MockService; use crate::error::TransactionError; @@ -592,7 +593,7 @@ async fn mempool_request_with_past_lock_time_is_accepted() { /// Tests that calls to the transaction verifier with a mempool request that spends /// immature coinbase outputs will return an error. #[tokio::test] -async fn mempool_request_with_immature_spent_is_rejected() { +async fn mempool_request_with_immature_spend_is_rejected() { let _init_guard = zebra_test::init(); let mut state: MockService<_, _, _, _> = MockService::build().for_prop_tests(); @@ -692,6 +693,106 @@ async fn mempool_request_with_immature_spent_is_rejected() { ); } +/// Tests that errors from the read state service are correctly converted into +/// transaction verifier errors. +#[tokio::test] +async fn state_error_converted_correctly() { + use zebra_state::DuplicateNullifierError; + + let mut state: MockService<_, _, _, _> = MockService::build().for_prop_tests(); + let verifier = Verifier::new(Network::Mainnet, state.clone()); + + let height = NetworkUpgrade::Canopy + .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::V4 { + inputs: vec![input], + outputs: vec![output], + lock_time: LockTime::unlocked(), + expiry_height: height, + joinsplit_data: None, + sapling_shielded_data: None, + }; + + let input_outpoint = match tx.inputs()[0] { + transparent::Input::PrevOut { outpoint, .. } => outpoint, + transparent::Input::Coinbase { .. } => panic!("requires a non-coinbase transaction"), + }; + + let make_validate_context_error = + || sprout::Nullifier([0; 32].into()).duplicate_nullifier_error(true); + + tokio::spawn(async move { + 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( + known_utxos + .get(&input_outpoint) + .map(|utxo| utxo.utxo.clone()), + )); + + state + .expect_request_that(|req| { + matches!( + req, + zebra_state::Request::CheckBestChainTipNullifiersAndAnchors(_) + ) + }) + .await + .expect("verifier should call mock state service with correct request") + .respond(Err::( + make_validate_context_error().into(), + )); + }); + + let verifier_response = verifier + .oneshot(Request::Mempool { + transaction: tx.into(), + height, + }) + .await; + + let transaction_error = + verifier_response.expect_err("expected failed verification, got: {verifier_response:?}"); + + assert_eq!( + TransactionError::from(make_validate_context_error()), + transaction_error, + "expected matching state and transaction errors" + ); + + let state_error = zebra_state::BoxError::from(make_validate_context_error()) + .downcast::() + .map(|boxed| TransactionError::from(*boxed)) + .expect("downcast should succeed"); + + assert_eq!( + state_error, transaction_error, + "expected matching state and transaction errors" + ); + + let TransactionError::ValidateContextError(propagated_validate_context_error) = transaction_error else { + panic!("should be a ValidateContextError variant"); + }; + + assert_eq!( + *propagated_validate_context_error, + make_validate_context_error(), + "expected matching state and transaction errors" + ); +} + #[test] fn v5_transaction_with_no_outputs_fails_validation() { let transaction = fake_v5_transactions_for_network( diff --git a/zebra-state/src/error.rs b/zebra-state/src/error.rs index 39039e5a08c..f75f0386810 100644 --- a/zebra-state/src/error.rs +++ b/zebra-state/src/error.rs @@ -267,7 +267,7 @@ pub enum ValidateContextError { } /// Trait for creating the corresponding duplicate nullifier error from a nullifier. -pub(crate) trait DuplicateNullifierError { +pub trait DuplicateNullifierError { /// Returns the corresponding duplicate nullifier error for `self`. fn duplicate_nullifier_error(&self, in_finalized_state: bool) -> ValidateContextError; } diff --git a/zebra-state/src/lib.rs b/zebra-state/src/lib.rs index 53ec2dc3e95..a3509e57ce3 100644 --- a/zebra-state/src/lib.rs +++ b/zebra-state/src/lib.rs @@ -31,7 +31,9 @@ mod tests; pub use config::{check_and_delete_old_databases, Config}; pub use constants::MAX_BLOCK_REORG_HEIGHT; -pub use error::{BoxError, CloneError, CommitBlockError, ValidateContextError}; +pub use error::{ + BoxError, CloneError, CommitBlockError, DuplicateNullifierError, ValidateContextError, +}; pub use request::{FinalizedBlock, HashOrHeight, PreparedBlock, ReadRequest, Request}; pub use response::{KnownBlock, MinedTx, ReadResponse, Response}; pub use service::{