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}