Skip to main content

hotshot_types/data/
vid_disperse.rs

1// Copyright (c) 2021-2024 Espresso Systems (espressosys.com)
2// This file is part of the HotShot repository.
3
4// You should have received a copy of the MIT License
5// along with the HotShot repository. If not, see <https://mit-license.org/>.
6
7//! This module provides types for VID disperse related data structures.
8//!
9//! We have three types of VID disperse related structs:
10//!
11//! 1. `ADVZ*`: VID V0, The most original VID scheme, which has guaranteed recovery but very inefficient.
12//! 2. `AvidM*`: VID V1, the efficient VID scheme, where we use it after the epocn upgrade. It's more
13//!    efficient but doesn't guarantee recovery. A VID V1 commitment could correspond to some junk
14//!    data, there'll be a proof of incorrect encoding in this case.
15//! 3. `AvidmGf2*`: VID V2, almost the same as VID V1 but we have a much more efficient recovery
16//!    implementation.
17
18use std::{collections::BTreeMap, fmt::Debug, hash::Hash, marker::PhantomData, time::Duration};
19
20use alloy::primitives::U256;
21use hotshot_utils::anytrace::*;
22use jf_advz::{VidDisperse as JfVidDisperse, VidScheme};
23use serde::{Deserialize, Serialize};
24use tokio::{task::spawn_blocking, time::Instant};
25
26use super::ns_table::parse_ns_table;
27use crate::{
28    PeerConfig,
29    data::{EpochNumber, ViewNumber},
30    epoch_membership::{EpochMembership, EpochMembershipCoordinator},
31    message::Proposal,
32    simple_vote::HasEpoch,
33    traits::{
34        BlockPayload,
35        block_contents::EncodeBytes,
36        node_implementation::NodeType,
37        signature_key::{SignatureKey, StakeTableEntryType},
38    },
39    vid::{
40        advz::{ADVZCommitment, ADVZCommon, ADVZScheme, ADVZShare, advz_scheme},
41        avidm::{AvidMCommitment, AvidMCommon, AvidMScheme, AvidMShare, init_avidm_param},
42        avidm_gf2::{
43            AvidmGf2Commitment, AvidmGf2Common, AvidmGf2Scheme, AvidmGf2Share, init_avidm_gf2_param,
44        },
45    },
46    vote::HasViewNumber,
47};
48
49impl<NODE: NodeType> HasEpoch for ADVZDisperse<NODE> {
50    fn epoch(&self) -> Option<EpochNumber> {
51        self.epoch
52    }
53}
54
55impl<NODE: NodeType> HasEpoch for AvidMDisperse<NODE> {
56    fn epoch(&self) -> Option<EpochNumber> {
57        self.epoch
58    }
59}
60
61impl<NODE: NodeType> HasEpoch for AvidMDisperseShare<NODE> {
62    fn epoch(&self) -> Option<EpochNumber> {
63        self.epoch
64    }
65}
66
67impl<NODE: NodeType> HasEpoch for AvidmGf2Disperse<NODE> {
68    fn epoch(&self) -> Option<EpochNumber> {
69        self.epoch
70    }
71}
72
73impl<NODE: NodeType> HasEpoch for AvidmGf2DisperseShare<NODE> {
74    fn epoch(&self) -> Option<EpochNumber> {
75        self.epoch
76    }
77}
78
79/// ADVZ dispersal data
80#[derive(Debug, Serialize, Deserialize, Clone, Eq, PartialEq, Hash)]
81pub struct ADVZDisperse<TYPES: NodeType> {
82    /// The view number for which this VID data is intended
83    pub view_number: ViewNumber,
84    /// Epoch the data of this proposal belongs to
85    pub epoch: Option<EpochNumber>,
86    /// Epoch to which the recipients of this VID belong to
87    pub target_epoch: Option<EpochNumber>,
88    /// VidCommitment calculated based on the number of nodes in `target_epoch`.
89    pub payload_commitment: ADVZCommitment,
90    /// A storage node's key and its corresponding VID share
91    pub shares: BTreeMap<TYPES::SignatureKey, ADVZShare>,
92    /// VID common data sent to all storage nodes
93    pub common: ADVZCommon,
94}
95
96impl<TYPES: NodeType> HasViewNumber for ADVZDisperse<TYPES> {
97    fn view_number(&self) -> ViewNumber {
98        self.view_number
99    }
100}
101
102impl<TYPES: NodeType> ADVZDisperse<TYPES> {
103    /// Create VID dispersal from a specified membership for the target epoch.
104    /// Uses the specified function to calculate share dispersal
105    /// Allows for more complex stake table functionality
106    async fn from_membership(
107        view_number: ViewNumber,
108        mut vid_disperse: JfVidDisperse<ADVZScheme>,
109        membership: &EpochMembershipCoordinator<TYPES>,
110        target_epoch: Option<EpochNumber>,
111        data_epoch: Option<EpochNumber>,
112    ) -> Result<Self> {
113        let shares = membership
114            .stake_table_for_epoch(target_epoch)?
115            .stake_table()
116            .map(|entry| entry.stake_table_entry.public_key())
117            .map(|node| (node.clone(), vid_disperse.shares.remove(0)))
118            .collect();
119
120        Ok(Self {
121            view_number,
122            shares,
123            common: vid_disperse.common,
124            payload_commitment: vid_disperse.commit,
125            epoch: data_epoch,
126            target_epoch,
127        })
128    }
129
130    /// Calculate the vid disperse information from the payload given a view, epoch and membership,
131    /// If the sender epoch is missing, it means it's the same as the target epoch.
132    ///
133    /// # Errors
134    /// Returns an error if the disperse or commitment calculation fails
135    #[allow(clippy::panic)]
136    pub async fn calculate_vid_disperse(
137        payload: &TYPES::BlockPayload,
138        membership: &EpochMembershipCoordinator<TYPES>,
139        view: ViewNumber,
140        target_epoch: Option<EpochNumber>,
141        data_epoch: Option<EpochNumber>,
142    ) -> Result<(Self, Duration)> {
143        let num_nodes = membership
144            .stake_table_for_epoch(target_epoch)?
145            .total_nodes();
146
147        let txns = payload.encode();
148
149        let now = Instant::now();
150        let vid_disperse = spawn_blocking(move || advz_scheme(num_nodes).disperse(&txns))
151            .await
152            .wrap()
153            .context(error!("Join error"))?
154            .wrap()
155            .context(|err| error!("Failed to calculate VID disperse. Error: {err}"))?;
156        let advz_scheme_duration = now.elapsed();
157
158        Ok((
159            Self::from_membership(view, vid_disperse, membership, target_epoch, data_epoch).await?,
160            advz_scheme_duration,
161        ))
162    }
163
164    /// This function splits a VID disperse into individual shares.
165    pub fn to_shares(self) -> Vec<ADVZDisperseShare<TYPES>> {
166        self.shares
167            .into_iter()
168            .map(|(recipient_key, share)| ADVZDisperseShare {
169                share,
170                recipient_key,
171                view_number: self.view_number,
172                common: self.common.clone(),
173                payload_commitment: self.payload_commitment,
174            })
175            .collect()
176    }
177
178    /// Split a VID disperse into a share proposal for each recipient.
179    pub fn to_share_proposals(
180        self,
181        signature: &<<TYPES as NodeType>::SignatureKey as SignatureKey>::PureAssembledSignatureType,
182    ) -> Vec<Proposal<TYPES, ADVZDisperseShare<TYPES>>> {
183        self.shares
184            .into_iter()
185            .map(|(recipient_key, share)| Proposal {
186                data: ADVZDisperseShare {
187                    share,
188                    recipient_key,
189                    view_number: self.view_number,
190                    payload_commitment: self.payload_commitment,
191                    common: self.common.clone(),
192                },
193                signature: signature.clone(),
194                _pd: PhantomData,
195            })
196            .collect()
197    }
198
199    /// Returns the payload length in bytes.
200    pub fn payload_byte_len(&self) -> u32 {
201        ADVZScheme::get_payload_byte_len(&self.common)
202    }
203}
204
205#[derive(Debug, Serialize, Deserialize, Clone, Eq, PartialEq, Hash)]
206/// ADVZ share and associated metadata for a single node
207pub struct ADVZDisperseShare<TYPES: NodeType> {
208    /// The view number for which this VID data is intended
209    pub view_number: ViewNumber,
210    /// Block payload commitment
211    pub payload_commitment: ADVZCommitment,
212    /// A storage node's key and its corresponding VID share
213    pub share: ADVZShare,
214    /// VID common data sent to all storage nodes
215    pub common: ADVZCommon,
216    /// a public key of the share recipient
217    pub recipient_key: TYPES::SignatureKey,
218}
219
220impl<NODE: NodeType> HasEpoch for ADVZDisperseShare<NODE> {
221    fn epoch(&self) -> Option<EpochNumber> {
222        None
223    }
224}
225
226impl<TYPES: NodeType> HasViewNumber for ADVZDisperseShare<TYPES> {
227    fn view_number(&self) -> ViewNumber {
228        self.view_number
229    }
230}
231
232impl<TYPES: NodeType> ADVZDisperseShare<TYPES> {
233    /// Consume `self` and return a `Proposal`
234    pub fn to_proposal(
235        self,
236        private_key: &<TYPES::SignatureKey as SignatureKey>::PrivateKey,
237    ) -> Option<Proposal<TYPES, Self>> {
238        let Ok(signature) =
239            TYPES::SignatureKey::sign(private_key, self.payload_commitment.as_ref())
240        else {
241            tracing::error!("VID: failed to sign dispersal share payload");
242            return None;
243        };
244        Some(Proposal {
245            signature,
246            _pd: PhantomData,
247            data: self,
248        })
249    }
250
251    /// Create `VidDisperse` out of an iterator to `VidDisperseShare`s
252    pub fn to_advz_disperse<'a, I>(mut it: I) -> Option<ADVZDisperse<TYPES>>
253    where
254        I: Iterator<Item = &'a Self>,
255    {
256        let first_vid_disperse_share = it.next()?.clone();
257        let mut share_map = BTreeMap::new();
258        share_map.insert(
259            first_vid_disperse_share.recipient_key,
260            first_vid_disperse_share.share,
261        );
262        let mut vid_disperse = ADVZDisperse {
263            view_number: first_vid_disperse_share.view_number,
264            epoch: None,
265            target_epoch: None,
266            payload_commitment: first_vid_disperse_share.payload_commitment,
267            common: first_vid_disperse_share.common,
268            shares: share_map,
269        };
270        it.for_each(|vid_disperse_share| {
271            vid_disperse.shares.insert(
272                vid_disperse_share.recipient_key.clone(),
273                vid_disperse_share.share.clone(),
274            );
275        });
276        Some(vid_disperse)
277    }
278
279    /// Check if vid common is consistent with the commitment.
280    pub fn is_consistent(&self) -> bool {
281        ADVZScheme::is_consistent(&self.payload_commitment, &self.common).is_ok()
282    }
283
284    /// Verify share assuming common data is already verified consistent.
285    /// Caller MUST call `is_consistent()` first.
286    pub fn verify_with_verified_common(&self) -> bool {
287        let total_weight = ADVZScheme::get_num_storage_nodes(&self.common) as usize;
288        advz_scheme(total_weight)
289            .verify_share(&self.share, &self.common, &self.payload_commitment)
290            .is_ok_and(|r| r.is_ok())
291    }
292
293    /// Internally verify the share given necessary information
294    pub fn verify(&self, _total_weight: usize) -> bool {
295        self.is_consistent() && self.verify_with_verified_common()
296    }
297
298    /// Returns the payload length in bytes.
299    pub fn payload_byte_len(&self) -> u32 {
300        ADVZScheme::get_payload_byte_len(&self.common)
301    }
302}
303
304/// AvidM dispersal data
305#[derive(Debug, Serialize, Deserialize, Clone, Eq, PartialEq, Hash)]
306pub struct AvidMDisperse<TYPES: NodeType> {
307    /// The view number for which this VID data is intended
308    pub view_number: ViewNumber,
309    /// Epoch the data of this proposal belongs to
310    pub epoch: Option<EpochNumber>,
311    /// Epoch to which the recipients of this VID belong to
312    pub target_epoch: Option<EpochNumber>,
313    /// VidCommitment calculated based on the number of nodes in `target_epoch`.
314    pub payload_commitment: AvidMCommitment,
315    /// A storage node's key and its corresponding VID share
316    pub shares: BTreeMap<TYPES::SignatureKey, AvidMShare>,
317    /// Length of payload in bytes
318    pub payload_byte_len: usize,
319    /// VID common data sent to all storage nodes
320    pub common: AvidMCommon,
321}
322
323impl<TYPES: NodeType> HasViewNumber for AvidMDisperse<TYPES> {
324    fn view_number(&self) -> ViewNumber {
325        self.view_number
326    }
327}
328
329/// The target total stake to scale to for VID.
330pub const VID_TARGET_TOTAL_STAKE: u32 = 1000;
331
332/// The weights and total weight used in VID calculations
333struct Weights {
334    // weights, in stake table order
335    weights: Vec<u32>,
336
337    // total weight
338    total_weight: usize,
339}
340
341pub fn vid_total_weight<'a, T, I>(stake_table: I, epoch: Option<EpochNumber>) -> usize
342where
343    T: NodeType,
344    I: Iterator<Item = &'a PeerConfig<T>>,
345{
346    if epoch.is_none() {
347        stake_table
348            .fold(U256::ZERO, |acc, entry| {
349                acc + entry.stake_table_entry.stake()
350            })
351            .to::<usize>()
352    } else {
353        let stake_table = stake_table.cloned().collect::<Vec<_>>();
354        approximate_weights(&stake_table[..]).total_weight
355    }
356}
357
358fn approximate_weights<TYPES: NodeType>(stake_table: &[PeerConfig<TYPES>]) -> Weights {
359    let total_stake = stake_table.iter().fold(U256::ZERO, |acc, entry| {
360        acc + entry.stake_table_entry.stake()
361    });
362
363    let mut total_weight: usize = 0;
364
365    // don't attempt to scale if the total stake is small enough
366    if total_stake <= U256::from(VID_TARGET_TOTAL_STAKE) {
367        let weights = stake_table
368            .iter()
369            .map(|entry| entry.stake_table_entry.stake().to::<u32>())
370            .collect();
371
372        // Note: this panics if `total_stake` exceeds `usize::MAX`, but this shouldn't happen.
373        total_weight = total_stake.to::<usize>();
374
375        Weights {
376            weights,
377            total_weight,
378        }
379    } else {
380        let weights = stake_table
381            .iter()
382            .map(|entry| {
383                let weight: U256 = ((entry.stake_table_entry.stake()
384                    * U256::from(VID_TARGET_TOTAL_STAKE))
385                    / total_stake)
386                    + U256::ONE;
387
388                // Note: this panics if `weight` exceeds `usize::MAX`, but this shouldn't happen.
389                total_weight += weight.to::<usize>();
390
391                // Note: this panics if `weight` exceeds `u32::MAX`, but this shouldn't happen
392                // and would likely cause a stack overflow in the VID calculation anyway
393                weight.to::<u32>()
394            })
395            .collect();
396
397        Weights {
398            weights,
399            total_weight,
400        }
401    }
402}
403
404impl<TYPES: NodeType> AvidMDisperse<TYPES> {
405    /// Create VID dispersal from a specified membership for the target epoch.
406    /// Uses the specified function to calculate share dispersal
407    /// Allows for more complex stake table functionality
408    async fn from_membership(
409        view_number: ViewNumber,
410        commit: AvidMCommitment,
411        shares: &[AvidMShare],
412        common: AvidMCommon,
413        membership: &EpochMembership<TYPES>,
414        target_epoch: Option<EpochNumber>,
415        data_epoch: Option<EpochNumber>,
416    ) -> Result<Self> {
417        let payload_byte_len = shares[0].payload_byte_len();
418        let shares = membership
419            .coordinator
420            .stake_table_for_epoch(target_epoch)?
421            .stake_table()
422            .map(|entry| entry.stake_table_entry.public_key())
423            .zip(shares)
424            .map(|(node, share)| (node.clone(), share.clone()))
425            .collect();
426
427        Ok(Self {
428            view_number,
429            shares,
430            payload_commitment: commit,
431            epoch: data_epoch,
432            target_epoch,
433            payload_byte_len,
434            common,
435        })
436    }
437
438    /// Calculate the vid disperse information from the payload given a view, epoch and membership,
439    /// If the sender epoch is missing, it means it's the same as the target epoch.
440    ///
441    /// # Errors
442    /// Returns an error if the disperse or commitment calculation fails
443    #[allow(clippy::panic)]
444    #[allow(clippy::single_range_in_vec_init)]
445    pub async fn calculate_vid_disperse(
446        payload: &TYPES::BlockPayload,
447        membership: &EpochMembershipCoordinator<TYPES>,
448        view: ViewNumber,
449        target_epoch: Option<EpochNumber>,
450        data_epoch: Option<EpochNumber>,
451        metadata: &<TYPES::BlockPayload as BlockPayload<TYPES>>::Metadata,
452    ) -> Result<(Self, Duration)> {
453        let target_mem = membership.stake_table_for_epoch(target_epoch)?;
454        let stake_table: Vec<_> = target_mem.stake_table().cloned().collect();
455        let approximate_weights = approximate_weights(&stake_table);
456
457        let txns = payload.encode();
458        let num_txns = txns.len();
459
460        let avidm_param = init_avidm_param(approximate_weights.total_weight)?;
461        let common = avidm_param.clone();
462
463        let ns_table = parse_ns_table(num_txns, &metadata.encode());
464        let ns_table_clone = ns_table.clone();
465
466        let now = Instant::now();
467        let (commit, shares) = spawn_blocking(move || {
468            AvidMScheme::ns_disperse(
469                &avidm_param,
470                &approximate_weights.weights,
471                &txns,
472                ns_table_clone,
473            )
474        })
475        .await
476        .wrap()
477        .context(error!("Join error"))?
478        .wrap()
479        .context(|err| error!("Failed to calculate VID disperse. Error: {err}"))?;
480        let ns_disperse_duration = now.elapsed();
481
482        Ok((
483            Self::from_membership(
484                view,
485                commit,
486                &shares,
487                common,
488                &target_mem,
489                target_epoch,
490                data_epoch,
491            )
492            .await?,
493            ns_disperse_duration,
494        ))
495    }
496
497    /// This function splits a VID disperse into individual shares.
498    pub fn to_shares(self) -> Vec<AvidMDisperseShare<TYPES>> {
499        self.shares
500            .into_iter()
501            .map(|(recipient_key, share)| AvidMDisperseShare {
502                share,
503                recipient_key,
504                view_number: self.view_number,
505                payload_commitment: self.payload_commitment,
506                epoch: self.epoch,
507                target_epoch: self.target_epoch,
508                common: self.common.clone(),
509            })
510            .collect()
511    }
512
513    /// Split a VID disperse into a share proposal for each recipient.
514    pub fn to_share_proposals(
515        self,
516        signature: &<<TYPES as NodeType>::SignatureKey as SignatureKey>::PureAssembledSignatureType,
517    ) -> Vec<Proposal<TYPES, AvidMDisperseShare<TYPES>>> {
518        self.shares
519            .into_iter()
520            .map(|(recipient_key, share)| Proposal {
521                data: AvidMDisperseShare {
522                    share,
523                    recipient_key,
524                    view_number: self.view_number,
525                    payload_commitment: self.payload_commitment,
526                    epoch: self.epoch,
527                    target_epoch: self.target_epoch,
528                    common: self.common.clone(),
529                },
530                signature: signature.clone(),
531                _pd: PhantomData,
532            })
533            .collect()
534    }
535
536    /// Construct a VID disperse from an iterator of disperse shares.
537    pub fn try_from_shares<'a, I>(mut it: I) -> Option<Self>
538    where
539        I: Iterator<Item = &'a AvidMDisperseShare<TYPES>>,
540    {
541        let first_vid_disperse_share = it.next()?.clone();
542        let payload_byte_len = first_vid_disperse_share.share.payload_byte_len();
543        let mut share_map = BTreeMap::new();
544        share_map.insert(
545            first_vid_disperse_share.recipient_key,
546            first_vid_disperse_share.share,
547        );
548        let mut vid_disperse = Self {
549            view_number: first_vid_disperse_share.view_number,
550            epoch: first_vid_disperse_share.epoch,
551            target_epoch: first_vid_disperse_share.target_epoch,
552            payload_commitment: first_vid_disperse_share.payload_commitment,
553            shares: share_map,
554            payload_byte_len,
555            common: first_vid_disperse_share.common,
556        };
557        it.for_each(|vid_disperse_share| {
558            vid_disperse.shares.insert(
559                vid_disperse_share.recipient_key.clone(),
560                vid_disperse_share.share.clone(),
561            );
562        });
563        Some(vid_disperse)
564    }
565
566    /// Returns the payload length in bytes.
567    pub fn payload_byte_len(&self) -> u32 {
568        self.payload_byte_len as u32
569    }
570}
571
572#[derive(Debug, Serialize, Deserialize, Clone, Eq, PartialEq, Hash)]
573/// VID share and associated metadata for a single node
574pub struct AvidMDisperseShare<TYPES: NodeType> {
575    /// The view number for which this VID data is intended
576    pub view_number: ViewNumber,
577    /// The epoch number for which this VID data belongs to
578    pub epoch: Option<EpochNumber>,
579    /// The epoch number to which the recipient of this VID belongs to
580    pub target_epoch: Option<EpochNumber>,
581    /// Block payload commitment
582    pub payload_commitment: AvidMCommitment,
583    /// A storage node's key and its corresponding VID share
584    pub share: AvidMShare,
585    /// a public key of the share recipient
586    pub recipient_key: TYPES::SignatureKey,
587    /// VID common data sent to all storage nodes
588    pub common: AvidMCommon,
589}
590
591impl<TYPES: NodeType> HasViewNumber for AvidMDisperseShare<TYPES> {
592    fn view_number(&self) -> ViewNumber {
593        self.view_number
594    }
595}
596
597impl<TYPES: NodeType> AvidMDisperseShare<TYPES> {
598    /// Consume `self` and return a `Proposal`
599    pub fn to_proposal(
600        self,
601        private_key: &<TYPES::SignatureKey as SignatureKey>::PrivateKey,
602    ) -> Option<Proposal<TYPES, Self>> {
603        let Ok(signature) =
604            TYPES::SignatureKey::sign(private_key, self.payload_commitment.as_ref())
605        else {
606            tracing::error!("VID: failed to sign dispersal share payload");
607            return None;
608        };
609        Some(Proposal {
610            signature,
611            _pd: PhantomData,
612            data: self,
613        })
614    }
615
616    /// Returns the payload length in bytes.
617    pub fn payload_byte_len(&self) -> u32 {
618        self.share.payload_byte_len() as u32
619    }
620
621    /// Check if vid common is consistent with the commitment.
622    /// For AvidM, ns_commits is inside the share, so there's no separate consistency check.
623    pub fn is_consistent(&self) -> bool {
624        true
625    }
626
627    /// Verify share assuming common data is already verified consistent.
628    /// For AvidM, this is equivalent to the full verify since there's
629    /// no separate consistency check (ns_commits is inside the share).
630    pub fn verify_with_verified_common(&self) -> bool {
631        AvidMScheme::verify_share(&self.common, &self.payload_commitment, &self.share)
632            .is_ok_and(|r| r.is_ok())
633    }
634
635    /// Internally verify the share given necessary information
636    pub fn verify(&self, _total_weight: usize) -> bool {
637        self.is_consistent() && self.verify_with_verified_common()
638    }
639}
640
641/// AvidmGf2 dispersal data
642#[derive(Debug, Serialize, Deserialize, Clone, Eq, PartialEq, Hash)]
643pub struct AvidmGf2Disperse<TYPES: NodeType> {
644    /// The view number for which this VID data is intended
645    pub view_number: ViewNumber,
646    /// Epoch the data of this proposal belongs to
647    pub epoch: Option<EpochNumber>,
648    /// Epoch to which the recipients of this VID belong to
649    pub target_epoch: Option<EpochNumber>,
650    /// VidCommitment calculated based on the number of nodes in `target_epoch`.
651    pub payload_commitment: AvidmGf2Commitment,
652    /// A storage node's key and its corresponding VID share
653    pub shares: BTreeMap<TYPES::SignatureKey, AvidmGf2Share>,
654    /// Length of payload in bytes
655    pub payload_byte_len: usize,
656    /// VID common data sent to all storage nodes
657    pub common: AvidmGf2Common,
658}
659
660impl<TYPES: NodeType> HasViewNumber for AvidmGf2Disperse<TYPES> {
661    fn view_number(&self) -> ViewNumber {
662        self.view_number
663    }
664}
665
666impl<TYPES: NodeType> AvidmGf2Disperse<TYPES> {
667    /// Create VID dispersal from a specified membership for the target epoch.
668    /// Uses the specified function to calculate share dispersal
669    /// Allows for more complex stake table functionality
670    async fn from_membership(
671        view_number: ViewNumber,
672        commit: AvidmGf2Commitment,
673        shares: &[AvidmGf2Share],
674        common: AvidmGf2Common,
675        membership: &EpochMembership<TYPES>,
676        target_epoch: Option<EpochNumber>,
677        data_epoch: Option<EpochNumber>,
678    ) -> Result<Self> {
679        let payload_byte_len = common.payload_byte_len();
680        let shares = membership
681            .coordinator
682            .stake_table_for_epoch(target_epoch)?
683            .stake_table()
684            .map(|entry| entry.stake_table_entry.public_key())
685            .zip(shares)
686            .map(|(node, share)| (node.clone(), share.clone()))
687            .collect();
688
689        Ok(Self {
690            view_number,
691            shares,
692            payload_commitment: commit,
693            epoch: data_epoch,
694            target_epoch,
695            payload_byte_len,
696            common,
697        })
698    }
699
700    /// Calculate the vid disperse information from the payload given a view, epoch and membership,
701    /// If the sender epoch is missing, it means it's the same as the target epoch.
702    ///
703    /// # Errors
704    /// Returns an error if the disperse or commitment calculation fails
705    pub async fn calculate_vid_disperse(
706        payload: &TYPES::BlockPayload,
707        membership: &EpochMembershipCoordinator<TYPES>,
708        view: ViewNumber,
709        target_epoch: Option<EpochNumber>,
710        data_epoch: Option<EpochNumber>,
711        metadata: &<TYPES::BlockPayload as BlockPayload<TYPES>>::Metadata,
712    ) -> Result<(Self, Duration)> {
713        let target_mem = membership.stake_table_for_epoch(target_epoch)?;
714        let stake_table: Vec<_> = target_mem.stake_table().cloned().collect();
715        let approximate_weights = approximate_weights(&stake_table);
716
717        let txns = payload.encode();
718        let num_txns = txns.len();
719
720        let avidm_param = init_avidm_gf2_param(approximate_weights.total_weight)?;
721
722        let ns_table = parse_ns_table(num_txns, &metadata.encode());
723        let ns_table_clone = ns_table.clone();
724
725        let now = Instant::now();
726        let (commit, common, shares) = spawn_blocking(move || {
727            AvidmGf2Scheme::ns_disperse(
728                &avidm_param,
729                &approximate_weights.weights,
730                &txns,
731                ns_table_clone,
732            )
733        })
734        .await
735        .wrap()
736        .context(error!("Join error"))?
737        .wrap()
738        .context(|err| error!("Failed to calculate VID disperse. Error: {err}"))?;
739        let ns_disperse_duration = now.elapsed();
740
741        Ok((
742            Self::from_membership(
743                view,
744                commit,
745                &shares,
746                common,
747                &target_mem,
748                target_epoch,
749                data_epoch,
750            )
751            .await?,
752            ns_disperse_duration,
753        ))
754    }
755
756    /// This function splits a VID disperse into individual shares.
757    pub fn to_shares(self) -> Vec<AvidmGf2DisperseShare<TYPES>> {
758        self.shares
759            .into_iter()
760            .map(|(recipient_key, share)| AvidmGf2DisperseShare {
761                share,
762                recipient_key,
763                view_number: self.view_number,
764                payload_commitment: self.payload_commitment,
765                epoch: self.epoch,
766                target_epoch: self.target_epoch,
767                common: self.common.clone(),
768            })
769            .collect()
770    }
771
772    /// Split a VID disperse into a share proposal for each recipient.
773    pub fn to_share_proposals(
774        self,
775        signature: &<<TYPES as NodeType>::SignatureKey as SignatureKey>::PureAssembledSignatureType,
776    ) -> Vec<Proposal<TYPES, AvidmGf2DisperseShare<TYPES>>> {
777        self.shares
778            .into_iter()
779            .map(|(recipient_key, share)| Proposal {
780                data: AvidmGf2DisperseShare {
781                    share,
782                    recipient_key,
783                    view_number: self.view_number,
784                    payload_commitment: self.payload_commitment,
785                    epoch: self.epoch,
786                    target_epoch: self.target_epoch,
787                    common: self.common.clone(),
788                },
789                signature: signature.clone(),
790                _pd: PhantomData,
791            })
792            .collect()
793    }
794
795    /// Construct a VID disperse from an iterator of disperse shares.
796    pub fn try_from_shares<'a, I>(mut it: I) -> Option<Self>
797    where
798        I: Iterator<Item = &'a AvidmGf2DisperseShare<TYPES>>,
799    {
800        let first_vid_disperse_share = it.next()?.clone();
801        let payload_byte_len = first_vid_disperse_share.common.payload_byte_len();
802        let mut share_map = BTreeMap::new();
803        share_map.insert(
804            first_vid_disperse_share.recipient_key,
805            first_vid_disperse_share.share,
806        );
807        let mut vid_disperse = Self {
808            view_number: first_vid_disperse_share.view_number,
809            epoch: first_vid_disperse_share.epoch,
810            target_epoch: first_vid_disperse_share.target_epoch,
811            payload_commitment: first_vid_disperse_share.payload_commitment,
812            shares: share_map,
813            payload_byte_len,
814            common: first_vid_disperse_share.common,
815        };
816        it.for_each(|vid_disperse_share| {
817            vid_disperse.shares.insert(
818                vid_disperse_share.recipient_key.clone(),
819                vid_disperse_share.share.clone(),
820            );
821        });
822        Some(vid_disperse)
823    }
824
825    /// Returns the payload length in bytes.
826    pub fn payload_byte_len(&self) -> u32 {
827        self.payload_byte_len as u32
828    }
829}
830
831#[derive(Debug, Serialize, Deserialize, Clone, Eq, PartialEq, Hash)]
832/// VID share and associated metadata for a single node
833pub struct AvidmGf2DisperseShare<TYPES: NodeType> {
834    /// The view number for which this VID data is intended
835    pub view_number: ViewNumber,
836    /// The epoch number for which this VID data belongs to
837    pub epoch: Option<EpochNumber>,
838    /// The epoch number to which the recipient of this VID belongs to
839    pub target_epoch: Option<EpochNumber>,
840    /// Block payload commitment
841    pub payload_commitment: AvidmGf2Commitment,
842    /// A storage node's key and its corresponding VID share
843    pub share: AvidmGf2Share,
844    /// a public key of the share recipient
845    pub recipient_key: TYPES::SignatureKey,
846    /// VID common data sent to all storage nodes
847    pub common: AvidmGf2Common,
848}
849
850impl<TYPES: NodeType> HasViewNumber for AvidmGf2DisperseShare<TYPES> {
851    fn view_number(&self) -> ViewNumber {
852        self.view_number
853    }
854}
855
856impl<TYPES: NodeType> AvidmGf2DisperseShare<TYPES> {
857    /// Consume `self` and return a `Proposal`
858    pub fn to_proposal(
859        self,
860        private_key: &<TYPES::SignatureKey as SignatureKey>::PrivateKey,
861    ) -> Option<Proposal<TYPES, Self>> {
862        let Ok(signature) =
863            TYPES::SignatureKey::sign(private_key, self.payload_commitment.as_ref())
864        else {
865            tracing::error!("VID: failed to sign dispersal share payload");
866            return None;
867        };
868        Some(Proposal {
869            signature,
870            _pd: PhantomData,
871            data: self,
872        })
873    }
874    /// Returns the payload length in bytes.
875    pub fn payload_byte_len(&self) -> u32 {
876        self.common.payload_byte_len() as u32
877    }
878    /// Check if vid common is consistent with the commitment.
879    pub fn is_consistent(&self) -> bool {
880        AvidmGf2Scheme::is_consistent(&self.payload_commitment, &self.common)
881    }
882
883    /// Verify share assuming common data is already verified consistent.
884    /// Caller MUST call `is_consistent()` first.
885    pub fn verify_with_verified_common(&self) -> bool {
886        AvidmGf2Scheme::verify_share_with_verified_common(&self.common, &self.share)
887            .is_ok_and(|r| r.is_ok())
888    }
889
890    /// Internally verify the share given necessary information
891    pub fn verify(&self, _total_weight: usize) -> bool {
892        self.is_consistent() && self.verify_with_verified_common()
893    }
894}