Skip to content

Commit

Permalink
change(mempool): Contextually validates mempool transactions in best …
Browse files Browse the repository at this point in the history
…chain (#5716)

* updates comments

* adds check nullifier no dup fns for transactions

* Adds:
- check::anchors fn for tx iter
- TODO comments for unifying nullifiers and anchors checks
- new state request

Updates unknown anchor errors to accomodate tx-only check

Calls new state fn from transaction verifier

* updates check::anchors fns to use transactions

updates TransactionContextualValidity request to check sprout anchors

adds comment mentioning TransactionContextualValidity ignores UTXOs

* conditions new state req call on is_mempool

updates tests

* fix doc link / lint error

* checks for duplicate nullifiers with closures

* Update zebra-state/src/service/check/nullifier.rs

Co-authored-by: teor <[email protected]>

* documents find_duplicate_nullifier params

moves if let statement into for loop

* renames new state req/res

* asserts correct response variant in tx verifier

* adds CheckBestChainTipShieldedSpends call in tx verifier to async checks

* re-adds tracing instrumentation to check::anchors fn

renames transaction_in_state to transaction_in_chain

* adds block/tx wrapper fns for anchors checks

* uses UnminedTx instead of transaction.hash()

deletes broken test

* updates new state req/res name

* updates tests and uses par_iter for anchors checks

* Updates check::anchors pub fn docs.

* Adds:
- comments / docs
- a TransactionError variant for ValidateContextError

* Apply suggestions from code review

Co-authored-by: teor <[email protected]>

* moves downcast to From impl

rustfmt

* moves the ValidateContextError into an Arc

updates comments and naming

* leaves par_iter for another PR

* puts io::Error in an Arc

* updates anchors tests to call tx_anchors check

* updates tests to call tx_no_duplicates_in_chain

slightly improves formatting

* Update zebra-consensus/src/error.rs

Co-authored-by: teor <[email protected]>

* moves Arc from HistoryError to ValidateContextError

Co-authored-by: teor <[email protected]>
  • Loading branch information
arya2 and teor2345 authored Nov 30, 2022
1 parent 0ec502b commit eb0a2ef
Show file tree
Hide file tree
Showing 16 changed files with 795 additions and 336 deletions.
2 changes: 1 addition & 1 deletion zebra-chain/src/block/commitment.rs
Original file line number Diff line number Diff line change
Expand Up @@ -332,7 +332,7 @@ impl FromHex for ChainHistoryBlockTxAuthCommitmentHash {
/// implement, and ensures that we don't reject blocks or transactions
/// for a non-enumerated reason.
#[allow(missing_docs)]
#[derive(Error, Debug, PartialEq, Eq)]
#[derive(Error, Clone, Debug, PartialEq, Eq)]
pub enum CommitmentError {
#[error(
"invalid final sapling root: expected {:?}, actual: {:?}",
Expand Down
10 changes: 10 additions & 0 deletions zebra-consensus/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ use chrono::{DateTime, Utc};
use thiserror::Error;

use zebra_chain::{amount, block, orchard, sapling, sprout, transparent};
use zebra_state::ValidateContextError;

use crate::{block::MAX_BLOCK_SIGOPS, BoxError};

Expand Down Expand Up @@ -180,6 +181,10 @@ pub enum TransactionError {

#[error("could not find a mempool transaction input UTXO in the best chain")]
TransparentInputNotFound,

#[error("could not validate nullifiers and anchors on best chain")]
#[cfg_attr(any(test, feature = "proptest-impl"), proptest(skip))]
ValidateNullifiersAndAnchorsError(#[from] ValidateContextError),
}

impl From<BoxError> for TransactionError {
Expand All @@ -190,6 +195,11 @@ impl From<BoxError> for TransactionError {
Err(e) => err = e,
}

match err.downcast::<ValidateContextError>() {
Ok(e) => return (*e).into(),
Err(e) => err = e,
}

// buffered transaction verifier service error
match err.downcast::<TransactionError>() {
Ok(e) => return *e,
Expand Down
26 changes: 21 additions & 5 deletions zebra-consensus/src/transaction.rs
Original file line number Diff line number Diff line change
Expand Up @@ -175,10 +175,10 @@ impl Request {
}

/// The unverified mempool transaction, if this is a mempool request.
pub fn into_mempool_transaction(self) -> Option<UnminedTx> {
pub fn mempool_transaction(&self) -> Option<UnminedTx> {
match self {
Request::Block { .. } => None,
Request::Mempool { transaction, .. } => Some(transaction),
Request::Mempool { transaction, .. } => Some(transaction.clone()),
}
}

Expand Down Expand Up @@ -357,15 +357,16 @@ where

// Load spent UTXOs from state.
// TODO: Make this a method of `Request` and replace `tx.clone()` with `self.transaction()`?
let (spent_utxos, spent_outputs) =
Self::spent_utxos(tx.clone(), req.known_utxos(), req.is_mempool(), state).await?;
let load_spent_utxos_fut =
Self::spent_utxos(tx.clone(), req.known_utxos(), req.is_mempool(), state.clone());
let (spent_utxos, spent_outputs) = load_spent_utxos_fut.await?;

let cached_ffi_transaction =
Arc::new(CachedFfiTransaction::new(tx.clone(), spent_outputs));

tracing::trace!(?tx_id, "got state UTXOs");

let async_checks = match tx.as_ref() {
let mut async_checks = match tx.as_ref() {
Transaction::V1 { .. } | Transaction::V2 { .. } | Transaction::V3 { .. } => {
tracing::debug!(?tx, "got transaction with wrong version");
return Err(TransactionError::WrongVersion);
Expand Down Expand Up @@ -396,6 +397,21 @@ where
)?,
};

if let Some(unmined_tx) = req.mempool_transaction() {
let check_anchors_and_revealed_nullifiers_query = state
.clone()
.oneshot(zs::Request::CheckBestChainTipNullifiersAndAnchors(
unmined_tx,
))
.map(|res| {
assert!(res? == zs::Response::ValidBestChainTipNullifiersAndAnchors, "unexpected response to CheckBestChainTipNullifiersAndAnchors request");
Ok(())
}
);

async_checks.push(check_anchors_and_revealed_nullifiers_query);
}

tracing::trace!(?tx_id, "awaiting async checks...");

// If the Groth16 parameter download hangs,
Expand Down
28 changes: 25 additions & 3 deletions zebra-consensus/src/transaction/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -189,17 +189,28 @@ async fn mempool_request_with_missing_input_is_rejected() {
.find(|(_, tx)| !(tx.is_coinbase() || tx.inputs().is_empty()))
.expect("At least one non-coinbase transaction with transparent inputs in test vectors");

let expected_state_request = zebra_state::Request::UnspentBestChainUtxo(match tx.inputs()[0] {
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(expected_state_request)
.expect_request(zebra_state::Request::UnspentBestChainUtxo(input_outpoint))
.await
.expect("verifier should call mock state service")
.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")
.respond(zebra_state::Response::ValidBestChainTipNullifiersAndAnchors);
});

let verifier_response = verifier
Expand Down Expand Up @@ -251,6 +262,17 @@ async fn mempool_request_with_present_input_is_accepted() {
.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")
.respond(zebra_state::Response::ValidBestChainTipNullifiersAndAnchors);
});

let verifier_response = verifier
Expand Down
16 changes: 8 additions & 8 deletions zebra-state/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ pub type BoxError = Box<dyn std::error::Error + Send + Sync + 'static>;
pub struct CommitBlockError(#[from] ValidateContextError);

/// An error describing why a block failed contextual validation.
#[derive(Debug, Error, PartialEq, Eq)]
#[derive(Debug, Error, Clone, PartialEq, Eq)]
#[non_exhaustive]
#[allow(missing_docs)]
pub enum ValidateContextError {
Expand Down Expand Up @@ -224,7 +224,7 @@ pub enum ValidateContextError {
NoteCommitmentTreeError(#[from] zebra_chain::parallel::tree::NoteCommitmentTreeError),

#[error("error building the history tree")]
HistoryTreeError(#[from] HistoryTreeError),
HistoryTreeError(#[from] Arc<HistoryTreeError>),

#[error("block contains an invalid commitment")]
InvalidBlockCommitment(#[from] block::CommitmentError),
Expand All @@ -236,8 +236,8 @@ pub enum ValidateContextError {
#[non_exhaustive]
UnknownSproutAnchor {
anchor: sprout::tree::Root,
height: block::Height,
tx_index_in_block: usize,
height: Option<block::Height>,
tx_index_in_block: Option<usize>,
transaction_hash: transaction::Hash,
},

Expand All @@ -248,8 +248,8 @@ pub enum ValidateContextError {
#[non_exhaustive]
UnknownSaplingAnchor {
anchor: sapling::tree::Root,
height: block::Height,
tx_index_in_block: usize,
height: Option<block::Height>,
tx_index_in_block: Option<usize>,
transaction_hash: transaction::Hash,
},

Expand All @@ -260,8 +260,8 @@ pub enum ValidateContextError {
#[non_exhaustive]
UnknownOrchardAnchor {
anchor: orchard::tree::Root,
height: block::Height,
tx_index_in_block: usize,
height: Option<block::Height>,
tx_index_in_block: Option<usize>,
transaction_hash: transaction::Hash,
},
}
Expand Down
23 changes: 22 additions & 1 deletion zebra-state/src/request.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@ use zebra_chain::{
parallel::tree::NoteCommitmentTrees,
sapling,
serialization::SerializationError,
sprout, transaction,
sprout,
transaction::{self, UnminedTx},
transparent::{self, utxos_from_ordered_utxos},
value_balance::{ValueBalance, ValueBalanceError},
};
Expand Down Expand Up @@ -539,6 +540,11 @@ pub enum Request {
/// Optionally, the hash of the last header to request.
stop: Option<block::Hash>,
},

/// Contextually validates anchors and nullifiers of a transaction on the best chain
///
/// Returns [`Response::ValidBestChainTipNullifiersAndAnchors`]
CheckBestChainTipNullifiersAndAnchors(UnminedTx),
}

impl Request {
Expand All @@ -555,6 +561,9 @@ impl Request {
Request::Block(_) => "block",
Request::FindBlockHashes { .. } => "find_block_hashes",
Request::FindBlockHeaders { .. } => "find_block_headers",
Request::CheckBestChainTipNullifiersAndAnchors(_) => {
"best_chain_tip_nullifiers_anchors"
}
}
}

Expand Down Expand Up @@ -736,6 +745,11 @@ pub enum ReadRequest {
/// Returns a type with found utxos and transaction information.
UtxosByAddresses(HashSet<transparent::Address>),

/// Contextually validates anchors and nullifiers of a transaction on the best chain
///
/// Returns [`ReadResponse::ValidBestChainTipNullifiersAndAnchors`].
CheckBestChainTipNullifiersAndAnchors(UnminedTx),

#[cfg(feature = "getblocktemplate-rpcs")]
/// Looks up a block hash by height in the current best chain.
///
Expand Down Expand Up @@ -772,6 +786,9 @@ impl ReadRequest {
ReadRequest::AddressBalance { .. } => "address_balance",
ReadRequest::TransactionIdsByAddresses { .. } => "transaction_ids_by_addesses",
ReadRequest::UtxosByAddresses(_) => "utxos_by_addesses",
ReadRequest::CheckBestChainTipNullifiersAndAnchors(_) => {
"best_chain_tip_nullifiers_anchors"
}
#[cfg(feature = "getblocktemplate-rpcs")]
ReadRequest::BestChainBlockHash(_) => "best_chain_block_hash",
#[cfg(feature = "getblocktemplate-rpcs")]
Expand Down Expand Up @@ -815,6 +832,10 @@ impl TryFrom<Request> for ReadRequest {
Ok(ReadRequest::FindBlockHeaders { known_blocks, stop })
}

Request::CheckBestChainTipNullifiersAndAnchors(tx) => {
Ok(ReadRequest::CheckBestChainTipNullifiersAndAnchors(tx))
}

Request::CommitBlock(_) | Request::CommitFinalizedBlock(_) => {
Err("ReadService does not write blocks")
}
Expand Down
12 changes: 12 additions & 0 deletions zebra-state/src/response.rs
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,11 @@ pub enum Response {

/// The response to a `FindBlockHeaders` request.
BlockHeaders(Vec<block::CountedHeader>),

/// Response to [`Request::CheckBestChainTipNullifiersAndAnchors`].
///
/// Does not check transparent UTXO inputs
ValidBestChainTipNullifiersAndAnchors,
}

#[derive(Clone, Debug, PartialEq, Eq)]
Expand Down Expand Up @@ -114,6 +119,11 @@ pub enum ReadResponse {
/// Response to [`ReadRequest::UtxosByAddresses`] with found utxos and transaction data.
AddressUtxos(AddressUtxos),

/// Response to [`ReadRequest::CheckBestChainTipNullifiersAndAnchors`].
///
/// Does not check transparent UTXO inputs
ValidBestChainTipNullifiersAndAnchors,

#[cfg(feature = "getblocktemplate-rpcs")]
/// Response to [`ReadRequest::BestChainBlockHash`](crate::ReadRequest::BestChainBlockHash) with the
/// specified block hash.
Expand Down Expand Up @@ -171,6 +181,8 @@ impl TryFrom<ReadResponse> for Response {
ReadResponse::BlockHashes(hashes) => Ok(Response::BlockHashes(hashes)),
ReadResponse::BlockHeaders(headers) => Ok(Response::BlockHeaders(headers)),

ReadResponse::ValidBestChainTipNullifiersAndAnchors => Ok(Response::ValidBestChainTipNullifiersAndAnchors),

ReadResponse::TransactionIdsForBlock(_)
| ReadResponse::SaplingTree(_)
| ReadResponse::OrchardTree(_)
Expand Down
37 changes: 35 additions & 2 deletions zebra-state/src/service.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1024,7 +1024,8 @@ impl Service<Request> for StateService {
| Request::UnspentBestChainUtxo(_)
| Request::Block(_)
| Request::FindBlockHashes { .. }
| Request::FindBlockHeaders { .. } => {
| Request::FindBlockHeaders { .. }
| Request::CheckBestChainTipNullifiersAndAnchors(_) => {
// Redirect the request to the concurrent ReadStateService
let read_service = self.read_service.clone();

Expand Down Expand Up @@ -1217,7 +1218,6 @@ impl Service<ReadRequest> for ReadStateService {
.boxed()
}

// Currently unused.
ReadRequest::UnspentBestChainUtxo(outpoint) => {
let timer = CodeTimer::start();

Expand Down Expand Up @@ -1519,6 +1519,39 @@ impl Service<ReadRequest> for ReadStateService {
.boxed()
}

ReadRequest::CheckBestChainTipNullifiersAndAnchors(unmined_tx) => {
let timer = CodeTimer::start();

let state = self.clone();

let span = Span::current();
tokio::task::spawn_blocking(move || {
span.in_scope(move || {
let latest_non_finalized_best_chain =
state.latest_non_finalized_state().best_chain().cloned();

check::nullifier::tx_no_duplicates_in_chain(
&state.db,
latest_non_finalized_best_chain.as_ref(),
&unmined_tx.transaction,
)?;

check::anchors::tx_anchors_refer_to_final_treestates(
&state.db,
latest_non_finalized_best_chain.as_ref(),
&unmined_tx,
)?;

// The work is done in the future.
timer.finish(module_path!(), line!(), "ReadRequest::UnspentBestChainUtxo");

Ok(ReadResponse::ValidBestChainTipNullifiersAndAnchors)
})
})
.map(|join_result| join_result.expect("panic in ReadRequest::UnspentBestChainUtxo"))
.boxed()
}

// Used by get_block_hash RPC.
#[cfg(feature = "getblocktemplate-rpcs")]
ReadRequest::BestChainBlockHash(height) => {
Expand Down
Loading

0 comments on commit eb0a2ef

Please sign in to comment.