Skip to main content

hotshot_new_protocol/vid/
fragments.rs

1use std::collections::{BTreeMap, BTreeSet};
2
3use hotshot_types::{
4    data::{
5        EpochNumber, VidCommitment2, VidDisperseShare2, ViewNumber,
6        vid_disperse::{AvidmGf2DisperseShareFragment, AvidmGf2NamespacePiece},
7    },
8    traits::node_implementation::NodeType,
9    vid::avidm_gf2::{AvidmGf2Common, AvidmGf2Param},
10};
11
12pub struct VidFragmentAccumulator<T: NodeType> {
13    /// Partial shares per `(view, epoch)`, keyed within by the disperser that
14    /// sent the fragments. Keying by disperser is what keeps one sender's
15    /// stream from displacing another's; see [`Self::accept`].
16    pending: BTreeMap<(ViewNumber, EpochNumber), BTreeMap<T::SignatureKey, PendingShare<T>>>,
17    completed: BTreeMap<(ViewNumber, EpochNumber), BTreeSet<T::SignatureKey>>,
18    lower_bound: ViewNumber,
19}
20
21#[derive(Debug, thiserror::Error)]
22pub enum VidFragmentError {
23    #[error("fragment disagrees with the view's pinned metadata")]
24    Inconsistent,
25
26    #[error("namespace index {index} out of range for {num_namespaces} namespaces")]
27    IndexOutOfRange { index: usize, num_namespaces: usize },
28
29    #[error("duplicate fragment for namespace index {0}")]
30    DuplicateIndex(usize),
31
32    #[error("fragment contains no namespaces")]
33    Empty,
34
35    #[error("fragment names no epoch")]
36    MissingEpoch,
37
38    #[error("fragment target epoch {target_epoch:?} does not match its epoch {epoch:?}")]
39    TargetEpochMismatch {
40        epoch: Option<EpochNumber>,
41        target_epoch: Option<EpochNumber>,
42    },
43}
44
45impl<T: NodeType> Default for VidFragmentAccumulator<T> {
46    fn default() -> Self {
47        Self::new()
48    }
49}
50
51impl<T: NodeType> VidFragmentAccumulator<T> {
52    pub fn new() -> Self {
53        Self {
54            pending: BTreeMap::new(),
55            completed: BTreeMap::new(),
56            lower_bound: ViewNumber::genesis(),
57        }
58    }
59
60    /// Buffer a `fragment` addressed to this node, sent by `disperser`.
61    ///
62    /// Returns `Ok(None)` while namespaces are still outstanding,
63    /// `Ok(Some(share))` once the final namespace completes the view, and
64    /// `Err` if the fragment is malformed or inconsistent with what
65    /// `disperser` already pinned for the view.
66    ///
67    /// `disperser` is the authenticated message sender, not a field of the
68    /// fragment. Each disperser gets its own buffer: a fragment stream commits to
69    /// nothing beyond its payload commitment, so a stream that pinned or
70    /// completed a view for one sender must not lock out the view's honest
71    /// leader.
72    pub fn accept(
73        &mut self,
74        disperser: &T::SignatureKey,
75        fragment: AvidmGf2DisperseShareFragment<T>,
76    ) -> Result<Option<VidDisperseShare2<T>>, VidFragmentError> {
77        let view = fragment.view_number;
78        let epoch = well_formed(&fragment)?;
79        if self.is_retired(view, epoch, disperser) {
80            return Ok(None);
81        }
82        let complete = {
83            let pending = self.pending_for(view, epoch, disperser, &fragment)?;
84            pending.insert_pieces(fragment.namespaces)?;
85            pending.is_complete()
86        };
87        if !complete {
88            return Ok(None);
89        }
90        let pending = self.take_pending(view, epoch, disperser);
91        self.completed
92            .entry((view, epoch))
93            .or_default()
94            .insert(disperser.clone());
95        Ok(Some(pending.into_share(view)))
96    }
97
98    pub fn gc(&mut self, view_number: ViewNumber) {
99        let cutoff = (view_number, EpochNumber::new(0));
100        self.pending = self.pending.split_off(&cutoff);
101        self.completed = self.completed.split_off(&cutoff);
102        self.lower_bound = view_number;
103    }
104
105    /// Has `view` been GCed, or `disperser` already completed a share for it
106    /// under `epoch`?
107    fn is_retired(
108        &self,
109        view: ViewNumber,
110        epoch: EpochNumber,
111        disperser: &T::SignatureKey,
112    ) -> bool {
113        view < self.lower_bound
114            || self
115                .completed
116                .get(&(view, epoch))
117                .is_some_and(|keys| keys.contains(disperser))
118    }
119
120    /// `disperser`'s buffer for `(view, epoch)`, opening it on the first
121    /// fragment and pinning the metadata every later fragment of the stream
122    /// must repeat.
123    fn pending_for<'a>(
124        &'a mut self,
125        view: ViewNumber,
126        epoch: EpochNumber,
127        disperser: &T::SignatureKey,
128        fragment: &AvidmGf2DisperseShareFragment<T>,
129    ) -> Result<&'a mut PendingShare<T>, VidFragmentError> {
130        let pending = self
131            .pending
132            .entry((view, epoch))
133            .or_default()
134            .entry(disperser.clone())
135            .or_insert_with(|| PendingShare {
136                epoch,
137                payload_commitment: fragment.payload_commitment,
138                recipient_key: fragment.recipient_key.clone(),
139                param: fragment.param.clone(),
140                num_namespaces: fragment.num_namespaces,
141                pieces: BTreeMap::new(),
142            });
143        if pending.num_namespaces != fragment.num_namespaces
144            || pending.payload_commitment != fragment.payload_commitment
145            || pending.recipient_key != fragment.recipient_key
146            || pending.param != fragment.param
147        {
148            return Err(VidFragmentError::Inconsistent);
149        }
150        Ok(pending)
151    }
152
153    /// Remove `disperser`'s buffer for `(view, epoch)`, dropping that map once
154    /// it holds no other disperser.
155    fn take_pending(
156        &mut self,
157        view: ViewNumber,
158        epoch: EpochNumber,
159        disperser: &T::SignatureKey,
160    ) -> PendingShare<T> {
161        let by_disperser = self
162            .pending
163            .get_mut(&(view, epoch))
164            .expect("buffer just opened above");
165        let pending = by_disperser
166            .remove(disperser)
167            .expect("buffer just opened above");
168        if by_disperser.is_empty() {
169            self.pending.remove(&(view, epoch));
170        }
171        pending
172    }
173}
174
175/// The fragment's epoch, if the fragment is structurally sound.
176///
177/// An honest disperser sends `epoch` and `target_epoch` equal. They are
178/// separately chosen by whoever sent the fragment and select committees on
179/// different paths downstream -- `epoch` the leader and share verification,
180/// `target_epoch` the erasure parameters the reconstructor holds every peer's
181/// share to -- so a fragment that lets them diverge is malformed.
182fn well_formed<T: NodeType>(
183    fragment: &AvidmGf2DisperseShareFragment<T>,
184) -> Result<EpochNumber, VidFragmentError> {
185    if fragment.num_namespaces == 0 {
186        return Err(VidFragmentError::Empty);
187    }
188    if fragment.target_epoch != fragment.epoch {
189        return Err(VidFragmentError::TargetEpochMismatch {
190            epoch: fragment.epoch,
191            target_epoch: fragment.target_epoch,
192        });
193    }
194    fragment.epoch.ok_or(VidFragmentError::MissingEpoch)
195}
196
197/// A view's partially-collected namespace pieces, keyed by namespace index.
198struct PendingShare<T: NodeType> {
199    epoch: EpochNumber,
200    payload_commitment: VidCommitment2,
201    recipient_key: T::SignatureKey,
202    param: AvidmGf2Param,
203    num_namespaces: usize,
204    pieces: BTreeMap<usize, AvidmGf2NamespacePiece>,
205}
206
207impl<T: NodeType> PendingShare<T> {
208    fn insert_pieces(
209        &mut self,
210        pieces: Vec<AvidmGf2NamespacePiece>,
211    ) -> Result<(), VidFragmentError> {
212        let mut incoming = BTreeSet::new();
213        for piece in &pieces {
214            let ns_index = piece.ns_index;
215            if ns_index >= self.num_namespaces {
216                return Err(VidFragmentError::IndexOutOfRange {
217                    index: ns_index,
218                    num_namespaces: self.num_namespaces,
219                });
220            }
221            if self.pieces.contains_key(&ns_index) || !incoming.insert(ns_index) {
222                return Err(VidFragmentError::DuplicateIndex(ns_index));
223            }
224        }
225        for piece in pieces {
226            self.pieces.insert(piece.ns_index, piece);
227        }
228        Ok(())
229    }
230
231    fn is_complete(&self) -> bool {
232        self.pieces.len() == self.num_namespaces
233    }
234
235    /// Assemble the completed share.
236    ///
237    /// Every namespace is present and indices are distinct and in range, so
238    /// they cover `0..num_namespaces` exactly; the `BTreeMap` yields them in
239    /// that order.
240    fn into_share(self, view: ViewNumber) -> VidDisperseShare2<T> {
241        let mut ns_commits = Vec::with_capacity(self.num_namespaces);
242        let mut ns_lens = Vec::with_capacity(self.num_namespaces);
243        let mut ns_shares = Vec::with_capacity(self.num_namespaces);
244        for piece in self.pieces.into_values() {
245            ns_commits.push(piece.ns_commit);
246            ns_lens.push(piece.ns_payload_byte_len);
247            ns_shares.push(piece.ns_share);
248        }
249        VidDisperseShare2 {
250            view_number: view,
251            epoch: Some(self.epoch),
252            // Equal to `epoch` by `well_formed`; reusing it is what keeps the
253            // two from diverging in the assembled share.
254            target_epoch: Some(self.epoch),
255            payload_commitment: self.payload_commitment,
256            share: ns_shares.into(),
257            recipient_key: self.recipient_key,
258            common: AvidmGf2Common {
259                param: self.param,
260                ns_commits,
261                ns_lens,
262            },
263        }
264    }
265}