Skip to main content

zebra_state/service/check/
nullifier.rs

1//! Checks for nullifier uniqueness.
2
3use std::{collections::HashMap, sync::Arc};
4
5use tracing::trace;
6use zebra_chain::transaction::Transaction;
7
8use crate::{
9    error::DuplicateNullifierError,
10    service::{
11        finalized_state::ZebraDb,
12        non_finalized_state::{Chain, SpendingTransactionId},
13    },
14    SemanticallyVerifiedBlock, ValidateContextError,
15};
16
17// Tidy up some doc links
18#[allow(unused_imports)]
19use crate::service;
20
21/// Reject double-spends of nullifiers:
22/// - one from this [`SemanticallyVerifiedBlock`], and the other already committed to the
23///   [`FinalizedState`](service::FinalizedState).
24///
25/// (Duplicate non-finalized nullifiers are rejected during the chain update,
26/// see [`add_to_non_finalized_chain_unique`] for details.)
27///
28/// # Consensus
29///
30/// > A nullifier MUST NOT repeat either within a transaction,
31/// > or across transactions in a valid blockchain.
32/// > Sprout, Sapling, Orchard, and Ironwood nullifiers are considered disjoint,
33/// > even if they have the same bit pattern.
34///
35/// <https://zips.z.cash/protocol/protocol.pdf#nullifierset>
36#[tracing::instrument(skip(semantically_verified, finalized_state))]
37pub(crate) fn no_duplicates_in_finalized_chain(
38    semantically_verified: &SemanticallyVerifiedBlock,
39    finalized_state: &ZebraDb,
40) -> Result<(), ValidateContextError> {
41    for nullifier in semantically_verified.block.sprout_nullifiers() {
42        if finalized_state.contains_sprout_nullifier(nullifier) {
43            Err(nullifier.duplicate_nullifier_error(true))?;
44        }
45    }
46
47    for nullifier in semantically_verified.block.sapling_nullifiers() {
48        if finalized_state.contains_sapling_nullifier(nullifier) {
49            Err(nullifier.duplicate_nullifier_error(true))?;
50        }
51    }
52
53    for nullifier in semantically_verified.block.orchard_nullifiers() {
54        if finalized_state.contains_orchard_nullifier(nullifier) {
55            Err(nullifier.duplicate_nullifier_error(true))?;
56        }
57    }
58
59    for nullifier in semantically_verified.block.ironwood_nullifiers() {
60        if finalized_state.contains_ironwood_nullifier(&nullifier) {
61            Err(nullifier.duplicate_nullifier_error(true))?;
62        }
63    }
64
65    Ok(())
66}
67
68/// Accepts an iterator of revealed nullifiers, a predicate fn for checking if a nullifier is in
69/// in the finalized chain, and a predicate fn for checking if the nullifier is in the non-finalized chain
70///
71/// Returns `Err(DuplicateNullifierError)` if any of the `revealed_nullifiers` are found in the
72/// non-finalized or finalized chains.
73///
74/// Returns `Ok(())` if all the `revealed_nullifiers` have not been seen in either chain.
75fn find_duplicate_nullifier<NullifierT, FinalizedStateContainsFn, NonFinalizedStateContainsFn>(
76    revealed_nullifiers: impl IntoIterator<Item = NullifierT>,
77    finalized_chain_contains: FinalizedStateContainsFn,
78    non_finalized_chain_contains: Option<NonFinalizedStateContainsFn>,
79) -> Result<(), ValidateContextError>
80where
81    NullifierT: DuplicateNullifierError + Copy,
82    FinalizedStateContainsFn: Fn(NullifierT) -> bool,
83    NonFinalizedStateContainsFn: Fn(NullifierT) -> bool,
84{
85    for nullifier in revealed_nullifiers {
86        if let Some(true) = non_finalized_chain_contains.as_ref().map(|f| f(nullifier)) {
87            Err(nullifier.duplicate_nullifier_error(false))?
88        } else if finalized_chain_contains(nullifier) {
89            Err(nullifier.duplicate_nullifier_error(true))?
90        }
91    }
92
93    Ok(())
94}
95
96/// Reject double-spends of nullifiers:
97/// - one from this [`Transaction`], and the other already committed to the
98///   provided non-finalized [`Chain`] or [`ZebraDb`].
99///
100/// # Consensus
101///
102/// > A nullifier MUST NOT repeat either within a transaction,
103/// > or across transactions in a valid blockchain.
104/// > Sprout, Sapling, Orchard, and Ironwood nullifiers are considered disjoint,
105/// > even if they have the same bit pattern.
106///
107/// <https://zips.z.cash/protocol/protocol.pdf#nullifierset>
108#[tracing::instrument(skip_all)]
109pub(crate) fn tx_no_duplicates_in_chain(
110    finalized_chain: &ZebraDb,
111    non_finalized_chain: Option<&Arc<Chain>>,
112    transaction: &Arc<Transaction>,
113) -> Result<(), ValidateContextError> {
114    find_duplicate_nullifier(
115        transaction.sprout_nullifiers().copied(),
116        |nullifier| finalized_chain.contains_sprout_nullifier(&nullifier),
117        non_finalized_chain
118            .map(|chain| move |nullifier| chain.sprout_nullifiers.contains_key(&nullifier)),
119    )?;
120
121    find_duplicate_nullifier(
122        transaction.sapling_nullifiers().copied(),
123        |nullifier| finalized_chain.contains_sapling_nullifier(&nullifier),
124        non_finalized_chain
125            .map(|chain| move |nullifier| chain.sapling_nullifiers.contains_key(&nullifier)),
126    )?;
127
128    find_duplicate_nullifier(
129        transaction.orchard_nullifiers().copied(),
130        |nullifier| finalized_chain.contains_orchard_nullifier(&nullifier),
131        non_finalized_chain
132            .map(|chain| move |nullifier| chain.orchard_nullifiers.contains_key(&nullifier)),
133    )?;
134
135    // `Transaction::ironwood_nullifiers` already yields owned `ironwood::Nullifier`s.
136    find_duplicate_nullifier(
137        transaction.ironwood_nullifiers(),
138        |nullifier| finalized_chain.contains_ironwood_nullifier(&nullifier),
139        non_finalized_chain
140            .map(|chain| move |nullifier| chain.ironwood_nullifiers.contains_key(&nullifier)),
141    )?;
142
143    Ok(())
144}
145
146/// Reject double-spends of nullifiers:
147/// - both within the same `JoinSplit` (sprout only),
148/// - from different `JoinSplit`s, [`sapling::Spend`](zebra_chain::sapling::Spend)s or
149///   [`orchard::Action`](zebra_chain::orchard::Action)s in this
150///   [`Transaction`]'s shielded data, or
151/// - one from this shielded data, and another from:
152///   - a previous transaction in this [`Block`](zebra_chain::block::Block), or
153///   - a previous block in this non-finalized
154///     [`Chain`].
155///
156/// (Duplicate finalized nullifiers are rejected during service contextual validation,
157/// see [`no_duplicates_in_finalized_chain`] for details.)
158///
159/// # Consensus
160///
161/// > A nullifier MUST NOT repeat either within a transaction,
162/// > or across transactions in a valid blockchain.
163/// > Sprout, Sapling, Orchard, and Ironwood nullifiers are considered disjoint,
164/// > even if they have the same bit pattern.
165///
166/// <https://zips.z.cash/protocol/protocol.pdf#nullifierset>
167///
168/// We comply with the "disjoint" rule by storing the nullifiers for each
169/// pool in separate sets (also with different types), so that even if
170/// different pools have nullifiers with same bit pattern, they won't be
171/// considered the same when determining uniqueness. This is enforced by the
172/// callers of this function.
173#[tracing::instrument(skip(chain_nullifiers, shielded_data_nullifiers))]
174pub(crate) fn add_to_non_finalized_chain_unique<NullifierT>(
175    chain_nullifiers: &mut HashMap<NullifierT, SpendingTransactionId>,
176    shielded_data_nullifiers: impl IntoIterator<Item = NullifierT>,
177    revealing_tx_id: SpendingTransactionId,
178) -> Result<(), ValidateContextError>
179where
180    NullifierT: DuplicateNullifierError + Copy + std::fmt::Debug + Eq + std::hash::Hash,
181{
182    for nullifier in shielded_data_nullifiers.into_iter() {
183        trace!(?nullifier, "adding nullifier");
184
185        // reject the nullifier if it is already present in this non-finalized chain
186        if chain_nullifiers
187            .insert(nullifier, revealing_tx_id)
188            .is_some()
189        {
190            Err(nullifier.duplicate_nullifier_error(false))?;
191        }
192    }
193
194    Ok(())
195}
196
197/// Remove nullifiers that were previously added to this non-finalized
198/// [`Chain`] by this shielded data.
199///
200/// "A note can change from being unspent to spent as a node’s view
201/// of the best valid block chain is extended by new transactions.
202///
203/// Also, block chain reorganizations can cause a node to switch
204/// to a different best valid block chain that does not contain
205/// the transaction in which a note was output"
206///
207/// <https://zips.z.cash/protocol/nu5.pdf#decryptivk>
208///
209/// Note: reorganizations can also change the best chain to one
210/// where a note was unspent, rather than spent.
211///
212/// # Panics
213///
214/// Panics if any nullifier is missing from the chain when we try to remove it.
215///
216/// Blocks with duplicate nullifiers are rejected by
217/// [`add_to_non_finalized_chain_unique`], so this shielded data should be the
218/// only shielded data that added this nullifier to this
219/// [`Chain`].
220#[tracing::instrument(skip(chain_nullifiers, shielded_data_nullifiers))]
221pub(crate) fn remove_from_non_finalized_chain<NullifierT>(
222    chain_nullifiers: &mut HashMap<NullifierT, SpendingTransactionId>,
223    shielded_data_nullifiers: impl IntoIterator<Item = NullifierT>,
224) where
225    NullifierT: std::fmt::Debug + Eq + std::hash::Hash,
226{
227    for nullifier in shielded_data_nullifiers.into_iter() {
228        trace!(?nullifier, "removing nullifier");
229
230        assert!(
231            chain_nullifiers.remove(&nullifier).is_some(),
232            "nullifier must be present if block was added to chain"
233        );
234    }
235}