Skip to main content

hotshot_new_protocol/
utils.rs

1use alloy::primitives::U256;
2use anyhow::{anyhow, ensure};
3use committable::Committable;
4use hotshot_types::{
5    PeerConfig,
6    data::Leaf2,
7    message::UpgradeLock,
8    stake_table::StakeTableEntries,
9    traits::node_implementation::NodeType,
10    vote::{Certificate, HasViewNumber},
11};
12
13use crate::message::Certificate2;
14
15/// Verify that a leaf is finalized by a new-protocol Certificate2.
16///
17/// `cert2` directly commits the newest leaf in `leaf_chain`. By the indirect
18/// commit rule, every ancestor of that leaf is finalized as well. This verifier
19/// validates `cert2`, then walks backward through the certified leaf's parent
20/// links until it finds `expected_height`.
21///
22/// View numbers are allowed to skip after timeouts, so the input may contain
23/// leaves that are not on the certified ancestry path. Those leaves are ignored;
24/// every accepted step must match the current leaf's justify QC, parent
25/// commitment, and block height.
26pub async fn verify_leaf_chain_with_cert2<T: NodeType>(
27    mut leaf_chain: Vec<Leaf2<T>>,
28    stake_table: &[PeerConfig<T>],
29    success_threshold: U256,
30    expected_height: u64,
31    upgrade_lock: &UpgradeLock<T>,
32    cert2: Certificate2<T>,
33) -> anyhow::Result<Leaf2<T>> {
34    leaf_chain.sort_by_key(|l| l.view_number());
35    leaf_chain.reverse();
36
37    ensure!(!leaf_chain.is_empty(), "empty leaf chain");
38
39    let stake_table_entries = StakeTableEntries::<T>::from(stake_table.to_vec()).0;
40
41    cert2.is_valid_cert(&stake_table_entries, success_threshold, upgrade_lock)?;
42
43    ensure!(
44        cert2.data.leaf_commit == leaf_chain[0].commit(),
45        "cert2 does not match the newest leaf in the chain"
46    );
47    ensure!(
48        cert2.data.block_number == leaf_chain[0].height(),
49        "cert2 block number does not match the newest leaf"
50    );
51    ensure!(
52        cert2.view_number() == leaf_chain[0].view_number(),
53        "cert2 view does not match the newest leaf"
54    );
55
56    if leaf_chain[0].height() == expected_height {
57        return Ok(leaf_chain[0].clone());
58    }
59
60    let mut current = &leaf_chain[0];
61    for leaf in leaf_chain[1..].iter() {
62        let justify_qc = current.justify_qc();
63        if justify_qc.view_number() != leaf.view_number()
64            || justify_qc.data().leaf_commit != leaf.commit()
65        {
66            tracing::warn!(
67                view = ?leaf.view_number(),
68                expected_view = ?justify_qc.view_number(),
69                "leaf is off the leafchain path; expected only after a view timeout"
70            );
71            continue;
72        }
73        ensure!(
74            current.parent_commitment() == leaf.commit(),
75            "current leaf parent commitment does not match parent leaf"
76        );
77        ensure!(
78            leaf.height().checked_add(1) == Some(current.height()),
79            "leaf heights do not chain"
80        );
81        justify_qc.is_valid_cert(&stake_table_entries, success_threshold, upgrade_lock)?;
82        if leaf.height() == expected_height {
83            return Ok(leaf.clone());
84        }
85        current = leaf;
86    }
87
88    Err(anyhow!(
89        "expected height was not found in the cert2-finalized chain"
90    ))
91}