diff --git a/crates/pallet-domains/src/lib.rs b/crates/pallet-domains/src/lib.rs index 6740a8a77d..e35fe2eb76 100644 --- a/crates/pallet-domains/src/lib.rs +++ b/crates/pallet-domains/src/lib.rs @@ -34,10 +34,12 @@ use frame_support::traits::Get; use frame_system::offchain::SubmitTransaction; pub use pallet::*; use sp_core::H256; +use sp_domains::bundle_producer_election::BundleProducerElectionParams; use sp_domains::fraud_proof::FraudProof; -use sp_domains::{DomainId, OpaqueBundle, OperatorId}; +use sp_domains::{DomainId, OpaqueBundle, OperatorId, OperatorPublicKey}; use sp_runtime::traits::{BlockNumberProvider, CheckedSub, One, Zero}; use sp_runtime::transaction_validity::TransactionValidityError; +use sp_runtime::RuntimeAppPublic; use sp_std::vec::Vec; use subspace_core_primitives::U256; @@ -56,6 +58,9 @@ pub trait FreezeIdentifier { #[frame_support::pallet] mod pallet { + // TODO: a complaint on `submit_bundle` call, revisit once new v2 features are complete. + #![allow(clippy::large_enum_variant)] + use crate::domain_registry::{ do_instantiate_domain, DomainConfig, DomainObject, Error as DomainRegistryError, }; @@ -80,15 +85,14 @@ mod pallet { use sp_core::H256; use sp_domains::fraud_proof::FraudProof; use sp_domains::transaction::InvalidTransactionCode; - use sp_domains::{ - DomainId, GenesisDomain, OpaqueBundle, OperatorId, OperatorPublicKey, RuntimeId, - RuntimeType, - }; + use sp_domains::{DomainId, GenesisDomain, OpaqueBundle, OperatorId, RuntimeId, RuntimeType}; use sp_runtime::traits::{ AtLeast32BitUnsigned, BlockNumberProvider, Bounded, CheckEqual, MaybeDisplay, SimpleBitOps, Zero, }; + use sp_runtime::SaturatedConversion; use sp_std::fmt::Debug; + use sp_std::vec; use sp_std::vec::Vec; use subspace_core_primitives::U256; @@ -292,8 +296,8 @@ mod pallet { #[derive(TypeInfo, Encode, Decode, PalletError, Debug, PartialEq)] pub enum BundleError { - /// The signer of bundle is unexpected. - UnexpectedSigner, + /// Can not find the operator for given operator id. + InvalidOperatorId, /// Invalid bundle signature. BadSignature, /// Invalid vrf proof. @@ -376,7 +380,7 @@ mod pallet { BundleStored { domain_id: DomainId, bundle_hash: H256, - bundle_author: OperatorPublicKey, + bundle_author: OperatorId, }, DomainRuntimeCreated { runtime_id: RuntimeId, @@ -486,7 +490,7 @@ mod pallet { Self::deposit_event(Event::BundleStored { domain_id, bundle_hash, - bundle_author: opaque_bundle.into_operator_public_key(), + bundle_author: opaque_bundle.operator_id(), }); Ok(()) @@ -709,12 +713,44 @@ mod pallet { // Instantiate the genesis domain let domain_config = DomainConfig::from_genesis::(genesis_domain, runtime_id); - do_instantiate_domain::( - domain_config, - genesis_domain.owner_account_id.clone(), - Zero::zero(), + let domain_owner = genesis_domain.owner_account_id.clone(); + let domain_id = + do_instantiate_domain::(domain_config, domain_owner.clone(), Zero::zero()) + .expect("Genesis domain instantiation must always succeed"); + + // Register domain_owner as the genesis operator. + let operator_config = OperatorConfig { + signing_key: genesis_domain.signing_key.clone(), + minimum_nominator_stake: genesis_domain + .minimum_nominator_stake + .saturated_into(), + nomination_tax: genesis_domain.nomination_tax, + }; + let operator_stake = T::MinOperatorStake::get(); + let operator_id = do_register_operator::( + domain_owner, + domain_id, + operator_stake, + operator_config, ) - .expect("Genesis domain instantiation must always succeed"); + .expect("Genesis operator registration must succeed"); + + // TODO: Enact the epoch transition logic properly. + Operators::::mutate(operator_id, |maybe_operator| { + let operator = maybe_operator + .as_mut() + .expect("Genesis operator must exist"); + operator.current_total_stake = operator_stake; + }); + DomainStakingSummary::::insert( + domain_id, + StakingSummary { + current_epoch_index: 0, + current_total_stake: operator_stake, + current_operators: vec![operator_id], + next_operators: vec![], + }, + ); } } } @@ -816,6 +852,27 @@ impl Pallet { .unwrap_or_else(Self::initial_tx_range) } + pub fn bundle_producer_election_params( + domain_id: DomainId, + ) -> Option>> { + match ( + DomainRegistry::::get(domain_id), + DomainStakingSummary::::get(domain_id), + ) { + (Some(domain_object), Some(stake_summary)) => Some(BundleProducerElectionParams { + current_operators: stake_summary.current_operators, + total_domain_stake: stake_summary.current_total_stake, + bundle_slot_probability: domain_object.domain_config.bundle_slot_probability, + }), + _ => None, + } + } + + pub fn operator(operator_id: OperatorId) -> Option<(OperatorPublicKey, BalanceOf)> { + Operators::::get(operator_id) + .map(|operator| (operator.signing_key, operator.current_total_stake)) + } + fn pre_dispatch_submit_bundle( _opaque_bundle: &OpaqueBundle, ) -> Result<(), TransactionValidityError> { @@ -830,7 +887,11 @@ impl Pallet { extrinsics: _, }: &OpaqueBundle, ) -> Result<(), BundleError> { - if !sealed_header.verify_signature() { + let signing_key = Operators::::get(sealed_header.header.proof_of_election.operator_id) + .map(|operator| operator.signing_key) + .ok_or(BundleError::InvalidOperatorId)?; + + if !signing_key.verify(&sealed_header.pre_hash(), &sealed_header.signature) { return Err(BundleError::BadSignature); } @@ -868,6 +929,8 @@ impl Pallet { // TODO: Implement bundle validation. + // TODO: Verify ProofOfElection + Ok(()) } @@ -917,7 +980,7 @@ where pub fn submit_bundle_unsigned( opaque_bundle: OpaqueBundle, ) { - let slot = opaque_bundle.sealed_header.header.slot_number; + let slot = opaque_bundle.sealed_header.slot_number(); let extrincis_count = opaque_bundle.extrinsics.len(); let call = Call::submit_bundle { opaque_bundle }; diff --git a/crates/pallet-domains/src/tests.rs b/crates/pallet-domains/src/tests.rs index 6a2d3e494e..015428ffe7 100644 --- a/crates/pallet-domains/src/tests.rs +++ b/crates/pallet-domains/src/tests.rs @@ -7,9 +7,8 @@ use scale_info::TypeInfo; use sp_core::crypto::Pair; use sp_core::{Get, H256, U256}; use sp_domains::{ - create_dummy_bundle_with_receipts_generic, BundleHeader, BundleSolution, DomainId, - DomainsFreezeIdentifier, ExecutionReceipt, OpaqueBundle, OperatorId, OperatorPair, - SealedBundleHeader, + create_dummy_bundle_with_receipts_generic, BundleHeader, DomainId, DomainsFreezeIdentifier, + ExecutionReceipt, OpaqueBundle, OperatorId, OperatorPair, ProofOfElection, SealedBundleHeader, }; use sp_runtime::testing::Header; use sp_runtime::traits::{BlakeTwo256, IdentityLookup}; @@ -202,9 +201,8 @@ fn create_dummy_bundle( let header = BundleHeader { consensus_block_number, consensus_block_hash, - slot_number: 0u64, extrinsics_root: Default::default(), - bundle_solution: BundleSolution::dummy(domain_id, pair.public()), + proof_of_election: ProofOfElection::dummy(domain_id, 0u64), }; let signature = pair.sign(header.hash().as_ref()); @@ -231,7 +229,9 @@ fn create_dummy_bundle_with_receipts( ) } +// TODO: Unblock once bundle producer election v2 is finished. #[test] +#[ignore] fn test_stale_bundle_should_be_rejected() { // Small macro in order to be more readable. // diff --git a/crates/sp-domains/src/bundle_election.rs b/crates/sp-domains/src/bundle_election.rs deleted file mode 100644 index 05ade45458..0000000000 --- a/crates/sp-domains/src/bundle_election.rs +++ /dev/null @@ -1,99 +0,0 @@ -use crate::{DomainId, OperatorPublicKey, StakeWeight}; -use parity_scale_codec::{Decode, Encode}; -use scale_info::TypeInfo; -use sp_core::crypto::{VrfPublic, Wraps}; -use sp_core::sr25519::vrf::{VrfInput, VrfOutput, VrfSignature}; -use subspace_core_primitives::crypto::blake2b_256_hash_list; -use subspace_core_primitives::Blake2b256Hash; - -const VRF_TRANSCRIPT_LABEL: &[u8] = b"executor"; - -const LOCAL_RANDOMNESS_CONTEXT: &[u8] = b"bundle_election_local_randomness_context"; - -type LocalRandomness = [u8; core::mem::size_of::()]; - -fn derive_local_randomness( - vrf_output: &VrfOutput, - public_key: &OperatorPublicKey, - global_challenge: &Blake2b256Hash, -) -> Result { - vrf_output.make_bytes( - LOCAL_RANDOMNESS_CONTEXT, - &make_local_randomness_input(global_challenge), - public_key.as_ref(), - ) -} - -/// Returns the domain-specific solution for the challenge of producing a bundle. -pub fn derive_bundle_election_solution( - domain_id: DomainId, - vrf_output: &VrfOutput, - public_key: &OperatorPublicKey, - global_challenge: &Blake2b256Hash, -) -> Result { - let local_randomness = derive_local_randomness(vrf_output, public_key, global_challenge)?; - let local_domain_randomness = - blake2b_256_hash_list(&[&domain_id.to_le_bytes(), &local_randomness]); - - let election_solution = u128::from_le_bytes( - local_domain_randomness - .split_at(core::mem::size_of::()) - .0 - .try_into() - .expect("Local domain randomness must fit into u128; qed"), - ); - - Ok(election_solution) -} - -/// Returns the election threshold based on the stake weight proportion and slot probability. -pub fn calculate_bundle_election_threshold( - stake_weight: StakeWeight, - total_stake_weight: StakeWeight, - slot_probability: (u64, u64), -) -> u128 { - // The calculation is written for not causing the overflow, which might be harder to - // understand, the formula in a readable form is as followes: - // - // slot_probability.0 stake_weight - // threshold = ------------------ * --------------------- * u128::MAX - // slot_probability.1 total_stake_weight - // - // TODO: better to have more audits on this calculation. - u128::MAX / u128::from(slot_probability.1) * u128::from(slot_probability.0) / total_stake_weight - * stake_weight -} - -pub fn is_election_solution_within_threshold(election_solution: u128, threshold: u128) -> bool { - election_solution <= threshold -} - -/// Make a VRF inout. -pub fn make_local_randomness_input(global_challenge: &Blake2b256Hash) -> VrfInput { - VrfInput::new( - VRF_TRANSCRIPT_LABEL, - &[(b"global challenge", global_challenge)], - ) -} - -#[derive(Debug, Decode, Encode, TypeInfo, PartialEq, Eq, Clone)] -pub enum VrfProofError { - /// Invalid vrf proof. - BadProof, -} - -/// Verify the vrf proof generated in the bundle election. -pub(crate) fn verify_vrf_proof( - public_key: &OperatorPublicKey, - vrf_signature: &VrfSignature, - global_challenge: &Blake2b256Hash, -) -> Result<(), VrfProofError> { - if !public_key.as_inner_ref().vrf_verify( - &make_local_randomness_input(global_challenge).into(), - vrf_signature, - ) { - return Err(VrfProofError::BadProof); - } - - Ok(()) -} diff --git a/crates/sp-domains/src/bundle_producer_election.rs b/crates/sp-domains/src/bundle_producer_election.rs new file mode 100644 index 0000000000..ca36261efd --- /dev/null +++ b/crates/sp-domains/src/bundle_producer_election.rs @@ -0,0 +1,83 @@ +use crate::{DomainId, OperatorId, OperatorPublicKey, StakeWeight}; +use parity_scale_codec::{Decode, Encode}; +use scale_info::TypeInfo; +use sp_core::crypto::{VrfPublic, Wraps}; +use sp_core::sr25519::vrf::{VrfOutput, VrfSignature, VrfTranscript}; +use sp_std::vec::Vec; +use subspace_core_primitives::Blake2b256Hash; + +const VRF_TRANSCRIPT_LABEL: &[u8] = b"bundle_producer_election"; + +/// Generates a domain-specific vrf transcript from given global_challenge. +pub fn make_transcript(domain_id: DomainId, global_challenge: &Blake2b256Hash) -> VrfTranscript { + VrfTranscript::new( + VRF_TRANSCRIPT_LABEL, + &[ + (b"domain", &domain_id.to_le_bytes()), + (b"global_challenge", global_challenge.as_ref()), + ], + ) +} + +/// Returns the election threshold based on the operator stake proportion and slot probability. +pub fn calculate_threshold( + operator_stake: StakeWeight, + total_domain_stake: StakeWeight, + bundle_slot_probability: (u64, u64), +) -> u128 { + // The calculation is written for not causing the overflow, which might be harder to + // understand, the formula in a readable form is as followes: + // + // bundle_slot_probability.0 operator_stake + // threshold = ------------------------- * --------------------- * u128::MAX + // bundle_slot_probability.1 total_domain_stake + // + // TODO: better to have more audits on this calculation. + u128::MAX / u128::from(bundle_slot_probability.1) * u128::from(bundle_slot_probability.0) + / total_domain_stake + * operator_stake +} + +pub fn is_below_threshold(vrf_output: &VrfOutput, threshold: u128) -> bool { + let vrf_output = u128::from_le_bytes( + vrf_output + .0 + .to_bytes() + .split_at(core::mem::size_of::()) + .0 + .try_into() + .expect("Slice splitted from VrfOutput must fit into u128; qed"), + ); + + vrf_output < threshold +} + +#[derive(Debug, Decode, Encode, TypeInfo, PartialEq, Eq, Clone)] +pub struct BundleProducerElectionParams { + pub current_operators: Vec, + pub total_domain_stake: Balance, + pub bundle_slot_probability: (u64, u64), +} + +#[derive(Debug, Decode, Encode, TypeInfo, PartialEq, Eq, Clone)] +pub enum VrfProofError { + /// Invalid vrf proof. + BadProof, +} + +/// Verify the vrf proof generated in the bundle election. +pub(crate) fn verify_vrf_proof( + domain_id: DomainId, + public_key: &OperatorPublicKey, + vrf_signature: &VrfSignature, + global_challenge: &Blake2b256Hash, +) -> Result<(), VrfProofError> { + if !public_key.as_inner_ref().vrf_verify( + &make_transcript(domain_id, global_challenge).into(), + vrf_signature, + ) { + return Err(VrfProofError::BadProof); + } + + Ok(()) +} diff --git a/crates/sp-domains/src/fraud_proof.rs b/crates/sp-domains/src/fraud_proof.rs index dafafc00a8..32f1982199 100644 --- a/crates/sp-domains/src/fraud_proof.rs +++ b/crates/sp-domains/src/fraud_proof.rs @@ -1,5 +1,3 @@ -#[cfg(any(feature = "std", feature = "runtime-benchmarks"))] -use crate::BundleHeader; use crate::{DomainId, SealedBundleHeader}; use parity_scale_codec::{Decode, Encode}; use scale_info::TypeInfo; @@ -184,6 +182,8 @@ pub enum VerificationError { } /// Fraud proof. +// TODO: Revisit when fraud proof v2 is implemented. +#[allow(clippy::large_enum_variant)] #[derive(Debug, Decode, Encode, TypeInfo, PartialEq, Eq, Clone)] pub enum FraudProof { InvalidStateTransition(InvalidStateTransitionProof), @@ -279,36 +279,6 @@ impl + Encode, Hash: Clone + Default + Encode> pub fn hash(&self) -> H256 { BlakeTwo256::hash_of(self) } - - // TODO: remove this later. - /// Constructs a dummy bundle equivocation proof. - #[cfg(any(feature = "std", feature = "runtime-benchmarks"))] - pub fn dummy_at(domain_id: DomainId, slot_number: u64) -> Self { - use sp_application_crypto::UncheckedFrom; - - let dummy_header = SealedBundleHeader { - header: BundleHeader { - consensus_block_number: Number::from(0u32), - consensus_block_hash: Hash::default(), - slot_number, - extrinsics_root: H256::default(), - bundle_solution: crate::BundleSolution::dummy( - domain_id, - crate::OperatorPublicKey::unchecked_from([0u8; 32]), - ), - }, - signature: crate::OperatorSignature::unchecked_from([0u8; 64]), - }; - - Self { - domain_id, - offender: AccountId::decode(&mut sp_runtime::traits::TrailingZeroInput::zeroes()) - .expect("Failed to create zero account"), - slot: slot_number.into(), - first_header: dummy_header.clone(), - second_header: dummy_header, - } - } } /// Represents an invalid transaction proof. diff --git a/crates/sp-domains/src/lib.rs b/crates/sp-domains/src/lib.rs index 6c29a69a7f..ece8f53e3b 100644 --- a/crates/sp-domains/src/lib.rs +++ b/crates/sp-domains/src/lib.rs @@ -17,13 +17,12 @@ #![cfg_attr(not(feature = "std"), no_std)] -pub mod bundle_election; +pub mod bundle_producer_election; pub mod fraud_proof; pub mod merkle_tree; pub mod transaction; -use bundle_election::VrfProofError; -use merkle_tree::Witness; +use bundle_producer_election::{BundleProducerElectionParams, VrfProofError}; use parity_scale_codec::{Decode, Encode, MaxEncodedLen}; use scale_info::TypeInfo; use serde::{Deserialize, Serialize}; @@ -35,15 +34,14 @@ use sp_runtime::generic::OpaqueDigestItemId; use sp_runtime::traits::{ BlakeTwo256, Block as BlockT, CheckedAdd, Hash as HashT, NumberFor, Zero, }; -use sp_runtime::{DigestItem, OpaqueExtrinsic, RuntimeAppPublic}; +use sp_runtime::{DigestItem, OpaqueExtrinsic, Percent}; use sp_runtime_interface::pass_by::PassBy; use sp_runtime_interface::{pass_by, runtime_interface}; use sp_std::vec::Vec; -use sp_trie::StorageProof; use sp_weights::Weight; use subspace_core_primitives::crypto::blake2b_256_hash; -use subspace_core_primitives::{Blake2b256Hash, BlockNumber, Randomness, U256}; -use subspace_runtime_primitives::Moment; +use subspace_core_primitives::{Blake2b256Hash, Randomness, U256}; +use subspace_runtime_primitives::{Balance, Moment}; /// Key type for Operator. const KEY_TYPE: KeyTypeId = KeyTypeId(*b"oper"); @@ -156,12 +154,10 @@ pub struct BundleHeader { pub consensus_block_number: Number, /// The hash of consensus block corresponding to `consensus_block_number`. pub consensus_block_hash: Hash, - /// The slot number. - pub slot_number: u64, /// The merkle root of the extrinsics. pub extrinsics_root: H256, - /// Solution of the bundle election. - pub bundle_solution: BundleSolution, + /// Proof of bundle producer election. + pub proof_of_election: ProofOfElection, } impl BundleHeader { @@ -201,13 +197,8 @@ impl BlakeTwo256::hash_of(self) } - /// Returns whether the signature is valid. - pub fn verify_signature(&self) -> bool { - self.header - .bundle_solution - .proof_of_election() - .operator_public_key - .verify(&self.pre_hash(), &self.signature) + pub fn slot_number(&self) -> u64 { + self.header.proof_of_election.slot_number } } @@ -215,91 +206,56 @@ impl pub struct ProofOfElection { /// Domain id. pub domain_id: DomainId, - /// VRF output. - pub vrf_output: VrfOutput, - /// VRF proof. - pub vrf_proof: VrfProof, - /// VRF public key. - pub operator_public_key: OperatorPublicKey, + /// The slot number. + pub slot_number: u64, + // TODO: Use global_randomness instead. /// Global challenge. pub global_challenge: Blake2b256Hash, - /// Storage proof containing the partial state for verifying the bundle election. - pub storage_proof: StorageProof, - /// State root corresponding to the storage proof above. - pub system_state_root: DomainHash, - /// Number of the system domain block at which the proof of election was created. - pub system_block_number: BlockNumber, - /// Block hash corresponding to the `block_number` above. - pub system_block_hash: DomainHash, + /// VRF signature. + pub vrf_signature: VrfSignature, + /// Operator index in the OperatorRegistry. + pub operator_id: OperatorId, + // TODO: added temporarily in order to not change a lot of code to make it compile, remove later. + pub _phantom: DomainHash, } impl ProofOfElection { - pub fn verify_vrf_proof(&self) -> Result<(), VrfProofError> { - bundle_election::verify_vrf_proof( - &self.operator_public_key, - // TODO: Maybe we want to store signature in the struct rather than separate fields, - // such that we don't need to clone here? - &VrfSignature { - output: self.vrf_output.clone(), - proof: self.vrf_proof.clone(), - }, + pub fn verify_vrf_proof( + &self, + operator_signing_key: &OperatorPublicKey, + ) -> Result<(), VrfProofError> { + bundle_producer_election::verify_vrf_proof( + self.domain_id, + operator_signing_key, + &self.vrf_signature, &self.global_challenge, ) } /// Computes the VRF hash. pub fn vrf_hash(&self) -> Blake2b256Hash { - let mut bytes = self.vrf_output.encode(); - bytes.append(&mut self.vrf_proof.encode()); + let mut bytes = self.vrf_signature.output.encode(); + bytes.append(&mut self.vrf_signature.proof.encode()); blake2b_256_hash(&bytes) } } impl ProofOfElection { #[cfg(any(feature = "std", feature = "runtime-benchmarks"))] - pub fn dummy(domain_id: DomainId, operator_public_key: OperatorPublicKey) -> Self { + pub fn dummy(domain_id: DomainId, operator_id: OperatorId) -> Self { let output_bytes = vec![0u8; VrfOutput::max_encoded_len()]; let proof_bytes = vec![0u8; VrfProof::max_encoded_len()]; + let vrf_signature = VrfSignature { + output: VrfOutput::decode(&mut output_bytes.as_slice()).unwrap(), + proof: VrfProof::decode(&mut proof_bytes.as_slice()).unwrap(), + }; Self { domain_id, - vrf_output: VrfOutput::decode(&mut output_bytes.as_slice()).unwrap(), - vrf_proof: VrfProof::decode(&mut proof_bytes.as_slice()).unwrap(), - operator_public_key, + slot_number: 0u64, global_challenge: Blake2b256Hash::default(), - storage_proof: StorageProof::empty(), - system_state_root: Default::default(), - system_block_number: Default::default(), - system_block_hash: Default::default(), - } - } -} - -/// Domain bundle election solution. -#[derive(Debug, Decode, Encode, TypeInfo, PartialEq, Eq, Clone)] -pub struct BundleSolution { - /// Authority's stake weight. - authority_stake_weight: StakeWeight, - /// Authority membership witness. - authority_witness: Witness, - /// Proof of election - proof_of_election: ProofOfElection, -} - -impl BundleSolution { - pub fn proof_of_election(&self) -> &ProofOfElection { - &self.proof_of_election - } -} - -impl BundleSolution { - #[cfg(any(feature = "std", feature = "runtime-benchmarks"))] - pub fn dummy(domain_id: DomainId, operator_public_key: OperatorPublicKey) -> Self { - let proof_of_election = ProofOfElection::dummy(domain_id, operator_public_key); - - Self { - authority_stake_weight: Default::default(), - authority_witness: Default::default(), - proof_of_election, + vrf_signature, + operator_id, + _phantom: Default::default(), } } } @@ -326,20 +282,11 @@ impl DomainId { - self.sealed_header - .header - .bundle_solution - .proof_of_election() - .domain_id + self.sealed_header.header.proof_of_election.domain_id } - /// Consumes [`Bundle`] to extract the inner operator public key. - pub fn into_operator_public_key(self) -> OperatorPublicKey { - self.sealed_header - .header - .bundle_solution - .proof_of_election - .operator_public_key + pub fn operator_id(self) -> OperatorId { + self.sealed_header.header.proof_of_election.operator_id } } @@ -457,12 +404,8 @@ where header: BundleHeader { consensus_block_number, consensus_block_hash, - slot_number: 0u64, extrinsics_root: Default::default(), - bundle_solution: BundleSolution::dummy( - domain_id, - OperatorPublicKey::unchecked_from([0u8; 32]), - ), + proof_of_election: ProofOfElection::dummy(domain_id, 0u64), }, signature: OperatorSignature::unchecked_from([0u8; 64]), }; @@ -489,6 +432,11 @@ pub struct GenesisDomain { pub max_block_weight: Weight, pub bundle_slot_probability: (u64, u64), pub target_bundles_per_block: u32, + + // Genesis operator + pub signing_key: OperatorPublicKey, + pub minimum_nominator_stake: Balance, + pub nomination_tax: Percent, } /// Types of runtime pallet domains currently supports @@ -615,4 +563,10 @@ sp_api::decl_runtime_apis! { /// Returns the current Tx range for the given domain Id. fn domain_tx_range(domain_id: DomainId) -> U256; } + + pub trait BundleProducerElectionApi { + fn bundle_producer_election_params(domain_id: DomainId) -> Option>; + + fn operator(operator_id: OperatorId) -> Option<(OperatorPublicKey, Balance)>; + } } diff --git a/crates/sp-domains/src/transaction.rs b/crates/sp-domains/src/transaction.rs index eb1675d654..bc2d98f500 100644 --- a/crates/sp-domains/src/transaction.rs +++ b/crates/sp-domains/src/transaction.rs @@ -31,6 +31,8 @@ impl From for TransactionValidity { /// Object for performing the pre-validation in the transaction pool /// before calling into the regular `validate_transaction` runtime api. +// TODO: Revisit +#[allow(clippy::large_enum_variant)] #[derive(Debug, Decode, Encode, TypeInfo, PartialEq, Eq, Clone)] pub enum PreValidationObject where diff --git a/crates/subspace-node/src/chain_spec.rs b/crates/subspace-node/src/chain_spec.rs index 9a2fc16544..e10450c578 100644 --- a/crates/subspace-node/src/chain_spec.rs +++ b/crates/subspace-node/src/chain_spec.rs @@ -16,13 +16,16 @@ //! Subspace chain configurations. -use crate::chain_spec_utils::{chain_spec_properties, get_account_id_from_seed}; +use crate::chain_spec_utils::{ + chain_spec_properties, get_account_id_from_seed, get_public_key_from_seed, +}; use sc_service::{ChainType, NoExtension}; use sc_subspace_chain_specs::ConsensusChainSpec; use sc_telemetry::TelemetryEndpoints; use sp_consensus_subspace::FarmerPublicKey; use sp_core::crypto::{Ss58Codec, UncheckedFrom}; -use sp_domains::RuntimeType; +use sp_domains::{OperatorPublicKey, RuntimeType}; +use sp_runtime::Percent; use subspace_runtime::{ AllowAuthoringBy, BalancesConfig, DomainsConfig, GenesisConfig, MaxDomainBlockSize, MaxDomainBlockWeight, RuntimeConfigsConfig, SubspaceConfig, SudoConfig, SystemConfig, @@ -414,6 +417,11 @@ fn subspace_genesis_config( max_block_weight: MaxDomainBlockWeight::get(), bundle_slot_probability: (1, 1), target_bundles_per_block: 10, + + // TODO: Configurable genesis operator signing key. + signing_key: get_public_key_from_seed::("Alice"), + nomination_tax: Percent::from_percent(5), + minimum_nominator_stake: 100 * SSC, }), }, } diff --git a/crates/subspace-runtime/src/lib.rs b/crates/subspace-runtime/src/lib.rs index 0160622186..eb4496b5e4 100644 --- a/crates/subspace-runtime/src/lib.rs +++ b/crates/subspace-runtime/src/lib.rs @@ -59,7 +59,8 @@ use sp_consensus_subspace::{ }; use sp_core::crypto::{ByteArray, KeyTypeId}; use sp_core::{OpaqueMetadata, H256}; -use sp_domains::{DomainId, DomainsFreezeIdentifier, OpaqueBundle, OperatorId}; +use sp_domains::bundle_producer_election::BundleProducerElectionParams; +use sp_domains::{DomainId, DomainsFreezeIdentifier, OpaqueBundle, OperatorId, OperatorPublicKey}; use sp_runtime::traits::{AccountIdLookup, BlakeTwo256, NumberFor}; use sp_runtime::transaction_validity::{TransactionSource, TransactionValidity}; use sp_runtime::{ @@ -852,6 +853,17 @@ impl_runtime_apis! { fn domain_tx_range(domain_id: DomainId) -> U256 { Domains::domain_tx_range(domain_id) } + + } + + impl sp_domains::BundleProducerElectionApi for Runtime { + fn bundle_producer_election_params(domain_id: DomainId) -> Option> { + Domains::bundle_producer_election_params(domain_id) + } + + fn operator(operator_id: OperatorId) -> Option<(OperatorPublicKey, Balance)> { + Domains::operator(operator_id) + } } impl sp_session::SessionKeys for Runtime { diff --git a/domains/client/domain-operator/src/bundle_producer_election_solver.rs b/domains/client/domain-operator/src/bundle_producer_election_solver.rs index d41bd8dff3..bc4fb56ace 100644 --- a/domains/client/domain-operator/src/bundle_producer_election_solver.rs +++ b/domains/client/domain-operator/src/bundle_producer_election_solver.rs @@ -1,50 +1,104 @@ -use sp_domains::{BundleSolution, DomainId, OperatorPublicKey}; -use sp_keystore::KeystorePtr; +use sp_api::ProvideRuntimeApi; +use sp_consensus_slots::Slot; +use sp_domains::bundle_producer_election::{ + calculate_threshold, is_below_threshold, make_transcript, BundleProducerElectionParams, +}; +use sp_domains::{BundleProducerElectionApi, DomainId, OperatorPublicKey, ProofOfElection}; +use sp_keystore::{Keystore, KeystorePtr}; use sp_runtime::traits::Block as BlockT; use sp_runtime::RuntimeAppPublic; use std::marker::PhantomData; +use std::sync::Arc; use subspace_core_primitives::Blake2b256Hash; +use subspace_runtime_primitives::Balance; -pub(super) struct BundleProducerElectionSolver { +pub(super) struct BundleProducerElectionSolver { keystore: KeystorePtr, + consensus_client: Arc, _phantom_data: PhantomData<(Block, CBlock)>, } -impl Clone for BundleProducerElectionSolver { +impl Clone for BundleProducerElectionSolver { fn clone(&self) -> Self { Self { keystore: self.keystore.clone(), + consensus_client: self.consensus_client.clone(), _phantom_data: self._phantom_data, } } } -impl BundleProducerElectionSolver +impl BundleProducerElectionSolver where Block: BlockT, CBlock: BlockT, + CClient: ProvideRuntimeApi, + CClient::Api: BundleProducerElectionApi, { - pub(super) fn new(keystore: KeystorePtr) -> Self { + pub(super) fn new(keystore: KeystorePtr, consensus_client: Arc) -> Self { Self { keystore, + consensus_client, _phantom_data: PhantomData, } } pub(super) fn solve_challenge( &self, + slot: Slot, + consensus_block_hash: CBlock::Hash, domain_id: DomainId, - _global_challenge: Blake2b256Hash, - ) -> sp_blockchain::Result>> { - // TODO: Implement Bundle Producer Election v2 - - if let Some(authority_id) = self - .keystore - .sr25519_public_keys(OperatorPublicKey::ID) - .into_iter() - .next() + global_challenge: Blake2b256Hash, + ) -> sp_blockchain::Result, OperatorPublicKey)>> { + let BundleProducerElectionParams { + current_operators, + total_domain_stake, + bundle_slot_probability, + } = match self + .consensus_client + .runtime_api() + .bundle_producer_election_params(consensus_block_hash, domain_id)? { - return Ok(Some(BundleSolution::dummy(domain_id, authority_id.into()))); + Some(params) => params, + None => return Ok(None), + }; + + let vrf_sign_data = make_transcript(domain_id, &global_challenge).into_sign_data(); + + // TODO: The runtime API may take 10~20 microseonds each time, looping the operator set + // could take too long for the bundle production, track a mapping of signing_key to + // operator_id in the runtime and then we can update it to loop the keys in the keystore. + for operator_id in current_operators { + if let Some((operator_signing_key, operator_stake)) = self + .consensus_client + .runtime_api() + .operator(consensus_block_hash, operator_id)? + { + if let Ok(Some(vrf_signature)) = Keystore::sr25519_vrf_sign( + &*self.keystore, + OperatorPublicKey::ID, + &operator_signing_key.clone().into(), + &vrf_sign_data, + ) { + let threshold = calculate_threshold( + operator_stake, + total_domain_stake, + bundle_slot_probability, + ); + + if is_below_threshold(&vrf_signature.output, threshold) { + let proof_of_election = ProofOfElection { + domain_id, + slot_number: slot.into(), + global_challenge, + vrf_signature, + operator_id, + _phantom: Default::default(), + }; + return Ok(Some((proof_of_election, operator_signing_key))); + } + } + } } Ok(None) diff --git a/domains/client/domain-operator/src/domain_bundle_producer.rs b/domains/client/domain-operator/src/domain_bundle_producer.rs index dd792a5b2d..a226e94919 100644 --- a/domains/client/domain-operator/src/domain_bundle_producer.rs +++ b/domains/client/domain-operator/src/domain_bundle_producer.rs @@ -11,7 +11,8 @@ use sp_api::{NumberFor, ProvideRuntimeApi}; use sp_block_builder::BlockBuilder; use sp_blockchain::{HashAndNumber, HeaderBackend}; use sp_domains::{ - Bundle, DomainId, DomainsApi, OperatorPublicKey, OperatorSignature, SealedBundleHeader, + Bundle, BundleProducerElectionApi, DomainId, DomainsApi, OperatorPublicKey, OperatorSignature, + SealedBundleHeader, }; use sp_keystore::KeystorePtr; use sp_runtime::traits::{Block as BlockT, One, Saturating, Zero}; @@ -19,6 +20,7 @@ use sp_runtime::RuntimeAppPublic; use std::marker::PhantomData; use std::sync::Arc; use subspace_core_primitives::U256; +use subspace_runtime_primitives::Balance; type OpaqueBundle = sp_domains::OpaqueBundle< NumberFor, @@ -45,7 +47,7 @@ pub(super) struct DomainBundleProducer< parent_chain: ParentChain, bundle_sender: Arc>, keystore: KeystorePtr, - bundle_producer_election_solver: BundleProducerElectionSolver, + bundle_producer_election_solver: BundleProducerElectionSolver, domain_bundle_proposer: DomainBundleProposer, _phantom_data: PhantomData, } @@ -99,7 +101,8 @@ where Client: HeaderBackend + BlockBackend + AuxStore + ProvideRuntimeApi, Client::Api: BlockBuilder + DomainCoreApi, CClient: HeaderBackend + ProvideRuntimeApi, - CClient::Api: DomainsApi, Block::Hash>, + CClient::Api: DomainsApi, Block::Hash> + + BundleProducerElectionApi, ParentChain: ParentChainInterface + Clone, TransactionPool: sc_transaction_pool_api::TransactionPool, { @@ -118,8 +121,10 @@ where bundle_sender: Arc>, keystore: KeystorePtr, ) -> Self { - let bundle_producer_election_solver = - BundleProducerElectionSolver::::new(keystore.clone()); + let bundle_producer_election_solver = BundleProducerElectionSolver::::new( + keystore.clone(), + consensus_client.clone(), + ); Self { domain_id, consensus_client, @@ -185,14 +190,16 @@ where return Ok(None); } - if let Some(bundle_solution) = self - .bundle_producer_election_solver - .solve_challenge(self.domain_id, global_challenge)? + if let Some((proof_of_election, operator_signing_key)) = + self.bundle_producer_election_solver.solve_challenge( + slot, + consensus_block_info.hash, + self.domain_id, + global_challenge, + )? { tracing::info!("📦 Claimed bundle at slot {slot}"); - let proof_of_election = bundle_solution.proof_of_election(); - let bundle_author = proof_of_election.operator_public_key.clone(); let tx_range = self .consensus_client .runtime_api() @@ -210,8 +217,7 @@ where let (bundle_header, receipt, extrinsics) = self .domain_bundle_proposer .propose_bundle_at( - bundle_solution, - slot, + proof_of_election, consensus_block_info, self.parent_chain.clone(), tx_selector, @@ -224,7 +230,7 @@ where .keystore .sr25519_sign( OperatorPublicKey::ID, - bundle_author.as_ref(), + operator_signing_key.as_ref(), to_sign.as_ref(), ) .map_err(|error| { diff --git a/domains/client/domain-operator/src/domain_bundle_proposer.rs b/domains/client/domain-operator/src/domain_bundle_proposer.rs index 13146ac8a6..3e6181ed4e 100644 --- a/domains/client/domain-operator/src/domain_bundle_proposer.rs +++ b/domains/client/domain-operator/src/domain_bundle_proposer.rs @@ -9,8 +9,7 @@ use sc_transaction_pool_api::InPoolTransaction; use sp_api::{NumberFor, ProvideRuntimeApi}; use sp_block_builder::BlockBuilder; use sp_blockchain::{HashAndNumber, HeaderBackend}; -use sp_consensus_slots::Slot; -use sp_domains::{BundleHeader, BundleSolution, ExecutionReceipt}; +use sp_domains::{BundleHeader, ExecutionReceipt, ProofOfElection}; use sp_runtime::traits::{BlakeTwo256, Block as BlockT, Hash as HashT, One, Saturating, Zero}; use std::marker::PhantomData; use std::sync::Arc; @@ -68,8 +67,7 @@ where pub(crate) async fn propose_bundle_at( &self, - bundle_solution: BundleSolution, - slot: Slot, + proof_of_election: ProofOfElection, consensus_block_info: HashAndNumber, parent_chain: ParentChain, tx_selector: TransactionSelector, @@ -130,9 +128,8 @@ where let header = BundleHeader { consensus_block_number: consensus_block_info.number, consensus_block_hash: consensus_block_info.hash, - slot_number: slot.into(), extrinsics_root, - bundle_solution, + proof_of_election, }; Ok((header, receipt, extrinsics)) diff --git a/domains/client/domain-operator/src/domain_worker_starter.rs b/domains/client/domain-operator/src/domain_worker_starter.rs index 1c9c987767..d6bb28010b 100644 --- a/domains/client/domain-operator/src/domain_worker_starter.rs +++ b/domains/client/domain-operator/src/domain_worker_starter.rs @@ -33,11 +33,12 @@ use sp_block_builder::BlockBuilder; use sp_blockchain::{HeaderBackend, HeaderMetadata}; use sp_consensus_slots::Slot; use sp_core::traits::{CodeExecutor, SpawnEssentialNamed}; -use sp_domains::DomainsApi; +use sp_domains::{BundleProducerElectionApi, DomainsApi}; use sp_messenger::MessengerApi; use sp_runtime::traits::{HashFor, NumberFor}; use std::sync::Arc; use subspace_core_primitives::Blake2b256Hash; +use subspace_runtime_primitives::Balance; use tracing::Instrument; #[allow(clippy::type_complexity, clippy::too_many_arguments)] @@ -93,7 +94,8 @@ pub(super) async fn start_worker< + ProvideRuntimeApi + BlockchainEvents + 'static, - CClient::Api: DomainsApi, Block::Hash>, + CClient::Api: DomainsApi, Block::Hash> + + BundleProducerElectionApi, TransactionPool: sc_transaction_pool_api::TransactionPool + 'static, Backend: sc_client_api::Backend + 'static, IBNS: Stream, mpsc::Sender<()>)> + Send + 'static, diff --git a/domains/client/domain-operator/src/operator.rs b/domains/client/domain-operator/src/operator.rs index 6579921553..41be7bdcb4 100644 --- a/domains/client/domain-operator/src/operator.rs +++ b/domains/client/domain-operator/src/operator.rs @@ -18,11 +18,12 @@ use sp_blockchain::{HeaderBackend, HeaderMetadata}; use sp_consensus::SelectChain; use sp_consensus_slots::Slot; use sp_core::traits::{CodeExecutor, SpawnEssentialNamed}; -use sp_domains::DomainsApi; +use sp_domains::{BundleProducerElectionApi, DomainsApi}; use sp_messenger::MessengerApi; use sp_runtime::traits::{Block as BlockT, HashFor, NumberFor}; use std::sync::Arc; use subspace_core_primitives::Blake2b256Hash; +use subspace_runtime_primitives::Balance; /// Domain operator. pub struct Operator @@ -85,7 +86,8 @@ where + Send + Sync + 'static, - CClient::Api: DomainsApi, Block::Hash>, + CClient::Api: DomainsApi, Block::Hash> + + BundleProducerElectionApi, Backend: sc_client_api::Backend + Send + Sync + 'static, TransactionPool: sc_transaction_pool_api::TransactionPool + 'static, E: CodeExecutor, diff --git a/domains/service/src/domain.rs b/domains/service/src/domain.rs index 072e8fbe40..aeb02294b6 100644 --- a/domains/service/src/domain.rs +++ b/domains/service/src/domain.rs @@ -28,7 +28,7 @@ use sp_consensus::{SelectChain, SyncOracle}; use sp_consensus_slots::Slot; use sp_core::traits::SpawnEssentialNamed; use sp_core::{Decode, Encode}; -use sp_domains::{DomainId, DomainsApi}; +use sp_domains::{BundleProducerElectionApi, DomainId, DomainsApi}; use sp_messenger::{MessengerApi, RelayerApi}; use sp_offchain::OffchainWorkerApi; use sp_session::SessionKeys; @@ -291,7 +291,8 @@ where + Send + Sync + 'static, - CClient::Api: DomainsApi, + CClient::Api: DomainsApi + + BundleProducerElectionApi, SC: SelectChain, IBNS: Stream, mpsc::Sender<()>)> + Send + 'static, CIBNS: Stream> + Send + 'static, diff --git a/domains/test/service/src/domain.rs b/domains/test/service/src/domain.rs index 7911fe7608..092e89e519 100644 --- a/domains/test/service/src/domain.rs +++ b/domains/test/service/src/domain.rs @@ -405,7 +405,7 @@ impl DomainNodeBuilder { mock_consensus_node: &mut MockConsensusNode, ) -> EvmDomainNode { DomainNode::build( - DomainId::new(3u32), + DomainId::new(0u32), self.tokio_handle, self.key, self.base_path, diff --git a/test/subspace-test-client/src/chain_spec.rs b/test/subspace-test-client/src/chain_spec.rs index 981bd97ca8..154aa1505c 100644 --- a/test/subspace-test-client/src/chain_spec.rs +++ b/test/subspace-test-client/src/chain_spec.rs @@ -2,8 +2,9 @@ use sc_chain_spec::ChainType; use sp_core::{sr25519, Pair, Public}; -use sp_domains::{GenesisDomain, RuntimeType}; +use sp_domains::{GenesisDomain, OperatorPublicKey, RuntimeType}; use sp_runtime::traits::{IdentifyAccount, Verify}; +use sp_runtime::Percent; use subspace_runtime_primitives::{AccountId, Balance, BlockNumber, Signature}; use subspace_test_runtime::{ AllowAuthoringBy, BalancesConfig, DomainsConfig, GenesisConfig, MaxDomainBlockSize, @@ -108,6 +109,10 @@ fn create_genesis_config( max_block_weight: MaxDomainBlockWeight::get(), bundle_slot_probability: (1, 1), target_bundles_per_block: 10, + + signing_key: get_from_seed::("Alice"), + minimum_nominator_stake: 100 * SSC, + nomination_tax: Percent::from_percent(5), }), }, } diff --git a/test/subspace-test-runtime/src/lib.rs b/test/subspace-test-runtime/src/lib.rs index 18767de68a..5c04d91760 100644 --- a/test/subspace-test-runtime/src/lib.rs +++ b/test/subspace-test-runtime/src/lib.rs @@ -49,9 +49,13 @@ use sp_consensus_subspace::{ }; use sp_core::crypto::{ByteArray, KeyTypeId}; use sp_core::{Hasher, OpaqueMetadata, H256}; +use sp_domains::bundle_producer_election::BundleProducerElectionParams; use sp_domains::fraud_proof::FraudProof; use sp_domains::transaction::PreValidationObject; -use sp_domains::{DomainId, DomainsFreezeIdentifier, ExecutionReceipt, OpaqueBundle, OperatorId}; +use sp_domains::{ + DomainId, DomainsFreezeIdentifier, ExecutionReceipt, OpaqueBundle, OperatorId, + OperatorPublicKey, +}; use sp_runtime::traits::{ AccountIdLookup, BlakeTwo256, DispatchInfoOf, NumberFor, PostDispatchInfoOf, Zero, }; @@ -1205,6 +1209,16 @@ impl_runtime_apis! { } } + impl sp_domains::BundleProducerElectionApi for Runtime { + fn bundle_producer_election_params(domain_id: DomainId) -> Option> { + Domains::bundle_producer_election_params(domain_id) + } + + fn operator(operator_id: OperatorId) -> Option<(OperatorPublicKey, Balance)> { + Domains::operator(operator_id) + } + } + impl sp_session::SessionKeys for Runtime { fn generate_session_keys(seed: Option>) -> Vec { SessionKeys::generate(seed) diff --git a/test/subspace-test-service/src/lib.rs b/test/subspace-test-service/src/lib.rs index 36de9e9579..8300a53dab 100644 --- a/test/subspace-test-service/src/lib.rs +++ b/test/subspace-test-service/src/lib.rs @@ -512,7 +512,7 @@ impl MockConsensusNode { if let RuntimeCall::Domains(pallet_domains::Call::submit_bundle { opaque_bundle }) = ext.function { - if opaque_bundle.sealed_header.header.slot_number == slot { + if opaque_bundle.sealed_header.slot_number() == slot { return Some(opaque_bundle); } }