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:
parent
0ec502bb85
commit
eb0a2ef581
|
@ -332,7 +332,7 @@ impl FromHex for ChainHistoryBlockTxAuthCommitmentHash {
|
||||||
/// implement, and ensures that we don't reject blocks or transactions
|
/// implement, and ensures that we don't reject blocks or transactions
|
||||||
/// for a non-enumerated reason.
|
/// for a non-enumerated reason.
|
||||||
#[allow(missing_docs)]
|
#[allow(missing_docs)]
|
||||||
#[derive(Error, Debug, PartialEq, Eq)]
|
#[derive(Error, Clone, Debug, PartialEq, Eq)]
|
||||||
pub enum CommitmentError {
|
pub enum CommitmentError {
|
||||||
#[error(
|
#[error(
|
||||||
"invalid final sapling root: expected {:?}, actual: {:?}",
|
"invalid final sapling root: expected {:?}, actual: {:?}",
|
||||||
|
|
|
@ -9,6 +9,7 @@ use chrono::{DateTime, Utc};
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
|
|
||||||
use zebra_chain::{amount, block, orchard, sapling, sprout, transparent};
|
use zebra_chain::{amount, block, orchard, sapling, sprout, transparent};
|
||||||
|
use zebra_state::ValidateContextError;
|
||||||
|
|
||||||
use crate::{block::MAX_BLOCK_SIGOPS, BoxError};
|
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")]
|
#[error("could not find a mempool transaction input UTXO in the best chain")]
|
||||||
TransparentInputNotFound,
|
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 {
|
impl From<BoxError> for TransactionError {
|
||||||
|
@ -190,6 +195,11 @@ impl From<BoxError> for TransactionError {
|
||||||
Err(e) => err = e,
|
Err(e) => err = e,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
match err.downcast::<ValidateContextError>() {
|
||||||
|
Ok(e) => return (*e).into(),
|
||||||
|
Err(e) => err = e,
|
||||||
|
}
|
||||||
|
|
||||||
// buffered transaction verifier service error
|
// buffered transaction verifier service error
|
||||||
match err.downcast::<TransactionError>() {
|
match err.downcast::<TransactionError>() {
|
||||||
Ok(e) => return *e,
|
Ok(e) => return *e,
|
||||||
|
|
|
@ -175,10 +175,10 @@ impl Request {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The unverified mempool transaction, if this is a mempool 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 {
|
match self {
|
||||||
Request::Block { .. } => None,
|
Request::Block { .. } => None,
|
||||||
Request::Mempool { transaction, .. } => Some(transaction),
|
Request::Mempool { transaction, .. } => Some(transaction.clone()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -357,15 +357,16 @@ where
|
||||||
|
|
||||||
// Load spent UTXOs from state.
|
// Load spent UTXOs from state.
|
||||||
// TODO: Make this a method of `Request` and replace `tx.clone()` with `self.transaction()`?
|
// TODO: Make this a method of `Request` and replace `tx.clone()` with `self.transaction()`?
|
||||||
let (spent_utxos, spent_outputs) =
|
let load_spent_utxos_fut =
|
||||||
Self::spent_utxos(tx.clone(), req.known_utxos(), req.is_mempool(), state).await?;
|
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 =
|
let cached_ffi_transaction =
|
||||||
Arc::new(CachedFfiTransaction::new(tx.clone(), spent_outputs));
|
Arc::new(CachedFfiTransaction::new(tx.clone(), spent_outputs));
|
||||||
|
|
||||||
tracing::trace!(?tx_id, "got state UTXOs");
|
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 { .. } => {
|
Transaction::V1 { .. } | Transaction::V2 { .. } | Transaction::V3 { .. } => {
|
||||||
tracing::debug!(?tx, "got transaction with wrong version");
|
tracing::debug!(?tx, "got transaction with wrong version");
|
||||||
return Err(TransactionError::WrongVersion);
|
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...");
|
tracing::trace!(?tx_id, "awaiting async checks...");
|
||||||
|
|
||||||
// If the Groth16 parameter download hangs,
|
// If the Groth16 parameter download hangs,
|
||||||
|
|
|
@ -189,17 +189,28 @@ async fn mempool_request_with_missing_input_is_rejected() {
|
||||||
.find(|(_, tx)| !(tx.is_coinbase() || tx.inputs().is_empty()))
|
.find(|(_, tx)| !(tx.is_coinbase() || tx.inputs().is_empty()))
|
||||||
.expect("At least one non-coinbase transaction with transparent inputs in test vectors");
|
.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::PrevOut { outpoint, .. } => outpoint,
|
||||||
transparent::Input::Coinbase { .. } => panic!("requires a non-coinbase transaction"),
|
transparent::Input::Coinbase { .. } => panic!("requires a non-coinbase transaction"),
|
||||||
});
|
};
|
||||||
|
|
||||||
tokio::spawn(async move {
|
tokio::spawn(async move {
|
||||||
state
|
state
|
||||||
.expect_request(expected_state_request)
|
.expect_request(zebra_state::Request::UnspentBestChainUtxo(input_outpoint))
|
||||||
.await
|
.await
|
||||||
.expect("verifier should call mock state service")
|
.expect("verifier should call mock state service")
|
||||||
.respond(zebra_state::Response::UnspentBestChainUtxo(None));
|
.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
|
let verifier_response = verifier
|
||||||
|
@ -251,6 +262,17 @@ async fn mempool_request_with_present_input_is_accepted() {
|
||||||
.get(&input_outpoint)
|
.get(&input_outpoint)
|
||||||
.map(|utxo| utxo.utxo.clone()),
|
.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
|
let verifier_response = verifier
|
||||||
|
|
|
@ -47,7 +47,7 @@ pub type BoxError = Box<dyn std::error::Error + Send + Sync + 'static>;
|
||||||
pub struct CommitBlockError(#[from] ValidateContextError);
|
pub struct CommitBlockError(#[from] ValidateContextError);
|
||||||
|
|
||||||
/// An error describing why a block failed contextual validation.
|
/// An error describing why a block failed contextual validation.
|
||||||
#[derive(Debug, Error, PartialEq, Eq)]
|
#[derive(Debug, Error, Clone, PartialEq, Eq)]
|
||||||
#[non_exhaustive]
|
#[non_exhaustive]
|
||||||
#[allow(missing_docs)]
|
#[allow(missing_docs)]
|
||||||
pub enum ValidateContextError {
|
pub enum ValidateContextError {
|
||||||
|
@ -224,7 +224,7 @@ pub enum ValidateContextError {
|
||||||
NoteCommitmentTreeError(#[from] zebra_chain::parallel::tree::NoteCommitmentTreeError),
|
NoteCommitmentTreeError(#[from] zebra_chain::parallel::tree::NoteCommitmentTreeError),
|
||||||
|
|
||||||
#[error("error building the history tree")]
|
#[error("error building the history tree")]
|
||||||
HistoryTreeError(#[from] HistoryTreeError),
|
HistoryTreeError(#[from] Arc<HistoryTreeError>),
|
||||||
|
|
||||||
#[error("block contains an invalid commitment")]
|
#[error("block contains an invalid commitment")]
|
||||||
InvalidBlockCommitment(#[from] block::CommitmentError),
|
InvalidBlockCommitment(#[from] block::CommitmentError),
|
||||||
|
@ -236,8 +236,8 @@ pub enum ValidateContextError {
|
||||||
#[non_exhaustive]
|
#[non_exhaustive]
|
||||||
UnknownSproutAnchor {
|
UnknownSproutAnchor {
|
||||||
anchor: sprout::tree::Root,
|
anchor: sprout::tree::Root,
|
||||||
height: block::Height,
|
height: Option<block::Height>,
|
||||||
tx_index_in_block: usize,
|
tx_index_in_block: Option<usize>,
|
||||||
transaction_hash: transaction::Hash,
|
transaction_hash: transaction::Hash,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -248,8 +248,8 @@ pub enum ValidateContextError {
|
||||||
#[non_exhaustive]
|
#[non_exhaustive]
|
||||||
UnknownSaplingAnchor {
|
UnknownSaplingAnchor {
|
||||||
anchor: sapling::tree::Root,
|
anchor: sapling::tree::Root,
|
||||||
height: block::Height,
|
height: Option<block::Height>,
|
||||||
tx_index_in_block: usize,
|
tx_index_in_block: Option<usize>,
|
||||||
transaction_hash: transaction::Hash,
|
transaction_hash: transaction::Hash,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -260,8 +260,8 @@ pub enum ValidateContextError {
|
||||||
#[non_exhaustive]
|
#[non_exhaustive]
|
||||||
UnknownOrchardAnchor {
|
UnknownOrchardAnchor {
|
||||||
anchor: orchard::tree::Root,
|
anchor: orchard::tree::Root,
|
||||||
height: block::Height,
|
height: Option<block::Height>,
|
||||||
tx_index_in_block: usize,
|
tx_index_in_block: Option<usize>,
|
||||||
transaction_hash: transaction::Hash,
|
transaction_hash: transaction::Hash,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,7 +14,8 @@ use zebra_chain::{
|
||||||
parallel::tree::NoteCommitmentTrees,
|
parallel::tree::NoteCommitmentTrees,
|
||||||
sapling,
|
sapling,
|
||||||
serialization::SerializationError,
|
serialization::SerializationError,
|
||||||
sprout, transaction,
|
sprout,
|
||||||
|
transaction::{self, UnminedTx},
|
||||||
transparent::{self, utxos_from_ordered_utxos},
|
transparent::{self, utxos_from_ordered_utxos},
|
||||||
value_balance::{ValueBalance, ValueBalanceError},
|
value_balance::{ValueBalance, ValueBalanceError},
|
||||||
};
|
};
|
||||||
|
@ -539,6 +540,11 @@ pub enum Request {
|
||||||
/// Optionally, the hash of the last header to request.
|
/// Optionally, the hash of the last header to request.
|
||||||
stop: Option<block::Hash>,
|
stop: Option<block::Hash>,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/// Contextually validates anchors and nullifiers of a transaction on the best chain
|
||||||
|
///
|
||||||
|
/// Returns [`Response::ValidBestChainTipNullifiersAndAnchors`]
|
||||||
|
CheckBestChainTipNullifiersAndAnchors(UnminedTx),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Request {
|
impl Request {
|
||||||
|
@ -555,6 +561,9 @@ impl Request {
|
||||||
Request::Block(_) => "block",
|
Request::Block(_) => "block",
|
||||||
Request::FindBlockHashes { .. } => "find_block_hashes",
|
Request::FindBlockHashes { .. } => "find_block_hashes",
|
||||||
Request::FindBlockHeaders { .. } => "find_block_headers",
|
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.
|
/// Returns a type with found utxos and transaction information.
|
||||||
UtxosByAddresses(HashSet<transparent::Address>),
|
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")]
|
#[cfg(feature = "getblocktemplate-rpcs")]
|
||||||
/// Looks up a block hash by height in the current best chain.
|
/// Looks up a block hash by height in the current best chain.
|
||||||
///
|
///
|
||||||
|
@ -772,6 +786,9 @@ impl ReadRequest {
|
||||||
ReadRequest::AddressBalance { .. } => "address_balance",
|
ReadRequest::AddressBalance { .. } => "address_balance",
|
||||||
ReadRequest::TransactionIdsByAddresses { .. } => "transaction_ids_by_addesses",
|
ReadRequest::TransactionIdsByAddresses { .. } => "transaction_ids_by_addesses",
|
||||||
ReadRequest::UtxosByAddresses(_) => "utxos_by_addesses",
|
ReadRequest::UtxosByAddresses(_) => "utxos_by_addesses",
|
||||||
|
ReadRequest::CheckBestChainTipNullifiersAndAnchors(_) => {
|
||||||
|
"best_chain_tip_nullifiers_anchors"
|
||||||
|
}
|
||||||
#[cfg(feature = "getblocktemplate-rpcs")]
|
#[cfg(feature = "getblocktemplate-rpcs")]
|
||||||
ReadRequest::BestChainBlockHash(_) => "best_chain_block_hash",
|
ReadRequest::BestChainBlockHash(_) => "best_chain_block_hash",
|
||||||
#[cfg(feature = "getblocktemplate-rpcs")]
|
#[cfg(feature = "getblocktemplate-rpcs")]
|
||||||
|
@ -815,6 +832,10 @@ impl TryFrom<Request> for ReadRequest {
|
||||||
Ok(ReadRequest::FindBlockHeaders { known_blocks, stop })
|
Ok(ReadRequest::FindBlockHeaders { known_blocks, stop })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Request::CheckBestChainTipNullifiersAndAnchors(tx) => {
|
||||||
|
Ok(ReadRequest::CheckBestChainTipNullifiersAndAnchors(tx))
|
||||||
|
}
|
||||||
|
|
||||||
Request::CommitBlock(_) | Request::CommitFinalizedBlock(_) => {
|
Request::CommitBlock(_) | Request::CommitFinalizedBlock(_) => {
|
||||||
Err("ReadService does not write blocks")
|
Err("ReadService does not write blocks")
|
||||||
}
|
}
|
||||||
|
|
|
@ -54,6 +54,11 @@ pub enum Response {
|
||||||
|
|
||||||
/// The response to a `FindBlockHeaders` request.
|
/// The response to a `FindBlockHeaders` request.
|
||||||
BlockHeaders(Vec<block::CountedHeader>),
|
BlockHeaders(Vec<block::CountedHeader>),
|
||||||
|
|
||||||
|
/// Response to [`Request::CheckBestChainTipNullifiersAndAnchors`].
|
||||||
|
///
|
||||||
|
/// Does not check transparent UTXO inputs
|
||||||
|
ValidBestChainTipNullifiersAndAnchors,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||||
|
@ -114,6 +119,11 @@ pub enum ReadResponse {
|
||||||
/// Response to [`ReadRequest::UtxosByAddresses`] with found utxos and transaction data.
|
/// Response to [`ReadRequest::UtxosByAddresses`] with found utxos and transaction data.
|
||||||
AddressUtxos(AddressUtxos),
|
AddressUtxos(AddressUtxos),
|
||||||
|
|
||||||
|
/// Response to [`ReadRequest::CheckBestChainTipNullifiersAndAnchors`].
|
||||||
|
///
|
||||||
|
/// Does not check transparent UTXO inputs
|
||||||
|
ValidBestChainTipNullifiersAndAnchors,
|
||||||
|
|
||||||
#[cfg(feature = "getblocktemplate-rpcs")]
|
#[cfg(feature = "getblocktemplate-rpcs")]
|
||||||
/// Response to [`ReadRequest::BestChainBlockHash`](crate::ReadRequest::BestChainBlockHash) with the
|
/// Response to [`ReadRequest::BestChainBlockHash`](crate::ReadRequest::BestChainBlockHash) with the
|
||||||
/// specified block hash.
|
/// specified block hash.
|
||||||
|
@ -171,6 +181,8 @@ impl TryFrom<ReadResponse> for Response {
|
||||||
ReadResponse::BlockHashes(hashes) => Ok(Response::BlockHashes(hashes)),
|
ReadResponse::BlockHashes(hashes) => Ok(Response::BlockHashes(hashes)),
|
||||||
ReadResponse::BlockHeaders(headers) => Ok(Response::BlockHeaders(headers)),
|
ReadResponse::BlockHeaders(headers) => Ok(Response::BlockHeaders(headers)),
|
||||||
|
|
||||||
|
ReadResponse::ValidBestChainTipNullifiersAndAnchors => Ok(Response::ValidBestChainTipNullifiersAndAnchors),
|
||||||
|
|
||||||
ReadResponse::TransactionIdsForBlock(_)
|
ReadResponse::TransactionIdsForBlock(_)
|
||||||
| ReadResponse::SaplingTree(_)
|
| ReadResponse::SaplingTree(_)
|
||||||
| ReadResponse::OrchardTree(_)
|
| ReadResponse::OrchardTree(_)
|
||||||
|
|
|
@ -1024,7 +1024,8 @@ impl Service<Request> for StateService {
|
||||||
| Request::UnspentBestChainUtxo(_)
|
| Request::UnspentBestChainUtxo(_)
|
||||||
| Request::Block(_)
|
| Request::Block(_)
|
||||||
| Request::FindBlockHashes { .. }
|
| Request::FindBlockHashes { .. }
|
||||||
| Request::FindBlockHeaders { .. } => {
|
| Request::FindBlockHeaders { .. }
|
||||||
|
| Request::CheckBestChainTipNullifiersAndAnchors(_) => {
|
||||||
// Redirect the request to the concurrent ReadStateService
|
// Redirect the request to the concurrent ReadStateService
|
||||||
let read_service = self.read_service.clone();
|
let read_service = self.read_service.clone();
|
||||||
|
|
||||||
|
@ -1217,7 +1218,6 @@ impl Service<ReadRequest> for ReadStateService {
|
||||||
.boxed()
|
.boxed()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Currently unused.
|
|
||||||
ReadRequest::UnspentBestChainUtxo(outpoint) => {
|
ReadRequest::UnspentBestChainUtxo(outpoint) => {
|
||||||
let timer = CodeTimer::start();
|
let timer = CodeTimer::start();
|
||||||
|
|
||||||
|
@ -1519,6 +1519,39 @@ impl Service<ReadRequest> for ReadStateService {
|
||||||
.boxed()
|
.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.
|
// Used by get_block_hash RPC.
|
||||||
#[cfg(feature = "getblocktemplate-rpcs")]
|
#[cfg(feature = "getblocktemplate-rpcs")]
|
||||||
ReadRequest::BestChainBlockHash(height) => {
|
ReadRequest::BestChainBlockHash(height) => {
|
||||||
|
|
|
@ -5,7 +5,8 @@ use std::{collections::HashMap, sync::Arc};
|
||||||
|
|
||||||
use zebra_chain::{
|
use zebra_chain::{
|
||||||
block::{Block, Height},
|
block::{Block, Height},
|
||||||
sprout, transaction,
|
sprout,
|
||||||
|
transaction::{Hash as TransactionHash, Transaction, UnminedTx},
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
|
@ -13,18 +14,19 @@ use crate::{
|
||||||
PreparedBlock, ValidateContextError,
|
PreparedBlock, ValidateContextError,
|
||||||
};
|
};
|
||||||
|
|
||||||
/// Checks the final Sapling and Orchard anchors specified by transactions in this
|
/// Checks the final Sapling and Orchard anchors specified by `transaction`
|
||||||
/// `prepared` block.
|
|
||||||
///
|
///
|
||||||
/// This method checks for anchors computed from the final treestate of each block in
|
/// This method checks for anchors computed from the final treestate of each block in
|
||||||
/// the `parent_chain` or `finalized_state`.
|
/// the `parent_chain` or `finalized_state`.
|
||||||
#[tracing::instrument(skip(finalized_state, parent_chain, prepared))]
|
#[tracing::instrument(skip(finalized_state, parent_chain, transaction))]
|
||||||
pub(crate) fn sapling_orchard_anchors_refer_to_final_treestates(
|
fn sapling_orchard_anchors_refer_to_final_treestates(
|
||||||
finalized_state: &ZebraDb,
|
finalized_state: &ZebraDb,
|
||||||
parent_chain: &Chain,
|
parent_chain: Option<&Arc<Chain>>,
|
||||||
prepared: &PreparedBlock,
|
transaction: &Arc<Transaction>,
|
||||||
|
transaction_hash: TransactionHash,
|
||||||
|
tx_index_in_block: Option<usize>,
|
||||||
|
height: Option<Height>,
|
||||||
) -> Result<(), ValidateContextError> {
|
) -> Result<(), ValidateContextError> {
|
||||||
for (tx_index_in_block, transaction) in prepared.block.transactions.iter().enumerate() {
|
|
||||||
// Sapling Spends
|
// Sapling Spends
|
||||||
//
|
//
|
||||||
// MUST refer to some earlier block’s final Sapling treestate.
|
// MUST refer to some earlier block’s final Sapling treestate.
|
||||||
|
@ -47,18 +49,20 @@ pub(crate) fn sapling_orchard_anchors_refer_to_final_treestates(
|
||||||
?anchor,
|
?anchor,
|
||||||
?anchor_index_in_tx,
|
?anchor_index_in_tx,
|
||||||
?tx_index_in_block,
|
?tx_index_in_block,
|
||||||
height = ?prepared.height,
|
?height,
|
||||||
"observed sapling anchor",
|
"observed sapling anchor",
|
||||||
);
|
);
|
||||||
|
|
||||||
if !parent_chain.sapling_anchors.contains(&anchor)
|
if !parent_chain
|
||||||
|
.map(|chain| chain.sapling_anchors.contains(&anchor))
|
||||||
|
.unwrap_or(false)
|
||||||
&& !finalized_state.contains_sapling_anchor(&anchor)
|
&& !finalized_state.contains_sapling_anchor(&anchor)
|
||||||
{
|
{
|
||||||
return Err(ValidateContextError::UnknownSaplingAnchor {
|
return Err(ValidateContextError::UnknownSaplingAnchor {
|
||||||
anchor,
|
anchor,
|
||||||
height: prepared.height,
|
height,
|
||||||
tx_index_in_block,
|
tx_index_in_block,
|
||||||
transaction_hash: prepared.transaction_hashes[tx_index_in_block],
|
transaction_hash,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -66,7 +70,7 @@ pub(crate) fn sapling_orchard_anchors_refer_to_final_treestates(
|
||||||
?anchor,
|
?anchor,
|
||||||
?anchor_index_in_tx,
|
?anchor_index_in_tx,
|
||||||
?tx_index_in_block,
|
?tx_index_in_block,
|
||||||
height = ?prepared.height,
|
?height,
|
||||||
"validated sapling anchor",
|
"validated sapling anchor",
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -86,31 +90,34 @@ pub(crate) fn sapling_orchard_anchors_refer_to_final_treestates(
|
||||||
tracing::debug!(
|
tracing::debug!(
|
||||||
?orchard_shielded_data.shared_anchor,
|
?orchard_shielded_data.shared_anchor,
|
||||||
?tx_index_in_block,
|
?tx_index_in_block,
|
||||||
height = ?prepared.height,
|
?height,
|
||||||
"observed orchard anchor",
|
"observed orchard anchor",
|
||||||
);
|
);
|
||||||
|
|
||||||
if !parent_chain
|
if !parent_chain
|
||||||
|
.map(|chain| {
|
||||||
|
chain
|
||||||
.orchard_anchors
|
.orchard_anchors
|
||||||
.contains(&orchard_shielded_data.shared_anchor)
|
.contains(&orchard_shielded_data.shared_anchor)
|
||||||
|
})
|
||||||
|
.unwrap_or(false)
|
||||||
&& !finalized_state.contains_orchard_anchor(&orchard_shielded_data.shared_anchor)
|
&& !finalized_state.contains_orchard_anchor(&orchard_shielded_data.shared_anchor)
|
||||||
{
|
{
|
||||||
return Err(ValidateContextError::UnknownOrchardAnchor {
|
return Err(ValidateContextError::UnknownOrchardAnchor {
|
||||||
anchor: orchard_shielded_data.shared_anchor,
|
anchor: orchard_shielded_data.shared_anchor,
|
||||||
height: prepared.height,
|
height,
|
||||||
tx_index_in_block,
|
tx_index_in_block,
|
||||||
transaction_hash: prepared.transaction_hashes[tx_index_in_block],
|
transaction_hash,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
tracing::debug!(
|
tracing::debug!(
|
||||||
?orchard_shielded_data.shared_anchor,
|
?orchard_shielded_data.shared_anchor,
|
||||||
?tx_index_in_block,
|
?tx_index_in_block,
|
||||||
height = ?prepared.height,
|
?height,
|
||||||
"validated orchard anchor",
|
"validated orchard anchor",
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
@ -122,31 +129,28 @@ pub(crate) fn sapling_orchard_anchors_refer_to_final_treestates(
|
||||||
/// Sprout anchors may also refer to the interstitial output treestate of any prior
|
/// Sprout anchors may also refer to the interstitial output treestate of any prior
|
||||||
/// `JoinSplit` _within the same transaction_; these are created on the fly
|
/// `JoinSplit` _within the same transaction_; these are created on the fly
|
||||||
/// in [`sprout_anchors_refer_to_treestates()`].
|
/// in [`sprout_anchors_refer_to_treestates()`].
|
||||||
#[tracing::instrument(skip(finalized_state, parent_chain, prepared))]
|
#[tracing::instrument(skip(sprout_final_treestates, finalized_state, parent_chain, transaction))]
|
||||||
pub(crate) fn fetch_sprout_final_treestates(
|
fn fetch_sprout_final_treestates(
|
||||||
|
sprout_final_treestates: &mut HashMap<
|
||||||
|
sprout::tree::Root,
|
||||||
|
Arc<sprout::tree::NoteCommitmentTree>,
|
||||||
|
>,
|
||||||
finalized_state: &ZebraDb,
|
finalized_state: &ZebraDb,
|
||||||
parent_chain: &Chain,
|
parent_chain: Option<&Arc<Chain>>,
|
||||||
prepared: &PreparedBlock,
|
transaction: &Arc<Transaction>,
|
||||||
) -> HashMap<sprout::tree::Root, Arc<sprout::tree::NoteCommitmentTree>> {
|
tx_index_in_block: Option<usize>,
|
||||||
let mut sprout_final_treestates = HashMap::new();
|
height: Option<Height>,
|
||||||
|
) {
|
||||||
for (tx_index_in_block, transaction) in prepared.block.transactions.iter().enumerate() {
|
|
||||||
// Fetch and return Sprout JoinSplit final treestates
|
// Fetch and return Sprout JoinSplit final treestates
|
||||||
for (joinsplit_index_in_tx, joinsplit) in
|
for (joinsplit_index_in_tx, joinsplit) in transaction.sprout_groth16_joinsplits().enumerate() {
|
||||||
transaction.sprout_groth16_joinsplits().enumerate()
|
|
||||||
{
|
|
||||||
// Avoid duplicate fetches
|
// Avoid duplicate fetches
|
||||||
if sprout_final_treestates.contains_key(&joinsplit.anchor) {
|
if sprout_final_treestates.contains_key(&joinsplit.anchor) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
let input_tree = parent_chain
|
let input_tree = parent_chain
|
||||||
.sprout_trees_by_anchor
|
.and_then(|chain| chain.sprout_trees_by_anchor.get(&joinsplit.anchor).cloned())
|
||||||
.get(&joinsplit.anchor)
|
.or_else(|| finalized_state.sprout_note_commitment_tree_by_anchor(&joinsplit.anchor));
|
||||||
.cloned()
|
|
||||||
.or_else(|| {
|
|
||||||
finalized_state.sprout_note_commitment_tree_by_anchor(&joinsplit.anchor)
|
|
||||||
});
|
|
||||||
|
|
||||||
if let Some(input_tree) = input_tree {
|
if let Some(input_tree) = input_tree {
|
||||||
/* TODO:
|
/* TODO:
|
||||||
|
@ -172,24 +176,21 @@ pub(crate) fn fetch_sprout_final_treestates(
|
||||||
?joinsplit.anchor,
|
?joinsplit.anchor,
|
||||||
?joinsplit_index_in_tx,
|
?joinsplit_index_in_tx,
|
||||||
?tx_index_in_block,
|
?tx_index_in_block,
|
||||||
height = ?prepared.height,
|
?height,
|
||||||
"observed sprout final treestate anchor",
|
"observed sprout final treestate anchor",
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
tracing::trace!(
|
tracing::trace!(
|
||||||
sprout_final_treestate_count = ?sprout_final_treestates.len(),
|
sprout_final_treestate_count = ?sprout_final_treestates.len(),
|
||||||
?sprout_final_treestates,
|
?sprout_final_treestates,
|
||||||
height = ?prepared.height,
|
?height,
|
||||||
"returning sprout final treestate anchors",
|
"returning sprout final treestate anchors",
|
||||||
);
|
);
|
||||||
|
|
||||||
sprout_final_treestates
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Checks the Sprout anchors specified by transactions in `block`.
|
/// Checks the Sprout anchors specified by `transactions`.
|
||||||
///
|
///
|
||||||
/// Sprout anchors may refer to some earlier block's final treestate (like
|
/// Sprout anchors may refer to some earlier block's final treestate (like
|
||||||
/// Sapling and Orchard do exclusively) _or_ to the interstitial output
|
/// Sapling and Orchard do exclusively) _or_ to the interstitial output
|
||||||
|
@ -199,33 +200,21 @@ pub(crate) fn fetch_sprout_final_treestates(
|
||||||
/// (which must be populated with all treestates pointed to in the `prepared` block;
|
/// (which must be populated with all treestates pointed to in the `prepared` block;
|
||||||
/// see [`fetch_sprout_final_treestates()`]); or in the interstitial
|
/// see [`fetch_sprout_final_treestates()`]); or in the interstitial
|
||||||
/// treestates which are computed on the fly in this function.
|
/// treestates which are computed on the fly in this function.
|
||||||
#[tracing::instrument(skip(sprout_final_treestates, block, transaction_hashes))]
|
#[tracing::instrument(skip(sprout_final_treestates, transaction))]
|
||||||
pub(crate) fn sprout_anchors_refer_to_treestates(
|
fn sprout_anchors_refer_to_treestates(
|
||||||
sprout_final_treestates: HashMap<sprout::tree::Root, Arc<sprout::tree::NoteCommitmentTree>>,
|
sprout_final_treestates: &HashMap<sprout::tree::Root, Arc<sprout::tree::NoteCommitmentTree>>,
|
||||||
block: Arc<Block>,
|
transaction: &Arc<Transaction>,
|
||||||
// Only used for debugging
|
transaction_hash: TransactionHash,
|
||||||
height: Height,
|
tx_index_in_block: Option<usize>,
|
||||||
transaction_hashes: Arc<[transaction::Hash]>,
|
height: Option<Height>,
|
||||||
) -> Result<(), ValidateContextError> {
|
) -> Result<(), ValidateContextError> {
|
||||||
tracing::trace!(
|
|
||||||
sprout_final_treestate_count = ?sprout_final_treestates.len(),
|
|
||||||
?sprout_final_treestates,
|
|
||||||
?height,
|
|
||||||
"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.
|
// Sprout JoinSplits, with interstitial treestates to check as well.
|
||||||
let mut interstitial_trees: HashMap<
|
let mut interstitial_trees: HashMap<sprout::tree::Root, Arc<sprout::tree::NoteCommitmentTree>> =
|
||||||
sprout::tree::Root,
|
HashMap::new();
|
||||||
Arc<sprout::tree::NoteCommitmentTree>,
|
|
||||||
> = HashMap::new();
|
|
||||||
|
|
||||||
let joinsplit_count = transaction.sprout_groth16_joinsplits().count();
|
let joinsplit_count = transaction.sprout_groth16_joinsplits().count();
|
||||||
|
|
||||||
for (joinsplit_index_in_tx, joinsplit) in
|
for (joinsplit_index_in_tx, joinsplit) in transaction.sprout_groth16_joinsplits().enumerate() {
|
||||||
transaction.sprout_groth16_joinsplits().enumerate()
|
|
||||||
{
|
|
||||||
// Check all anchor sets, including the one for interstitial
|
// Check all anchor sets, including the one for interstitial
|
||||||
// anchors.
|
// anchors.
|
||||||
//
|
//
|
||||||
|
@ -288,7 +277,7 @@ pub(crate) fn sprout_anchors_refer_to_treestates(
|
||||||
anchor: joinsplit.anchor,
|
anchor: joinsplit.anchor,
|
||||||
height,
|
height,
|
||||||
tx_index_in_block,
|
tx_index_in_block,
|
||||||
transaction_hash: transaction_hashes[tx_index_in_block],
|
transaction_hash,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -326,7 +315,159 @@ pub(crate) fn sprout_anchors_refer_to_treestates(
|
||||||
"observed sprout interstitial anchor",
|
"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
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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
|
||||||
|
/// 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, block, transaction_hashes))]
|
||||||
|
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,
|
||||||
|
) -> Result<(), ValidateContextError> {
|
||||||
|
tracing::trace!(
|
||||||
|
sprout_final_treestate_count = ?sprout_final_treestates.len(),
|
||||||
|
?sprout_final_treestates,
|
||||||
|
?height,
|
||||||
|
"received sprout final treestate anchors",
|
||||||
|
);
|
||||||
|
|
||||||
|
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),
|
||||||
|
)?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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,
|
||||||
|
)?;
|
||||||
|
|
||||||
|
let mut sprout_final_treestates = HashMap::new();
|
||||||
|
|
||||||
|
fetch_sprout_final_treestates(
|
||||||
|
&mut sprout_final_treestates,
|
||||||
|
finalized_state,
|
||||||
|
parent_chain,
|
||||||
|
&unmined_tx.transaction,
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
);
|
||||||
|
|
||||||
|
tracing::trace!(
|
||||||
|
sprout_final_treestate_count = ?sprout_final_treestates.len(),
|
||||||
|
?sprout_final_treestates,
|
||||||
|
"received sprout final treestate anchors",
|
||||||
|
);
|
||||||
|
|
||||||
|
sprout_anchors_refer_to_treestates(
|
||||||
|
&sprout_final_treestates,
|
||||||
|
&unmined_tx.transaction,
|
||||||
|
unmined_tx.id.mined_id(),
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
)?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,12 +1,14 @@
|
||||||
//! Checks for nullifier uniqueness.
|
//! Checks for nullifier uniqueness.
|
||||||
|
|
||||||
use std::collections::HashSet;
|
use std::{collections::HashSet, sync::Arc};
|
||||||
|
|
||||||
use tracing::trace;
|
use tracing::trace;
|
||||||
|
use zebra_chain::transaction::Transaction;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
error::DuplicateNullifierError, service::finalized_state::ZebraDb, PreparedBlock,
|
error::DuplicateNullifierError,
|
||||||
ValidateContextError,
|
service::{finalized_state::ZebraDb, non_finalized_state::Chain},
|
||||||
|
PreparedBlock, ValidateContextError,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Tidy up some doc links
|
// Tidy up some doc links
|
||||||
|
@ -54,6 +56,73 @@ pub(crate) fn no_duplicates_in_finalized_chain(
|
||||||
Ok(())
|
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:
|
/// Reject double-spends of nullifers:
|
||||||
/// - both within the same `JoinSplit` (sprout only),
|
/// - both within the same `JoinSplit` (sprout only),
|
||||||
/// - from different `JoinSplit`s, [`sapling::Spend`][2]s or
|
/// - from different `JoinSplit`s, [`sapling::Spend`][2]s or
|
||||||
|
|
|
@ -8,14 +8,17 @@ use zebra_chain::{
|
||||||
primitives::Groth16Proof,
|
primitives::Groth16Proof,
|
||||||
serialization::ZcashDeserializeInto,
|
serialization::ZcashDeserializeInto,
|
||||||
sprout::JoinSplit,
|
sprout::JoinSplit,
|
||||||
transaction::{JoinSplitData, LockTime, Transaction},
|
transaction::{JoinSplitData, LockTime, Transaction, UnminedTx},
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
arbitrary::Prepare,
|
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},
|
tests::setup::{new_state_with_mainnet_genesis, transaction_v4_from_coinbase},
|
||||||
PreparedBlock,
|
PreparedBlock, ValidateContextError,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Sprout
|
// Sprout
|
||||||
|
@ -41,13 +44,6 @@ fn check_sprout_anchors() {
|
||||||
// Add initial transactions to [`block_1`].
|
// Add initial transactions to [`block_1`].
|
||||||
let block_1 = prepare_sprout_block(block_1, block_395);
|
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
|
// Bootstrap a block at height == 2 that references the Sprout note commitment tree state
|
||||||
// from [`block_1`].
|
// from [`block_1`].
|
||||||
let block_2 = zebra_test::vectors::BLOCK_MAINNET_2_BYTES
|
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`].
|
// Add the transactions with the first anchors to [`block_2`].
|
||||||
let block_2 = prepare_sprout_block(block_2, block_396);
|
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.
|
// Validate and commit [`block_2`]. This will also check the anchors.
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
validate_and_commit_non_finalized(&finalized_state, &mut non_finalized_state, block_2),
|
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();
|
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
|
// Bootstrap a block at height == 2 that references the Sapling note commitment tree state
|
||||||
// from earlier block
|
// from earlier block
|
||||||
|
@ -238,6 +267,42 @@ fn check_sapling_anchors() {
|
||||||
});
|
});
|
||||||
|
|
||||||
let block2 = Arc::new(block2).prepare();
|
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!(
|
assert_eq!(
|
||||||
validate_and_commit_non_finalized(&finalized_state, &mut non_finalized_state, block2),
|
validate_and_commit_non_finalized(&finalized_state, &mut non_finalized_state, block2),
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|
|
@ -19,7 +19,9 @@ use zebra_chain::{
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
arbitrary::Prepare,
|
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},
|
tests::setup::{new_state_with_mainnet_genesis, transaction_v4_from_coinbase},
|
||||||
FinalizedBlock,
|
FinalizedBlock,
|
||||||
ValidateContextError::{
|
ValidateContextError::{
|
||||||
|
@ -155,7 +157,9 @@ proptest! {
|
||||||
let block1 = Arc::new(block1).prepare();
|
let block1 = Arc::new(block1).prepare();
|
||||||
let commit_result = validate_and_commit_non_finalized(
|
let commit_result = validate_and_commit_non_finalized(
|
||||||
&finalized_state,
|
&finalized_state,
|
||||||
&mut non_finalized_state, block1);
|
&mut non_finalized_state,
|
||||||
|
block1
|
||||||
|
);
|
||||||
|
|
||||||
// if the random proptest data produces other errors,
|
// if the random proptest data produces other errors,
|
||||||
// we might need to just check `is_err()` here
|
// we might need to just check `is_err()` here
|
||||||
|
@ -214,7 +218,9 @@ proptest! {
|
||||||
let block1 = Arc::new(block1).prepare();
|
let block1 = Arc::new(block1).prepare();
|
||||||
let commit_result = validate_and_commit_non_finalized(
|
let commit_result = validate_and_commit_non_finalized(
|
||||||
&finalized_state,
|
&finalized_state,
|
||||||
&mut non_finalized_state, block1);
|
&mut non_finalized_state,
|
||||||
|
block1
|
||||||
|
);
|
||||||
|
|
||||||
prop_assert_eq!(
|
prop_assert_eq!(
|
||||||
commit_result,
|
commit_result,
|
||||||
|
@ -318,13 +324,13 @@ proptest! {
|
||||||
let duplicate_nullifier = joinsplit1.nullifiers[0];
|
let duplicate_nullifier = joinsplit1.nullifiers[0];
|
||||||
joinsplit2.nullifiers[0] = duplicate_nullifier;
|
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]);
|
let transaction2 = transaction_v4_with_joinsplit_data(joinsplit_data2.0, [joinsplit2.0]);
|
||||||
|
|
||||||
block1.transactions[0] = transaction_v4_from_coinbase(&block1.transactions[0]).into();
|
block1.transactions[0] = transaction_v4_from_coinbase(&block1.transactions[0]).into();
|
||||||
block2.transactions[0] = transaction_v4_from_coinbase(&block2.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());
|
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();
|
||||||
|
@ -335,6 +341,11 @@ proptest! {
|
||||||
|
|
||||||
let mut previous_mem = non_finalized_state.clone();
|
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;
|
let block1_hash;
|
||||||
// randomly choose to commit the next block to the finalized or non-finalized state
|
// randomly choose to commit the next block to the finalized or non-finalized state
|
||||||
if duplicate_in_finalized_state {
|
if duplicate_in_finalized_state {
|
||||||
|
@ -354,7 +365,9 @@ proptest! {
|
||||||
let block1 = Arc::new(block1).prepare();
|
let block1 = Arc::new(block1).prepare();
|
||||||
let commit_result = validate_and_commit_non_finalized(
|
let commit_result = validate_and_commit_non_finalized(
|
||||||
&finalized_state,
|
&finalized_state,
|
||||||
&mut non_finalized_state, block1.clone());
|
&mut non_finalized_state,
|
||||||
|
block1.clone()
|
||||||
|
);
|
||||||
|
|
||||||
prop_assert_eq!(commit_result, Ok(()));
|
prop_assert_eq!(commit_result, Ok(()));
|
||||||
prop_assert_eq!(Some((Height(1), block1.hash)), read::best_tip(&non_finalized_state, &finalized_state.db));
|
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 block2 = Arc::new(block2).prepare();
|
||||||
let commit_result = validate_and_commit_non_finalized(
|
let commit_result = validate_and_commit_non_finalized(
|
||||||
&finalized_state,
|
&finalized_state,
|
||||||
&mut non_finalized_state, block2);
|
&mut non_finalized_state,
|
||||||
|
block2
|
||||||
|
);
|
||||||
|
|
||||||
prop_assert_eq!(
|
prop_assert_eq!(
|
||||||
commit_result,
|
commit_result,
|
||||||
|
@ -381,6 +396,18 @@ proptest! {
|
||||||
}
|
}
|
||||||
.into())
|
.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_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));
|
prop_assert!(non_finalized_state.eq_internal_state(&previous_mem));
|
||||||
}
|
}
|
||||||
|
@ -534,7 +561,9 @@ proptest! {
|
||||||
let block1 = Arc::new(block1).prepare();
|
let block1 = Arc::new(block1).prepare();
|
||||||
let commit_result = validate_and_commit_non_finalized(
|
let commit_result = validate_and_commit_non_finalized(
|
||||||
&finalized_state,
|
&finalized_state,
|
||||||
&mut non_finalized_state, block1);
|
&mut non_finalized_state,
|
||||||
|
block1
|
||||||
|
);
|
||||||
|
|
||||||
prop_assert_eq!(
|
prop_assert_eq!(
|
||||||
commit_result,
|
commit_result,
|
||||||
|
@ -572,14 +601,14 @@ proptest! {
|
||||||
spend2.nullifier = duplicate_nullifier;
|
spend2.nullifier = duplicate_nullifier;
|
||||||
|
|
||||||
let transaction1 =
|
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 =
|
let transaction2 =
|
||||||
transaction_v4_with_sapling_shielded_data(sapling_shielded_data2.0, [spend2.0]);
|
transaction_v4_with_sapling_shielded_data(sapling_shielded_data2.0, [spend2.0]);
|
||||||
|
|
||||||
block1.transactions[0] = transaction_v4_from_coinbase(&block1.transactions[0]).into();
|
block1.transactions[0] = transaction_v4_from_coinbase(&block1.transactions[0]).into();
|
||||||
block2.transactions[0] = transaction_v4_from_coinbase(&block2.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());
|
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();
|
||||||
|
@ -590,6 +619,11 @@ proptest! {
|
||||||
|
|
||||||
let mut previous_mem = non_finalized_state.clone();
|
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;
|
let block1_hash;
|
||||||
// randomly choose to commit the next block to the finalized or non-finalized state
|
// randomly choose to commit the next block to the finalized or non-finalized state
|
||||||
if duplicate_in_finalized_state {
|
if duplicate_in_finalized_state {
|
||||||
|
@ -632,6 +666,18 @@ proptest! {
|
||||||
}
|
}
|
||||||
.into())
|
.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_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));
|
prop_assert!(non_finalized_state.eq_internal_state(&previous_mem));
|
||||||
}
|
}
|
||||||
|
@ -829,10 +875,10 @@ proptest! {
|
||||||
let duplicate_nullifier = authorized_action1.action.nullifier;
|
let duplicate_nullifier = authorized_action1.action.nullifier;
|
||||||
authorized_action2.action.nullifier = duplicate_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,
|
orchard_shielded_data1.0,
|
||||||
[authorized_action1.0],
|
[authorized_action1.0],
|
||||||
);
|
));
|
||||||
let transaction2 = transaction_v5_with_orchard_shielded_data(
|
let transaction2 = transaction_v5_with_orchard_shielded_data(
|
||||||
orchard_shielded_data2.0,
|
orchard_shielded_data2.0,
|
||||||
[authorized_action2.0],
|
[authorized_action2.0],
|
||||||
|
@ -841,7 +887,7 @@ proptest! {
|
||||||
block1.transactions[0] = transaction_v4_from_coinbase(&block1.transactions[0]).into();
|
block1.transactions[0] = transaction_v4_from_coinbase(&block1.transactions[0]).into();
|
||||||
block2.transactions[0] = transaction_v4_from_coinbase(&block2.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());
|
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();
|
||||||
|
@ -852,6 +898,11 @@ proptest! {
|
||||||
|
|
||||||
let mut previous_mem = non_finalized_state.clone();
|
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;
|
let block1_hash;
|
||||||
// randomly choose to commit the next block to the finalized or non-finalized state
|
// randomly choose to commit the next block to the finalized or non-finalized state
|
||||||
if duplicate_in_finalized_state {
|
if duplicate_in_finalized_state {
|
||||||
|
@ -893,6 +944,18 @@ proptest! {
|
||||||
}
|
}
|
||||||
.into())
|
.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_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));
|
prop_assert!(non_finalized_state.eq_internal_state(&previous_mem));
|
||||||
}
|
}
|
||||||
|
|
|
@ -226,15 +226,18 @@ impl NonFinalizedState {
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
// Reads from disk
|
// Reads from disk
|
||||||
check::anchors::sapling_orchard_anchors_refer_to_final_treestates(
|
check::anchors::block_sapling_orchard_anchors_refer_to_final_treestates(
|
||||||
finalized_state,
|
finalized_state,
|
||||||
&new_chain,
|
&new_chain,
|
||||||
&prepared,
|
&prepared,
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
// Reads from disk
|
// Reads from disk
|
||||||
let sprout_final_treestates =
|
let sprout_final_treestates = check::anchors::block_fetch_sprout_final_treestates(
|
||||||
check::anchors::fetch_sprout_final_treestates(finalized_state, &new_chain, &prepared);
|
finalized_state,
|
||||||
|
&new_chain,
|
||||||
|
&prepared,
|
||||||
|
);
|
||||||
|
|
||||||
// Quick check that doesn't read from disk
|
// Quick check that doesn't read from disk
|
||||||
let contextual = ContextuallyValidBlock::with_block_and_spent_utxos(
|
let contextual = ContextuallyValidBlock::with_block_and_spent_utxos(
|
||||||
|
@ -285,11 +288,12 @@ impl NonFinalizedState {
|
||||||
});
|
});
|
||||||
|
|
||||||
scope.spawn_fifo(|_scope| {
|
scope.spawn_fifo(|_scope| {
|
||||||
sprout_anchor_result = Some(check::anchors::sprout_anchors_refer_to_treestates(
|
sprout_anchor_result =
|
||||||
|
Some(check::anchors::block_sprout_anchors_refer_to_treestates(
|
||||||
sprout_final_treestates,
|
sprout_final_treestates,
|
||||||
block2,
|
block2,
|
||||||
height,
|
|
||||||
transaction_hashes,
|
transaction_hashes,
|
||||||
|
height,
|
||||||
));
|
));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -105,11 +105,11 @@ pub struct Chain {
|
||||||
pub(crate) orchard_anchors_by_height: BTreeMap<block::Height, orchard::tree::Root>,
|
pub(crate) orchard_anchors_by_height: BTreeMap<block::Height, orchard::tree::Root>,
|
||||||
|
|
||||||
/// The Sprout nullifiers revealed by `blocks`.
|
/// 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`.
|
/// 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`.
|
/// 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`.
|
/// Partial transparent address index data from `blocks`.
|
||||||
pub(super) partial_transparent_transfers: HashMap<transparent::Address, TransparentTransfers>,
|
pub(super) partial_transparent_transfers: HashMap<transparent::Address, TransparentTransfers>,
|
||||||
|
@ -410,12 +410,14 @@ impl Chain {
|
||||||
.expect("Orchard anchors must exist for pre-fork blocks");
|
.expect("Orchard anchors must exist for pre-fork blocks");
|
||||||
|
|
||||||
let history_tree_mut = Arc::make_mut(&mut self.history_tree);
|
let history_tree_mut = Arc::make_mut(&mut self.history_tree);
|
||||||
history_tree_mut.push(
|
history_tree_mut
|
||||||
|
.push(
|
||||||
self.network,
|
self.network,
|
||||||
block.block.clone(),
|
block.block.clone(),
|
||||||
*sapling_root,
|
*sapling_root,
|
||||||
*orchard_root,
|
*orchard_root,
|
||||||
)?;
|
)
|
||||||
|
.map_err(Arc::new)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
|
@ -909,12 +911,14 @@ impl Chain {
|
||||||
|
|
||||||
// TODO: update the history trees in a rayon thread, if they show up in CPU profiles
|
// 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);
|
let history_tree_mut = Arc::make_mut(&mut self.history_tree);
|
||||||
history_tree_mut.push(
|
history_tree_mut
|
||||||
|
.push(
|
||||||
self.network,
|
self.network,
|
||||||
contextually_valid.block.clone(),
|
contextually_valid.block.clone(),
|
||||||
sapling_root,
|
sapling_root,
|
||||||
orchard_root,
|
orchard_root,
|
||||||
)?;
|
)
|
||||||
|
.map_err(Arc::new)?;
|
||||||
|
|
||||||
self.history_trees_by_height
|
self.history_trees_by_height
|
||||||
.insert(height, self.history_tree.clone());
|
.insert(height, self.history_tree.clone());
|
||||||
|
|
|
@ -163,7 +163,7 @@ async fn mempool_push_transaction() -> Result<(), crate::BoxError> {
|
||||||
let transaction = responder
|
let transaction = responder
|
||||||
.request()
|
.request()
|
||||||
.clone()
|
.clone()
|
||||||
.into_mempool_transaction()
|
.mempool_transaction()
|
||||||
.expect("unexpected non-mempool request");
|
.expect("unexpected non-mempool request");
|
||||||
|
|
||||||
// Set a dummy fee and sigops.
|
// Set a dummy fee and sigops.
|
||||||
|
@ -267,7 +267,7 @@ async fn mempool_advertise_transaction_ids() -> Result<(), crate::BoxError> {
|
||||||
let transaction = responder
|
let transaction = responder
|
||||||
.request()
|
.request()
|
||||||
.clone()
|
.clone()
|
||||||
.into_mempool_transaction()
|
.mempool_transaction()
|
||||||
.expect("unexpected non-mempool request");
|
.expect("unexpected non-mempool request");
|
||||||
|
|
||||||
// Set a dummy fee and sigops.
|
// Set a dummy fee and sigops.
|
||||||
|
@ -368,7 +368,7 @@ async fn mempool_transaction_expiration() -> Result<(), crate::BoxError> {
|
||||||
let transaction = responder
|
let transaction = responder
|
||||||
.request()
|
.request()
|
||||||
.clone()
|
.clone()
|
||||||
.into_mempool_transaction()
|
.mempool_transaction()
|
||||||
.expect("unexpected non-mempool request");
|
.expect("unexpected non-mempool request");
|
||||||
|
|
||||||
// Set a dummy fee and sigops.
|
// Set a dummy fee and sigops.
|
||||||
|
@ -502,7 +502,7 @@ async fn mempool_transaction_expiration() -> Result<(), crate::BoxError> {
|
||||||
let transaction = responder
|
let transaction = responder
|
||||||
.request()
|
.request()
|
||||||
.clone()
|
.clone()
|
||||||
.into_mempool_transaction()
|
.mempool_transaction()
|
||||||
.expect("unexpected non-mempool request");
|
.expect("unexpected non-mempool request");
|
||||||
|
|
||||||
// Set a dummy fee and sigops.
|
// Set a dummy fee and sigops.
|
||||||
|
|
|
@ -271,10 +271,10 @@ where
|
||||||
let mut state = self.state.clone();
|
let mut state = self.state.clone();
|
||||||
|
|
||||||
let fut = async move {
|
let fut = async move {
|
||||||
// Don't download/verify if the transaction is already in the state.
|
// Don't download/verify if the transaction is already in the best chain.
|
||||||
Self::transaction_in_state(&mut state, txid).await?;
|
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 {
|
let next_height = match state.oneshot(zs::Request::Tip).await {
|
||||||
Ok(zs::Response::Tip(None)) => Ok(Height(0)),
|
Ok(zs::Response::Tip(None)) => Ok(Height(0)),
|
||||||
|
@ -442,12 +442,11 @@ where
|
||||||
self.pending.len()
|
self.pending.len()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Check if transaction is already in the state.
|
/// Check if transaction is already in the best chain.
|
||||||
async fn transaction_in_state(
|
async fn transaction_in_best_chain(
|
||||||
state: &mut ZS,
|
state: &mut ZS,
|
||||||
txid: UnminedTxId,
|
txid: UnminedTxId,
|
||||||
) -> Result<(), TransactionDownloadVerifyError> {
|
) -> Result<(), TransactionDownloadVerifyError> {
|
||||||
// Check if the transaction is already in the state.
|
|
||||||
match state
|
match state
|
||||||
.ready()
|
.ready()
|
||||||
.await
|
.await
|
||||||
|
|
Loading…
Reference in New Issue