sp_domains/
bundle_producer_election.rs1use 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
11pub 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
22pub fn calculate_threshold(
24 operator_stake: StakeWeight,
25 total_domain_stake: StakeWeight,
26 bundle_slot_probability: (u64, u64),
27) -> Option<u128> {
28 if total_domain_stake.is_zero() || bundle_slot_probability.1.is_zero() {
30 return None;
31 }
32
33 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 BadVrfProof,
72 ThresholdUnsatisfied,
74 InvalidThreshold,
76}
77
78pub(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}