1use std::{
2 collections::{BTreeMap, BTreeSet, HashSet, btree_map::Entry},
3 ops::Range,
4};
5
6use committable::Commitment;
7use hotshot::traits::BlockPayload;
8use hotshot_types::{
9 data::{EpochNumber, VidCommitment2, VidDisperseShare2, ViewNumber, ns_table::parse_ns_table},
10 traits::{block_contents::EncodeBytes, node_implementation::NodeType},
11 vid::avidm_gf2::{AvidmGf2Common, AvidmGf2Param, AvidmGf2Scheme, AvidmGf2Share},
12};
13use tokio::task::{AbortHandle, JoinSet};
14use tracing::warn;
15
16type Metadata<T> = <<T as NodeType>::BlockPayload as BlockPayload<T>>::Metadata;
17
18type ReconstructResult<T> =
19 Result<VidReconstructOutput<T>, VidReconstructError<<T as NodeType>::SignatureKey>>;
20
21pub struct VidReconstructOutput<T: NodeType> {
22 pub view: ViewNumber,
23 pub epoch: EpochNumber,
24 pub payload_commitment: VidCommitment2,
25 pub payload: T::BlockPayload,
26 pub metadata: <T::BlockPayload as BlockPayload<T>>::Metadata,
27 pub tx_commitments: Vec<Commitment<T::Transaction>>,
28}
29
30#[derive(Clone, Copy, Debug, Eq, PartialEq, thiserror::Error)]
32pub enum VidReconstructErrorKind {
33 #[error("awaiting more shares after weeding out unverifiable ones")]
36 AwaitingShares,
37 #[error("unrecoverable: verified shares cannot decode to a payload matching the commitment")]
40 Unrecoverable,
41}
42
43#[derive(Clone, Debug, Eq, PartialEq, thiserror::Error)]
45#[error("VID reconstruction failed for view {view}: {kind}")]
46pub struct VidReconstructError<K> {
47 pub view: ViewNumber,
48 pub payload_commitment: VidCommitment2,
49 pub kind: VidReconstructErrorKind,
50 pub bad_share_keys: Vec<K>,
54}
55
56#[derive(Default)]
57pub struct VidReconstructor<T: NodeType> {
58 pending: BTreeMap<ViewNumber, BTreeMap<T::SignatureKey, VidDisperseShare2<T>>>,
61 accumulators: BTreeMap<ViewNumber, VidShareAccumulator<T>>,
65 reconstructed: BTreeSet<ViewNumber>,
66 tasks: JoinSet<ReconstructResult<T>>,
67 calculations: BTreeMap<ViewNumber, AbortHandle>,
68}
69
70pub(crate) struct VidShareAccumulator<T: NodeType> {
71 payload_commitment: VidCommitment2,
73 metadata: Metadata<T>,
74 epoch: EpochNumber,
75 expected_param: Option<AvidmGf2Param>,
80 common: Option<AvidmGf2Common>,
83 shares: BTreeMap<T::SignatureKey, AvidmGf2Share>,
86 seen_keys: HashSet<T::SignatureKey>,
89 exhausted: bool,
93}
94
95impl<T: NodeType> VidReconstructor<T> {
96 pub fn new() -> Self {
97 Self {
98 pending: BTreeMap::new(),
99 accumulators: BTreeMap::new(),
100 reconstructed: BTreeSet::new(),
101 tasks: JoinSet::new(),
102 calculations: BTreeMap::new(),
103 }
104 }
105
106 pub(crate) fn handle_proposal(
109 &mut self,
110 view: ViewNumber,
111 payload_commitment: VidCommitment2,
112 metadata: Metadata<T>,
113 epoch: EpochNumber,
114 expected_param: Option<AvidmGf2Param>,
115 ) {
116 if self.reconstructed.contains(&view) {
117 return;
118 }
119 let accumulator = match self.accumulators.entry(view) {
120 Entry::Occupied(existing) => {
121 if existing.get().payload_commitment != payload_commitment {
124 warn!(%view, "conflicting proposal for a view pinned to another commitment");
125 }
126 return;
127 },
128 Entry::Vacant(slot) => slot.insert(VidShareAccumulator::new(
129 payload_commitment,
130 metadata,
131 epoch,
132 expected_param,
133 )),
134 };
135 for (sender, share) in self.pending.remove(&view).into_iter().flatten() {
136 accumulator.accept(view, sender, share);
137 }
138 self.try_reconstruct(view);
139 }
140
141 pub(crate) fn handle_vid_share(
142 &mut self,
143 sender: T::SignatureKey,
144 share: VidDisperseShare2<T>,
145 ) {
146 let view = share.view_number;
147 if share.recipient_key != sender {
152 warn!(%view, ?sender, "VID share recipient key does not match its sender");
153 return;
154 }
155 if self.reconstructed.contains(&view) {
156 return;
157 }
158 let Some(accumulator) = self.accumulators.get_mut(&view) else {
159 self.pending
162 .entry(view)
163 .or_default()
164 .entry(sender)
165 .or_insert(share);
166 return;
167 };
168 accumulator.accept(view, sender, share);
169 self.try_reconstruct(view);
170 }
171
172 pub async fn next(&mut self) -> Option<ReconstructResult<T>> {
173 loop {
174 match self.tasks.join_next().await {
175 Some(Ok(Ok(out))) => {
176 self.calculations.remove(&out.view);
177 self.accumulators.remove(&out.view);
178 self.reconstructed.insert(out.view);
179 return Some(Ok(out));
180 },
181 Some(Ok(Err(err))) => {
182 self.calculations.remove(&err.view);
183 self.handle_failed_attempt(&err);
184 return Some(Err(err));
185 },
186 Some(Err(_)) => continue,
187 None => return None,
188 }
189 }
190 }
191
192 fn handle_failed_attempt(&mut self, err: &VidReconstructError<T::SignatureKey>) {
197 let Some(accumulator) = self.accumulators.get_mut(&err.view) else {
198 return;
199 };
200 if accumulator.payload_commitment != err.payload_commitment {
204 return;
205 }
206 for key in &err.bad_share_keys {
207 accumulator.shares.remove(key);
208 }
209 match err.kind {
210 VidReconstructErrorKind::Unrecoverable => accumulator.exhausted = true,
211 VidReconstructErrorKind::AwaitingShares => self.try_reconstruct(err.view),
212 }
213 }
214
215 fn try_reconstruct(&mut self, view: ViewNumber) {
216 if self.calculations.contains_key(&view) {
217 return;
218 }
219 let Some(accumulator) = self.accumulators.get(&view) else {
220 return;
221 };
222 if accumulator.exhausted || !accumulator.has_enough_shares() {
223 return;
224 }
225 let Some(common) = accumulator.common.clone() else {
227 return;
228 };
229 let payload_commitment = accumulator.payload_commitment;
230 let metadata = accumulator.metadata.clone();
231 let shares: Vec<(T::SignatureKey, AvidmGf2Share)> = accumulator
232 .shares
233 .iter()
234 .map(|(key, share)| (key.clone(), share.clone()))
235 .collect();
236 let epoch = accumulator.epoch;
237 let task = self.tasks.spawn_blocking(move || {
238 reconstruct::<T>(view, epoch, payload_commitment, common, shares, metadata)
239 });
240 self.calculations.insert(view, task);
241 }
242
243 pub fn gc(&mut self, view_number: ViewNumber) {
244 let keep = self.calculations.split_off(&view_number);
245 for handle in self.calculations.values_mut() {
246 handle.abort();
247 }
248 self.calculations = keep;
249 self.pending = self.pending.split_off(&view_number);
250 self.accumulators = self.accumulators.split_off(&view_number);
251 self.reconstructed = self.reconstructed.split_off(&view_number);
252 }
253
254 pub fn retire_view(&mut self, view: ViewNumber) {
261 self.reconstructed.insert(view);
262 self.pending.remove(&view);
263 self.accumulators.remove(&view);
264 if let Some(handle) = self.calculations.remove(&view) {
265 handle.abort();
266 }
267 }
268}
269
270impl<T: NodeType> VidShareAccumulator<T> {
271 fn new(
272 payload_commitment: VidCommitment2,
273 metadata: Metadata<T>,
274 epoch: EpochNumber,
275 expected_param: Option<AvidmGf2Param>,
276 ) -> Self {
277 Self {
278 payload_commitment,
279 metadata,
280 epoch,
281 expected_param,
282 common: None,
283 shares: BTreeMap::new(),
284 seen_keys: HashSet::new(),
285 exhausted: false,
286 }
287 }
288
289 fn accept(&mut self, view: ViewNumber, sender: T::SignatureKey, share: VidDisperseShare2<T>) {
294 if self.exhausted {
295 return;
296 }
297 if share.payload_commitment != self.payload_commitment {
298 warn!(%view, ?sender, "VID share commitment differs from the proposal's");
299 return;
300 }
301 if let Some(expected) = &self.expected_param
306 && share.common.param != *expected
307 {
308 warn!(%view, ?sender, "VID share common param differs from the committee's");
309 return;
310 }
311 if let Some(common) = &self.common {
314 if share.common != *common {
315 warn!(%view, ?sender, "VID share common differs from the accumulator's");
316 return;
317 }
318 } else if AvidmGf2Scheme::is_consistent(&self.payload_commitment, &share.common) {
319 self.common = Some(share.common.clone());
320 } else {
321 warn!(%view, ?sender, "VID share common is inconsistent with its commitment");
322 return;
323 }
324 let Some(range) = share.share.range() else {
326 warn!(%view, ?sender, "VID share has an inconsistent shard range");
327 return;
328 };
329 if range.is_empty() || range.end > share.common.param.total_weights {
332 warn!(%view, ?sender, ?range, "VID share has an empty or out-of-bounds shard range");
333 return;
334 }
335 if self.seen_keys.contains(&sender) {
336 return;
337 }
338 let conflicts: Vec<T::SignatureKey> = self
341 .shares
342 .iter()
343 .filter(|(_, admitted)| {
344 admitted
345 .range()
346 .is_some_and(|covered| covered.start < range.end && range.start < covered.end)
347 })
348 .map(|(key, _)| key.clone())
349 .collect();
350 if conflicts.is_empty() {
351 self.seen_keys.insert(sender.clone());
352 self.shares.insert(sender, share.share);
353 return;
354 }
355 self.resolve_conflict(view, sender, share, conflicts);
356 }
357
358 fn resolve_conflict(
363 &mut self,
364 view: ViewNumber,
365 sender: T::SignatureKey,
366 share: VidDisperseShare2<T>,
367 conflicts: Vec<T::SignatureKey>,
368 ) {
369 let Some(common) = self.common.clone() else {
371 return;
372 };
373 self.seen_keys.insert(sender.clone());
375 let mut survivor = false;
376 for key in conflicts {
377 let verified = self
378 .shares
379 .get(&key)
380 .is_some_and(|admitted| share_verifies(&common, admitted));
381 if verified {
382 survivor = true;
383 } else {
384 warn!(%view, ?key, "evicting unverifiable VID share squatting a shard range");
385 self.shares.remove(&key);
386 }
387 }
388 if survivor {
391 return;
392 }
393 if share_verifies(&common, &share.share) {
394 self.shares.insert(sender, share.share);
395 } else {
396 warn!(%view, ?sender, "dropping unverifiable VID share at intake conflict");
397 }
398 }
399
400 fn ranges(&self) -> impl Iterator<Item = &Range<usize>> {
401 self.shares.values().filter_map(AvidmGf2Share::range)
403 }
404
405 fn coverage(&self) -> usize {
408 self.ranges().map(ExactSizeIterator::len).sum()
409 }
410
411 fn has_enough_shares(&self) -> bool {
412 self.common
413 .as_ref()
414 .is_some_and(|common| self.coverage() >= common.param.recovery_threshold)
415 }
416}
417
418fn reconstruct<T: NodeType>(
426 view: ViewNumber,
427 epoch: EpochNumber,
428 payload_commitment: VidCommitment2,
429 common: AvidmGf2Common,
430 shares: Vec<(T::SignatureKey, AvidmGf2Share)>,
431 metadata: Metadata<T>,
432) -> ReconstructResult<T> {
433 let (keys, shares): (Vec<_>, Vec<_>) = shares.into_iter().unzip();
434 if let Some(bytes) =
435 decode_and_recommit::<T>(view, &common, &shares, &payload_commitment, &metadata)
436 {
437 let payload = T::BlockPayload::from_bytes(&bytes, &metadata);
438 let tx_commitments = payload.transaction_commitments(&metadata);
439 let output = VidReconstructOutput {
440 view,
441 epoch,
442 payload_commitment,
443 payload,
444 metadata,
445 tx_commitments,
446 };
447 return Ok(output);
448 }
449
450 let bad_share_keys: Vec<_> = keys
451 .into_iter()
452 .zip(&shares)
453 .filter(|(_, share)| !share_verifies(&common, share))
454 .map(|(key, _)| key)
455 .collect();
456
457 let kind = if bad_share_keys.is_empty() {
458 warn!(
459 %view,
460 %payload_commitment,
461 "verified shares cannot decode to a payload matching the commitment"
462 );
463 VidReconstructErrorKind::Unrecoverable
464 } else {
465 warn!(
466 %view,
467 %payload_commitment,
468 ?bad_share_keys,
469 "weeded out VID shares that failed verification"
470 );
471 VidReconstructErrorKind::AwaitingShares
472 };
473 Err(VidReconstructError {
474 view,
475 payload_commitment,
476 kind,
477 bad_share_keys,
478 })
479}
480
481fn decode_and_recommit<T: NodeType>(
486 view: ViewNumber,
487 common: &AvidmGf2Common,
488 shares: &[AvidmGf2Share],
489 payload_commitment: &VidCommitment2,
490 metadata: &Metadata<T>,
491) -> Option<Vec<u8>> {
492 let bytes = match AvidmGf2Scheme::recover(common, shares) {
493 Ok(bytes) => bytes,
494 Err(err) => {
495 warn!(%view, %err, "VID recovery failed");
496 return None;
497 },
498 };
499 let ns_table = parse_ns_table(bytes.len(), &metadata.encode());
500 match AvidmGf2Scheme::commit(&common.param, &bytes, ns_table) {
501 Ok((recomputed, _)) if recomputed == *payload_commitment => Some(bytes),
502 Ok((recomputed, _)) => {
503 warn!(
504 %view,
505 expected = %payload_commitment,
506 %recomputed,
507 "reconstructed payload does not match the payload commitment"
508 );
509 None
510 },
511 Err(err) => {
512 warn!(%view, %err, "failed to recommit reconstructed VID payload");
513 None
514 },
515 }
516}
517
518fn share_verifies(common: &AvidmGf2Common, share: &AvidmGf2Share) -> bool {
521 matches!(
522 AvidmGf2Scheme::verify_share_with_verified_common(common, share),
523 Ok(Ok(()))
524 )
525}