diff --git a/Cargo.lock b/Cargo.lock index f44268f0d9e19..871bae578ef89 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4699,7 +4699,7 @@ dependencies = [ [[package]] name = "node-sassafras" -version = "0.3.0" +version = "0.3.1-dev" dependencies = [ "clap 4.0.11", "frame-benchmarking", @@ -4741,7 +4741,7 @@ dependencies = [ [[package]] name = "node-sassafras-runtime" -version = "0.3.0" +version = "0.3.1-dev" dependencies = [ "frame-benchmarking", "frame-executive", @@ -6089,7 +6089,7 @@ dependencies = [ [[package]] name = "pallet-sassafras" -version = "0.3.0" +version = "0.3.1-dev" dependencies = [ "frame-benchmarking", "frame-support", @@ -8044,7 +8044,7 @@ dependencies = [ [[package]] name = "sc-consensus-sassafras" -version = "0.3.0" +version = "0.3.1-dev" dependencies = [ "async-trait", "fork-tree", @@ -9638,7 +9638,7 @@ dependencies = [ [[package]] name = "sp-consensus-sassafras" -version = "0.3.0" +version = "0.3.1-dev" dependencies = [ "async-trait", "merlin", diff --git a/bin/node-sassafras/node/Cargo.toml b/bin/node-sassafras/node/Cargo.toml index 58a276b416a62..48453f20c1446 100644 --- a/bin/node-sassafras/node/Cargo.toml +++ b/bin/node-sassafras/node/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "node-sassafras" -version = "0.3.0" +version = "0.3.1-dev" authors = ["Parity Technologies "] description = "Node testbed for Sassafras consensus." homepage = "https://substrate.io/" @@ -26,8 +26,8 @@ sc-telemetry = { version = "4.0.0-dev", path = "../../../client/telemetry" } sc-keystore = { version = "4.0.0-dev", path = "../../../client/keystore" } sc-transaction-pool = { version = "4.0.0-dev", path = "../../../client/transaction-pool" } sc-transaction-pool-api = { version = "4.0.0-dev", path = "../../../client/transaction-pool/api" } -sc-consensus-sassafras = { version = "0.3.0", path = "../../../client/consensus/sassafras" } -sp-consensus-sassafras = { version = "0.3.0", path = "../../../primitives/consensus/sassafras" } +sc-consensus-sassafras = { version = "0.3.1-dev", path = "../../../client/consensus/sassafras" } +sp-consensus-sassafras = { version = "0.3.1-dev", path = "../../../primitives/consensus/sassafras" } sp-consensus = { version = "0.10.0-dev", path = "../../../primitives/consensus/common" } sc-consensus = { version = "0.10.0-dev", path = "../../../client/consensus/common" } sc-finality-grandpa = { version = "0.10.0-dev", path = "../../../client/finality-grandpa" } @@ -56,7 +56,7 @@ frame-benchmarking = { version = "4.0.0-dev", path = "../../../frame/benchmarkin frame-benchmarking-cli = { version = "4.0.0-dev", path = "../../../utils/frame/benchmarking-cli" } # Local Dependencies -node-sassafras-runtime = { version = "0.3.0", path = "../runtime" } +node-sassafras-runtime = { version = "0.3.1-dev", path = "../runtime" } # CLI-specific dependencies try-runtime-cli = { version = "0.10.0-dev", optional = true, path = "../../../utils/frame/try-runtime/cli" } diff --git a/bin/node-sassafras/runtime/Cargo.toml b/bin/node-sassafras/runtime/Cargo.toml index 6aa670d7b36bc..8fe0108ae4de0 100644 --- a/bin/node-sassafras/runtime/Cargo.toml +++ b/bin/node-sassafras/runtime/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "node-sassafras-runtime" -version = "0.3.0" +version = "0.3.1-dev" authors = ["Parity Technologies "] description = "Runtime testbed for Sassafras consensus." homepage = "https://substrate.io/" @@ -15,7 +15,7 @@ targets = ["x86_64-unknown-linux-gnu"] codec = { package = "parity-scale-codec", version = "3.0.0", default-features = false, features = ["derive"] } scale-info = { version = "2.1.1", default-features = false, features = ["derive"] } -pallet-sassafras = { version = "0.3.0", default-features = false, path = "../../../frame/sassafras" } +pallet-sassafras = { version = "0.3.1-dev", default-features = false, path = "../../../frame/sassafras" } pallet-balances = { version = "4.0.0-dev", default-features = false, path = "../../../frame/balances" } pallet-session = { version = "4.0.0-dev", default-features = false, path = "../../../frame/session" } frame-support = { version = "4.0.0-dev", default-features = false, path = "../../../frame/support" } @@ -28,7 +28,7 @@ pallet-transaction-payment = { version = "4.0.0-dev", default-features = false, frame-executive = { version = "4.0.0-dev", default-features = false, path = "../../../frame/executive" } sp-api = { version = "4.0.0-dev", default-features = false, path = "../../../primitives/api" } sp-block-builder = { version = "4.0.0-dev", default-features = false, path = "../../../primitives/block-builder"} -sp-consensus-sassafras = { version = "0.3.0", default-features = false, path = "../../../primitives/consensus/sassafras" } +sp-consensus-sassafras = { version = "0.3.1-dev", default-features = false, path = "../../../primitives/consensus/sassafras" } sp-core = { version = "7.0.0", default-features = false, path = "../../../primitives/core" } sp-inherents = { version = "4.0.0-dev", default-features = false, path = "../../../primitives/inherents"} sp-offchain = { version = "4.0.0-dev", default-features = false, path = "../../../primitives/offchain" } diff --git a/bin/node-sassafras/runtime/src/lib.rs b/bin/node-sassafras/runtime/src/lib.rs index 4db5e3aa3dd2a..95b38ffa718c2 100644 --- a/bin/node-sassafras/runtime/src/lib.rs +++ b/bin/node-sassafras/runtime/src/lib.rs @@ -379,16 +379,6 @@ impl_runtime_apis! { } impl sp_consensus_sassafras::SassafrasApi for Runtime { - fn configuration() -> sp_consensus_sassafras::SassafrasConfiguration { - sp_consensus_sassafras::SassafrasConfiguration { - slot_duration: SLOT_DURATION_IN_MILLISECONDS, - epoch_duration: EPOCH_DURATION_IN_SLOTS, - authorities: Sassafras::authorities().to_vec(), - randomness: Sassafras::randomness(), - threshold_params: Sassafras::config(), - } - } - fn submit_tickets_unsigned_extrinsic( tickets: Vec ) -> bool { @@ -399,6 +389,14 @@ impl_runtime_apis! { Sassafras::slot_ticket(slot) } + fn current_epoch() -> sp_consensus_sassafras::Epoch { + Sassafras::current_epoch() + } + + fn next_epoch() -> sp_consensus_sassafras::Epoch { + Sassafras::next_epoch() + } + fn generate_key_ownership_proof( _slot: sp_consensus_sassafras::Slot, _authority_id: sp_consensus_sassafras::AuthorityId, diff --git a/client/consensus/sassafras/Cargo.toml b/client/consensus/sassafras/Cargo.toml index df996bf9bea94..d6dec3a830b46 100644 --- a/client/consensus/sassafras/Cargo.toml +++ b/client/consensus/sassafras/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "sc-consensus-sassafras" -version = "0.3.0" +version = "0.3.1-dev" authors = ["Parity Technologies "] description = "Sassafras consensus algorithm for substrate" edition = "2021" @@ -33,7 +33,7 @@ sp-application-crypto = { version = "7.0.0", path = "../../../primitives/applica sp-block-builder = { version = "4.0.0-dev", path = "../../../primitives/block-builder" } sp-blockchain = { version = "4.0.0-dev", path = "../../../primitives/blockchain" } sp-consensus = { version = "0.10.0-dev", path = "../../../primitives/consensus/common" } -sp-consensus-sassafras = { version = "0.3.0", path = "../../../primitives/consensus/sassafras" } +sp-consensus-sassafras = { version = "0.3.1-dev", path = "../../../primitives/consensus/sassafras" } sp-consensus-slots = { version = "0.10.0-dev", path = "../../../primitives/consensus/slots" } sp-consensus-vrf = { version = "0.10.0-dev", path = "../../../primitives/consensus/vrf" } sp-core = { version = "7.0.0", path = "../../../primitives/core" } diff --git a/client/consensus/sassafras/src/authorship.rs b/client/consensus/sassafras/src/authorship.rs index 9e620194ac09d..563a0fdf89fb7 100644 --- a/client/consensus/sassafras/src/authorship.rs +++ b/client/consensus/sassafras/src/authorship.rs @@ -55,9 +55,9 @@ pub(crate) fn claim_slot( let (authority_idx, ticket_aux) = match ticket { Some(ticket) => { log::debug!(target: "sassafras", "🌳 [TRY PRIMARY]"); - let (authority_idx, ticket_aux) = epoch.tickets_aux.get(&ticket)?.clone(); + let (authority_idx, ticket_aux) = epoch.tickets_aux.get(&ticket.output)?.clone(); log::debug!(target: "sassafras", "🌳 Ticket = [ticket: {:02x?}, auth: {}, attempt: {}]", - &ticket.as_bytes()[0..8], authority_idx, ticket_aux.attempt); + &ticket.output.as_bytes()[0..8], authority_idx, ticket_aux.attempt); (authority_idx, Some(ticket_aux)) }, None => { @@ -128,7 +128,11 @@ fn generate_epoch_tickets(epoch: &mut Epoch, keystore: &SyncCryptoStorePtr) -> V ) .ok()??; - let ticket = VRFOutput(signature.output); + let ticket = Ticket { + output: VRFOutput(signature.output), + // TODO-SASS-P3 + proof: VRFProof::try_from([0; 64]).expect("FIXME"), + }; if !sp_consensus_sassafras::check_threshold(&ticket, threshold) { return None } @@ -141,8 +145,10 @@ fn generate_epoch_tickets(epoch: &mut Epoch, keystore: &SyncCryptoStorePtr) -> V for attempt in 0..max_attempts { if let Some((ticket, ticket_aux)) = make_ticket(attempt) { + epoch + .tickets_aux + .insert(ticket.output, (authority_idx as AuthorityIndex, ticket_aux)); tickets.push(ticket); - epoch.tickets_aux.insert(ticket, (authority_idx as AuthorityIndex, ticket_aux)); } } } diff --git a/client/consensus/sassafras/src/block_import.rs b/client/consensus/sassafras/src/block_import.rs index 81563256ef30e..bf6e37a825b76 100644 --- a/client/consensus/sassafras/src/block_import.rs +++ b/client/consensus/sassafras/src/block_import.rs @@ -20,6 +20,7 @@ use super::*; use sc_client_api::{AuxDataOperations, FinalityNotification, PreCommitActions}; +use sp_blockchain::BlockStatus; /// Block-import handler for Sassafras. /// @@ -230,6 +231,77 @@ where } } +impl SassafrasBlockImport +where + Block: BlockT, + Inner: BlockImport> + Send + Sync, + Inner::Error: Into, + Client: HeaderBackend + + HeaderMetadata + + AuxStore + + ProvideRuntimeApi + + Send + + Sync, + Client::Api: SassafrasApi + ApiExt, +{ + /// Import whole state after a warp sync. + /// + /// This function makes multiple transactions to the DB. If one of them fails we may + /// end up in an inconsistent state and have to resync + async fn import_state( + &mut self, + mut block: BlockImportParams>, + new_cache: HashMap>, + ) -> Result { + let hash = block.post_hash(); + let parent_hash = *block.header.parent_hash(); + let number = *block.header.number(); + + // Check for the unit tag. + block.remove_intermediate::<()>(INTERMEDIATE_KEY)?; + + // Import as best + block.fork_choice = Some(ForkChoiceStrategy::Custom(true)); + + // Reset block weight + aux_schema::write_block_weight(hash, 0, |values| { + block + .auxiliary + .extend(values.iter().map(|(k, v)| (k.to_vec(), Some(v.to_vec())))) + }); + + // First make the client import the state + let aux = match self.inner.import_block(block, new_cache).await { + Ok(ImportResult::Imported(aux)) => aux, + Ok(r) => + return Err(ConsensusError::ClientImport(format!( + "Unexpected import result: {:?}", + r + ))), + Err(e) => return Err(e.into()), + }; + + // Read epoch info from the imported state + let block_id = BlockId::Hash(hash); + let curr_epoch = self.client.runtime_api().current_epoch(&block_id).map_err(|e| { + ConsensusError::ClientImport(sassafras_err::(Error::RuntimeApi(e)).into()) + })?; + let next_epoch = self.client.runtime_api().next_epoch(&block_id).map_err(|e| { + ConsensusError::ClientImport(sassafras_err::(Error::RuntimeApi(e)).into()) + })?; + + let mut epoch_changes = self.epoch_changes.shared_data(); + epoch_changes.reset(parent_hash, hash, number, curr_epoch.into(), next_epoch.into()); + + aux_schema::write_epoch_changes::(&*epoch_changes, |insert| { + self.client.insert_aux(insert, []) + }) + .map_err(|e| ConsensusError::ClientImport(e.to_string()))?; + + Ok(ImportResult::Imported(aux)) + } +} + #[async_trait::async_trait] impl BlockImport for SassafrasBlockImport where @@ -255,6 +327,22 @@ where let hash = block.post_hash(); let number = *block.header.number(); + // Early exit if block already in chain, otherwise the check for epoch changes + // will error when trying to re-import + match self.client.status(BlockId::Hash(hash)) { + Ok(BlockStatus::InChain) => { + block.remove_intermediate::>(INTERMEDIATE_KEY)?; + block.fork_choice = Some(ForkChoiceStrategy::Custom(false)); + return self.inner.import_block(block, new_cache).await.map_err(Into::into) + }, + Ok(BlockStatus::Unknown) => {}, + Err(e) => return Err(ConsensusError::ClientImport(e.to_string())), + } + + if block.with_state() { + return self.import_state(block, new_cache).await + } + let viable_epoch_desc = block .remove_intermediate::>(INTERMEDIATE_KEY)? .epoch_descriptor; diff --git a/client/consensus/sassafras/src/lib.rs b/client/consensus/sassafras/src/lib.rs index f5134e38266f7..be24e2b77f3a3 100644 --- a/client/consensus/sassafras/src/lib.rs +++ b/client/consensus/sassafras/src/lib.rs @@ -184,17 +184,28 @@ fn sassafras_err(error: Error) -> Error { error } -/// Sassafras epoch information +/// Sassafras epoch information augmented with private tickets information. #[derive(Encode, Decode, PartialEq, Eq, Clone, Debug)] pub struct Epoch { /// The epoch index. pub epoch_idx: u64, /// The starting slot of the epoch. pub start_slot: Slot, - /// Epoch configuration + /// Epoch configuration. pub config: SassafrasConfiguration, /// Tickets auxiliary data. - pub tickets_aux: BTreeMap, + pub tickets_aux: BTreeMap, +} + +impl From for Epoch { + fn from(epoch: sp_consensus_sassafras::Epoch) -> Self { + Epoch { + epoch_idx: epoch.epoch_idx, + start_slot: epoch.start_slot, + config: epoch.config, + tickets_aux: BTreeMap::new(), + } + } } impl EpochT for Epoch { @@ -252,8 +263,8 @@ where info.genesis_hash }); - let config = client.runtime_api().configuration(&BlockId::Hash(hash))?; - Ok(config) + let epoch = client.runtime_api().current_epoch(&BlockId::Hash(hash))?; + Ok(epoch.config) } /// Intermediate value passed to block importer from authoring or validation logic. diff --git a/client/consensus/sassafras/src/verification.rs b/client/consensus/sassafras/src/verification.rs index a7f6707565d0b..74ec2e865ee82 100644 --- a/client/consensus/sassafras/src/verification.rs +++ b/client/consensus/sassafras/src/verification.rs @@ -23,24 +23,28 @@ use super::*; // Allowed slot drift. const MAX_SLOT_DRIFT: u64 = 1; -/// Sassafras verification parameters -pub struct VerificationParams<'a, B: 'a + BlockT> { +/// Verification parameters +struct VerificationParams<'a, B: 'a + BlockT> { /// The header being verified. - pub header: B::Header, + header: B::Header, /// The pre-digest of the header being verified. - pub pre_digest: PreDigest, + pre_digest: &'a PreDigest, /// The slot number of the current time. - pub slot_now: Slot, + slot_now: Slot, /// Epoch descriptor of the epoch this block _should_ be under, if it's valid. - pub epoch: &'a Epoch, + epoch: &'a Epoch, + /// Origin + origin: BlockOrigin, /// Expected ticket for this block. - pub ticket: Option, + ticket: Option, } -pub struct VerifiedHeaderInfo { - pub authority_id: AuthorityId, - pub pre_digest: DigestItem, - pub seal: DigestItem, +/// Verified information +struct VerifiedHeaderInfo { + /// Authority index. + authority_id: AuthorityId, + /// Seal found within the header. + seal: DigestItem, } /// Check a header has been signed by the right key. If the slot is too far in @@ -52,10 +56,10 @@ pub struct VerifiedHeaderInfo { /// /// The given header can either be from a primary or secondary slot assignment, /// with each having different validation logic. -pub fn check_header( +fn check_header( params: VerificationParams, ) -> Result, Error> { - let VerificationParams { mut header, pre_digest, slot_now, epoch, ticket } = params; + let VerificationParams { mut header, pre_digest, slot_now, epoch, origin, ticket } = params; let config = &epoch.config; // Check that the slot is not in the future, with some drift being allowed. @@ -92,7 +96,7 @@ pub fn check_header( let transcript = make_ticket_transcript(&config.randomness, ticket_aux.attempt, epoch.epoch_idx); schnorrkel::PublicKey::from_bytes(authority_id.as_slice()) - .and_then(|p| p.vrf_verify(transcript, &ticket, &ticket_aux.proof)) + .and_then(|p| p.vrf_verify(transcript, &ticket.output, &ticket_aux.proof)) .map_err(|s| sassafras_err(Error::VRFVerificationFailed(s)))?; }, (None, None) => { @@ -107,10 +111,11 @@ pub fn check_header( log::warn!(target: "sassafras", "🌳 Unexpected secondary authoring mechanism"); return Err(Error::UnexpectedAuthoringMechanism) }, - (None, Some(_)) => { - log::warn!(target: "sassafras", "🌳 Unexpected primary authoring mechanism"); - return Err(Error::UnexpectedAuthoringMechanism) - }, + (None, Some(_)) => + if origin != BlockOrigin::NetworkInitialSync { + log::warn!(target: "sassafras", "🌳 Unexpected primary authoring mechanism"); + return Err(Error::UnexpectedAuthoringMechanism) + }, } // Check slot-vrf proof @@ -120,11 +125,7 @@ pub fn check_header( .and_then(|p| p.vrf_verify(transcript, &pre_digest.vrf_output, &pre_digest.vrf_proof)) .map_err(|s| sassafras_err(Error::VRFVerificationFailed(s)))?; - let info = VerifiedHeaderInfo { - authority_id, - pre_digest: CompatibleDigestItem::sassafras_pre_digest(pre_digest), - seal, - }; + let info = VerifiedHeaderInfo { authority_id, seal }; Ok(CheckedHeader::Checked(header, info)) } @@ -317,11 +318,12 @@ where // read it from the state after import. We also skip all verifications // because there's no parent state and we trust the sync module to verify // that the state is correct and finalized. + // Just insert a tag to notify that this is indeed a Sassafras block to the + // `BlockImport` implementation. + block.insert_intermediate(INTERMEDIATE_KEY, ()); return Ok((block, Default::default())) } - trace!(target: "sassafras", "🌳 We have {:?} logs in this header", block.header.digest().logs().len()); - let hash = block.header.hash(); let parent_hash = *block.header.parent_hash(); @@ -340,7 +342,7 @@ where let pre_digest = find_pre_digest::(&block.header)?; - let (check_header, epoch_descriptor) = { + let (checked_header, epoch_descriptor) = { let epoch_changes = self.epoch_changes.shared_data(); let epoch_descriptor = epoch_changes .epoch_descriptor_for_child_of( @@ -359,34 +361,31 @@ where .client .runtime_api() .slot_ticket(&BlockId::Hash(parent_hash), pre_digest.slot) - .map_err(|err| err.to_string())?; + .ok() + .unwrap_or_else(|| None); - let v_params = VerificationParams { + let verification_params = VerificationParams { header: block.header.clone(), - pre_digest, + pre_digest: &pre_digest, slot_now, epoch: viable_epoch.as_ref(), + origin: block.origin, ticket, }; + let checked_header = check_header::(verification_params)?; - (check_header::(v_params)?, epoch_descriptor) + (checked_header, epoch_descriptor) }; - match check_header { + match checked_header { CheckedHeader::Checked(pre_header, verified_info) => { - let sassafras_pre_digest = verified_info - .pre_digest - .as_sassafras_pre_digest() - .expect("check_header always returns a pre-digest digest item; qed"); - let slot = sassafras_pre_digest.slot; - // The header is valid but let's check if there was something else already // proposed at the same slot by the given author. If there was, we will // report the equivocation to the runtime. if let Err(err) = self .check_and_report_equivocation( slot_now, - slot, + pre_digest.slot, &block.header, &verified_info.authority_id, &block.origin, @@ -400,20 +399,23 @@ where // internally-set timestamp in the inherents actually matches the slot set in the // seal. if let Some(inner_body) = block.body { - let mut inherent_data = create_inherent_data_providers - .create_inherent_data() - .map_err(Error::::CreateInherents)?; - inherent_data.sassafras_replace_inherent_data(slot); let new_block = Block::new(pre_header.clone(), inner_body); - self.check_inherents( - new_block.clone(), - BlockId::Hash(parent_hash), - inherent_data, - create_inherent_data_providers, - block.origin.into(), - ) - .await?; + if !block.state_action.skip_execution_checks() { + // TODO-SASS-P3 :??? DOC + let mut inherent_data = create_inherent_data_providers + .create_inherent_data() + .map_err(Error::::CreateInherents)?; + inherent_data.sassafras_replace_inherent_data(pre_digest.slot); + self.check_inherents( + new_block.clone(), + BlockId::Hash(parent_hash), + inherent_data, + create_inherent_data_providers, + block.origin.into(), + ) + .await?; + } let (_, inner_body) = new_block.deconstruct(); block.body = Some(inner_body); @@ -428,12 +430,12 @@ where ); block.header = pre_header; + block.post_hash = Some(hash); block.post_digests.push(verified_info.seal); block.insert_intermediate( INTERMEDIATE_KEY, SassafrasIntermediate:: { epoch_descriptor }, ); - block.post_hash = Some(hash); Ok((block, Default::default())) }, diff --git a/frame/sassafras/Cargo.toml b/frame/sassafras/Cargo.toml index 0ed0c5c6654c0..22aee8632c968 100644 --- a/frame/sassafras/Cargo.toml +++ b/frame/sassafras/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "pallet-sassafras" -version = "0.3.0" +version = "0.3.1-dev" authors = ["Parity Technologies "] edition = "2021" license = "Apache-2.0" @@ -22,7 +22,7 @@ pallet-session = { version = "4.0.0-dev", default-features = false, path = "../s pallet-timestamp = { version = "4.0.0-dev", default-features = false, path = "../timestamp" } scale-info = { version = "2.0.1", default-features = false, features = ["derive"] } sp-application-crypto = { version = "7.0.0", default-features = false, path = "../../primitives/application-crypto" } -sp-consensus-sassafras = { version = "0.3.0", default-features = false, path = "../../primitives/consensus/sassafras" } +sp-consensus-sassafras = { version = "0.3.1-dev", default-features = false, path = "../../primitives/consensus/sassafras" } sp-io = { version = "7.0.0", default-features = false, path = "../../primitives/io" } sp-runtime = { version = "7.0.0", default-features = false, path = "../../primitives/runtime" } sp-std = { version = "5.0.0", default-features = false, path = "../../primitives/std" } diff --git a/frame/sassafras/src/lib.rs b/frame/sassafras/src/lib.rs index fda4226008e88..d465d61ddceaa 100644 --- a/frame/sassafras/src/lib.rs +++ b/frame/sassafras/src/lib.rs @@ -54,8 +54,9 @@ use frame_support::{traits::Get, weights::Weight, BoundedVec, WeakBoundedVec}; use frame_system::offchain::{SendTransactionTypes, SubmitTransaction}; use sp_consensus_sassafras::{ digests::{ConsensusLog, NextEpochDescriptor, PreDigest}, - AuthorityId, EquivocationProof, Randomness, SassafrasAuthorityWeight, - SassafrasEpochConfiguration, Slot, Ticket, SASSAFRAS_ENGINE_ID, + AuthorityId, Epoch, EquivocationProof, Randomness, SassafrasAuthorityWeight, + SassafrasConfiguration, SassafrasEpochConfiguration, Slot, Ticket, VRFOutput, + SASSAFRAS_ENGINE_ID, }; use sp_io::hashing; use sp_runtime::{ @@ -151,6 +152,7 @@ pub mod pallet { /// Next epoch authorities. #[pallet::storage] + #[pallet::getter(fn next_authorities)] pub type NextAuthorities = StorageValue< _, WeakBoundedVec<(AuthorityId, SassafrasAuthorityWeight), T::MaxAuthorities>, @@ -175,6 +177,7 @@ pub mod pallet { /// Next epoch randomness. #[pallet::storage] + #[pallet::getter(fn next_randomness)] pub type NextRandomness = StorageValue<_, Randomness, ValueQuery>; /// Randomness accumulator. @@ -194,6 +197,7 @@ pub mod pallet { /// The configuration for the next epoch. #[pallet::storage] + #[pallet::getter(fn next_config)] pub type NextEpochConfig = StorageValue<_, SassafrasEpochConfiguration>; /// Pending epoch configuration change that will be set as `NextEpochConfig` when the next @@ -302,12 +306,12 @@ pub mod pallet { let mut metadata = TicketsMeta::::get(); if metadata.segments_count != 0 { let epoch_idx = EpochIndex::::get() + 1; - let epoch_key = (epoch_idx & 1) as u8; + let epoch_tag = (epoch_idx & 1) as u8; if metadata.segments_count != 0 { let slots_left = epoch_duration.checked_sub(current_slot_idx).unwrap_or(1); Self::sort_tickets( u32::max(1, metadata.segments_count / slots_left as u32), - epoch_key, + epoch_tag, &mut metadata, ); TicketsMeta::::set(metadata); @@ -560,15 +564,15 @@ impl Pallet { }; Self::deposit_consensus(ConsensusLog::NextEpochData(next_epoch)); - let epoch_key = (epoch_idx & 1) as u8; + let epoch_tag = (epoch_idx & 1) as u8; let mut tickets_metadata = TicketsMeta::::get(); // Optionally finish sorting if tickets_metadata.segments_count != 0 { - Self::sort_tickets(tickets_metadata.segments_count, epoch_key, &mut tickets_metadata); + Self::sort_tickets(tickets_metadata.segments_count, epoch_tag, &mut tickets_metadata); } // Clear the prev (equal to the next) epoch tickets counter. - let next_epoch_key = epoch_key ^ 1; - tickets_metadata.tickets_count[next_epoch_key as usize] = 0; + let next_epoch_tag = epoch_tag ^ 1; + tickets_metadata.tickets_count[next_epoch_tag as usize] = 0; TicketsMeta::::set(tickets_metadata); } @@ -651,6 +655,36 @@ impl Pallet { Self::deposit_consensus(ConsensusLog::NextEpochData(next)); } + /// Current epoch configuration. + pub fn current_epoch() -> Epoch { + let config = SassafrasConfiguration { + slot_duration: T::SlotDuration::get(), + epoch_duration: T::EpochDuration::get(), + authorities: Self::authorities().to_vec(), + randomness: Self::randomness(), + threshold_params: Self::config(), + }; + let epoch_idx = EpochIndex::::get(); + let start_slot = Self::current_epoch_start(); + Epoch { epoch_idx, start_slot, config } + } + + /// Current epoch configuration. + pub fn next_epoch() -> Epoch { + let config = SassafrasConfiguration { + slot_duration: T::SlotDuration::get(), + epoch_duration: T::EpochDuration::get(), + authorities: Self::next_authorities().to_vec(), + randomness: Self::next_randomness(), + threshold_params: Self::next_config().unwrap_or_else(|| Self::config()), + }; + let epoch_idx = EpochIndex::::get() + .checked_add(1) + .expect("epoch indices will never reach 2^64 before the death of the universe; qed"); + let start_slot = Self::epoch_start(epoch_idx); + Epoch { epoch_idx, start_slot, config } + } + /// Fetch expected ticket for the given slot according to an "outside-in" sorting strategy. /// /// Given an ordered sequence of tickets [t0, t1, t2, ..., tk] to be assigned to n slots, @@ -689,15 +723,15 @@ impl Pallet { ticket_idx as u32 }; - let mut epoch_key = (epoch_idx & 1) as u8; + let mut epoch_tag = (epoch_idx & 1) as u8; if duration <= slot_idx && slot_idx < 2 * duration { // Try to get a ticket for the next epoch. Since its state values were not enacted yet, // we may have to finish sorting the tickets. - epoch_key ^= 1; + epoch_tag ^= 1; slot_idx -= duration; if tickets_meta.segments_count != 0 { - Self::sort_tickets(tickets_meta.segments_count, epoch_key, &mut tickets_meta); + Self::sort_tickets(tickets_meta.segments_count, epoch_tag, &mut tickets_meta); TicketsMeta::::set(tickets_meta.clone()); } } else if slot_idx >= 2 * duration { @@ -705,42 +739,47 @@ impl Pallet { } let ticket_idx = get_ticket_idx(slot_idx); - if ticket_idx < tickets_meta.tickets_count[epoch_key as usize] { - Tickets::::get((epoch_key, ticket_idx)) + if ticket_idx < tickets_meta.tickets_count[epoch_tag as usize] { + Tickets::::get((epoch_tag, ticket_idx)) } else { None } } // Lexicographically sort the tickets who belongs to the next epoch. - // The tickets are fetched from at most `max_iter` segments received via the `submit_tickets` - // extrinsic. The resulting sorted vector is truncated and if all the segments where sorted - // it is saved to be as the next epoch tickets. - // Else the result is saved to be used by next calls. - fn sort_tickets(max_iter: u32, epoch_key: u8, metadata: &mut TicketsMetadata) { + // + // Tickets are fetched from at most `max_iter` segments received via the `submit_tickets` + // extrinsic. + // + // The resulting sorted vector is optionally truncated to contain at most `MaxTickets` + // entries. If all the segments were consumed then the sorted vector is saved as the + // next epoch tickets, else it is saved to be used by next calls to this function. + fn sort_tickets(max_iter: u32, epoch_tag: u8, metadata: &mut TicketsMetadata) { let mut segments_count = metadata.segments_count; let max_iter = max_iter.min(segments_count); let max_tickets = T::MaxTickets::get() as usize; + // Fetch the partial result. let mut new_segment = NextTicketsSegments::::take(u32::MAX).into_inner(); let mut require_sort = max_iter != 0; let mut sup = if new_segment.len() >= max_tickets { - new_segment[new_segment.len() - 1] + new_segment[new_segment.len() - 1].output } else { - Ticket::try_from([0xFF; 32]).expect("This is a valid ticket value; qed") + VRFOutput::try_from([0xFF; 32]).expect("This is a valid vrf output value; qed") }; + // Consume at most `max_iter` segments. for _ in 0..max_iter { let segment = NextTicketsSegments::::take(segments_count); - segment.into_iter().filter(|t| t < &sup).for_each(|t| new_segment.push(t)); + segment.into_iter().filter(|t| t.output < sup).for_each(|t| new_segment.push(t)); if new_segment.len() > max_tickets { require_sort = false; new_segment.sort_unstable(); new_segment.truncate(max_tickets); - sup = new_segment[new_segment.len() - 1]; + sup = new_segment[max_tickets - 1].output; } segments_count -= 1; @@ -751,13 +790,14 @@ impl Pallet { } if segments_count == 0 { - // Sort is over, write to the map. + // Sort is over, write to next epoch map. // TODO-SASS-P3: is there a better way to write a map from a vector? new_segment.iter().enumerate().for_each(|(i, t)| { - Tickets::::insert((epoch_key, i as u32), t); + Tickets::::insert((epoch_tag, i as u32), t); }); - metadata.tickets_count[epoch_key as usize] = new_segment.len() as u32; + metadata.tickets_count[epoch_tag as usize] = new_segment.len() as u32; } else { + // Keep the partial result for next invocations. NextTicketsSegments::::insert(u32::MAX, BoundedVec::truncate_from(new_segment)); } diff --git a/primitives/consensus/sassafras/Cargo.toml b/primitives/consensus/sassafras/Cargo.toml index fd9aa2fcdd3ce..7fdec9e22371d 100644 --- a/primitives/consensus/sassafras/Cargo.toml +++ b/primitives/consensus/sassafras/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "sp-consensus-sassafras" -version = "0.3.0" +version = "0.3.1-dev" authors = ["Parity Technologies "] description = "Primitives for Sassafras consensus" edition = "2021" diff --git a/primitives/consensus/sassafras/src/lib.rs b/primitives/consensus/sassafras/src/lib.rs index c4bd2daca8f97..d84e0c7ef5352 100644 --- a/primitives/consensus/sassafras/src/lib.rs +++ b/primitives/consensus/sassafras/src/lib.rs @@ -103,6 +103,17 @@ impl SassafrasConfiguration { } } +/// Sassafras epoch information +#[derive(Encode, Decode, PartialEq, Eq, Clone, Debug)] +pub struct Epoch { + /// The epoch index. + pub epoch_idx: u64, + /// The starting slot of the epoch. + pub start_slot: Slot, + /// Epoch configuration. + pub config: SassafrasConfiguration, +} + /// Configuration data used by the Sassafras consensus engine that can be modified on epoch change. #[derive(Clone, PartialEq, Eq, Encode, Decode, RuntimeDebug, MaxEncodedLen, TypeInfo, Default)] #[cfg_attr(feature = "std", derive(Serialize, Deserialize))] @@ -114,14 +125,43 @@ pub struct SassafrasEpochConfiguration { } /// Ticket type. -pub type Ticket = VRFOutput; +// TODO-SASS-P3: we are currently using Shnorrkel structures as placeholders. +// Should switch to new RVRF primitive. +#[derive(Debug, Clone, PartialEq, Eq, Encode, Decode, MaxEncodedLen, TypeInfo)] +pub struct Ticket { + /// Ring VRF output. + pub output: VRFOutput, + /// Ring VRF commitment proof. + pub proof: VRFProof, + // Ticket opaque utility data. + // TODO-SASS-P3: Interpretation of this data is up to the application? Investigate + // Suggested by Jeff: + // - ephemeral_pk: public key used to... + // - revealed_pk: ??? + // - gossip_auth_id: identifier to reach this actor in a separate gossip network + //pub data: Vec, +} + +use core::cmp::Ordering; + +impl PartialOrd for Ticket { + fn partial_cmp(&self, other: &Self) -> Option { + self.output.partial_cmp(&other.output) + } +} + +impl Ord for Ticket { + fn cmp(&self, other: &Self) -> Ordering { + self.output.cmp(&other.output) + } +} /// Ticket auxiliary information. #[derive(Debug, Clone, PartialEq, Eq, Encode, Decode, MaxEncodedLen, TypeInfo)] pub struct TicketAux { /// Attempt number. pub attempt: u32, - /// Ticket proof. + /// Ticket revelation proof. pub proof: VRFProof, } @@ -144,7 +184,7 @@ pub fn compute_threshold(redundancy: u32, slots: u32, attempts: u32, validators: /// Returns true if the given VRF output is lower than the given threshold, false otherwise. pub fn check_threshold(ticket: &Ticket, threshold: U256) -> bool { - U256::from(ticket.as_bytes()) < threshold + U256::from(ticket.output.as_bytes()) < threshold } /// An opaque type used to represent the key ownership proof at the runtime API boundary. @@ -172,9 +212,6 @@ impl OpaqueKeyOwnershipProof { sp_api::decl_runtime_apis! { /// API necessary for block authorship with Sassafras. pub trait SassafrasApi { - /// Return the genesis configuration for Sassafras. The configuration is only read on genesis. - fn configuration() -> SassafrasConfiguration; - /// Submit next epoch validator tickets via an unsigned extrinsic. /// This method returns `false` when creation of the extrinsics fails. fn submit_tickets_unsigned_extrinsic(tickets: Vec) -> bool; @@ -182,6 +219,12 @@ sp_api::decl_runtime_apis! { /// Get expected ticket for the given slot. fn slot_ticket(slot: Slot) -> Option; + /// Current epoch information. + fn current_epoch() -> Epoch; + + /// Next epoch information. + fn next_epoch() -> Epoch; + /// Generates a proof of key ownership for the given authority in the current epoch. /// /// An example usage of this module is coupled with the session historical module to prove diff --git a/test-utils/runtime/Cargo.toml b/test-utils/runtime/Cargo.toml index 0d820fe45166f..a492e3124fc0c 100644 --- a/test-utils/runtime/Cargo.toml +++ b/test-utils/runtime/Cargo.toml @@ -18,7 +18,7 @@ beefy-merkle-tree = { version = "4.0.0-dev", default-features = false, path = ". sp-application-crypto = { version = "7.0.0", default-features = false, path = "../../primitives/application-crypto" } sp-consensus-aura = { version = "0.10.0-dev", default-features = false, path = "../../primitives/consensus/aura" } sp-consensus-babe = { version = "0.10.0-dev", default-features = false, path = "../../primitives/consensus/babe" } -sp-consensus-sassafras = { version = "0.3.0", default-features = false, path = "../../primitives/consensus/sassafras" } +sp-consensus-sassafras = { version = "0.3.1-dev", default-features = false, path = "../../primitives/consensus/sassafras" } sp-block-builder = { version = "4.0.0-dev", default-features = false, path = "../../primitives/block-builder" } codec = { package = "parity-scale-codec", version = "3.0.0", default-features = false, features = ["derive"] } scale-info = { version = "2.1.1", default-features = false, features = ["derive"] } @@ -36,7 +36,7 @@ sp-session = { version = "4.0.0-dev", default-features = false, path = "../../pr sp-api = { version = "4.0.0-dev", default-features = false, path = "../../primitives/api" } sp-runtime = { version = "7.0.0", default-features = false, path = "../../primitives/runtime" } pallet-babe = { version = "4.0.0-dev", default-features = false, path = "../../frame/babe" } -pallet-sassafras = { version = "0.3.0", default-features = false, path = "../../frame/sassafras" } +pallet-sassafras = { version = "0.3.1-dev", default-features = false, path = "../../frame/sassafras" } frame-system = { version = "4.0.0-dev", default-features = false, path = "../../frame/system" } frame-system-rpc-runtime-api = { version = "4.0.0-dev", default-features = false, path = "../../frame/system/rpc/runtime-api" } pallet-timestamp = { version = "4.0.0-dev", default-features = false, path = "../../frame/timestamp" } diff --git a/test-utils/runtime/src/lib.rs b/test-utils/runtime/src/lib.rs index 7025f9fdcc079..df8a013ad3a20 100644 --- a/test-utils/runtime/src/lib.rs +++ b/test-utils/runtime/src/lib.rs @@ -969,24 +969,30 @@ cfg_if! { } impl sp_consensus_sassafras::SassafrasApi for Runtime { - fn configuration() -> sp_consensus_sassafras::SassafrasConfiguration { + fn submit_tickets_unsigned_extrinsic( + tickets: Vec + ) -> bool { + >::submit_tickets_unsigned_extrinsic(tickets) + } + + fn current_epoch() -> sp_consensus_sassafras::Epoch { let authorities = system::authorities().into_iter().map(|x| { let authority: sr25519::Public = x.into(); (SassafrasId::from(authority), 1) }).collect(); - sp_consensus_sassafras::SassafrasConfiguration { - slot_duration: SlotDuration::get(), - epoch_duration: EpochDuration::get(), - authorities, - randomness: >::randomness(), - threshold_params: >::config(), - } + let mut epoch = >::current_epoch(); + epoch.config.authorities = authorities; + epoch } - fn submit_tickets_unsigned_extrinsic( - tickets: Vec - ) -> bool { - >::submit_tickets_unsigned_extrinsic(tickets) + fn next_epoch() -> sp_consensus_sassafras::Epoch { + let authorities = system::authorities().into_iter().map(|x| { + let authority: sr25519::Public = x.into(); + (SassafrasId::from(authority), 1) + }).collect(); + let mut epoch = >::next_epoch(); + epoch.config.authorities = authorities; + epoch } fn slot_ticket(slot: sp_consensus_sassafras::Slot) -> Option { @@ -1285,22 +1291,32 @@ cfg_if! { } impl sp_consensus_sassafras::SassafrasApi for Runtime { - fn configuration() -> sp_consensus_sassafras::SassafrasConfiguration { - sp_consensus_sassafras::SassafrasConfiguration { - slot_duration: SlotDuration::get(), - epoch_duration: EpochDuration::get(), - authorities: >::authorities().to_vec(), - randomness: >::randomness(), - threshold_params: >::config(), - } - } - fn submit_tickets_unsigned_extrinsic( tickets: Vec ) -> bool { >::submit_tickets_unsigned_extrinsic(tickets) } + fn current_epoch() -> sp_consensus_sassafras::Epoch { + let authorities = system::authorities().into_iter().map(|x| { + let authority: sr25519::Public = x.into(); + (SassafrasId::from(authority), 1) + }).collect(); + let mut epoch = >::current_epoch(); + epoch.config.authorities = authorities; + epoch + } + + fn next_epoch() -> sp_consensus_sassafras::Epoch { + let authorities = system::authorities().into_iter().map(|x| { + let authority: sr25519::Public = x.into(); + (SassafrasId::from(authority), 1) + }).collect(); + let mut epoch = >::next_epoch(); + epoch.config.authorities = authorities; + epoch + } + fn slot_ticket(slot: sp_consensus_sassafras::Slot) -> Option { >::slot_ticket(slot) }