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