change(mempool): Contextually validates mempool transactions in best 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 <teor@riseup.net>

* 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 <teor@riseup.net>

* 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 <teor@riseup.net>

* moves Arc from HistoryError to ValidateContextError

Co-authored-by: teor <teor@riseup.net>
This commit is contained in:
Arya 2022-11-29 23:40:15 -05:00 committed by GitHub
parent 0ec502bb85
commit eb0a2ef581
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 796 additions and 337 deletions

View File

@ -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: {:?}",

View File

@ -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};
@ -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 {
@ -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,

View File

@ -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()),
}
}
@ -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);
@ -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,

View File

@ -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
@ -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

View File

@ -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 {
@ -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),
@ -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,
},
@ -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,
},
@ -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,
},
}

View File

@ -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},
};
@ -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 {
@ -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"
}
}
}
@ -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.
///
@ -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")]
@ -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")
}

View File

@ -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)]
@ -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.
@ -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(_)

View File

@ -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();
@ -1217,7 +1218,6 @@ impl Service<ReadRequest> for ReadStateService {
.boxed()
}
// Currently unused.
ReadRequest::UnspentBestChainUtxo(outpoint) => {
let timer = CodeTimer::start();
@ -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) => {

View File

@ -5,7 +5,8 @@ use std::{collections::HashMap, sync::Arc};
use zebra_chain::{
block::{Block, Height},
sprout, transaction,
sprout,
transaction::{Hash as TransactionHash, Transaction, UnminedTx},
};
use crate::{
@ -13,103 +14,109 @@ use crate::{
PreparedBlock, ValidateContextError,
};
/// Checks the final Sapling and Orchard anchors specified by transactions in this
/// `prepared` block.
/// Checks the final Sapling and Orchard anchors specified by `transaction`
///
/// This method checks for anchors computed from the final treestate of each block in
/// the `parent_chain` or `finalized_state`.
#[tracing::instrument(skip(finalized_state, parent_chain, prepared))]
pub(crate) fn sapling_orchard_anchors_refer_to_final_treestates(
#[tracing::instrument(skip(finalized_state, parent_chain, transaction))]
fn sapling_orchard_anchors_refer_to_final_treestates(
finalized_state: &ZebraDb,
parent_chain: &Chain,
prepared: &PreparedBlock,
parent_chain: Option<&Arc<Chain>>,
transaction: &Arc<Transaction>,
transaction_hash: TransactionHash,
tx_index_in_block: Option<usize>,
height: Option<Height>,
) -> Result<(), ValidateContextError> {
for (tx_index_in_block, transaction) in prepared.block.transactions.iter().enumerate() {
// Sapling Spends
//
// MUST refer to some earlier blocks final Sapling treestate.
//
// # Consensus
//
// > The anchor of each Spend description MUST refer to some earlier
// > blocks final Sapling treestate. The anchor is encoded separately
// > in each Spend description for v4 transactions, or encoded once and
// > shared between all Spend descriptions in a v5 transaction.
//
// <https://zips.z.cash/protocol/protocol.pdf#spendsandoutputs>
//
// This rule is also implemented in
// [`zebra_chain::sapling::shielded_data`].
//
// The "earlier treestate" check is implemented here.
for (anchor_index_in_tx, anchor) in transaction.sapling_anchors().enumerate() {
tracing::debug!(
?anchor,
?anchor_index_in_tx,
?tx_index_in_block,
height = ?prepared.height,
"observed sapling anchor",
);
// Sapling Spends
//
// MUST refer to some earlier blocks final Sapling treestate.
//
// # Consensus
//
// > The anchor of each Spend description MUST refer to some earlier
// > blocks final Sapling treestate. The anchor is encoded separately
// > in each Spend description for v4 transactions, or encoded once and
// > shared between all Spend descriptions in a v5 transaction.
//
// <https://zips.z.cash/protocol/protocol.pdf#spendsandoutputs>
//
// This rule is also implemented in
// [`zebra_chain::sapling::shielded_data`].
//
// The "earlier treestate" check is implemented here.
for (anchor_index_in_tx, anchor) in transaction.sapling_anchors().enumerate() {
tracing::debug!(
?anchor,
?anchor_index_in_tx,
?tx_index_in_block,
?height,
"observed sapling anchor",
);
if !parent_chain.sapling_anchors.contains(&anchor)
&& !finalized_state.contains_sapling_anchor(&anchor)
{
return Err(ValidateContextError::UnknownSaplingAnchor {
anchor,
height: prepared.height,
tx_index_in_block,
transaction_hash: prepared.transaction_hashes[tx_index_in_block],
});
}
tracing::debug!(
?anchor,
?anchor_index_in_tx,
?tx_index_in_block,
height = ?prepared.height,
"validated sapling anchor",
);
if !parent_chain
.map(|chain| chain.sapling_anchors.contains(&anchor))
.unwrap_or(false)
&& !finalized_state.contains_sapling_anchor(&anchor)
{
return Err(ValidateContextError::UnknownSaplingAnchor {
anchor,
height,
tx_index_in_block,
transaction_hash,
});
}
// Orchard Actions
//
// MUST refer to some earlier blocks final Orchard treestate.
//
// # Consensus
//
// > The anchorOrchard field of the transaction, whenever it exists
// > (i.e. when there are any Action descriptions), MUST refer to some
// > earlier blocks final Orchard treestate.
//
// <https://zips.z.cash/protocol/protocol.pdf#actions>
if let Some(orchard_shielded_data) = transaction.orchard_shielded_data() {
tracing::debug!(
?orchard_shielded_data.shared_anchor,
?tx_index_in_block,
height = ?prepared.height,
"observed orchard anchor",
);
tracing::debug!(
?anchor,
?anchor_index_in_tx,
?tx_index_in_block,
?height,
"validated sapling anchor",
);
}
if !parent_chain
.orchard_anchors
.contains(&orchard_shielded_data.shared_anchor)
&& !finalized_state.contains_orchard_anchor(&orchard_shielded_data.shared_anchor)
{
return Err(ValidateContextError::UnknownOrchardAnchor {
anchor: orchard_shielded_data.shared_anchor,
height: prepared.height,
tx_index_in_block,
transaction_hash: prepared.transaction_hashes[tx_index_in_block],
});
}
// Orchard Actions
//
// MUST refer to some earlier blocks final Orchard treestate.
//
// # Consensus
//
// > The anchorOrchard field of the transaction, whenever it exists
// > (i.e. when there are any Action descriptions), MUST refer to some
// > earlier blocks final Orchard treestate.
//
// <https://zips.z.cash/protocol/protocol.pdf#actions>
if let Some(orchard_shielded_data) = transaction.orchard_shielded_data() {
tracing::debug!(
?orchard_shielded_data.shared_anchor,
?tx_index_in_block,
?height,
"observed orchard anchor",
);
tracing::debug!(
?orchard_shielded_data.shared_anchor,
?tx_index_in_block,
height = ?prepared.height,
"validated orchard anchor",
);
if !parent_chain
.map(|chain| {
chain
.orchard_anchors
.contains(&orchard_shielded_data.shared_anchor)
})
.unwrap_or(false)
&& !finalized_state.contains_orchard_anchor(&orchard_shielded_data.shared_anchor)
{
return Err(ValidateContextError::UnknownOrchardAnchor {
anchor: orchard_shielded_data.shared_anchor,
height,
tx_index_in_block,
transaction_hash,
});
}
tracing::debug!(
?orchard_shielded_data.shared_anchor,
?tx_index_in_block,
?height,
"validated orchard anchor",
);
}
Ok(())
@ -122,74 +129,259 @@ pub(crate) fn sapling_orchard_anchors_refer_to_final_treestates(
/// Sprout anchors may also refer to the interstitial output treestate of any prior
/// `JoinSplit` _within the same transaction_; these are created on the fly
/// in [`sprout_anchors_refer_to_treestates()`].
#[tracing::instrument(skip(finalized_state, parent_chain, prepared))]
pub(crate) fn fetch_sprout_final_treestates(
#[tracing::instrument(skip(sprout_final_treestates, finalized_state, parent_chain, transaction))]
fn fetch_sprout_final_treestates(
sprout_final_treestates: &mut HashMap<
sprout::tree::Root,
Arc<sprout::tree::NoteCommitmentTree>,
>,
finalized_state: &ZebraDb,
parent_chain: &Chain,
prepared: &PreparedBlock,
) -> HashMap<sprout::tree::Root, Arc<sprout::tree::NoteCommitmentTree>> {
let mut sprout_final_treestates = HashMap::new();
parent_chain: Option<&Arc<Chain>>,
transaction: &Arc<Transaction>,
tx_index_in_block: Option<usize>,
height: Option<Height>,
) {
// Fetch and return Sprout JoinSplit final treestates
for (joinsplit_index_in_tx, joinsplit) in transaction.sprout_groth16_joinsplits().enumerate() {
// Avoid duplicate fetches
if sprout_final_treestates.contains_key(&joinsplit.anchor) {
continue;
}
for (tx_index_in_block, transaction) in prepared.block.transactions.iter().enumerate() {
// Fetch and return Sprout JoinSplit final treestates
for (joinsplit_index_in_tx, joinsplit) in
transaction.sprout_groth16_joinsplits().enumerate()
{
// Avoid duplicate fetches
if sprout_final_treestates.contains_key(&joinsplit.anchor) {
continue;
}
let input_tree = parent_chain
.and_then(|chain| chain.sprout_trees_by_anchor.get(&joinsplit.anchor).cloned())
.or_else(|| finalized_state.sprout_note_commitment_tree_by_anchor(&joinsplit.anchor));
let input_tree = parent_chain
.sprout_trees_by_anchor
.get(&joinsplit.anchor)
.cloned()
.or_else(|| {
finalized_state.sprout_note_commitment_tree_by_anchor(&joinsplit.anchor)
});
if let Some(input_tree) = input_tree {
/* TODO:
- fix tests that generate incorrect root data
- assert that roots match the fetched tree during tests
- move this CPU-intensive check to sprout_anchors_refer_to_treestates()
if let Some(input_tree) = input_tree {
/* TODO:
- fix tests that generate incorrect root data
- assert that roots match the fetched tree during tests
- move this CPU-intensive check to sprout_anchors_refer_to_treestates()
assert_eq!(
input_tree.root(),
joinsplit.anchor,
"anchor and fetched input tree root did not match:\n\
anchor: {anchor:?},\n\
input tree root: {input_tree_root:?},\n\
input_tree: {input_tree:?}",
anchor = joinsplit.anchor
);
*/
assert_eq!(
input_tree.root(),
joinsplit.anchor,
"anchor and fetched input tree root did not match:\n\
anchor: {anchor:?},\n\
input tree root: {input_tree_root:?},\n\
input_tree: {input_tree:?}",
anchor = joinsplit.anchor
);
*/
sprout_final_treestates.insert(joinsplit.anchor, input_tree);
sprout_final_treestates.insert(joinsplit.anchor, input_tree);
tracing::debug!(
sprout_final_treestate_count = ?sprout_final_treestates.len(),
?joinsplit.anchor,
?joinsplit_index_in_tx,
?tx_index_in_block,
height = ?prepared.height,
"observed sprout final treestate anchor",
);
}
tracing::debug!(
sprout_final_treestate_count = ?sprout_final_treestates.len(),
?joinsplit.anchor,
?joinsplit_index_in_tx,
?tx_index_in_block,
?height,
"observed sprout final treestate anchor",
);
}
}
tracing::trace!(
sprout_final_treestate_count = ?sprout_final_treestates.len(),
?sprout_final_treestates,
height = ?prepared.height,
?height,
"returning sprout final treestate anchors",
);
}
/// Checks the Sprout anchors specified by `transactions`.
///
/// Sprout anchors may refer to some earlier block's final treestate (like
/// Sapling and Orchard do exclusively) _or_ to the interstitial output
/// treestate of any prior `JoinSplit` _within the same transaction_.
///
/// This method searches for anchors in the supplied `sprout_final_treestates`
/// (which must be populated with all treestates pointed to in the `prepared` block;
/// see [`fetch_sprout_final_treestates()`]); or in the interstitial
/// treestates which are computed on the fly in this function.
#[tracing::instrument(skip(sprout_final_treestates, transaction))]
fn sprout_anchors_refer_to_treestates(
sprout_final_treestates: &HashMap<sprout::tree::Root, Arc<sprout::tree::NoteCommitmentTree>>,
transaction: &Arc<Transaction>,
transaction_hash: TransactionHash,
tx_index_in_block: Option<usize>,
height: Option<Height>,
) -> Result<(), ValidateContextError> {
// Sprout JoinSplits, with interstitial treestates to check as well.
let mut interstitial_trees: HashMap<sprout::tree::Root, Arc<sprout::tree::NoteCommitmentTree>> =
HashMap::new();
let joinsplit_count = transaction.sprout_groth16_joinsplits().count();
for (joinsplit_index_in_tx, joinsplit) in transaction.sprout_groth16_joinsplits().enumerate() {
// Check all anchor sets, including the one for interstitial
// anchors.
//
// The anchor is checked and the matching tree is obtained,
// which is used to create the interstitial tree state for this
// JoinSplit:
//
// > For each JoinSplit description in a transaction, an
// > interstitial output treestate is constructed which adds the
// > note commitments and nullifiers specified in that JoinSplit
// > description to the input treestate referred to by its
// > anchor. This interstitial output treestate is available for
// > use as the anchor of subsequent JoinSplit descriptions in
// > the same transaction.
//
// <https://zips.z.cash/protocol/protocol.pdf#joinsplit>
//
// # Consensus
//
// > The anchor of each JoinSplit description in a transaction
// > MUST refer to either some earlier blocks final Sprout
// > treestate, or to the interstitial output treestate of any
// > prior JoinSplit description in the same transaction.
//
// > For the first JoinSplit description of a transaction, the
// > anchor MUST be the output Sprout treestate of a previous
// > block.
//
// <https://zips.z.cash/protocol/protocol.pdf#joinsplit>
//
// Note that in order to satisfy the latter consensus rule above,
// [`interstitial_trees`] is always empty in the first iteration
// of the loop.
let input_tree = interstitial_trees
.get(&joinsplit.anchor)
.cloned()
.or_else(|| sprout_final_treestates.get(&joinsplit.anchor).cloned());
tracing::trace!(
?input_tree,
final_lookup = ?sprout_final_treestates.get(&joinsplit.anchor),
interstitial_lookup = ?interstitial_trees.get(&joinsplit.anchor),
interstitial_tree_count = ?interstitial_trees.len(),
?interstitial_trees,
?height,
"looked up sprout treestate anchor",
);
let mut input_tree = match input_tree {
Some(tree) => tree,
None => {
tracing::debug!(
?joinsplit.anchor,
?joinsplit_index_in_tx,
?tx_index_in_block,
?height,
"failed to find sprout anchor",
);
return Err(ValidateContextError::UnknownSproutAnchor {
anchor: joinsplit.anchor,
height,
tx_index_in_block,
transaction_hash,
});
}
};
tracing::debug!(
?joinsplit.anchor,
?joinsplit_index_in_tx,
?tx_index_in_block,
?height,
"validated sprout anchor",
);
// The last interstitial treestate in a transaction can never be used,
// so we avoid generating it.
if joinsplit_index_in_tx == joinsplit_count - 1 {
continue;
}
let input_tree_inner = Arc::make_mut(&mut input_tree);
// Add new anchors to the interstitial note commitment tree.
for cm in joinsplit.commitments {
input_tree_inner
.append(cm)
.expect("note commitment should be appendable to the tree");
}
interstitial_trees.insert(input_tree.root(), input_tree);
tracing::debug!(
?joinsplit.anchor,
?joinsplit_index_in_tx,
?tx_index_in_block,
?height,
"observed sprout interstitial anchor",
);
}
Ok(())
}
/// Accepts a [`ZebraDb`], [`Chain`], and [`PreparedBlock`].
///
/// Iterates over the transactions in the [`PreparedBlock`] checking the final Sapling and Orchard anchors.
///
/// This method checks for anchors computed from the final treestate of each block in
/// the `parent_chain` or `finalized_state`.
#[tracing::instrument(skip_all)]
pub(crate) fn block_sapling_orchard_anchors_refer_to_final_treestates(
finalized_state: &ZebraDb,
parent_chain: &Arc<Chain>,
prepared: &PreparedBlock,
) -> Result<(), ValidateContextError> {
prepared.block.transactions.iter().enumerate().try_for_each(
|(tx_index_in_block, transaction)| {
sapling_orchard_anchors_refer_to_final_treestates(
finalized_state,
Some(parent_chain),
transaction,
prepared.transaction_hashes[tx_index_in_block],
Some(tx_index_in_block),
Some(prepared.height),
)
},
)
}
/// Accepts a [`ZebraDb`], [`Arc<Chain>`](Chain), and [`PreparedBlock`].
///
/// Iterates over the transactions in the [`PreparedBlock`], and fetches the Sprout final treestates
/// from the state.
///
/// Returns a `HashMap` of the Sprout final treestates from the state for [`sprout_anchors_refer_to_treestates()`]
/// to check Sprout final and interstitial treestates without accessing the disk.
///
/// Sprout anchors may also refer to the interstitial output treestate of any prior
/// `JoinSplit` _within the same transaction_; these are created on the fly
/// in [`sprout_anchors_refer_to_treestates()`].
#[tracing::instrument(skip_all)]
pub(crate) fn block_fetch_sprout_final_treestates(
finalized_state: &ZebraDb,
parent_chain: &Arc<Chain>,
prepared: &PreparedBlock,
) -> HashMap<sprout::tree::Root, Arc<sprout::tree::NoteCommitmentTree>> {
let mut sprout_final_treestates = HashMap::new();
for (tx_index_in_block, transaction) in prepared.block.transactions.iter().enumerate() {
fetch_sprout_final_treestates(
&mut sprout_final_treestates,
finalized_state,
Some(parent_chain),
transaction,
Some(tx_index_in_block),
Some(prepared.height),
);
}
sprout_final_treestates
}
/// Checks the Sprout anchors specified by transactions in `block`.
/// Accepts a [`ZebraDb`], [`Arc<Chain>`](Chain), [`Arc<Block>`](Block), and an
/// [`Arc<[transaction::Hash]>`](TransactionHash) of hashes corresponding to the transactions in [`Block`]
///
/// Iterates over the transactions in the [`Block`] checking the final Sprout anchors.
///
/// Sprout anchors may refer to some earlier block's final treestate (like
/// Sapling and Orchard do exclusively) _or_ to the interstitial output
@ -200,12 +392,12 @@ pub(crate) fn fetch_sprout_final_treestates(
/// see [`fetch_sprout_final_treestates()`]); or in the interstitial
/// treestates which are computed on the fly in this function.
#[tracing::instrument(skip(sprout_final_treestates, block, transaction_hashes))]
pub(crate) fn sprout_anchors_refer_to_treestates(
pub(crate) fn block_sprout_anchors_refer_to_treestates(
sprout_final_treestates: HashMap<sprout::tree::Root, Arc<sprout::tree::NoteCommitmentTree>>,
block: Arc<Block>,
// Only used for debugging
transaction_hashes: Arc<[TransactionHash]>,
height: Height,
transaction_hashes: Arc<[transaction::Hash]>,
) -> Result<(), ValidateContextError> {
tracing::trace!(
sprout_final_treestate_count = ?sprout_final_treestates.len(),
@ -214,119 +406,68 @@ pub(crate) fn sprout_anchors_refer_to_treestates(
"received sprout final treestate anchors",
);
for (tx_index_in_block, transaction) in block.transactions.iter().enumerate() {
// Sprout JoinSplits, with interstitial treestates to check as well.
let mut interstitial_trees: HashMap<
sprout::tree::Root,
Arc<sprout::tree::NoteCommitmentTree>,
> = HashMap::new();
block
.transactions
.iter()
.enumerate()
.try_for_each(|(tx_index_in_block, transaction)| {
sprout_anchors_refer_to_treestates(
&sprout_final_treestates,
transaction,
transaction_hashes[tx_index_in_block],
Some(tx_index_in_block),
Some(height),
)?;
let joinsplit_count = transaction.sprout_groth16_joinsplits().count();
Ok(())
})
}
for (joinsplit_index_in_tx, joinsplit) in
transaction.sprout_groth16_joinsplits().enumerate()
{
// Check all anchor sets, including the one for interstitial
// anchors.
//
// The anchor is checked and the matching tree is obtained,
// which is used to create the interstitial tree state for this
// JoinSplit:
//
// > For each JoinSplit description in a transaction, an
// > interstitial output treestate is constructed which adds the
// > note commitments and nullifiers specified in that JoinSplit
// > description to the input treestate referred to by its
// > anchor. This interstitial output treestate is available for
// > use as the anchor of subsequent JoinSplit descriptions in
// > the same transaction.
//
// <https://zips.z.cash/protocol/protocol.pdf#joinsplit>
//
// # Consensus
//
// > The anchor of each JoinSplit description in a transaction
// > MUST refer to either some earlier blocks final Sprout
// > treestate, or to the interstitial output treestate of any
// > prior JoinSplit description in the same transaction.
//
// > For the first JoinSplit description of a transaction, the
// > anchor MUST be the output Sprout treestate of a previous
// > block.
//
// <https://zips.z.cash/protocol/protocol.pdf#joinsplit>
//
// Note that in order to satisfy the latter consensus rule above,
// [`interstitial_trees`] is always empty in the first iteration
// of the loop.
let input_tree = interstitial_trees
.get(&joinsplit.anchor)
.cloned()
.or_else(|| sprout_final_treestates.get(&joinsplit.anchor).cloned());
/// Accepts a [`ZebraDb`], an optional [`Option<Arc<Chain>>`](Chain), and an [`UnminedTx`].
///
/// Checks the final Sprout, Sapling and Orchard anchors specified in the [`UnminedTx`].
///
/// This method checks for anchors computed from the final treestate of each block in
/// the `parent_chain` or `finalized_state`.
#[tracing::instrument(skip_all)]
pub(crate) fn tx_anchors_refer_to_final_treestates(
finalized_state: &ZebraDb,
parent_chain: Option<&Arc<Chain>>,
unmined_tx: &UnminedTx,
) -> Result<(), ValidateContextError> {
sapling_orchard_anchors_refer_to_final_treestates(
finalized_state,
parent_chain,
&unmined_tx.transaction,
unmined_tx.id.mined_id(),
None,
None,
)?;
tracing::trace!(
?input_tree,
final_lookup = ?sprout_final_treestates.get(&joinsplit.anchor),
interstitial_lookup = ?interstitial_trees.get(&joinsplit.anchor),
interstitial_tree_count = ?interstitial_trees.len(),
?interstitial_trees,
?height,
"looked up sprout treestate anchor",
);
let mut sprout_final_treestates = HashMap::new();
let mut input_tree = match input_tree {
Some(tree) => tree,
None => {
tracing::debug!(
?joinsplit.anchor,
?joinsplit_index_in_tx,
?tx_index_in_block,
?height,
"failed to find sprout anchor",
);
return Err(ValidateContextError::UnknownSproutAnchor {
anchor: joinsplit.anchor,
height,
tx_index_in_block,
transaction_hash: transaction_hashes[tx_index_in_block],
});
}
};
fetch_sprout_final_treestates(
&mut sprout_final_treestates,
finalized_state,
parent_chain,
&unmined_tx.transaction,
None,
None,
);
tracing::debug!(
?joinsplit.anchor,
?joinsplit_index_in_tx,
?tx_index_in_block,
?height,
"validated sprout anchor",
);
tracing::trace!(
sprout_final_treestate_count = ?sprout_final_treestates.len(),
?sprout_final_treestates,
"received sprout final treestate anchors",
);
// The last interstitial treestate in a transaction can never be used,
// so we avoid generating it.
if joinsplit_index_in_tx == joinsplit_count - 1 {
continue;
}
let input_tree_inner = Arc::make_mut(&mut input_tree);
// Add new anchors to the interstitial note commitment tree.
for cm in joinsplit.commitments {
input_tree_inner
.append(cm)
.expect("note commitment should be appendable to the tree");
}
interstitial_trees.insert(input_tree.root(), input_tree);
tracing::debug!(
?joinsplit.anchor,
?joinsplit_index_in_tx,
?tx_index_in_block,
?height,
"observed sprout interstitial anchor",
);
}
}
sprout_anchors_refer_to_treestates(
&sprout_final_treestates,
&unmined_tx.transaction,
unmined_tx.id.mined_id(),
None,
None,
)?;
Ok(())
}

View File

@ -1,12 +1,14 @@
//! Checks for nullifier uniqueness.
use std::collections::HashSet;
use std::{collections::HashSet, sync::Arc};
use tracing::trace;
use zebra_chain::transaction::Transaction;
use crate::{
error::DuplicateNullifierError, service::finalized_state::ZebraDb, PreparedBlock,
ValidateContextError,
error::DuplicateNullifierError,
service::{finalized_state::ZebraDb, non_finalized_state::Chain},
PreparedBlock, ValidateContextError,
};
// Tidy up some doc links
@ -54,6 +56,73 @@ pub(crate) fn no_duplicates_in_finalized_chain(
Ok(())
}
/// Accepts an iterator of revealed nullifiers, a predicate fn for checking if a nullifier is in
/// in the finalized chain, and a predicate fn for checking if the nullifier is in the non-finalized chain
///
/// Returns `Err(DuplicateNullifierError)` if any of the `revealed_nullifiers` are found in the
/// non-finalized or finalized chains.
///
/// Returns `Ok(())` if all the `revealed_nullifiers` have not been seen in either chain.
fn find_duplicate_nullifier<'a, NullifierT, FinalizedStateContainsFn, NonFinalizedStateContainsFn>(
revealed_nullifiers: impl IntoIterator<Item = &'a NullifierT>,
finalized_chain_contains: FinalizedStateContainsFn,
non_finalized_chain_contains: Option<NonFinalizedStateContainsFn>,
) -> Result<(), ValidateContextError>
where
NullifierT: DuplicateNullifierError + 'a,
FinalizedStateContainsFn: Fn(&'a NullifierT) -> bool,
NonFinalizedStateContainsFn: Fn(&'a NullifierT) -> bool,
{
for nullifier in revealed_nullifiers {
if let Some(true) = non_finalized_chain_contains.as_ref().map(|f| f(nullifier)) {
Err(nullifier.duplicate_nullifier_error(false))?
} else if finalized_chain_contains(nullifier) {
Err(nullifier.duplicate_nullifier_error(true))?
}
}
Ok(())
}
/// Reject double-spends of nullifiers:
/// - one from this [`Transaction`], and the other already committed to the
/// provided non-finalized [`Chain`] or [`ZebraDb`].
///
/// # Consensus
///
/// > A nullifier MUST NOT repeat either within a transaction,
/// > or across transactions in a valid blockchain.
/// > Sprout and Sapling and Orchard nullifiers are considered disjoint,
/// > even if they have the same bit pattern.
///
/// <https://zips.z.cash/protocol/protocol.pdf#nullifierset>
#[tracing::instrument(skip_all)]
pub(crate) fn tx_no_duplicates_in_chain(
finalized_chain: &ZebraDb,
non_finalized_chain: Option<&Arc<Chain>>,
transaction: &Arc<Transaction>,
) -> Result<(), ValidateContextError> {
find_duplicate_nullifier(
transaction.sprout_nullifiers(),
|nullifier| finalized_chain.contains_sprout_nullifier(nullifier),
non_finalized_chain.map(|chain| |nullifier| chain.sprout_nullifiers.contains(nullifier)),
)?;
find_duplicate_nullifier(
transaction.sapling_nullifiers(),
|nullifier| finalized_chain.contains_sapling_nullifier(nullifier),
non_finalized_chain.map(|chain| |nullifier| chain.sapling_nullifiers.contains(nullifier)),
)?;
find_duplicate_nullifier(
transaction.orchard_nullifiers(),
|nullifier| finalized_chain.contains_orchard_nullifier(nullifier),
non_finalized_chain.map(|chain| |nullifier| chain.orchard_nullifiers.contains(nullifier)),
)?;
Ok(())
}
/// Reject double-spends of nullifers:
/// - both within the same `JoinSplit` (sprout only),
/// - from different `JoinSplit`s, [`sapling::Spend`][2]s or

View File

@ -8,14 +8,17 @@ use zebra_chain::{
primitives::Groth16Proof,
serialization::ZcashDeserializeInto,
sprout::JoinSplit,
transaction::{JoinSplitData, LockTime, Transaction},
transaction::{JoinSplitData, LockTime, Transaction, UnminedTx},
};
use crate::{
arbitrary::Prepare,
service::write::validate_and_commit_non_finalized,
service::{
check::anchors::tx_anchors_refer_to_final_treestates,
write::validate_and_commit_non_finalized,
},
tests::setup::{new_state_with_mainnet_genesis, transaction_v4_from_coinbase},
PreparedBlock,
PreparedBlock, ValidateContextError,
};
// Sprout
@ -41,13 +44,6 @@ fn check_sprout_anchors() {
// Add initial transactions to [`block_1`].
let block_1 = prepare_sprout_block(block_1, block_395);
// Validate and commit [`block_1`]. This will add an anchor referencing the
// empty note commitment tree to the state.
assert!(
validate_and_commit_non_finalized(&finalized_state, &mut non_finalized_state, block_1)
.is_ok()
);
// Bootstrap a block at height == 2 that references the Sprout note commitment tree state
// from [`block_1`].
let block_2 = zebra_test::vectors::BLOCK_MAINNET_2_BYTES
@ -63,6 +59,43 @@ fn check_sprout_anchors() {
// Add the transactions with the first anchors to [`block_2`].
let block_2 = prepare_sprout_block(block_2, block_396);
let unmined_txs: Vec<_> = block_2
.block
.transactions
.iter()
.map(UnminedTx::from)
.collect();
let check_unmined_tx_anchors_result = unmined_txs.iter().try_for_each(|unmined_tx| {
tx_anchors_refer_to_final_treestates(
&finalized_state.db,
non_finalized_state.best_chain(),
unmined_tx,
)
});
assert!(matches!(
check_unmined_tx_anchors_result,
Err(ValidateContextError::UnknownSproutAnchor { .. })
));
// Validate and commit [`block_1`]. This will add an anchor referencing the
// empty note commitment tree to the state.
assert!(
validate_and_commit_non_finalized(&finalized_state, &mut non_finalized_state, block_1)
.is_ok()
);
let check_unmined_tx_anchors_result = unmined_txs.iter().try_for_each(|unmined_tx| {
tx_anchors_refer_to_final_treestates(
&finalized_state.db,
non_finalized_state.best_chain(),
unmined_tx,
)
});
assert!(check_unmined_tx_anchors_result.is_ok());
// Validate and commit [`block_2`]. This will also check the anchors.
assert_eq!(
validate_and_commit_non_finalized(&finalized_state, &mut non_finalized_state, block_2),
@ -188,10 +221,6 @@ fn check_sapling_anchors() {
});
let block1 = Arc::new(block1).prepare();
assert!(
validate_and_commit_non_finalized(&finalized_state, &mut non_finalized_state, block1)
.is_ok()
);
// Bootstrap a block at height == 2 that references the Sapling note commitment tree state
// from earlier block
@ -238,6 +267,42 @@ fn check_sapling_anchors() {
});
let block2 = Arc::new(block2).prepare();
let unmined_txs: Vec<_> = block2
.block
.transactions
.iter()
.map(UnminedTx::from)
.collect();
let check_unmined_tx_anchors_result = unmined_txs.iter().try_for_each(|unmined_tx| {
tx_anchors_refer_to_final_treestates(
&finalized_state.db,
non_finalized_state.best_chain(),
unmined_tx,
)
});
assert!(matches!(
check_unmined_tx_anchors_result,
Err(ValidateContextError::UnknownSaplingAnchor { .. })
));
assert!(
validate_and_commit_non_finalized(&finalized_state, &mut non_finalized_state, block1)
.is_ok()
);
let check_unmined_tx_anchors_result = unmined_txs.iter().try_for_each(|unmined_tx| {
tx_anchors_refer_to_final_treestates(
&finalized_state.db,
non_finalized_state.best_chain(),
unmined_tx,
)
});
assert!(check_unmined_tx_anchors_result.is_ok());
assert_eq!(
validate_and_commit_non_finalized(&finalized_state, &mut non_finalized_state, block2),
Ok(())

View File

@ -19,7 +19,9 @@ use zebra_chain::{
use crate::{
arbitrary::Prepare,
service::{read, write::validate_and_commit_non_finalized},
service::{
check::nullifier::tx_no_duplicates_in_chain, read, write::validate_and_commit_non_finalized,
},
tests::setup::{new_state_with_mainnet_genesis, transaction_v4_from_coinbase},
FinalizedBlock,
ValidateContextError::{
@ -73,7 +75,7 @@ proptest! {
block1.transactions.push(transaction.into());
let (mut finalized_state, mut non_finalized_state, _genesis) = new_state_with_mainnet_genesis();
let (mut finalized_state, mut non_finalized_state, _genesis) = new_state_with_mainnet_genesis();
// Allows anchor checks to pass
finalized_state.populate_with_anchors(&block1);
@ -102,7 +104,7 @@ proptest! {
let commit_result = validate_and_commit_non_finalized(
&finalized_state,
&mut non_finalized_state,
block1.clone()
block1.clone()
);
// the block was committed
@ -155,7 +157,9 @@ proptest! {
let block1 = Arc::new(block1).prepare();
let commit_result = validate_and_commit_non_finalized(
&finalized_state,
&mut non_finalized_state, block1);
&mut non_finalized_state,
block1
);
// if the random proptest data produces other errors,
// we might need to just check `is_err()` here
@ -214,7 +218,9 @@ proptest! {
let block1 = Arc::new(block1).prepare();
let commit_result = validate_and_commit_non_finalized(
&finalized_state,
&mut non_finalized_state, block1);
&mut non_finalized_state,
block1
);
prop_assert_eq!(
commit_result,
@ -318,16 +324,16 @@ proptest! {
let duplicate_nullifier = joinsplit1.nullifiers[0];
joinsplit2.nullifiers[0] = duplicate_nullifier;
let transaction1 = transaction_v4_with_joinsplit_data(joinsplit_data1.0, [joinsplit1.0]);
let transaction1 = Arc::new(transaction_v4_with_joinsplit_data(joinsplit_data1.0, [joinsplit1.0]));
let transaction2 = transaction_v4_with_joinsplit_data(joinsplit_data2.0, [joinsplit2.0]);
block1.transactions[0] = transaction_v4_from_coinbase(&block1.transactions[0]).into();
block2.transactions[0] = transaction_v4_from_coinbase(&block2.transactions[0]).into();
block1.transactions.push(transaction1.into());
block1.transactions.push(transaction1.clone());
block2.transactions.push(transaction2.into());
let (mut finalized_state, mut non_finalized_state, _genesis) = new_state_with_mainnet_genesis();
let (mut finalized_state, mut non_finalized_state, _genesis) = new_state_with_mainnet_genesis();
// Allows anchor checks to pass
finalized_state.populate_with_anchors(&block1);
@ -335,6 +341,11 @@ proptest! {
let mut previous_mem = non_finalized_state.clone();
// makes sure there are no spurious rejections that might hide bugs in `tx_no_duplicates_in_chain`
let check_tx_no_duplicates_in_chain =
tx_no_duplicates_in_chain(&finalized_state.db, non_finalized_state.best_chain(), &transaction1);
prop_assert!(check_tx_no_duplicates_in_chain.is_ok());
let block1_hash;
// randomly choose to commit the next block to the finalized or non-finalized state
if duplicate_in_finalized_state {
@ -353,8 +364,10 @@ proptest! {
} else {
let block1 = Arc::new(block1).prepare();
let commit_result = validate_and_commit_non_finalized(
&finalized_state,
&mut non_finalized_state, block1.clone());
&finalized_state,
&mut non_finalized_state,
block1.clone()
);
prop_assert_eq!(commit_result, Ok(()));
prop_assert_eq!(Some((Height(1), block1.hash)), read::best_tip(&non_finalized_state, &finalized_state.db));
@ -371,7 +384,9 @@ proptest! {
let block2 = Arc::new(block2).prepare();
let commit_result = validate_and_commit_non_finalized(
&finalized_state,
&mut non_finalized_state, block2);
&mut non_finalized_state,
block2
);
prop_assert_eq!(
commit_result,
@ -381,6 +396,18 @@ proptest! {
}
.into())
);
let check_tx_no_duplicates_in_chain =
tx_no_duplicates_in_chain(&finalized_state.db, non_finalized_state.best_chain(), &transaction1);
prop_assert_eq!(
check_tx_no_duplicates_in_chain,
Err(DuplicateSproutNullifier {
nullifier: duplicate_nullifier,
in_finalized_state: duplicate_in_finalized_state,
})
);
prop_assert_eq!(Some((Height(1), block1_hash)), read::best_tip(&non_finalized_state, &finalized_state.db));
prop_assert!(non_finalized_state.eq_internal_state(&previous_mem));
}
@ -413,7 +440,7 @@ proptest! {
block1.transactions.push(transaction.into());
let (mut finalized_state, mut non_finalized_state, _genesis) = new_state_with_mainnet_genesis();
let (mut finalized_state, mut non_finalized_state, _genesis) = new_state_with_mainnet_genesis();
// Allows anchor checks to pass
finalized_state.populate_with_anchors(&block1);
@ -524,7 +551,7 @@ proptest! {
.transactions
.extend([transaction1.into(), transaction2.into()]);
let (finalized_state, mut non_finalized_state, genesis) = new_state_with_mainnet_genesis();
let (finalized_state, mut non_finalized_state, genesis) = new_state_with_mainnet_genesis();
// Allows anchor checks to pass
finalized_state.populate_with_anchors(&block1);
@ -534,7 +561,9 @@ proptest! {
let block1 = Arc::new(block1).prepare();
let commit_result = validate_and_commit_non_finalized(
&finalized_state,
&mut non_finalized_state, block1);
&mut non_finalized_state,
block1
);
prop_assert_eq!(
commit_result,
@ -572,17 +601,17 @@ proptest! {
spend2.nullifier = duplicate_nullifier;
let transaction1 =
transaction_v4_with_sapling_shielded_data(sapling_shielded_data1.0, [spend1.0]);
Arc::new(transaction_v4_with_sapling_shielded_data(sapling_shielded_data1.0, [spend1.0]));
let transaction2 =
transaction_v4_with_sapling_shielded_data(sapling_shielded_data2.0, [spend2.0]);
block1.transactions[0] = transaction_v4_from_coinbase(&block1.transactions[0]).into();
block2.transactions[0] = transaction_v4_from_coinbase(&block2.transactions[0]).into();
block1.transactions.push(transaction1.into());
block1.transactions.push(transaction1.clone());
block2.transactions.push(transaction2.into());
let (mut finalized_state, mut non_finalized_state, _genesis) = new_state_with_mainnet_genesis();
let (mut finalized_state, mut non_finalized_state, _genesis) = new_state_with_mainnet_genesis();
// Allows anchor checks to pass
finalized_state.populate_with_anchors(&block1);
@ -590,6 +619,11 @@ proptest! {
let mut previous_mem = non_finalized_state.clone();
// makes sure there are no spurious rejections that might hide bugs in `tx_no_duplicates_in_chain`
let check_tx_no_duplicates_in_chain =
tx_no_duplicates_in_chain(&finalized_state.db, non_finalized_state.best_chain(), &transaction1);
prop_assert!(check_tx_no_duplicates_in_chain.is_ok());
let block1_hash;
// randomly choose to commit the next block to the finalized or non-finalized state
if duplicate_in_finalized_state {
@ -632,6 +666,18 @@ proptest! {
}
.into())
);
let check_tx_no_duplicates_in_chain =
tx_no_duplicates_in_chain(&finalized_state.db, non_finalized_state.best_chain(), &transaction1);
prop_assert_eq!(
check_tx_no_duplicates_in_chain,
Err(DuplicateSaplingNullifier {
nullifier: duplicate_nullifier,
in_finalized_state: duplicate_in_finalized_state,
})
);
prop_assert_eq!(Some((Height(1), block1_hash)), read::best_tip(&non_finalized_state, &finalized_state.db));
prop_assert!(non_finalized_state.eq_internal_state(&previous_mem));
}
@ -829,10 +875,10 @@ proptest! {
let duplicate_nullifier = authorized_action1.action.nullifier;
authorized_action2.action.nullifier = duplicate_nullifier;
let transaction1 = transaction_v5_with_orchard_shielded_data(
let transaction1 = Arc::new(transaction_v5_with_orchard_shielded_data(
orchard_shielded_data1.0,
[authorized_action1.0],
);
));
let transaction2 = transaction_v5_with_orchard_shielded_data(
orchard_shielded_data2.0,
[authorized_action2.0],
@ -841,7 +887,7 @@ proptest! {
block1.transactions[0] = transaction_v4_from_coinbase(&block1.transactions[0]).into();
block2.transactions[0] = transaction_v4_from_coinbase(&block2.transactions[0]).into();
block1.transactions.push(transaction1.into());
block1.transactions.push(transaction1.clone());
block2.transactions.push(transaction2.into());
let (mut finalized_state, mut non_finalized_state, _genesis) = new_state_with_mainnet_genesis();
@ -852,6 +898,11 @@ proptest! {
let mut previous_mem = non_finalized_state.clone();
// makes sure there are no spurious rejections that might hide bugs in `tx_no_duplicates_in_chain`
let check_tx_no_duplicates_in_chain =
tx_no_duplicates_in_chain(&finalized_state.db, non_finalized_state.best_chain(), &transaction1);
prop_assert!(check_tx_no_duplicates_in_chain.is_ok());
let block1_hash;
// randomly choose to commit the next block to the finalized or non-finalized state
if duplicate_in_finalized_state {
@ -893,6 +944,18 @@ proptest! {
}
.into())
);
let check_tx_no_duplicates_in_chain =
tx_no_duplicates_in_chain(&finalized_state.db, non_finalized_state.best_chain(), &transaction1);
prop_assert_eq!(
check_tx_no_duplicates_in_chain,
Err(DuplicateOrchardNullifier {
nullifier: duplicate_nullifier,
in_finalized_state: duplicate_in_finalized_state,
})
);
prop_assert_eq!(Some((Height(1), block1_hash)), read::best_tip(&non_finalized_state, &finalized_state.db));
prop_assert!(non_finalized_state.eq_internal_state(&previous_mem));
}

View File

@ -226,15 +226,18 @@ impl NonFinalizedState {
)?;
// Reads from disk
check::anchors::sapling_orchard_anchors_refer_to_final_treestates(
check::anchors::block_sapling_orchard_anchors_refer_to_final_treestates(
finalized_state,
&new_chain,
&prepared,
)?;
// Reads from disk
let sprout_final_treestates =
check::anchors::fetch_sprout_final_treestates(finalized_state, &new_chain, &prepared);
let sprout_final_treestates = check::anchors::block_fetch_sprout_final_treestates(
finalized_state,
&new_chain,
&prepared,
);
// Quick check that doesn't read from disk
let contextual = ContextuallyValidBlock::with_block_and_spent_utxos(
@ -285,12 +288,13 @@ impl NonFinalizedState {
});
scope.spawn_fifo(|_scope| {
sprout_anchor_result = Some(check::anchors::sprout_anchors_refer_to_treestates(
sprout_final_treestates,
block2,
height,
transaction_hashes,
));
sprout_anchor_result =
Some(check::anchors::block_sprout_anchors_refer_to_treestates(
sprout_final_treestates,
block2,
transaction_hashes,
height,
));
});
// We're pretty sure the new block is valid,

View File

@ -105,11 +105,11 @@ pub struct Chain {
pub(crate) orchard_anchors_by_height: BTreeMap<block::Height, orchard::tree::Root>,
/// The Sprout nullifiers revealed by `blocks`.
pub(super) sprout_nullifiers: HashSet<sprout::Nullifier>,
pub(crate) sprout_nullifiers: HashSet<sprout::Nullifier>,
/// The Sapling nullifiers revealed by `blocks`.
pub(super) sapling_nullifiers: HashSet<sapling::Nullifier>,
pub(crate) sapling_nullifiers: HashSet<sapling::Nullifier>,
/// The Orchard nullifiers revealed by `blocks`.
pub(super) orchard_nullifiers: HashSet<orchard::Nullifier>,
pub(crate) orchard_nullifiers: HashSet<orchard::Nullifier>,
/// Partial transparent address index data from `blocks`.
pub(super) partial_transparent_transfers: HashMap<transparent::Address, TransparentTransfers>,
@ -410,12 +410,14 @@ impl Chain {
.expect("Orchard anchors must exist for pre-fork blocks");
let history_tree_mut = Arc::make_mut(&mut self.history_tree);
history_tree_mut.push(
self.network,
block.block.clone(),
*sapling_root,
*orchard_root,
)?;
history_tree_mut
.push(
self.network,
block.block.clone(),
*sapling_root,
*orchard_root,
)
.map_err(Arc::new)?;
}
Ok(())
@ -909,12 +911,14 @@ impl Chain {
// TODO: update the history trees in a rayon thread, if they show up in CPU profiles
let history_tree_mut = Arc::make_mut(&mut self.history_tree);
history_tree_mut.push(
self.network,
contextually_valid.block.clone(),
sapling_root,
orchard_root,
)?;
history_tree_mut
.push(
self.network,
contextually_valid.block.clone(),
sapling_root,
orchard_root,
)
.map_err(Arc::new)?;
self.history_trees_by_height
.insert(height, self.history_tree.clone());

View File

@ -163,7 +163,7 @@ async fn mempool_push_transaction() -> Result<(), crate::BoxError> {
let transaction = responder
.request()
.clone()
.into_mempool_transaction()
.mempool_transaction()
.expect("unexpected non-mempool request");
// Set a dummy fee and sigops.
@ -267,7 +267,7 @@ async fn mempool_advertise_transaction_ids() -> Result<(), crate::BoxError> {
let transaction = responder
.request()
.clone()
.into_mempool_transaction()
.mempool_transaction()
.expect("unexpected non-mempool request");
// Set a dummy fee and sigops.
@ -368,7 +368,7 @@ async fn mempool_transaction_expiration() -> Result<(), crate::BoxError> {
let transaction = responder
.request()
.clone()
.into_mempool_transaction()
.mempool_transaction()
.expect("unexpected non-mempool request");
// Set a dummy fee and sigops.
@ -502,7 +502,7 @@ async fn mempool_transaction_expiration() -> Result<(), crate::BoxError> {
let transaction = responder
.request()
.clone()
.into_mempool_transaction()
.mempool_transaction()
.expect("unexpected non-mempool request");
// Set a dummy fee and sigops.

View File

@ -271,10 +271,10 @@ where
let mut state = self.state.clone();
let fut = async move {
// Don't download/verify if the transaction is already in the state.
Self::transaction_in_state(&mut state, txid).await?;
// Don't download/verify if the transaction is already in the best chain.
Self::transaction_in_best_chain(&mut state, txid).await?;
trace!(?txid, "transaction is not in state");
trace!(?txid, "transaction is not in best chain");
let next_height = match state.oneshot(zs::Request::Tip).await {
Ok(zs::Response::Tip(None)) => Ok(Height(0)),
@ -442,12 +442,11 @@ where
self.pending.len()
}
/// Check if transaction is already in the state.
async fn transaction_in_state(
/// Check if transaction is already in the best chain.
async fn transaction_in_best_chain(
state: &mut ZS,
txid: UnminedTxId,
) -> Result<(), TransactionDownloadVerifyError> {
// Check if the transaction is already in the state.
match state
.ready()
.await