Skip to main content

zebra_state/service/
check.rs

1//! Consensus critical contextual checks
2
3use std::{borrow::Borrow, sync::Arc};
4
5use chrono::Duration;
6
7use zebra_chain::{
8    block::{self, Block, ChainHistoryBlockTxAuthCommitmentHash, CommitmentError},
9    history_tree::HistoryTree,
10    parameters::{Network, NetworkUpgrade},
11    work::difficulty::CompactDifficulty,
12};
13
14use crate::{
15    service::{
16        block_iter::any_ancestor_blocks, check::difficulty::POW_ADJUSTMENT_BLOCK_SPAN,
17        finalized_state::ZebraDb, non_finalized_state::NonFinalizedState,
18    },
19    BoxError, SemanticallyVerifiedBlock, ValidateContextError,
20};
21
22// use self as check
23use super::check;
24
25// These types are used in doc links
26#[allow(unused_imports)]
27use crate::service::non_finalized_state::Chain;
28
29pub(crate) mod anchors;
30pub(crate) mod difficulty;
31pub(crate) mod nullifier;
32pub(crate) mod utxo;
33
34pub use utxo::transparent_coinbase_spend;
35
36#[cfg(test)]
37mod tests;
38
39pub(crate) use difficulty::AdjustedDifficulty;
40
41/// Check that the semantically verified block is contextually valid for `network`,
42/// based on the `finalized_tip_height` and `relevant_chain`.
43///
44/// This function performs checks that require a small number of recent blocks,
45/// including previous hash, previous height, and block difficulty.
46///
47/// The relevant chain is an iterator over the ancestors of `block`, starting
48/// with its parent block.
49#[tracing::instrument(skip(semantically_verified, finalized_tip_height, relevant_chain))]
50pub(crate) fn block_is_valid_for_recent_chain<C>(
51    semantically_verified: &SemanticallyVerifiedBlock,
52    network: &Network,
53    finalized_tip_height: Option<block::Height>,
54    relevant_chain: C,
55) -> Result<(), ValidateContextError>
56where
57    C: IntoIterator,
58    C::Item: Borrow<Block>,
59    C::IntoIter: ExactSizeIterator,
60{
61    let finalized_tip_height = finalized_tip_height
62        .expect("finalized state must contain at least one block to do contextual validation");
63    check::block_is_not_orphaned(finalized_tip_height, semantically_verified.height)?;
64
65    let relevant_chain: Vec<_> = relevant_chain
66        .into_iter()
67        .take(POW_ADJUSTMENT_BLOCK_SPAN)
68        .collect();
69
70    let Some(parent_block) = relevant_chain.first() else {
71        warn!(
72            ?semantically_verified,
73            ?finalized_tip_height,
74            "state must contain parent block to do contextual validation"
75        );
76
77        return Err(ValidateContextError::NotReadyToBeCommitted);
78    };
79
80    let parent_block = parent_block.borrow();
81    let parent_height = parent_block
82        .coinbase_height()
83        .expect("valid blocks have a coinbase height");
84    check::height_one_more_than_parent_height(parent_height, semantically_verified.height)?;
85
86    // skip this check during tests if we don't have enough blocks in the chain
87    // process_queued also checks the chain length, so we can skip this assertion during testing
88    // (tests that want to check this code should use the correct number of blocks)
89    //
90    // TODO: accept a NotReadyToBeCommitted error in those tests instead
91    #[cfg(test)]
92    if relevant_chain.len() < POW_ADJUSTMENT_BLOCK_SPAN {
93        return Ok(());
94    }
95
96    // In production, blocks without enough context are invalid.
97    //
98    // The BlockVerifierRouter makes sure that the first 1 million blocks (or more) are
99    // checkpoint verified. The state queues and block write task make sure that blocks are
100    // committed in strict height order. But this function is only called on semantically
101    // verified blocks, so there will be at least 1 million blocks in the state when it is
102    // called. So this error should never happen on Mainnet or the default Testnet.
103    //
104    // It's okay to use a relevant chain of fewer than `POW_ADJUSTMENT_BLOCK_SPAN` blocks, because
105    // the MedianTime function uses height 0 if passed a negative height by the ActualTimespan function:
106    // > ActualTimespan(height : N) := MedianTime(height) − MedianTime(height − PoWAveragingWindow)
107    // > MedianTime(height : N) := median([[ nTime(𝑖) for 𝑖 from max(0, height − PoWMedianBlockSpan) up to height − 1 ]])
108    // and the MeanTarget function only requires the past `PoWAveragingWindow` (17) blocks for heights above 17,
109    // > PoWLimit, if height ≤ PoWAveragingWindow
110    // > ([ToTarget(nBits(𝑖)) for 𝑖 from height−PoWAveragingWindow up to height−1]) otherwise
111    //
112    // See the 'Difficulty Adjustment' section (page 132) in the Zcash specification.
113    #[cfg(not(test))]
114    if relevant_chain.is_empty() {
115        return Err(ValidateContextError::NotReadyToBeCommitted);
116    }
117
118    let relevant_data = relevant_chain.iter().map(|block| {
119        (
120            block.borrow().header.difficulty_threshold,
121            block.borrow().header.time,
122        )
123    });
124    let difficulty_adjustment =
125        AdjustedDifficulty::new_from_block(&semantically_verified.block, network, relevant_data);
126    check::difficulty_threshold_and_time_are_valid(
127        semantically_verified.block.header.difficulty_threshold,
128        difficulty_adjustment,
129    )?;
130
131    Ok(())
132}
133
134/// Check that `block` is contextually valid for `network`, using
135/// the `history_tree` up to and including the previous block.
136#[tracing::instrument(skip(block, history_tree))]
137pub(crate) fn block_commitment_is_valid_for_chain_history(
138    block: Arc<Block>,
139    network: &Network,
140    history_tree: &HistoryTree,
141) -> Result<(), ValidateContextError> {
142    match block.commitment(network)? {
143        block::Commitment::PreSaplingReserved(_)
144        | block::Commitment::FinalSaplingRoot(_)
145        | block::Commitment::ChainHistoryActivationReserved => {
146            // # Consensus
147            //
148            // > [Sapling and Blossom only, pre-Heartwood] hashLightClientRoot MUST
149            // > be LEBS2OSP_{256}(rt^{Sapling}) where rt^{Sapling} is the root of
150            // > the Sapling note commitment tree for the final Sapling treestate of
151            // > this block .
152            //
153            // https://zips.z.cash/protocol/protocol.pdf#blockheader
154            //
155            // We don't need to validate this rule since we checkpoint on Canopy.
156            //
157            // We also don't need to do anything in the other cases.
158            Ok(())
159        }
160        block::Commitment::ChainHistoryRoot(actual_history_tree_root) => {
161            // # Consensus
162            //
163            // > [Heartwood and Canopy only, pre-NU5] hashLightClientRoot MUST be set to the
164            // > hashChainHistoryRoot for this block , as specified in [ZIP-221].
165            //
166            // https://zips.z.cash/protocol/protocol.pdf#blockheader
167            //
168            // The network is checked by [`Block::commitment`] above; it will only
169            // return the chain history root if it's Heartwood or Canopy.
170            let history_tree_root = history_tree
171                .hash()
172                .expect("the history tree of the previous block must exist since the current block has a ChainHistoryRoot");
173            if actual_history_tree_root == history_tree_root {
174                Ok(())
175            } else {
176                Err(ValidateContextError::InvalidBlockCommitment(
177                    CommitmentError::InvalidChainHistoryRoot {
178                        actual: actual_history_tree_root.into(),
179                        expected: history_tree_root.into(),
180                    },
181                ))
182            }
183        }
184        block::Commitment::ChainHistoryBlockTxAuthCommitment(actual_hash_block_commitments) => {
185            // # Consensus
186            //
187            // > [NU5 onward] hashBlockCommitments MUST be set to the value of
188            // > hashBlockCommitments for this block, as specified in [ZIP-244].
189            //
190            // The network is checked by [`Block::commitment`] above; it will only
191            // return the block commitments if it's NU5 onward.
192            let history_tree_root = history_tree
193                .hash()
194                .or_else(|| {
195                    (NetworkUpgrade::Heartwood.activation_height(network)
196                        == block.coinbase_height())
197                    .then_some(block::CHAIN_HISTORY_ACTIVATION_RESERVED.into())
198                })
199                .expect(
200                    "the history tree of the previous block must exist \
201                 since the current block has a ChainHistoryBlockTxAuthCommitment",
202                );
203            let auth_data_root = block.auth_data_root();
204
205            let hash_block_commitments = ChainHistoryBlockTxAuthCommitmentHash::from_commitments(
206                &history_tree_root,
207                &auth_data_root,
208            );
209
210            if actual_hash_block_commitments == hash_block_commitments {
211                Ok(())
212            } else {
213                Err(ValidateContextError::InvalidBlockCommitment(
214                    CommitmentError::InvalidChainHistoryBlockTxAuthCommitment {
215                        actual: actual_hash_block_commitments.into(),
216                        expected: hash_block_commitments.into(),
217                    },
218                ))
219            }
220        }
221    }
222}
223
224/// Returns `ValidateContextError::OrphanedBlock` if the height of the given
225/// block is less than or equal to the finalized tip height.
226fn block_is_not_orphaned(
227    finalized_tip_height: block::Height,
228    candidate_height: block::Height,
229) -> Result<(), ValidateContextError> {
230    if candidate_height <= finalized_tip_height {
231        Err(ValidateContextError::OrphanedBlock {
232            candidate_height,
233            finalized_tip_height,
234        })
235    } else {
236        Ok(())
237    }
238}
239
240/// Returns `ValidateContextError::NonSequentialBlock` if the block height isn't
241/// equal to the parent_height+1.
242fn height_one_more_than_parent_height(
243    parent_height: block::Height,
244    candidate_height: block::Height,
245) -> Result<(), ValidateContextError> {
246    if parent_height + 1 != Some(candidate_height) {
247        Err(ValidateContextError::NonSequentialBlock {
248            candidate_height,
249            parent_height,
250        })
251    } else {
252        Ok(())
253    }
254}
255
256/// Validate the time and `difficulty_threshold` from a candidate block's
257/// header.
258///
259/// Uses the `difficulty_adjustment` context for the block to:
260///   * check that the candidate block's time is within the valid range,
261///     based on the network and  candidate height, and
262///   * check that the expected difficulty is equal to the block's
263///     `difficulty_threshold`.
264///
265/// These checks are performed together, because the time field is used to
266/// calculate the expected difficulty adjustment.
267fn difficulty_threshold_and_time_are_valid(
268    difficulty_threshold: CompactDifficulty,
269    difficulty_adjustment: AdjustedDifficulty,
270) -> Result<(), ValidateContextError> {
271    // Check the block header time consensus rules from the Zcash specification
272    let candidate_height = difficulty_adjustment.candidate_height();
273    let candidate_time = difficulty_adjustment.candidate_time();
274    let network = difficulty_adjustment.network();
275    let median_time_past = difficulty_adjustment.median_time_past();
276    let block_time_max =
277        median_time_past + Duration::seconds(difficulty::BLOCK_MAX_TIME_SINCE_MEDIAN.into());
278
279    // # Consensus
280    //
281    // > For each block other than the genesis block, `nTime` MUST be strictly greater
282    // than the median-time-past of that block.
283    //
284    // https://zips.z.cash/protocol/protocol.pdf#blockheader
285    let genesis_height = NetworkUpgrade::Genesis
286        .activation_height(&network)
287        .expect("Zebra always has a genesis height available");
288
289    if candidate_time <= median_time_past && candidate_height != genesis_height {
290        Err(ValidateContextError::TimeTooEarly {
291            candidate_time,
292            median_time_past,
293        })?
294    }
295
296    // # Consensus
297    //
298    // > For each block at block height 2 or greater on Mainnet, or block height 653_606
299    // or greater on Testnet, `nTime` MUST be less than or equal to the median-time-past
300    // of that block plus 90*60 seconds.
301    //
302    // https://zips.z.cash/protocol/protocol.pdf#blockheader
303    if network.is_max_block_time_enforced(candidate_height) && candidate_time > block_time_max {
304        Err(ValidateContextError::TimeTooLate {
305            candidate_time,
306            block_time_max,
307        })?
308    }
309
310    // # Consensus
311    //
312    // > For a block at block height `Height`, `nBits` MUST be equal to `ThresholdBits(Height)`.
313    //
314    // https://zips.z.cash/protocol/protocol.pdf#blockheader
315    let expected_difficulty = difficulty_adjustment.expected_difficulty_threshold();
316    if difficulty_threshold != expected_difficulty {
317        Err(ValidateContextError::InvalidDifficultyThreshold {
318            difficulty_threshold,
319            expected_difficulty,
320        })?
321    }
322
323    Ok(())
324}
325
326/// Check if zebra is following a legacy chain and return an error if so.
327///
328/// `nu5_activation_height` should be `NetworkUpgrade::Nu5.activation_height(network)`, and
329/// `max_legacy_chain_blocks` should be [`MAX_LEGACY_CHAIN_BLOCKS`](crate::constants::MAX_LEGACY_CHAIN_BLOCKS).
330/// They are only changed from the defaults for testing.
331pub(crate) fn legacy_chain<I>(
332    nu5_activation_height: block::Height,
333    ancestors: I,
334    network: &Network,
335    max_legacy_chain_blocks: usize,
336) -> Result<(), BoxError>
337where
338    I: Iterator<Item = Arc<Block>>,
339{
340    let mut ancestors = ancestors.peekable();
341    let tip_height = ancestors.peek().and_then(|block| block.coinbase_height());
342
343    for (index, block) in ancestors.enumerate() {
344        // Stop checking if the chain reaches Canopy. We won't find any more V5 transactions,
345        // so the rest of our checks are useless.
346        //
347        // If the cached tip is close to NU5 activation, but there aren't any V5 transactions in the
348        // chain yet, we could reach MAX_BLOCKS_TO_CHECK in Canopy, and incorrectly return an error.
349        if block
350            .coinbase_height()
351            .expect("valid blocks have coinbase heights")
352            < nu5_activation_height
353        {
354            return Ok(());
355        }
356
357        // If we are past our NU5 activation height, but there are no V5 transactions in recent blocks,
358        // the last Zebra instance that updated this cached state had no NU5 activation height.
359        if index >= max_legacy_chain_blocks {
360            return Err(format!(
361                "could not find any transactions in recent blocks: \
362                 checked {index} blocks back from {:?}",
363                tip_height.expect("database contains valid blocks"),
364            )
365            .into());
366        }
367
368        // If a transaction `network_upgrade` field is different from the network upgrade calculated
369        // using our activation heights, the Zebra instance that verified those blocks had different
370        // network upgrade heights.
371        block
372            .check_transaction_network_upgrade_consistency(network)
373            .map_err(|error| {
374                format!("inconsistent network upgrade found in transaction: {error:?}")
375            })?;
376
377        // If we find at least one transaction with a valid `network_upgrade` field, the Zebra instance that
378        // verified those blocks used the same network upgrade heights. (Up to this point in the chain.)
379        let has_network_upgrade = block
380            .transactions
381            .iter()
382            .find_map(|trans| trans.network_upgrade())
383            .is_some();
384        if has_network_upgrade {
385            return Ok(());
386        }
387    }
388
389    Ok(())
390}
391
392/// Perform initial contextual validity checks for the configured network,
393/// based on the committed finalized and non-finalized state.
394///
395/// Additional contextual validity checks are performed by the non-finalized [`Chain`].
396pub(crate) fn initial_contextual_validity(
397    finalized_state: &ZebraDb,
398    non_finalized_state: &NonFinalizedState,
399    semantically_verified: &SemanticallyVerifiedBlock,
400) -> Result<(), ValidateContextError> {
401    let relevant_chain = any_ancestor_blocks(
402        non_finalized_state,
403        finalized_state,
404        semantically_verified.block.header.previous_block_hash,
405    );
406
407    // Security: check proof of work before any other checks
408    check::block_is_valid_for_recent_chain(
409        semantically_verified,
410        &non_finalized_state.network,
411        finalized_state.finalized_tip_height(),
412        relevant_chain,
413    )?;
414
415    check::nullifier::no_duplicates_in_finalized_chain(semantically_verified, finalized_state)?;
416
417    Ok(())
418}