Skip to main content

hotshot_new_protocol/
vid.rs

1//! Verifiable information dispersal (VID) for the new protocol.
2//!
3//! A block's payload is erasure-coded per namespace and spread across the
4//! committee so the block can be recovered from any subset of storage nodes
5//! whose shards cover the recovery threshold. This module owns the three stages
6//! of that lifecycle, one per submodule:
7//!
8//! - [`disperse`] -- the leader side. The block builder erasure-codes each
9//!   namespace into a [`BuiltDispersal`] and folds the payload commitment from
10//!   it. [`VidDisperser`] coalesces namespaces into size-balanced buckets and
11//!   unicasts to every node a stream of [`AvidmGf2DisperseShareFragment`]
12//!   messages (one per bucket), each carrying that node's shares for the
13//!   bucket's namespaces. Without a built dispersal it erasure-codes the
14//!   namespaces itself.
15//!
16//! - [`fragments`] -- the receive side of dispersal, the mirror of
17//!   [`VidDisperser`]. [`VidFragmentAccumulator`] buffers the fragments a node
18//!   receives for its *own* share and, once every namespace has arrived,
19//!   reassembles them into a complete [`VidDisperseShare2`]. That share is then
20//!   verified, attached to this node's vote, and fed to the reconstructor.
21//!
22//! - [`reconstruct`] -- block recovery. [`VidReconstructor`] collects the
23//!   verified shares contributed by *many* voters (each node's own share,
24//!   carried on its vote) and decodes the payload once their shards cover the
25//!   recovery threshold.
26//!
27//! [`AvidmGf2DisperseShareFragment`]: hotshot_types::data::vid_disperse::AvidmGf2DisperseShareFragment
28//! [`VidDisperseShare2`]: hotshot_types::data::VidDisperseShare2
29
30mod disperse;
31mod fragments;
32mod reconstruct;
33
34pub use disperse::{
35    BuiltDispersal, Dispersal, VidDisperseError, VidDisperseOutput, VidDisperseRequest,
36    VidDisperser, disperse_payload,
37};
38pub use fragments::{VidFragmentAccumulator, VidFragmentError};
39use hotshot_types::{
40    data::{
41        EpochNumber,
42        ns_table::{ns_table_payload_byte_len, parse_ns_table},
43        vid_disperse::vid_total_weight,
44    },
45    epoch_membership::EpochMembershipCoordinator,
46    traits::node_implementation::NodeType,
47    vid::avidm_gf2::{AvidmGf2Common, AvidmGf2Param, init_avidm_gf2_param},
48};
49pub(crate) use reconstruct::matches_commitment;
50pub use reconstruct::{
51    ObtainedPayload, PinnedBy, VidReconstructError, VidReconstructErrorKind, VidReconstructor,
52};
53
54/// The VID erasure parameters the committee for `epoch` fixes, matching what
55/// an honest disperser derives. Used to reject shares whose `common.param` is
56/// forged (the commitment binds `ns_commits`, not `param`) and to verify
57/// payloads fetched whole. `None` if the committee cannot be resolved.
58pub fn expected_vid_param<T: NodeType>(
59    membership: &EpochMembershipCoordinator<T>,
60    epoch: EpochNumber,
61) -> Option<AvidmGf2Param> {
62    let membership = membership.stake_table_for_epoch(Some(epoch)).ok()?;
63    let total_weight = vid_total_weight::<T, _>(membership.stake_table(), Some(epoch));
64    init_avidm_gf2_param(total_weight).ok()
65}
66
67/// Whether `common.ns_lens` are the namespace lengths an honest disperser
68/// derives from the header's namespace table `ns_table` at the payload length
69/// the common claims, which the table must be sized for if it is one.
70///
71/// The commitment binds `ns_commits` but not `ns_lens`, so a leader could
72/// otherwise pair honest namespace commitments with lengths shuffled between
73/// namespaces. Such a block reconstructs and decides, but its stored common
74/// then slices the payload at the wrong namespace boundaries and no namespace
75/// proof for it can be served.
76///
77/// This pins `ns_lens` to the header. Whether the header's table is sized for
78/// the payload is settled by `matches_commitment` at reconstruction, which
79/// sees the recovered bytes.
80pub(crate) fn ns_lens_match_metadata(common: &AvidmGf2Common, ns_table: &[u8]) -> bool {
81    let total = common.payload_byte_len();
82    if ns_table_payload_byte_len(ns_table).is_some_and(|claimed| claimed != total) {
83        return false;
84    }
85    let expected = parse_ns_table(total, ns_table);
86    common.ns_lens.len() == expected.len()
87        && common
88            .ns_lens
89            .iter()
90            .zip(&expected)
91            .all(|(len, range)| *len == range.len())
92}