sp_domains/
bundle_producer_election.rs

1use crate::{DomainId, OperatorPublicKey, ProofOfElection, StakeWeight};
2use parity_scale_codec::{Decode, Encode};
3use scale_info::TypeInfo;
4use sp_core::crypto::{VrfPublic, Wraps};
5use sp_core::sr25519::vrf::{VrfPreOutput, VrfSignature, VrfTranscript};
6use sp_runtime::traits::Zero;
7use subspace_core_primitives::hashes::Blake3Hash;
8
9const VRF_TRANSCRIPT_LABEL: &[u8] = b"bundle_producer_election";
10
11/// Generates a domain-specific vrf transcript from given global_challenge.
12pub fn make_transcript(domain_id: DomainId, global_challenge: &Blake3Hash) -> VrfTranscript {
13    VrfTranscript::new(
14        VRF_TRANSCRIPT_LABEL,
15        &[
16            (b"domain", &domain_id.to_le_bytes()),
17            (b"global_challenge", global_challenge.as_ref()),
18        ],
19    )
20}
21
22/// Returns the election threshold based on the operator stake proportion and slot probability.
23pub fn calculate_threshold(
24    operator_stake: StakeWeight,
25    total_domain_stake: StakeWeight,
26    bundle_slot_probability: (u64, u64),
27) -> Option<u128> {
28    // check ensures we do not panic if the either divisors are zero.
29    if total_domain_stake.is_zero() || bundle_slot_probability.1.is_zero() {
30        return None;
31    }
32
33    // The calculation is written for not causing the overflow, which might be harder to
34    // understand, the formula in a readable form is as follows:
35    //
36    //              bundle_slot_probability.0      operator_stake
37    // threshold =  ------------------------- * --------------------- * u128::MAX
38    //              bundle_slot_probability.1    total_domain_stake
39    //
40    // TODO: better to have more audits on this calculation.
41    Some(
42        u128::MAX / u128::from(bundle_slot_probability.1) * u128::from(bundle_slot_probability.0)
43            / total_domain_stake
44            * operator_stake,
45    )
46}
47
48pub fn is_below_threshold(vrf_output: &VrfPreOutput, threshold: u128) -> bool {
49    let vrf_pre_output = u128::from_le_bytes(
50        vrf_output
51            .0
52            .to_bytes()
53            .split_at(core::mem::size_of::<u128>())
54            .0
55            .try_into()
56            .expect("Slice splitted from VrfPreOutput must fit into u128; qed"),
57    );
58
59    vrf_pre_output < threshold
60}
61
62#[derive(Debug, Decode, Encode, TypeInfo, PartialEq, Eq, Clone)]
63pub struct BundleProducerElectionParams<Balance> {
64    pub total_domain_stake: Balance,
65    pub bundle_slot_probability: (u64, u64),
66}
67
68#[derive(Debug, Decode, Encode, TypeInfo, PartialEq, Eq, Clone)]
69pub enum ProofOfElectionError {
70    /// Invalid vrf proof.
71    BadVrfProof,
72    /// Threshold unsatisfied error.
73    ThresholdUnsatisfied,
74    /// Invalid Threshold.
75    InvalidThreshold,
76}
77
78/// Verify the vrf proof generated in the bundle election.
79pub(crate) fn verify_vrf_signature(
80    domain_id: DomainId,
81    public_key: &OperatorPublicKey,
82    vrf_signature: &VrfSignature,
83    global_challenge: &Blake3Hash,
84) -> Result<(), ProofOfElectionError> {
85    if !public_key.as_inner_ref().vrf_verify(
86        &make_transcript(domain_id, global_challenge).into(),
87        vrf_signature,
88    ) {
89        return Err(ProofOfElectionError::BadVrfProof);
90    }
91
92    Ok(())
93}
94
95pub fn check_proof_of_election(
96    operator_signing_key: &OperatorPublicKey,
97    bundle_slot_probability: (u64, u64),
98    proof_of_election: &ProofOfElection,
99    operator_stake: StakeWeight,
100    total_domain_stake: StakeWeight,
101) -> Result<(), ProofOfElectionError> {
102    proof_of_election.verify_vrf_signature(operator_signing_key)?;
103
104    let Some(threshold) =
105        calculate_threshold(operator_stake, total_domain_stake, bundle_slot_probability)
106    else {
107        return Err(ProofOfElectionError::InvalidThreshold);
108    };
109
110    if !is_below_threshold(&proof_of_election.vrf_signature.pre_output, threshold) {
111        return Err(ProofOfElectionError::ThresholdUnsatisfied);
112    }
113
114    Ok(())
115}