// This file is part of Substrate. // Copyright (C) Parity Technologies (UK) Ltd. // SPDX-License-Identifier: Apache-2.0 // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. //! Extension module for Sassafras consensus. //! //! [Sassafras](https://research.web3.foundation/Polkadot/protocols/block-production/SASSAFRAS) //! is a constant-time block production protocol that aims to ensure that there is //! exactly one block produced with constant time intervals rather than multiple or none. //! //! We run a lottery to distribute block production slots in an epoch and to fix the //! order validators produce blocks in, by the beginning of an epoch. //! //! Each validator signs the same VRF input and publishes the output on-chain. This //! value is their lottery ticket that can be validated against their public key. //! //! We want to keep lottery winners secret, i.e. do not publish their public keys. //! At the beginning of the epoch all the validators tickets are published but not //! their public keys. //! //! A valid tickets is validated when an honest validator reclaims it on block //! production. //! //! To prevent submission of fake tickets, resulting in empty slots, the validator //! when submitting the ticket accompanies it with a SNARK of the statement: "Here's //! my VRF output that has been generated using the given VRF input and my secret //! key. I'm not telling you my keys, but my public key is among those of the //! nominated validators", that is validated before the lottery. //! //! To anonymously publish the ticket to the chain a validator sends their tickets //! to a random validator who later puts it on-chain as a transaction. #![deny(warnings)] #![warn(unused_must_use, unsafe_code, unused_variables, unused_imports, missing_docs)] #![cfg_attr(not(feature = "std"), no_std)] use log::{debug, error, trace, warn}; use scale_codec::{Decode, Encode, MaxEncodedLen}; use scale_info::TypeInfo; use frame_support::{ dispatch::{DispatchResultWithPostInfo, Pays}, traits::{Defensive, Get}, weights::Weight, BoundedVec, WeakBoundedVec, }; use frame_system::{ offchain::{SendTransactionTypes, SubmitTransaction}, pallet_prelude::BlockNumberFor, }; use sp_consensus_sassafras::{ digests::{ConsensusLog, NextEpochDescriptor, SlotClaim}, vrf, AuthorityId, Epoch, EpochConfiguration, Randomness, Slot, TicketBody, TicketEnvelope, TicketId, RANDOMNESS_LENGTH, SASSAFRAS_ENGINE_ID, }; use sp_io::hashing; use sp_runtime::{ generic::DigestItem, traits::{One, Zero}, BoundToRuntimeAppPublic, }; use sp_std::prelude::Vec; #[cfg(feature = "runtime-benchmarks")] mod benchmarking; #[cfg(all(feature = "std", test))] mod mock; #[cfg(all(feature = "std", test))] mod tests; pub mod weights; pub use weights::WeightInfo; pub use pallet::*; const LOG_TARGET: &str = "sassafras::runtime"; // Contextual string used by the VRF to generate per-block randomness. const RANDOMNESS_VRF_CONTEXT: &[u8] = b"SassafrasOnChainRandomness"; // Max length for segments holding unsorted tickets. const SEGMENT_MAX_SIZE: u32 = 128; /// Authorities bounded vector convenience type. pub type AuthoritiesVec = WeakBoundedVec::MaxAuthorities>; /// Epoch length defined by the configuration. pub type EpochLengthFor = ::EpochLength; /// Tickets metadata. #[derive(Debug, Default, PartialEq, Encode, Decode, TypeInfo, MaxEncodedLen, Clone, Copy)] pub struct TicketsMetadata { /// Number of outstanding next epoch tickets requiring to be sorted. /// /// These tickets are held by the [`UnsortedSegments`] storage map in segments /// containing at most `SEGMENT_MAX_SIZE` items. pub unsorted_tickets_count: u32, /// Number of tickets available for current and next epoch. /// /// These tickets are held by the [`TicketsIds`] storage map. /// /// The array entry to be used for the current epoch is computed as epoch index modulo 2. pub tickets_count: [u32; 2], } #[frame_support::pallet] pub mod pallet { use super::*; use frame_support::pallet_prelude::*; use frame_system::pallet_prelude::*; /// The Sassafras pallet. #[pallet::pallet] pub struct Pallet(_); /// Configuration parameters. #[pallet::config] pub trait Config: frame_system::Config + SendTransactionTypes> { /// Amount of slots that each epoch should last. #[pallet::constant] type EpochLength: Get; /// Max number of authorities allowed. #[pallet::constant] type MaxAuthorities: Get; /// Epoch change trigger. /// /// Logic to be triggered on every block to query for whether an epoch has ended /// and to perform the transition to the next epoch. type EpochChangeTrigger: EpochChangeTrigger; /// Weight information for all calls of this pallet. type WeightInfo: WeightInfo; } /// Sassafras runtime errors. #[pallet::error] pub enum Error { /// Submitted configuration is invalid. InvalidConfiguration, } /// Current epoch index. #[pallet::storage] #[pallet::getter(fn epoch_index)] pub type EpochIndex = StorageValue<_, u64, ValueQuery>; /// Current epoch authorities. #[pallet::storage] #[pallet::getter(fn authorities)] pub type Authorities = StorageValue<_, AuthoritiesVec, ValueQuery>; /// Next epoch authorities. #[pallet::storage] #[pallet::getter(fn next_authorities)] pub type NextAuthorities = StorageValue<_, AuthoritiesVec, ValueQuery>; /// First block slot number. /// /// As the slots may not be zero-based, we record the slot value for the fist block. /// This allows to always compute relative indices for epochs and slots. #[pallet::storage] #[pallet::getter(fn genesis_slot)] pub type GenesisSlot = StorageValue<_, Slot, ValueQuery>; /// Current block slot number. #[pallet::storage] #[pallet::getter(fn current_slot)] pub type CurrentSlot = StorageValue<_, Slot, ValueQuery>; /// Current epoch randomness. #[pallet::storage] #[pallet::getter(fn randomness)] pub type CurrentRandomness = StorageValue<_, Randomness, ValueQuery>; /// Next epoch randomness. #[pallet::storage] #[pallet::getter(fn next_randomness)] pub type NextRandomness = StorageValue<_, Randomness, ValueQuery>; /// Randomness accumulator. /// /// Excluded the first imported block, its value is updated on block finalization. #[pallet::storage] #[pallet::getter(fn randomness_accumulator)] pub(crate) type RandomnessAccumulator = StorageValue<_, Randomness, ValueQuery>; /// The configuration for the current epoch. #[pallet::storage] #[pallet::getter(fn config)] pub type EpochConfig = StorageValue<_, EpochConfiguration, ValueQuery>; /// The configuration for the next epoch. #[pallet::storage] #[pallet::getter(fn next_config)] pub type NextEpochConfig = StorageValue<_, EpochConfiguration>; /// Pending epoch configuration change that will be set as `NextEpochConfig` when the next /// epoch is enacted. /// /// In other words, a configuration change submitted during epoch N will be enacted on epoch /// N+2. This is to maintain coherence for already submitted tickets for epoch N+1 that where /// computed using configuration parameters stored for epoch N+1. #[pallet::storage] pub type PendingEpochConfigChange = StorageValue<_, EpochConfiguration>; /// Stored tickets metadata. #[pallet::storage] pub type TicketsMeta = StorageValue<_, TicketsMetadata, ValueQuery>; /// Tickets identifiers map. /// /// The map holds tickets ids for the current and next epoch. /// /// The key is a tuple composed by: /// - `u8` equal to epoch's index modulo 2; /// - `u32` equal to the ticket's index in a sorted list of epoch's tickets. /// /// Epoch X first N-th ticket has key (X mod 2, N) /// /// Note that the ticket's index doesn't directly correspond to the slot index within the epoch. /// The assignment is computed dynamically using an *outside-in* strategy. /// /// Be aware that entries within this map are never removed, only overwritten. /// Last element index should be fetched from the [`TicketsMeta`] value. #[pallet::storage] pub type TicketsIds = StorageMap<_, Identity, (u8, u32), TicketId>; /// Tickets to be used for current and next epoch. #[pallet::storage] pub type TicketsData = StorageMap<_, Identity, TicketId, TicketBody>; /// Next epoch tickets unsorted segments. /// /// Contains lists of tickets where each list represents a batch of tickets /// received via the `submit_tickets` extrinsic. /// /// Each segment has max length [`SEGMENT_MAX_SIZE`]. #[pallet::storage] pub type UnsortedSegments = StorageMap<_, Identity, u32, BoundedVec>, ValueQuery>; /// The most recently set of tickets which are candidates to become the next /// epoch tickets. #[pallet::storage] pub type SortedCandidates = StorageValue<_, BoundedVec>, ValueQuery>; /// Parameters used to construct the epoch's ring verifier. /// /// In practice: Updatable Universal Reference String and the seed. #[pallet::storage] #[pallet::getter(fn ring_context)] pub type RingContext = StorageValue<_, vrf::RingContext>; /// Ring verifier data for the current epoch. #[pallet::storage] pub type RingVerifierData = StorageValue<_, vrf::RingVerifierData>; /// Slot claim VRF pre-output used to generate per-slot randomness. /// /// The value is ephemeral and is cleared on block finalization. #[pallet::storage] pub(crate) type ClaimTemporaryData = StorageValue<_, vrf::VrfPreOutput>; /// Genesis configuration for Sassafras protocol. #[pallet::genesis_config] #[derive(frame_support::DefaultNoBound)] pub struct GenesisConfig { /// Genesis authorities. pub authorities: Vec, /// Genesis epoch configuration. pub epoch_config: EpochConfiguration, /// Phantom config #[serde(skip)] pub _phantom: sp_std::marker::PhantomData, } #[pallet::genesis_build] impl BuildGenesisConfig for GenesisConfig { fn build(&self) { EpochConfig::::put(self.epoch_config); Pallet::::genesis_authorities_initialize(&self.authorities); #[cfg(feature = "construct-dummy-ring-context")] { debug!(target: LOG_TARGET, "Constructing dummy ring context"); let ring_ctx = vrf::RingContext::new_testing(); RingContext::::put(ring_ctx); Pallet::::update_ring_verifier(&self.authorities); } } } #[pallet::hooks] impl Hooks> for Pallet { fn on_initialize(block_num: BlockNumberFor) -> Weight { debug_assert_eq!(block_num, frame_system::Pallet::::block_number()); let claim = >::digest() .logs .iter() .find_map(|item| item.pre_runtime_try_to::(&SASSAFRAS_ENGINE_ID)) .expect("Valid block must have a slot claim. qed"); CurrentSlot::::put(claim.slot); if block_num == One::one() { Self::post_genesis_initialize(claim.slot); } let randomness_pre_output = claim .vrf_signature .pre_outputs .get(0) .expect("Valid claim must have VRF signature; qed"); ClaimTemporaryData::::put(randomness_pre_output); let trigger_weight = T::EpochChangeTrigger::trigger::(block_num); T::WeightInfo::on_initialize() + trigger_weight } fn on_finalize(_: BlockNumberFor) { // At the end of the block, we can safely include the current slot randomness // to the accumulator. If we've determined that this block was the first in // a new epoch, the changeover logic has already occurred at this point // (i.e. `enact_epoch_change` has already been called). let randomness_input = vrf::slot_claim_input( &Self::randomness(), CurrentSlot::::get(), EpochIndex::::get(), ); let randomness_pre_output = ClaimTemporaryData::::take() .expect("Unconditionally populated in `on_initialize`; `on_finalize` is always called after; qed"); let randomness = randomness_pre_output .make_bytes::(RANDOMNESS_VRF_CONTEXT, &randomness_input); Self::deposit_slot_randomness(&randomness); // Check if we are in the epoch's second half. // If so, start sorting the next epoch tickets. let epoch_length = T::EpochLength::get(); let current_slot_idx = Self::current_slot_index(); if current_slot_idx >= epoch_length / 2 { let mut metadata = TicketsMeta::::get(); if metadata.unsorted_tickets_count != 0 { let next_epoch_idx = EpochIndex::::get() + 1; let next_epoch_tag = (next_epoch_idx & 1) as u8; let slots_left = epoch_length.checked_sub(current_slot_idx).unwrap_or(1); Self::sort_segments( metadata .unsorted_tickets_count .div_ceil(SEGMENT_MAX_SIZE * slots_left as u32), next_epoch_tag, &mut metadata, ); TicketsMeta::::set(metadata); } } } } #[pallet::call] impl Pallet { /// Submit next epoch tickets candidates. /// /// The number of tickets allowed to be submitted in one call is equal to the epoch length. #[pallet::call_index(0)] #[pallet::weight(T::WeightInfo::submit_tickets(tickets.len() as u32))] pub fn submit_tickets( origin: OriginFor, tickets: BoundedVec>, ) -> DispatchResultWithPostInfo { ensure_none(origin)?; debug!(target: LOG_TARGET, "Received {} tickets", tickets.len()); let epoch_length = T::EpochLength::get(); let current_slot_idx = Self::current_slot_index(); if current_slot_idx > epoch_length / 2 { warn!(target: LOG_TARGET, "Tickets shall be submitted in the first epoch half",); return Err("Tickets shall be submitted in the first epoch half".into()) } let Some(verifier) = RingVerifierData::::get().map(|v| v.into()) else { warn!(target: LOG_TARGET, "Ring verifier key not initialized"); return Err("Ring verifier key not initialized".into()) }; let next_authorities = Self::next_authorities(); // Compute tickets threshold let next_config = Self::next_config().unwrap_or_else(|| Self::config()); let ticket_threshold = sp_consensus_sassafras::ticket_id_threshold( next_config.redundancy_factor, epoch_length as u32, next_config.attempts_number, next_authorities.len() as u32, ); // Get next epoch params let randomness = NextRandomness::::get(); let epoch_idx = EpochIndex::::get() + 1; let mut valid_tickets = BoundedVec::with_bounded_capacity(tickets.len()); for ticket in tickets { debug!(target: LOG_TARGET, "Checking ring proof"); let Some(ticket_id_pre_output) = ticket.signature.pre_outputs.get(0) else { debug!(target: LOG_TARGET, "Missing ticket VRF pre-output from ring signature"); continue }; let ticket_id_input = vrf::ticket_id_input(&randomness, ticket.body.attempt_idx, epoch_idx); // Check threshold constraint let ticket_id = vrf::make_ticket_id(&ticket_id_input, &ticket_id_pre_output); if ticket_id >= ticket_threshold { debug!(target: LOG_TARGET, "Ignoring ticket over threshold ({:032x} >= {:032x})", ticket_id, ticket_threshold); continue } // Check for duplicates if TicketsData::::contains_key(ticket_id) { debug!(target: LOG_TARGET, "Ignoring duplicate ticket ({:032x})", ticket_id); continue } // Check ring signature let sign_data = vrf::ticket_body_sign_data(&ticket.body, ticket_id_input); if !ticket.signature.ring_vrf_verify(&sign_data, &verifier) { debug!(target: LOG_TARGET, "Proof verification failure for ticket ({:032x})", ticket_id); continue } if let Ok(_) = valid_tickets.try_push(ticket_id).defensive_proof( "Input segment has same length as bounded destination vector; qed", ) { TicketsData::::set(ticket_id, Some(ticket.body)); } } if !valid_tickets.is_empty() { Self::append_tickets(valid_tickets); } Ok(Pays::No.into()) } /// Plan an epoch configuration change. /// /// The epoch configuration change is recorded and will be announced at the beginning /// of the next epoch together with next epoch authorities information. /// In other words, the configuration will be enacted one epoch later. /// /// Multiple calls to this method will replace any existing planned config change /// that has not been enacted yet. #[pallet::call_index(1)] #[pallet::weight(T::WeightInfo::plan_config_change())] pub fn plan_config_change( origin: OriginFor, config: EpochConfiguration, ) -> DispatchResult { ensure_root(origin)?; ensure!( config.redundancy_factor != 0 && config.attempts_number != 0, Error::::InvalidConfiguration ); PendingEpochConfigChange::::put(config); Ok(()) } } #[pallet::validate_unsigned] impl ValidateUnsigned for Pallet { type Call = Call; fn validate_unsigned(source: TransactionSource, call: &Self::Call) -> TransactionValidity { let Call::submit_tickets { tickets } = call else { return InvalidTransaction::Call.into() }; // Discard tickets not coming from the local node or that are not included in a block if source == TransactionSource::External { warn!( target: LOG_TARGET, "Rejecting unsigned `submit_tickets` transaction from external source", ); return InvalidTransaction::BadSigner.into() } // Current slot should be less than half of epoch length. let epoch_length = T::EpochLength::get(); let current_slot_idx = Self::current_slot_index(); if current_slot_idx > epoch_length / 2 { warn!(target: LOG_TARGET, "Tickets shall be proposed in the first epoch half",); return InvalidTransaction::Stale.into() } // This should be set such that it is discarded after the first epoch half let tickets_longevity = epoch_length / 2 - current_slot_idx; let tickets_tag = tickets.using_encoded(|bytes| hashing::blake2_256(bytes)); ValidTransaction::with_tag_prefix("Sassafras") .priority(TransactionPriority::max_value()) .longevity(tickets_longevity as u64) .and_provides(tickets_tag) .propagate(true) .build() } } } // Inherent methods impl Pallet { /// Determine whether an epoch change should take place at this block. /// /// Assumes that initialization has already taken place. pub(crate) fn should_end_epoch(block_num: BlockNumberFor) -> bool { // The epoch has technically ended during the passage of time between this block and the // last, but we have to "end" the epoch now, since there is no earlier possible block we // could have done it. // // The exception is for block 1: the genesis has slot 0, so we treat epoch 0 as having // started at the slot of block 1. We want to use the same randomness and validator set as // signalled in the genesis, so we don't rotate the epoch. block_num > One::one() && Self::current_slot_index() >= T::EpochLength::get() } /// Current slot index relative to the current epoch. fn current_slot_index() -> u32 { Self::slot_index(CurrentSlot::::get()) } /// Slot index relative to the current epoch. fn slot_index(slot: Slot) -> u32 { slot.checked_sub(*Self::current_epoch_start()) .and_then(|v| v.try_into().ok()) .unwrap_or(u32::MAX) } /// Finds the start slot of the current epoch. /// /// Only guaranteed to give correct results after `initialize` of the first /// block in the chain (as its result is based off of `GenesisSlot`). fn current_epoch_start() -> Slot { Self::epoch_start(EpochIndex::::get()) } /// Get the epoch's first slot. fn epoch_start(epoch_index: u64) -> Slot { const PROOF: &str = "slot number is u64; it should relate in some way to wall clock time; \ if u64 is not enough we should crash for safety; qed."; let epoch_start = epoch_index.checked_mul(T::EpochLength::get() as u64).expect(PROOF); GenesisSlot::::get().checked_add(epoch_start).expect(PROOF).into() } pub(crate) fn update_ring_verifier(authorities: &[AuthorityId]) { debug!(target: LOG_TARGET, "Loading ring context"); let Some(ring_ctx) = RingContext::::get() else { debug!(target: LOG_TARGET, "Ring context not initialized"); return }; let pks: Vec<_> = authorities.iter().map(|auth| *auth.as_ref()).collect(); debug!(target: LOG_TARGET, "Building ring verifier (ring size: {})", pks.len()); let verifier_data = ring_ctx .verifier_data(&pks) .expect("Failed to build ring verifier. This is a bug"); RingVerifierData::::put(verifier_data); } /// Enact an epoch change. /// /// WARNING: Should be called on every block once and if and only if [`should_end_epoch`] /// has returned `true`. /// /// If we detect one or more skipped epochs the policy is to use the authorities and values /// from the first skipped epoch. The tickets data is invalidated. pub(crate) fn enact_epoch_change( authorities: WeakBoundedVec, next_authorities: WeakBoundedVec, ) { if next_authorities != authorities { Self::update_ring_verifier(&next_authorities); } // Update authorities Authorities::::put(&authorities); NextAuthorities::::put(&next_authorities); // Update epoch index let mut epoch_idx = EpochIndex::::get() + 1; let slot_idx = CurrentSlot::::get().saturating_sub(Self::epoch_start(epoch_idx)); if slot_idx >= T::EpochLength::get() { // Detected one or more skipped epochs, clear tickets data and recompute epoch index. Self::reset_tickets_data(); let skipped_epochs = *slot_idx / T::EpochLength::get() as u64; epoch_idx += skipped_epochs; warn!( target: LOG_TARGET, "Detected {} skipped epochs, resuming from epoch {}", skipped_epochs, epoch_idx ); } let mut metadata = TicketsMeta::::get(); let mut metadata_dirty = false; EpochIndex::::put(epoch_idx); let next_epoch_idx = epoch_idx + 1; // Updates current epoch randomness and computes the *next* epoch randomness. let next_randomness = Self::update_epoch_randomness(next_epoch_idx); if let Some(config) = NextEpochConfig::::take() { EpochConfig::::put(config); } let next_config = PendingEpochConfigChange::::take(); if let Some(next_config) = next_config { NextEpochConfig::::put(next_config); } // After we update the current epoch, we signal the *next* epoch change // so that nodes can track changes. let next_epoch = NextEpochDescriptor { randomness: next_randomness, authorities: next_authorities.into_inner(), config: next_config, }; Self::deposit_next_epoch_descriptor_digest(next_epoch); let epoch_tag = (epoch_idx & 1) as u8; // Optionally finish sorting if metadata.unsorted_tickets_count != 0 { Self::sort_segments(u32::MAX, epoch_tag, &mut metadata); metadata_dirty = true; } // Clear the "prev ≡ next (mod 2)" epoch tickets counter and bodies. // Ids are left since are just cyclically overwritten on-the-go. let prev_epoch_tag = epoch_tag ^ 1; let prev_epoch_tickets_count = &mut metadata.tickets_count[prev_epoch_tag as usize]; if *prev_epoch_tickets_count != 0 { for idx in 0..*prev_epoch_tickets_count { if let Some(ticket_id) = TicketsIds::::get((prev_epoch_tag, idx)) { TicketsData::::remove(ticket_id); } } *prev_epoch_tickets_count = 0; metadata_dirty = true; } if metadata_dirty { TicketsMeta::::set(metadata); } } // Call this function on epoch change to enact current epoch randomness. // // Returns the next epoch randomness. fn update_epoch_randomness(next_epoch_index: u64) -> Randomness { let curr_epoch_randomness = NextRandomness::::get(); CurrentRandomness::::put(curr_epoch_randomness); let accumulator = RandomnessAccumulator::::get(); let mut buf = [0; RANDOMNESS_LENGTH + 8]; buf[..RANDOMNESS_LENGTH].copy_from_slice(&accumulator[..]); buf[RANDOMNESS_LENGTH..].copy_from_slice(&next_epoch_index.to_le_bytes()); let next_randomness = hashing::blake2_256(&buf); NextRandomness::::put(&next_randomness); next_randomness } // Deposit per-slot randomness. fn deposit_slot_randomness(randomness: &Randomness) { let accumulator = RandomnessAccumulator::::get(); let mut buf = [0; 2 * RANDOMNESS_LENGTH]; buf[..RANDOMNESS_LENGTH].copy_from_slice(&accumulator[..]); buf[RANDOMNESS_LENGTH..].copy_from_slice(&randomness[..]); let accumulator = hashing::blake2_256(&buf); RandomnessAccumulator::::put(accumulator); } // Deposit next epoch descriptor in the block header digest. fn deposit_next_epoch_descriptor_digest(desc: NextEpochDescriptor) { let item = ConsensusLog::NextEpochData(desc); let log = DigestItem::Consensus(SASSAFRAS_ENGINE_ID, item.encode()); >::deposit_log(log) } // Initialize authorities on genesis phase. // // Genesis authorities may have been initialized via other means (e.g. via session pallet). // // If this function has already been called with some authorities, then the new list // should match the previously set one. fn genesis_authorities_initialize(authorities: &[AuthorityId]) { let prev_authorities = Authorities::::get(); if !prev_authorities.is_empty() { // This function has already been called. if prev_authorities.as_slice() == authorities { return } else { panic!("Authorities were already initialized"); } } let authorities = WeakBoundedVec::try_from(authorities.to_vec()) .expect("Initial number of authorities should be lower than T::MaxAuthorities"); Authorities::::put(&authorities); NextAuthorities::::put(&authorities); } // Method to be called on first block `on_initialize` to properly populate some key parameters. fn post_genesis_initialize(slot: Slot) { // Keep track of the actual first slot used (may not be zero based). GenesisSlot::::put(slot); // Properly initialize randomness using genesis hash and current slot. // This is important to guarantee that a different set of tickets are produced for: // - different chains which share the same ring parameters and // - same chain started with a different slot base. let genesis_hash = frame_system::Pallet::::parent_hash(); let mut buf = genesis_hash.as_ref().to_vec(); buf.extend_from_slice(&slot.to_le_bytes()); let randomness = hashing::blake2_256(buf.as_slice()); RandomnessAccumulator::::put(randomness); let next_randomness = Self::update_epoch_randomness(1); // Deposit a log as this is the first block in first epoch. let next_epoch = NextEpochDescriptor { randomness: next_randomness, authorities: Self::next_authorities().into_inner(), config: None, }; Self::deposit_next_epoch_descriptor_digest(next_epoch); } /// Current epoch information. pub fn current_epoch() -> Epoch { let index = EpochIndex::::get(); Epoch { index, start: Self::epoch_start(index), length: T::EpochLength::get(), authorities: Self::authorities().into_inner(), randomness: Self::randomness(), config: Self::config(), } } /// Next epoch information. pub fn next_epoch() -> Epoch { let index = EpochIndex::::get() + 1; Epoch { index, start: Self::epoch_start(index), length: T::EpochLength::get(), authorities: Self::next_authorities().into_inner(), randomness: Self::next_randomness(), config: Self::next_config().unwrap_or_else(|| Self::config()), } } /// Fetch expected ticket-id 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, /// with n >= k, then the tickets are assigned to the slots according to the following /// strategy: /// /// slot-index : [ 0, 1, 2, ............ , n ] /// tickets : [ t1, t3, t5, ... , t4, t2, t0 ]. /// /// With slot-index computed as `epoch_start() - slot`. /// /// If `slot` value falls within the current epoch then we fetch tickets from the current epoch /// tickets list. /// /// If `slot` value falls within the next epoch then we fetch tickets from the next epoch /// tickets ids list. Note that in this case we may have not finished receiving all the tickets /// for that epoch yet. The next epoch tickets should be considered "stable" only after the /// current epoch first half slots were elapsed (see `submit_tickets_unsigned_extrinsic`). /// /// Returns `None` if, according to the sorting strategy, there is no ticket associated to the /// specified slot-index (happens if a ticket falls in the middle of an epoch and n > k), /// or if the slot falls beyond the next epoch. /// /// Before importing the first block this returns `None`. pub fn slot_ticket_id(slot: Slot) -> Option { if frame_system::Pallet::::block_number().is_zero() { return None } let epoch_idx = EpochIndex::::get(); let epoch_len = T::EpochLength::get(); let mut slot_idx = Self::slot_index(slot); let mut metadata = TicketsMeta::::get(); let get_ticket_idx = |slot_idx| { let ticket_idx = if slot_idx < epoch_len / 2 { 2 * slot_idx + 1 } else { 2 * (epoch_len - (slot_idx + 1)) }; debug!( target: LOG_TARGET, "slot-idx {} <-> ticket-idx {}", slot_idx, ticket_idx ); ticket_idx as u32 }; let mut epoch_tag = (epoch_idx & 1) as u8; if epoch_len <= slot_idx && slot_idx < 2 * epoch_len { // 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_tag ^= 1; slot_idx -= epoch_len; if metadata.unsorted_tickets_count != 0 { Self::sort_segments(u32::MAX, epoch_tag, &mut metadata); TicketsMeta::::set(metadata); } } else if slot_idx >= 2 * epoch_len { return None } let ticket_idx = get_ticket_idx(slot_idx); if ticket_idx < metadata.tickets_count[epoch_tag as usize] { TicketsIds::::get((epoch_tag, ticket_idx)) } else { None } } /// Returns ticket id and data associated with the given `slot`. /// /// Refer to the `slot_ticket_id` documentation for the slot-ticket association /// criteria. pub fn slot_ticket(slot: Slot) -> Option<(TicketId, TicketBody)> { Self::slot_ticket_id(slot).and_then(|id| TicketsData::::get(id).map(|body| (id, body))) } // Sort and truncate candidate tickets, cleanup storage. fn sort_and_truncate(candidates: &mut Vec, max_tickets: usize) -> u128 { candidates.sort_unstable(); candidates.drain(max_tickets..).for_each(TicketsData::::remove); candidates[max_tickets - 1] } /// Sort the tickets which belong to the epoch with the specified `epoch_tag`. /// /// At most `max_segments` are taken from the `UnsortedSegments` structure. /// /// The tickets of the removed segments are merged with the tickets on the `SortedCandidates` /// which is then sorted an truncated to contain at most `MaxTickets` entries. /// /// If all the entries in `UnsortedSegments` are consumed, then `SortedCandidates` is elected /// as the next epoch tickets, else it is saved to be used by next calls of this function. pub(crate) fn sort_segments(max_segments: u32, epoch_tag: u8, metadata: &mut TicketsMetadata) { let unsorted_segments_count = metadata.unsorted_tickets_count.div_ceil(SEGMENT_MAX_SIZE); let max_segments = max_segments.min(unsorted_segments_count); let max_tickets = Self::epoch_length() as usize; // Fetch the sorted candidates (if any). let mut candidates = SortedCandidates::::take().into_inner(); // There is an upper bound to check only if we already sorted the max number // of allowed tickets. let mut upper_bound = *candidates.get(max_tickets - 1).unwrap_or(&TicketId::MAX); let mut require_sort = false; // Consume at most `max_segments` segments. // During the process remove every stale ticket from `TicketsData` storage. for segment_idx in (0..unsorted_segments_count).rev().take(max_segments as usize) { let segment = UnsortedSegments::::take(segment_idx); metadata.unsorted_tickets_count -= segment.len() as u32; // Push only ids with a value less than the current `upper_bound`. let prev_len = candidates.len(); for ticket_id in segment { if ticket_id < upper_bound { candidates.push(ticket_id); } else { TicketsData::::remove(ticket_id); } } require_sort = candidates.len() != prev_len; // As we approach the tail of the segments buffer the `upper_bound` value is expected // to decrease (fast). We thus expect the number of tickets pushed into the // `candidates` vector to follow an exponential drop. // // Given this, sorting and truncating after processing each segment may be an overkill // as we may find pushing few tickets more and more often. Is preferable to perform // the sort and truncation operations only when we reach some bigger threshold // (currently set as twice the capacity of `SortCandidate`). // // The more is the protocol's redundancy factor (i.e. the ratio between tickets allowed // to be submitted and the epoch length) the more this check becomes relevant. if candidates.len() > 2 * max_tickets { upper_bound = Self::sort_and_truncate(&mut candidates, max_tickets); require_sort = false; } } if candidates.len() > max_tickets { Self::sort_and_truncate(&mut candidates, max_tickets); } else if require_sort { candidates.sort_unstable(); } if metadata.unsorted_tickets_count == 0 { // Sorting is over, write to next epoch map. candidates.iter().enumerate().for_each(|(i, id)| { TicketsIds::::insert((epoch_tag, i as u32), id); }); metadata.tickets_count[epoch_tag as usize] = candidates.len() as u32; } else { // Keep the partial result for the next calls. SortedCandidates::::set(BoundedVec::truncate_from(candidates)); } } /// Append a set of tickets to the segments map. pub(crate) fn append_tickets(mut tickets: BoundedVec>) { debug!(target: LOG_TARGET, "Appending batch with {} tickets", tickets.len()); tickets.iter().for_each(|t| trace!(target: LOG_TARGET, " + {t:032x}")); let mut metadata = TicketsMeta::::get(); let mut segment_idx = metadata.unsorted_tickets_count / SEGMENT_MAX_SIZE; while !tickets.is_empty() { let rem = metadata.unsorted_tickets_count % SEGMENT_MAX_SIZE; let to_be_added = tickets.len().min((SEGMENT_MAX_SIZE - rem) as usize); let mut segment = UnsortedSegments::::get(segment_idx); let _ = segment .try_extend(tickets.drain(..to_be_added)) .defensive_proof("We don't add more than `SEGMENT_MAX_SIZE` and this is the maximum bound for the vector."); UnsortedSegments::::insert(segment_idx, segment); metadata.unsorted_tickets_count += to_be_added as u32; segment_idx += 1; } TicketsMeta::::set(metadata); } /// Remove all tickets related data. /// /// May not be efficient as the calling places may repeat some of this operations /// but is a very extraordinary operation (hopefully never happens in production) /// and better safe than sorry. fn reset_tickets_data() { let metadata = TicketsMeta::::get(); // Remove even/odd-epoch data. for epoch_tag in 0..=1 { for idx in 0..metadata.tickets_count[epoch_tag] { if let Some(id) = TicketsIds::::get((epoch_tag as u8, idx)) { TicketsData::::remove(id); } } } // Remove all unsorted tickets segments. let segments_count = metadata.unsorted_tickets_count.div_ceil(SEGMENT_MAX_SIZE); (0..segments_count).for_each(UnsortedSegments::::remove); // Reset sorted candidates SortedCandidates::::kill(); // Reset tickets metadata TicketsMeta::::kill(); } /// Submit next epoch validator tickets via an unsigned extrinsic constructed with a call to /// `submit_unsigned_transaction`. /// /// The submitted tickets are added to the next epoch outstanding tickets as long as the /// extrinsic is called within the first half of the epoch. Tickets received during the /// second half are dropped. pub fn submit_tickets_unsigned_extrinsic(tickets: Vec) -> bool { let tickets = BoundedVec::truncate_from(tickets); let call = Call::submit_tickets { tickets }; match SubmitTransaction::>::submit_unsigned_transaction(call.into()) { Ok(_) => true, Err(e) => { error!(target: LOG_TARGET, "Error submitting tickets {:?}", e); false }, } } /// Epoch length pub fn epoch_length() -> u32 { T::EpochLength::get() } } /// Trigger an epoch change, if any should take place. pub trait EpochChangeTrigger { /// May trigger an epoch change, if any should take place. /// /// Returns an optional `Weight` if epoch change has been triggered. /// /// This should be called during every block, after initialization is done. fn trigger(_: BlockNumberFor) -> Weight; } /// An `EpochChangeTrigger` which does nothing. /// /// In practice this means that the epoch change logic is left to some external component /// (e.g. pallet-session). pub struct EpochChangeExternalTrigger; impl EpochChangeTrigger for EpochChangeExternalTrigger { fn trigger(_: BlockNumberFor) -> Weight { // nothing - trigger is external. Weight::zero() } } /// An `EpochChangeTrigger` which recycle the same authorities set forever. /// /// The internal trigger should only be used when no other module is responsible for /// changing authority set. pub struct EpochChangeInternalTrigger; impl EpochChangeTrigger for EpochChangeInternalTrigger { fn trigger(block_num: BlockNumberFor) -> Weight { if Pallet::::should_end_epoch(block_num) { let authorities = Pallet::::next_authorities(); let next_authorities = authorities.clone(); let len = next_authorities.len() as u32; Pallet::::enact_epoch_change(authorities, next_authorities); T::WeightInfo::enact_epoch_change(len, T::EpochLength::get()) } else { Weight::zero() } } } impl BoundToRuntimeAppPublic for Pallet { type Public = AuthorityId; }