1use std::{
2 collections::{BTreeMap, BTreeSet, HashSet, btree_map::Entry},
3 ops::Range,
4};
5
6use committable::Commitment;
7use hotshot_types::{
8 data::{
9 EpochNumber, VidCommitment2, VidDisperseShare2, ViewNumber,
10 ns_table::{ns_table_payload_byte_len, parse_ns_table},
11 },
12 traits::{BlockPayload, block_contents::EncodeBytes, node_implementation::NodeType},
13 vid::avidm_gf2::{AvidmGf2Common, AvidmGf2Param, AvidmGf2Scheme, AvidmGf2Share},
14};
15use tokio::task::{AbortHandle, JoinSet};
16use tracing::{error, warn};
17
18use super::ns_lens_match_metadata;
19
20pub(crate) type Metadata<T> = <<T as NodeType>::BlockPayload as BlockPayload<T>>::Metadata;
21
22type ReconstructResult<T> =
23 Result<ObtainedPayload<T>, VidReconstructError<<T as NodeType>::SignatureKey>>;
24
25pub struct ObtainedPayload<T: NodeType> {
31 pub view: ViewNumber,
32 pub epoch: EpochNumber,
33 pub payload_commitment: VidCommitment2,
34 pub payload: T::BlockPayload,
35 pub metadata: <T::BlockPayload as BlockPayload<T>>::Metadata,
36 pub tx_commitments: Vec<Commitment<T::Transaction>>,
37}
38
39#[derive(Clone, Copy, Debug, Eq, PartialEq, thiserror::Error)]
41pub enum VidReconstructErrorKind {
42 #[error("awaiting more shares after weeding out unverifiable ones")]
45 AwaitingShares,
46 #[error("unrecoverable: verified shares cannot decode to a payload matching the commitment")]
49 Unrecoverable,
50}
51
52#[derive(Clone, Debug, Eq, PartialEq, thiserror::Error)]
54#[error("VID reconstruction failed for view {view}: {kind}")]
55pub struct VidReconstructError<K> {
56 pub view: ViewNumber,
57 pub payload_commitment: VidCommitment2,
58 pub kind: VidReconstructErrorKind,
59 pub bad_share_keys: Vec<K>,
63}
64
65#[derive(Clone, Copy, Debug, Eq, PartialEq)]
67pub enum PinnedBy {
68 Leader,
71 Certificate,
74}
75
76#[derive(Default)]
77pub struct VidReconstructor<T: NodeType> {
78 pending: BTreeMap<ViewNumber, BTreeMap<T::SignatureKey, VidDisperseShare2<T>>>,
82 accumulators: BTreeMap<ViewNumber, VidShareAccumulator<T>>,
86 reconstructed: BTreeSet<ViewNumber>,
87 tasks: JoinSet<ReconstructResult<T>>,
88 calculations: BTreeMap<ViewNumber, AbortHandle>,
89 tracer: Option<crate::leader_trace::LeaderTracerHandle>,
90}
91
92pub(crate) struct VidShareAccumulator<T: NodeType> {
93 payload_commitment: VidCommitment2,
95 pinned_by: PinnedBy,
97 metadata: Metadata<T>,
98 epoch: EpochNumber,
99 expected_param: Option<AvidmGf2Param>,
104 common: Option<AvidmGf2Common>,
107 shares: BTreeMap<T::SignatureKey, AvidmGf2Share>,
110 seen_keys: HashSet<T::SignatureKey>,
113 exhausted: bool,
117}
118
119impl<T: NodeType> VidReconstructor<T> {
120 pub fn new() -> Self {
121 Self {
122 pending: BTreeMap::new(),
123 accumulators: BTreeMap::new(),
124 reconstructed: BTreeSet::new(),
125 tasks: JoinSet::new(),
126 calculations: BTreeMap::new(),
127 tracer: None,
128 }
129 }
130
131 pub fn set_tracer(&mut self, tracer: Option<crate::leader_trace::LeaderTracerHandle>) {
132 self.tracer = tracer;
133 }
134
135 pub(crate) fn handle_proposal(
142 &mut self,
143 view: ViewNumber,
144 payload_commitment: VidCommitment2,
145 metadata: Metadata<T>,
146 epoch: EpochNumber,
147 expected_param: Option<AvidmGf2Param>,
148 pinned_by: PinnedBy,
149 ) {
150 if self.reconstructed.contains(&view) {
151 return;
152 }
153 let accumulator = match self.accumulators.entry(view) {
154 Entry::Occupied(mut existing) => {
155 let pinned = existing.get_mut();
156 if pinned.payload_commitment == payload_commitment {
157 if pinned_by == PinnedBy::Certificate {
158 pinned.pinned_by = PinnedBy::Certificate;
159 }
160 return;
161 }
162 if pinned_by == PinnedBy::Leader || pinned.pinned_by == PinnedBy::Certificate {
163 warn!(%view, "conflicting proposal for a view pinned to another commitment");
164 return;
165 }
166 warn!(%view, "leader equivocated; re-pinning to the certified proposal");
167 if let Some(task) = self.calculations.remove(&view) {
168 task.abort();
169 }
170 existing.insert(VidShareAccumulator::new(
171 payload_commitment,
172 metadata,
173 epoch,
174 expected_param,
175 pinned_by,
176 ));
177 existing.into_mut()
178 },
179 Entry::Vacant(slot) => slot.insert(VidShareAccumulator::new(
180 payload_commitment,
181 metadata,
182 epoch,
183 expected_param,
184 pinned_by,
185 )),
186 };
187 let held = self.pending.remove(&view).unwrap_or_default();
190 let (admitted, held): (BTreeMap<_, _>, BTreeMap<_, _>) = held
191 .into_iter()
192 .partition(|(_, share)| share.payload_commitment == payload_commitment);
193 if !held.is_empty() {
194 self.pending.insert(view, held);
195 }
196 for (sender, share) in admitted {
197 accumulator.accept(view, sender, share);
198 }
199 self.try_reconstruct(view);
200 }
201
202 pub(crate) fn handle_vid_share(
203 &mut self,
204 sender: T::SignatureKey,
205 share: VidDisperseShare2<T>,
206 ) {
207 let view = share.view_number;
208 if share.recipient_key != sender {
213 warn!(%view, ?sender, "VID share recipient key does not match its sender");
214 return;
215 }
216 if self.reconstructed.contains(&view) {
217 return;
218 }
219 let Some(accumulator) = self.accumulators.get_mut(&view) else {
220 self.hold(view, sender, share);
221 return;
222 };
223 if share.payload_commitment == accumulator.payload_commitment {
224 accumulator.accept(view, sender, share);
225 self.try_reconstruct(view);
226 return;
227 }
228 match accumulator.pinned_by {
229 PinnedBy::Leader => self.hold(view, sender, share),
231 PinnedBy::Certificate => {
232 warn!(%view, ?sender, "VID share commitment differs from the certified proposal's");
233 },
234 }
235 }
236
237 fn hold(&mut self, view: ViewNumber, sender: T::SignatureKey, share: VidDisperseShare2<T>) {
240 self.pending
241 .entry(view)
242 .or_default()
243 .entry(sender)
244 .or_insert(share);
245 }
246
247 pub async fn next(&mut self) -> Option<ReconstructResult<T>> {
248 loop {
249 match self.tasks.join_next().await? {
250 Ok(Ok(out)) => {
251 if self.is_superseded(out.view, out.payload_commitment) {
252 continue;
253 }
254 self.calculations.remove(&out.view);
255 self.accumulators.remove(&out.view);
256 self.reconstructed.insert(out.view);
257 return Some(Ok(out));
258 },
259 Ok(Err(err)) => {
260 if self.is_superseded(err.view, err.payload_commitment) {
261 continue;
262 }
263 self.calculations.remove(&err.view);
264 self.handle_failed_attempt(&err);
265 return Some(Err(err));
266 },
267 Err(err) => {
268 if err.is_panic() {
269 error!(%err, "VID reconstruction task panicked");
270 }
271 self.calculations.retain(|_, task| task.id() != err.id());
272 },
273 }
274 }
275 }
276
277 fn is_superseded(&self, view: ViewNumber, payload_commitment: VidCommitment2) -> bool {
281 self.accumulators
282 .get(&view)
283 .is_some_and(|accumulator| accumulator.payload_commitment != payload_commitment)
284 }
285
286 fn handle_failed_attempt(&mut self, err: &VidReconstructError<T::SignatureKey>) {
291 let Some(accumulator) = self.accumulators.get_mut(&err.view) else {
292 return;
293 };
294 for key in &err.bad_share_keys {
295 accumulator.shares.remove(key);
296 }
297 match err.kind {
298 VidReconstructErrorKind::Unrecoverable => accumulator.exhausted = true,
299 VidReconstructErrorKind::AwaitingShares => self.try_reconstruct(err.view),
300 }
301 }
302
303 fn try_reconstruct(&mut self, view: ViewNumber) {
304 if self.calculations.contains_key(&view) {
305 return;
306 }
307 let Some(accumulator) = self.accumulators.get(&view) else {
308 return;
309 };
310 if accumulator.exhausted || !accumulator.has_enough_shares() {
311 return;
312 }
313 let Some(common) = accumulator.common.clone() else {
315 return;
316 };
317 let payload_commitment = accumulator.payload_commitment;
318 let metadata = accumulator.metadata.clone();
319 let shares: Vec<(T::SignatureKey, AvidmGf2Share)> = accumulator
320 .shares
321 .iter()
322 .map(|(key, share)| (key.clone(), share.clone()))
323 .collect();
324 let epoch = accumulator.epoch;
325 crate::trace_leader_event!(
326 self.tracer,
327 view,
328 crate::leader_trace::LeaderEvent::ThresholdShareReachedVMinus1
329 );
330 let tracer = self.tracer.clone();
331 let task = self.tasks.spawn_blocking(move || {
332 crate::trace_leader_event!(
333 tracer,
334 view,
335 crate::leader_trace::LeaderEvent::RecoverVMinus1Start
336 );
337 let result = reconstruct::<T>(
338 view,
339 epoch,
340 payload_commitment,
341 common,
342 shares,
343 metadata,
344 tracer.clone(),
345 );
346 if result.is_ok() {
347 crate::trace_leader_event!(
348 tracer,
349 view,
350 crate::leader_trace::LeaderEvent::RecoverVMinus1End
351 );
352 }
353 result
354 });
355 self.calculations.insert(view, task);
356 }
357
358 pub fn gc(&mut self, view_number: ViewNumber) {
359 let keep = self.calculations.split_off(&view_number);
360 for handle in self.calculations.values_mut() {
361 handle.abort();
362 }
363 self.calculations = keep;
364 self.pending = self.pending.split_off(&view_number);
365 self.accumulators = self.accumulators.split_off(&view_number);
366 self.reconstructed = self.reconstructed.split_off(&view_number);
367 }
368
369 pub fn retire_view(&mut self, view: ViewNumber) {
376 self.reconstructed.insert(view);
377 self.pending.remove(&view);
378 self.accumulators.remove(&view);
379 if let Some(handle) = self.calculations.remove(&view) {
380 handle.abort();
381 }
382 }
383}
384
385impl<T: NodeType> VidShareAccumulator<T> {
386 fn new(
387 payload_commitment: VidCommitment2,
388 metadata: Metadata<T>,
389 epoch: EpochNumber,
390 expected_param: Option<AvidmGf2Param>,
391 pinned_by: PinnedBy,
392 ) -> Self {
393 Self {
394 payload_commitment,
395 pinned_by,
396 metadata,
397 epoch,
398 expected_param,
399 common: None,
400 shares: BTreeMap::new(),
401 seen_keys: HashSet::new(),
402 exhausted: false,
403 }
404 }
405
406 fn accept(&mut self, view: ViewNumber, sender: T::SignatureKey, share: VidDisperseShare2<T>) {
412 debug_assert_eq!(share.payload_commitment, self.payload_commitment);
413 if self.exhausted {
414 return;
415 }
416 if let Some(expected) = &self.expected_param
421 && share.common.param != *expected
422 {
423 warn!(%view, ?sender, "VID share common param differs from the committee's");
424 return;
425 }
426 if let Some(common) = &self.common {
431 if share.common != *common {
432 warn!(%view, ?sender, "VID share common differs from the accumulator's");
433 return;
434 }
435 } else if !AvidmGf2Scheme::is_consistent(&self.payload_commitment, &share.common) {
436 warn!(%view, ?sender, "VID share common is inconsistent with its commitment");
437 return;
438 } else if !ns_lens_match_metadata(&share.common, &self.metadata.encode()) {
439 warn!(
440 %view, ?sender,
441 "VID share namespace lengths disagree with the proposal's namespace table"
442 );
443 return;
444 } else {
445 self.common = Some(share.common.clone());
446 }
447 let Some(range) = share.share.range() else {
449 warn!(%view, ?sender, "VID share has an inconsistent shard range");
450 return;
451 };
452 if range.is_empty() || range.end > share.common.param.total_weights {
455 warn!(%view, ?sender, ?range, "VID share has an empty or out-of-bounds shard range");
456 return;
457 }
458 if self.seen_keys.contains(&sender) {
459 return;
460 }
461 let conflicts: Vec<T::SignatureKey> = self
464 .shares
465 .iter()
466 .filter(|(_, admitted)| {
467 admitted
468 .range()
469 .is_some_and(|covered| covered.start < range.end && range.start < covered.end)
470 })
471 .map(|(key, _)| key.clone())
472 .collect();
473 if conflicts.is_empty() {
474 self.seen_keys.insert(sender.clone());
475 self.shares.insert(sender, share.share);
476 return;
477 }
478 self.resolve_conflict(view, sender, share, conflicts);
479 }
480
481 fn resolve_conflict(
486 &mut self,
487 view: ViewNumber,
488 sender: T::SignatureKey,
489 share: VidDisperseShare2<T>,
490 conflicts: Vec<T::SignatureKey>,
491 ) {
492 let Some(common) = self.common.clone() else {
494 return;
495 };
496 self.seen_keys.insert(sender.clone());
498 let mut survivor = false;
499 for key in conflicts {
500 let verified = self
501 .shares
502 .get(&key)
503 .is_some_and(|admitted| share_verifies(&common, admitted));
504 if verified {
505 survivor = true;
506 } else {
507 warn!(%view, ?key, "evicting unverifiable VID share squatting a shard range");
508 self.shares.remove(&key);
509 }
510 }
511 if survivor {
514 return;
515 }
516 if share_verifies(&common, &share.share) {
517 self.shares.insert(sender, share.share);
518 } else {
519 warn!(%view, ?sender, "dropping unverifiable VID share at intake conflict");
520 }
521 }
522
523 fn ranges(&self) -> impl Iterator<Item = &Range<usize>> {
524 self.shares.values().filter_map(AvidmGf2Share::range)
526 }
527
528 fn coverage(&self) -> usize {
531 self.ranges().map(ExactSizeIterator::len).sum()
532 }
533
534 fn has_enough_shares(&self) -> bool {
535 self.common
536 .as_ref()
537 .is_some_and(|common| self.coverage() >= common.param.recovery_threshold)
538 }
539}
540
541fn reconstruct<T: NodeType>(
549 view: ViewNumber,
550 epoch: EpochNumber,
551 payload_commitment: VidCommitment2,
552 common: AvidmGf2Common,
553 shares: Vec<(T::SignatureKey, AvidmGf2Share)>,
554 metadata: Metadata<T>,
555 tracer: Option<crate::leader_trace::LeaderTracerHandle>,
556) -> ReconstructResult<T> {
557 let (keys, shares): (Vec<_>, Vec<_>) = shares.into_iter().unzip();
558 if let Some(bytes) =
559 decode_and_recommit::<T>(view, &common, &shares, &payload_commitment, &metadata)
560 {
561 crate::trace_leader_event!(
562 tracer,
563 view,
564 crate::leader_trace::LeaderEvent::RecoverVMinus1DecodeEnd
565 );
566 let payload = T::BlockPayload::from_bytes(&bytes, &metadata);
567 let tx_commitments = payload.transaction_commitments(&metadata);
568 let output = ObtainedPayload {
569 view,
570 epoch,
571 payload_commitment,
572 payload,
573 metadata,
574 tx_commitments,
575 };
576 return Ok(output);
577 }
578
579 let bad_share_keys: Vec<_> = keys
580 .into_iter()
581 .zip(&shares)
582 .filter(|(_, share)| !share_verifies(&common, share))
583 .map(|(key, _)| key)
584 .collect();
585
586 let kind = if bad_share_keys.is_empty() {
587 warn!(
588 %view,
589 %payload_commitment,
590 "verified shares cannot decode to a payload matching the commitment"
591 );
592 VidReconstructErrorKind::Unrecoverable
593 } else {
594 warn!(
595 %view,
596 %payload_commitment,
597 ?bad_share_keys,
598 "weeded out VID shares that failed verification"
599 );
600 VidReconstructErrorKind::AwaitingShares
601 };
602 Err(VidReconstructError {
603 view,
604 payload_commitment,
605 kind,
606 bad_share_keys,
607 })
608}
609
610fn decode_and_recommit<T: NodeType>(
615 view: ViewNumber,
616 common: &AvidmGf2Common,
617 shares: &[AvidmGf2Share],
618 payload_commitment: &VidCommitment2,
619 metadata: &Metadata<T>,
620) -> Option<Vec<u8>> {
621 let bytes = match AvidmGf2Scheme::recover(common, shares) {
622 Ok(bytes) => bytes,
623 Err(err) => {
624 warn!(%view, %err, "VID recovery failed");
625 return None;
626 },
627 };
628 matches_commitment(
629 view,
630 &common.param,
631 &metadata.encode(),
632 &bytes,
633 payload_commitment,
634 )
635 .then_some(bytes)
636}
637
638pub(crate) fn matches_commitment(
649 view: ViewNumber,
650 param: &AvidmGf2Param,
651 ns_table: &[u8],
652 payload: &[u8],
653 payload_commitment: &VidCommitment2,
654) -> bool {
655 if let Some(claimed) = ns_table_payload_byte_len(ns_table)
656 && claimed != payload.len()
657 {
658 warn!(
659 %view,
660 claimed,
661 actual = payload.len(),
662 "namespace table is sized for another payload"
663 );
664 return false;
665 }
666 let ns_table = parse_ns_table(payload.len(), ns_table);
667 match AvidmGf2Scheme::commit(param, payload, ns_table) {
668 Ok((recomputed, _)) if recomputed == *payload_commitment => true,
669 Ok((recomputed, _)) => {
670 warn!(
671 %view,
672 expected = %payload_commitment,
673 %recomputed,
674 "payload does not match the payload commitment"
675 );
676 false
677 },
678 Err(err) => {
679 warn!(%view, %err, "failed to recommit VID payload");
680 false
681 },
682 }
683}
684
685fn share_verifies(common: &AvidmGf2Common, share: &AvidmGf2Share) -> bool {
688 matches!(
689 AvidmGf2Scheme::verify_share_with_verified_common(common, share),
690 Ok(Ok(()))
691 )
692}