From d26386be6e7c8b4c7f4cbb7b3da9598bd26510cd Mon Sep 17 00:00:00 2001 From: Davide Galassi Date: Fri, 3 May 2024 17:25:31 +0200 Subject: [PATCH 01/42] Begin Sassafras overhaul to align to the latest specs --- substrate/frame/sassafras-old/Cargo.toml | 63 + substrate/frame/sassafras-old/README.md | 8 + .../src/benchmarking.rs | 0 .../src/data/benchmark-results.md | 0 .../src/data/tickets-sort.md | 0 .../src/data/tickets-sort.png | Bin substrate/frame/sassafras-old/src/lib.rs | 1028 +++++++++++++++++ substrate/frame/sassafras-old/src/mock.rs | 343 ++++++ substrate/frame/sassafras-old/src/tests.rs | 874 ++++++++++++++ .../src/weights.rs | 0 .../src/data/25_tickets_100_auths.bin | Bin 24728 -> 0 bytes substrate/frame/sassafras/src/lib.rs | 567 +++------ substrate/frame/sassafras/src/mock.rs | 204 +--- substrate/frame/sassafras/src/tests.rs | 861 +++----------- .../consensus/sassafras/src/digests.rs | 14 +- .../primitives/consensus/sassafras/src/lib.rs | 7 +- .../consensus/sassafras/src/ticket.rs | 68 +- .../primitives/consensus/sassafras/src/vrf.rs | 4 +- 18 files changed, 2707 insertions(+), 1334 deletions(-) create mode 100644 substrate/frame/sassafras-old/Cargo.toml create mode 100644 substrate/frame/sassafras-old/README.md rename substrate/frame/{sassafras => sassafras-old}/src/benchmarking.rs (100%) rename substrate/frame/{sassafras => sassafras-old}/src/data/benchmark-results.md (100%) rename substrate/frame/{sassafras => sassafras-old}/src/data/tickets-sort.md (100%) rename substrate/frame/{sassafras => sassafras-old}/src/data/tickets-sort.png (100%) create mode 100644 substrate/frame/sassafras-old/src/lib.rs create mode 100644 substrate/frame/sassafras-old/src/mock.rs create mode 100644 substrate/frame/sassafras-old/src/tests.rs rename substrate/frame/{sassafras => sassafras-old}/src/weights.rs (100%) delete mode 100644 substrate/frame/sassafras/src/data/25_tickets_100_auths.bin diff --git a/substrate/frame/sassafras-old/Cargo.toml b/substrate/frame/sassafras-old/Cargo.toml new file mode 100644 index 000000000000..888b1d8f31fc --- /dev/null +++ b/substrate/frame/sassafras-old/Cargo.toml @@ -0,0 +1,63 @@ +[package] +name = "pallet-sassafras" +version = "0.3.5-dev" +authors = ["Parity Technologies "] +edition.workspace = true +license = "Apache-2.0" +homepage = "https://substrate.io" +repository = "https://github.com/paritytech/substrate/" +description = "Consensus extension module for Sassafras consensus." +readme = "README.md" +publish = false + +[lints] +workspace = true + +[package.metadata.docs.rs] +targets = ["x86_64-unknown-linux-gnu"] + +[dependencies] +scale-codec = { package = "parity-scale-codec", version = "3.6.1", default-features = false, features = ["derive"] } +scale-info = { version = "2.11.1", default-features = false, features = ["derive"] } +frame-benchmarking = { path = "../benchmarking", default-features = false, optional = true } +frame-support = { path = "../support", default-features = false } +frame-system = { path = "../system", default-features = false } +log = { workspace = true } +sp-consensus-sassafras = { path = "../../primitives/consensus/sassafras", default-features = false, features = ["serde"] } +sp-io = { path = "../../primitives/io", default-features = false } +sp-runtime = { path = "../../primitives/runtime", default-features = false } +sp-std = { path = "../../primitives/std", default-features = false } + +[dev-dependencies] +array-bytes = "6.1" +sp-core = { path = "../../primitives/core" } +sp-crypto-hashing = { path = "../../primitives/crypto/hashing" } + +[features] +default = ["std"] +std = [ + "frame-benchmarking?/std", + "frame-support/std", + "frame-system/std", + "log/std", + "scale-codec/std", + "scale-info/std", + "sp-consensus-sassafras/std", + "sp-io/std", + "sp-runtime/std", + "sp-std/std", +] +runtime-benchmarks = [ + "frame-benchmarking/runtime-benchmarks", + "frame-support/runtime-benchmarks", + "frame-system/runtime-benchmarks", + "sp-runtime/runtime-benchmarks", +] +try-runtime = [ + "frame-support/try-runtime", + "frame-system/try-runtime", + "sp-runtime/try-runtime", +] +# Construct dummy ring context on genesis. +# Mostly used for testing and development. +construct-dummy-ring-context = [] diff --git a/substrate/frame/sassafras-old/README.md b/substrate/frame/sassafras-old/README.md new file mode 100644 index 000000000000..f0e24a053557 --- /dev/null +++ b/substrate/frame/sassafras-old/README.md @@ -0,0 +1,8 @@ +Runtime module for SASSAFRAS consensus. + +- Tracking issue: https://github.com/paritytech/polkadot-sdk/issues/41 +- Protocol RFC proposal: https://github.com/polkadot-fellows/RFCs/pull/26 + +# ⚠️ WARNING ⚠️ + +The crate interfaces and structures are experimental and may be subject to changes. diff --git a/substrate/frame/sassafras/src/benchmarking.rs b/substrate/frame/sassafras-old/src/benchmarking.rs similarity index 100% rename from substrate/frame/sassafras/src/benchmarking.rs rename to substrate/frame/sassafras-old/src/benchmarking.rs diff --git a/substrate/frame/sassafras/src/data/benchmark-results.md b/substrate/frame/sassafras-old/src/data/benchmark-results.md similarity index 100% rename from substrate/frame/sassafras/src/data/benchmark-results.md rename to substrate/frame/sassafras-old/src/data/benchmark-results.md diff --git a/substrate/frame/sassafras/src/data/tickets-sort.md b/substrate/frame/sassafras-old/src/data/tickets-sort.md similarity index 100% rename from substrate/frame/sassafras/src/data/tickets-sort.md rename to substrate/frame/sassafras-old/src/data/tickets-sort.md diff --git a/substrate/frame/sassafras/src/data/tickets-sort.png b/substrate/frame/sassafras-old/src/data/tickets-sort.png similarity index 100% rename from substrate/frame/sassafras/src/data/tickets-sort.png rename to substrate/frame/sassafras-old/src/data/tickets-sort.png diff --git a/substrate/frame/sassafras-old/src/lib.rs b/substrate/frame/sassafras-old/src/lib.rs new file mode 100644 index 000000000000..522229912bca --- /dev/null +++ b/substrate/frame/sassafras-old/src/lib.rs @@ -0,0 +1,1028 @@ +// 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; + + /// Redundancy factor + #[pallet::constant] + type RedundancyFactor: Get; + + /// Max attempts number + #[pallet::constant] + type AttemptsNumber: 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>; + + /// 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>; + + /// 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) { + 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 ticket_threshold = sp_consensus_sassafras::ticket_id_threshold( + T::RedundancyFactor::get(), + epoch_length as u32, + T::AttemptsNumber::get(), + 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 ({:?} >= {:?})", ticket_id, ticket_threshold); + continue + } + + // Check for duplicates + if TicketsData::::contains_key(ticket_id) { + debug!(target: LOG_TARGET, "Ignoring duplicate ticket ({:?})", 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 ({:?})", 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()) + } + } + + #[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); + + // 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(), + }; + 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(), + }; + 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: EpochConfiguration { + redundancy_factor: T::RedundancyFactor::get(), + attempts_number: T::AttemptsNumber::get(), + }, + } + } + + /// 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: EpochConfiguration { + redundancy_factor: T::RedundancyFactor::get(), + attempts_number: T::AttemptsNumber::get(), + }, + } + } + + /// 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; +} diff --git a/substrate/frame/sassafras-old/src/mock.rs b/substrate/frame/sassafras-old/src/mock.rs new file mode 100644 index 000000000000..f145bffa3a05 --- /dev/null +++ b/substrate/frame/sassafras-old/src/mock.rs @@ -0,0 +1,343 @@ +// 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. + +//! Test utilities for Sassafras pallet. + +use crate::{self as pallet_sassafras, EpochChangeInternalTrigger, *}; + +use frame_support::{ + derive_impl, + traits::{ConstU32, OnFinalize, OnInitialize}, +}; +use sp_consensus_sassafras::{ + digests::SlotClaim, + vrf::{RingProver, VrfSignature}, + AuthorityIndex, AuthorityPair, EpochConfiguration, Slot, TicketBody, TicketEnvelope, TicketId, +}; +use sp_core::{ + crypto::{ByteArray, Pair, UncheckedFrom, VrfSecret, Wraps}, + ed25519::Public as EphemeralPublic, + H256, U256, +}; +use sp_runtime::{ + testing::{Digest, DigestItem, Header, TestXt}, + BuildStorage, +}; + +const LOG_TARGET: &str = "sassafras::tests"; + +const EPOCH_LENGTH: u32 = 10; +const MAX_AUTHORITIES: u32 = 100; + +#[derive_impl(frame_system::config_preludes::TestDefaultConfig)] +impl frame_system::Config for Test { + type Block = frame_system::mocking::MockBlock; +} + +impl frame_system::offchain::SendTransactionTypes for Test +where + RuntimeCall: From, +{ + type OverarchingCall = RuntimeCall; + type Extrinsic = TestXt; +} + +impl pallet_sassafras::Config for Test { + type EpochLength = ConstU32; + type MaxAuthorities = ConstU32; + type EpochChangeTrigger = EpochChangeInternalTrigger; + type WeightInfo = (); +} + +frame_support::construct_runtime!( + pub enum Test { + System: frame_system, + Sassafras: pallet_sassafras, + } +); + +// Default used for most of the tests. +// +// The redundancy factor has been set to max value to accept all submitted +// tickets without worrying about the threshold. +pub const TEST_EPOCH_CONFIGURATION: EpochConfiguration = + EpochConfiguration { redundancy_factor: u32::MAX, attempts_number: 5 }; + +/// Build and returns test storage externalities +pub fn new_test_ext(authorities_len: usize) -> sp_io::TestExternalities { + new_test_ext_with_pairs(authorities_len, false).1 +} + +/// Build and returns test storage externalities and authority set pairs used +/// by Sassafras genesis configuration. +pub fn new_test_ext_with_pairs( + authorities_len: usize, + with_ring_context: bool, +) -> (Vec, sp_io::TestExternalities) { + let pairs = (0..authorities_len) + .map(|i| AuthorityPair::from_seed(&U256::from(i).into())) + .collect::>(); + + let authorities: Vec<_> = pairs.iter().map(|p| p.public()).collect(); + + let mut storage = frame_system::GenesisConfig::::default().build_storage().unwrap(); + + pallet_sassafras::GenesisConfig:: { + authorities: authorities.clone(), + epoch_config: TEST_EPOCH_CONFIGURATION, + _phantom: core::marker::PhantomData, + } + .assimilate_storage(&mut storage) + .unwrap(); + + let mut ext: sp_io::TestExternalities = storage.into(); + + if with_ring_context { + ext.execute_with(|| { + log::debug!(target: LOG_TARGET, "Building testing ring context"); + let ring_ctx = vrf::RingContext::new_testing(); + RingContext::::set(Some(ring_ctx.clone())); + Sassafras::update_ring_verifier(&authorities); + }); + } + + (pairs, ext) +} + +fn make_ticket_with_prover( + attempt: u32, + pair: &AuthorityPair, + prover: &RingProver, +) -> TicketEnvelope { + log::debug!("attempt: {}", attempt); + + // Values are referring to the next epoch + let epoch = Sassafras::epoch_index() + 1; + let randomness = Sassafras::next_randomness(); + + // Make a dummy ephemeral public that hopefully is unique within one test instance. + // In the tests, the values within the erased public are just used to compare + // ticket bodies, so it is not important to be a valid key. + let mut raw: [u8; 32] = [0; 32]; + raw.copy_from_slice(&pair.public().as_slice()[0..32]); + let erased_public = EphemeralPublic::unchecked_from(raw); + let revealed_public = erased_public; + + let ticket_id_input = vrf::ticket_id_input(&randomness, attempt, epoch); + + let body = TicketBody { attempt_idx: attempt, erased_public, revealed_public }; + let sign_data = vrf::ticket_body_sign_data(&body, ticket_id_input); + + let signature = pair.as_ref().ring_vrf_sign(&sign_data, &prover); + + // Ticket-id can be generated via vrf-preout. + // We don't care that much about its value here. + TicketEnvelope { body, signature } +} + +pub fn make_prover(pair: &AuthorityPair) -> RingProver { + let public = pair.public(); + let mut prover_idx = None; + + let ring_ctx = Sassafras::ring_context().unwrap(); + + let pks: Vec = Sassafras::authorities() + .iter() + .enumerate() + .map(|(idx, auth)| { + if public == *auth { + prover_idx = Some(idx); + } + *auth.as_ref() + }) + .collect(); + + log::debug!("Building prover. Ring size: {}", pks.len()); + let prover = ring_ctx.prover(&pks, prover_idx.unwrap()).unwrap(); + log::debug!("Done"); + + prover +} + +/// Construct `attempts` tickets envelopes for the next epoch. +/// +/// E.g. by passing an optional threshold +pub fn make_tickets(attempts: u32, pair: &AuthorityPair) -> Vec { + let prover = make_prover(pair); + (0..attempts) + .into_iter() + .map(|attempt| make_ticket_with_prover(attempt, pair, &prover)) + .collect() +} + +pub fn make_ticket_body(attempt_idx: u32, pair: &AuthorityPair) -> (TicketId, TicketBody) { + // Values are referring to the next epoch + let epoch = Sassafras::epoch_index() + 1; + let randomness = Sassafras::next_randomness(); + + let ticket_id_input = vrf::ticket_id_input(&randomness, attempt_idx, epoch); + let ticket_id_pre_output = pair.as_inner_ref().vrf_pre_output(&ticket_id_input); + + let id = vrf::make_ticket_id(&ticket_id_input, &ticket_id_pre_output); + + // Make a dummy ephemeral public that hopefully is unique within one test instance. + // In the tests, the values within the erased public are just used to compare + // ticket bodies, so it is not important to be a valid key. + let mut raw: [u8; 32] = [0; 32]; + raw[..16].copy_from_slice(&pair.public().as_slice()[0..16]); + raw[16..].copy_from_slice(&id.to_le_bytes()); + let erased_public = EphemeralPublic::unchecked_from(raw); + let revealed_public = erased_public; + + let body = TicketBody { attempt_idx, erased_public, revealed_public }; + + (id, body) +} + +pub fn make_dummy_ticket_body(attempt_idx: u32) -> (TicketId, TicketBody) { + let hash = sp_crypto_hashing::blake2_256(&attempt_idx.to_le_bytes()); + + let erased_public = EphemeralPublic::unchecked_from(hash); + let revealed_public = erased_public; + + let body = TicketBody { attempt_idx, erased_public, revealed_public }; + + let mut bytes = [0u8; 16]; + bytes.copy_from_slice(&hash[..16]); + let id = TicketId::from_le_bytes(bytes); + + (id, body) +} + +pub fn make_ticket_bodies( + number: u32, + pair: Option<&AuthorityPair>, +) -> Vec<(TicketId, TicketBody)> { + (0..number) + .into_iter() + .map(|i| match pair { + Some(pair) => make_ticket_body(i, pair), + None => make_dummy_ticket_body(i), + }) + .collect() +} + +/// Persist the given tickets in the unsorted segments buffer. +/// +/// This function skips all the checks performed by the `submit_tickets` extrinsic and +/// directly appends the tickets to the `UnsortedSegments` structure. +pub fn persist_next_epoch_tickets_as_segments(tickets: &[(TicketId, TicketBody)]) { + let mut ids = Vec::with_capacity(tickets.len()); + tickets.iter().for_each(|(id, body)| { + TicketsData::::set(id, Some(body.clone())); + ids.push(*id); + }); + let max_chunk_size = Sassafras::epoch_length() as usize; + ids.chunks(max_chunk_size).for_each(|chunk| { + Sassafras::append_tickets(BoundedVec::truncate_from(chunk.to_vec())); + }) +} + +/// Calls the [`persist_next_epoch_tickets_as_segments`] and then proceeds to the +/// sorting of the candidates. +/// +/// Only "winning" tickets are left. +pub fn persist_next_epoch_tickets(tickets: &[(TicketId, TicketBody)]) { + persist_next_epoch_tickets_as_segments(tickets); + // Force sorting of next epoch tickets (enactment) by explicitly querying the first of them. + let next_epoch = Sassafras::next_epoch(); + assert_eq!(TicketsMeta::::get().unsorted_tickets_count, tickets.len() as u32); + Sassafras::slot_ticket(next_epoch.start).unwrap(); + assert_eq!(TicketsMeta::::get().unsorted_tickets_count, 0); +} + +fn slot_claim_vrf_signature(slot: Slot, pair: &AuthorityPair) -> VrfSignature { + let mut epoch = Sassafras::epoch_index(); + let mut randomness = Sassafras::randomness(); + + // Check if epoch is going to change on initialization. + let epoch_start = Sassafras::current_epoch_start(); + let epoch_length = EPOCH_LENGTH.into(); + if epoch_start != 0_u64 && slot >= epoch_start + epoch_length { + epoch += slot.saturating_sub(epoch_start).saturating_div(epoch_length); + randomness = crate::NextRandomness::::get(); + } + + let data = vrf::slot_claim_sign_data(&randomness, slot, epoch); + pair.as_ref().vrf_sign(&data) +} + +/// Construct a `PreDigest` instance for the given parameters. +pub fn make_slot_claim( + authority_idx: AuthorityIndex, + slot: Slot, + pair: &AuthorityPair, +) -> SlotClaim { + let vrf_signature = slot_claim_vrf_signature(slot, pair); + SlotClaim { authority_idx, slot, vrf_signature, ticket_claim: None } +} + +/// Construct a `Digest` with a `SlotClaim` item. +pub fn make_digest(authority_idx: AuthorityIndex, slot: Slot, pair: &AuthorityPair) -> Digest { + let claim = make_slot_claim(authority_idx, slot, pair); + Digest { logs: vec![DigestItem::from(&claim)] } +} + +pub fn initialize_block( + number: u64, + slot: Slot, + parent_hash: H256, + pair: &AuthorityPair, +) -> Digest { + let digest = make_digest(0, slot, pair); + System::reset_events(); + System::initialize(&number, &parent_hash, &digest); + Sassafras::on_initialize(number); + digest +} + +pub fn finalize_block(number: u64) -> Header { + Sassafras::on_finalize(number); + System::finalize() +} + +/// Progress the pallet state up to the given block `number` and `slot`. +pub fn go_to_block(number: u64, slot: Slot, pair: &AuthorityPair) -> Digest { + Sassafras::on_finalize(System::block_number()); + let parent_hash = System::finalize().hash(); + + let digest = make_digest(0, slot, pair); + + System::reset_events(); + System::initialize(&number, &parent_hash, &digest); + Sassafras::on_initialize(number); + + digest +} + +/// Progress the pallet state up to the given block `number`. +/// Slots will grow linearly accordingly to blocks. +pub fn progress_to_block(number: u64, pair: &AuthorityPair) -> Option { + let mut slot = Sassafras::current_slot() + 1; + let mut digest = None; + for i in System::block_number() + 1..=number { + let dig = go_to_block(i, slot, pair); + digest = Some(dig); + slot = slot + 1; + } + digest +} diff --git a/substrate/frame/sassafras-old/src/tests.rs b/substrate/frame/sassafras-old/src/tests.rs new file mode 100644 index 000000000000..ec3425cce7bf --- /dev/null +++ b/substrate/frame/sassafras-old/src/tests.rs @@ -0,0 +1,874 @@ +// 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. + +//! Tests for Sassafras pallet. + +use crate::*; +use mock::*; + +use sp_consensus_sassafras::Slot; + +fn h2b(hex: &str) -> [u8; N] { + array_bytes::hex2array_unchecked(hex) +} + +fn b2h(bytes: [u8; N]) -> String { + array_bytes::bytes2hex("", &bytes) +} + +#[test] +fn genesis_values_assumptions_check() { + new_test_ext(3).execute_with(|| { + assert_eq!(Sassafras::authorities().len(), 3); + assert_eq!(Sassafras::config(), TEST_EPOCH_CONFIGURATION); + }); +} + +#[test] +fn post_genesis_randomness_initialization() { + let (pairs, mut ext) = new_test_ext_with_pairs(1, false); + let pair = &pairs[0]; + + ext.execute_with(|| { + assert_eq!(Sassafras::randomness(), [0; 32]); + assert_eq!(Sassafras::next_randomness(), [0; 32]); + assert_eq!(Sassafras::randomness_accumulator(), [0; 32]); + + // Test the values with a zero genesis block hash + let _ = initialize_block(1, 123.into(), [0x00; 32].into(), pair); + + assert_eq!(Sassafras::randomness(), [0; 32]); + println!("[DEBUG] {}", b2h(Sassafras::next_randomness())); + assert_eq!( + Sassafras::next_randomness(), + h2b("b9497550deeeb4adc134555930de61968a0558f8947041eb515b2f5fa68ffaf7") + ); + println!("[DEBUG] {}", b2h(Sassafras::randomness_accumulator())); + assert_eq!( + Sassafras::randomness_accumulator(), + h2b("febcc7fe9539fe17ed29f525831394edfb30b301755dc9bd91584a1f065faf87") + ); + let (id1, _) = make_ticket_bodies(1, Some(pair))[0]; + + // Reset what is relevant + NextRandomness::::set([0; 32]); + RandomnessAccumulator::::set([0; 32]); + + // Test the values with a non-zero genesis block hash + let _ = initialize_block(1, 123.into(), [0xff; 32].into(), pair); + + assert_eq!(Sassafras::randomness(), [0; 32]); + println!("[DEBUG] {}", b2h(Sassafras::next_randomness())); + assert_eq!( + Sassafras::next_randomness(), + h2b("51c1e3b3a73d2043b3cabae98ff27bdd4aad8967c21ecda7b9465afaa0e70f37") + ); + println!("[DEBUG] {}", b2h(Sassafras::randomness_accumulator())); + assert_eq!( + Sassafras::randomness_accumulator(), + h2b("466bf3007f2e17bffee0b3c42c90f33d654f5ff61eff28b0cc650825960abd52") + ); + let (id2, _) = make_ticket_bodies(1, Some(pair))[0]; + + // Ticket ids should be different when next epoch randomness is different + assert_ne!(id1, id2); + + // Reset what is relevant + NextRandomness::::set([0; 32]); + RandomnessAccumulator::::set([0; 32]); + + // Test the values with a non-zero genesis block hash + let _ = initialize_block(1, 321.into(), [0x00; 32].into(), pair); + + println!("[DEBUG] {}", b2h(Sassafras::next_randomness())); + assert_eq!( + Sassafras::next_randomness(), + h2b("d85d84a54f79453000eb62e8a17b30149bd728d3232bc2787a89d51dc9a36008") + ); + println!("[DEBUG] {}", b2h(Sassafras::randomness_accumulator())); + assert_eq!( + Sassafras::randomness_accumulator(), + h2b("8a035eed02b5b8642b1515ed19752df8df156627aea45c4ef6e3efa88be9a74d") + ); + let (id2, _) = make_ticket_bodies(1, Some(pair))[0]; + + // Ticket ids should be different when next epoch randomness is different + assert_ne!(id1, id2); + }); +} + +// Tests if the sorted tickets are assigned to each slot outside-in. +#[test] +fn slot_ticket_id_outside_in_fetch() { + let genesis_slot = Slot::from(100); + let tickets_count = 6; + + // Current epoch tickets + let curr_tickets: Vec = (0..tickets_count).map(|i| i as TicketId).collect(); + + // Next epoch tickets + let next_tickets: Vec = + (0..tickets_count - 1).map(|i| (i + tickets_count) as TicketId).collect(); + + new_test_ext(0).execute_with(|| { + // Some corner cases + TicketsIds::::insert((0, 0_u32), 1_u128); + + // Cleanup + (0..3).for_each(|i| TicketsIds::::remove((0, i as u32))); + + curr_tickets + .iter() + .enumerate() + .for_each(|(i, id)| TicketsIds::::insert((0, i as u32), id)); + + next_tickets + .iter() + .enumerate() + .for_each(|(i, id)| TicketsIds::::insert((1, i as u32), id)); + + TicketsMeta::::set(TicketsMetadata { + tickets_count: [curr_tickets.len() as u32, next_tickets.len() as u32], + unsorted_tickets_count: 0, + }); + + // Before importing the first block the pallet always return `None` + // This is a kind of special hardcoded case that should never happen in practice + // as the first thing the pallet does is to initialize the genesis slot. + + assert_eq!(Sassafras::slot_ticket_id(0.into()), None); + assert_eq!(Sassafras::slot_ticket_id(genesis_slot + 0), None); + assert_eq!(Sassafras::slot_ticket_id(genesis_slot + 1), None); + assert_eq!(Sassafras::slot_ticket_id(genesis_slot + 100), None); + + // Initialize genesis slot.. + GenesisSlot::::set(genesis_slot); + frame_system::Pallet::::set_block_number(One::one()); + + // Try to fetch a ticket for a slot before current epoch. + assert_eq!(Sassafras::slot_ticket_id(0.into()), None); + + // Current epoch tickets. + assert_eq!(Sassafras::slot_ticket_id(genesis_slot + 0), Some(curr_tickets[1])); + assert_eq!(Sassafras::slot_ticket_id(genesis_slot + 1), Some(curr_tickets[3])); + assert_eq!(Sassafras::slot_ticket_id(genesis_slot + 2), Some(curr_tickets[5])); + assert_eq!(Sassafras::slot_ticket_id(genesis_slot + 3), None); + assert_eq!(Sassafras::slot_ticket_id(genesis_slot + 4), None); + assert_eq!(Sassafras::slot_ticket_id(genesis_slot + 5), None); + assert_eq!(Sassafras::slot_ticket_id(genesis_slot + 6), None); + assert_eq!(Sassafras::slot_ticket_id(genesis_slot + 7), Some(curr_tickets[4])); + assert_eq!(Sassafras::slot_ticket_id(genesis_slot + 8), Some(curr_tickets[2])); + assert_eq!(Sassafras::slot_ticket_id(genesis_slot + 9), Some(curr_tickets[0])); + + // Next epoch tickets (note that only 5 tickets are available) + assert_eq!(Sassafras::slot_ticket_id(genesis_slot + 10), Some(next_tickets[1])); + assert_eq!(Sassafras::slot_ticket_id(genesis_slot + 11), Some(next_tickets[3])); + assert_eq!(Sassafras::slot_ticket_id(genesis_slot + 12), None); + assert_eq!(Sassafras::slot_ticket_id(genesis_slot + 13), None); + assert_eq!(Sassafras::slot_ticket_id(genesis_slot + 14), None); + assert_eq!(Sassafras::slot_ticket_id(genesis_slot + 15), None); + assert_eq!(Sassafras::slot_ticket_id(genesis_slot + 16), None); + assert_eq!(Sassafras::slot_ticket_id(genesis_slot + 17), Some(next_tickets[4])); + assert_eq!(Sassafras::slot_ticket_id(genesis_slot + 18), Some(next_tickets[2])); + assert_eq!(Sassafras::slot_ticket_id(genesis_slot + 19), Some(next_tickets[0])); + + // Try to fetch the tickets for slots beyond the next epoch. + assert_eq!(Sassafras::slot_ticket_id(genesis_slot + 20), None); + assert_eq!(Sassafras::slot_ticket_id(genesis_slot + 42), None); + }); +} + +// Different test for outside-in test with more focus on corner case correctness. +#[test] +fn slot_ticket_id_outside_in_fetch_corner_cases() { + new_test_ext(0).execute_with(|| { + frame_system::Pallet::::set_block_number(One::one()); + + let mut meta = TicketsMetadata { tickets_count: [0, 0], unsorted_tickets_count: 0 }; + let curr_epoch_idx = EpochIndex::::get(); + + let mut epoch_test = |epoch_idx| { + let tag = (epoch_idx & 1) as u8; + let epoch_start = Sassafras::epoch_start(epoch_idx); + + // cleanup + meta.tickets_count = [0, 0]; + TicketsMeta::::set(meta); + assert!((0..10).all(|i| Sassafras::slot_ticket_id((epoch_start + i).into()).is_none())); + + meta.tickets_count[tag as usize] += 1; + TicketsMeta::::set(meta); + TicketsIds::::insert((tag, 0_u32), 1_u128); + assert_eq!(Sassafras::slot_ticket_id((epoch_start + 9).into()), Some(1_u128)); + assert!((0..9).all(|i| Sassafras::slot_ticket_id((epoch_start + i).into()).is_none())); + + meta.tickets_count[tag as usize] += 1; + TicketsMeta::::set(meta); + TicketsIds::::insert((tag, 1_u32), 2_u128); + assert_eq!(Sassafras::slot_ticket_id((epoch_start + 0).into()), Some(2_u128)); + assert!((1..9).all(|i| Sassafras::slot_ticket_id((epoch_start + i).into()).is_none())); + + meta.tickets_count[tag as usize] += 2; + TicketsMeta::::set(meta); + TicketsIds::::insert((tag, 2_u32), 3_u128); + assert_eq!(Sassafras::slot_ticket_id((epoch_start + 8).into()), Some(3_u128)); + assert!((1..8).all(|i| Sassafras::slot_ticket_id((epoch_start + i).into()).is_none())); + }; + + // Even epoch + epoch_test(curr_epoch_idx); + epoch_test(curr_epoch_idx + 1); + }); +} + +#[test] +fn on_first_block_after_genesis() { + let (pairs, mut ext) = new_test_ext_with_pairs(4, false); + + ext.execute_with(|| { + let start_slot = Slot::from(100); + let start_block = 1; + + let digest = initialize_block(start_block, start_slot, Default::default(), &pairs[0]); + + let common_assertions = || { + assert_eq!(Sassafras::genesis_slot(), start_slot); + assert_eq!(Sassafras::current_slot(), start_slot); + assert_eq!(Sassafras::epoch_index(), 0); + assert_eq!(Sassafras::current_epoch_start(), start_slot); + assert_eq!(Sassafras::current_slot_index(), 0); + assert_eq!(Sassafras::randomness(), [0; 32]); + println!("[DEBUG] {}", b2h(Sassafras::next_randomness())); + assert_eq!( + Sassafras::next_randomness(), + h2b("a49592ef190b96f3eb87bde4c8355e33df28c75006156e8c81998158de2ed49e") + ); + }; + + // Post-initialization status + + assert!(ClaimTemporaryData::::exists()); + common_assertions(); + println!("[DEBUG] {}", b2h(Sassafras::randomness_accumulator())); + assert_eq!( + Sassafras::randomness_accumulator(), + h2b("f0d42f6b7c0d157ecbd788be44847b80a96c290c04b5dfa5d1d40c98aa0c04ed") + ); + + let header = finalize_block(start_block); + + // Post-finalization status + + assert!(!ClaimTemporaryData::::exists()); + common_assertions(); + println!("[DEBUG] {}", b2h(Sassafras::randomness_accumulator())); + assert_eq!( + Sassafras::randomness_accumulator(), + h2b("9f2b9fd19a772c34d437dcd8b84a927e73a5cb43d3d1cd00093223d60d2b4843"), + ); + + // Header data check + + assert_eq!(header.digest.logs.len(), 2); + assert_eq!(header.digest.logs[0], digest.logs[0]); + + // Genesis epoch start deposits consensus + let consensus_log = sp_consensus_sassafras::digests::ConsensusLog::NextEpochData( + sp_consensus_sassafras::digests::NextEpochDescriptor { + authorities: Sassafras::next_authorities().into_inner(), + randomness: Sassafras::next_randomness(), + config: None, + }, + ); + let consensus_digest = DigestItem::Consensus(SASSAFRAS_ENGINE_ID, consensus_log.encode()); + assert_eq!(header.digest.logs[1], consensus_digest) + }) +} + +#[test] +fn on_normal_block() { + let (pairs, mut ext) = new_test_ext_with_pairs(4, false); + let start_slot = Slot::from(100); + let start_block = 1; + let end_block = start_block + 1; + + ext.execute_with(|| { + initialize_block(start_block, start_slot, Default::default(), &pairs[0]); + + // We don't want to trigger an epoch change in this test. + let epoch_length = Sassafras::epoch_length() as u64; + assert!(epoch_length > end_block); + + // Progress to block 2 + let digest = progress_to_block(end_block, &pairs[0]).unwrap(); + + let common_assertions = || { + assert_eq!(Sassafras::genesis_slot(), start_slot); + assert_eq!(Sassafras::current_slot(), start_slot + 1); + assert_eq!(Sassafras::epoch_index(), 0); + assert_eq!(Sassafras::current_epoch_start(), start_slot); + assert_eq!(Sassafras::current_slot_index(), 1); + assert_eq!(Sassafras::randomness(), [0; 32]); + println!("[DEBUG] {}", b2h(Sassafras::next_randomness())); + assert_eq!( + Sassafras::next_randomness(), + h2b("a49592ef190b96f3eb87bde4c8355e33df28c75006156e8c81998158de2ed49e") + ); + }; + + // Post-initialization status + + assert!(ClaimTemporaryData::::exists()); + common_assertions(); + println!("[DEBUG] {}", b2h(Sassafras::randomness_accumulator())); + assert_eq!( + Sassafras::randomness_accumulator(), + h2b("9f2b9fd19a772c34d437dcd8b84a927e73a5cb43d3d1cd00093223d60d2b4843"), + ); + + let header = finalize_block(end_block); + + // Post-finalization status + + assert!(!ClaimTemporaryData::::exists()); + common_assertions(); + assert_eq!( + Sassafras::randomness_accumulator(), + h2b("be9261adb9686dfd3f23f8a276b7acc7f4beb3137070beb64c282ac22d84cbf0"), + ); + + // Header data check + + assert_eq!(header.digest.logs.len(), 1); + assert_eq!(header.digest.logs[0], digest.logs[0]); + }); +} + +#[test] +fn produce_epoch_change_digest_no_config() { + let (pairs, mut ext) = new_test_ext_with_pairs(4, false); + + ext.execute_with(|| { + let start_slot = Slot::from(100); + let start_block = 1; + + initialize_block(start_block, start_slot, Default::default(), &pairs[0]); + + // We want to trigger an epoch change in this test. + let epoch_length = Sassafras::epoch_length() as u64; + let end_block = start_block + epoch_length; + + let digest = progress_to_block(end_block, &pairs[0]).unwrap(); + + let common_assertions = || { + assert_eq!(Sassafras::genesis_slot(), start_slot); + assert_eq!(Sassafras::current_slot(), start_slot + epoch_length); + assert_eq!(Sassafras::epoch_index(), 1); + assert_eq!(Sassafras::current_epoch_start(), start_slot + epoch_length); + assert_eq!(Sassafras::current_slot_index(), 0); + println!("[DEBUG] {}", b2h(Sassafras::randomness())); + assert_eq!( + Sassafras::randomness(), + h2b("a49592ef190b96f3eb87bde4c8355e33df28c75006156e8c81998158de2ed49e") + ); + }; + + // Post-initialization status + + assert!(ClaimTemporaryData::::exists()); + common_assertions(); + println!("[DEBUG] {}", b2h(Sassafras::next_randomness())); + assert_eq!( + Sassafras::next_randomness(), + h2b("d3a18b857af6ecc7b52f047107e684fff0058b5722d540a296d727e37eaa55b3"), + ); + println!("[DEBUG] {}", b2h(Sassafras::randomness_accumulator())); + assert_eq!( + Sassafras::randomness_accumulator(), + h2b("bf0f1228f4ff953c8c1bda2cceb668bf86ea05d7ae93e26d021c9690995d5279"), + ); + + let header = finalize_block(end_block); + + // Post-finalization status + + assert!(!ClaimTemporaryData::::exists()); + common_assertions(); + println!("[DEBUG] {}", b2h(Sassafras::next_randomness())); + assert_eq!( + Sassafras::next_randomness(), + h2b("d3a18b857af6ecc7b52f047107e684fff0058b5722d540a296d727e37eaa55b3"), + ); + println!("[DEBUG] {}", b2h(Sassafras::randomness_accumulator())); + assert_eq!( + Sassafras::randomness_accumulator(), + h2b("8a1ceb346036c386d021264b10912c8b656799668004c4a487222462b394cd89"), + ); + + // Header data check + + assert_eq!(header.digest.logs.len(), 2); + assert_eq!(header.digest.logs[0], digest.logs[0]); + // Deposits consensus log on epoch change + let consensus_log = sp_consensus_sassafras::digests::ConsensusLog::NextEpochData( + sp_consensus_sassafras::digests::NextEpochDescriptor { + authorities: Sassafras::next_authorities().into_inner(), + randomness: Sassafras::next_randomness(), + config: None, + }, + ); + let consensus_digest = DigestItem::Consensus(SASSAFRAS_ENGINE_ID, consensus_log.encode()); + assert_eq!(header.digest.logs[1], consensus_digest) + }) +} + +#[test] +fn produce_epoch_change_digest_with_config() { + let (pairs, mut ext) = new_test_ext_with_pairs(4, false); + + ext.execute_with(|| { + let start_slot = Slot::from(100); + let start_block = 1; + + initialize_block(start_block, start_slot, Default::default(), &pairs[0]); + + let config = EpochConfiguration { redundancy_factor: 1, attempts_number: 123 }; + Sassafras::plan_config_change(RuntimeOrigin::root(), config).unwrap(); + + // We want to trigger an epoch change in this test. + let epoch_length = Sassafras::epoch_length() as u64; + let end_block = start_block + epoch_length; + + let digest = progress_to_block(end_block, &pairs[0]).unwrap(); + + let header = finalize_block(end_block); + + // Header data check. + // Skip pallet status checks that were already performed by other tests. + + assert_eq!(header.digest.logs.len(), 2); + assert_eq!(header.digest.logs[0], digest.logs[0]); + // Deposits consensus log on epoch change + let consensus_log = sp_consensus_sassafras::digests::ConsensusLog::NextEpochData( + sp_consensus_sassafras::digests::NextEpochDescriptor { + authorities: Sassafras::next_authorities().into_inner(), + randomness: Sassafras::next_randomness(), + config: Some(config), + }, + ); + let consensus_digest = DigestItem::Consensus(SASSAFRAS_ENGINE_ID, consensus_log.encode()); + assert_eq!(header.digest.logs[1], consensus_digest) + }) +} + +#[test] +fn segments_incremental_sort_works() { + let (pairs, mut ext) = new_test_ext_with_pairs(1, false); + let pair = &pairs[0]; + let segments_count = 14; + let start_slot = Slot::from(100); + let start_block = 1; + + ext.execute_with(|| { + let epoch_length = Sassafras::epoch_length() as u64; + // -3 just to have the last segment not full... + let submitted_tickets_count = segments_count * SEGMENT_MAX_SIZE - 3; + + initialize_block(start_block, start_slot, Default::default(), pair); + + // Manually populate the segments to skip the threshold check + let mut tickets = make_ticket_bodies(submitted_tickets_count, None); + persist_next_epoch_tickets_as_segments(&tickets); + + // Proceed to half of the epoch (sortition should not have been started yet) + let half_epoch_block = start_block + epoch_length / 2; + progress_to_block(half_epoch_block, pair); + + let mut unsorted_tickets_count = submitted_tickets_count; + + // Check that next epoch tickets sortition is not started yet + let meta = TicketsMeta::::get(); + assert_eq!(meta.unsorted_tickets_count, unsorted_tickets_count); + assert_eq!(meta.tickets_count, [0, 0]); + + // Follow the incremental sortition block by block + + progress_to_block(half_epoch_block + 1, pair); + unsorted_tickets_count -= 3 * SEGMENT_MAX_SIZE - 3; + let meta = TicketsMeta::::get(); + assert_eq!(meta.unsorted_tickets_count, unsorted_tickets_count,); + assert_eq!(meta.tickets_count, [0, 0]); + + progress_to_block(half_epoch_block + 2, pair); + unsorted_tickets_count -= 3 * SEGMENT_MAX_SIZE; + let meta = TicketsMeta::::get(); + assert_eq!(meta.unsorted_tickets_count, unsorted_tickets_count); + assert_eq!(meta.tickets_count, [0, 0]); + + progress_to_block(half_epoch_block + 3, pair); + unsorted_tickets_count -= 3 * SEGMENT_MAX_SIZE; + let meta = TicketsMeta::::get(); + assert_eq!(meta.unsorted_tickets_count, unsorted_tickets_count); + assert_eq!(meta.tickets_count, [0, 0]); + + progress_to_block(half_epoch_block + 4, pair); + unsorted_tickets_count -= 3 * SEGMENT_MAX_SIZE; + let meta = TicketsMeta::::get(); + assert_eq!(meta.unsorted_tickets_count, unsorted_tickets_count); + assert_eq!(meta.tickets_count, [0, 0]); + + let header = finalize_block(half_epoch_block + 4); + + // Sort should be finished now. + // Check that next epoch tickets count have the correct value. + // Bigger ticket ids were discarded during sortition. + unsorted_tickets_count -= 2 * SEGMENT_MAX_SIZE; + assert_eq!(unsorted_tickets_count, 0); + let meta = TicketsMeta::::get(); + assert_eq!(meta.unsorted_tickets_count, unsorted_tickets_count); + assert_eq!(meta.tickets_count, [0, epoch_length as u32]); + // Epoch change log should have been pushed as well + assert_eq!(header.digest.logs.len(), 1); + // No tickets for the current epoch + assert_eq!(TicketsIds::::get((0, 0)), None); + + // Check persistence of "winning" tickets + tickets.sort_by_key(|t| t.0); + (0..epoch_length as usize).into_iter().for_each(|i| { + let id = TicketsIds::::get((1, i as u32)).unwrap(); + let body = TicketsData::::get(id).unwrap(); + assert_eq!((id, body), tickets[i]); + }); + // Check removal of "loosing" tickets + (epoch_length as usize..tickets.len()).into_iter().for_each(|i| { + assert!(TicketsIds::::get((1, i as u32)).is_none()); + assert!(TicketsData::::get(tickets[i].0).is_none()); + }); + + // The next block will be the first produced on the new epoch. + // At this point the tickets are found already sorted and ready to be used. + let slot = Sassafras::current_slot() + 1; + let number = System::block_number() + 1; + initialize_block(number, slot, header.hash(), pair); + let header = finalize_block(number); + // Epoch changes digest is also produced + assert_eq!(header.digest.logs.len(), 2); + }); +} + +#[test] +fn tickets_fetch_works_after_epoch_change() { + let (pairs, mut ext) = new_test_ext_with_pairs(4, false); + let pair = &pairs[0]; + let start_slot = Slot::from(100); + let start_block = 1; + let submitted_tickets = 300; + + ext.execute_with(|| { + initialize_block(start_block, start_slot, Default::default(), pair); + + // We don't want to trigger an epoch change in this test. + let epoch_length = Sassafras::epoch_length() as u64; + assert!(epoch_length > 2); + progress_to_block(2, &pairs[0]).unwrap(); + + // Persist tickets as three different segments. + let tickets = make_ticket_bodies(submitted_tickets, None); + persist_next_epoch_tickets_as_segments(&tickets); + + let meta = TicketsMeta::::get(); + assert_eq!(meta.unsorted_tickets_count, submitted_tickets); + assert_eq!(meta.tickets_count, [0, 0]); + + // Progress up to the last epoch slot (do not enact epoch change) + progress_to_block(epoch_length, &pairs[0]).unwrap(); + + // At this point next epoch tickets should have been sorted and ready to be used + let meta = TicketsMeta::::get(); + assert_eq!(meta.unsorted_tickets_count, 0); + assert_eq!(meta.tickets_count, [0, epoch_length as u32]); + + // Compute and sort the tickets ids (aka tickets scores) + let mut expected_ids: Vec<_> = tickets.into_iter().map(|(id, _)| id).collect(); + expected_ids.sort(); + expected_ids.truncate(epoch_length as usize); + + // Check if we can fetch next epoch tickets ids (outside-in). + let slot = Sassafras::current_slot(); + assert_eq!(Sassafras::slot_ticket_id(slot + 1).unwrap(), expected_ids[1]); + assert_eq!(Sassafras::slot_ticket_id(slot + 2).unwrap(), expected_ids[3]); + assert_eq!(Sassafras::slot_ticket_id(slot + 3).unwrap(), expected_ids[5]); + assert_eq!(Sassafras::slot_ticket_id(slot + 4).unwrap(), expected_ids[7]); + assert_eq!(Sassafras::slot_ticket_id(slot + 7).unwrap(), expected_ids[6]); + assert_eq!(Sassafras::slot_ticket_id(slot + 8).unwrap(), expected_ids[4]); + assert_eq!(Sassafras::slot_ticket_id(slot + 9).unwrap(), expected_ids[2]); + assert_eq!(Sassafras::slot_ticket_id(slot + 10).unwrap(), expected_ids[0]); + assert!(Sassafras::slot_ticket_id(slot + 11).is_none()); + + // Enact epoch change by progressing one more block + + progress_to_block(epoch_length + 1, &pairs[0]).unwrap(); + + let meta = TicketsMeta::::get(); + assert_eq!(meta.unsorted_tickets_count, 0); + assert_eq!(meta.tickets_count, [0, 10]); + + // Check if we can fetch current epoch tickets ids (outside-in). + let slot = Sassafras::current_slot(); + assert_eq!(Sassafras::slot_ticket_id(slot).unwrap(), expected_ids[1]); + assert_eq!(Sassafras::slot_ticket_id(slot + 1).unwrap(), expected_ids[3]); + assert_eq!(Sassafras::slot_ticket_id(slot + 2).unwrap(), expected_ids[5]); + assert_eq!(Sassafras::slot_ticket_id(slot + 3).unwrap(), expected_ids[7]); + assert_eq!(Sassafras::slot_ticket_id(slot + 6).unwrap(), expected_ids[6]); + assert_eq!(Sassafras::slot_ticket_id(slot + 7).unwrap(), expected_ids[4]); + assert_eq!(Sassafras::slot_ticket_id(slot + 8).unwrap(), expected_ids[2]); + assert_eq!(Sassafras::slot_ticket_id(slot + 9).unwrap(), expected_ids[0]); + assert!(Sassafras::slot_ticket_id(slot + 10).is_none()); + + // Enact another epoch change, for which we don't have any ticket + progress_to_block(2 * epoch_length + 1, &pairs[0]).unwrap(); + let meta = TicketsMeta::::get(); + assert_eq!(meta.unsorted_tickets_count, 0); + assert_eq!(meta.tickets_count, [0, 0]); + }); +} + +#[test] +fn block_allowed_to_skip_epochs() { + let (pairs, mut ext) = new_test_ext_with_pairs(4, false); + let pair = &pairs[0]; + let start_slot = Slot::from(100); + let start_block = 1; + + ext.execute_with(|| { + let epoch_length = Sassafras::epoch_length() as u64; + + initialize_block(start_block, start_slot, Default::default(), pair); + + let tickets = make_ticket_bodies(3, Some(pair)); + persist_next_epoch_tickets(&tickets); + + let next_random = Sassafras::next_randomness(); + + // We want to skip 3 epochs in this test. + let offset = 4 * epoch_length; + go_to_block(start_block + offset, start_slot + offset, &pairs[0]); + + // Post-initialization status + + assert!(ClaimTemporaryData::::exists()); + assert_eq!(Sassafras::genesis_slot(), start_slot); + assert_eq!(Sassafras::current_slot(), start_slot + offset); + assert_eq!(Sassafras::epoch_index(), 4); + assert_eq!(Sassafras::current_epoch_start(), start_slot + offset); + assert_eq!(Sassafras::current_slot_index(), 0); + + // Tickets data has been discarded + assert_eq!(TicketsMeta::::get(), TicketsMetadata::default()); + assert!(tickets.iter().all(|(id, _)| TicketsData::::get(id).is_none())); + assert_eq!(SortedCandidates::::get().len(), 0); + + // We used the last known next epoch randomness as a fallback + assert_eq!(next_random, Sassafras::randomness()); + }); +} + +#[test] +fn obsolete_tickets_are_removed_on_epoch_change() { + let (pairs, mut ext) = new_test_ext_with_pairs(4, false); + let pair = &pairs[0]; + let start_slot = Slot::from(100); + let start_block = 1; + + ext.execute_with(|| { + let epoch_length = Sassafras::epoch_length() as u64; + + initialize_block(start_block, start_slot, Default::default(), pair); + + let tickets = make_ticket_bodies(10, Some(pair)); + let mut epoch1_tickets = tickets[..4].to_vec(); + let mut epoch2_tickets = tickets[4..].to_vec(); + + // Persist some tickets for next epoch (N) + persist_next_epoch_tickets(&epoch1_tickets); + assert_eq!(TicketsMeta::::get().tickets_count, [0, 4]); + // Check next epoch tickets presence + epoch1_tickets.sort_by_key(|t| t.0); + (0..epoch1_tickets.len()).into_iter().for_each(|i| { + let id = TicketsIds::::get((1, i as u32)).unwrap(); + let body = TicketsData::::get(id).unwrap(); + assert_eq!((id, body), epoch1_tickets[i]); + }); + + // Advance one epoch to enact the tickets + go_to_block(start_block + epoch_length, start_slot + epoch_length, pair); + assert_eq!(TicketsMeta::::get().tickets_count, [0, 4]); + + // Persist some tickets for next epoch (N+1) + persist_next_epoch_tickets(&epoch2_tickets); + assert_eq!(TicketsMeta::::get().tickets_count, [6, 4]); + epoch2_tickets.sort_by_key(|t| t.0); + // Check for this epoch and next epoch tickets presence + (0..epoch1_tickets.len()).into_iter().for_each(|i| { + let id = TicketsIds::::get((1, i as u32)).unwrap(); + let body = TicketsData::::get(id).unwrap(); + assert_eq!((id, body), epoch1_tickets[i]); + }); + (0..epoch2_tickets.len()).into_iter().for_each(|i| { + let id = TicketsIds::::get((0, i as u32)).unwrap(); + let body = TicketsData::::get(id).unwrap(); + assert_eq!((id, body), epoch2_tickets[i]); + }); + + // Advance to epoch 2 and check for cleanup + + go_to_block(start_block + 2 * epoch_length, start_slot + 2 * epoch_length, pair); + assert_eq!(TicketsMeta::::get().tickets_count, [6, 0]); + + (0..epoch1_tickets.len()).into_iter().for_each(|i| { + let id = TicketsIds::::get((1, i as u32)).unwrap(); + assert!(TicketsData::::get(id).is_none()); + }); + (0..epoch2_tickets.len()).into_iter().for_each(|i| { + let id = TicketsIds::::get((0, i as u32)).unwrap(); + let body = TicketsData::::get(id).unwrap(); + assert_eq!((id, body), epoch2_tickets[i]); + }); + }) +} + +const TICKETS_FILE: &str = "src/data/25_tickets_100_auths.bin"; + +fn data_read(filename: &str) -> T { + use std::{fs::File, io::Read}; + let mut file = File::open(filename).unwrap(); + let mut buf = Vec::new(); + file.read_to_end(&mut buf).unwrap(); + T::decode(&mut &buf[..]).unwrap() +} + +fn data_write(filename: &str, data: T) { + use std::{fs::File, io::Write}; + let mut file = File::create(filename).unwrap(); + let buf = data.encode(); + file.write_all(&buf).unwrap(); +} + +// We don't want to implement anything secure here. +// Just a trivial shuffle for the tests. +fn trivial_fisher_yates_shuffle(vector: &mut Vec, random_seed: u64) { + let mut rng = random_seed as usize; + for i in (1..vector.len()).rev() { + let j = rng % (i + 1); + vector.swap(i, j); + rng = (rng.wrapping_mul(6364793005) + 1) as usize; // Some random number generation + } +} + +// For this test we use a set of pre-constructed tickets from a file. +// Creating a large set of tickets on the fly takes time, and may be annoying +// for test execution. +// +// A valid ring-context is required for this test since we are passing through the +// `submit_ticket` call which tests for ticket validity. +#[test] +fn submit_tickets_with_ring_proof_check_works() { + use sp_core::Pair as _; + // env_logger::init(); + + let (authorities, mut tickets): (Vec, Vec) = + data_read(TICKETS_FILE); + + // Also checks that duplicates are discarded + tickets.extend(tickets.clone()); + trivial_fisher_yates_shuffle(&mut tickets, 321); + + let (pairs, mut ext) = new_test_ext_with_pairs(authorities.len(), true); + let pair = &pairs[0]; + // Check if deserialized data has been generated for the correct set of authorities... + assert!(authorities.iter().zip(pairs.iter()).all(|(auth, pair)| auth == &pair.public())); + + ext.execute_with(|| { + let start_slot = Slot::from(0); + let start_block = 1; + + // Tweak the config to discard ~half of the tickets. + let mut config = EpochConfig::::get(); + config.redundancy_factor = 25; + EpochConfig::::set(config); + + initialize_block(start_block, start_slot, Default::default(), pair); + NextRandomness::::set([0; 32]); + + // Check state before tickets submission + assert_eq!( + TicketsMeta::::get(), + TicketsMetadata { unsorted_tickets_count: 0, tickets_count: [0, 0] }, + ); + + // Submit the tickets + let max_tickets_per_call = Sassafras::epoch_length() as usize; + tickets.chunks(max_tickets_per_call).for_each(|chunk| { + let chunk = BoundedVec::truncate_from(chunk.to_vec()); + Sassafras::submit_tickets(RuntimeOrigin::none(), chunk).unwrap(); + }); + + // Check state after submission + assert_eq!( + TicketsMeta::::get(), + TicketsMetadata { unsorted_tickets_count: 16, tickets_count: [0, 0] }, + ); + assert_eq!(UnsortedSegments::::get(0).len(), 16); + assert_eq!(UnsortedSegments::::get(1).len(), 0); + + finalize_block(start_block); + }) +} + +#[test] +#[ignore = "test tickets data generator"] +fn make_tickets_data() { + use super::*; + use sp_core::crypto::Pair; + + // Number of authorities who produces tickets (for the sake of this test) + let tickets_authors_count = 5; + // Total number of authorities (the ring) + let authorities_count = 100; + let (pairs, mut ext) = new_test_ext_with_pairs(authorities_count, true); + + let authorities: Vec<_> = pairs.iter().map(|sk| sk.public()).collect(); + + ext.execute_with(|| { + let config = EpochConfig::::get(); + + let tickets_count = tickets_authors_count * config.attempts_number as usize; + let mut tickets = Vec::with_capacity(tickets_count); + + // Construct pre-built tickets with a well known `NextRandomness` value. + NextRandomness::::set([0; 32]); + + println!("Constructing {} tickets", tickets_count); + pairs.iter().take(tickets_authors_count).enumerate().for_each(|(i, pair)| { + let t = make_tickets(config.attempts_number, pair); + tickets.extend(t); + println!("{:.2}%", 100f32 * ((i + 1) as f32 / tickets_authors_count as f32)); + }); + + data_write(TICKETS_FILE, (authorities, tickets)); + }); +} diff --git a/substrate/frame/sassafras/src/weights.rs b/substrate/frame/sassafras-old/src/weights.rs similarity index 100% rename from substrate/frame/sassafras/src/weights.rs rename to substrate/frame/sassafras-old/src/weights.rs diff --git a/substrate/frame/sassafras/src/data/25_tickets_100_auths.bin b/substrate/frame/sassafras/src/data/25_tickets_100_auths.bin deleted file mode 100644 index 6e81f216455ae9dc61be31a9edef583a652721a8..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 24728 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 = WeakBoundedVec::MaxAuthorities>; @@ -103,12 +102,6 @@ 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. @@ -117,6 +110,23 @@ pub struct TicketsMetadata { pub tickets_count: [u32; 2], } +#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Encode, Decode, MaxEncodedLen, TypeInfo)] +struct TicketKey([u8; 32]); + +impl From for TicketKey { + fn from(mut value: TicketId) -> Self { + value.0.iter_mut().for_each(|b| *b ^= 0xff); + TicketKey(value.0) + } +} + +impl From for TicketId { + fn from(mut value: TicketKey) -> Self { + value.0.iter_mut().for_each(|b| *b ^= 0xff); + TicketId(value.0) + } +} + #[frame_support::pallet] pub mod pallet { use super::*; @@ -134,25 +144,31 @@ pub mod pallet { #[pallet::constant] type EpochLength: Get; + #[pallet::constant] + type SubmitMax: 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; + /// Redundancy factor + #[pallet::constant] + type RedundancyFactor: Get; - /// Weight information for all calls of this pallet. - type WeightInfo: WeightInfo; + /// Max attempts number + #[pallet::constant] + type AttemptsNumber: Get; } /// Sassafras runtime errors. #[pallet::error] pub enum Error { - /// Submitted configuration is invalid. - InvalidConfiguration, + /// Ticket identifier is too big. + TicketOverThreshold, + /// Duplicate ticket + TicketDuplicate, + /// Invalid ticket + TicketInvalid, } /// Current epoch index. @@ -183,47 +199,20 @@ pub mod pallet { #[pallet::getter(fn current_slot)] pub type CurrentSlot = StorageValue<_, Slot, ValueQuery>; - /// Current epoch randomness. + /// Randomness buffer. #[pallet::storage] #[pallet::getter(fn randomness)] - pub type CurrentRandomness = StorageValue<_, Randomness, ValueQuery>; + pub type RandomnessBuf = StorageValue<_, RandomnessBuffer, ValueQuery>; - /// Next epoch randomness. + /// Tickets accumulator. #[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>; + pub(crate) type TicketsAccumulator = CountedStorageMap<_, Identity, TicketKey, TicketBody>; /// Stored tickets metadata. #[pallet::storage] pub type TicketsMeta = StorageValue<_, TicketsMetadata, ValueQuery>; - /// Tickets identifiers map. + /// Tickets map. /// /// The map holds tickets ids for the current and next epoch. /// @@ -239,27 +228,7 @@ pub mod pallet { /// 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>; + pub type Tickets = StorageMap<_, Identity, (u8, u32), (TicketId, TicketBody)>; /// Parameters used to construct the epoch's ring verifier. /// @@ -294,7 +263,6 @@ pub mod pallet { #[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")] @@ -331,9 +299,7 @@ pub mod pallet { .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 + Weight::zero() } fn on_finalize(_: BlockNumberFor) { @@ -342,7 +308,7 @@ pub mod pallet { // 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(), + &Self::randomness()[0], CurrentSlot::::get(), EpochIndex::::get(), ); @@ -350,27 +316,19 @@ pub mod pallet { .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); + Self::deposit_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); - } + let outstanding_count = TicketsAccumulator::::count(); + if current_slot_idx >= epoch_length - epoch_length / 6 && outstanding_count != 0 { + let slots_left = epoch_length.checked_sub(current_slot_idx).unwrap_or(1); + let consume_count = outstanding_count.div_ceil(slots_left) as usize; + let next_epoch_idx = EpochIndex::::get() + 1; + let next_epoch_tag = (next_epoch_idx & 1) as u8; + Self::consume_tickets_accumulator(consume_count, next_epoch_tag); } } } @@ -381,9 +339,10 @@ pub mod pallet { /// /// 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))] + #[pallet::weight((Weight::zero(), DispatchClass::Mandatory))] pub fn submit_tickets( origin: OriginFor, + // TODO: change max length tickets: BoundedVec>, ) -> DispatchResultWithPostInfo { ensure_none(origin)?; @@ -405,26 +364,24 @@ pub mod pallet { 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, + T::RedundancyFactor::get(), epoch_length as u32, - next_config.attempts_number, + T::AttemptsNumber::get(), next_authorities.len() as u32, ); - // Get next epoch params - let randomness = NextRandomness::::get(); + // Get target epoch params + let randomness = RandomnessBuf::::get()[2]; let epoch_idx = EpochIndex::::get() + 1; - let mut valid_tickets = BoundedVec::with_bounded_capacity(tickets.len()); - + let mut candidates = Vec::new(); 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 + panic!("TODO") }; let ticket_id_input = vrf::ticket_id_input(&randomness, ticket.body.attempt_idx, epoch_idx); @@ -432,60 +389,24 @@ pub mod pallet { // 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 + debug!(target: LOG_TARGET, "Ticket over threshold ({:?} >= {:?})", ticket_id, ticket_threshold); + return Err(Error::::TicketOverThreshold.into()) } // 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 + debug!(target: LOG_TARGET, "Proof verification failure for ticket ({:?})", ticket_id); + panic!("TODO") } - 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)); - } + candidates.push((ticket_id, ticket.body)); } - if !valid_tickets.is_empty() { - Self::append_tickets(valid_tickets); - } + Self::deposit_tickets(&candidates)?; 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] @@ -626,90 +547,80 @@ impl Pallet { ); } - 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); - } + let announced_randomness = Self::update_randomness_buffer(); // 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, + let epoch_signal = NextEpochDescriptor { + randomness: announced_randomness, authorities: next_authorities.into_inner(), - config: next_config, }; - Self::deposit_next_epoch_descriptor_digest(next_epoch); + Self::deposit_next_epoch_descriptor_digest(epoch_signal); let epoch_tag = (epoch_idx & 1) as u8; + Self::consume_tickets_accumulator(usize::MAX, epoch_tag); + } - // Optionally finish sorting - if metadata.unsorted_tickets_count != 0 { - Self::sort_segments(u32::MAX, epoch_tag, &mut metadata); - metadata_dirty = true; + pub(crate) fn deposit_tickets(tickets: &[(TicketId, TicketBody)]) -> Result<(), Error> { + let prev_count = TicketsAccumulator::::count(); + for (id, body) in tickets.iter() { + TicketsAccumulator::::insert(TicketKey::from(*id), body); } - - // 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); + let count = TicketsAccumulator::::count(); + if count != prev_count + tickets.len() as u32 { + // Duplicates are not allowed + return Err(Error::TicketDuplicate) + } + let diff = count.saturating_sub(T::EpochLength::get()); + if diff > 0 { + let keys: Vec<_> = TicketsAccumulator::::iter_keys().take(diff as usize).collect(); + for key in keys { + let ticket_id = TicketId::from(key); + if tickets.binary_search_by_key(&&ticket_id, |(id, _)| id).is_ok() { + return Err(Error::TicketInvalid) } + TicketsAccumulator::::remove(key); } - *prev_epoch_tickets_count = 0; - metadata_dirty = true; } + Ok(()) + } - if metadata_dirty { - TicketsMeta::::set(metadata); + fn consume_tickets_accumulator(max_items: usize, epoch_tag: u8) { + let mut metadata = TicketsMeta::::get(); + let base = metadata.tickets_count[epoch_tag as usize]; + let mut idx = 0; + for (key, body) in TicketsAccumulator::::iter().take(max_items) { + Tickets::::insert((epoch_tag, base + idx), (TicketId::from(key), body)); + idx += 1; } + metadata.tickets_count[epoch_tag as usize] += idx; + 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 + fn update_randomness_buffer() -> Randomness { + let mut randomness = RandomnessBuf::::get(); + randomness[3] = randomness[2]; + randomness[2] = randomness[1]; + randomness[1] = randomness[0]; + let announce = randomness[0]; + RandomnessBuf::::put(randomness); + announce } // Deposit per-slot randomness. - fn deposit_slot_randomness(randomness: &Randomness) { - let accumulator = RandomnessAccumulator::::get(); - + fn deposit_randomness(randomness: Randomness) { + let mut accumulator = RandomnessBuf::::get(); let mut buf = [0; 2 * RANDOMNESS_LENGTH]; - buf[..RANDOMNESS_LENGTH].copy_from_slice(&accumulator[..]); + buf[..RANDOMNESS_LENGTH].copy_from_slice(&accumulator[0][..]); buf[RANDOMNESS_LENGTH..].copy_from_slice(&randomness[..]); - - let accumulator = hashing::blake2_256(&buf); - RandomnessAccumulator::::put(accumulator); + accumulator[0] = hashing::blake2_256(&buf); + RandomnessBuf::::put(accumulator); } // Deposit next epoch descriptor in the block header digest. @@ -755,16 +666,15 @@ impl Pallet { 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); + let mut accumulator = RandomnessBuffer::default(); + accumulator[0] = hashing::blake2_256(buf.as_slice()); + accumulator[1] = accumulator[0]; + RandomnessBuf::::put(accumulator); // Deposit a log as this is the first block in first epoch. let next_epoch = NextEpochDescriptor { - randomness: next_randomness, + randomness: accumulator[1], authorities: Self::next_authorities().into_inner(), - config: None, }; Self::deposit_next_epoch_descriptor_digest(next_epoch); } @@ -778,11 +688,15 @@ impl Pallet { length: T::EpochLength::get(), authorities: Self::authorities().into_inner(), randomness: Self::randomness(), - config: Self::config(), + config: EpochConfiguration { + redundancy_factor: T::RedundancyFactor::get(), + attempts_number: T::AttemptsNumber::get(), + }, } } /// Next epoch information. + /// TODO @davxy: Makes sense? pub fn next_epoch() -> Epoch { let index = EpochIndex::::get() + 1; Epoch { @@ -790,8 +704,11 @@ impl Pallet { 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()), + randomness: Self::randomness(), + config: EpochConfiguration { + redundancy_factor: T::RedundancyFactor::get(), + attempts_number: T::AttemptsNumber::get(), + }, } } @@ -801,8 +718,8 @@ impl Pallet { /// 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 ]. + /// slot-index : [ 0, 1, 2, 3, .................. ,n ] + /// tickets : [ t1, tk, t2, t_{k-1} ..... ]. /// /// With slot-index computed as `epoch_start() - slot`. /// @@ -812,173 +729,57 @@ impl Pallet { /// 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`). + /// current epoch "submission period" is completed. /// /// 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. + /// specified slot-index (may happen if n > k and we are requesting for a ticket for a slot with + /// relative index i > 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 { + pub fn slot_ticket(slot: Slot) -> Option<(TicketId, TicketBody)> { if frame_system::Pallet::::block_number().is_zero() { return None } + let epoch_idx = EpochIndex::::get(); + let mut epoch_tag = (epoch_idx & 1) as u8; 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); + if TicketsAccumulator::::count() != 0 { + Self::consume_tickets_accumulator(usize::MAX, epoch_tag); } } 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 + let mut metadata = TicketsMeta::::get(); + let tickets_count = metadata.tickets_count[epoch_tag as usize]; + if tickets_count <= slot_idx { + return 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; + let get_ticket_index = |slot_index: u32| { + let mut ticket_index = slot_idx / 2; + if slot_index & 1 != 0 { + ticket_index = tickets_count - (ticket_index + 1); } - } - - if candidates.len() > max_tickets { - Self::sort_and_truncate(&mut candidates, max_tickets); - } else if require_sort { - candidates.sort_unstable(); - } + ticket_index as u32 + }; - 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); + let ticket_idx = get_ticket_index(slot_idx); + debug!( + target: LOG_TARGET, + "slot-idx {} <-> ticket-idx {}", + slot_idx, + ticket_idx + ); + Tickets::::get((epoch_tag, ticket_idx)) } /// Remove all tickets related data. @@ -989,44 +790,14 @@ impl Pallet { 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(); + // TODO: This can fail? + let _ = TicketsAccumulator::::clear(u32::MAX, None); // 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() @@ -1062,20 +833,6 @@ impl EpochChangeTrigger for EpochChangeExternalTrigger { /// 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; } diff --git a/substrate/frame/sassafras/src/mock.rs b/substrate/frame/sassafras/src/mock.rs index f145bffa3a05..ec92c885f53b 100644 --- a/substrate/frame/sassafras/src/mock.rs +++ b/substrate/frame/sassafras/src/mock.rs @@ -59,8 +59,9 @@ where impl pallet_sassafras::Config for Test { type EpochLength = ConstU32; type MaxAuthorities = ConstU32; - type EpochChangeTrigger = EpochChangeInternalTrigger; - type WeightInfo = (); + type SubmitMax = ConstU32<16>; + type RedundancyFactor = ConstU32<2>; + type AttemptsNumber = ConstU32<2>; } frame_support::construct_runtime!( @@ -77,7 +78,6 @@ frame_support::construct_runtime!( pub const TEST_EPOCH_CONFIGURATION: EpochConfiguration = EpochConfiguration { redundancy_factor: u32::MAX, attempts_number: 5 }; -/// Build and returns test storage externalities pub fn new_test_ext(authorities_len: usize) -> sp_io::TestExternalities { new_test_ext_with_pairs(authorities_len, false).1 } @@ -118,108 +118,62 @@ pub fn new_test_ext_with_pairs( (pairs, ext) } -fn make_ticket_with_prover( - attempt: u32, - pair: &AuthorityPair, - prover: &RingProver, -) -> TicketEnvelope { - log::debug!("attempt: {}", attempt); - - // Values are referring to the next epoch - let epoch = Sassafras::epoch_index() + 1; - let randomness = Sassafras::next_randomness(); - - // Make a dummy ephemeral public that hopefully is unique within one test instance. - // In the tests, the values within the erased public are just used to compare - // ticket bodies, so it is not important to be a valid key. - let mut raw: [u8; 32] = [0; 32]; - raw.copy_from_slice(&pair.public().as_slice()[0..32]); - let erased_public = EphemeralPublic::unchecked_from(raw); - let revealed_public = erased_public; - - let ticket_id_input = vrf::ticket_id_input(&randomness, attempt, epoch); - - let body = TicketBody { attempt_idx: attempt, erased_public, revealed_public }; - let sign_data = vrf::ticket_body_sign_data(&body, ticket_id_input); +fn slot_claim_vrf_signature(slot: Slot, pair: &AuthorityPair) -> VrfSignature { + let mut epoch = Sassafras::epoch_index(); + let mut randomness = Sassafras::randomness(); - let signature = pair.as_ref().ring_vrf_sign(&sign_data, &prover); + // Check if epoch is going to change on initialization. + let epoch_start = Sassafras::current_epoch_start(); + let epoch_length = EPOCH_LENGTH.into(); + if epoch_start != 0_u64 && slot >= epoch_start + epoch_length { + epoch += slot.saturating_sub(epoch_start).saturating_div(epoch_length); + } - // Ticket-id can be generated via vrf-preout. - // We don't care that much about its value here. - TicketEnvelope { body, signature } + let data = vrf::slot_claim_sign_data(&randomness[0], slot, epoch); + pair.as_ref().vrf_sign(&data) } -pub fn make_prover(pair: &AuthorityPair) -> RingProver { - let public = pair.public(); - let mut prover_idx = None; - - let ring_ctx = Sassafras::ring_context().unwrap(); - - let pks: Vec = Sassafras::authorities() - .iter() - .enumerate() - .map(|(idx, auth)| { - if public == *auth { - prover_idx = Some(idx); - } - *auth.as_ref() - }) - .collect(); - - log::debug!("Building prover. Ring size: {}", pks.len()); - let prover = ring_ctx.prover(&pks, prover_idx.unwrap()).unwrap(); - log::debug!("Done"); - - prover +/// Construct a `PreDigest` instance for the given parameters. +pub fn make_slot_claim( + authority_idx: AuthorityIndex, + slot: Slot, + pair: &AuthorityPair, +) -> SlotClaim { + let vrf_signature = slot_claim_vrf_signature(slot, pair); + SlotClaim { authority_idx, slot, vrf_signature } } -/// Construct `attempts` tickets envelopes for the next epoch. -/// -/// E.g. by passing an optional threshold -pub fn make_tickets(attempts: u32, pair: &AuthorityPair) -> Vec { - let prover = make_prover(pair); - (0..attempts) - .into_iter() - .map(|attempt| make_ticket_with_prover(attempt, pair, &prover)) - .collect() +/// Construct a `Digest` with a `SlotClaim` item. +pub fn make_digest(authority_idx: AuthorityIndex, slot: Slot, pair: &AuthorityPair) -> Digest { + let claim = make_slot_claim(authority_idx, slot, pair); + Digest { logs: vec![DigestItem::from(&claim)] } } pub fn make_ticket_body(attempt_idx: u32, pair: &AuthorityPair) -> (TicketId, TicketBody) { // Values are referring to the next epoch let epoch = Sassafras::epoch_index() + 1; - let randomness = Sassafras::next_randomness(); + let randomness = Sassafras::randomness()[1]; let ticket_id_input = vrf::ticket_id_input(&randomness, attempt_idx, epoch); let ticket_id_pre_output = pair.as_inner_ref().vrf_pre_output(&ticket_id_input); let id = vrf::make_ticket_id(&ticket_id_input, &ticket_id_pre_output); - // Make a dummy ephemeral public that hopefully is unique within one test instance. - // In the tests, the values within the erased public are just used to compare - // ticket bodies, so it is not important to be a valid key. - let mut raw: [u8; 32] = [0; 32]; - raw[..16].copy_from_slice(&pair.public().as_slice()[0..16]); - raw[16..].copy_from_slice(&id.to_le_bytes()); - let erased_public = EphemeralPublic::unchecked_from(raw); - let revealed_public = erased_public; + // Make dummy extra data. + let mut extra = [pair.public().as_slice(), &id.0[..]].concat(); + let extra = BoundedVec::truncate_from(extra); - let body = TicketBody { attempt_idx, erased_public, revealed_public }; + let body = TicketBody { attempt_idx, extra }; (id, body) } pub fn make_dummy_ticket_body(attempt_idx: u32) -> (TicketId, TicketBody) { let hash = sp_crypto_hashing::blake2_256(&attempt_idx.to_le_bytes()); - - let erased_public = EphemeralPublic::unchecked_from(hash); - let revealed_public = erased_public; - - let body = TicketBody { attempt_idx, erased_public, revealed_public }; - - let mut bytes = [0u8; 16]; - bytes.copy_from_slice(&hash[..16]); - let id = TicketId::from_le_bytes(bytes); - + let id = TicketId(hash); + let hash = sp_crypto_hashing::blake2_256(&hash); + let extra = BoundedVec::truncate_from(hash.to_vec()); + let body = TicketBody { attempt_idx, extra }; (id, body) } @@ -236,67 +190,6 @@ pub fn make_ticket_bodies( .collect() } -/// Persist the given tickets in the unsorted segments buffer. -/// -/// This function skips all the checks performed by the `submit_tickets` extrinsic and -/// directly appends the tickets to the `UnsortedSegments` structure. -pub fn persist_next_epoch_tickets_as_segments(tickets: &[(TicketId, TicketBody)]) { - let mut ids = Vec::with_capacity(tickets.len()); - tickets.iter().for_each(|(id, body)| { - TicketsData::::set(id, Some(body.clone())); - ids.push(*id); - }); - let max_chunk_size = Sassafras::epoch_length() as usize; - ids.chunks(max_chunk_size).for_each(|chunk| { - Sassafras::append_tickets(BoundedVec::truncate_from(chunk.to_vec())); - }) -} - -/// Calls the [`persist_next_epoch_tickets_as_segments`] and then proceeds to the -/// sorting of the candidates. -/// -/// Only "winning" tickets are left. -pub fn persist_next_epoch_tickets(tickets: &[(TicketId, TicketBody)]) { - persist_next_epoch_tickets_as_segments(tickets); - // Force sorting of next epoch tickets (enactment) by explicitly querying the first of them. - let next_epoch = Sassafras::next_epoch(); - assert_eq!(TicketsMeta::::get().unsorted_tickets_count, tickets.len() as u32); - Sassafras::slot_ticket(next_epoch.start).unwrap(); - assert_eq!(TicketsMeta::::get().unsorted_tickets_count, 0); -} - -fn slot_claim_vrf_signature(slot: Slot, pair: &AuthorityPair) -> VrfSignature { - let mut epoch = Sassafras::epoch_index(); - let mut randomness = Sassafras::randomness(); - - // Check if epoch is going to change on initialization. - let epoch_start = Sassafras::current_epoch_start(); - let epoch_length = EPOCH_LENGTH.into(); - if epoch_start != 0_u64 && slot >= epoch_start + epoch_length { - epoch += slot.saturating_sub(epoch_start).saturating_div(epoch_length); - randomness = crate::NextRandomness::::get(); - } - - let data = vrf::slot_claim_sign_data(&randomness, slot, epoch); - pair.as_ref().vrf_sign(&data) -} - -/// Construct a `PreDigest` instance for the given parameters. -pub fn make_slot_claim( - authority_idx: AuthorityIndex, - slot: Slot, - pair: &AuthorityPair, -) -> SlotClaim { - let vrf_signature = slot_claim_vrf_signature(slot, pair); - SlotClaim { authority_idx, slot, vrf_signature, ticket_claim: None } -} - -/// Construct a `Digest` with a `SlotClaim` item. -pub fn make_digest(authority_idx: AuthorityIndex, slot: Slot, pair: &AuthorityPair) -> Digest { - let claim = make_slot_claim(authority_idx, slot, pair); - Digest { logs: vec![DigestItem::from(&claim)] } -} - pub fn initialize_block( number: u64, slot: Slot, @@ -314,30 +207,3 @@ pub fn finalize_block(number: u64) -> Header { Sassafras::on_finalize(number); System::finalize() } - -/// Progress the pallet state up to the given block `number` and `slot`. -pub fn go_to_block(number: u64, slot: Slot, pair: &AuthorityPair) -> Digest { - Sassafras::on_finalize(System::block_number()); - let parent_hash = System::finalize().hash(); - - let digest = make_digest(0, slot, pair); - - System::reset_events(); - System::initialize(&number, &parent_hash, &digest); - Sassafras::on_initialize(number); - - digest -} - -/// Progress the pallet state up to the given block `number`. -/// Slots will grow linearly accordingly to blocks. -pub fn progress_to_block(number: u64, pair: &AuthorityPair) -> Option { - let mut slot = Sassafras::current_slot() + 1; - let mut digest = None; - for i in System::block_number() + 1..=number { - let dig = go_to_block(i, slot, pair); - digest = Some(dig); - slot = slot + 1; - } - digest -} diff --git a/substrate/frame/sassafras/src/tests.rs b/substrate/frame/sassafras/src/tests.rs index ec3425cce7bf..3c322d7a3910 100644 --- a/substrate/frame/sassafras/src/tests.rs +++ b/substrate/frame/sassafras/src/tests.rs @@ -30,11 +30,67 @@ fn b2h(bytes: [u8; N]) -> String { array_bytes::bytes2hex("", &bytes) } +// Fisher Yates shuffle. +// +// We don't want to implement anything secure here. +// Just a trivial shuffle for the tests. +fn shuffle(vector: &mut Vec, random_seed: u64) { + let mut rng = random_seed as usize; + for i in (1..vector.len()).rev() { + let j = rng % (i + 1); + vector.swap(i, j); + rng = (rng.wrapping_mul(6364793005) + 1) as usize; // Some random number generation + } +} + +fn dummy_tickets(count: usize) -> Vec<(TicketId, TicketBody)> { + (0..count) + .map(|v| { + let id = TicketId([v as u8; 32]); + let body = TicketBody { attempt_idx: v as u32, extra: Default::default() }; + (id, body) + }) + .collect() +} + #[test] fn genesis_values_assumptions_check() { new_test_ext(3).execute_with(|| { assert_eq!(Sassafras::authorities().len(), 3); - assert_eq!(Sassafras::config(), TEST_EPOCH_CONFIGURATION); + }); +} + +#[test] +fn deposit_tickets_failure() { + new_test_ext(3).execute_with(|| { + let mut tickets = dummy_tickets(15); + shuffle(&mut tickets, 123); + + let mut candidates = tickets[..5].to_vec(); + Sassafras::deposit_tickets(&candidates).unwrap(); + assert_eq!(TicketsAccumulator::::count(), 5); + + candidates.sort_unstable_by(|a, b| b.0.cmp(&a.0)); + let stored: Vec<_> = TicketsAccumulator::::iter() + .map(|(k, b)| (TicketId::from(k), b)) + .collect(); + assert_eq!(candidates, stored); + + TicketsAccumulator::::iter().for_each(|(key, body)| { + println!("{:?}, {:?}", TicketId::from(key), body); + }); + + println!("-----------------------"); + + Sassafras::deposit_tickets(&tickets[5..7]).unwrap(); + assert_eq!(TicketsAccumulator::::count(), 7); + + TicketsAccumulator::::iter().for_each(|(key, body)| { + println!("{:?}, {:?}", TicketId::from(key), body); + }); + + assert!(Sassafras::deposit_tickets(&tickets[7..]).is_err()); + println!("ENTRIES: {}", TicketsAccumulator::::count()); }); } @@ -44,67 +100,43 @@ fn post_genesis_randomness_initialization() { let pair = &pairs[0]; ext.execute_with(|| { - assert_eq!(Sassafras::randomness(), [0; 32]); - assert_eq!(Sassafras::next_randomness(), [0; 32]); - assert_eq!(Sassafras::randomness_accumulator(), [0; 32]); + let genesis_randomness = Sassafras::randomness(); + assert_eq!(genesis_randomness, RandomnessBuffer::default()); // Test the values with a zero genesis block hash + let _ = initialize_block(1, 123.into(), [0x00; 32].into(), pair); - assert_eq!(Sassafras::randomness(), [0; 32]); - println!("[DEBUG] {}", b2h(Sassafras::next_randomness())); - assert_eq!( - Sassafras::next_randomness(), - h2b("b9497550deeeb4adc134555930de61968a0558f8947041eb515b2f5fa68ffaf7") - ); - println!("[DEBUG] {}", b2h(Sassafras::randomness_accumulator())); + let randomness = Sassafras::randomness(); + assert_eq!(randomness[0], randomness[1]); + println!("[RAND] {}", b2h(randomness[0])); assert_eq!( - Sassafras::randomness_accumulator(), + randomness[0], h2b("febcc7fe9539fe17ed29f525831394edfb30b301755dc9bd91584a1f065faf87") ); - let (id1, _) = make_ticket_bodies(1, Some(pair))[0]; + assert_eq!(randomness[2], randomness[3]); + assert_eq!(randomness[2], Randomness::default()); + + let (id1, _) = make_ticket_body(0, pair); // Reset what is relevant - NextRandomness::::set([0; 32]); - RandomnessAccumulator::::set([0; 32]); + RandomnessBuf::::set(genesis_randomness); // Test the values with a non-zero genesis block hash + let _ = initialize_block(1, 123.into(), [0xff; 32].into(), pair); - assert_eq!(Sassafras::randomness(), [0; 32]); - println!("[DEBUG] {}", b2h(Sassafras::next_randomness())); - assert_eq!( - Sassafras::next_randomness(), - h2b("51c1e3b3a73d2043b3cabae98ff27bdd4aad8967c21ecda7b9465afaa0e70f37") - ); - println!("[DEBUG] {}", b2h(Sassafras::randomness_accumulator())); + let randomness = Sassafras::randomness(); + assert_eq!(randomness[0], randomness[1]); + println!("[RAND] {}", b2h(randomness[0])); assert_eq!( - Sassafras::randomness_accumulator(), + randomness[0], h2b("466bf3007f2e17bffee0b3c42c90f33d654f5ff61eff28b0cc650825960abd52") ); - let (id2, _) = make_ticket_bodies(1, Some(pair))[0]; + assert_eq!(randomness[2], randomness[3]); + assert_eq!(randomness[2], Randomness::default()); - // Ticket ids should be different when next epoch randomness is different - assert_ne!(id1, id2); - - // Reset what is relevant - NextRandomness::::set([0; 32]); - RandomnessAccumulator::::set([0; 32]); - - // Test the values with a non-zero genesis block hash - let _ = initialize_block(1, 321.into(), [0x00; 32].into(), pair); - - println!("[DEBUG] {}", b2h(Sassafras::next_randomness())); - assert_eq!( - Sassafras::next_randomness(), - h2b("d85d84a54f79453000eb62e8a17b30149bd728d3232bc2787a89d51dc9a36008") - ); - println!("[DEBUG] {}", b2h(Sassafras::randomness_accumulator())); - assert_eq!( - Sassafras::randomness_accumulator(), - h2b("8a035eed02b5b8642b1515ed19752df8df156627aea45c4ef6e3efa88be9a74d") - ); - let (id2, _) = make_ticket_bodies(1, Some(pair))[0]; + let (id2, _) = make_ticket_body(0, pair); // Ticket ids should be different when next epoch randomness is different assert_ne!(id1, id2); @@ -115,123 +147,77 @@ fn post_genesis_randomness_initialization() { #[test] fn slot_ticket_id_outside_in_fetch() { let genesis_slot = Slot::from(100); - let tickets_count = 6; - + let curr_count = 8; + let next_count = 6; + let tickets_count = curr_count + next_count; + + let tickets: Vec<_> = (0..tickets_count) + .map(|i| { + (TicketId([i as u8; 32]), TicketBody { attempt_idx: 0, extra: Default::default() }) + }) + .collect(); // Current epoch tickets - let curr_tickets: Vec = (0..tickets_count).map(|i| i as TicketId).collect(); - - // Next epoch tickets - let next_tickets: Vec = - (0..tickets_count - 1).map(|i| (i + tickets_count) as TicketId).collect(); + let curr_tickets = tickets[..curr_count].to_vec(); + let next_tickets = tickets[curr_count..].to_vec(); new_test_ext(0).execute_with(|| { - // Some corner cases - TicketsIds::::insert((0, 0_u32), 1_u128); - - // Cleanup - (0..3).for_each(|i| TicketsIds::::remove((0, i as u32))); - curr_tickets .iter() .enumerate() - .for_each(|(i, id)| TicketsIds::::insert((0, i as u32), id)); + .for_each(|(i, t)| Tickets::::insert((0, i as u32), t)); next_tickets .iter() .enumerate() - .for_each(|(i, id)| TicketsIds::::insert((1, i as u32), id)); + .for_each(|(i, t)| Tickets::::insert((1, i as u32), t)); TicketsMeta::::set(TicketsMetadata { - tickets_count: [curr_tickets.len() as u32, next_tickets.len() as u32], - unsorted_tickets_count: 0, + tickets_count: [curr_count as u32, next_count as u32], }); // Before importing the first block the pallet always return `None` // This is a kind of special hardcoded case that should never happen in practice // as the first thing the pallet does is to initialize the genesis slot. - assert_eq!(Sassafras::slot_ticket_id(0.into()), None); - assert_eq!(Sassafras::slot_ticket_id(genesis_slot + 0), None); - assert_eq!(Sassafras::slot_ticket_id(genesis_slot + 1), None); - assert_eq!(Sassafras::slot_ticket_id(genesis_slot + 100), None); + assert_eq!(Sassafras::slot_ticket(0.into()), None); + assert_eq!(Sassafras::slot_ticket(genesis_slot + 0), None); + assert_eq!(Sassafras::slot_ticket(genesis_slot + 1), None); + assert_eq!(Sassafras::slot_ticket(genesis_slot + 100), None); // Initialize genesis slot.. GenesisSlot::::set(genesis_slot); frame_system::Pallet::::set_block_number(One::one()); // Try to fetch a ticket for a slot before current epoch. - assert_eq!(Sassafras::slot_ticket_id(0.into()), None); + assert_eq!(Sassafras::slot_ticket(0.into()), None); // Current epoch tickets. - assert_eq!(Sassafras::slot_ticket_id(genesis_slot + 0), Some(curr_tickets[1])); - assert_eq!(Sassafras::slot_ticket_id(genesis_slot + 1), Some(curr_tickets[3])); - assert_eq!(Sassafras::slot_ticket_id(genesis_slot + 2), Some(curr_tickets[5])); - assert_eq!(Sassafras::slot_ticket_id(genesis_slot + 3), None); - assert_eq!(Sassafras::slot_ticket_id(genesis_slot + 4), None); - assert_eq!(Sassafras::slot_ticket_id(genesis_slot + 5), None); - assert_eq!(Sassafras::slot_ticket_id(genesis_slot + 6), None); - assert_eq!(Sassafras::slot_ticket_id(genesis_slot + 7), Some(curr_tickets[4])); - assert_eq!(Sassafras::slot_ticket_id(genesis_slot + 8), Some(curr_tickets[2])); - assert_eq!(Sassafras::slot_ticket_id(genesis_slot + 9), Some(curr_tickets[0])); - - // Next epoch tickets (note that only 5 tickets are available) - assert_eq!(Sassafras::slot_ticket_id(genesis_slot + 10), Some(next_tickets[1])); - assert_eq!(Sassafras::slot_ticket_id(genesis_slot + 11), Some(next_tickets[3])); - assert_eq!(Sassafras::slot_ticket_id(genesis_slot + 12), None); - assert_eq!(Sassafras::slot_ticket_id(genesis_slot + 13), None); - assert_eq!(Sassafras::slot_ticket_id(genesis_slot + 14), None); - assert_eq!(Sassafras::slot_ticket_id(genesis_slot + 15), None); - assert_eq!(Sassafras::slot_ticket_id(genesis_slot + 16), None); - assert_eq!(Sassafras::slot_ticket_id(genesis_slot + 17), Some(next_tickets[4])); - assert_eq!(Sassafras::slot_ticket_id(genesis_slot + 18), Some(next_tickets[2])); - assert_eq!(Sassafras::slot_ticket_id(genesis_slot + 19), Some(next_tickets[0])); + assert_eq!(Sassafras::slot_ticket(genesis_slot + 0).unwrap(), curr_tickets[0]); + assert_eq!(Sassafras::slot_ticket(genesis_slot + 1).unwrap(), curr_tickets[7]); + assert_eq!(Sassafras::slot_ticket(genesis_slot + 2).unwrap(), curr_tickets[1]); + assert_eq!(Sassafras::slot_ticket(genesis_slot + 3).unwrap(), curr_tickets[6]); + assert_eq!(Sassafras::slot_ticket(genesis_slot + 4).unwrap(), curr_tickets[2]); + assert_eq!(Sassafras::slot_ticket(genesis_slot + 5).unwrap(), curr_tickets[5]); + assert_eq!(Sassafras::slot_ticket(genesis_slot + 6).unwrap(), curr_tickets[3]); + assert_eq!(Sassafras::slot_ticket(genesis_slot + 7).unwrap(), curr_tickets[4]); + assert!(Sassafras::slot_ticket(genesis_slot + 8).is_none()); + assert!(Sassafras::slot_ticket(genesis_slot + 9).is_none()); + + // Next epoch tickets. + assert_eq!(Sassafras::slot_ticket(genesis_slot + 10).unwrap(), next_tickets[0]); + assert_eq!(Sassafras::slot_ticket(genesis_slot + 11).unwrap(), next_tickets[5]); + assert_eq!(Sassafras::slot_ticket(genesis_slot + 12).unwrap(), next_tickets[1]); + assert_eq!(Sassafras::slot_ticket(genesis_slot + 13).unwrap(), next_tickets[4]); + assert_eq!(Sassafras::slot_ticket(genesis_slot + 14).unwrap(), next_tickets[2]); + assert_eq!(Sassafras::slot_ticket(genesis_slot + 15).unwrap(), next_tickets[3]); + assert!(Sassafras::slot_ticket(genesis_slot + 16).is_none()); + assert!(Sassafras::slot_ticket(genesis_slot + 17).is_none()); + assert!(Sassafras::slot_ticket(genesis_slot + 18).is_none()); + assert!(Sassafras::slot_ticket(genesis_slot + 19).is_none()); // Try to fetch the tickets for slots beyond the next epoch. - assert_eq!(Sassafras::slot_ticket_id(genesis_slot + 20), None); - assert_eq!(Sassafras::slot_ticket_id(genesis_slot + 42), None); - }); -} - -// Different test for outside-in test with more focus on corner case correctness. -#[test] -fn slot_ticket_id_outside_in_fetch_corner_cases() { - new_test_ext(0).execute_with(|| { - frame_system::Pallet::::set_block_number(One::one()); - - let mut meta = TicketsMetadata { tickets_count: [0, 0], unsorted_tickets_count: 0 }; - let curr_epoch_idx = EpochIndex::::get(); - - let mut epoch_test = |epoch_idx| { - let tag = (epoch_idx & 1) as u8; - let epoch_start = Sassafras::epoch_start(epoch_idx); - - // cleanup - meta.tickets_count = [0, 0]; - TicketsMeta::::set(meta); - assert!((0..10).all(|i| Sassafras::slot_ticket_id((epoch_start + i).into()).is_none())); - - meta.tickets_count[tag as usize] += 1; - TicketsMeta::::set(meta); - TicketsIds::::insert((tag, 0_u32), 1_u128); - assert_eq!(Sassafras::slot_ticket_id((epoch_start + 9).into()), Some(1_u128)); - assert!((0..9).all(|i| Sassafras::slot_ticket_id((epoch_start + i).into()).is_none())); - - meta.tickets_count[tag as usize] += 1; - TicketsMeta::::set(meta); - TicketsIds::::insert((tag, 1_u32), 2_u128); - assert_eq!(Sassafras::slot_ticket_id((epoch_start + 0).into()), Some(2_u128)); - assert!((1..9).all(|i| Sassafras::slot_ticket_id((epoch_start + i).into()).is_none())); - - meta.tickets_count[tag as usize] += 2; - TicketsMeta::::set(meta); - TicketsIds::::insert((tag, 2_u32), 3_u128); - assert_eq!(Sassafras::slot_ticket_id((epoch_start + 8).into()), Some(3_u128)); - assert!((1..8).all(|i| Sassafras::slot_ticket_id((epoch_start + i).into()).is_none())); - }; - - // Even epoch - epoch_test(curr_epoch_idx); - epoch_test(curr_epoch_idx + 1); + assert!(Sassafras::slot_ticket(genesis_slot + 20).is_none()); + assert!(Sassafras::slot_ticket(genesis_slot + 42).is_none()); }); } @@ -243,632 +229,59 @@ fn on_first_block_after_genesis() { let start_slot = Slot::from(100); let start_block = 1; - let digest = initialize_block(start_block, start_slot, Default::default(), &pairs[0]); - let common_assertions = || { assert_eq!(Sassafras::genesis_slot(), start_slot); assert_eq!(Sassafras::current_slot(), start_slot); assert_eq!(Sassafras::epoch_index(), 0); assert_eq!(Sassafras::current_epoch_start(), start_slot); assert_eq!(Sassafras::current_slot_index(), 0); - assert_eq!(Sassafras::randomness(), [0; 32]); - println!("[DEBUG] {}", b2h(Sassafras::next_randomness())); - assert_eq!( - Sassafras::next_randomness(), - h2b("a49592ef190b96f3eb87bde4c8355e33df28c75006156e8c81998158de2ed49e") - ); }; // Post-initialization status - assert!(ClaimTemporaryData::::exists()); - common_assertions(); - println!("[DEBUG] {}", b2h(Sassafras::randomness_accumulator())); - assert_eq!( - Sassafras::randomness_accumulator(), - h2b("f0d42f6b7c0d157ecbd788be44847b80a96c290c04b5dfa5d1d40c98aa0c04ed") - ); - - let header = finalize_block(start_block); - - // Post-finalization status - - assert!(!ClaimTemporaryData::::exists()); - common_assertions(); - println!("[DEBUG] {}", b2h(Sassafras::randomness_accumulator())); - assert_eq!( - Sassafras::randomness_accumulator(), - h2b("9f2b9fd19a772c34d437dcd8b84a927e73a5cb43d3d1cd00093223d60d2b4843"), - ); - - // Header data check - - assert_eq!(header.digest.logs.len(), 2); - assert_eq!(header.digest.logs[0], digest.logs[0]); + assert_eq!(Sassafras::randomness(), RandomnessBuffer::default()); - // Genesis epoch start deposits consensus - let consensus_log = sp_consensus_sassafras::digests::ConsensusLog::NextEpochData( - sp_consensus_sassafras::digests::NextEpochDescriptor { - authorities: Sassafras::next_authorities().into_inner(), - randomness: Sassafras::next_randomness(), - config: None, - }, - ); - let consensus_digest = DigestItem::Consensus(SASSAFRAS_ENGINE_ID, consensus_log.encode()); - assert_eq!(header.digest.logs[1], consensus_digest) - }) -} - -#[test] -fn on_normal_block() { - let (pairs, mut ext) = new_test_ext_with_pairs(4, false); - let start_slot = Slot::from(100); - let start_block = 1; - let end_block = start_block + 1; - - ext.execute_with(|| { - initialize_block(start_block, start_slot, Default::default(), &pairs[0]); - - // We don't want to trigger an epoch change in this test. - let epoch_length = Sassafras::epoch_length() as u64; - assert!(epoch_length > end_block); - - // Progress to block 2 - let digest = progress_to_block(end_block, &pairs[0]).unwrap(); - - let common_assertions = || { - assert_eq!(Sassafras::genesis_slot(), start_slot); - assert_eq!(Sassafras::current_slot(), start_slot + 1); - assert_eq!(Sassafras::epoch_index(), 0); - assert_eq!(Sassafras::current_epoch_start(), start_slot); - assert_eq!(Sassafras::current_slot_index(), 1); - assert_eq!(Sassafras::randomness(), [0; 32]); - println!("[DEBUG] {}", b2h(Sassafras::next_randomness())); - assert_eq!( - Sassafras::next_randomness(), - h2b("a49592ef190b96f3eb87bde4c8355e33df28c75006156e8c81998158de2ed49e") - ); - }; - - // Post-initialization status + let digest = initialize_block(start_block, start_slot, Default::default(), &pairs[0]); assert!(ClaimTemporaryData::::exists()); common_assertions(); - println!("[DEBUG] {}", b2h(Sassafras::randomness_accumulator())); + let post_ini_randomness = Sassafras::randomness(); + println!("[DEBUG] {}", b2h(post_ini_randomness[0])); + assert_eq!(post_ini_randomness[0], post_ini_randomness[1]); assert_eq!( - Sassafras::randomness_accumulator(), - h2b("9f2b9fd19a772c34d437dcd8b84a927e73a5cb43d3d1cd00093223d60d2b4843"), + post_ini_randomness[0], + h2b("f0d42f6b7c0d157ecbd788be44847b80a96c290c04b5dfa5d1d40c98aa0c04ed") ); - - let header = finalize_block(end_block); + assert_eq!(post_ini_randomness[2], post_ini_randomness[3]); + assert_eq!(post_ini_randomness[2], Randomness::default()); // Post-finalization status - assert!(!ClaimTemporaryData::::exists()); - common_assertions(); - assert_eq!( - Sassafras::randomness_accumulator(), - h2b("be9261adb9686dfd3f23f8a276b7acc7f4beb3137070beb64c282ac22d84cbf0"), - ); - - // Header data check - - assert_eq!(header.digest.logs.len(), 1); - assert_eq!(header.digest.logs[0], digest.logs[0]); - }); -} - -#[test] -fn produce_epoch_change_digest_no_config() { - let (pairs, mut ext) = new_test_ext_with_pairs(4, false); - - ext.execute_with(|| { - let start_slot = Slot::from(100); - let start_block = 1; - - initialize_block(start_block, start_slot, Default::default(), &pairs[0]); - - // We want to trigger an epoch change in this test. - let epoch_length = Sassafras::epoch_length() as u64; - let end_block = start_block + epoch_length; - - let digest = progress_to_block(end_block, &pairs[0]).unwrap(); - - let common_assertions = || { - assert_eq!(Sassafras::genesis_slot(), start_slot); - assert_eq!(Sassafras::current_slot(), start_slot + epoch_length); - assert_eq!(Sassafras::epoch_index(), 1); - assert_eq!(Sassafras::current_epoch_start(), start_slot + epoch_length); - assert_eq!(Sassafras::current_slot_index(), 0); - println!("[DEBUG] {}", b2h(Sassafras::randomness())); - assert_eq!( - Sassafras::randomness(), - h2b("a49592ef190b96f3eb87bde4c8355e33df28c75006156e8c81998158de2ed49e") - ); - }; - - // Post-initialization status - - assert!(ClaimTemporaryData::::exists()); - common_assertions(); - println!("[DEBUG] {}", b2h(Sassafras::next_randomness())); - assert_eq!( - Sassafras::next_randomness(), - h2b("d3a18b857af6ecc7b52f047107e684fff0058b5722d540a296d727e37eaa55b3"), - ); - println!("[DEBUG] {}", b2h(Sassafras::randomness_accumulator())); - assert_eq!( - Sassafras::randomness_accumulator(), - h2b("bf0f1228f4ff953c8c1bda2cceb668bf86ea05d7ae93e26d021c9690995d5279"), - ); - - let header = finalize_block(end_block); - - // Post-finalization status + let header = finalize_block(start_block); assert!(!ClaimTemporaryData::::exists()); common_assertions(); - println!("[DEBUG] {}", b2h(Sassafras::next_randomness())); - assert_eq!( - Sassafras::next_randomness(), - h2b("d3a18b857af6ecc7b52f047107e684fff0058b5722d540a296d727e37eaa55b3"), - ); - println!("[DEBUG] {}", b2h(Sassafras::randomness_accumulator())); + let post_fin_randomness = Sassafras::randomness(); + println!("[DEBUG] {}", b2h(post_fin_randomness[0])); + assert_ne!(post_fin_randomness[0], post_fin_randomness[1]); assert_eq!( - Sassafras::randomness_accumulator(), - h2b("8a1ceb346036c386d021264b10912c8b656799668004c4a487222462b394cd89"), + post_fin_randomness[0], + h2b("30361b634c74109911e59b5b773cb428ff17e13ff8ab52d4f56636c39575a9d2"), ); // Header data check assert_eq!(header.digest.logs.len(), 2); assert_eq!(header.digest.logs[0], digest.logs[0]); - // Deposits consensus log on epoch change - let consensus_log = sp_consensus_sassafras::digests::ConsensusLog::NextEpochData( - sp_consensus_sassafras::digests::NextEpochDescriptor { - authorities: Sassafras::next_authorities().into_inner(), - randomness: Sassafras::next_randomness(), - config: None, - }, - ); - let consensus_digest = DigestItem::Consensus(SASSAFRAS_ENGINE_ID, consensus_log.encode()); - assert_eq!(header.digest.logs[1], consensus_digest) - }) -} -#[test] -fn produce_epoch_change_digest_with_config() { - let (pairs, mut ext) = new_test_ext_with_pairs(4, false); - - ext.execute_with(|| { - let start_slot = Slot::from(100); - let start_block = 1; - - initialize_block(start_block, start_slot, Default::default(), &pairs[0]); - - let config = EpochConfiguration { redundancy_factor: 1, attempts_number: 123 }; - Sassafras::plan_config_change(RuntimeOrigin::root(), config).unwrap(); - - // We want to trigger an epoch change in this test. - let epoch_length = Sassafras::epoch_length() as u64; - let end_block = start_block + epoch_length; - - let digest = progress_to_block(end_block, &pairs[0]).unwrap(); - - let header = finalize_block(end_block); - - // Header data check. - // Skip pallet status checks that were already performed by other tests. - - assert_eq!(header.digest.logs.len(), 2); - assert_eq!(header.digest.logs[0], digest.logs[0]); - // Deposits consensus log on epoch change + // Genesis epoch start deposits consensus let consensus_log = sp_consensus_sassafras::digests::ConsensusLog::NextEpochData( sp_consensus_sassafras::digests::NextEpochDescriptor { authorities: Sassafras::next_authorities().into_inner(), - randomness: Sassafras::next_randomness(), - config: Some(config), + randomness: Sassafras::randomness()[1], }, ); let consensus_digest = DigestItem::Consensus(SASSAFRAS_ENGINE_ID, consensus_log.encode()); assert_eq!(header.digest.logs[1], consensus_digest) }) } - -#[test] -fn segments_incremental_sort_works() { - let (pairs, mut ext) = new_test_ext_with_pairs(1, false); - let pair = &pairs[0]; - let segments_count = 14; - let start_slot = Slot::from(100); - let start_block = 1; - - ext.execute_with(|| { - let epoch_length = Sassafras::epoch_length() as u64; - // -3 just to have the last segment not full... - let submitted_tickets_count = segments_count * SEGMENT_MAX_SIZE - 3; - - initialize_block(start_block, start_slot, Default::default(), pair); - - // Manually populate the segments to skip the threshold check - let mut tickets = make_ticket_bodies(submitted_tickets_count, None); - persist_next_epoch_tickets_as_segments(&tickets); - - // Proceed to half of the epoch (sortition should not have been started yet) - let half_epoch_block = start_block + epoch_length / 2; - progress_to_block(half_epoch_block, pair); - - let mut unsorted_tickets_count = submitted_tickets_count; - - // Check that next epoch tickets sortition is not started yet - let meta = TicketsMeta::::get(); - assert_eq!(meta.unsorted_tickets_count, unsorted_tickets_count); - assert_eq!(meta.tickets_count, [0, 0]); - - // Follow the incremental sortition block by block - - progress_to_block(half_epoch_block + 1, pair); - unsorted_tickets_count -= 3 * SEGMENT_MAX_SIZE - 3; - let meta = TicketsMeta::::get(); - assert_eq!(meta.unsorted_tickets_count, unsorted_tickets_count,); - assert_eq!(meta.tickets_count, [0, 0]); - - progress_to_block(half_epoch_block + 2, pair); - unsorted_tickets_count -= 3 * SEGMENT_MAX_SIZE; - let meta = TicketsMeta::::get(); - assert_eq!(meta.unsorted_tickets_count, unsorted_tickets_count); - assert_eq!(meta.tickets_count, [0, 0]); - - progress_to_block(half_epoch_block + 3, pair); - unsorted_tickets_count -= 3 * SEGMENT_MAX_SIZE; - let meta = TicketsMeta::::get(); - assert_eq!(meta.unsorted_tickets_count, unsorted_tickets_count); - assert_eq!(meta.tickets_count, [0, 0]); - - progress_to_block(half_epoch_block + 4, pair); - unsorted_tickets_count -= 3 * SEGMENT_MAX_SIZE; - let meta = TicketsMeta::::get(); - assert_eq!(meta.unsorted_tickets_count, unsorted_tickets_count); - assert_eq!(meta.tickets_count, [0, 0]); - - let header = finalize_block(half_epoch_block + 4); - - // Sort should be finished now. - // Check that next epoch tickets count have the correct value. - // Bigger ticket ids were discarded during sortition. - unsorted_tickets_count -= 2 * SEGMENT_MAX_SIZE; - assert_eq!(unsorted_tickets_count, 0); - let meta = TicketsMeta::::get(); - assert_eq!(meta.unsorted_tickets_count, unsorted_tickets_count); - assert_eq!(meta.tickets_count, [0, epoch_length as u32]); - // Epoch change log should have been pushed as well - assert_eq!(header.digest.logs.len(), 1); - // No tickets for the current epoch - assert_eq!(TicketsIds::::get((0, 0)), None); - - // Check persistence of "winning" tickets - tickets.sort_by_key(|t| t.0); - (0..epoch_length as usize).into_iter().for_each(|i| { - let id = TicketsIds::::get((1, i as u32)).unwrap(); - let body = TicketsData::::get(id).unwrap(); - assert_eq!((id, body), tickets[i]); - }); - // Check removal of "loosing" tickets - (epoch_length as usize..tickets.len()).into_iter().for_each(|i| { - assert!(TicketsIds::::get((1, i as u32)).is_none()); - assert!(TicketsData::::get(tickets[i].0).is_none()); - }); - - // The next block will be the first produced on the new epoch. - // At this point the tickets are found already sorted and ready to be used. - let slot = Sassafras::current_slot() + 1; - let number = System::block_number() + 1; - initialize_block(number, slot, header.hash(), pair); - let header = finalize_block(number); - // Epoch changes digest is also produced - assert_eq!(header.digest.logs.len(), 2); - }); -} - -#[test] -fn tickets_fetch_works_after_epoch_change() { - let (pairs, mut ext) = new_test_ext_with_pairs(4, false); - let pair = &pairs[0]; - let start_slot = Slot::from(100); - let start_block = 1; - let submitted_tickets = 300; - - ext.execute_with(|| { - initialize_block(start_block, start_slot, Default::default(), pair); - - // We don't want to trigger an epoch change in this test. - let epoch_length = Sassafras::epoch_length() as u64; - assert!(epoch_length > 2); - progress_to_block(2, &pairs[0]).unwrap(); - - // Persist tickets as three different segments. - let tickets = make_ticket_bodies(submitted_tickets, None); - persist_next_epoch_tickets_as_segments(&tickets); - - let meta = TicketsMeta::::get(); - assert_eq!(meta.unsorted_tickets_count, submitted_tickets); - assert_eq!(meta.tickets_count, [0, 0]); - - // Progress up to the last epoch slot (do not enact epoch change) - progress_to_block(epoch_length, &pairs[0]).unwrap(); - - // At this point next epoch tickets should have been sorted and ready to be used - let meta = TicketsMeta::::get(); - assert_eq!(meta.unsorted_tickets_count, 0); - assert_eq!(meta.tickets_count, [0, epoch_length as u32]); - - // Compute and sort the tickets ids (aka tickets scores) - let mut expected_ids: Vec<_> = tickets.into_iter().map(|(id, _)| id).collect(); - expected_ids.sort(); - expected_ids.truncate(epoch_length as usize); - - // Check if we can fetch next epoch tickets ids (outside-in). - let slot = Sassafras::current_slot(); - assert_eq!(Sassafras::slot_ticket_id(slot + 1).unwrap(), expected_ids[1]); - assert_eq!(Sassafras::slot_ticket_id(slot + 2).unwrap(), expected_ids[3]); - assert_eq!(Sassafras::slot_ticket_id(slot + 3).unwrap(), expected_ids[5]); - assert_eq!(Sassafras::slot_ticket_id(slot + 4).unwrap(), expected_ids[7]); - assert_eq!(Sassafras::slot_ticket_id(slot + 7).unwrap(), expected_ids[6]); - assert_eq!(Sassafras::slot_ticket_id(slot + 8).unwrap(), expected_ids[4]); - assert_eq!(Sassafras::slot_ticket_id(slot + 9).unwrap(), expected_ids[2]); - assert_eq!(Sassafras::slot_ticket_id(slot + 10).unwrap(), expected_ids[0]); - assert!(Sassafras::slot_ticket_id(slot + 11).is_none()); - - // Enact epoch change by progressing one more block - - progress_to_block(epoch_length + 1, &pairs[0]).unwrap(); - - let meta = TicketsMeta::::get(); - assert_eq!(meta.unsorted_tickets_count, 0); - assert_eq!(meta.tickets_count, [0, 10]); - - // Check if we can fetch current epoch tickets ids (outside-in). - let slot = Sassafras::current_slot(); - assert_eq!(Sassafras::slot_ticket_id(slot).unwrap(), expected_ids[1]); - assert_eq!(Sassafras::slot_ticket_id(slot + 1).unwrap(), expected_ids[3]); - assert_eq!(Sassafras::slot_ticket_id(slot + 2).unwrap(), expected_ids[5]); - assert_eq!(Sassafras::slot_ticket_id(slot + 3).unwrap(), expected_ids[7]); - assert_eq!(Sassafras::slot_ticket_id(slot + 6).unwrap(), expected_ids[6]); - assert_eq!(Sassafras::slot_ticket_id(slot + 7).unwrap(), expected_ids[4]); - assert_eq!(Sassafras::slot_ticket_id(slot + 8).unwrap(), expected_ids[2]); - assert_eq!(Sassafras::slot_ticket_id(slot + 9).unwrap(), expected_ids[0]); - assert!(Sassafras::slot_ticket_id(slot + 10).is_none()); - - // Enact another epoch change, for which we don't have any ticket - progress_to_block(2 * epoch_length + 1, &pairs[0]).unwrap(); - let meta = TicketsMeta::::get(); - assert_eq!(meta.unsorted_tickets_count, 0); - assert_eq!(meta.tickets_count, [0, 0]); - }); -} - -#[test] -fn block_allowed_to_skip_epochs() { - let (pairs, mut ext) = new_test_ext_with_pairs(4, false); - let pair = &pairs[0]; - let start_slot = Slot::from(100); - let start_block = 1; - - ext.execute_with(|| { - let epoch_length = Sassafras::epoch_length() as u64; - - initialize_block(start_block, start_slot, Default::default(), pair); - - let tickets = make_ticket_bodies(3, Some(pair)); - persist_next_epoch_tickets(&tickets); - - let next_random = Sassafras::next_randomness(); - - // We want to skip 3 epochs in this test. - let offset = 4 * epoch_length; - go_to_block(start_block + offset, start_slot + offset, &pairs[0]); - - // Post-initialization status - - assert!(ClaimTemporaryData::::exists()); - assert_eq!(Sassafras::genesis_slot(), start_slot); - assert_eq!(Sassafras::current_slot(), start_slot + offset); - assert_eq!(Sassafras::epoch_index(), 4); - assert_eq!(Sassafras::current_epoch_start(), start_slot + offset); - assert_eq!(Sassafras::current_slot_index(), 0); - - // Tickets data has been discarded - assert_eq!(TicketsMeta::::get(), TicketsMetadata::default()); - assert!(tickets.iter().all(|(id, _)| TicketsData::::get(id).is_none())); - assert_eq!(SortedCandidates::::get().len(), 0); - - // We used the last known next epoch randomness as a fallback - assert_eq!(next_random, Sassafras::randomness()); - }); -} - -#[test] -fn obsolete_tickets_are_removed_on_epoch_change() { - let (pairs, mut ext) = new_test_ext_with_pairs(4, false); - let pair = &pairs[0]; - let start_slot = Slot::from(100); - let start_block = 1; - - ext.execute_with(|| { - let epoch_length = Sassafras::epoch_length() as u64; - - initialize_block(start_block, start_slot, Default::default(), pair); - - let tickets = make_ticket_bodies(10, Some(pair)); - let mut epoch1_tickets = tickets[..4].to_vec(); - let mut epoch2_tickets = tickets[4..].to_vec(); - - // Persist some tickets for next epoch (N) - persist_next_epoch_tickets(&epoch1_tickets); - assert_eq!(TicketsMeta::::get().tickets_count, [0, 4]); - // Check next epoch tickets presence - epoch1_tickets.sort_by_key(|t| t.0); - (0..epoch1_tickets.len()).into_iter().for_each(|i| { - let id = TicketsIds::::get((1, i as u32)).unwrap(); - let body = TicketsData::::get(id).unwrap(); - assert_eq!((id, body), epoch1_tickets[i]); - }); - - // Advance one epoch to enact the tickets - go_to_block(start_block + epoch_length, start_slot + epoch_length, pair); - assert_eq!(TicketsMeta::::get().tickets_count, [0, 4]); - - // Persist some tickets for next epoch (N+1) - persist_next_epoch_tickets(&epoch2_tickets); - assert_eq!(TicketsMeta::::get().tickets_count, [6, 4]); - epoch2_tickets.sort_by_key(|t| t.0); - // Check for this epoch and next epoch tickets presence - (0..epoch1_tickets.len()).into_iter().for_each(|i| { - let id = TicketsIds::::get((1, i as u32)).unwrap(); - let body = TicketsData::::get(id).unwrap(); - assert_eq!((id, body), epoch1_tickets[i]); - }); - (0..epoch2_tickets.len()).into_iter().for_each(|i| { - let id = TicketsIds::::get((0, i as u32)).unwrap(); - let body = TicketsData::::get(id).unwrap(); - assert_eq!((id, body), epoch2_tickets[i]); - }); - - // Advance to epoch 2 and check for cleanup - - go_to_block(start_block + 2 * epoch_length, start_slot + 2 * epoch_length, pair); - assert_eq!(TicketsMeta::::get().tickets_count, [6, 0]); - - (0..epoch1_tickets.len()).into_iter().for_each(|i| { - let id = TicketsIds::::get((1, i as u32)).unwrap(); - assert!(TicketsData::::get(id).is_none()); - }); - (0..epoch2_tickets.len()).into_iter().for_each(|i| { - let id = TicketsIds::::get((0, i as u32)).unwrap(); - let body = TicketsData::::get(id).unwrap(); - assert_eq!((id, body), epoch2_tickets[i]); - }); - }) -} - -const TICKETS_FILE: &str = "src/data/25_tickets_100_auths.bin"; - -fn data_read(filename: &str) -> T { - use std::{fs::File, io::Read}; - let mut file = File::open(filename).unwrap(); - let mut buf = Vec::new(); - file.read_to_end(&mut buf).unwrap(); - T::decode(&mut &buf[..]).unwrap() -} - -fn data_write(filename: &str, data: T) { - use std::{fs::File, io::Write}; - let mut file = File::create(filename).unwrap(); - let buf = data.encode(); - file.write_all(&buf).unwrap(); -} - -// We don't want to implement anything secure here. -// Just a trivial shuffle for the tests. -fn trivial_fisher_yates_shuffle(vector: &mut Vec, random_seed: u64) { - let mut rng = random_seed as usize; - for i in (1..vector.len()).rev() { - let j = rng % (i + 1); - vector.swap(i, j); - rng = (rng.wrapping_mul(6364793005) + 1) as usize; // Some random number generation - } -} - -// For this test we use a set of pre-constructed tickets from a file. -// Creating a large set of tickets on the fly takes time, and may be annoying -// for test execution. -// -// A valid ring-context is required for this test since we are passing through the -// `submit_ticket` call which tests for ticket validity. -#[test] -fn submit_tickets_with_ring_proof_check_works() { - use sp_core::Pair as _; - // env_logger::init(); - - let (authorities, mut tickets): (Vec, Vec) = - data_read(TICKETS_FILE); - - // Also checks that duplicates are discarded - tickets.extend(tickets.clone()); - trivial_fisher_yates_shuffle(&mut tickets, 321); - - let (pairs, mut ext) = new_test_ext_with_pairs(authorities.len(), true); - let pair = &pairs[0]; - // Check if deserialized data has been generated for the correct set of authorities... - assert!(authorities.iter().zip(pairs.iter()).all(|(auth, pair)| auth == &pair.public())); - - ext.execute_with(|| { - let start_slot = Slot::from(0); - let start_block = 1; - - // Tweak the config to discard ~half of the tickets. - let mut config = EpochConfig::::get(); - config.redundancy_factor = 25; - EpochConfig::::set(config); - - initialize_block(start_block, start_slot, Default::default(), pair); - NextRandomness::::set([0; 32]); - - // Check state before tickets submission - assert_eq!( - TicketsMeta::::get(), - TicketsMetadata { unsorted_tickets_count: 0, tickets_count: [0, 0] }, - ); - - // Submit the tickets - let max_tickets_per_call = Sassafras::epoch_length() as usize; - tickets.chunks(max_tickets_per_call).for_each(|chunk| { - let chunk = BoundedVec::truncate_from(chunk.to_vec()); - Sassafras::submit_tickets(RuntimeOrigin::none(), chunk).unwrap(); - }); - - // Check state after submission - assert_eq!( - TicketsMeta::::get(), - TicketsMetadata { unsorted_tickets_count: 16, tickets_count: [0, 0] }, - ); - assert_eq!(UnsortedSegments::::get(0).len(), 16); - assert_eq!(UnsortedSegments::::get(1).len(), 0); - - finalize_block(start_block); - }) -} - -#[test] -#[ignore = "test tickets data generator"] -fn make_tickets_data() { - use super::*; - use sp_core::crypto::Pair; - - // Number of authorities who produces tickets (for the sake of this test) - let tickets_authors_count = 5; - // Total number of authorities (the ring) - let authorities_count = 100; - let (pairs, mut ext) = new_test_ext_with_pairs(authorities_count, true); - - let authorities: Vec<_> = pairs.iter().map(|sk| sk.public()).collect(); - - ext.execute_with(|| { - let config = EpochConfig::::get(); - - let tickets_count = tickets_authors_count * config.attempts_number as usize; - let mut tickets = Vec::with_capacity(tickets_count); - - // Construct pre-built tickets with a well known `NextRandomness` value. - NextRandomness::::set([0; 32]); - - println!("Constructing {} tickets", tickets_count); - pairs.iter().take(tickets_authors_count).enumerate().for_each(|(i, pair)| { - let t = make_tickets(config.attempts_number, pair); - tickets.extend(t); - println!("{:.2}%", 100f32 * ((i + 1) as f32 / tickets_authors_count as f32)); - }); - - data_write(TICKETS_FILE, (authorities, tickets)); - }); -} diff --git a/substrate/primitives/consensus/sassafras/src/digests.rs b/substrate/primitives/consensus/sassafras/src/digests.rs index 64190a41ce1c..fee664f8f75a 100644 --- a/substrate/primitives/consensus/sassafras/src/digests.rs +++ b/substrate/primitives/consensus/sassafras/src/digests.rs @@ -18,8 +18,8 @@ //! Sassafras digests structures and helpers. use crate::{ - ticket::TicketClaim, vrf::VrfSignature, AuthorityId, AuthorityIndex, AuthoritySignature, - EpochConfiguration, Randomness, Slot, SASSAFRAS_ENGINE_ID, + vrf::VrfSignature, AuthorityId, AuthorityIndex, AuthoritySignature, Randomness, Slot, + SASSAFRAS_ENGINE_ID, }; use scale_codec::{Decode, Encode, MaxEncodedLen}; @@ -34,14 +34,12 @@ use sp_runtime::{DigestItem, RuntimeDebug}; /// This is mandatory for each block. #[derive(Clone, RuntimeDebug, Encode, Decode, MaxEncodedLen, TypeInfo)] pub struct SlotClaim { - /// Authority index that claimed the slot. - pub authority_idx: AuthorityIndex, /// Corresponding slot number. pub slot: Slot, + /// Authority index that claimed the slot. + pub authority_idx: AuthorityIndex, /// Slot claim VRF signature. pub vrf_signature: VrfSignature, - /// Ticket auxiliary information for claim check. - pub ticket_claim: Option, } /// Information about the next epoch. @@ -53,10 +51,6 @@ pub struct NextEpochDescriptor { pub randomness: Randomness, /// Authorities list. pub authorities: Vec, - /// Epoch configuration. - /// - /// If not present previous epoch parameters are used. - pub config: Option, } /// Runtime digest entries. diff --git a/substrate/primitives/consensus/sassafras/src/lib.rs b/substrate/primitives/consensus/sassafras/src/lib.rs index c1fea74d0452..7cf97473d06a 100644 --- a/substrate/primitives/consensus/sassafras/src/lib.rs +++ b/substrate/primitives/consensus/sassafras/src/lib.rs @@ -39,8 +39,7 @@ pub mod ticket; pub mod vrf; pub use ticket::{ - ticket_id_threshold, EphemeralPublic, EphemeralSignature, TicketBody, TicketClaim, - TicketEnvelope, TicketId, + ticket_id_threshold, EphemeralPublic, EphemeralSignature, TicketBody, TicketEnvelope, TicketId, }; mod app { @@ -115,8 +114,8 @@ pub struct Epoch { pub start: Slot, /// Number of slots in the epoch. pub length: u32, - /// Randomness value. - pub randomness: Randomness, + /// Randomness accumulator. + pub randomness: [Randomness; 4], /// Authorities list. pub authorities: Vec, /// Epoch configuration. diff --git a/substrate/primitives/consensus/sassafras/src/ticket.rs b/substrate/primitives/consensus/sassafras/src/ticket.rs index 345de99be28d..1153675bdf24 100644 --- a/substrate/primitives/consensus/sassafras/src/ticket.rs +++ b/substrate/primitives/consensus/sassafras/src/ticket.rs @@ -20,8 +20,12 @@ use crate::vrf::RingVrfSignature; use scale_codec::{Decode, Encode, MaxEncodedLen}; use scale_info::TypeInfo; +use sp_core::{bounded::BoundedVec, ConstU32}; pub use sp_core::ed25519::{Public as EphemeralPublic, Signature as EphemeralSignature}; +use sp_core::U256; + +const TICKET_EXTRA_MAX_LEN: u32 = 128; /// Ticket identifier. /// @@ -30,17 +34,36 @@ pub use sp_core::ed25519::{Public as EphemeralPublic, Signature as EphemeralSign /// Because of this, it is also used as the ticket score to compare against /// the epoch ticket's threshold to decide if the ticket is worth being considered /// for slot assignment (refer to [`ticket_id_threshold`]). -pub type TicketId = u128; +#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Encode, Decode, MaxEncodedLen, TypeInfo)] +pub struct TicketId(pub [u8; 32]); + +impl core::fmt::Debug for TicketId { + fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { + write!(f, "{}", sp_core::hexdisplay::HexDisplay::from(&self.0)) + } +} + +impl From for TicketId { + fn from(value: U256) -> Self { + let mut inner = [0; 32]; + value.to_big_endian(&mut inner); + Self(inner) + } +} + +impl From for U256 { + fn from(ticket: TicketId) -> U256 { + U256::from_big_endian(&ticket.0[..]) + } +} /// Ticket data persisted on-chain. #[derive(Debug, Clone, PartialEq, Eq, Encode, Decode, MaxEncodedLen, TypeInfo)] pub struct TicketBody { /// Attempt index. pub attempt_idx: u32, - /// Ephemeral public key which gets erased when the ticket is claimed. - pub erased_public: EphemeralPublic, - /// Ephemeral public key which gets exposed when the ticket is claimed. - pub revealed_public: EphemeralPublic, + /// User opaque data. + pub extra: BoundedVec>, } /// Ticket ring vrf signature. @@ -55,13 +78,6 @@ pub struct TicketEnvelope { pub signature: TicketSignature, } -/// Ticket claim information filled by the block author. -#[derive(Debug, Clone, PartialEq, Eq, Encode, Decode, MaxEncodedLen, TypeInfo)] -pub struct TicketClaim { - /// Signature verified via `TicketBody::erased_public`. - pub erased_signature: EphemeralSignature, -} - /// Computes a boundary for [`TicketId`] maximum allowed value for a given epoch. /// /// Only ticket identifiers below this threshold should be considered as candidates @@ -80,43 +96,55 @@ pub struct TicketClaim { /// For details about the formula and implications refer to /// [*probabilities an parameters*](https://research.web3.foundation/Polkadot/protocols/block-production/SASSAFRAS#probabilities-and-parameters) /// paragraph of the w3f introduction to the protocol. -// TODO: replace with [RFC-26](https://github.com/polkadot-fellows/RFCs/pull/26) -// "Tickets Threshold" paragraph once is merged pub fn ticket_id_threshold( redundancy: u32, slots: u32, attempts: u32, validators: u32, ) -> TicketId { - let num = redundancy as u64 * slots as u64; let den = attempts as u64 * validators as u64; - TicketId::max_value() + let num = redundancy as u64 * slots as u64; + U256::MAX .checked_div(den.into()) .unwrap_or_default() .saturating_mul(num.into()) + .into() } #[cfg(test)] mod tests { use super::*; + fn normalize_u256(bytes: [u8; 32]) -> f64 { + let max_u128 = u128::MAX as f64; + let base = max_u128 + 1.0; + let max = max_u128 * (base + 1.0); + + // Extract four u128 segments from the byte array + let h = u128::from_be_bytes(bytes[..16].try_into().unwrap()) as f64; + let l = u128::from_be_bytes(bytes[16..].try_into().unwrap()) as f64; + (h * base + l) / max + } + // This is a trivial example/check which just better explain explains the rationale // behind the threshold. // // After this reading the formula should become obvious. #[test] fn ticket_id_threshold_trivial_check() { - // For an epoch with `s` slots we want to accept a number of tickets equal to ~s·r + // For an epoch with `s` slots, with a redundancy factor `r`, we want to accept + // a number of tickets equal to ~s·r. let redundancy = 2; let slots = 1000; let attempts = 100; let validators = 500; let threshold = ticket_id_threshold(redundancy, slots, attempts, validators); - let threshold = threshold as f64 / TicketId::MAX as f64; + println!("{:?}", threshold); + let threshold = normalize_u256(threshold.0); + println!("{}", threshold); - // We expect that the total number of tickets allowed to be submitted - // is slots*redundancy + // We expect that the total number of tickets allowed to be submitted is slots*redundancy let avt = ((attempts * validators) as f64 * threshold) as u32; assert_eq!(avt, slots * redundancy); diff --git a/substrate/primitives/consensus/sassafras/src/vrf.rs b/substrate/primitives/consensus/sassafras/src/vrf.rs index 537cff52ab6f..39534eeb66c4 100644 --- a/substrate/primitives/consensus/sassafras/src/vrf.rs +++ b/substrate/primitives/consensus/sassafras/src/vrf.rs @@ -97,8 +97,8 @@ pub fn ticket_body_sign_data(ticket_body: &TicketBody, ticket_id_input: VrfInput /// Pre-output should have been obtained from the input directly using the vrf /// secret key or from the vrf signature pre-outputs. pub fn make_ticket_id(input: &VrfInput, pre_output: &VrfPreOutput) -> TicketId { - let bytes = pre_output.make_bytes::<16>(b"ticket-id", input); - u128::from_le_bytes(bytes) + let bytes = pre_output.make_bytes::<32>(b"ticket-id", input); + TicketId(bytes) } /// Make revealed key seed from a given VRF input and pre-output. From f5ce73bde3ce814920b49b42134acafa6546009f Mon Sep 17 00:00:00 2001 From: Davide Galassi Date: Mon, 6 May 2024 19:58:54 +0200 Subject: [PATCH 02/42] Rething epoch index management --- substrate/frame/sassafras/src/lib.rs | 277 ++++++++++------ substrate/frame/sassafras/src/mock.rs | 49 ++- substrate/frame/sassafras/src/tests.rs | 307 +++++++++++++----- .../primitives/consensus/sassafras/src/lib.rs | 2 - .../primitives/consensus/sassafras/src/vrf.rs | 27 +- substrate/primitives/core/src/bandersnatch.rs | 32 +- 6 files changed, 456 insertions(+), 238 deletions(-) diff --git a/substrate/frame/sassafras/src/lib.rs b/substrate/frame/sassafras/src/lib.rs index a3c28a6a45f4..ddca865b6974 100644 --- a/substrate/frame/sassafras/src/lib.rs +++ b/substrate/frame/sassafras/src/lib.rs @@ -87,8 +87,7 @@ 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; +const EPOCH_TAIL_FRACTION: u32 = 6; /// Randomness buffer type. pub type RandomnessBuffer = [Randomness; 4]; @@ -158,6 +157,16 @@ pub mod pallet { /// Max attempts number #[pallet::constant] type AttemptsNumber: 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. + // TODO: weights::WeigthInfo bound + type WeightInfo; } /// Sassafras runtime errors. @@ -171,11 +180,6 @@ pub mod pallet { TicketInvalid, } - /// 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)] @@ -186,22 +190,18 @@ pub mod pallet { #[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 block slot number. + #[pallet::storage] + pub type EpochIndex = StorageValue<_, u64, ValueQuery>; + /// Randomness buffer. #[pallet::storage] - #[pallet::getter(fn randomness)] + #[pallet::getter(fn randomness_buf)] pub type RandomnessBuf = StorageValue<_, RandomnessBuffer, ValueQuery>; /// Tickets accumulator. @@ -218,34 +218,34 @@ pub mod pallet { /// /// 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. + /// - `u32` equal to the ticket's index in an abstract sorted sequence of epoch's tickets. /// - /// Epoch X first N-th ticket has key (X mod 2, N) + /// For example, `N`-th ticket for epoch `E` has key: (E 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. + /// Note that the ticket's index `N` doesn't correspond to the offset of the associated + /// slot within the epoch. The assignment is computed 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. + /// Be aware that entries within this map are never removed, but only overwritten. + /// Last element index should be fetched from [`TicketsMeta`]. #[pallet::storage] pub type Tickets = StorageMap<_, Identity, (u8, u32), (TicketId, TicketBody)>; /// Parameters used to construct the epoch's ring verifier. /// - /// In practice: Updatable Universal Reference String and the seed. + /// In practice, this is the SNARK "Universal Reference String" (powers of tau). #[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>; + pub type RingVerifierKey = StorageValue<_, vrf::RingVerifierKey>; /// 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>; + pub(crate) type PostInitCache = StorageValue<_, vrf::VrfPreOutput>; /// Genesis configuration for Sassafras protocol. #[pallet::genesis_config] @@ -286,6 +286,7 @@ pub mod pallet { .find_map(|item| item.pre_runtime_try_to::(&SASSAFRAS_ENGINE_ID)) .expect("Valid block must have a slot claim. qed"); + println!("Processing block {}, slot {}", block_num, claim.slot); CurrentSlot::::put(claim.slot); if block_num == One::one() { @@ -297,9 +298,13 @@ pub mod pallet { .pre_outputs .get(0) .expect("Valid claim must have VRF signature; qed"); - ClaimTemporaryData::::put(randomness_pre_output); + PostInitCache::::put(randomness_pre_output); - Weight::zero() + let trigger_weight = T::EpochChangeTrigger::trigger::(block_num); + Weight::zero() + trigger_weight + + // TODO + // T::WeightInfo::on_initialize() + trigger_weight } fn on_finalize(_: BlockNumberFor) { @@ -307,12 +312,9 @@ pub mod pallet { // 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()[0], - CurrentSlot::::get(), - EpochIndex::::get(), - ); - let randomness_pre_output = ClaimTemporaryData::::take() + let randomness_input = + vrf::slot_claim_input(&Self::randomness_accumulator(), CurrentSlot::::get()); + let randomness_pre_output = PostInitCache::::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); @@ -322,13 +324,23 @@ pub mod pallet { // If so, start sorting the next epoch tickets. let epoch_length = T::EpochLength::get(); let current_slot_idx = Self::current_slot_index(); - let outstanding_count = TicketsAccumulator::::count(); - if current_slot_idx >= epoch_length - epoch_length / 6 && outstanding_count != 0 { - let slots_left = epoch_length.checked_sub(current_slot_idx).unwrap_or(1); - let consume_count = outstanding_count.div_ceil(slots_left) as usize; - let next_epoch_idx = EpochIndex::::get() + 1; - let next_epoch_tag = (next_epoch_idx & 1) as u8; - Self::consume_tickets_accumulator(consume_count, next_epoch_tag); + let mut outstanding_count = TicketsAccumulator::::count() as usize; + println!( + "A: {}, B: {}", + current_slot_idx, + epoch_length - epoch_length / EPOCH_TAIL_FRACTION + ); + if current_slot_idx >= epoch_length - epoch_length / EPOCH_TAIL_FRACTION && + outstanding_count != 0 + { + let slots_left = epoch_length.checked_sub(current_slot_idx + 1).unwrap_or(1); + println!("SLOTS LEFT: {}", slots_left); + if slots_left > 0 { + outstanding_count = outstanding_count.div_ceil(slots_left as usize); + } + + let next_epoch_tag = (Self::curr_epoch_index() & 1) as u8 ^ 1; + Self::consume_tickets_accumulator(outstanding_count, next_epoch_tag); } } } @@ -356,11 +368,13 @@ pub mod pallet { return Err("Tickets shall be submitted in the first epoch half".into()) } - let Some(verifier) = RingVerifierData::::get().map(|v| v.into()) else { + let Some(verifier) = RingVerifierKey::::get().map(|v| v.into()) else { warn!(target: LOG_TARGET, "Ring verifier key not initialized"); return Err("Ring verifier key not initialized".into()) }; + // Get next epoch params + let randomness = Self::next_randomness(); let next_authorities = Self::next_authorities(); // Compute tickets threshold @@ -371,10 +385,6 @@ pub mod pallet { next_authorities.len() as u32, ); - // Get target epoch params - let randomness = RandomnessBuf::::get()[2]; - let epoch_idx = EpochIndex::::get() + 1; - let mut candidates = Vec::new(); for ticket in tickets { debug!(target: LOG_TARGET, "Checking ring proof"); @@ -383,8 +393,7 @@ pub mod pallet { debug!(target: LOG_TARGET, "Missing ticket VRF pre-output from ring signature"); panic!("TODO") }; - let ticket_id_input = - vrf::ticket_id_input(&randomness, ticket.body.attempt_idx, epoch_idx); + let ticket_id_input = vrf::ticket_id_input(&randomness, ticket.body.attempt_idx); // Check threshold constraint let ticket_id = vrf::make_ticket_id(&ticket_id_input, &ticket_id_pre_output); @@ -462,7 +471,7 @@ impl Pallet { // 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() + block_num > One::one() && Self::current_slot_index() == 0 } /// Current slot index relative to the current epoch. @@ -472,27 +481,28 @@ impl Pallet { /// 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) + // slot.checked_sub(*Self::current_epoch_start()) + // .and_then(|v| v.try_into().ok()) + // .unwrap_or(u32::MAX) + (*slot % ::EpochLength::get() as u64) as u32 } - /// 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()) - } + // /// 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."; + // /// 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() - } + // 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"); @@ -504,11 +514,11 @@ impl Pallet { 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"); + let verifier_key = ring_ctx + .verifier_key(&pks) + .expect("Failed to build ring verifier key. This is a bug"); - RingVerifierData::::put(verifier_data); + RingVerifierKey::::put(verifier_key); } /// Enact an epoch change. @@ -522,6 +532,7 @@ impl Pallet { authorities: WeakBoundedVec, next_authorities: WeakBoundedVec, ) { + println!("ENACT EPOCH CHANGE!!!!!!!!!!!!!"); if next_authorities != authorities { Self::update_ring_verifier(&next_authorities); } @@ -531,13 +542,20 @@ impl Pallet { NextAuthorities::::put(&next_authorities); // Update epoch index - let mut epoch_idx = EpochIndex::::get() + 1; + let expected_epoch_idx = EpochIndex::::get() + 1; + let mut epoch_idx = Self::curr_epoch_index(); - let slot_idx = CurrentSlot::::get().saturating_sub(Self::epoch_start(epoch_idx)); - if slot_idx >= T::EpochLength::get() { + if epoch_idx < expected_epoch_idx { + panic!( + "Undexpected epoch value, expected: {} - found: {}, aborting", + expected_epoch_idx, epoch_idx + ); + } + + if expected_epoch_idx != epoch_idx { // 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; + let skipped_epochs = epoch_idx - expected_epoch_idx; epoch_idx += skipped_epochs; warn!( target: LOG_TARGET, @@ -549,15 +567,10 @@ impl Pallet { EpochIndex::::put(epoch_idx); - let next_epoch_idx = epoch_idx + 1; - - // Updates current epoch randomness and computes the *next* epoch randomness. - let announced_randomness = Self::update_randomness_buffer(); - // After we update the current epoch, we signal the *next* epoch change // so that nodes can track changes. let epoch_signal = NextEpochDescriptor { - randomness: announced_randomness, + randomness: Self::update_randomness_buffer(), authorities: next_authorities.into_inner(), }; Self::deposit_next_epoch_descriptor_digest(epoch_signal); @@ -608,7 +621,7 @@ impl Pallet { randomness[3] = randomness[2]; randomness[2] = randomness[1]; randomness[1] = randomness[0]; - let announce = randomness[0]; + let announce = randomness[2]; RandomnessBuf::::put(randomness); announce } @@ -657,7 +670,7 @@ impl Pallet { // 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); + EpochIndex::::put(Self::epoch_index(slot)); // Properly initialize randomness using genesis hash and current slot. // This is important to guarantee that a different set of tickets are produced for: @@ -668,12 +681,14 @@ impl Pallet { buf.extend_from_slice(&slot.to_le_bytes()); let mut accumulator = RandomnessBuffer::default(); accumulator[0] = hashing::blake2_256(buf.as_slice()); - accumulator[1] = accumulator[0]; + accumulator[1] = hashing::blake2_256(&accumulator[0]); + accumulator[2] = hashing::blake2_256(&accumulator[1]); + accumulator[3] = hashing::blake2_256(&accumulator[2]); RandomnessBuf::::put(accumulator); // Deposit a log as this is the first block in first epoch. let next_epoch = NextEpochDescriptor { - randomness: accumulator[1], + randomness: accumulator[2], authorities: Self::next_authorities().into_inner(), }; Self::deposit_next_epoch_descriptor_digest(next_epoch); @@ -681,30 +696,13 @@ impl Pallet { /// Current epoch information. pub fn current_epoch() -> Epoch { - let index = EpochIndex::::get(); + let curr_slot = *Self::current_slot(); + let epoch_start = curr_slot - curr_slot % Self::epoch_length() as u64; Epoch { - index, - start: Self::epoch_start(index), + start: epoch_start.into(), length: T::EpochLength::get(), authorities: Self::authorities().into_inner(), - randomness: Self::randomness(), - config: EpochConfiguration { - redundancy_factor: T::RedundancyFactor::get(), - attempts_number: T::AttemptsNumber::get(), - }, - } - } - - /// Next epoch information. - /// TODO @davxy: Makes sense? - 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::randomness(), + randomness: Self::randomness_buf(), config: EpochConfiguration { redundancy_factor: T::RedundancyFactor::get(), attempts_number: T::AttemptsNumber::get(), @@ -741,8 +739,13 @@ impl Pallet { return None } - let epoch_idx = EpochIndex::::get(); - let mut epoch_tag = (epoch_idx & 1) as u8; + let curr_epoch_idx = EpochIndex::::get(); + let slot_epoch_idx = Self::epoch_index(slot); + if slot_epoch_idx < curr_epoch_idx || slot_epoch_idx > curr_epoch_idx + 1 { + return None + } + + let mut epoch_tag = (slot_epoch_idx & 1) as u8; let epoch_len = T::EpochLength::get(); let mut slot_idx = Self::slot_index(slot); @@ -798,6 +801,47 @@ impl Pallet { TicketsMeta::::kill(); } + /// Randomness buffer entries. + /// + /// Assuming we're executing a block during epoch with index `N`. Entry values: + /// - 0 : randomness accumulator after last block execution. + /// - 1 : randomness accumulator snapshot after execution of epoch `N-1` last block. + /// - 2 : randomness accumulator snapshot after execution of epoch `N-2` last block. + /// - 3 : randomness accumulator snapshot after execution of epoch `N-3` last block. + /// + /// At a higher level, the semantic of these entries is defined as: + /// - 3 : epoch `N` randomness + /// - 2 : epoch `N+1` randomness + /// - 1 : epoch `N+2` randomness + /// + /// If `index` is greater than 3 the `Default` is returned. + pub fn randomness(index: usize) -> Randomness { + Self::randomness_buf().get(index).cloned().unwrap_or_default() + } + + /// Current epoch's randomness. + pub fn curr_randomness() -> Randomness { + Self::randomness(3) + } + + /// Next epoch's randomness. + pub fn next_randomness() -> Randomness { + Self::randomness(2) + } + + /// Randomness accumulator + pub fn randomness_accumulator() -> Randomness { + Self::randomness(0) + } + + pub fn curr_epoch_index() -> u64 { + Self::epoch_index(Self::current_slot()) + } + + pub fn epoch_index(slot: Slot) -> u64 { + *slot / ::EpochLength::get() as u64 + } + /// Epoch length pub fn epoch_length() -> u32 { T::EpochLength::get() @@ -833,6 +877,27 @@ impl EpochChangeTrigger for EpochChangeExternalTrigger { /// changing authority set. pub struct EpochChangeInternalTrigger; +impl EpochChangeTrigger for EpochChangeInternalTrigger { + fn trigger(block_num: BlockNumberFor) -> Weight { + if Pallet::::should_end_epoch(block_num) { + println!( + "CURRENT SLOT INDEX: {}, EPOCH LEN: {}", + Pallet::::current_slot_index(), + T::EpochLength::get() + ); + let authorities = Pallet::::next_authorities(); + let next_authorities = authorities.clone(); + let len = next_authorities.len() as u32; + Pallet::::enact_epoch_change(authorities, next_authorities); + // TODO + // T::WeightInfo::enact_epoch_change(len, T::EpochLength::get()) + Weight::zero() + } else { + Weight::zero() + } + } +} + impl BoundToRuntimeAppPublic for Pallet { type Public = AuthorityId; } diff --git a/substrate/frame/sassafras/src/mock.rs b/substrate/frame/sassafras/src/mock.rs index ec92c885f53b..80b96aac3503 100644 --- a/substrate/frame/sassafras/src/mock.rs +++ b/substrate/frame/sassafras/src/mock.rs @@ -62,6 +62,8 @@ impl pallet_sassafras::Config for Test { type SubmitMax = ConstU32<16>; type RedundancyFactor = ConstU32<2>; type AttemptsNumber = ConstU32<2>; + type EpochChangeTrigger = EpochChangeInternalTrigger; + type WeightInfo = (); } frame_support::construct_runtime!( @@ -119,17 +121,8 @@ pub fn new_test_ext_with_pairs( } fn slot_claim_vrf_signature(slot: Slot, pair: &AuthorityPair) -> VrfSignature { - let mut epoch = Sassafras::epoch_index(); - let mut randomness = Sassafras::randomness(); - - // Check if epoch is going to change on initialization. - let epoch_start = Sassafras::current_epoch_start(); - let epoch_length = EPOCH_LENGTH.into(); - if epoch_start != 0_u64 && slot >= epoch_start + epoch_length { - epoch += slot.saturating_sub(epoch_start).saturating_div(epoch_length); - } - - let data = vrf::slot_claim_sign_data(&randomness[0], slot, epoch); + let randomness = Sassafras::randomness(0); + let data = vrf::slot_claim_sign_data(&randomness, slot); pair.as_ref().vrf_sign(&data) } @@ -149,12 +142,11 @@ pub fn make_digest(authority_idx: AuthorityIndex, slot: Slot, pair: &AuthorityPa Digest { logs: vec![DigestItem::from(&claim)] } } +/// Make a ticket which is claimable during the next epoch. pub fn make_ticket_body(attempt_idx: u32, pair: &AuthorityPair) -> (TicketId, TicketBody) { - // Values are referring to the next epoch - let epoch = Sassafras::epoch_index() + 1; - let randomness = Sassafras::randomness()[1]; + let randomness = Sassafras::next_randomness(); - let ticket_id_input = vrf::ticket_id_input(&randomness, attempt_idx, epoch); + let ticket_id_input = vrf::ticket_id_input(&randomness, attempt_idx); let ticket_id_pre_output = pair.as_inner_ref().vrf_pre_output(&ticket_id_input); let id = vrf::make_ticket_id(&ticket_id_input, &ticket_id_pre_output); @@ -207,3 +199,30 @@ pub fn finalize_block(number: u64) -> Header { Sassafras::on_finalize(number); System::finalize() } + +/// Progress the pallet state up to the given block `number` and `slot`. +pub fn go_to_block(number: u64, slot: Slot, pair: &AuthorityPair) -> Digest { + Sassafras::on_finalize(System::block_number()); + let parent_hash = System::finalize().hash(); + + let digest = make_digest(0, slot, pair); + + System::reset_events(); + System::initialize(&number, &parent_hash, &digest); + Sassafras::on_initialize(number); + + digest +} + +/// Progress the pallet state up to the given block `number`. +/// Slots will grow linearly accordingly to blocks. +pub fn progress_to_block(number: u64, pair: &AuthorityPair) -> Option { + let mut slot = Sassafras::current_slot() + 1; + let mut digest = None; + for i in System::block_number() + 1..=number { + let dig = go_to_block(i, slot, pair); + digest = Some(dig); + slot = slot + 1; + } + digest +} diff --git a/substrate/frame/sassafras/src/tests.rs b/substrate/frame/sassafras/src/tests.rs index 3c322d7a3910..08912e751b12 100644 --- a/substrate/frame/sassafras/src/tests.rs +++ b/substrate/frame/sassafras/src/tests.rs @@ -22,12 +22,23 @@ use mock::*; use sp_consensus_sassafras::Slot; -fn h2b(hex: &str) -> [u8; N] { - array_bytes::hex2array_unchecked(hex) +const GENESIS_SLOT: u64 = 100; + +fn h2b(hex: &str) -> Vec { + array_bytes::hex2bytes_unchecked(hex) +} + +fn b2h(bytes: &[u8]) -> String { + array_bytes::bytes2hex("", bytes) } -fn b2h(bytes: [u8; N]) -> String { - array_bytes::bytes2hex("", &bytes) +macro_rules! prefix_eq { + ($a:expr, $b:expr) => {{ + let len = $a.len().min($b.len()); + if &$a[..len] != &$b[..len] { + panic!("left: {}, right: {}", b2h(&$a[..len]), b2h(&$b[..len])); + } + }}; } // Fisher Yates shuffle. @@ -80,8 +91,6 @@ fn deposit_tickets_failure() { println!("{:?}, {:?}", TicketId::from(key), body); }); - println!("-----------------------"); - Sassafras::deposit_tickets(&tickets[5..7]).unwrap(); assert_eq!(TicketsAccumulator::::count(), 7); @@ -98,24 +107,21 @@ fn deposit_tickets_failure() { fn post_genesis_randomness_initialization() { let (pairs, mut ext) = new_test_ext_with_pairs(1, false); let pair = &pairs[0]; + let first_slot = GENESIS_SLOT.into(); ext.execute_with(|| { - let genesis_randomness = Sassafras::randomness(); + let genesis_randomness = Sassafras::randomness_buf(); assert_eq!(genesis_randomness, RandomnessBuffer::default()); // Test the values with a zero genesis block hash - let _ = initialize_block(1, 123.into(), [0x00; 32].into(), pair); + let _ = initialize_block(1, first_slot, [0x00; 32].into(), pair); - let randomness = Sassafras::randomness(); - assert_eq!(randomness[0], randomness[1]); - println!("[RAND] {}", b2h(randomness[0])); - assert_eq!( - randomness[0], - h2b("febcc7fe9539fe17ed29f525831394edfb30b301755dc9bd91584a1f065faf87") - ); - assert_eq!(randomness[2], randomness[3]); - assert_eq!(randomness[2], Randomness::default()); + let randomness = Sassafras::randomness_buf(); + prefix_eq!(randomness[0], h2b("f0d42f6b")); + prefix_eq!(randomness[1], h2b("28702cc1")); + prefix_eq!(randomness[2], h2b("a2bd8b31")); + prefix_eq!(randomness[3], h2b("76d83666")); let (id1, _) = make_ticket_body(0, pair); @@ -124,17 +130,13 @@ fn post_genesis_randomness_initialization() { // Test the values with a non-zero genesis block hash - let _ = initialize_block(1, 123.into(), [0xff; 32].into(), pair); + let _ = initialize_block(1, first_slot, [0xff; 32].into(), pair); - let randomness = Sassafras::randomness(); - assert_eq!(randomness[0], randomness[1]); - println!("[RAND] {}", b2h(randomness[0])); - assert_eq!( - randomness[0], - h2b("466bf3007f2e17bffee0b3c42c90f33d654f5ff61eff28b0cc650825960abd52") - ); - assert_eq!(randomness[2], randomness[3]); - assert_eq!(randomness[2], Randomness::default()); + let randomness = Sassafras::randomness_buf(); + prefix_eq!(randomness[0], h2b("548534cf")); + prefix_eq!(randomness[1], h2b("5b9cb838")); + prefix_eq!(randomness[2], h2b("192a2a4b")); + prefix_eq!(randomness[3], h2b("2e152bf9")); let (id2, _) = make_ticket_body(0, pair); @@ -143,10 +145,162 @@ fn post_genesis_randomness_initialization() { }); } +#[test] +fn on_first_block() { + let (pairs, mut ext) = new_test_ext_with_pairs(4, false); + let start_slot = GENESIS_SLOT.into(); + let start_block = 1; + + ext.execute_with(|| { + let common_assertions = |initialized| { + assert_eq!(Sassafras::current_slot(), start_slot); + assert_eq!(Sassafras::current_slot_index(), 0); + assert_eq!(PostInitCache::::exists(), initialized); + }; + + // Post-initialization status + + assert_eq!(Sassafras::randomness_buf(), RandomnessBuffer::default()); + + let digest = initialize_block(start_block, start_slot, Default::default(), &pairs[0]); + + common_assertions(true); + let post_init_randomness = Sassafras::randomness_buf(); + prefix_eq!(post_init_randomness[0], h2b("f0d42f6b")); + prefix_eq!(post_init_randomness[1], h2b("28702cc1")); + prefix_eq!(post_init_randomness[2], h2b("a2bd8b31")); + prefix_eq!(post_init_randomness[3], h2b("76d83666")); + + // // Post-finalization status + + let header = finalize_block(start_block); + + common_assertions(false); + let post_fini_randomness = Sassafras::randomness_buf(); + prefix_eq!(post_fini_randomness[0], h2b("6b117a72")); + prefix_eq!(post_fini_randomness[1], post_init_randomness[1]); + prefix_eq!(post_fini_randomness[2], post_init_randomness[2]); + prefix_eq!(post_fini_randomness[3], post_init_randomness[3]); + + // Header data check + + assert_eq!(header.digest.logs.len(), 2); + assert_eq!(header.digest.logs[0], digest.logs[0]); + + // Genesis epoch start deposits consensus + let consensus_log = sp_consensus_sassafras::digests::ConsensusLog::NextEpochData( + sp_consensus_sassafras::digests::NextEpochDescriptor { + randomness: Sassafras::next_randomness(), + authorities: Sassafras::next_authorities().into_inner(), + }, + ); + let consensus_digest = DigestItem::Consensus(SASSAFRAS_ENGINE_ID, consensus_log.encode()); + assert_eq!(header.digest.logs[1], consensus_digest) + }) +} + +#[test] +fn on_normal_block() { + let (pairs, mut ext) = new_test_ext_with_pairs(4, false); + let start_slot = GENESIS_SLOT.into(); + let start_block = 1; + let end_block = start_block + 1; + + ext.execute_with(|| { + initialize_block(start_block, start_slot, Default::default(), &pairs[0]); + + // We don't want to trigger an epoch change in this test. + let epoch_length = Sassafras::epoch_length() as u64; + assert!(epoch_length > end_block); + + // Progress to block 2 + let digest = progress_to_block(end_block, &pairs[0]).unwrap(); + + let common_assertions = |initialized| { + assert_eq!(Sassafras::current_slot(), start_slot + 1); + assert_eq!(Sassafras::current_slot_index(), 1); + assert_eq!(PostInitCache::::exists(), initialized); + }; + + // Post-initialization status + + common_assertions(true); + let post_init_randomness = Sassafras::randomness_buf(); + prefix_eq!(post_init_randomness[0], h2b("6b117a72")); + prefix_eq!(post_init_randomness[1], h2b("28702cc1")); + prefix_eq!(post_init_randomness[2], h2b("a2bd8b31")); + prefix_eq!(post_init_randomness[3], h2b("76d83666")); + + let header = finalize_block(end_block); + + // Post-finalization status + + common_assertions(false); + let post_fini_randomness = Sassafras::randomness_buf(); + prefix_eq!(post_fini_randomness[0], h2b("3489b933")); + prefix_eq!(post_fini_randomness[1], h2b("28702cc1")); + prefix_eq!(post_fini_randomness[2], h2b("a2bd8b31")); + prefix_eq!(post_fini_randomness[3], h2b("76d83666")); + + // Header data check + + assert_eq!(header.digest.logs.len(), 1); + assert_eq!(header.digest.logs[0], digest.logs[0]); + }); +} + +#[test] +fn produce_epoch_change_digest() { + let (pairs, mut ext) = new_test_ext_with_pairs(4, false); + let start_slot = (GENESIS_SLOT + 1).into(); + let start_block = 1; + + ext.execute_with(|| { + initialize_block(start_block, start_slot, Default::default(), &pairs[0]); + + // We want to trigger an epoch change in this test. + let epoch_length = Sassafras::epoch_length() as u64; + let end_block = start_block + epoch_length - 1; + println!("END BLOCK: {}", end_block); + + let common_assertions = |initialized| { + assert_eq!(Sassafras::current_slot(), GENESIS_SLOT + epoch_length); + assert_eq!(Sassafras::current_slot_index(), 0); + assert_eq!(PostInitCache::::exists(), initialized); + }; + + let digest = progress_to_block(end_block, &pairs[0]).unwrap(); + + // Post-initialization status + + common_assertions(true); + + let header = finalize_block(end_block); + + // Post-finalization status + + common_assertions(false); + + // Header data check + + assert_eq!(header.digest.logs.len(), 2); + assert_eq!(header.digest.logs[0], digest.logs[0]); + // Deposits consensus log on epoch change + let consensus_log = sp_consensus_sassafras::digests::ConsensusLog::NextEpochData( + sp_consensus_sassafras::digests::NextEpochDescriptor { + authorities: Sassafras::next_authorities().into_inner(), + randomness: Sassafras::next_randomness(), + }, + ); + let consensus_digest = DigestItem::Consensus(SASSAFRAS_ENGINE_ID, consensus_log.encode()); + assert_eq!(header.digest.logs[1], consensus_digest) + }) +} + // Tests if the sorted tickets are assigned to each slot outside-in. #[test] fn slot_ticket_id_outside_in_fetch() { - let genesis_slot = Slot::from(100); + let genesis_slot = Slot::from(GENESIS_SLOT); let curr_count = 8; let next_count = 6; let tickets_count = curr_count + next_count; @@ -174,6 +328,7 @@ fn slot_ticket_id_outside_in_fetch() { TicketsMeta::::set(TicketsMetadata { tickets_count: [curr_count as u32, next_count as u32], }); + EpochIndex::::set(*genesis_slot / Sassafras::epoch_length() as u64); // Before importing the first block the pallet always return `None` // This is a kind of special hardcoded case that should never happen in practice @@ -184,8 +339,7 @@ fn slot_ticket_id_outside_in_fetch() { assert_eq!(Sassafras::slot_ticket(genesis_slot + 1), None); assert_eq!(Sassafras::slot_ticket(genesis_slot + 100), None); - // Initialize genesis slot.. - GenesisSlot::::set(genesis_slot); + // Reset block number.. frame_system::Pallet::::set_block_number(One::one()); // Try to fetch a ticket for a slot before current epoch. @@ -222,66 +376,57 @@ fn slot_ticket_id_outside_in_fetch() { } #[test] -fn on_first_block_after_genesis() { - let (pairs, mut ext) = new_test_ext_with_pairs(4, false); +fn tickets_accumulator_consume() { + let curr_count = 8; + let next_count = 6; + let accu_count = 10; - ext.execute_with(|| { - let start_slot = Slot::from(100); - let start_block = 1; + let tickets_count = curr_count + next_count + accu_count; + let start_block = 1; + let start_slot = (GENESIS_SLOT + 1).into(); - let common_assertions = || { - assert_eq!(Sassafras::genesis_slot(), start_slot); - assert_eq!(Sassafras::current_slot(), start_slot); - assert_eq!(Sassafras::epoch_index(), 0); - assert_eq!(Sassafras::current_epoch_start(), start_slot); - assert_eq!(Sassafras::current_slot_index(), 0); - }; - - // Post-initialization status + let tickets: Vec<_> = (0..tickets_count) + .map(|i| { + (TicketId([i as u8; 32]), TicketBody { attempt_idx: 0, extra: Default::default() }) + }) + .collect(); + // Current epoch tickets + let curr_tickets = tickets[..curr_count].to_vec(); + let next_tickets = tickets[curr_count..curr_count + next_count].to_vec(); + let accu_tickets = tickets[curr_count + next_count..].to_vec(); - assert_eq!(Sassafras::randomness(), RandomnessBuffer::default()); + let (pairs, mut ext) = new_test_ext_with_pairs(4, false); - let digest = initialize_block(start_block, start_slot, Default::default(), &pairs[0]); + ext.execute_with(|| { + curr_tickets + .iter() + .enumerate() + .for_each(|(i, t)| Tickets::::insert((0, i as u32), t)); - assert!(ClaimTemporaryData::::exists()); - common_assertions(); - let post_ini_randomness = Sassafras::randomness(); - println!("[DEBUG] {}", b2h(post_ini_randomness[0])); - assert_eq!(post_ini_randomness[0], post_ini_randomness[1]); - assert_eq!( - post_ini_randomness[0], - h2b("f0d42f6b7c0d157ecbd788be44847b80a96c290c04b5dfa5d1d40c98aa0c04ed") - ); - assert_eq!(post_ini_randomness[2], post_ini_randomness[3]); - assert_eq!(post_ini_randomness[2], Randomness::default()); + next_tickets + .iter() + .enumerate() + .for_each(|(i, t)| Tickets::::insert((1, i as u32), t)); - // Post-finalization status + TicketsMeta::::set(TicketsMetadata { + tickets_count: [curr_count as u32, next_count as u32], + }); - let header = finalize_block(start_block); + initialize_block(start_block, start_slot, Default::default(), &pairs[0]); - assert!(!ClaimTemporaryData::::exists()); - common_assertions(); - let post_fin_randomness = Sassafras::randomness(); - println!("[DEBUG] {}", b2h(post_fin_randomness[0])); - assert_ne!(post_fin_randomness[0], post_fin_randomness[1]); - assert_eq!( - post_fin_randomness[0], - h2b("30361b634c74109911e59b5b773cb428ff17e13ff8ab52d4f56636c39575a9d2"), - ); + accu_tickets + .iter() + .for_each(|t| TicketsAccumulator::::insert(TicketKey::from(t.0), &t.1)); - // Header data check + // We don't want to trigger an epoch change in this test. + let epoch_length = Sassafras::epoch_length(); - assert_eq!(header.digest.logs.len(), 2); - assert_eq!(header.digest.logs[0], digest.logs[0]); + // Progress to block 2 + let end_block = + start_block + (epoch_length - epoch_length / EPOCH_TAIL_FRACTION) as u64 - 1; + println!("ST: {}, EN: {}, L: {}", start_block, end_block, epoch_length); + let digest = progress_to_block(end_block, &pairs[0]).unwrap(); - // Genesis epoch start deposits consensus - let consensus_log = sp_consensus_sassafras::digests::ConsensusLog::NextEpochData( - sp_consensus_sassafras::digests::NextEpochDescriptor { - authorities: Sassafras::next_authorities().into_inner(), - randomness: Sassafras::randomness()[1], - }, - ); - let consensus_digest = DigestItem::Consensus(SASSAFRAS_ENGINE_ID, consensus_log.encode()); - assert_eq!(header.digest.logs[1], consensus_digest) - }) + let header = finalize_block(end_block); + }); } diff --git a/substrate/primitives/consensus/sassafras/src/lib.rs b/substrate/primitives/consensus/sassafras/src/lib.rs index 7cf97473d06a..dd0a30d6eb9c 100644 --- a/substrate/primitives/consensus/sassafras/src/lib.rs +++ b/substrate/primitives/consensus/sassafras/src/lib.rs @@ -108,8 +108,6 @@ pub struct EpochConfiguration { /// Sassafras epoch information #[derive(Debug, Clone, PartialEq, Eq, Encode, Decode, TypeInfo)] pub struct Epoch { - /// Epoch index. - pub index: u64, /// Starting slot of the epoch. pub start: Slot, /// Number of slots in the epoch. diff --git a/substrate/primitives/consensus/sassafras/src/vrf.rs b/substrate/primitives/consensus/sassafras/src/vrf.rs index 39534eeb66c4..b0946cb61efb 100644 --- a/substrate/primitives/consensus/sassafras/src/vrf.rs +++ b/substrate/primitives/consensus/sassafras/src/vrf.rs @@ -24,7 +24,7 @@ use scale_codec::Encode; use sp_consensus_slots::Slot; pub use sp_core::bandersnatch::{ - ring_vrf::{RingProver, RingVerifier, RingVerifierData, RingVrfSignature}, + ring_vrf::{RingProver, RingVerifier, RingVerifierKey, RingVrfSignature}, vrf::{VrfInput, VrfPreOutput, VrfSignData, VrfSignature}, }; @@ -49,16 +49,13 @@ fn vrf_input_from_data( } /// VRF input to claim slot ownership during block production. -pub fn slot_claim_input(randomness: &Randomness, slot: Slot, epoch: u64) -> VrfInput { - vrf_input_from_data( - b"sassafras-claim-v1.0", - [randomness.as_slice(), &slot.to_le_bytes(), &epoch.to_le_bytes()], - ) +pub fn slot_claim_input(randomness: &Randomness, slot: Slot) -> VrfInput { + vrf_input_from_data(b"sassafras-claim-v1.0", [randomness.as_slice(), &slot.to_le_bytes()]) } /// Signing-data to claim slot ownership during block production. -pub fn slot_claim_sign_data(randomness: &Randomness, slot: Slot, epoch: u64) -> VrfSignData { - let input = slot_claim_input(randomness, slot, epoch); +pub fn slot_claim_sign_data(randomness: &Randomness, slot: Slot) -> VrfSignData { + let input = slot_claim_input(randomness, slot); VrfSignData::new_unchecked( b"sassafras-slot-claim-transcript-v1.0", Option::<&[u8]>::None, @@ -67,19 +64,13 @@ pub fn slot_claim_sign_data(randomness: &Randomness, slot: Slot, epoch: u64) -> } /// VRF input to generate the ticket id. -pub fn ticket_id_input(randomness: &Randomness, attempt: u32, epoch: u64) -> VrfInput { - vrf_input_from_data( - b"sassafras-ticket-v1.0", - [randomness.as_slice(), &attempt.to_le_bytes(), &epoch.to_le_bytes()], - ) +pub fn ticket_id_input(randomness: &Randomness, attempt: u32) -> VrfInput { + vrf_input_from_data(b"sassafras-ticket-v1.0", [randomness.as_slice(), &attempt.to_le_bytes()]) } /// VRF input to generate the revealed key. -pub fn revealed_key_input(randomness: &Randomness, attempt: u32, epoch: u64) -> VrfInput { - vrf_input_from_data( - b"sassafras-revealed-v1.0", - [randomness.as_slice(), &attempt.to_le_bytes(), &epoch.to_le_bytes()], - ) +pub fn revealed_key_input(randomness: &Randomness, attempt: u32) -> VrfInput { + vrf_input_from_data(b"sassafras-revealed-v1.0", [randomness.as_slice(), &attempt.to_le_bytes()]) } /// Data to be signed via ring-vrf. diff --git a/substrate/primitives/core/src/bandersnatch.rs b/substrate/primitives/core/src/bandersnatch.rs index 71ee2da53834..9049fd208378 100644 --- a/substrate/primitives/core/src/bandersnatch.rs +++ b/substrate/primitives/core/src/bandersnatch.rs @@ -503,20 +503,20 @@ pub mod ring_vrf { pub(crate) const RING_SIGNATURE_SERIALIZED_SIZE: usize = 755; /// remove as soon as soon as serialization is implemented by the backend - pub struct RingVerifierData { + pub struct RingVerifierKey { /// Domain size. pub domain_size: u32, /// Verifier key. pub verifier_key: VerifierKey, } - impl From for RingVerifier { - fn from(vd: RingVerifierData) -> RingVerifier { + impl From for RingVerifier { + fn from(vd: RingVerifierKey) -> RingVerifier { bandersnatch_vrfs::ring::make_ring_verifier(vd.verifier_key, vd.domain_size as usize) } } - impl Encode for RingVerifierData { + impl Encode for RingVerifierKey { fn encode(&self) -> Vec { const ERR_STR: &str = "serialization length is constant and checked by test; qed"; let mut buf = [0; RING_VERIFIER_DATA_SERIALIZED_SIZE]; @@ -526,7 +526,7 @@ pub mod ring_vrf { } } - impl Decode for RingVerifierData { + impl Decode for RingVerifierKey { fn decode(i: &mut R) -> Result { const ERR_STR: &str = "serialization length is constant and checked by test; qed"; let buf = <[u8; RING_VERIFIER_DATA_SERIALIZED_SIZE]>::decode(i)?; @@ -535,19 +535,19 @@ pub mod ring_vrf { .expect(ERR_STR); let verifier_key = ::deserialize_compressed_unchecked(&mut &buf[4..]).expect(ERR_STR); - Ok(RingVerifierData { domain_size, verifier_key }) + Ok(RingVerifierKey { domain_size, verifier_key }) } } - impl EncodeLike for RingVerifierData {} + impl EncodeLike for RingVerifierKey {} - impl MaxEncodedLen for RingVerifierData { + impl MaxEncodedLen for RingVerifierKey { fn max_encoded_len() -> usize { <[u8; RING_VERIFIER_DATA_SERIALIZED_SIZE]>::max_encoded_len() } } - impl TypeInfo for RingVerifierData { + impl TypeInfo for RingVerifierKey { type Identity = [u8; RING_VERIFIER_DATA_SERIALIZED_SIZE]; fn type_info() -> scale_info::Type { @@ -601,13 +601,13 @@ pub mod ring_vrf { } /// Information required for a lazy construction of a ring verifier. - pub fn verifier_data(&self, public_keys: &[Public]) -> Option { + pub fn verifier_key(&self, public_keys: &[Public]) -> Option { let mut pks = Vec::with_capacity(public_keys.len()); for public_key in public_keys { let pk = PublicKey::deserialize_compressed_unchecked(public_key.as_slice()).ok()?; pks.push(pk.0.into()); } - Some(RingVerifierData { + Some(RingVerifierKey { verifier_key: self.0.verifier_key(pks), domain_size: self.0.domain_size, }) @@ -1070,19 +1070,19 @@ mod tests { } #[test] - fn encode_decode_verifier_data() { + fn encode_decode_verifier_key() { let ring_ctx = TestRingContext::new_testing(); let pks: Vec<_> = (0..16).map(|i| Pair::from_seed(&[i as u8; 32]).public()).collect(); assert!(pks.len() <= ring_ctx.max_keyset_size()); - let verifier_data = ring_ctx.verifier_data(&pks).unwrap(); - let enc1 = verifier_data.encode(); + let verifier_key = ring_ctx.verifier_key(&pks).unwrap(); + let enc1 = verifier_key.encode(); assert_eq!(enc1.len(), RING_VERIFIER_DATA_SERIALIZED_SIZE); - assert_eq!(RingVerifierData::max_encoded_len(), RING_VERIFIER_DATA_SERIALIZED_SIZE); + assert_eq!(RingVerifierKey::max_encoded_len(), RING_VERIFIER_DATA_SERIALIZED_SIZE); - let vd2 = RingVerifierData::decode(&mut enc1.as_slice()).unwrap(); + let vd2 = RingVerifierKey::decode(&mut enc1.as_slice()).unwrap(); let enc2 = vd2.encode(); assert_eq!(enc1, enc2); From fe461ba983fdf75430f5fd149ee8de09679c0118 Mon Sep 17 00:00:00 2001 From: Davide Galassi Date: Tue, 7 May 2024 08:40:49 +0200 Subject: [PATCH 03/42] Configuration separated by current epoch data --- substrate/frame/sassafras/src/lib.rs | 24 +++++++++---------- substrate/frame/sassafras/src/mock.rs | 13 ++-------- .../primitives/consensus/sassafras/src/lib.rs | 16 ++++++------- 3 files changed, 22 insertions(+), 31 deletions(-) diff --git a/substrate/frame/sassafras/src/lib.rs b/substrate/frame/sassafras/src/lib.rs index ddca865b6974..be2570f7a411 100644 --- a/substrate/frame/sassafras/src/lib.rs +++ b/substrate/frame/sassafras/src/lib.rs @@ -64,8 +64,8 @@ use frame_system::{ }; use sp_consensus_sassafras::{ digests::{ConsensusLog, NextEpochDescriptor, SlotClaim}, - vrf, AuthorityId, Epoch, EpochConfiguration, Randomness, Slot, TicketBody, TicketEnvelope, - TicketId, RANDOMNESS_LENGTH, SASSAFRAS_ENGINE_ID, + vrf, AuthorityId, Configuration, Epoch, Randomness, Slot, TicketBody, TicketEnvelope, TicketId, + RANDOMNESS_LENGTH, SASSAFRAS_ENGINE_ID, }; use sp_io::hashing; use sp_runtime::{ @@ -143,9 +143,6 @@ pub mod pallet { #[pallet::constant] type EpochLength: Get; - #[pallet::constant] - type SubmitMax: Get; - /// Max number of authorities allowed. #[pallet::constant] type MaxAuthorities: Get; @@ -253,8 +250,6 @@ pub mod pallet { 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, @@ -694,19 +689,24 @@ impl Pallet { Self::deposit_next_epoch_descriptor_digest(next_epoch); } + pub fn protocol_config() -> Configuration { + Configuration { + epoch_length: T::EpochLength::get(), + epoch_tail_length: T::EpochLength::get() / 6, + max_authorities: T::MaxAuthorities::get(), + redundancy_factor: T::RedundancyFactor::get(), + attempts_number: T::AttemptsNumber::get(), + } + } + /// Current epoch information. pub fn current_epoch() -> Epoch { let curr_slot = *Self::current_slot(); let epoch_start = curr_slot - curr_slot % Self::epoch_length() as u64; Epoch { start: epoch_start.into(), - length: T::EpochLength::get(), authorities: Self::authorities().into_inner(), randomness: Self::randomness_buf(), - config: EpochConfiguration { - redundancy_factor: T::RedundancyFactor::get(), - attempts_number: T::AttemptsNumber::get(), - }, } } diff --git a/substrate/frame/sassafras/src/mock.rs b/substrate/frame/sassafras/src/mock.rs index 80b96aac3503..4d657c97558f 100644 --- a/substrate/frame/sassafras/src/mock.rs +++ b/substrate/frame/sassafras/src/mock.rs @@ -26,7 +26,7 @@ use frame_support::{ use sp_consensus_sassafras::{ digests::SlotClaim, vrf::{RingProver, VrfSignature}, - AuthorityIndex, AuthorityPair, EpochConfiguration, Slot, TicketBody, TicketEnvelope, TicketId, + AuthorityIndex, AuthorityPair, Configuration, Slot, TicketBody, TicketEnvelope, TicketId, }; use sp_core::{ crypto::{ByteArray, Pair, UncheckedFrom, VrfSecret, Wraps}, @@ -59,8 +59,7 @@ where impl pallet_sassafras::Config for Test { type EpochLength = ConstU32; type MaxAuthorities = ConstU32; - type SubmitMax = ConstU32<16>; - type RedundancyFactor = ConstU32<2>; + type RedundancyFactor = ConstU32<32>; type AttemptsNumber = ConstU32<2>; type EpochChangeTrigger = EpochChangeInternalTrigger; type WeightInfo = (); @@ -73,13 +72,6 @@ frame_support::construct_runtime!( } ); -// Default used for most of the tests. -// -// The redundancy factor has been set to max value to accept all submitted -// tickets without worrying about the threshold. -pub const TEST_EPOCH_CONFIGURATION: EpochConfiguration = - EpochConfiguration { redundancy_factor: u32::MAX, attempts_number: 5 }; - pub fn new_test_ext(authorities_len: usize) -> sp_io::TestExternalities { new_test_ext_with_pairs(authorities_len, false).1 } @@ -100,7 +92,6 @@ pub fn new_test_ext_with_pairs( pallet_sassafras::GenesisConfig:: { authorities: authorities.clone(), - epoch_config: TEST_EPOCH_CONFIGURATION, _phantom: core::marker::PhantomData, } .assimilate_storage(&mut storage) diff --git a/substrate/primitives/consensus/sassafras/src/lib.rs b/substrate/primitives/consensus/sassafras/src/lib.rs index dd0a30d6eb9c..5ae8a79d85b5 100644 --- a/substrate/primitives/consensus/sassafras/src/lib.rs +++ b/substrate/primitives/consensus/sassafras/src/lib.rs @@ -81,14 +81,18 @@ pub type EquivocationProof = sp_consensus_slots::EquivocationProof, - /// Epoch configuration. - pub config: EpochConfiguration, } /// An opaque type used to represent the key ownership proof at the runtime API boundary. From 2d08b1a35ccf5b772e5abb6f21cb7882f223a63a Mon Sep 17 00:00:00 2001 From: Davide Galassi Date: Tue, 7 May 2024 10:52:14 +0200 Subject: [PATCH 04/42] More tests --- substrate/frame/sassafras/src/lib.rs | 83 +++++------ substrate/frame/sassafras/src/tests.rs | 195 +++++++++++++++++++++---- 2 files changed, 201 insertions(+), 77 deletions(-) diff --git a/substrate/frame/sassafras/src/lib.rs b/substrate/frame/sassafras/src/lib.rs index be2570f7a411..a27138dc3a54 100644 --- a/substrate/frame/sassafras/src/lib.rs +++ b/substrate/frame/sassafras/src/lib.rs @@ -38,30 +38,24 @@ //! 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. +//! nominated validators". #![allow(unused)] // #![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 log::{debug, warn}; use scale_codec::{Decode, Encode, MaxEncodedLen}; use scale_info::TypeInfo; use frame_support::{ dispatch::{DispatchResultWithPostInfo, Pays}, - traits::{Defensive, Get}, + traits::Get, weights::Weight, BoundedVec, WeakBoundedVec, }; -use frame_system::{ - offchain::{SendTransactionTypes, SubmitTransaction}, - pallet_prelude::BlockNumberFor, -}; +use frame_system::{offchain::SendTransactionTypes, pallet_prelude::BlockNumberFor}; use sp_consensus_sassafras::{ digests::{ConsensusLog, NextEpochDescriptor, SlotClaim}, vrf, AuthorityId, Configuration, Epoch, Randomness, Slot, TicketBody, TicketEnvelope, TicketId, @@ -320,20 +314,13 @@ pub mod pallet { let epoch_length = T::EpochLength::get(); let current_slot_idx = Self::current_slot_index(); let mut outstanding_count = TicketsAccumulator::::count() as usize; - println!( - "A: {}, B: {}", - current_slot_idx, - epoch_length - epoch_length / EPOCH_TAIL_FRACTION - ); if current_slot_idx >= epoch_length - epoch_length / EPOCH_TAIL_FRACTION && outstanding_count != 0 { let slots_left = epoch_length.checked_sub(current_slot_idx + 1).unwrap_or(1); - println!("SLOTS LEFT: {}", slots_left); if slots_left > 0 { outstanding_count = outstanding_count.div_ceil(slots_left as usize); } - let next_epoch_tag = (Self::curr_epoch_index() & 1) as u8 ^ 1; Self::consume_tickets_accumulator(outstanding_count, next_epoch_tag); } @@ -482,23 +469,6 @@ impl Pallet { (*slot % ::EpochLength::get() as u64) as u32 } - // /// 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 { @@ -572,6 +542,11 @@ impl Pallet { let epoch_tag = (epoch_idx & 1) as u8; Self::consume_tickets_accumulator(usize::MAX, epoch_tag); + + // Reset next epoch counter as we're start accumulating. + let mut metadata = TicketsMeta::::get(); + metadata.tickets_count[(epoch_tag ^ 1) as usize] = 0; + TicketsMeta::::set(metadata); } pub(crate) fn deposit_tickets(tickets: &[(TicketId, TicketBody)]) -> Result<(), Error> { @@ -602,7 +577,7 @@ impl Pallet { let mut metadata = TicketsMeta::::get(); let base = metadata.tickets_count[epoch_tag as usize]; let mut idx = 0; - for (key, body) in TicketsAccumulator::::iter().take(max_items) { + for (key, body) in TicketsAccumulator::::drain().take(max_items) { Tickets::::insert((epoch_tag, base + idx), (TicketId::from(key), body)); idx += 1; } @@ -785,34 +760,32 @@ impl Pallet { Tickets::::get((epoch_tag, ticket_idx)) } - /// Remove all tickets related data. + /// Reset 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. + /// Optimization note: tickets are left in place, only the metadata which counts + /// their number is resetted. fn reset_tickets_data() { - let metadata = TicketsMeta::::get(); - - // Reset sorted candidates - // TODO: This can fail? + // Reset sorted candidates (TODO: How this can fail?) let _ = TicketsAccumulator::::clear(u32::MAX, None); - // Reset tickets metadata TicketsMeta::::kill(); } /// Randomness buffer entries. /// - /// Assuming we're executing a block during epoch with index `N`. Entry values: - /// - 0 : randomness accumulator after last block execution. + /// Assuming we're executing a block during epoch with index `N`. + /// + /// Entries: + /// - 0 : randomness accumulator after execution of previous block. /// - 1 : randomness accumulator snapshot after execution of epoch `N-1` last block. /// - 2 : randomness accumulator snapshot after execution of epoch `N-2` last block. /// - 3 : randomness accumulator snapshot after execution of epoch `N-3` last block. /// - /// At a higher level, the semantic of these entries is defined as: + /// The semantic of these entries is defined as: /// - 3 : epoch `N` randomness /// - 2 : epoch `N+1` randomness /// - 1 : epoch `N+2` randomness + /// - 0 : accumulator for epoch `N+3` randomness /// /// If `index` is greater than 3 the `Default` is returned. pub fn randomness(index: usize) -> Randomness { @@ -834,10 +807,12 @@ impl Pallet { Self::randomness(0) } + /// Current epoch index. pub fn curr_epoch_index() -> u64 { Self::epoch_index(Self::current_slot()) } + /// Epoch's index from slot. pub fn epoch_index(slot: Slot) -> u64 { *slot / ::EpochLength::get() as u64 } @@ -846,6 +821,18 @@ impl Pallet { pub fn epoch_length() -> u32 { T::EpochLength::get() } + + /// Finds the start slot of the current epoch. + 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."; + epoch_index.checked_mul(T::EpochLength::get() as u64).expect(PROOF).into() + } } /// Trigger an epoch change, if any should take place. @@ -887,9 +874,9 @@ impl EpochChangeTrigger for EpochChangeInternalTrigger { ); let authorities = Pallet::::next_authorities(); let next_authorities = authorities.clone(); - let len = next_authorities.len() as u32; Pallet::::enact_epoch_change(authorities, next_authorities); // TODO + // let len = next_authorities.len() as u32; // T::WeightInfo::enact_epoch_change(len, T::EpochLength::get()) Weight::zero() } else { diff --git a/substrate/frame/sassafras/src/tests.rs b/substrate/frame/sassafras/src/tests.rs index 08912e751b12..be5613895307 100644 --- a/substrate/frame/sassafras/src/tests.rs +++ b/substrate/frame/sassafras/src/tests.rs @@ -376,57 +376,194 @@ fn slot_ticket_id_outside_in_fetch() { } #[test] -fn tickets_accumulator_consume() { - let curr_count = 8; - let next_count = 6; - let accu_count = 10; +fn slot_and_epoch_helpers_works() { + let start_block = 1; + let start_slot = (GENESIS_SLOT + 1).into(); + + let (pairs, mut ext) = new_test_ext_with_pairs(1, false); + + ext.execute_with(|| { + let epoch_length = Sassafras::epoch_length() as u64; + assert_eq!(epoch_length, 10); + + let check = |slot, slot_idx, epoch_slot, epoch_idx| { + assert_eq!(Sassafras::current_slot(), Slot::from(slot)); + assert_eq!(Sassafras::current_slot_index(), slot_idx); + assert_eq!(Sassafras::current_epoch_start(), Slot::from(epoch_slot)); + assert_eq!(Sassafras::curr_epoch_index(), epoch_idx); + }; + + // Post genesis state (before first initialization of epoch N) + check(0, 0, 0, 0); + + // Epoch N first block + initialize_block(start_block, start_slot, Default::default(), &pairs[0]); + check(101, 1, 100, 10); + + // Progress to epoch N last block + let end_block = start_block + epoch_length - 2; + progress_to_block(end_block, &pairs[0]).unwrap(); + check(109, 9, 100, 10); + + // Progres to epoch N+1 first block + progress_to_block(end_block + 1, &pairs[0]).unwrap(); + check(110, 0, 110, 11); + + // Progress to epoch N+1 last block + let end_block = end_block + epoch_length; + progress_to_block(end_block, &pairs[0]).unwrap(); + check(119, 9, 110, 11); + + // Progres to epoch N+2 first block + progress_to_block(end_block + 1, &pairs[0]).unwrap(); + check(120, 0, 120, 12); + }) +} - let tickets_count = curr_count + next_count + accu_count; +#[test] +fn tickets_accumulator_works() { + let e1_count = 6; + let e2_count = 10; + + let tot_count = e1_count + e2_count; let start_block = 1; let start_slot = (GENESIS_SLOT + 1).into(); - let tickets: Vec<_> = (0..tickets_count) + let tickets: Vec<_> = (0..tot_count) .map(|i| { (TicketId([i as u8; 32]), TicketBody { attempt_idx: 0, extra: Default::default() }) }) .collect(); - // Current epoch tickets - let curr_tickets = tickets[..curr_count].to_vec(); - let next_tickets = tickets[curr_count..curr_count + next_count].to_vec(); - let accu_tickets = tickets[curr_count + next_count..].to_vec(); + let e1_tickets = tickets[..e1_count].to_vec(); + let e2_tickets = tickets[e1_count..].to_vec(); - let (pairs, mut ext) = new_test_ext_with_pairs(4, false); + let (pairs, mut ext) = new_test_ext_with_pairs(1, false); ext.execute_with(|| { - curr_tickets - .iter() - .enumerate() - .for_each(|(i, t)| Tickets::::insert((0, i as u32), t)); + let epoch_length = Sassafras::epoch_length() as u64; - next_tickets - .iter() - .enumerate() - .for_each(|(i, t)| Tickets::::insert((1, i as u32), t)); + let epoch_idx = Sassafras::curr_epoch_index(); + let epoch_tag = (epoch_idx % 2) as u8; + let next_epoch_tag = epoch_tag ^ 1; - TicketsMeta::::set(TicketsMetadata { - tickets_count: [curr_count as u32, next_count as u32], - }); + let mut metadata = TicketsMetadata::default(); + TicketsMeta::::set(metadata); initialize_block(start_block, start_slot, Default::default(), &pairs[0]); - accu_tickets + // Append some tickets to the accumulator + e1_tickets .iter() .for_each(|t| TicketsAccumulator::::insert(TicketKey::from(t.0), &t.1)); - // We don't want to trigger an epoch change in this test. - let epoch_length = Sassafras::epoch_length(); + // Progress to epoch's last block + let end_block = start_block + epoch_length - 2; + let digest = progress_to_block(end_block, &pairs[0]).unwrap(); - // Progress to block 2 - let end_block = - start_block + (epoch_length - epoch_length / EPOCH_TAIL_FRACTION) as u64 - 1; - println!("ST: {}, EN: {}, L: {}", start_block, end_block, epoch_length); + let metadata = TicketsMeta::::get(); + assert_eq!(metadata.tickets_count[epoch_tag as usize], 0); + assert_eq!(metadata.tickets_count[next_epoch_tag as usize], 0); + + let header = finalize_block(end_block); + + let metadata = TicketsMeta::::get(); + assert_eq!(metadata.tickets_count[epoch_tag as usize], 0); + assert_eq!(metadata.tickets_count[next_epoch_tag as usize], e1_count as u32); + + // Start new epoch + + initialize_block( + end_block + 1, + Sassafras::current_slot() + 1, + Default::default(), + &pairs[0], + ); + + let metadata = TicketsMeta::::get(); + let next_epoch_tag = epoch_tag; + let epoch_tag = epoch_tag ^ 1; + assert_eq!(metadata.tickets_count[epoch_tag as usize], e1_count as u32); + assert_eq!(metadata.tickets_count[next_epoch_tag as usize], 0); + + // Append some tickets to the accumulator + e2_tickets + .iter() + .for_each(|t| TicketsAccumulator::::insert(TicketKey::from(t.0), &t.1)); + + // Progress to epoch's last block + let end_block = end_block + epoch_length; let digest = progress_to_block(end_block, &pairs[0]).unwrap(); + let metadata = TicketsMeta::::get(); + assert_eq!(metadata.tickets_count[epoch_tag as usize], e1_count as u32); + assert_eq!(metadata.tickets_count[next_epoch_tag as usize], 0); + let header = finalize_block(end_block); + + let metadata = TicketsMeta::::get(); + assert_eq!(metadata.tickets_count[epoch_tag as usize], e1_count as u32); + assert_eq!(metadata.tickets_count[next_epoch_tag as usize], e2_count as u32); + + let metadata = TicketsMeta::::get(); + + // Start new epoch + initialize_block( + end_block + 1, + Sassafras::current_slot() + 1, + Default::default(), + &pairs[0], + ); + + let metadata = TicketsMeta::::get(); + let next_epoch_tag = epoch_tag; + let epoch_tag = epoch_tag ^ 1; + assert_eq!(metadata.tickets_count[epoch_tag as usize], e2_count as u32); + assert_eq!(metadata.tickets_count[next_epoch_tag as usize], 0); + }); +} + +#[test] +fn incremental_accumulator_drain() { + let tot_count = 10; + let tickets: Vec<_> = (0..tot_count) + .map(|i| { + (TicketId([i as u8; 32]), TicketBody { attempt_idx: 0, extra: Default::default() }) + }) + .collect(); + + new_test_ext(0).execute_with(|| { + let mut metadata = TicketsMetadata::default(); + TicketsMeta::::set(metadata); + + tickets + .iter() + .for_each(|t| TicketsAccumulator::::insert(TicketKey::from(t.0), &t.1)); + + Sassafras::consume_tickets_accumulator(5, 0); + let meta = TicketsMeta::::get(); + assert_eq!(meta.tickets_count[0], 5); + assert_eq!(meta.tickets_count[1], 0); + tickets.iter().rev().take(5).enumerate().for_each(|(i, (id, _))| { + let (id2, _) = Tickets::::get((0, i as u32)).unwrap(); + assert_eq!(id, &id2); + }); + + Sassafras::consume_tickets_accumulator(3, 0); + let meta = TicketsMeta::::get(); + assert_eq!(meta.tickets_count[0], 8); + assert_eq!(meta.tickets_count[1], 0); + tickets.iter().rev().take(8).enumerate().for_each(|(i, (id, _))| { + let (id2, _) = Tickets::::get((0, i as u32)).unwrap(); + assert_eq!(id, &id2); + }); + + Sassafras::consume_tickets_accumulator(5, 0); + let meta = TicketsMeta::::get(); + assert_eq!(meta.tickets_count[0], 10); + assert_eq!(meta.tickets_count[1], 0); + tickets.iter().rev().enumerate().for_each(|(i, (id, _))| { + let (id2, _) = Tickets::::get((0, i as u32)).unwrap(); + assert_eq!(id, &id2); + }); }); } From 9aa0d1214f18df7ed32fb657a363700b9d79221b Mon Sep 17 00:00:00 2001 From: Davide Galassi Date: Tue, 7 May 2024 16:13:45 +0200 Subject: [PATCH 05/42] Test tickets submission and correct order for committed tickets --- Cargo.lock | 1 + substrate/frame/sassafras/Cargo.toml | 1 + substrate/frame/sassafras/src/lib.rs | 19 ++-- substrate/frame/sassafras/src/mock.rs | 62 +++++++++++ substrate/frame/sassafras/src/tests.rs | 138 ++++++++++++++++++++++++- 5 files changed, 211 insertions(+), 10 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 30a4d62900fa..0379ba4befbe 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -11173,6 +11173,7 @@ name = "pallet-sassafras" version = "0.3.5-dev" dependencies = [ "array-bytes 6.1.0", + "env_logger 0.11.3", "frame-benchmarking", "frame-support", "frame-system", diff --git a/substrate/frame/sassafras/Cargo.toml b/substrate/frame/sassafras/Cargo.toml index 888b1d8f31fc..ab02dd843ddc 100644 --- a/substrate/frame/sassafras/Cargo.toml +++ b/substrate/frame/sassafras/Cargo.toml @@ -29,6 +29,7 @@ sp-runtime = { path = "../../primitives/runtime", default-features = false } sp-std = { path = "../../primitives/std", default-features = false } [dev-dependencies] +env_logger = "0.11.3" array-bytes = "6.1" sp-core = { path = "../../primitives/core" } sp-crypto-hashing = { path = "../../primitives/crypto/hashing" } diff --git a/substrate/frame/sassafras/src/lib.rs b/substrate/frame/sassafras/src/lib.rs index a27138dc3a54..887f8a95210e 100644 --- a/substrate/frame/sassafras/src/lib.rs +++ b/substrate/frame/sassafras/src/lib.rs @@ -169,6 +169,8 @@ pub mod pallet { TicketDuplicate, /// Invalid ticket TicketInvalid, + /// Invalid ticket signature + TicketBadProof, } /// Current epoch authorities. @@ -373,7 +375,7 @@ pub mod pallet { 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"); - panic!("TODO") + return Err(Error::::TicketInvalid.into()) }; let ticket_id_input = vrf::ticket_id_input(&randomness, ticket.body.attempt_idx); @@ -388,12 +390,13 @@ pub mod pallet { 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 ({:?})", ticket_id); - panic!("TODO") + return Err(Error::::TicketBadProof.into()) } candidates.push((ticket_id, ticket.body)); } + println!("Depositing {} tickets", candidates.len()); Self::deposit_tickets(&candidates)?; Ok(Pays::No.into()) @@ -561,10 +564,12 @@ impl Pallet { } let diff = count.saturating_sub(T::EpochLength::get()); if diff > 0 { + println!("REMOVING {:?}", diff); let keys: Vec<_> = TicketsAccumulator::::iter_keys().take(diff as usize).collect(); for key in keys { let ticket_id = TicketId::from(key); if tickets.binary_search_by_key(&&ticket_id, |(id, _)| id).is_ok() { + println!("Removed one new candidate"); return Err(Error::TicketInvalid) } TicketsAccumulator::::remove(key); @@ -575,13 +580,13 @@ impl Pallet { fn consume_tickets_accumulator(max_items: usize, epoch_tag: u8) { let mut metadata = TicketsMeta::::get(); - let base = metadata.tickets_count[epoch_tag as usize]; - let mut idx = 0; + let mut accumulator_count = TicketsAccumulator::::count(); + let mut idx = accumulator_count; for (key, body) in TicketsAccumulator::::drain().take(max_items) { - Tickets::::insert((epoch_tag, base + idx), (TicketId::from(key), body)); - idx += 1; + idx -= 1; + Tickets::::insert((epoch_tag, idx), (TicketId::from(key), body)); } - metadata.tickets_count[epoch_tag as usize] += idx; + metadata.tickets_count[epoch_tag as usize] += (accumulator_count - idx); TicketsMeta::::set(metadata); } diff --git a/substrate/frame/sassafras/src/mock.rs b/substrate/frame/sassafras/src/mock.rs index 4d657c97558f..65e99a337be0 100644 --- a/substrate/frame/sassafras/src/mock.rs +++ b/substrate/frame/sassafras/src/mock.rs @@ -217,3 +217,65 @@ pub fn progress_to_block(number: u64, pair: &AuthorityPair) -> Option { } digest } + +fn make_ticket_with_prover( + attempt: u32, + pair: &AuthorityPair, + prover: &RingProver, +) -> (TicketId, TicketEnvelope) { + log::debug!("attempt: {}", attempt); + + // Values are referring to the next epoch + let randomness = Sassafras::next_randomness(); + + let ticket_id_input = vrf::ticket_id_input(&randomness, attempt); + + let body = TicketBody { attempt_idx: attempt, extra: Default::default() }; + let sign_data = vrf::ticket_body_sign_data(&body, ticket_id_input.clone()); + + let signature = pair.as_ref().ring_vrf_sign(&sign_data, &prover); + + let pre_output = &signature.pre_outputs[0]; + let ticket_id = vrf::make_ticket_id(&ticket_id_input, pre_output); + println!("T: {:?}", ticket_id); + + let envelope = TicketEnvelope { body, signature }; + + (ticket_id, envelope) +} + +pub fn make_prover(pair: &AuthorityPair) -> RingProver { + let public = pair.public(); + let mut prover_idx = None; + + let ring_ctx = Sassafras::ring_context().unwrap(); + + let pks: Vec = Sassafras::authorities() + .iter() + .enumerate() + .map(|(idx, auth)| { + if public == *auth { + prover_idx = Some(idx); + } + *auth.as_ref() + }) + .collect(); + + log::debug!("Building prover. Ring size: {}", pks.len()); + let prover = ring_ctx.prover(&pks, prover_idx.unwrap()).unwrap(); + log::debug!("Done"); + + prover +} + +/// Construct `attempts` tickets envelopes for the next epoch. +/// +/// E.g. by passing an optional threshold +pub fn make_tickets(attempts: u32, pair: &AuthorityPair) -> Vec<(TicketId, TicketEnvelope)> { + println!("MAKE TICKETS---"); + let prover = make_prover(pair); + (0..attempts) + .into_iter() + .map(|attempt| make_ticket_with_prover(attempt, pair, &prover)) + .collect() +} diff --git a/substrate/frame/sassafras/src/tests.rs b/substrate/frame/sassafras/src/tests.rs index be5613895307..a1b5459d5586 100644 --- a/substrate/frame/sassafras/src/tests.rs +++ b/substrate/frame/sassafras/src/tests.rs @@ -20,8 +20,11 @@ use crate::*; use mock::*; +use frame_support::dispatch::DispatchErrorWithPostInfo; use sp_consensus_sassafras::Slot; +const TICKETS_FILE: &str = "src/data/tickets.bin"; + const GENESIS_SLOT: u64 = 100; fn h2b(hex: &str) -> Vec { @@ -539,11 +542,17 @@ fn incremental_accumulator_drain() { .iter() .for_each(|t| TicketsAccumulator::::insert(TicketKey::from(t.0), &t.1)); + let accumulator: Vec<_> = TicketsAccumulator::::iter() + .map(|(k, b)| (TicketId::from(k), b)) + .collect(); + // Assess accumulator expected order (bigger id first) + assert!(accumulator.windows(2).all(|chunk| chunk[0].0 > chunk[1].0)); + Sassafras::consume_tickets_accumulator(5, 0); let meta = TicketsMeta::::get(); assert_eq!(meta.tickets_count[0], 5); assert_eq!(meta.tickets_count[1], 0); - tickets.iter().rev().take(5).enumerate().for_each(|(i, (id, _))| { + tickets.iter().enumerate().skip(5).for_each(|(i, (id, _))| { let (id2, _) = Tickets::::get((0, i as u32)).unwrap(); assert_eq!(id, &id2); }); @@ -552,7 +561,7 @@ fn incremental_accumulator_drain() { let meta = TicketsMeta::::get(); assert_eq!(meta.tickets_count[0], 8); assert_eq!(meta.tickets_count[1], 0); - tickets.iter().rev().take(8).enumerate().for_each(|(i, (id, _))| { + tickets.iter().enumerate().skip(2).for_each(|(i, (id, _))| { let (id2, _) = Tickets::::get((0, i as u32)).unwrap(); assert_eq!(id, &id2); }); @@ -561,9 +570,132 @@ fn incremental_accumulator_drain() { let meta = TicketsMeta::::get(); assert_eq!(meta.tickets_count[0], 10); assert_eq!(meta.tickets_count[1], 0); - tickets.iter().rev().enumerate().for_each(|(i, (id, _))| { + tickets.iter().enumerate().for_each(|(i, (id, _))| { let (id2, _) = Tickets::::get((0, i as u32)).unwrap(); assert_eq!(id, &id2); }); }); } + +fn data_read(filename: &str) -> T { + use std::{fs::File, io::Read}; + let mut file = File::open(filename).unwrap(); + let mut buf = Vec::new(); + file.read_to_end(&mut buf).unwrap(); + T::decode(&mut &buf[..]).unwrap() +} + +fn data_write(filename: &str, data: T) { + use std::{fs::File, io::Write}; + println!("Writing: {}", filename); + let mut file = File::create(filename).unwrap(); + let buf = data.encode(); + file.write_all(&buf).unwrap(); +} + +#[test] +fn submit_tickets_with_ring_proof_check_works() { + use sp_core::Pair as _; + let _ = env_logger::try_init(); + let start_block = 1; + let start_slot = (GENESIS_SLOT + 1).into(); + + let (authorities, mut candidates): (Vec, Vec) = + data_read(TICKETS_FILE); + + // Also checks that duplicates are discarded + + let (pairs, mut ext) = new_test_ext_with_pairs(authorities.len(), true); + let pair = &pairs[0]; + // Check if deserialized data has been generated for the correct set of authorities... + assert!(authorities.iter().zip(pairs.iter()).all(|(auth, pair)| auth == &pair.public())); + + ext.execute_with(|| { + initialize_block(start_block, start_slot, Default::default(), pair); + + // Submit the tickets + let candidates_per_call = 4; + let chunks: Vec<_> = candidates + .chunks(candidates_per_call) + .map(|chunk| BoundedVec::truncate_from(chunk.to_vec())) + .collect(); + assert_eq!(chunks.len(), 5); + + // Submit an invalid candidate + let mut chunk = chunks[2].clone(); + chunk[0].body.attempt_idx += 1; + let e = Sassafras::submit_tickets(RuntimeOrigin::none(), chunk).unwrap_err(); + assert_eq!(e, DispatchErrorWithPostInfo::from(Error::::TicketBadProof)); + assert_eq!(TicketsAccumulator::::count(), 0); + + // Start submitting from the mid valued chunks. + Sassafras::submit_tickets(RuntimeOrigin::none(), chunks[2].clone()).unwrap(); + assert_eq!(TicketsAccumulator::::count(), 4); + + // Submit something bigger, but we have space for all the candidates. + Sassafras::submit_tickets(RuntimeOrigin::none(), chunks[3].clone()).unwrap(); + assert_eq!(TicketsAccumulator::::count(), 8); + + // Try to submit duplicates + let e = Sassafras::submit_tickets(RuntimeOrigin::none(), chunks[2].clone()).unwrap_err(); + assert_eq!(e, DispatchErrorWithPostInfo::from(Error::::TicketDuplicate)); + assert_eq!(TicketsAccumulator::::count(), 8); + + // Submit something smaller. This is accepted (2 old tickets removed). + Sassafras::submit_tickets(RuntimeOrigin::none(), chunks[1].clone()).unwrap(); + assert_eq!(TicketsAccumulator::::count(), 10); + + // Try to submit a chunk with bigger tickets. This is discarded + let e = Sassafras::submit_tickets(RuntimeOrigin::none(), chunks[4].clone()).unwrap_err(); + assert_eq!(e, DispatchErrorWithPostInfo::from(Error::::TicketInvalid)); + assert_eq!(TicketsAccumulator::::count(), 10); + + // Submit the smaller candidates chunks. This is accepted (4 old tickets removed). + Sassafras::submit_tickets(RuntimeOrigin::none(), chunks[0].clone()).unwrap(); + assert_eq!(TicketsAccumulator::::count(), 10); + + // NOTE (implementation detail): the accumulator internally saves bigger tickets first. + let tickets: Vec<_> = TicketsAccumulator::::iter_values().collect(); + let expected: Vec<_> = + candidates.into_iter().take(10).rev().map(|candidate| candidate.body).collect(); + assert_eq!(tickets, expected) + }) +} + +#[test] +#[ignore = "test tickets generator"] +fn generate_test_tickets() { + use super::*; + use sp_core::crypto::Pair; + + let start_block = 1; + let start_slot = (GENESIS_SLOT + 1).into(); + + // Total number of authorities (the ring) + let authorities_count = 10; + let (pairs, mut ext) = new_test_ext_with_pairs(authorities_count, true); + + let authorities: Vec<_> = pairs.iter().map(|sk| sk.public()).collect(); + + let mut tickets = Vec::new(); + ext.execute_with(|| { + let config = Sassafras::protocol_config(); + assert!(authorities_count < config.max_authorities as usize); + + let tickets_count = authorities_count * config.attempts_number as usize; + + initialize_block(start_block, start_slot, Default::default(), &pairs[0]); + + println!("Constructing {} tickets", tickets_count); + + pairs.iter().take(authorities_count).enumerate().for_each(|(i, pair)| { + let t = make_tickets(config.attempts_number, pair); + tickets.extend(t); + println!("{:.2}%", 100f32 * ((i + 1) as f32 / authorities_count as f32)); + }); + }); + + tickets.sort_unstable_by_key(|t| t.0); + let envelopes: Vec<_> = tickets.into_iter().map(|t| t.1).collect(); + data_write(TICKETS_FILE, (authorities, envelopes)); +} From 680d88d06104806fe72c944da50cf7a1d89bbede Mon Sep 17 00:00:00 2001 From: Davide Galassi Date: Tue, 7 May 2024 16:14:04 +0200 Subject: [PATCH 06/42] Remove old implementation --- substrate/frame/sassafras-old/Cargo.toml | 63 - substrate/frame/sassafras-old/README.md | 8 - .../frame/sassafras-old/src/benchmarking.rs | 272 ----- .../src/data/benchmark-results.md | 99 -- .../sassafras-old/src/data/tickets-sort.md | 274 ----- .../sassafras-old/src/data/tickets-sort.png | Bin 33919 -> 0 bytes substrate/frame/sassafras-old/src/lib.rs | 1028 ----------------- substrate/frame/sassafras-old/src/mock.rs | 343 ------ substrate/frame/sassafras-old/src/tests.rs | 874 -------------- substrate/frame/sassafras-old/src/weights.rs | 425 ------- 10 files changed, 3386 deletions(-) delete mode 100644 substrate/frame/sassafras-old/Cargo.toml delete mode 100644 substrate/frame/sassafras-old/README.md delete mode 100644 substrate/frame/sassafras-old/src/benchmarking.rs delete mode 100644 substrate/frame/sassafras-old/src/data/benchmark-results.md delete mode 100644 substrate/frame/sassafras-old/src/data/tickets-sort.md delete mode 100644 substrate/frame/sassafras-old/src/data/tickets-sort.png delete mode 100644 substrate/frame/sassafras-old/src/lib.rs delete mode 100644 substrate/frame/sassafras-old/src/mock.rs delete mode 100644 substrate/frame/sassafras-old/src/tests.rs delete mode 100644 substrate/frame/sassafras-old/src/weights.rs diff --git a/substrate/frame/sassafras-old/Cargo.toml b/substrate/frame/sassafras-old/Cargo.toml deleted file mode 100644 index 888b1d8f31fc..000000000000 --- a/substrate/frame/sassafras-old/Cargo.toml +++ /dev/null @@ -1,63 +0,0 @@ -[package] -name = "pallet-sassafras" -version = "0.3.5-dev" -authors = ["Parity Technologies "] -edition.workspace = true -license = "Apache-2.0" -homepage = "https://substrate.io" -repository = "https://github.com/paritytech/substrate/" -description = "Consensus extension module for Sassafras consensus." -readme = "README.md" -publish = false - -[lints] -workspace = true - -[package.metadata.docs.rs] -targets = ["x86_64-unknown-linux-gnu"] - -[dependencies] -scale-codec = { package = "parity-scale-codec", version = "3.6.1", default-features = false, features = ["derive"] } -scale-info = { version = "2.11.1", default-features = false, features = ["derive"] } -frame-benchmarking = { path = "../benchmarking", default-features = false, optional = true } -frame-support = { path = "../support", default-features = false } -frame-system = { path = "../system", default-features = false } -log = { workspace = true } -sp-consensus-sassafras = { path = "../../primitives/consensus/sassafras", default-features = false, features = ["serde"] } -sp-io = { path = "../../primitives/io", default-features = false } -sp-runtime = { path = "../../primitives/runtime", default-features = false } -sp-std = { path = "../../primitives/std", default-features = false } - -[dev-dependencies] -array-bytes = "6.1" -sp-core = { path = "../../primitives/core" } -sp-crypto-hashing = { path = "../../primitives/crypto/hashing" } - -[features] -default = ["std"] -std = [ - "frame-benchmarking?/std", - "frame-support/std", - "frame-system/std", - "log/std", - "scale-codec/std", - "scale-info/std", - "sp-consensus-sassafras/std", - "sp-io/std", - "sp-runtime/std", - "sp-std/std", -] -runtime-benchmarks = [ - "frame-benchmarking/runtime-benchmarks", - "frame-support/runtime-benchmarks", - "frame-system/runtime-benchmarks", - "sp-runtime/runtime-benchmarks", -] -try-runtime = [ - "frame-support/try-runtime", - "frame-system/try-runtime", - "sp-runtime/try-runtime", -] -# Construct dummy ring context on genesis. -# Mostly used for testing and development. -construct-dummy-ring-context = [] diff --git a/substrate/frame/sassafras-old/README.md b/substrate/frame/sassafras-old/README.md deleted file mode 100644 index f0e24a053557..000000000000 --- a/substrate/frame/sassafras-old/README.md +++ /dev/null @@ -1,8 +0,0 @@ -Runtime module for SASSAFRAS consensus. - -- Tracking issue: https://github.com/paritytech/polkadot-sdk/issues/41 -- Protocol RFC proposal: https://github.com/polkadot-fellows/RFCs/pull/26 - -# ⚠️ WARNING ⚠️ - -The crate interfaces and structures are experimental and may be subject to changes. diff --git a/substrate/frame/sassafras-old/src/benchmarking.rs b/substrate/frame/sassafras-old/src/benchmarking.rs deleted file mode 100644 index 2b2467c6f84d..000000000000 --- a/substrate/frame/sassafras-old/src/benchmarking.rs +++ /dev/null @@ -1,272 +0,0 @@ -// 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. - -//! Benchmarks for the Sassafras pallet. - -use crate::*; -use sp_consensus_sassafras::{vrf::VrfSignature, EphemeralPublic, EpochConfiguration}; - -use frame_benchmarking::v2::*; -use frame_support::traits::Hooks; -use frame_system::RawOrigin; - -const LOG_TARGET: &str = "sassafras::benchmark"; - -const TICKETS_DATA: &[u8] = include_bytes!("data/25_tickets_100_auths.bin"); - -fn make_dummy_vrf_signature() -> VrfSignature { - // This leverages our knowledge about serialized vrf signature structure. - // Mostly to avoid to import all the bandersnatch primitive just for this test. - let buf = [ - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 0xb5, 0x5f, 0x8e, 0xc7, 0x68, 0xf5, 0x05, 0x3f, 0xa9, - 0x18, 0xca, 0x07, 0x13, 0xc7, 0x4b, 0xa3, 0x9a, 0x97, 0xd3, 0x76, 0x8f, 0x0c, 0xbf, 0x2e, - 0xd4, 0xf9, 0x3a, 0xae, 0xc1, 0x96, 0x2a, 0x64, 0x80, - ]; - VrfSignature::decode(&mut &buf[..]).unwrap() -} - -#[benchmarks] -mod benchmarks { - use super::*; - - // For first block (#1) we do some extra operation. - // But is a one shot operation, so we don't account for it here. - // We use 0, as it will be the path used by all the blocks with n != 1 - #[benchmark] - fn on_initialize() { - let block_num = BlockNumberFor::::from(0u32); - - let slot_claim = SlotClaim { - authority_idx: 0, - slot: Default::default(), - vrf_signature: make_dummy_vrf_signature(), - ticket_claim: None, - }; - frame_system::Pallet::::deposit_log((&slot_claim).into()); - - // We currently don't account for the potential weight added by the `on_finalize` - // incremental sorting of the tickets. - - #[block] - { - // According to `Hooks` trait docs, `on_finalize` `Weight` should be bundled - // together with `on_initialize` `Weight`. - Pallet::::on_initialize(block_num); - Pallet::::on_finalize(block_num) - } - } - - // Weight for the default internal epoch change trigger. - // - // Parameters: - // - `x`: number of authorities (1:100). - // - `y`: epoch length in slots (1000:5000) - // - // This accounts for the worst case which includes: - // - load the full ring context. - // - recompute the ring verifier. - // - sorting the epoch tickets in one shot - // (here we account for the very unlucky scenario where we haven't done any sort work yet) - // - pending epoch change config. - // - // For this bench we assume a redundancy factor of 2 (suggested value to be used in prod). - #[benchmark] - fn enact_epoch_change(x: Linear<1, 100>, y: Linear<1000, 5000>) { - let authorities_count = x as usize; - let epoch_length = y as u32; - let redundancy_factor = 2; - - let unsorted_tickets_count = epoch_length * redundancy_factor; - - let mut meta = TicketsMetadata { unsorted_tickets_count, tickets_count: [0, 0] }; - let config = EpochConfiguration { redundancy_factor, attempts_number: 32 }; - - // Triggers ring verifier computation for `x` authorities - let mut raw_data = TICKETS_DATA; - let (authorities, _): (Vec, Vec) = - Decode::decode(&mut raw_data).expect("Failed to decode tickets buffer"); - let next_authorities: Vec<_> = authorities[..authorities_count].to_vec(); - let next_authorities = WeakBoundedVec::force_from(next_authorities, None); - NextAuthorities::::set(next_authorities); - - // Triggers JIT sorting tickets - (0..meta.unsorted_tickets_count) - .collect::>() - .chunks(SEGMENT_MAX_SIZE as usize) - .enumerate() - .for_each(|(segment_id, chunk)| { - let segment = chunk - .iter() - .map(|i| { - let id_bytes = crate::hashing::blake2_128(&i.to_le_bytes()); - TicketId::from_le_bytes(id_bytes) - }) - .collect::>(); - UnsortedSegments::::insert( - segment_id as u32, - BoundedVec::truncate_from(segment), - ); - }); - - // Triggers some code related to config change (dummy values) - NextEpochConfig::::set(Some(config)); - PendingEpochConfigChange::::set(Some(config)); - - // Triggers the cleanup of the "just elapsed" epoch tickets (i.e. the current one) - let epoch_tag = EpochIndex::::get() & 1; - meta.tickets_count[epoch_tag as usize] = epoch_length; - (0..epoch_length).for_each(|i| { - let id_bytes = crate::hashing::blake2_128(&i.to_le_bytes()); - let id = TicketId::from_le_bytes(id_bytes); - TicketsIds::::insert((epoch_tag as u8, i), id); - let body = TicketBody { - attempt_idx: i, - erased_public: EphemeralPublic::from([i as u8; 32]), - revealed_public: EphemeralPublic::from([i as u8; 32]), - }; - TicketsData::::set(id, Some(body)); - }); - - TicketsMeta::::set(meta); - - #[block] - { - Pallet::::should_end_epoch(BlockNumberFor::::from(3u32)); - let next_authorities = Pallet::::next_authorities(); - // Using a different set of authorities triggers the recomputation of ring verifier. - Pallet::::enact_epoch_change(Default::default(), next_authorities); - } - } - - #[benchmark] - fn submit_tickets(x: Linear<1, 25>) { - let tickets_count = x as usize; - - let mut raw_data = TICKETS_DATA; - let (authorities, tickets): (Vec, Vec) = - Decode::decode(&mut raw_data).expect("Failed to decode tickets buffer"); - - log::debug!(target: LOG_TARGET, "PreBuiltTickets: {} tickets, {} authorities", tickets.len(), authorities.len()); - - // Set `NextRandomness` to the same value used for pre-built tickets - // (see `make_tickets_data` test). - NextRandomness::::set([0; 32]); - - Pallet::::update_ring_verifier(&authorities); - - // Set next epoch config to accept all the tickets - let next_config = EpochConfiguration { attempts_number: 1, redundancy_factor: u32::MAX }; - NextEpochConfig::::set(Some(next_config)); - - // Use the authorities in the pre-build tickets - let authorities = WeakBoundedVec::force_from(authorities, None); - NextAuthorities::::set(authorities); - - let tickets = tickets[..tickets_count].to_vec(); - let tickets = BoundedVec::truncate_from(tickets); - - log::debug!(target: LOG_TARGET, "Submitting {} tickets", tickets_count); - - #[extrinsic_call] - submit_tickets(RawOrigin::None, tickets); - } - - #[benchmark] - fn plan_config_change() { - let config = EpochConfiguration { redundancy_factor: 1, attempts_number: 10 }; - - #[extrinsic_call] - plan_config_change(RawOrigin::Root, config); - } - - // Construction of ring verifier - #[benchmark] - fn update_ring_verifier(x: Linear<1, 100>) { - let authorities_count = x as usize; - - let mut raw_data = TICKETS_DATA; - let (authorities, _): (Vec, Vec) = - Decode::decode(&mut raw_data).expect("Failed to decode tickets buffer"); - let authorities: Vec<_> = authorities[..authorities_count].to_vec(); - - #[block] - { - Pallet::::update_ring_verifier(&authorities); - } - } - - // Bare loading of ring context. - // - // It is interesting to see how this compares to 'update_ring_verifier', which - // also recomputes and stores the new verifier. - #[benchmark] - fn load_ring_context() { - #[block] - { - let _ring_ctx = RingContext::::get().unwrap(); - } - } - - // Tickets segments sorting function benchmark. - #[benchmark] - fn sort_segments(x: Linear<1, 100>) { - let segments_count = x as u32; - let tickets_count = segments_count * SEGMENT_MAX_SIZE; - - // Construct a bunch of dummy tickets - let tickets: Vec<_> = (0..tickets_count) - .map(|i| { - let body = TicketBody { - attempt_idx: i, - erased_public: EphemeralPublic::from([i as u8; 32]), - revealed_public: EphemeralPublic::from([i as u8; 32]), - }; - let id_bytes = crate::hashing::blake2_128(&i.to_le_bytes()); - let id = TicketId::from_le_bytes(id_bytes); - (id, body) - }) - .collect(); - - for (chunk_id, chunk) in tickets.chunks(SEGMENT_MAX_SIZE as usize).enumerate() { - let segment: Vec = chunk - .iter() - .map(|(id, body)| { - TicketsData::::set(id, Some(body.clone())); - *id - }) - .collect(); - let segment = BoundedVec::truncate_from(segment); - UnsortedSegments::::insert(chunk_id as u32, segment); - } - - // Update metadata - let mut meta = TicketsMeta::::get(); - meta.unsorted_tickets_count = tickets_count; - TicketsMeta::::set(meta); - - log::debug!(target: LOG_TARGET, "Before sort: {:?}", meta); - #[block] - { - Pallet::::sort_segments(u32::MAX, 0, &mut meta); - } - log::debug!(target: LOG_TARGET, "After sort: {:?}", meta); - } -} diff --git a/substrate/frame/sassafras-old/src/data/benchmark-results.md b/substrate/frame/sassafras-old/src/data/benchmark-results.md deleted file mode 100644 index 8682f96cbe5a..000000000000 --- a/substrate/frame/sassafras-old/src/data/benchmark-results.md +++ /dev/null @@ -1,99 +0,0 @@ -# Benchmarks High Level Results - -- **Ring size**: the actual number of validators for an epoch -- **Domain size**: a value which bounds the max size of the ring (max_ring_size = domain_size - 256) - -## Verify Submitted Tickets (extrinsic) - -`x` = Number of tickets - -### Domain=1024, Uncompressed (~ 13 ms + 11·x ms) - - Time ~= 13400 - + x 11390 - µs - -### Domain=1024, Compressed (~ 13 ms + 11·x ms) - - Time ~= 13120 - + x 11370 - µs - -### Domain=2048, Uncompressed (~ 26 ms + 11·x ms) - - Time ~= 26210 - + x 11440 - µs - -### Domain=2048, Compressed (~ 26 ms + 11·x ms) - - Time ~= 26250 - + x 11460 - µs - -### Conclusions - -- Verification doesn't depend on ring size as verification key is already constructed. -- The call is fast as far as the max number of tickets which can be submitted in one shot - is appropriately bounded. -- Currently, the bound is set equal epoch length, which iirc for Polkadot is 3600. - In this case if all the tickets are submitted in one shot timing is expected to be - ~39 seconds, which is not acceptable. TODO: find a sensible bound - ---- - -## Recompute Ring Verifier Key (on epoch change) - -`x` = Ring size - -### Domain=1024, Uncompressed (~ 50 ms) - - Time ~= 54070 - + x 98.53 - µs - -### Domain=1024, Compressed (~ 700 ms) - - Time ~= 733700 - + x 90.49 - µs - -### Domain=2048, Uncompressed (~ 100 ms) - - Time ~= 107700 - + x 108.5 - µs - -### Domain=2048, Compressed (~ 1.5 s) - - Time ~= 1462400 - + x 65.14 - µs - -### Conclusions - -- Here we load the full ring context data to recompute verification key for the epoch -- Ring size influence is marginal (e.g. for 1500 validators → ~98 ms to be added to the base time) -- This step is performed at most once per epoch (if validator set changes). -- Domain size for ring context influence the PoV size (see next paragraph) -- Decompression heavily influence timings (1.5sec vs 100ms for same domain size) - ---- - -## Ring Context Data Size - -### Domain=1024, Uncompressed - - 295412 bytes = ~ 300 KiB - -### Domain=1024, Compressed - - 147716 bytes = ~ 150 KiB - -### Domain=2048, Uncompressed - - 590324 bytes = ~ 590 KiB - -### Domain=2048, Compressed - - 295172 bytes = ~ 300 KiB diff --git a/substrate/frame/sassafras-old/src/data/tickets-sort.md b/substrate/frame/sassafras-old/src/data/tickets-sort.md deleted file mode 100644 index 64fc45e4fb00..000000000000 --- a/substrate/frame/sassafras-old/src/data/tickets-sort.md +++ /dev/null @@ -1,274 +0,0 @@ -# Segments Incremental Sorting Strategy Empirical Results - -Parameters: -- 128 segments -- segment max length 128 -- 32767 random tickets ids -- epoch length 3600 (== max tickets to keep) - -The table shows the comparison between the segments left in the unsorted segments buffer -and the number of new tickets which are added from the last segment to the sorted tickets -buffer (i.e. how many tickets we retain from the last processed segment) - -| Segments Left | Tickets Pushed | -|-----|-----| -| 255 | 128 | -| 254 | 128 | -| 253 | 128 | -| 252 | 128 | -| 251 | 128 | -| 250 | 128 | -| 249 | 128 | -| 248 | 128 | -| 247 | 128 | -| 246 | 128 | -| 245 | 128 | -| 244 | 128 | -| 243 | 128 | -| 242 | 128 | -| 241 | 128 | -| 240 | 128 | -| 239 | 128 | -| 238 | 128 | -| 237 | 128 | -| 236 | 128 | -| 235 | 128 | -| 234 | 128 | -| 233 | 128 | -| 232 | 128 | -| 231 | 128 | -| 230 | 128 | -| 229 | 128 | -| 228 | 128 | -| 227 | 128 | -| 226 | 126 | -| 225 | 117 | -| 224 | 120 | -| 223 | 110 | -| 222 | 110 | -| 221 | 102 | -| 220 | 107 | -| 219 | 96 | -| 218 | 105 | -| 217 | 92 | -| 216 | 91 | -| 215 | 85 | -| 214 | 84 | -| 213 | 88 | -| 212 | 77 | -| 211 | 86 | -| 210 | 73 | -| 209 | 73 | -| 208 | 81 | -| 207 | 83 | -| 206 | 70 | -| 205 | 84 | -| 204 | 71 | -| 203 | 63 | -| 202 | 60 | -| 201 | 53 | -| 200 | 73 | -| 199 | 55 | -| 198 | 65 | -| 197 | 62 | -| 196 | 55 | -| 195 | 63 | -| 194 | 61 | -| 193 | 48 | -| 192 | 67 | -| 191 | 61 | -| 190 | 55 | -| 189 | 49 | -| 188 | 60 | -| 187 | 49 | -| 186 | 51 | -| 185 | 53 | -| 184 | 47 | -| 183 | 51 | -| 182 | 51 | -| 181 | 53 | -| 180 | 42 | -| 179 | 43 | -| 178 | 48 | -| 177 | 46 | -| 176 | 39 | -| 175 | 54 | -| 174 | 39 | -| 173 | 44 | -| 172 | 51 | -| 171 | 49 | -| 170 | 48 | -| 169 | 48 | -| 168 | 41 | -| 167 | 39 | -| 166 | 41 | -| 165 | 40 | -| 164 | 43 | -| 163 | 53 | -| 162 | 51 | -| 161 | 36 | -| 160 | 45 | -| 159 | 40 | -| 158 | 29 | -| 157 | 37 | -| 156 | 31 | -| 155 | 38 | -| 154 | 31 | -| 153 | 38 | -| 152 | 39 | -| 151 | 30 | -| 150 | 37 | -| 149 | 42 | -| 148 | 35 | -| 147 | 33 | -| 146 | 35 | -| 145 | 37 | -| 144 | 38 | -| 143 | 31 | -| 142 | 38 | -| 141 | 38 | -| 140 | 27 | -| 139 | 31 | -| 138 | 25 | -| 137 | 31 | -| 136 | 26 | -| 135 | 30 | -| 134 | 31 | -| 133 | 37 | -| 132 | 29 | -| 131 | 24 | -| 130 | 31 | -| 129 | 34 | -| 128 | 31 | -| 127 | 28 | -| 126 | 28 | -| 125 | 19 | -| 124 | 27 | -| 123 | 29 | -| 122 | 36 | -| 121 | 32 | -| 120 | 29 | -| 119 | 28 | -| 118 | 33 | -| 117 | 18 | -| 116 | 28 | -| 115 | 27 | -| 114 | 28 | -| 113 | 21 | -| 112 | 23 | -| 111 | 19 | -| 110 | 21 | -| 109 | 20 | -| 108 | 26 | -| 107 | 23 | -| 106 | 30 | -| 105 | 31 | -| 104 | 19 | -| 103 | 25 | -| 102 | 23 | -| 101 | 29 | -| 100 | 18 | -| 99 | 19 | -| 98 | 20 | -| 97 | 21 | -| 96 | 23 | -| 95 | 20 | -| 94 | 27 | -| 93 | 20 | -| 92 | 22 | -| 91 | 23 | -| 90 | 23 | -| 89 | 20 | -| 88 | 15 | -| 87 | 17 | -| 86 | 28 | -| 85 | 25 | -| 84 | 10 | -| 83 | 20 | -| 82 | 23 | -| 81 | 28 | -| 80 | 17 | -| 79 | 23 | -| 78 | 24 | -| 77 | 22 | -| 76 | 18 | -| 75 | 25 | -| 74 | 31 | -| 73 | 27 | -| 72 | 19 | -| 71 | 13 | -| 70 | 17 | -| 69 | 24 | -| 68 | 20 | -| 67 | 12 | -| 66 | 17 | -| 65 | 16 | -| 64 | 26 | -| 63 | 24 | -| 62 | 12 | -| 61 | 19 | -| 60 | 18 | -| 59 | 20 | -| 58 | 18 | -| 57 | 12 | -| 56 | 15 | -| 55 | 17 | -| 54 | 14 | -| 53 | 25 | -| 52 | 22 | -| 51 | 15 | -| 50 | 17 | -| 49 | 15 | -| 48 | 17 | -| 47 | 18 | -| 46 | 17 | -| 45 | 23 | -| 44 | 17 | -| 43 | 13 | -| 42 | 15 | -| 41 | 18 | -| 40 | 11 | -| 39 | 19 | -| 38 | 18 | -| 37 | 12 | -| 36 | 19 | -| 35 | 18 | -| 34 | 15 | -| 33 | 12 | -| 32 | 25 | -| 31 | 20 | -| 30 | 24 | -| 29 | 20 | -| 28 | 10 | -| 27 | 15 | -| 26 | 16 | -| 25 | 15 | -| 24 | 15 | -| 23 | 13 | -| 22 | 12 | -| 21 | 14 | -| 20 | 19 | -| 19 | 17 | -| 18 | 17 | -| 17 | 18 | -| 16 | 15 | -| 15 | 13 | -| 14 | 11 | -| 13 | 16 | -| 12 | 13 | -| 11 | 18 | -| 10 | 19 | -| 9 | 10 | -| 8 | 7 | -| 7 | 15 | -| 6 | 12 | -| 5 | 12 | -| 4 | 17 | -| 3 | 14 | -| 2 | 17 | -| 1 | 9 | -| 0 | 13 | - -# Graph of the same data - -![graph](tickets-sort.png) diff --git a/substrate/frame/sassafras-old/src/data/tickets-sort.png b/substrate/frame/sassafras-old/src/data/tickets-sort.png deleted file mode 100644 index b34ce3f37ba9d39aa649cc6d5a216373048c0064..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 33919 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 k^`E)Px# diff --git a/substrate/frame/sassafras-old/src/lib.rs b/substrate/frame/sassafras-old/src/lib.rs deleted file mode 100644 index 522229912bca..000000000000 --- a/substrate/frame/sassafras-old/src/lib.rs +++ /dev/null @@ -1,1028 +0,0 @@ -// 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; - - /// Redundancy factor - #[pallet::constant] - type RedundancyFactor: Get; - - /// Max attempts number - #[pallet::constant] - type AttemptsNumber: 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>; - - /// 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>; - - /// 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) { - 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 ticket_threshold = sp_consensus_sassafras::ticket_id_threshold( - T::RedundancyFactor::get(), - epoch_length as u32, - T::AttemptsNumber::get(), - 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 ({:?} >= {:?})", ticket_id, ticket_threshold); - continue - } - - // Check for duplicates - if TicketsData::::contains_key(ticket_id) { - debug!(target: LOG_TARGET, "Ignoring duplicate ticket ({:?})", 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 ({:?})", 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()) - } - } - - #[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); - - // 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(), - }; - 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(), - }; - 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: EpochConfiguration { - redundancy_factor: T::RedundancyFactor::get(), - attempts_number: T::AttemptsNumber::get(), - }, - } - } - - /// 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: EpochConfiguration { - redundancy_factor: T::RedundancyFactor::get(), - attempts_number: T::AttemptsNumber::get(), - }, - } - } - - /// 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; -} diff --git a/substrate/frame/sassafras-old/src/mock.rs b/substrate/frame/sassafras-old/src/mock.rs deleted file mode 100644 index f145bffa3a05..000000000000 --- a/substrate/frame/sassafras-old/src/mock.rs +++ /dev/null @@ -1,343 +0,0 @@ -// 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. - -//! Test utilities for Sassafras pallet. - -use crate::{self as pallet_sassafras, EpochChangeInternalTrigger, *}; - -use frame_support::{ - derive_impl, - traits::{ConstU32, OnFinalize, OnInitialize}, -}; -use sp_consensus_sassafras::{ - digests::SlotClaim, - vrf::{RingProver, VrfSignature}, - AuthorityIndex, AuthorityPair, EpochConfiguration, Slot, TicketBody, TicketEnvelope, TicketId, -}; -use sp_core::{ - crypto::{ByteArray, Pair, UncheckedFrom, VrfSecret, Wraps}, - ed25519::Public as EphemeralPublic, - H256, U256, -}; -use sp_runtime::{ - testing::{Digest, DigestItem, Header, TestXt}, - BuildStorage, -}; - -const LOG_TARGET: &str = "sassafras::tests"; - -const EPOCH_LENGTH: u32 = 10; -const MAX_AUTHORITIES: u32 = 100; - -#[derive_impl(frame_system::config_preludes::TestDefaultConfig)] -impl frame_system::Config for Test { - type Block = frame_system::mocking::MockBlock; -} - -impl frame_system::offchain::SendTransactionTypes for Test -where - RuntimeCall: From, -{ - type OverarchingCall = RuntimeCall; - type Extrinsic = TestXt; -} - -impl pallet_sassafras::Config for Test { - type EpochLength = ConstU32; - type MaxAuthorities = ConstU32; - type EpochChangeTrigger = EpochChangeInternalTrigger; - type WeightInfo = (); -} - -frame_support::construct_runtime!( - pub enum Test { - System: frame_system, - Sassafras: pallet_sassafras, - } -); - -// Default used for most of the tests. -// -// The redundancy factor has been set to max value to accept all submitted -// tickets without worrying about the threshold. -pub const TEST_EPOCH_CONFIGURATION: EpochConfiguration = - EpochConfiguration { redundancy_factor: u32::MAX, attempts_number: 5 }; - -/// Build and returns test storage externalities -pub fn new_test_ext(authorities_len: usize) -> sp_io::TestExternalities { - new_test_ext_with_pairs(authorities_len, false).1 -} - -/// Build and returns test storage externalities and authority set pairs used -/// by Sassafras genesis configuration. -pub fn new_test_ext_with_pairs( - authorities_len: usize, - with_ring_context: bool, -) -> (Vec, sp_io::TestExternalities) { - let pairs = (0..authorities_len) - .map(|i| AuthorityPair::from_seed(&U256::from(i).into())) - .collect::>(); - - let authorities: Vec<_> = pairs.iter().map(|p| p.public()).collect(); - - let mut storage = frame_system::GenesisConfig::::default().build_storage().unwrap(); - - pallet_sassafras::GenesisConfig:: { - authorities: authorities.clone(), - epoch_config: TEST_EPOCH_CONFIGURATION, - _phantom: core::marker::PhantomData, - } - .assimilate_storage(&mut storage) - .unwrap(); - - let mut ext: sp_io::TestExternalities = storage.into(); - - if with_ring_context { - ext.execute_with(|| { - log::debug!(target: LOG_TARGET, "Building testing ring context"); - let ring_ctx = vrf::RingContext::new_testing(); - RingContext::::set(Some(ring_ctx.clone())); - Sassafras::update_ring_verifier(&authorities); - }); - } - - (pairs, ext) -} - -fn make_ticket_with_prover( - attempt: u32, - pair: &AuthorityPair, - prover: &RingProver, -) -> TicketEnvelope { - log::debug!("attempt: {}", attempt); - - // Values are referring to the next epoch - let epoch = Sassafras::epoch_index() + 1; - let randomness = Sassafras::next_randomness(); - - // Make a dummy ephemeral public that hopefully is unique within one test instance. - // In the tests, the values within the erased public are just used to compare - // ticket bodies, so it is not important to be a valid key. - let mut raw: [u8; 32] = [0; 32]; - raw.copy_from_slice(&pair.public().as_slice()[0..32]); - let erased_public = EphemeralPublic::unchecked_from(raw); - let revealed_public = erased_public; - - let ticket_id_input = vrf::ticket_id_input(&randomness, attempt, epoch); - - let body = TicketBody { attempt_idx: attempt, erased_public, revealed_public }; - let sign_data = vrf::ticket_body_sign_data(&body, ticket_id_input); - - let signature = pair.as_ref().ring_vrf_sign(&sign_data, &prover); - - // Ticket-id can be generated via vrf-preout. - // We don't care that much about its value here. - TicketEnvelope { body, signature } -} - -pub fn make_prover(pair: &AuthorityPair) -> RingProver { - let public = pair.public(); - let mut prover_idx = None; - - let ring_ctx = Sassafras::ring_context().unwrap(); - - let pks: Vec = Sassafras::authorities() - .iter() - .enumerate() - .map(|(idx, auth)| { - if public == *auth { - prover_idx = Some(idx); - } - *auth.as_ref() - }) - .collect(); - - log::debug!("Building prover. Ring size: {}", pks.len()); - let prover = ring_ctx.prover(&pks, prover_idx.unwrap()).unwrap(); - log::debug!("Done"); - - prover -} - -/// Construct `attempts` tickets envelopes for the next epoch. -/// -/// E.g. by passing an optional threshold -pub fn make_tickets(attempts: u32, pair: &AuthorityPair) -> Vec { - let prover = make_prover(pair); - (0..attempts) - .into_iter() - .map(|attempt| make_ticket_with_prover(attempt, pair, &prover)) - .collect() -} - -pub fn make_ticket_body(attempt_idx: u32, pair: &AuthorityPair) -> (TicketId, TicketBody) { - // Values are referring to the next epoch - let epoch = Sassafras::epoch_index() + 1; - let randomness = Sassafras::next_randomness(); - - let ticket_id_input = vrf::ticket_id_input(&randomness, attempt_idx, epoch); - let ticket_id_pre_output = pair.as_inner_ref().vrf_pre_output(&ticket_id_input); - - let id = vrf::make_ticket_id(&ticket_id_input, &ticket_id_pre_output); - - // Make a dummy ephemeral public that hopefully is unique within one test instance. - // In the tests, the values within the erased public are just used to compare - // ticket bodies, so it is not important to be a valid key. - let mut raw: [u8; 32] = [0; 32]; - raw[..16].copy_from_slice(&pair.public().as_slice()[0..16]); - raw[16..].copy_from_slice(&id.to_le_bytes()); - let erased_public = EphemeralPublic::unchecked_from(raw); - let revealed_public = erased_public; - - let body = TicketBody { attempt_idx, erased_public, revealed_public }; - - (id, body) -} - -pub fn make_dummy_ticket_body(attempt_idx: u32) -> (TicketId, TicketBody) { - let hash = sp_crypto_hashing::blake2_256(&attempt_idx.to_le_bytes()); - - let erased_public = EphemeralPublic::unchecked_from(hash); - let revealed_public = erased_public; - - let body = TicketBody { attempt_idx, erased_public, revealed_public }; - - let mut bytes = [0u8; 16]; - bytes.copy_from_slice(&hash[..16]); - let id = TicketId::from_le_bytes(bytes); - - (id, body) -} - -pub fn make_ticket_bodies( - number: u32, - pair: Option<&AuthorityPair>, -) -> Vec<(TicketId, TicketBody)> { - (0..number) - .into_iter() - .map(|i| match pair { - Some(pair) => make_ticket_body(i, pair), - None => make_dummy_ticket_body(i), - }) - .collect() -} - -/// Persist the given tickets in the unsorted segments buffer. -/// -/// This function skips all the checks performed by the `submit_tickets` extrinsic and -/// directly appends the tickets to the `UnsortedSegments` structure. -pub fn persist_next_epoch_tickets_as_segments(tickets: &[(TicketId, TicketBody)]) { - let mut ids = Vec::with_capacity(tickets.len()); - tickets.iter().for_each(|(id, body)| { - TicketsData::::set(id, Some(body.clone())); - ids.push(*id); - }); - let max_chunk_size = Sassafras::epoch_length() as usize; - ids.chunks(max_chunk_size).for_each(|chunk| { - Sassafras::append_tickets(BoundedVec::truncate_from(chunk.to_vec())); - }) -} - -/// Calls the [`persist_next_epoch_tickets_as_segments`] and then proceeds to the -/// sorting of the candidates. -/// -/// Only "winning" tickets are left. -pub fn persist_next_epoch_tickets(tickets: &[(TicketId, TicketBody)]) { - persist_next_epoch_tickets_as_segments(tickets); - // Force sorting of next epoch tickets (enactment) by explicitly querying the first of them. - let next_epoch = Sassafras::next_epoch(); - assert_eq!(TicketsMeta::::get().unsorted_tickets_count, tickets.len() as u32); - Sassafras::slot_ticket(next_epoch.start).unwrap(); - assert_eq!(TicketsMeta::::get().unsorted_tickets_count, 0); -} - -fn slot_claim_vrf_signature(slot: Slot, pair: &AuthorityPair) -> VrfSignature { - let mut epoch = Sassafras::epoch_index(); - let mut randomness = Sassafras::randomness(); - - // Check if epoch is going to change on initialization. - let epoch_start = Sassafras::current_epoch_start(); - let epoch_length = EPOCH_LENGTH.into(); - if epoch_start != 0_u64 && slot >= epoch_start + epoch_length { - epoch += slot.saturating_sub(epoch_start).saturating_div(epoch_length); - randomness = crate::NextRandomness::::get(); - } - - let data = vrf::slot_claim_sign_data(&randomness, slot, epoch); - pair.as_ref().vrf_sign(&data) -} - -/// Construct a `PreDigest` instance for the given parameters. -pub fn make_slot_claim( - authority_idx: AuthorityIndex, - slot: Slot, - pair: &AuthorityPair, -) -> SlotClaim { - let vrf_signature = slot_claim_vrf_signature(slot, pair); - SlotClaim { authority_idx, slot, vrf_signature, ticket_claim: None } -} - -/// Construct a `Digest` with a `SlotClaim` item. -pub fn make_digest(authority_idx: AuthorityIndex, slot: Slot, pair: &AuthorityPair) -> Digest { - let claim = make_slot_claim(authority_idx, slot, pair); - Digest { logs: vec![DigestItem::from(&claim)] } -} - -pub fn initialize_block( - number: u64, - slot: Slot, - parent_hash: H256, - pair: &AuthorityPair, -) -> Digest { - let digest = make_digest(0, slot, pair); - System::reset_events(); - System::initialize(&number, &parent_hash, &digest); - Sassafras::on_initialize(number); - digest -} - -pub fn finalize_block(number: u64) -> Header { - Sassafras::on_finalize(number); - System::finalize() -} - -/// Progress the pallet state up to the given block `number` and `slot`. -pub fn go_to_block(number: u64, slot: Slot, pair: &AuthorityPair) -> Digest { - Sassafras::on_finalize(System::block_number()); - let parent_hash = System::finalize().hash(); - - let digest = make_digest(0, slot, pair); - - System::reset_events(); - System::initialize(&number, &parent_hash, &digest); - Sassafras::on_initialize(number); - - digest -} - -/// Progress the pallet state up to the given block `number`. -/// Slots will grow linearly accordingly to blocks. -pub fn progress_to_block(number: u64, pair: &AuthorityPair) -> Option { - let mut slot = Sassafras::current_slot() + 1; - let mut digest = None; - for i in System::block_number() + 1..=number { - let dig = go_to_block(i, slot, pair); - digest = Some(dig); - slot = slot + 1; - } - digest -} diff --git a/substrate/frame/sassafras-old/src/tests.rs b/substrate/frame/sassafras-old/src/tests.rs deleted file mode 100644 index ec3425cce7bf..000000000000 --- a/substrate/frame/sassafras-old/src/tests.rs +++ /dev/null @@ -1,874 +0,0 @@ -// 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. - -//! Tests for Sassafras pallet. - -use crate::*; -use mock::*; - -use sp_consensus_sassafras::Slot; - -fn h2b(hex: &str) -> [u8; N] { - array_bytes::hex2array_unchecked(hex) -} - -fn b2h(bytes: [u8; N]) -> String { - array_bytes::bytes2hex("", &bytes) -} - -#[test] -fn genesis_values_assumptions_check() { - new_test_ext(3).execute_with(|| { - assert_eq!(Sassafras::authorities().len(), 3); - assert_eq!(Sassafras::config(), TEST_EPOCH_CONFIGURATION); - }); -} - -#[test] -fn post_genesis_randomness_initialization() { - let (pairs, mut ext) = new_test_ext_with_pairs(1, false); - let pair = &pairs[0]; - - ext.execute_with(|| { - assert_eq!(Sassafras::randomness(), [0; 32]); - assert_eq!(Sassafras::next_randomness(), [0; 32]); - assert_eq!(Sassafras::randomness_accumulator(), [0; 32]); - - // Test the values with a zero genesis block hash - let _ = initialize_block(1, 123.into(), [0x00; 32].into(), pair); - - assert_eq!(Sassafras::randomness(), [0; 32]); - println!("[DEBUG] {}", b2h(Sassafras::next_randomness())); - assert_eq!( - Sassafras::next_randomness(), - h2b("b9497550deeeb4adc134555930de61968a0558f8947041eb515b2f5fa68ffaf7") - ); - println!("[DEBUG] {}", b2h(Sassafras::randomness_accumulator())); - assert_eq!( - Sassafras::randomness_accumulator(), - h2b("febcc7fe9539fe17ed29f525831394edfb30b301755dc9bd91584a1f065faf87") - ); - let (id1, _) = make_ticket_bodies(1, Some(pair))[0]; - - // Reset what is relevant - NextRandomness::::set([0; 32]); - RandomnessAccumulator::::set([0; 32]); - - // Test the values with a non-zero genesis block hash - let _ = initialize_block(1, 123.into(), [0xff; 32].into(), pair); - - assert_eq!(Sassafras::randomness(), [0; 32]); - println!("[DEBUG] {}", b2h(Sassafras::next_randomness())); - assert_eq!( - Sassafras::next_randomness(), - h2b("51c1e3b3a73d2043b3cabae98ff27bdd4aad8967c21ecda7b9465afaa0e70f37") - ); - println!("[DEBUG] {}", b2h(Sassafras::randomness_accumulator())); - assert_eq!( - Sassafras::randomness_accumulator(), - h2b("466bf3007f2e17bffee0b3c42c90f33d654f5ff61eff28b0cc650825960abd52") - ); - let (id2, _) = make_ticket_bodies(1, Some(pair))[0]; - - // Ticket ids should be different when next epoch randomness is different - assert_ne!(id1, id2); - - // Reset what is relevant - NextRandomness::::set([0; 32]); - RandomnessAccumulator::::set([0; 32]); - - // Test the values with a non-zero genesis block hash - let _ = initialize_block(1, 321.into(), [0x00; 32].into(), pair); - - println!("[DEBUG] {}", b2h(Sassafras::next_randomness())); - assert_eq!( - Sassafras::next_randomness(), - h2b("d85d84a54f79453000eb62e8a17b30149bd728d3232bc2787a89d51dc9a36008") - ); - println!("[DEBUG] {}", b2h(Sassafras::randomness_accumulator())); - assert_eq!( - Sassafras::randomness_accumulator(), - h2b("8a035eed02b5b8642b1515ed19752df8df156627aea45c4ef6e3efa88be9a74d") - ); - let (id2, _) = make_ticket_bodies(1, Some(pair))[0]; - - // Ticket ids should be different when next epoch randomness is different - assert_ne!(id1, id2); - }); -} - -// Tests if the sorted tickets are assigned to each slot outside-in. -#[test] -fn slot_ticket_id_outside_in_fetch() { - let genesis_slot = Slot::from(100); - let tickets_count = 6; - - // Current epoch tickets - let curr_tickets: Vec = (0..tickets_count).map(|i| i as TicketId).collect(); - - // Next epoch tickets - let next_tickets: Vec = - (0..tickets_count - 1).map(|i| (i + tickets_count) as TicketId).collect(); - - new_test_ext(0).execute_with(|| { - // Some corner cases - TicketsIds::::insert((0, 0_u32), 1_u128); - - // Cleanup - (0..3).for_each(|i| TicketsIds::::remove((0, i as u32))); - - curr_tickets - .iter() - .enumerate() - .for_each(|(i, id)| TicketsIds::::insert((0, i as u32), id)); - - next_tickets - .iter() - .enumerate() - .for_each(|(i, id)| TicketsIds::::insert((1, i as u32), id)); - - TicketsMeta::::set(TicketsMetadata { - tickets_count: [curr_tickets.len() as u32, next_tickets.len() as u32], - unsorted_tickets_count: 0, - }); - - // Before importing the first block the pallet always return `None` - // This is a kind of special hardcoded case that should never happen in practice - // as the first thing the pallet does is to initialize the genesis slot. - - assert_eq!(Sassafras::slot_ticket_id(0.into()), None); - assert_eq!(Sassafras::slot_ticket_id(genesis_slot + 0), None); - assert_eq!(Sassafras::slot_ticket_id(genesis_slot + 1), None); - assert_eq!(Sassafras::slot_ticket_id(genesis_slot + 100), None); - - // Initialize genesis slot.. - GenesisSlot::::set(genesis_slot); - frame_system::Pallet::::set_block_number(One::one()); - - // Try to fetch a ticket for a slot before current epoch. - assert_eq!(Sassafras::slot_ticket_id(0.into()), None); - - // Current epoch tickets. - assert_eq!(Sassafras::slot_ticket_id(genesis_slot + 0), Some(curr_tickets[1])); - assert_eq!(Sassafras::slot_ticket_id(genesis_slot + 1), Some(curr_tickets[3])); - assert_eq!(Sassafras::slot_ticket_id(genesis_slot + 2), Some(curr_tickets[5])); - assert_eq!(Sassafras::slot_ticket_id(genesis_slot + 3), None); - assert_eq!(Sassafras::slot_ticket_id(genesis_slot + 4), None); - assert_eq!(Sassafras::slot_ticket_id(genesis_slot + 5), None); - assert_eq!(Sassafras::slot_ticket_id(genesis_slot + 6), None); - assert_eq!(Sassafras::slot_ticket_id(genesis_slot + 7), Some(curr_tickets[4])); - assert_eq!(Sassafras::slot_ticket_id(genesis_slot + 8), Some(curr_tickets[2])); - assert_eq!(Sassafras::slot_ticket_id(genesis_slot + 9), Some(curr_tickets[0])); - - // Next epoch tickets (note that only 5 tickets are available) - assert_eq!(Sassafras::slot_ticket_id(genesis_slot + 10), Some(next_tickets[1])); - assert_eq!(Sassafras::slot_ticket_id(genesis_slot + 11), Some(next_tickets[3])); - assert_eq!(Sassafras::slot_ticket_id(genesis_slot + 12), None); - assert_eq!(Sassafras::slot_ticket_id(genesis_slot + 13), None); - assert_eq!(Sassafras::slot_ticket_id(genesis_slot + 14), None); - assert_eq!(Sassafras::slot_ticket_id(genesis_slot + 15), None); - assert_eq!(Sassafras::slot_ticket_id(genesis_slot + 16), None); - assert_eq!(Sassafras::slot_ticket_id(genesis_slot + 17), Some(next_tickets[4])); - assert_eq!(Sassafras::slot_ticket_id(genesis_slot + 18), Some(next_tickets[2])); - assert_eq!(Sassafras::slot_ticket_id(genesis_slot + 19), Some(next_tickets[0])); - - // Try to fetch the tickets for slots beyond the next epoch. - assert_eq!(Sassafras::slot_ticket_id(genesis_slot + 20), None); - assert_eq!(Sassafras::slot_ticket_id(genesis_slot + 42), None); - }); -} - -// Different test for outside-in test with more focus on corner case correctness. -#[test] -fn slot_ticket_id_outside_in_fetch_corner_cases() { - new_test_ext(0).execute_with(|| { - frame_system::Pallet::::set_block_number(One::one()); - - let mut meta = TicketsMetadata { tickets_count: [0, 0], unsorted_tickets_count: 0 }; - let curr_epoch_idx = EpochIndex::::get(); - - let mut epoch_test = |epoch_idx| { - let tag = (epoch_idx & 1) as u8; - let epoch_start = Sassafras::epoch_start(epoch_idx); - - // cleanup - meta.tickets_count = [0, 0]; - TicketsMeta::::set(meta); - assert!((0..10).all(|i| Sassafras::slot_ticket_id((epoch_start + i).into()).is_none())); - - meta.tickets_count[tag as usize] += 1; - TicketsMeta::::set(meta); - TicketsIds::::insert((tag, 0_u32), 1_u128); - assert_eq!(Sassafras::slot_ticket_id((epoch_start + 9).into()), Some(1_u128)); - assert!((0..9).all(|i| Sassafras::slot_ticket_id((epoch_start + i).into()).is_none())); - - meta.tickets_count[tag as usize] += 1; - TicketsMeta::::set(meta); - TicketsIds::::insert((tag, 1_u32), 2_u128); - assert_eq!(Sassafras::slot_ticket_id((epoch_start + 0).into()), Some(2_u128)); - assert!((1..9).all(|i| Sassafras::slot_ticket_id((epoch_start + i).into()).is_none())); - - meta.tickets_count[tag as usize] += 2; - TicketsMeta::::set(meta); - TicketsIds::::insert((tag, 2_u32), 3_u128); - assert_eq!(Sassafras::slot_ticket_id((epoch_start + 8).into()), Some(3_u128)); - assert!((1..8).all(|i| Sassafras::slot_ticket_id((epoch_start + i).into()).is_none())); - }; - - // Even epoch - epoch_test(curr_epoch_idx); - epoch_test(curr_epoch_idx + 1); - }); -} - -#[test] -fn on_first_block_after_genesis() { - let (pairs, mut ext) = new_test_ext_with_pairs(4, false); - - ext.execute_with(|| { - let start_slot = Slot::from(100); - let start_block = 1; - - let digest = initialize_block(start_block, start_slot, Default::default(), &pairs[0]); - - let common_assertions = || { - assert_eq!(Sassafras::genesis_slot(), start_slot); - assert_eq!(Sassafras::current_slot(), start_slot); - assert_eq!(Sassafras::epoch_index(), 0); - assert_eq!(Sassafras::current_epoch_start(), start_slot); - assert_eq!(Sassafras::current_slot_index(), 0); - assert_eq!(Sassafras::randomness(), [0; 32]); - println!("[DEBUG] {}", b2h(Sassafras::next_randomness())); - assert_eq!( - Sassafras::next_randomness(), - h2b("a49592ef190b96f3eb87bde4c8355e33df28c75006156e8c81998158de2ed49e") - ); - }; - - // Post-initialization status - - assert!(ClaimTemporaryData::::exists()); - common_assertions(); - println!("[DEBUG] {}", b2h(Sassafras::randomness_accumulator())); - assert_eq!( - Sassafras::randomness_accumulator(), - h2b("f0d42f6b7c0d157ecbd788be44847b80a96c290c04b5dfa5d1d40c98aa0c04ed") - ); - - let header = finalize_block(start_block); - - // Post-finalization status - - assert!(!ClaimTemporaryData::::exists()); - common_assertions(); - println!("[DEBUG] {}", b2h(Sassafras::randomness_accumulator())); - assert_eq!( - Sassafras::randomness_accumulator(), - h2b("9f2b9fd19a772c34d437dcd8b84a927e73a5cb43d3d1cd00093223d60d2b4843"), - ); - - // Header data check - - assert_eq!(header.digest.logs.len(), 2); - assert_eq!(header.digest.logs[0], digest.logs[0]); - - // Genesis epoch start deposits consensus - let consensus_log = sp_consensus_sassafras::digests::ConsensusLog::NextEpochData( - sp_consensus_sassafras::digests::NextEpochDescriptor { - authorities: Sassafras::next_authorities().into_inner(), - randomness: Sassafras::next_randomness(), - config: None, - }, - ); - let consensus_digest = DigestItem::Consensus(SASSAFRAS_ENGINE_ID, consensus_log.encode()); - assert_eq!(header.digest.logs[1], consensus_digest) - }) -} - -#[test] -fn on_normal_block() { - let (pairs, mut ext) = new_test_ext_with_pairs(4, false); - let start_slot = Slot::from(100); - let start_block = 1; - let end_block = start_block + 1; - - ext.execute_with(|| { - initialize_block(start_block, start_slot, Default::default(), &pairs[0]); - - // We don't want to trigger an epoch change in this test. - let epoch_length = Sassafras::epoch_length() as u64; - assert!(epoch_length > end_block); - - // Progress to block 2 - let digest = progress_to_block(end_block, &pairs[0]).unwrap(); - - let common_assertions = || { - assert_eq!(Sassafras::genesis_slot(), start_slot); - assert_eq!(Sassafras::current_slot(), start_slot + 1); - assert_eq!(Sassafras::epoch_index(), 0); - assert_eq!(Sassafras::current_epoch_start(), start_slot); - assert_eq!(Sassafras::current_slot_index(), 1); - assert_eq!(Sassafras::randomness(), [0; 32]); - println!("[DEBUG] {}", b2h(Sassafras::next_randomness())); - assert_eq!( - Sassafras::next_randomness(), - h2b("a49592ef190b96f3eb87bde4c8355e33df28c75006156e8c81998158de2ed49e") - ); - }; - - // Post-initialization status - - assert!(ClaimTemporaryData::::exists()); - common_assertions(); - println!("[DEBUG] {}", b2h(Sassafras::randomness_accumulator())); - assert_eq!( - Sassafras::randomness_accumulator(), - h2b("9f2b9fd19a772c34d437dcd8b84a927e73a5cb43d3d1cd00093223d60d2b4843"), - ); - - let header = finalize_block(end_block); - - // Post-finalization status - - assert!(!ClaimTemporaryData::::exists()); - common_assertions(); - assert_eq!( - Sassafras::randomness_accumulator(), - h2b("be9261adb9686dfd3f23f8a276b7acc7f4beb3137070beb64c282ac22d84cbf0"), - ); - - // Header data check - - assert_eq!(header.digest.logs.len(), 1); - assert_eq!(header.digest.logs[0], digest.logs[0]); - }); -} - -#[test] -fn produce_epoch_change_digest_no_config() { - let (pairs, mut ext) = new_test_ext_with_pairs(4, false); - - ext.execute_with(|| { - let start_slot = Slot::from(100); - let start_block = 1; - - initialize_block(start_block, start_slot, Default::default(), &pairs[0]); - - // We want to trigger an epoch change in this test. - let epoch_length = Sassafras::epoch_length() as u64; - let end_block = start_block + epoch_length; - - let digest = progress_to_block(end_block, &pairs[0]).unwrap(); - - let common_assertions = || { - assert_eq!(Sassafras::genesis_slot(), start_slot); - assert_eq!(Sassafras::current_slot(), start_slot + epoch_length); - assert_eq!(Sassafras::epoch_index(), 1); - assert_eq!(Sassafras::current_epoch_start(), start_slot + epoch_length); - assert_eq!(Sassafras::current_slot_index(), 0); - println!("[DEBUG] {}", b2h(Sassafras::randomness())); - assert_eq!( - Sassafras::randomness(), - h2b("a49592ef190b96f3eb87bde4c8355e33df28c75006156e8c81998158de2ed49e") - ); - }; - - // Post-initialization status - - assert!(ClaimTemporaryData::::exists()); - common_assertions(); - println!("[DEBUG] {}", b2h(Sassafras::next_randomness())); - assert_eq!( - Sassafras::next_randomness(), - h2b("d3a18b857af6ecc7b52f047107e684fff0058b5722d540a296d727e37eaa55b3"), - ); - println!("[DEBUG] {}", b2h(Sassafras::randomness_accumulator())); - assert_eq!( - Sassafras::randomness_accumulator(), - h2b("bf0f1228f4ff953c8c1bda2cceb668bf86ea05d7ae93e26d021c9690995d5279"), - ); - - let header = finalize_block(end_block); - - // Post-finalization status - - assert!(!ClaimTemporaryData::::exists()); - common_assertions(); - println!("[DEBUG] {}", b2h(Sassafras::next_randomness())); - assert_eq!( - Sassafras::next_randomness(), - h2b("d3a18b857af6ecc7b52f047107e684fff0058b5722d540a296d727e37eaa55b3"), - ); - println!("[DEBUG] {}", b2h(Sassafras::randomness_accumulator())); - assert_eq!( - Sassafras::randomness_accumulator(), - h2b("8a1ceb346036c386d021264b10912c8b656799668004c4a487222462b394cd89"), - ); - - // Header data check - - assert_eq!(header.digest.logs.len(), 2); - assert_eq!(header.digest.logs[0], digest.logs[0]); - // Deposits consensus log on epoch change - let consensus_log = sp_consensus_sassafras::digests::ConsensusLog::NextEpochData( - sp_consensus_sassafras::digests::NextEpochDescriptor { - authorities: Sassafras::next_authorities().into_inner(), - randomness: Sassafras::next_randomness(), - config: None, - }, - ); - let consensus_digest = DigestItem::Consensus(SASSAFRAS_ENGINE_ID, consensus_log.encode()); - assert_eq!(header.digest.logs[1], consensus_digest) - }) -} - -#[test] -fn produce_epoch_change_digest_with_config() { - let (pairs, mut ext) = new_test_ext_with_pairs(4, false); - - ext.execute_with(|| { - let start_slot = Slot::from(100); - let start_block = 1; - - initialize_block(start_block, start_slot, Default::default(), &pairs[0]); - - let config = EpochConfiguration { redundancy_factor: 1, attempts_number: 123 }; - Sassafras::plan_config_change(RuntimeOrigin::root(), config).unwrap(); - - // We want to trigger an epoch change in this test. - let epoch_length = Sassafras::epoch_length() as u64; - let end_block = start_block + epoch_length; - - let digest = progress_to_block(end_block, &pairs[0]).unwrap(); - - let header = finalize_block(end_block); - - // Header data check. - // Skip pallet status checks that were already performed by other tests. - - assert_eq!(header.digest.logs.len(), 2); - assert_eq!(header.digest.logs[0], digest.logs[0]); - // Deposits consensus log on epoch change - let consensus_log = sp_consensus_sassafras::digests::ConsensusLog::NextEpochData( - sp_consensus_sassafras::digests::NextEpochDescriptor { - authorities: Sassafras::next_authorities().into_inner(), - randomness: Sassafras::next_randomness(), - config: Some(config), - }, - ); - let consensus_digest = DigestItem::Consensus(SASSAFRAS_ENGINE_ID, consensus_log.encode()); - assert_eq!(header.digest.logs[1], consensus_digest) - }) -} - -#[test] -fn segments_incremental_sort_works() { - let (pairs, mut ext) = new_test_ext_with_pairs(1, false); - let pair = &pairs[0]; - let segments_count = 14; - let start_slot = Slot::from(100); - let start_block = 1; - - ext.execute_with(|| { - let epoch_length = Sassafras::epoch_length() as u64; - // -3 just to have the last segment not full... - let submitted_tickets_count = segments_count * SEGMENT_MAX_SIZE - 3; - - initialize_block(start_block, start_slot, Default::default(), pair); - - // Manually populate the segments to skip the threshold check - let mut tickets = make_ticket_bodies(submitted_tickets_count, None); - persist_next_epoch_tickets_as_segments(&tickets); - - // Proceed to half of the epoch (sortition should not have been started yet) - let half_epoch_block = start_block + epoch_length / 2; - progress_to_block(half_epoch_block, pair); - - let mut unsorted_tickets_count = submitted_tickets_count; - - // Check that next epoch tickets sortition is not started yet - let meta = TicketsMeta::::get(); - assert_eq!(meta.unsorted_tickets_count, unsorted_tickets_count); - assert_eq!(meta.tickets_count, [0, 0]); - - // Follow the incremental sortition block by block - - progress_to_block(half_epoch_block + 1, pair); - unsorted_tickets_count -= 3 * SEGMENT_MAX_SIZE - 3; - let meta = TicketsMeta::::get(); - assert_eq!(meta.unsorted_tickets_count, unsorted_tickets_count,); - assert_eq!(meta.tickets_count, [0, 0]); - - progress_to_block(half_epoch_block + 2, pair); - unsorted_tickets_count -= 3 * SEGMENT_MAX_SIZE; - let meta = TicketsMeta::::get(); - assert_eq!(meta.unsorted_tickets_count, unsorted_tickets_count); - assert_eq!(meta.tickets_count, [0, 0]); - - progress_to_block(half_epoch_block + 3, pair); - unsorted_tickets_count -= 3 * SEGMENT_MAX_SIZE; - let meta = TicketsMeta::::get(); - assert_eq!(meta.unsorted_tickets_count, unsorted_tickets_count); - assert_eq!(meta.tickets_count, [0, 0]); - - progress_to_block(half_epoch_block + 4, pair); - unsorted_tickets_count -= 3 * SEGMENT_MAX_SIZE; - let meta = TicketsMeta::::get(); - assert_eq!(meta.unsorted_tickets_count, unsorted_tickets_count); - assert_eq!(meta.tickets_count, [0, 0]); - - let header = finalize_block(half_epoch_block + 4); - - // Sort should be finished now. - // Check that next epoch tickets count have the correct value. - // Bigger ticket ids were discarded during sortition. - unsorted_tickets_count -= 2 * SEGMENT_MAX_SIZE; - assert_eq!(unsorted_tickets_count, 0); - let meta = TicketsMeta::::get(); - assert_eq!(meta.unsorted_tickets_count, unsorted_tickets_count); - assert_eq!(meta.tickets_count, [0, epoch_length as u32]); - // Epoch change log should have been pushed as well - assert_eq!(header.digest.logs.len(), 1); - // No tickets for the current epoch - assert_eq!(TicketsIds::::get((0, 0)), None); - - // Check persistence of "winning" tickets - tickets.sort_by_key(|t| t.0); - (0..epoch_length as usize).into_iter().for_each(|i| { - let id = TicketsIds::::get((1, i as u32)).unwrap(); - let body = TicketsData::::get(id).unwrap(); - assert_eq!((id, body), tickets[i]); - }); - // Check removal of "loosing" tickets - (epoch_length as usize..tickets.len()).into_iter().for_each(|i| { - assert!(TicketsIds::::get((1, i as u32)).is_none()); - assert!(TicketsData::::get(tickets[i].0).is_none()); - }); - - // The next block will be the first produced on the new epoch. - // At this point the tickets are found already sorted and ready to be used. - let slot = Sassafras::current_slot() + 1; - let number = System::block_number() + 1; - initialize_block(number, slot, header.hash(), pair); - let header = finalize_block(number); - // Epoch changes digest is also produced - assert_eq!(header.digest.logs.len(), 2); - }); -} - -#[test] -fn tickets_fetch_works_after_epoch_change() { - let (pairs, mut ext) = new_test_ext_with_pairs(4, false); - let pair = &pairs[0]; - let start_slot = Slot::from(100); - let start_block = 1; - let submitted_tickets = 300; - - ext.execute_with(|| { - initialize_block(start_block, start_slot, Default::default(), pair); - - // We don't want to trigger an epoch change in this test. - let epoch_length = Sassafras::epoch_length() as u64; - assert!(epoch_length > 2); - progress_to_block(2, &pairs[0]).unwrap(); - - // Persist tickets as three different segments. - let tickets = make_ticket_bodies(submitted_tickets, None); - persist_next_epoch_tickets_as_segments(&tickets); - - let meta = TicketsMeta::::get(); - assert_eq!(meta.unsorted_tickets_count, submitted_tickets); - assert_eq!(meta.tickets_count, [0, 0]); - - // Progress up to the last epoch slot (do not enact epoch change) - progress_to_block(epoch_length, &pairs[0]).unwrap(); - - // At this point next epoch tickets should have been sorted and ready to be used - let meta = TicketsMeta::::get(); - assert_eq!(meta.unsorted_tickets_count, 0); - assert_eq!(meta.tickets_count, [0, epoch_length as u32]); - - // Compute and sort the tickets ids (aka tickets scores) - let mut expected_ids: Vec<_> = tickets.into_iter().map(|(id, _)| id).collect(); - expected_ids.sort(); - expected_ids.truncate(epoch_length as usize); - - // Check if we can fetch next epoch tickets ids (outside-in). - let slot = Sassafras::current_slot(); - assert_eq!(Sassafras::slot_ticket_id(slot + 1).unwrap(), expected_ids[1]); - assert_eq!(Sassafras::slot_ticket_id(slot + 2).unwrap(), expected_ids[3]); - assert_eq!(Sassafras::slot_ticket_id(slot + 3).unwrap(), expected_ids[5]); - assert_eq!(Sassafras::slot_ticket_id(slot + 4).unwrap(), expected_ids[7]); - assert_eq!(Sassafras::slot_ticket_id(slot + 7).unwrap(), expected_ids[6]); - assert_eq!(Sassafras::slot_ticket_id(slot + 8).unwrap(), expected_ids[4]); - assert_eq!(Sassafras::slot_ticket_id(slot + 9).unwrap(), expected_ids[2]); - assert_eq!(Sassafras::slot_ticket_id(slot + 10).unwrap(), expected_ids[0]); - assert!(Sassafras::slot_ticket_id(slot + 11).is_none()); - - // Enact epoch change by progressing one more block - - progress_to_block(epoch_length + 1, &pairs[0]).unwrap(); - - let meta = TicketsMeta::::get(); - assert_eq!(meta.unsorted_tickets_count, 0); - assert_eq!(meta.tickets_count, [0, 10]); - - // Check if we can fetch current epoch tickets ids (outside-in). - let slot = Sassafras::current_slot(); - assert_eq!(Sassafras::slot_ticket_id(slot).unwrap(), expected_ids[1]); - assert_eq!(Sassafras::slot_ticket_id(slot + 1).unwrap(), expected_ids[3]); - assert_eq!(Sassafras::slot_ticket_id(slot + 2).unwrap(), expected_ids[5]); - assert_eq!(Sassafras::slot_ticket_id(slot + 3).unwrap(), expected_ids[7]); - assert_eq!(Sassafras::slot_ticket_id(slot + 6).unwrap(), expected_ids[6]); - assert_eq!(Sassafras::slot_ticket_id(slot + 7).unwrap(), expected_ids[4]); - assert_eq!(Sassafras::slot_ticket_id(slot + 8).unwrap(), expected_ids[2]); - assert_eq!(Sassafras::slot_ticket_id(slot + 9).unwrap(), expected_ids[0]); - assert!(Sassafras::slot_ticket_id(slot + 10).is_none()); - - // Enact another epoch change, for which we don't have any ticket - progress_to_block(2 * epoch_length + 1, &pairs[0]).unwrap(); - let meta = TicketsMeta::::get(); - assert_eq!(meta.unsorted_tickets_count, 0); - assert_eq!(meta.tickets_count, [0, 0]); - }); -} - -#[test] -fn block_allowed_to_skip_epochs() { - let (pairs, mut ext) = new_test_ext_with_pairs(4, false); - let pair = &pairs[0]; - let start_slot = Slot::from(100); - let start_block = 1; - - ext.execute_with(|| { - let epoch_length = Sassafras::epoch_length() as u64; - - initialize_block(start_block, start_slot, Default::default(), pair); - - let tickets = make_ticket_bodies(3, Some(pair)); - persist_next_epoch_tickets(&tickets); - - let next_random = Sassafras::next_randomness(); - - // We want to skip 3 epochs in this test. - let offset = 4 * epoch_length; - go_to_block(start_block + offset, start_slot + offset, &pairs[0]); - - // Post-initialization status - - assert!(ClaimTemporaryData::::exists()); - assert_eq!(Sassafras::genesis_slot(), start_slot); - assert_eq!(Sassafras::current_slot(), start_slot + offset); - assert_eq!(Sassafras::epoch_index(), 4); - assert_eq!(Sassafras::current_epoch_start(), start_slot + offset); - assert_eq!(Sassafras::current_slot_index(), 0); - - // Tickets data has been discarded - assert_eq!(TicketsMeta::::get(), TicketsMetadata::default()); - assert!(tickets.iter().all(|(id, _)| TicketsData::::get(id).is_none())); - assert_eq!(SortedCandidates::::get().len(), 0); - - // We used the last known next epoch randomness as a fallback - assert_eq!(next_random, Sassafras::randomness()); - }); -} - -#[test] -fn obsolete_tickets_are_removed_on_epoch_change() { - let (pairs, mut ext) = new_test_ext_with_pairs(4, false); - let pair = &pairs[0]; - let start_slot = Slot::from(100); - let start_block = 1; - - ext.execute_with(|| { - let epoch_length = Sassafras::epoch_length() as u64; - - initialize_block(start_block, start_slot, Default::default(), pair); - - let tickets = make_ticket_bodies(10, Some(pair)); - let mut epoch1_tickets = tickets[..4].to_vec(); - let mut epoch2_tickets = tickets[4..].to_vec(); - - // Persist some tickets for next epoch (N) - persist_next_epoch_tickets(&epoch1_tickets); - assert_eq!(TicketsMeta::::get().tickets_count, [0, 4]); - // Check next epoch tickets presence - epoch1_tickets.sort_by_key(|t| t.0); - (0..epoch1_tickets.len()).into_iter().for_each(|i| { - let id = TicketsIds::::get((1, i as u32)).unwrap(); - let body = TicketsData::::get(id).unwrap(); - assert_eq!((id, body), epoch1_tickets[i]); - }); - - // Advance one epoch to enact the tickets - go_to_block(start_block + epoch_length, start_slot + epoch_length, pair); - assert_eq!(TicketsMeta::::get().tickets_count, [0, 4]); - - // Persist some tickets for next epoch (N+1) - persist_next_epoch_tickets(&epoch2_tickets); - assert_eq!(TicketsMeta::::get().tickets_count, [6, 4]); - epoch2_tickets.sort_by_key(|t| t.0); - // Check for this epoch and next epoch tickets presence - (0..epoch1_tickets.len()).into_iter().for_each(|i| { - let id = TicketsIds::::get((1, i as u32)).unwrap(); - let body = TicketsData::::get(id).unwrap(); - assert_eq!((id, body), epoch1_tickets[i]); - }); - (0..epoch2_tickets.len()).into_iter().for_each(|i| { - let id = TicketsIds::::get((0, i as u32)).unwrap(); - let body = TicketsData::::get(id).unwrap(); - assert_eq!((id, body), epoch2_tickets[i]); - }); - - // Advance to epoch 2 and check for cleanup - - go_to_block(start_block + 2 * epoch_length, start_slot + 2 * epoch_length, pair); - assert_eq!(TicketsMeta::::get().tickets_count, [6, 0]); - - (0..epoch1_tickets.len()).into_iter().for_each(|i| { - let id = TicketsIds::::get((1, i as u32)).unwrap(); - assert!(TicketsData::::get(id).is_none()); - }); - (0..epoch2_tickets.len()).into_iter().for_each(|i| { - let id = TicketsIds::::get((0, i as u32)).unwrap(); - let body = TicketsData::::get(id).unwrap(); - assert_eq!((id, body), epoch2_tickets[i]); - }); - }) -} - -const TICKETS_FILE: &str = "src/data/25_tickets_100_auths.bin"; - -fn data_read(filename: &str) -> T { - use std::{fs::File, io::Read}; - let mut file = File::open(filename).unwrap(); - let mut buf = Vec::new(); - file.read_to_end(&mut buf).unwrap(); - T::decode(&mut &buf[..]).unwrap() -} - -fn data_write(filename: &str, data: T) { - use std::{fs::File, io::Write}; - let mut file = File::create(filename).unwrap(); - let buf = data.encode(); - file.write_all(&buf).unwrap(); -} - -// We don't want to implement anything secure here. -// Just a trivial shuffle for the tests. -fn trivial_fisher_yates_shuffle(vector: &mut Vec, random_seed: u64) { - let mut rng = random_seed as usize; - for i in (1..vector.len()).rev() { - let j = rng % (i + 1); - vector.swap(i, j); - rng = (rng.wrapping_mul(6364793005) + 1) as usize; // Some random number generation - } -} - -// For this test we use a set of pre-constructed tickets from a file. -// Creating a large set of tickets on the fly takes time, and may be annoying -// for test execution. -// -// A valid ring-context is required for this test since we are passing through the -// `submit_ticket` call which tests for ticket validity. -#[test] -fn submit_tickets_with_ring_proof_check_works() { - use sp_core::Pair as _; - // env_logger::init(); - - let (authorities, mut tickets): (Vec, Vec) = - data_read(TICKETS_FILE); - - // Also checks that duplicates are discarded - tickets.extend(tickets.clone()); - trivial_fisher_yates_shuffle(&mut tickets, 321); - - let (pairs, mut ext) = new_test_ext_with_pairs(authorities.len(), true); - let pair = &pairs[0]; - // Check if deserialized data has been generated for the correct set of authorities... - assert!(authorities.iter().zip(pairs.iter()).all(|(auth, pair)| auth == &pair.public())); - - ext.execute_with(|| { - let start_slot = Slot::from(0); - let start_block = 1; - - // Tweak the config to discard ~half of the tickets. - let mut config = EpochConfig::::get(); - config.redundancy_factor = 25; - EpochConfig::::set(config); - - initialize_block(start_block, start_slot, Default::default(), pair); - NextRandomness::::set([0; 32]); - - // Check state before tickets submission - assert_eq!( - TicketsMeta::::get(), - TicketsMetadata { unsorted_tickets_count: 0, tickets_count: [0, 0] }, - ); - - // Submit the tickets - let max_tickets_per_call = Sassafras::epoch_length() as usize; - tickets.chunks(max_tickets_per_call).for_each(|chunk| { - let chunk = BoundedVec::truncate_from(chunk.to_vec()); - Sassafras::submit_tickets(RuntimeOrigin::none(), chunk).unwrap(); - }); - - // Check state after submission - assert_eq!( - TicketsMeta::::get(), - TicketsMetadata { unsorted_tickets_count: 16, tickets_count: [0, 0] }, - ); - assert_eq!(UnsortedSegments::::get(0).len(), 16); - assert_eq!(UnsortedSegments::::get(1).len(), 0); - - finalize_block(start_block); - }) -} - -#[test] -#[ignore = "test tickets data generator"] -fn make_tickets_data() { - use super::*; - use sp_core::crypto::Pair; - - // Number of authorities who produces tickets (for the sake of this test) - let tickets_authors_count = 5; - // Total number of authorities (the ring) - let authorities_count = 100; - let (pairs, mut ext) = new_test_ext_with_pairs(authorities_count, true); - - let authorities: Vec<_> = pairs.iter().map(|sk| sk.public()).collect(); - - ext.execute_with(|| { - let config = EpochConfig::::get(); - - let tickets_count = tickets_authors_count * config.attempts_number as usize; - let mut tickets = Vec::with_capacity(tickets_count); - - // Construct pre-built tickets with a well known `NextRandomness` value. - NextRandomness::::set([0; 32]); - - println!("Constructing {} tickets", tickets_count); - pairs.iter().take(tickets_authors_count).enumerate().for_each(|(i, pair)| { - let t = make_tickets(config.attempts_number, pair); - tickets.extend(t); - println!("{:.2}%", 100f32 * ((i + 1) as f32 / tickets_authors_count as f32)); - }); - - data_write(TICKETS_FILE, (authorities, tickets)); - }); -} diff --git a/substrate/frame/sassafras-old/src/weights.rs b/substrate/frame/sassafras-old/src/weights.rs deleted file mode 100644 index 32ea2d29a180..000000000000 --- a/substrate/frame/sassafras-old/src/weights.rs +++ /dev/null @@ -1,425 +0,0 @@ -// 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. - -//! Autogenerated weights for `pallet_sassafras` -//! -//! THIS FILE WAS AUTO-GENERATED USING THE SUBSTRATE BENCHMARK CLI VERSION 4.0.0-dev -//! DATE: 2023-11-16, STEPS: `20`, REPEAT: `3`, LOW RANGE: `[]`, HIGH RANGE: `[]` -//! WORST CASE MAP SIZE: `1000000` -//! HOSTNAME: `behemoth`, CPU: `AMD Ryzen Threadripper 3970X 32-Core Processor` -//! WASM-EXECUTION: `Compiled`, CHAIN: `Some("dev")`, DB CACHE: `1024` - -// Executed Command: -// ./target/release/node-template -// benchmark -// pallet -// --chain -// dev -// --pallet -// pallet_sassafras -// --extrinsic -// * -// --steps -// 20 -// --repeat -// 3 -// --output -// weights.rs -// --template -// substrate/.maintain/frame-weight-template.hbs - -#![cfg_attr(rustfmt, rustfmt_skip)] -#![allow(unused_parens)] -#![allow(unused_imports)] -#![allow(missing_docs)] - -use frame_support::{traits::Get, weights::{Weight, constants::RocksDbWeight}}; -use core::marker::PhantomData; - -/// Weight functions needed for `pallet_sassafras`. -pub trait WeightInfo { - fn on_initialize() -> Weight; - fn enact_epoch_change(x: u32, y: u32, ) -> Weight; - fn submit_tickets(x: u32, ) -> Weight; - fn plan_config_change() -> Weight; - fn update_ring_verifier(x: u32, ) -> Weight; - fn load_ring_context() -> Weight; - fn sort_segments(x: u32, ) -> Weight; -} - -/// Weights for `pallet_sassafras` using the Substrate node and recommended hardware. -pub struct SubstrateWeight(PhantomData); -impl WeightInfo for SubstrateWeight { - /// Storage: `System::Digest` (r:1 w:1) - /// Proof: `System::Digest` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) - /// Storage: `Sassafras::NextRandomness` (r:1 w:0) - /// Proof: `Sassafras::NextRandomness` (`max_values`: Some(1), `max_size`: Some(32), added: 527, mode: `MaxEncodedLen`) - /// Storage: `Sassafras::NextAuthorities` (r:1 w:0) - /// Proof: `Sassafras::NextAuthorities` (`max_values`: Some(1), `max_size`: Some(3302), added: 3797, mode: `MaxEncodedLen`) - /// Storage: `Sassafras::CurrentRandomness` (r:1 w:0) - /// Proof: `Sassafras::CurrentRandomness` (`max_values`: Some(1), `max_size`: Some(32), added: 527, mode: `MaxEncodedLen`) - /// Storage: `Sassafras::EpochIndex` (r:1 w:0) - /// Proof: `Sassafras::EpochIndex` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `MaxEncodedLen`) - /// Storage: `Sassafras::RandomnessAccumulator` (r:1 w:1) - /// Proof: `Sassafras::RandomnessAccumulator` (`max_values`: Some(1), `max_size`: Some(32), added: 527, mode: `MaxEncodedLen`) - /// Storage: `Sassafras::CurrentSlot` (r:0 w:1) - /// Proof: `Sassafras::CurrentSlot` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `MaxEncodedLen`) - /// Storage: `Sassafras::ClaimTemporaryData` (r:0 w:1) - /// Proof: `Sassafras::ClaimTemporaryData` (`max_values`: Some(1), `max_size`: Some(33), added: 528, mode: `MaxEncodedLen`) - /// Storage: `Sassafras::GenesisSlot` (r:0 w:1) - /// Proof: `Sassafras::GenesisSlot` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `MaxEncodedLen`) - fn on_initialize() -> Weight { - // Proof Size summary in bytes: - // Measured: `302` - // Estimated: `4787` - // Minimum execution time: 438_039_000 picoseconds. - Weight::from_parts(439_302_000, 4787) - .saturating_add(T::DbWeight::get().reads(6_u64)) - .saturating_add(T::DbWeight::get().writes(5_u64)) - } - /// Storage: `Sassafras::CurrentSlot` (r:1 w:0) - /// Proof: `Sassafras::CurrentSlot` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `MaxEncodedLen`) - /// Storage: `Sassafras::EpochIndex` (r:1 w:1) - /// Proof: `Sassafras::EpochIndex` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `MaxEncodedLen`) - /// Storage: `Sassafras::GenesisSlot` (r:1 w:0) - /// Proof: `Sassafras::GenesisSlot` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `MaxEncodedLen`) - /// Storage: `Sassafras::NextAuthorities` (r:1 w:1) - /// Proof: `Sassafras::NextAuthorities` (`max_values`: Some(1), `max_size`: Some(3302), added: 3797, mode: `MaxEncodedLen`) - /// Storage: `Sassafras::RingContext` (r:1 w:0) - /// Proof: `Sassafras::RingContext` (`max_values`: Some(1), `max_size`: Some(590324), added: 590819, mode: `MaxEncodedLen`) - /// Storage: `Sassafras::TicketsMeta` (r:1 w:1) - /// Proof: `Sassafras::TicketsMeta` (`max_values`: Some(1), `max_size`: Some(12), added: 507, mode: `MaxEncodedLen`) - /// Storage: `Sassafras::NextRandomness` (r:1 w:1) - /// Proof: `Sassafras::NextRandomness` (`max_values`: Some(1), `max_size`: Some(32), added: 527, mode: `MaxEncodedLen`) - /// Storage: `Sassafras::RandomnessAccumulator` (r:1 w:0) - /// Proof: `Sassafras::RandomnessAccumulator` (`max_values`: Some(1), `max_size`: Some(32), added: 527, mode: `MaxEncodedLen`) - /// Storage: `Sassafras::NextEpochConfig` (r:1 w:1) - /// Proof: `Sassafras::NextEpochConfig` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `MaxEncodedLen`) - /// Storage: `Sassafras::PendingEpochConfigChange` (r:1 w:1) - /// Proof: `Sassafras::PendingEpochConfigChange` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `MaxEncodedLen`) - /// Storage: `System::Digest` (r:1 w:1) - /// Proof: `System::Digest` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) - /// Storage: `Sassafras::SortedCandidates` (r:1 w:0) - /// Proof: `Sassafras::SortedCandidates` (`max_values`: Some(1), `max_size`: Some(3202), added: 3697, mode: `MaxEncodedLen`) - /// Storage: `Sassafras::UnsortedSegments` (r:79 w:79) - /// Proof: `Sassafras::UnsortedSegments` (`max_values`: None, `max_size`: Some(2054), added: 4529, mode: `MaxEncodedLen`) - /// Storage: `Sassafras::TicketsIds` (r:5000 w:200) - /// Proof: `Sassafras::TicketsIds` (`max_values`: None, `max_size`: Some(21), added: 2496, mode: `MaxEncodedLen`) - /// Storage: `Sassafras::Authorities` (r:0 w:1) - /// Proof: `Sassafras::Authorities` (`max_values`: Some(1), `max_size`: Some(3302), added: 3797, mode: `MaxEncodedLen`) - /// Storage: `Sassafras::TicketsData` (r:0 w:9896) - /// Proof: `Sassafras::TicketsData` (`max_values`: None, `max_size`: Some(84), added: 2559, mode: `MaxEncodedLen`) - /// Storage: `Sassafras::RingVerifierData` (r:0 w:1) - /// Proof: `Sassafras::RingVerifierData` (`max_values`: Some(1), `max_size`: Some(388), added: 883, mode: `MaxEncodedLen`) - /// Storage: `Sassafras::EpochConfig` (r:0 w:1) - /// Proof: `Sassafras::EpochConfig` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `MaxEncodedLen`) - /// Storage: `Sassafras::CurrentRandomness` (r:0 w:1) - /// Proof: `Sassafras::CurrentRandomness` (`max_values`: Some(1), `max_size`: Some(32), added: 527, mode: `MaxEncodedLen`) - /// The range of component `x` is `[1, 100]`. - /// The range of component `y` is `[1000, 5000]`. - fn enact_epoch_change(x: u32, y: u32, ) -> Weight { - // Proof Size summary in bytes: - // Measured: `594909 + x * (33 ±0) + y * (53 ±0)` - // Estimated: `593350 + x * (24 ±1) + y * (2496 ±0)` - // Minimum execution time: 121_279_846_000 picoseconds. - Weight::from_parts(94_454_851_972, 593350) - // Standard Error: 24_177_301 - .saturating_add(Weight::from_parts(8_086_191, 0).saturating_mul(x.into())) - // Standard Error: 601_053 - .saturating_add(Weight::from_parts(15_578_413, 0).saturating_mul(y.into())) - .saturating_add(T::DbWeight::get().reads(13_u64)) - .saturating_add(T::DbWeight::get().reads((1_u64).saturating_mul(y.into()))) - .saturating_add(T::DbWeight::get().writes(112_u64)) - .saturating_add(T::DbWeight::get().writes((2_u64).saturating_mul(y.into()))) - .saturating_add(Weight::from_parts(0, 24).saturating_mul(x.into())) - .saturating_add(Weight::from_parts(0, 2496).saturating_mul(y.into())) - } - /// Storage: `Sassafras::CurrentSlot` (r:1 w:0) - /// Proof: `Sassafras::CurrentSlot` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `MaxEncodedLen`) - /// Storage: `Sassafras::EpochIndex` (r:1 w:0) - /// Proof: `Sassafras::EpochIndex` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `MaxEncodedLen`) - /// Storage: `Sassafras::GenesisSlot` (r:1 w:0) - /// Proof: `Sassafras::GenesisSlot` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `MaxEncodedLen`) - /// Storage: `Sassafras::RingVerifierData` (r:1 w:0) - /// Proof: `Sassafras::RingVerifierData` (`max_values`: Some(1), `max_size`: Some(388), added: 883, mode: `MaxEncodedLen`) - /// Storage: `Sassafras::NextAuthorities` (r:1 w:0) - /// Proof: `Sassafras::NextAuthorities` (`max_values`: Some(1), `max_size`: Some(3302), added: 3797, mode: `MaxEncodedLen`) - /// Storage: `Sassafras::NextEpochConfig` (r:1 w:0) - /// Proof: `Sassafras::NextEpochConfig` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `MaxEncodedLen`) - /// Storage: `Sassafras::NextRandomness` (r:1 w:0) - /// Proof: `Sassafras::NextRandomness` (`max_values`: Some(1), `max_size`: Some(32), added: 527, mode: `MaxEncodedLen`) - /// Storage: `Sassafras::TicketsData` (r:25 w:25) - /// Proof: `Sassafras::TicketsData` (`max_values`: None, `max_size`: Some(84), added: 2559, mode: `MaxEncodedLen`) - /// Storage: `Sassafras::TicketsMeta` (r:1 w:1) - /// Proof: `Sassafras::TicketsMeta` (`max_values`: Some(1), `max_size`: Some(12), added: 507, mode: `MaxEncodedLen`) - /// Storage: `Sassafras::UnsortedSegments` (r:1 w:1) - /// Proof: `Sassafras::UnsortedSegments` (`max_values`: None, `max_size`: Some(2054), added: 4529, mode: `MaxEncodedLen`) - /// The range of component `x` is `[1, 25]`. - fn submit_tickets(x: u32, ) -> Weight { - // Proof Size summary in bytes: - // Measured: `3869` - // Estimated: `5519 + x * (2559 ±0)` - // Minimum execution time: 36_904_934_000 picoseconds. - Weight::from_parts(25_822_957_295, 5519) - // Standard Error: 11_047_832 - .saturating_add(Weight::from_parts(11_338_353_299, 0).saturating_mul(x.into())) - .saturating_add(T::DbWeight::get().reads(9_u64)) - .saturating_add(T::DbWeight::get().reads((1_u64).saturating_mul(x.into()))) - .saturating_add(T::DbWeight::get().writes(2_u64)) - .saturating_add(T::DbWeight::get().writes((1_u64).saturating_mul(x.into()))) - .saturating_add(Weight::from_parts(0, 2559).saturating_mul(x.into())) - } - /// Storage: `Sassafras::PendingEpochConfigChange` (r:0 w:1) - /// Proof: `Sassafras::PendingEpochConfigChange` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `MaxEncodedLen`) - fn plan_config_change() -> Weight { - // Proof Size summary in bytes: - // Measured: `0` - // Estimated: `0` - // Minimum execution time: 4_038_000 picoseconds. - Weight::from_parts(4_499_000, 0) - .saturating_add(T::DbWeight::get().writes(1_u64)) - } - /// Storage: `Sassafras::RingContext` (r:1 w:0) - /// Proof: `Sassafras::RingContext` (`max_values`: Some(1), `max_size`: Some(590324), added: 590819, mode: `MaxEncodedLen`) - /// Storage: `Sassafras::RingVerifierData` (r:0 w:1) - /// Proof: `Sassafras::RingVerifierData` (`max_values`: Some(1), `max_size`: Some(388), added: 883, mode: `MaxEncodedLen`) - /// The range of component `x` is `[1, 100]`. - fn update_ring_verifier(x: u32, ) -> Weight { - // Proof Size summary in bytes: - // Measured: `590485` - // Estimated: `591809` - // Minimum execution time: 105_121_424_000 picoseconds. - Weight::from_parts(105_527_334_385, 591809) - // Standard Error: 2_933_910 - .saturating_add(Weight::from_parts(96_136_261, 0).saturating_mul(x.into())) - .saturating_add(T::DbWeight::get().reads(1_u64)) - .saturating_add(T::DbWeight::get().writes(1_u64)) - } - /// Storage: `Sassafras::RingContext` (r:1 w:0) - /// Proof: `Sassafras::RingContext` (`max_values`: Some(1), `max_size`: Some(590324), added: 590819, mode: `MaxEncodedLen`) - fn load_ring_context() -> Weight { - // Proof Size summary in bytes: - // Measured: `590485` - // Estimated: `591809` - // Minimum execution time: 44_005_681_000 picoseconds. - Weight::from_parts(44_312_079_000, 591809) - .saturating_add(T::DbWeight::get().reads(1_u64)) - } - /// Storage: `Sassafras::SortedCandidates` (r:1 w:0) - /// Proof: `Sassafras::SortedCandidates` (`max_values`: Some(1), `max_size`: Some(3202), added: 3697, mode: `MaxEncodedLen`) - /// Storage: `Sassafras::UnsortedSegments` (r:100 w:100) - /// Proof: `Sassafras::UnsortedSegments` (`max_values`: None, `max_size`: Some(2054), added: 4529, mode: `MaxEncodedLen`) - /// Storage: `Sassafras::TicketsIds` (r:0 w:200) - /// Proof: `Sassafras::TicketsIds` (`max_values`: None, `max_size`: Some(21), added: 2496, mode: `MaxEncodedLen`) - /// Storage: `Sassafras::TicketsData` (r:0 w:12600) - /// Proof: `Sassafras::TicketsData` (`max_values`: None, `max_size`: Some(84), added: 2559, mode: `MaxEncodedLen`) - /// The range of component `x` is `[1, 100]`. - fn sort_segments(x: u32, ) -> Weight { - // Proof Size summary in bytes: - // Measured: `222 + x * (2060 ±0)` - // Estimated: `4687 + x * (4529 ±0)` - // Minimum execution time: 183_501_000 picoseconds. - Weight::from_parts(183_501_000, 4687) - // Standard Error: 1_426_363 - .saturating_add(Weight::from_parts(169_156_241, 0).saturating_mul(x.into())) - .saturating_add(T::DbWeight::get().reads(1_u64)) - .saturating_add(T::DbWeight::get().reads((1_u64).saturating_mul(x.into()))) - .saturating_add(T::DbWeight::get().writes((129_u64).saturating_mul(x.into()))) - .saturating_add(Weight::from_parts(0, 4529).saturating_mul(x.into())) - } -} - -// For backwards compatibility and tests. -impl WeightInfo for () { - /// Storage: `System::Digest` (r:1 w:1) - /// Proof: `System::Digest` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) - /// Storage: `Sassafras::NextRandomness` (r:1 w:0) - /// Proof: `Sassafras::NextRandomness` (`max_values`: Some(1), `max_size`: Some(32), added: 527, mode: `MaxEncodedLen`) - /// Storage: `Sassafras::NextAuthorities` (r:1 w:0) - /// Proof: `Sassafras::NextAuthorities` (`max_values`: Some(1), `max_size`: Some(3302), added: 3797, mode: `MaxEncodedLen`) - /// Storage: `Sassafras::CurrentRandomness` (r:1 w:0) - /// Proof: `Sassafras::CurrentRandomness` (`max_values`: Some(1), `max_size`: Some(32), added: 527, mode: `MaxEncodedLen`) - /// Storage: `Sassafras::EpochIndex` (r:1 w:0) - /// Proof: `Sassafras::EpochIndex` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `MaxEncodedLen`) - /// Storage: `Sassafras::RandomnessAccumulator` (r:1 w:1) - /// Proof: `Sassafras::RandomnessAccumulator` (`max_values`: Some(1), `max_size`: Some(32), added: 527, mode: `MaxEncodedLen`) - /// Storage: `Sassafras::CurrentSlot` (r:0 w:1) - /// Proof: `Sassafras::CurrentSlot` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `MaxEncodedLen`) - /// Storage: `Sassafras::ClaimTemporaryData` (r:0 w:1) - /// Proof: `Sassafras::ClaimTemporaryData` (`max_values`: Some(1), `max_size`: Some(33), added: 528, mode: `MaxEncodedLen`) - /// Storage: `Sassafras::GenesisSlot` (r:0 w:1) - /// Proof: `Sassafras::GenesisSlot` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `MaxEncodedLen`) - fn on_initialize() -> Weight { - // Proof Size summary in bytes: - // Measured: `302` - // Estimated: `4787` - // Minimum execution time: 438_039_000 picoseconds. - Weight::from_parts(439_302_000, 4787) - .saturating_add(RocksDbWeight::get().reads(6_u64)) - .saturating_add(RocksDbWeight::get().writes(5_u64)) - } - /// Storage: `Sassafras::CurrentSlot` (r:1 w:0) - /// Proof: `Sassafras::CurrentSlot` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `MaxEncodedLen`) - /// Storage: `Sassafras::EpochIndex` (r:1 w:1) - /// Proof: `Sassafras::EpochIndex` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `MaxEncodedLen`) - /// Storage: `Sassafras::GenesisSlot` (r:1 w:0) - /// Proof: `Sassafras::GenesisSlot` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `MaxEncodedLen`) - /// Storage: `Sassafras::NextAuthorities` (r:1 w:1) - /// Proof: `Sassafras::NextAuthorities` (`max_values`: Some(1), `max_size`: Some(3302), added: 3797, mode: `MaxEncodedLen`) - /// Storage: `Sassafras::RingContext` (r:1 w:0) - /// Proof: `Sassafras::RingContext` (`max_values`: Some(1), `max_size`: Some(590324), added: 590819, mode: `MaxEncodedLen`) - /// Storage: `Sassafras::TicketsMeta` (r:1 w:1) - /// Proof: `Sassafras::TicketsMeta` (`max_values`: Some(1), `max_size`: Some(12), added: 507, mode: `MaxEncodedLen`) - /// Storage: `Sassafras::NextRandomness` (r:1 w:1) - /// Proof: `Sassafras::NextRandomness` (`max_values`: Some(1), `max_size`: Some(32), added: 527, mode: `MaxEncodedLen`) - /// Storage: `Sassafras::RandomnessAccumulator` (r:1 w:0) - /// Proof: `Sassafras::RandomnessAccumulator` (`max_values`: Some(1), `max_size`: Some(32), added: 527, mode: `MaxEncodedLen`) - /// Storage: `Sassafras::NextEpochConfig` (r:1 w:1) - /// Proof: `Sassafras::NextEpochConfig` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `MaxEncodedLen`) - /// Storage: `Sassafras::PendingEpochConfigChange` (r:1 w:1) - /// Proof: `Sassafras::PendingEpochConfigChange` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `MaxEncodedLen`) - /// Storage: `System::Digest` (r:1 w:1) - /// Proof: `System::Digest` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) - /// Storage: `Sassafras::SortedCandidates` (r:1 w:0) - /// Proof: `Sassafras::SortedCandidates` (`max_values`: Some(1), `max_size`: Some(3202), added: 3697, mode: `MaxEncodedLen`) - /// Storage: `Sassafras::UnsortedSegments` (r:79 w:79) - /// Proof: `Sassafras::UnsortedSegments` (`max_values`: None, `max_size`: Some(2054), added: 4529, mode: `MaxEncodedLen`) - /// Storage: `Sassafras::TicketsIds` (r:5000 w:200) - /// Proof: `Sassafras::TicketsIds` (`max_values`: None, `max_size`: Some(21), added: 2496, mode: `MaxEncodedLen`) - /// Storage: `Sassafras::Authorities` (r:0 w:1) - /// Proof: `Sassafras::Authorities` (`max_values`: Some(1), `max_size`: Some(3302), added: 3797, mode: `MaxEncodedLen`) - /// Storage: `Sassafras::TicketsData` (r:0 w:9896) - /// Proof: `Sassafras::TicketsData` (`max_values`: None, `max_size`: Some(84), added: 2559, mode: `MaxEncodedLen`) - /// Storage: `Sassafras::RingVerifierData` (r:0 w:1) - /// Proof: `Sassafras::RingVerifierData` (`max_values`: Some(1), `max_size`: Some(388), added: 883, mode: `MaxEncodedLen`) - /// Storage: `Sassafras::EpochConfig` (r:0 w:1) - /// Proof: `Sassafras::EpochConfig` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `MaxEncodedLen`) - /// Storage: `Sassafras::CurrentRandomness` (r:0 w:1) - /// Proof: `Sassafras::CurrentRandomness` (`max_values`: Some(1), `max_size`: Some(32), added: 527, mode: `MaxEncodedLen`) - /// The range of component `x` is `[1, 100]`. - /// The range of component `y` is `[1000, 5000]`. - fn enact_epoch_change(x: u32, y: u32, ) -> Weight { - // Proof Size summary in bytes: - // Measured: `594909 + x * (33 ±0) + y * (53 ±0)` - // Estimated: `593350 + x * (24 ±1) + y * (2496 ±0)` - // Minimum execution time: 121_279_846_000 picoseconds. - Weight::from_parts(94_454_851_972, 593350) - // Standard Error: 24_177_301 - .saturating_add(Weight::from_parts(8_086_191, 0).saturating_mul(x.into())) - // Standard Error: 601_053 - .saturating_add(Weight::from_parts(15_578_413, 0).saturating_mul(y.into())) - .saturating_add(RocksDbWeight::get().reads(13_u64)) - .saturating_add(RocksDbWeight::get().reads((1_u64).saturating_mul(y.into()))) - .saturating_add(RocksDbWeight::get().writes(112_u64)) - .saturating_add(RocksDbWeight::get().writes((2_u64).saturating_mul(y.into()))) - .saturating_add(Weight::from_parts(0, 24).saturating_mul(x.into())) - .saturating_add(Weight::from_parts(0, 2496).saturating_mul(y.into())) - } - /// Storage: `Sassafras::CurrentSlot` (r:1 w:0) - /// Proof: `Sassafras::CurrentSlot` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `MaxEncodedLen`) - /// Storage: `Sassafras::EpochIndex` (r:1 w:0) - /// Proof: `Sassafras::EpochIndex` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `MaxEncodedLen`) - /// Storage: `Sassafras::GenesisSlot` (r:1 w:0) - /// Proof: `Sassafras::GenesisSlot` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `MaxEncodedLen`) - /// Storage: `Sassafras::RingVerifierData` (r:1 w:0) - /// Proof: `Sassafras::RingVerifierData` (`max_values`: Some(1), `max_size`: Some(388), added: 883, mode: `MaxEncodedLen`) - /// Storage: `Sassafras::NextAuthorities` (r:1 w:0) - /// Proof: `Sassafras::NextAuthorities` (`max_values`: Some(1), `max_size`: Some(3302), added: 3797, mode: `MaxEncodedLen`) - /// Storage: `Sassafras::NextEpochConfig` (r:1 w:0) - /// Proof: `Sassafras::NextEpochConfig` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `MaxEncodedLen`) - /// Storage: `Sassafras::NextRandomness` (r:1 w:0) - /// Proof: `Sassafras::NextRandomness` (`max_values`: Some(1), `max_size`: Some(32), added: 527, mode: `MaxEncodedLen`) - /// Storage: `Sassafras::TicketsData` (r:25 w:25) - /// Proof: `Sassafras::TicketsData` (`max_values`: None, `max_size`: Some(84), added: 2559, mode: `MaxEncodedLen`) - /// Storage: `Sassafras::TicketsMeta` (r:1 w:1) - /// Proof: `Sassafras::TicketsMeta` (`max_values`: Some(1), `max_size`: Some(12), added: 507, mode: `MaxEncodedLen`) - /// Storage: `Sassafras::UnsortedSegments` (r:1 w:1) - /// Proof: `Sassafras::UnsortedSegments` (`max_values`: None, `max_size`: Some(2054), added: 4529, mode: `MaxEncodedLen`) - /// The range of component `x` is `[1, 25]`. - fn submit_tickets(x: u32, ) -> Weight { - // Proof Size summary in bytes: - // Measured: `3869` - // Estimated: `5519 + x * (2559 ±0)` - // Minimum execution time: 36_904_934_000 picoseconds. - Weight::from_parts(25_822_957_295, 5519) - // Standard Error: 11_047_832 - .saturating_add(Weight::from_parts(11_338_353_299, 0).saturating_mul(x.into())) - .saturating_add(RocksDbWeight::get().reads(9_u64)) - .saturating_add(RocksDbWeight::get().reads((1_u64).saturating_mul(x.into()))) - .saturating_add(RocksDbWeight::get().writes(2_u64)) - .saturating_add(RocksDbWeight::get().writes((1_u64).saturating_mul(x.into()))) - .saturating_add(Weight::from_parts(0, 2559).saturating_mul(x.into())) - } - /// Storage: `Sassafras::PendingEpochConfigChange` (r:0 w:1) - /// Proof: `Sassafras::PendingEpochConfigChange` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `MaxEncodedLen`) - fn plan_config_change() -> Weight { - // Proof Size summary in bytes: - // Measured: `0` - // Estimated: `0` - // Minimum execution time: 4_038_000 picoseconds. - Weight::from_parts(4_499_000, 0) - .saturating_add(RocksDbWeight::get().writes(1_u64)) - } - /// Storage: `Sassafras::RingContext` (r:1 w:0) - /// Proof: `Sassafras::RingContext` (`max_values`: Some(1), `max_size`: Some(590324), added: 590819, mode: `MaxEncodedLen`) - /// Storage: `Sassafras::RingVerifierData` (r:0 w:1) - /// Proof: `Sassafras::RingVerifierData` (`max_values`: Some(1), `max_size`: Some(388), added: 883, mode: `MaxEncodedLen`) - /// The range of component `x` is `[1, 100]`. - fn update_ring_verifier(x: u32, ) -> Weight { - // Proof Size summary in bytes: - // Measured: `590485` - // Estimated: `591809` - // Minimum execution time: 105_121_424_000 picoseconds. - Weight::from_parts(105_527_334_385, 591809) - // Standard Error: 2_933_910 - .saturating_add(Weight::from_parts(96_136_261, 0).saturating_mul(x.into())) - .saturating_add(RocksDbWeight::get().reads(1_u64)) - .saturating_add(RocksDbWeight::get().writes(1_u64)) - } - /// Storage: `Sassafras::RingContext` (r:1 w:0) - /// Proof: `Sassafras::RingContext` (`max_values`: Some(1), `max_size`: Some(590324), added: 590819, mode: `MaxEncodedLen`) - fn load_ring_context() -> Weight { - // Proof Size summary in bytes: - // Measured: `590485` - // Estimated: `591809` - // Minimum execution time: 44_005_681_000 picoseconds. - Weight::from_parts(44_312_079_000, 591809) - .saturating_add(RocksDbWeight::get().reads(1_u64)) - } - /// Storage: `Sassafras::SortedCandidates` (r:1 w:0) - /// Proof: `Sassafras::SortedCandidates` (`max_values`: Some(1), `max_size`: Some(3202), added: 3697, mode: `MaxEncodedLen`) - /// Storage: `Sassafras::UnsortedSegments` (r:100 w:100) - /// Proof: `Sassafras::UnsortedSegments` (`max_values`: None, `max_size`: Some(2054), added: 4529, mode: `MaxEncodedLen`) - /// Storage: `Sassafras::TicketsIds` (r:0 w:200) - /// Proof: `Sassafras::TicketsIds` (`max_values`: None, `max_size`: Some(21), added: 2496, mode: `MaxEncodedLen`) - /// Storage: `Sassafras::TicketsData` (r:0 w:12600) - /// Proof: `Sassafras::TicketsData` (`max_values`: None, `max_size`: Some(84), added: 2559, mode: `MaxEncodedLen`) - /// The range of component `x` is `[1, 100]`. - fn sort_segments(x: u32, ) -> Weight { - // Proof Size summary in bytes: - // Measured: `222 + x * (2060 ±0)` - // Estimated: `4687 + x * (4529 ±0)` - // Minimum execution time: 183_501_000 picoseconds. - Weight::from_parts(183_501_000, 4687) - // Standard Error: 1_426_363 - .saturating_add(Weight::from_parts(169_156_241, 0).saturating_mul(x.into())) - .saturating_add(RocksDbWeight::get().reads(1_u64)) - .saturating_add(RocksDbWeight::get().reads((1_u64).saturating_mul(x.into()))) - .saturating_add(RocksDbWeight::get().writes((129_u64).saturating_mul(x.into()))) - .saturating_add(Weight::from_parts(0, 4529).saturating_mul(x.into())) - } -} From 1d0b093067a378efadd047faaf6114ed57631ee1 Mon Sep 17 00:00:00 2001 From: Davide Galassi Date: Tue, 7 May 2024 16:20:32 +0200 Subject: [PATCH 07/42] Fix warnings --- substrate/frame/sassafras/src/lib.rs | 5 +++-- substrate/frame/sassafras/src/mock.rs | 5 ++--- substrate/frame/sassafras/src/tests.rs | 10 ++++------ 3 files changed, 9 insertions(+), 11 deletions(-) diff --git a/substrate/frame/sassafras/src/lib.rs b/substrate/frame/sassafras/src/lib.rs index 887f8a95210e..2053a76bad89 100644 --- a/substrate/frame/sassafras/src/lib.rs +++ b/substrate/frame/sassafras/src/lib.rs @@ -41,8 +41,8 @@ //! nominated validators". #![allow(unused)] -// #![deny(warnings)] -// #![warn(unused_must_use, unsafe_code, unused_variables, unused_imports, missing_docs)] +#![deny(warnings)] +#![warn(unused_must_use, unsafe_code, unused_variables, unused_imports, missing_docs)] #![cfg_attr(not(feature = "std"), no_std)] use log::{debug, warn}; @@ -669,6 +669,7 @@ impl Pallet { Self::deposit_next_epoch_descriptor_digest(next_epoch); } + /// Static protocol configuration. pub fn protocol_config() -> Configuration { Configuration { epoch_length: T::EpochLength::get(), diff --git a/substrate/frame/sassafras/src/mock.rs b/substrate/frame/sassafras/src/mock.rs index 65e99a337be0..1718cdefb59c 100644 --- a/substrate/frame/sassafras/src/mock.rs +++ b/substrate/frame/sassafras/src/mock.rs @@ -26,11 +26,10 @@ use frame_support::{ use sp_consensus_sassafras::{ digests::SlotClaim, vrf::{RingProver, VrfSignature}, - AuthorityIndex, AuthorityPair, Configuration, Slot, TicketBody, TicketEnvelope, TicketId, + AuthorityIndex, AuthorityPair, Slot, TicketBody, TicketEnvelope, TicketId, }; use sp_core::{ - crypto::{ByteArray, Pair, UncheckedFrom, VrfSecret, Wraps}, - ed25519::Public as EphemeralPublic, + crypto::{ByteArray, Pair, VrfSecret, Wraps}, H256, U256, }; use sp_runtime::{ diff --git a/substrate/frame/sassafras/src/tests.rs b/substrate/frame/sassafras/src/tests.rs index a1b5459d5586..a76966c0e98c 100644 --- a/substrate/frame/sassafras/src/tests.rs +++ b/substrate/frame/sassafras/src/tests.rs @@ -461,13 +461,13 @@ fn tickets_accumulator_works() { // Progress to epoch's last block let end_block = start_block + epoch_length - 2; - let digest = progress_to_block(end_block, &pairs[0]).unwrap(); + progress_to_block(end_block, &pairs[0]).unwrap(); let metadata = TicketsMeta::::get(); assert_eq!(metadata.tickets_count[epoch_tag as usize], 0); assert_eq!(metadata.tickets_count[next_epoch_tag as usize], 0); - let header = finalize_block(end_block); + finalize_block(end_block); let metadata = TicketsMeta::::get(); assert_eq!(metadata.tickets_count[epoch_tag as usize], 0); @@ -495,20 +495,18 @@ fn tickets_accumulator_works() { // Progress to epoch's last block let end_block = end_block + epoch_length; - let digest = progress_to_block(end_block, &pairs[0]).unwrap(); + progress_to_block(end_block, &pairs[0]).unwrap(); let metadata = TicketsMeta::::get(); assert_eq!(metadata.tickets_count[epoch_tag as usize], e1_count as u32); assert_eq!(metadata.tickets_count[next_epoch_tag as usize], 0); - let header = finalize_block(end_block); + finalize_block(end_block); let metadata = TicketsMeta::::get(); assert_eq!(metadata.tickets_count[epoch_tag as usize], e1_count as u32); assert_eq!(metadata.tickets_count[next_epoch_tag as usize], e2_count as u32); - let metadata = TicketsMeta::::get(); - // Start new epoch initialize_block( end_block + 1, From 9e32ffb07d2bb6f3e0ec49e4190f89c10cf7bcd5 Mon Sep 17 00:00:00 2001 From: Davide Galassi Date: Tue, 7 May 2024 16:22:40 +0200 Subject: [PATCH 08/42] Remove some leftovers --- substrate/frame/sassafras/src/lib.rs | 10 ---------- substrate/frame/sassafras/src/mock.rs | 4 +--- substrate/frame/sassafras/src/tests.rs | 3 --- 3 files changed, 1 insertion(+), 16 deletions(-) diff --git a/substrate/frame/sassafras/src/lib.rs b/substrate/frame/sassafras/src/lib.rs index 2053a76bad89..395286cc15c3 100644 --- a/substrate/frame/sassafras/src/lib.rs +++ b/substrate/frame/sassafras/src/lib.rs @@ -277,7 +277,6 @@ pub mod pallet { .find_map(|item| item.pre_runtime_try_to::(&SASSAFRAS_ENGINE_ID)) .expect("Valid block must have a slot claim. qed"); - println!("Processing block {}, slot {}", block_num, claim.slot); CurrentSlot::::put(claim.slot); if block_num == One::one() { @@ -396,7 +395,6 @@ pub mod pallet { candidates.push((ticket_id, ticket.body)); } - println!("Depositing {} tickets", candidates.len()); Self::deposit_tickets(&candidates)?; Ok(Pays::No.into()) @@ -500,7 +498,6 @@ impl Pallet { authorities: WeakBoundedVec, next_authorities: WeakBoundedVec, ) { - println!("ENACT EPOCH CHANGE!!!!!!!!!!!!!"); if next_authorities != authorities { Self::update_ring_verifier(&next_authorities); } @@ -564,12 +561,10 @@ impl Pallet { } let diff = count.saturating_sub(T::EpochLength::get()); if diff > 0 { - println!("REMOVING {:?}", diff); let keys: Vec<_> = TicketsAccumulator::::iter_keys().take(diff as usize).collect(); for key in keys { let ticket_id = TicketId::from(key); if tickets.binary_search_by_key(&&ticket_id, |(id, _)| id).is_ok() { - println!("Removed one new candidate"); return Err(Error::TicketInvalid) } TicketsAccumulator::::remove(key); @@ -873,11 +868,6 @@ pub struct EpochChangeInternalTrigger; impl EpochChangeTrigger for EpochChangeInternalTrigger { fn trigger(block_num: BlockNumberFor) -> Weight { if Pallet::::should_end_epoch(block_num) { - println!( - "CURRENT SLOT INDEX: {}, EPOCH LEN: {}", - Pallet::::current_slot_index(), - T::EpochLength::get() - ); let authorities = Pallet::::next_authorities(); let next_authorities = authorities.clone(); Pallet::::enact_epoch_change(authorities, next_authorities); diff --git a/substrate/frame/sassafras/src/mock.rs b/substrate/frame/sassafras/src/mock.rs index 1718cdefb59c..1d7fb5b6b52c 100644 --- a/substrate/frame/sassafras/src/mock.rs +++ b/substrate/frame/sassafras/src/mock.rs @@ -235,9 +235,8 @@ fn make_ticket_with_prover( let signature = pair.as_ref().ring_vrf_sign(&sign_data, &prover); let pre_output = &signature.pre_outputs[0]; - let ticket_id = vrf::make_ticket_id(&ticket_id_input, pre_output); - println!("T: {:?}", ticket_id); + let ticket_id = vrf::make_ticket_id(&ticket_id_input, pre_output); let envelope = TicketEnvelope { body, signature }; (ticket_id, envelope) @@ -271,7 +270,6 @@ pub fn make_prover(pair: &AuthorityPair) -> RingProver { /// /// E.g. by passing an optional threshold pub fn make_tickets(attempts: u32, pair: &AuthorityPair) -> Vec<(TicketId, TicketEnvelope)> { - println!("MAKE TICKETS---"); let prover = make_prover(pair); (0..attempts) .into_iter() diff --git a/substrate/frame/sassafras/src/tests.rs b/substrate/frame/sassafras/src/tests.rs index a76966c0e98c..2878cc6fc084 100644 --- a/substrate/frame/sassafras/src/tests.rs +++ b/substrate/frame/sassafras/src/tests.rs @@ -102,7 +102,6 @@ fn deposit_tickets_failure() { }); assert!(Sassafras::deposit_tickets(&tickets[7..]).is_err()); - println!("ENTRIES: {}", TicketsAccumulator::::count()); }); } @@ -264,7 +263,6 @@ fn produce_epoch_change_digest() { // We want to trigger an epoch change in this test. let epoch_length = Sassafras::epoch_length() as u64; let end_block = start_block + epoch_length - 1; - println!("END BLOCK: {}", end_block); let common_assertions = |initialized| { assert_eq!(Sassafras::current_slot(), GENESIS_SLOT + epoch_length); @@ -585,7 +583,6 @@ fn data_read(filename: &str) -> T { fn data_write(filename: &str, data: T) { use std::{fs::File, io::Write}; - println!("Writing: {}", filename); let mut file = File::create(filename).unwrap(); let buf = data.encode(); file.write_all(&buf).unwrap(); From 5249537f0c290f9220dcaad0c109225535cfaced Mon Sep 17 00:00:00 2001 From: Davide Galassi Date: Wed, 8 May 2024 11:48:20 +0200 Subject: [PATCH 09/42] Benchmarking --- substrate/frame/sassafras/src/benchmarking.rs | 174 +++++++++++ substrate/frame/sassafras/src/lib.rs | 38 +-- substrate/frame/sassafras/src/tests.rs | 26 +- substrate/frame/sassafras/src/weights.rs | 283 ++++++++++++++++++ 4 files changed, 497 insertions(+), 24 deletions(-) create mode 100644 substrate/frame/sassafras/src/benchmarking.rs create mode 100644 substrate/frame/sassafras/src/weights.rs diff --git a/substrate/frame/sassafras/src/benchmarking.rs b/substrate/frame/sassafras/src/benchmarking.rs new file mode 100644 index 000000000000..6ba07dae9d40 --- /dev/null +++ b/substrate/frame/sassafras/src/benchmarking.rs @@ -0,0 +1,174 @@ +// 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. + +//! Benchmarks for the Sassafras pallet. + +use crate::*; +use sp_consensus_sassafras::vrf::VrfSignature; + +use frame_benchmarking::v2::*; +use frame_support::traits::Hooks; +use frame_system::RawOrigin; + +const LOG_TARGET: &str = "sassafras::benchmark"; + +const TICKETS_DATA: &[u8] = include_bytes!("data/tickets.bin"); + +fn dummy_vrf_signature() -> VrfSignature { + // This leverages our knowledge about serialized vrf signature structure. + // Mostly to avoid to import all the bandersnatch primitive just for this test. + let buf = [ + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 0xb5, 0x5f, 0x8e, 0xc7, 0x68, 0xf5, 0x05, 0x3f, 0xa9, + 0x18, 0xca, 0x07, 0x13, 0xc7, 0x4b, 0xa3, 0x9a, 0x97, 0xd3, 0x76, 0x8f, 0x0c, 0xbf, 0x2e, + 0xd4, 0xf9, 0x3a, 0xae, 0xc1, 0x96, 0x2a, 0x64, 0x80, + ]; + VrfSignature::decode(&mut &buf[..]).unwrap() +} + +#[benchmarks] +mod benchmarks { + use super::*; + + // For first block (#1) we do some extra operation. + // But is a one shot operation, so we don't account for it here. + // We use 0, as it will be the path used by all the blocks with n != 1 + #[benchmark] + fn on_initialize() { + let block_num = BlockNumberFor::::from(0u32); + + let slot_claim = SlotClaim { + authority_idx: 0, + slot: Default::default(), + vrf_signature: dummy_vrf_signature(), + }; + frame_system::Pallet::::deposit_log((&slot_claim).into()); + + // We currently don't account for the potential weight added by the `on_finalize` + // incremental sorting of the tickets. + + #[block] + { + // According to `Hooks` trait docs, `on_finalize` `Weight` should be bundled + // together with `on_initialize` `Weight`. + Pallet::::on_initialize(block_num); + Pallet::::on_finalize(block_num) + } + } + + // Weight for the default internal epoch change trigger. + // + // Parameters: + // - `x`: number of authorities (1:100). + // - `y`: number of tickets (100:1000) + // + // This accounts for the worst case which includes: + // - recompute the ring verifier key from the new authority set. + // - picking the epoch tickets from the accumulator in one shot. + #[benchmark] + fn enact_epoch_change(x: Linear<1, 100>, y: Linear<100, 1000>) { + let authorities_count = x as usize; + let outstanding_tickets = y as u32; + + let config = Pallet::::protocol_config(); + + // Makes the epoch change legit + CurrentSlot::::set(Slot::from(config.epoch_length as u64)); + + let tickets: Vec<_> = (0..outstanding_tickets) + .map(|i| { + let mut id = [0; 32]; + id[..4].copy_from_slice(&i.to_be_bytes()[..]); + (TicketId(id), TicketBody { attempt_idx: 0, extra: Default::default() }) + }) + .collect(); + + // Append some tickets to the accumulator + tickets + .iter() + .for_each(|t| TicketsAccumulator::::insert(TicketKey::from(t.0), &t.1)); + + // Force ring verifier key re-computation + let next_authorities: Vec<_> = + Authorities::::get().into_iter().cycle().take(authorities_count).collect(); + let next_authorities = WeakBoundedVec::force_from(next_authorities, None); + + NextAuthorities::::set(next_authorities); + + #[block] + { + Pallet::::should_end_epoch(BlockNumberFor::::from(3u32)); + let next_authorities = Pallet::::next_authorities(); + // Using a different set of authorities triggers the recomputation of ring verifier. + Pallet::::enact_epoch_change(Default::default(), next_authorities); + } + } + + #[benchmark] + fn submit_tickets(x: Linear<1, 20>) { + let tickets_count = x as usize; + + let mut raw_data = TICKETS_DATA; + let (randomness, authorities, tickets): ( + Randomness, + Vec, + Vec, + ) = Decode::decode(&mut raw_data).expect("Failed to decode tickets buffer"); + assert!(tickets.len() >= tickets_count); + + // Use the same values as the pre-built tickets + Pallet::::update_ring_verifier(&authorities); + let mut randomness_buf = RandomnessBuf::::get(); + randomness_buf[2] = randomness; + RandomnessBuf::::set(randomness_buf); + NextAuthorities::::set(WeakBoundedVec::force_from(authorities, None)); + + let tickets = tickets[..tickets_count].to_vec(); + let tickets = BoundedVec::truncate_from(tickets); + + #[extrinsic_call] + submit_tickets(RawOrigin::None, tickets); + } + + // Construction of ring verifier + #[benchmark] + fn update_ring_verifier(x: Linear<1, 100>) { + let authorities_count = x as usize; + let authorities: Vec<_> = + Authorities::::get().into_iter().cycle().take(authorities_count).collect(); + + #[block] + { + Pallet::::update_ring_verifier(&authorities); + } + } + + // Bare loading of ring context. + // + // It is interesting to see how this compares to 'update_ring_verifier', which + // also recomputes and stores the new verifier. + #[benchmark] + fn load_ring_context() { + #[block] + { + let _ = RingContext::::get().unwrap(); + } + } +} diff --git a/substrate/frame/sassafras/src/lib.rs b/substrate/frame/sassafras/src/lib.rs index 395286cc15c3..36787af2b757 100644 --- a/substrate/frame/sassafras/src/lib.rs +++ b/substrate/frame/sassafras/src/lib.rs @@ -45,15 +45,12 @@ #![warn(unused_must_use, unsafe_code, unused_variables, unused_imports, missing_docs)] #![cfg_attr(not(feature = "std"), no_std)] -use log::{debug, warn}; +use log::{debug, trace, warn}; use scale_codec::{Decode, Encode, MaxEncodedLen}; use scale_info::TypeInfo; use frame_support::{ - dispatch::{DispatchResultWithPostInfo, Pays}, - traits::Get, - weights::Weight, - BoundedVec, WeakBoundedVec, + dispatch::DispatchResult, traits::Get, weights::Weight, BoundedVec, WeakBoundedVec, }; use frame_system::{offchain::SendTransactionTypes, pallet_prelude::BlockNumberFor}; use sp_consensus_sassafras::{ @@ -71,11 +68,16 @@ use sp_std::prelude::Vec; pub use pallet::*; +#[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; + const LOG_TARGET: &str = "sassafras::runtime"; // Contextual string used by the VRF to generate per-block randomness. @@ -156,8 +158,7 @@ pub mod pallet { type EpochChangeTrigger: EpochChangeTrigger; /// Weight information for all calls of this pallet. - // TODO: weights::WeigthInfo bound - type WeightInfo; + type WeightInfo: weights::WeightInfo; } /// Sassafras runtime errors. @@ -291,10 +292,8 @@ pub mod pallet { PostInitCache::::put(randomness_pre_output); let trigger_weight = T::EpochChangeTrigger::trigger::(block_num); - Weight::zero() + trigger_weight - // TODO - // T::WeightInfo::on_initialize() + trigger_weight + T::WeightInfo::on_initialize() + trigger_weight } fn on_finalize(_: BlockNumberFor) { @@ -334,12 +333,14 @@ pub mod pallet { /// /// The number of tickets allowed to be submitted in one call is equal to the epoch length. #[pallet::call_index(0)] - #[pallet::weight((Weight::zero(), DispatchClass::Mandatory))] + #[pallet::weight(( + T::WeightInfo::submit_tickets(tickets.len() as u32), + DispatchClass::Mandatory + ))] pub fn submit_tickets( origin: OriginFor, - // TODO: change max length tickets: BoundedVec>, - ) -> DispatchResultWithPostInfo { + ) -> DispatchResult { ensure_none(origin)?; debug!(target: LOG_TARGET, "Received {} tickets", tickets.len()); @@ -370,8 +371,6 @@ pub mod pallet { let mut candidates = Vec::new(); 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"); return Err(Error::::TicketInvalid.into()) @@ -380,6 +379,8 @@ pub mod pallet { // Check threshold constraint let ticket_id = vrf::make_ticket_id(&ticket_id_input, &ticket_id_pre_output); + trace!(target: LOG_TARGET, "Checking ticket {:?}", ticket_id); + if ticket_id >= ticket_threshold { debug!(target: LOG_TARGET, "Ticket over threshold ({:?} >= {:?})", ticket_id, ticket_threshold); return Err(Error::::TicketOverThreshold.into()) @@ -397,7 +398,7 @@ pub mod pallet { Self::deposit_tickets(&candidates)?; - Ok(Pays::No.into()) + Ok(()) } } @@ -498,6 +499,8 @@ impl Pallet { authorities: WeakBoundedVec, next_authorities: WeakBoundedVec, ) { + debug_assert_eq!(authorities, NextAuthorities::::get()); + if next_authorities != authorities { Self::update_ring_verifier(&next_authorities); } @@ -512,7 +515,7 @@ impl Pallet { if epoch_idx < expected_epoch_idx { panic!( - "Undexpected epoch value, expected: {} - found: {}, aborting", + "Unexpected epoch value, expected: {} - found: {}, aborting", expected_epoch_idx, epoch_idx ); } @@ -871,7 +874,6 @@ impl EpochChangeTrigger for EpochChangeInternalTrigger { let authorities = Pallet::::next_authorities(); let next_authorities = authorities.clone(); Pallet::::enact_epoch_change(authorities, next_authorities); - // TODO // let len = next_authorities.len() as u32; // T::WeightInfo::enact_epoch_change(len, T::EpochLength::get()) Weight::zero() diff --git a/substrate/frame/sassafras/src/tests.rs b/substrate/frame/sassafras/src/tests.rs index 2878cc6fc084..74e7ee81ade8 100644 --- a/substrate/frame/sassafras/src/tests.rs +++ b/substrate/frame/sassafras/src/tests.rs @@ -595,8 +595,11 @@ fn submit_tickets_with_ring_proof_check_works() { let start_block = 1; let start_slot = (GENESIS_SLOT + 1).into(); - let (authorities, mut candidates): (Vec, Vec) = - data_read(TICKETS_FILE); + let (randomness, authorities, mut candidates): ( + Randomness, + Vec, + Vec, + ) = data_read(TICKETS_FILE); // Also checks that duplicates are discarded @@ -608,6 +611,13 @@ fn submit_tickets_with_ring_proof_check_works() { ext.execute_with(|| { initialize_block(start_block, start_slot, Default::default(), pair); + // Use the same values as the pre-built tickets + Sassafras::update_ring_verifier(&authorities); + let mut randomness_buf = RandomnessBuf::::get(); + randomness_buf[2] = randomness; + RandomnessBuf::::set(randomness_buf); + NextAuthorities::::set(WeakBoundedVec::force_from(authorities, None)); + // Submit the tickets let candidates_per_call = 4; let chunks: Vec<_> = candidates @@ -688,9 +698,13 @@ fn generate_test_tickets() { tickets.extend(t); println!("{:.2}%", 100f32 * ((i + 1) as f32 / authorities_count as f32)); }); - }); - tickets.sort_unstable_by_key(|t| t.0); - let envelopes: Vec<_> = tickets.into_iter().map(|t| t.1).collect(); - data_write(TICKETS_FILE, (authorities, envelopes)); + tickets.sort_unstable_by_key(|t| t.0); + let envelopes: Vec<_> = tickets.into_iter().map(|t| t.1).collect(); + + // Tickets were generated using `next_randomness` + let randomness = Sassafras::next_randomness(); + + data_write(TICKETS_FILE, (randomness, authorities, envelopes)); + }); } diff --git a/substrate/frame/sassafras/src/weights.rs b/substrate/frame/sassafras/src/weights.rs new file mode 100644 index 000000000000..4f9ed51a9672 --- /dev/null +++ b/substrate/frame/sassafras/src/weights.rs @@ -0,0 +1,283 @@ + +//! Autogenerated weights for `pallet_sassafras` +//! +//! THIS FILE WAS AUTO-GENERATED USING THE SUBSTRATE BENCHMARK CLI VERSION 32.0.0 +//! DATE: 2024-05-08, STEPS: `20`, REPEAT: `3`, LOW RANGE: `[]`, HIGH RANGE: `[]` +//! WORST CASE MAP SIZE: `1000000` +//! HOSTNAME: `behemoth`, CPU: `AMD Ryzen Threadripper 3970X 32-Core Processor` +//! WASM-EXECUTION: `Compiled`, CHAIN: `Some("dev")`, DB CACHE: `1024` + +// Executed Command: +// ./target/release/solochain-template-node +// benchmark +// pallet +// --chain +// dev +// --pallet +// pallet_sassafras +// --extrinsic +// * +// --steps +// 20 +// --repeat +// 3 +// --output +// weights.rs +// --template +// substrate/.maintain/frame-weight-template.hbs + +#![cfg_attr(rustfmt, rustfmt_skip)] +#![allow(unused_parens)] +#![allow(unused_imports)] +#![allow(missing_docs)] + +use frame_support::{traits::Get, weights::{Weight, constants::RocksDbWeight}}; +use core::marker::PhantomData; + +/// Weight functions needed for `pallet_sassafras`. +pub trait WeightInfo { + fn on_initialize() -> Weight; + fn enact_epoch_change(x: u32, y: u32, ) -> Weight; + fn submit_tickets(x: u32, ) -> Weight; + fn update_ring_verifier(x: u32, ) -> Weight; + fn load_ring_context() -> Weight; +} + +/// Weights for `pallet_sassafras` using the Substrate node and recommended hardware. +pub struct SubstrateWeight(PhantomData); +impl WeightInfo for SubstrateWeight { + /// Storage: `System::Digest` (r:1 w:0) + /// Proof: `System::Digest` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) + /// Storage: `Sassafras::RandomnessBuf` (r:1 w:1) + /// Proof: `Sassafras::RandomnessBuf` (`max_values`: Some(1), `max_size`: Some(128), added: 623, mode: `MaxEncodedLen`) + /// Storage: `Sassafras::CounterForTicketsAccumulator` (r:1 w:0) + /// Proof: `Sassafras::CounterForTicketsAccumulator` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`) + /// Storage: `Sassafras::CurrentSlot` (r:0 w:1) + /// Proof: `Sassafras::CurrentSlot` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `MaxEncodedLen`) + /// Storage: `Sassafras::PostInitCache` (r:0 w:1) + /// Proof: `Sassafras::PostInitCache` (`max_values`: Some(1), `max_size`: Some(33), added: 528, mode: `MaxEncodedLen`) + fn on_initialize() -> Weight { + // Proof Size summary in bytes: + // Measured: `270` + // Estimated: `1755` + // Minimum execution time: 419_644_000 picoseconds. + Weight::from_parts(426_998_000, 1755) + .saturating_add(T::DbWeight::get().reads(3_u64)) + .saturating_add(T::DbWeight::get().writes(3_u64)) + } + /// Storage: `Sassafras::CurrentSlot` (r:1 w:0) + /// Proof: `Sassafras::CurrentSlot` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `MaxEncodedLen`) + /// Storage: `Sassafras::NextAuthorities` (r:1 w:1) + /// Proof: `Sassafras::NextAuthorities` (`max_values`: Some(1), `max_size`: Some(3302), added: 3797, mode: `MaxEncodedLen`) + /// Storage: `Sassafras::RingContext` (r:1 w:0) + /// Proof: `Sassafras::RingContext` (`max_values`: Some(1), `max_size`: Some(590324), added: 590819, mode: `MaxEncodedLen`) + /// Storage: `Sassafras::EpochIndex` (r:1 w:1) + /// Proof: `Sassafras::EpochIndex` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `MaxEncodedLen`) + /// Storage: `Sassafras::RandomnessBuf` (r:1 w:1) + /// Proof: `Sassafras::RandomnessBuf` (`max_values`: Some(1), `max_size`: Some(128), added: 623, mode: `MaxEncodedLen`) + /// Storage: `System::Digest` (r:1 w:1) + /// Proof: `System::Digest` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) + /// Storage: `Sassafras::TicketsMeta` (r:1 w:1) + /// Proof: `Sassafras::TicketsMeta` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `MaxEncodedLen`) + /// Storage: `Sassafras::CounterForTicketsAccumulator` (r:1 w:1) + /// Proof: `Sassafras::CounterForTicketsAccumulator` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`) + /// Storage: `Sassafras::TicketsAccumulator` (r:1001 w:1000) + /// Proof: `Sassafras::TicketsAccumulator` (`max_values`: None, `max_size`: Some(166), added: 2641, mode: `MaxEncodedLen`) + /// Storage: `Sassafras::Tickets` (r:0 w:1000) + /// Proof: `Sassafras::Tickets` (`max_values`: None, `max_size`: Some(171), added: 2646, mode: `MaxEncodedLen`) + /// Storage: `Sassafras::Authorities` (r:0 w:1) + /// Proof: `Sassafras::Authorities` (`max_values`: Some(1), `max_size`: Some(3302), added: 3797, mode: `MaxEncodedLen`) + /// Storage: `Sassafras::RingVerifierKey` (r:0 w:1) + /// Proof: `Sassafras::RingVerifierKey` (`max_values`: Some(1), `max_size`: Some(388), added: 883, mode: `MaxEncodedLen`) + /// The range of component `x` is `[1, 100]`. + /// The range of component `y` is `[100, 1000]`. + fn enact_epoch_change(x: u32, y: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `590548 + x * (33 ±0) + y * (37 ±0)` + // Estimated: `592034 + x * (33 ±0) + y * (2641 ±0)` + // Minimum execution time: 143_083_854_000 picoseconds. + Weight::from_parts(135_741_817_675, 592034) + // Standard Error: 8_569_448 + .saturating_add(Weight::from_parts(191_947_077, 0).saturating_mul(x.into())) + // Standard Error: 946_406 + .saturating_add(Weight::from_parts(7_517_757, 0).saturating_mul(y.into())) + .saturating_add(T::DbWeight::get().reads(9_u64)) + .saturating_add(T::DbWeight::get().reads((1_u64).saturating_mul(y.into()))) + .saturating_add(T::DbWeight::get().writes(8_u64)) + .saturating_add(T::DbWeight::get().writes((2_u64).saturating_mul(y.into()))) + .saturating_add(Weight::from_parts(0, 33).saturating_mul(x.into())) + .saturating_add(Weight::from_parts(0, 2641).saturating_mul(y.into())) + } + /// Storage: `Sassafras::CurrentSlot` (r:1 w:0) + /// Proof: `Sassafras::CurrentSlot` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `MaxEncodedLen`) + /// Storage: `Sassafras::RingVerifierKey` (r:1 w:0) + /// Proof: `Sassafras::RingVerifierKey` (`max_values`: Some(1), `max_size`: Some(388), added: 883, mode: `MaxEncodedLen`) + /// Storage: `Sassafras::RandomnessBuf` (r:1 w:0) + /// Proof: `Sassafras::RandomnessBuf` (`max_values`: Some(1), `max_size`: Some(128), added: 623, mode: `MaxEncodedLen`) + /// Storage: `Sassafras::NextAuthorities` (r:1 w:0) + /// Proof: `Sassafras::NextAuthorities` (`max_values`: Some(1), `max_size`: Some(3302), added: 3797, mode: `MaxEncodedLen`) + /// Storage: `Sassafras::CounterForTicketsAccumulator` (r:1 w:1) + /// Proof: `Sassafras::CounterForTicketsAccumulator` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`) + /// Storage: `Sassafras::TicketsAccumulator` (r:20 w:20) + /// Proof: `Sassafras::TicketsAccumulator` (`max_values`: None, `max_size`: Some(166), added: 2641, mode: `MaxEncodedLen`) + /// The range of component `x` is `[1, 20]`. + fn submit_tickets(x: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `1029` + // Estimated: `4787 + x * (2641 ±0)` + // Minimum execution time: 52_831_436_000 picoseconds. + Weight::from_parts(37_535_482_110, 4787) + // Standard Error: 40_345_589 + .saturating_add(Weight::from_parts(14_828_490_127, 0).saturating_mul(x.into())) + .saturating_add(T::DbWeight::get().reads(5_u64)) + .saturating_add(T::DbWeight::get().reads((1_u64).saturating_mul(x.into()))) + .saturating_add(T::DbWeight::get().writes(1_u64)) + .saturating_add(T::DbWeight::get().writes((1_u64).saturating_mul(x.into()))) + .saturating_add(Weight::from_parts(0, 2641).saturating_mul(x.into())) + } + /// Storage: `Sassafras::RingContext` (r:1 w:0) + /// Proof: `Sassafras::RingContext` (`max_values`: Some(1), `max_size`: Some(590324), added: 590819, mode: `MaxEncodedLen`) + /// Storage: `Sassafras::RingVerifierKey` (r:0 w:1) + /// Proof: `Sassafras::RingVerifierKey` (`max_values`: Some(1), `max_size`: Some(388), added: 883, mode: `MaxEncodedLen`) + /// The range of component `x` is `[1, 100]`. + fn update_ring_verifier(x: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `590458` + // Estimated: `591809` + // Minimum execution time: 135_687_885_000 picoseconds. + Weight::from_parts(136_817_290_036, 591809) + // Standard Error: 6_721_283 + .saturating_add(Weight::from_parts(172_608_712, 0).saturating_mul(x.into())) + .saturating_add(T::DbWeight::get().reads(1_u64)) + .saturating_add(T::DbWeight::get().writes(1_u64)) + } + /// Storage: `Sassafras::RingContext` (r:1 w:0) + /// Proof: `Sassafras::RingContext` (`max_values`: Some(1), `max_size`: Some(590324), added: 590819, mode: `MaxEncodedLen`) + fn load_ring_context() -> Weight { + // Proof Size summary in bytes: + // Measured: `590458` + // Estimated: `591809` + // Minimum execution time: 57_089_719_000 picoseconds. + Weight::from_parts(58_050_050_000, 591809) + .saturating_add(T::DbWeight::get().reads(1_u64)) + } +} + +// For backwards compatibility and tests. +impl WeightInfo for () { + /// Storage: `System::Digest` (r:1 w:0) + /// Proof: `System::Digest` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) + /// Storage: `Sassafras::RandomnessBuf` (r:1 w:1) + /// Proof: `Sassafras::RandomnessBuf` (`max_values`: Some(1), `max_size`: Some(128), added: 623, mode: `MaxEncodedLen`) + /// Storage: `Sassafras::CounterForTicketsAccumulator` (r:1 w:0) + /// Proof: `Sassafras::CounterForTicketsAccumulator` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`) + /// Storage: `Sassafras::CurrentSlot` (r:0 w:1) + /// Proof: `Sassafras::CurrentSlot` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `MaxEncodedLen`) + /// Storage: `Sassafras::PostInitCache` (r:0 w:1) + /// Proof: `Sassafras::PostInitCache` (`max_values`: Some(1), `max_size`: Some(33), added: 528, mode: `MaxEncodedLen`) + fn on_initialize() -> Weight { + // Proof Size summary in bytes: + // Measured: `270` + // Estimated: `1755` + // Minimum execution time: 419_644_000 picoseconds. + Weight::from_parts(426_998_000, 1755) + .saturating_add(RocksDbWeight::get().reads(3_u64)) + .saturating_add(RocksDbWeight::get().writes(3_u64)) + } + /// Storage: `Sassafras::CurrentSlot` (r:1 w:0) + /// Proof: `Sassafras::CurrentSlot` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `MaxEncodedLen`) + /// Storage: `Sassafras::NextAuthorities` (r:1 w:1) + /// Proof: `Sassafras::NextAuthorities` (`max_values`: Some(1), `max_size`: Some(3302), added: 3797, mode: `MaxEncodedLen`) + /// Storage: `Sassafras::RingContext` (r:1 w:0) + /// Proof: `Sassafras::RingContext` (`max_values`: Some(1), `max_size`: Some(590324), added: 590819, mode: `MaxEncodedLen`) + /// Storage: `Sassafras::EpochIndex` (r:1 w:1) + /// Proof: `Sassafras::EpochIndex` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `MaxEncodedLen`) + /// Storage: `Sassafras::RandomnessBuf` (r:1 w:1) + /// Proof: `Sassafras::RandomnessBuf` (`max_values`: Some(1), `max_size`: Some(128), added: 623, mode: `MaxEncodedLen`) + /// Storage: `System::Digest` (r:1 w:1) + /// Proof: `System::Digest` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) + /// Storage: `Sassafras::TicketsMeta` (r:1 w:1) + /// Proof: `Sassafras::TicketsMeta` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `MaxEncodedLen`) + /// Storage: `Sassafras::CounterForTicketsAccumulator` (r:1 w:1) + /// Proof: `Sassafras::CounterForTicketsAccumulator` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`) + /// Storage: `Sassafras::TicketsAccumulator` (r:1001 w:1000) + /// Proof: `Sassafras::TicketsAccumulator` (`max_values`: None, `max_size`: Some(166), added: 2641, mode: `MaxEncodedLen`) + /// Storage: `Sassafras::Tickets` (r:0 w:1000) + /// Proof: `Sassafras::Tickets` (`max_values`: None, `max_size`: Some(171), added: 2646, mode: `MaxEncodedLen`) + /// Storage: `Sassafras::Authorities` (r:0 w:1) + /// Proof: `Sassafras::Authorities` (`max_values`: Some(1), `max_size`: Some(3302), added: 3797, mode: `MaxEncodedLen`) + /// Storage: `Sassafras::RingVerifierKey` (r:0 w:1) + /// Proof: `Sassafras::RingVerifierKey` (`max_values`: Some(1), `max_size`: Some(388), added: 883, mode: `MaxEncodedLen`) + /// The range of component `x` is `[1, 100]`. + /// The range of component `y` is `[100, 1000]`. + fn enact_epoch_change(x: u32, y: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `590548 + x * (33 ±0) + y * (37 ±0)` + // Estimated: `592034 + x * (33 ±0) + y * (2641 ±0)` + // Minimum execution time: 143_083_854_000 picoseconds. + Weight::from_parts(135_741_817_675, 592034) + // Standard Error: 8_569_448 + .saturating_add(Weight::from_parts(191_947_077, 0).saturating_mul(x.into())) + // Standard Error: 946_406 + .saturating_add(Weight::from_parts(7_517_757, 0).saturating_mul(y.into())) + .saturating_add(RocksDbWeight::get().reads(9_u64)) + .saturating_add(RocksDbWeight::get().reads((1_u64).saturating_mul(y.into()))) + .saturating_add(RocksDbWeight::get().writes(8_u64)) + .saturating_add(RocksDbWeight::get().writes((2_u64).saturating_mul(y.into()))) + .saturating_add(Weight::from_parts(0, 33).saturating_mul(x.into())) + .saturating_add(Weight::from_parts(0, 2641).saturating_mul(y.into())) + } + /// Storage: `Sassafras::CurrentSlot` (r:1 w:0) + /// Proof: `Sassafras::CurrentSlot` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `MaxEncodedLen`) + /// Storage: `Sassafras::RingVerifierKey` (r:1 w:0) + /// Proof: `Sassafras::RingVerifierKey` (`max_values`: Some(1), `max_size`: Some(388), added: 883, mode: `MaxEncodedLen`) + /// Storage: `Sassafras::RandomnessBuf` (r:1 w:0) + /// Proof: `Sassafras::RandomnessBuf` (`max_values`: Some(1), `max_size`: Some(128), added: 623, mode: `MaxEncodedLen`) + /// Storage: `Sassafras::NextAuthorities` (r:1 w:0) + /// Proof: `Sassafras::NextAuthorities` (`max_values`: Some(1), `max_size`: Some(3302), added: 3797, mode: `MaxEncodedLen`) + /// Storage: `Sassafras::CounterForTicketsAccumulator` (r:1 w:1) + /// Proof: `Sassafras::CounterForTicketsAccumulator` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`) + /// Storage: `Sassafras::TicketsAccumulator` (r:20 w:20) + /// Proof: `Sassafras::TicketsAccumulator` (`max_values`: None, `max_size`: Some(166), added: 2641, mode: `MaxEncodedLen`) + /// The range of component `x` is `[1, 20]`. + fn submit_tickets(x: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `1029` + // Estimated: `4787 + x * (2641 ±0)` + // Minimum execution time: 52_831_436_000 picoseconds. + Weight::from_parts(37_535_482_110, 4787) + // Standard Error: 40_345_589 + .saturating_add(Weight::from_parts(14_828_490_127, 0).saturating_mul(x.into())) + .saturating_add(RocksDbWeight::get().reads(5_u64)) + .saturating_add(RocksDbWeight::get().reads((1_u64).saturating_mul(x.into()))) + .saturating_add(RocksDbWeight::get().writes(1_u64)) + .saturating_add(RocksDbWeight::get().writes((1_u64).saturating_mul(x.into()))) + .saturating_add(Weight::from_parts(0, 2641).saturating_mul(x.into())) + } + /// Storage: `Sassafras::RingContext` (r:1 w:0) + /// Proof: `Sassafras::RingContext` (`max_values`: Some(1), `max_size`: Some(590324), added: 590819, mode: `MaxEncodedLen`) + /// Storage: `Sassafras::RingVerifierKey` (r:0 w:1) + /// Proof: `Sassafras::RingVerifierKey` (`max_values`: Some(1), `max_size`: Some(388), added: 883, mode: `MaxEncodedLen`) + /// The range of component `x` is `[1, 100]`. + fn update_ring_verifier(x: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `590458` + // Estimated: `591809` + // Minimum execution time: 135_687_885_000 picoseconds. + Weight::from_parts(136_817_290_036, 591809) + // Standard Error: 6_721_283 + .saturating_add(Weight::from_parts(172_608_712, 0).saturating_mul(x.into())) + .saturating_add(RocksDbWeight::get().reads(1_u64)) + .saturating_add(RocksDbWeight::get().writes(1_u64)) + } + /// Storage: `Sassafras::RingContext` (r:1 w:0) + /// Proof: `Sassafras::RingContext` (`max_values`: Some(1), `max_size`: Some(590324), added: 590819, mode: `MaxEncodedLen`) + fn load_ring_context() -> Weight { + // Proof Size summary in bytes: + // Measured: `590458` + // Estimated: `591809` + // Minimum execution time: 57_089_719_000 picoseconds. + Weight::from_parts(58_050_050_000, 591809) + .saturating_add(RocksDbWeight::get().reads(1_u64)) + } +} From edb1bdff78f860efaa42c1113e4ba582218d3703 Mon Sep 17 00:00:00 2001 From: Davide Galassi Date: Wed, 8 May 2024 11:50:07 +0200 Subject: [PATCH 10/42] Weights update --- substrate/frame/sassafras/src/weights.rs | 72 ++++++++++++------------ 1 file changed, 36 insertions(+), 36 deletions(-) diff --git a/substrate/frame/sassafras/src/weights.rs b/substrate/frame/sassafras/src/weights.rs index 4f9ed51a9672..a4f5d4aa17d5 100644 --- a/substrate/frame/sassafras/src/weights.rs +++ b/substrate/frame/sassafras/src/weights.rs @@ -60,8 +60,8 @@ impl WeightInfo for SubstrateWeight { // Proof Size summary in bytes: // Measured: `270` // Estimated: `1755` - // Minimum execution time: 419_644_000 picoseconds. - Weight::from_parts(426_998_000, 1755) + // Minimum execution time: 427_910_000 picoseconds. + Weight::from_parts(428_451_000, 1755) .saturating_add(T::DbWeight::get().reads(3_u64)) .saturating_add(T::DbWeight::get().writes(3_u64)) } @@ -95,12 +95,12 @@ impl WeightInfo for SubstrateWeight { // Proof Size summary in bytes: // Measured: `590548 + x * (33 ±0) + y * (37 ±0)` // Estimated: `592034 + x * (33 ±0) + y * (2641 ±0)` - // Minimum execution time: 143_083_854_000 picoseconds. - Weight::from_parts(135_741_817_675, 592034) - // Standard Error: 8_569_448 - .saturating_add(Weight::from_parts(191_947_077, 0).saturating_mul(x.into())) - // Standard Error: 946_406 - .saturating_add(Weight::from_parts(7_517_757, 0).saturating_mul(y.into())) + // Minimum execution time: 138_951_898_000 picoseconds. + Weight::from_parts(135_719_523_137, 592034) + // Standard Error: 5_842_563 + .saturating_add(Weight::from_parts(154_854_264, 0).saturating_mul(x.into())) + // Standard Error: 645_250 + .saturating_add(Weight::from_parts(4_311_218, 0).saturating_mul(y.into())) .saturating_add(T::DbWeight::get().reads(9_u64)) .saturating_add(T::DbWeight::get().reads((1_u64).saturating_mul(y.into()))) .saturating_add(T::DbWeight::get().writes(8_u64)) @@ -125,10 +125,10 @@ impl WeightInfo for SubstrateWeight { // Proof Size summary in bytes: // Measured: `1029` // Estimated: `4787 + x * (2641 ±0)` - // Minimum execution time: 52_831_436_000 picoseconds. - Weight::from_parts(37_535_482_110, 4787) - // Standard Error: 40_345_589 - .saturating_add(Weight::from_parts(14_828_490_127, 0).saturating_mul(x.into())) + // Minimum execution time: 52_109_411_000 picoseconds. + Weight::from_parts(38_584_018_164, 4787) + // Standard Error: 62_507_167 + .saturating_add(Weight::from_parts(14_381_572_906, 0).saturating_mul(x.into())) .saturating_add(T::DbWeight::get().reads(5_u64)) .saturating_add(T::DbWeight::get().reads((1_u64).saturating_mul(x.into()))) .saturating_add(T::DbWeight::get().writes(1_u64)) @@ -144,10 +144,10 @@ impl WeightInfo for SubstrateWeight { // Proof Size summary in bytes: // Measured: `590458` // Estimated: `591809` - // Minimum execution time: 135_687_885_000 picoseconds. - Weight::from_parts(136_817_290_036, 591809) - // Standard Error: 6_721_283 - .saturating_add(Weight::from_parts(172_608_712, 0).saturating_mul(x.into())) + // Minimum execution time: 131_132_781_000 picoseconds. + Weight::from_parts(132_019_264_471, 591809) + // Standard Error: 7_012_691 + .saturating_add(Weight::from_parts(195_019_609, 0).saturating_mul(x.into())) .saturating_add(T::DbWeight::get().reads(1_u64)) .saturating_add(T::DbWeight::get().writes(1_u64)) } @@ -157,8 +157,8 @@ impl WeightInfo for SubstrateWeight { // Proof Size summary in bytes: // Measured: `590458` // Estimated: `591809` - // Minimum execution time: 57_089_719_000 picoseconds. - Weight::from_parts(58_050_050_000, 591809) + // Minimum execution time: 55_893_928_000 picoseconds. + Weight::from_parts(56_862_553_000, 591809) .saturating_add(T::DbWeight::get().reads(1_u64)) } } @@ -179,8 +179,8 @@ impl WeightInfo for () { // Proof Size summary in bytes: // Measured: `270` // Estimated: `1755` - // Minimum execution time: 419_644_000 picoseconds. - Weight::from_parts(426_998_000, 1755) + // Minimum execution time: 427_910_000 picoseconds. + Weight::from_parts(428_451_000, 1755) .saturating_add(RocksDbWeight::get().reads(3_u64)) .saturating_add(RocksDbWeight::get().writes(3_u64)) } @@ -214,12 +214,12 @@ impl WeightInfo for () { // Proof Size summary in bytes: // Measured: `590548 + x * (33 ±0) + y * (37 ±0)` // Estimated: `592034 + x * (33 ±0) + y * (2641 ±0)` - // Minimum execution time: 143_083_854_000 picoseconds. - Weight::from_parts(135_741_817_675, 592034) - // Standard Error: 8_569_448 - .saturating_add(Weight::from_parts(191_947_077, 0).saturating_mul(x.into())) - // Standard Error: 946_406 - .saturating_add(Weight::from_parts(7_517_757, 0).saturating_mul(y.into())) + // Minimum execution time: 138_951_898_000 picoseconds. + Weight::from_parts(135_719_523_137, 592034) + // Standard Error: 5_842_563 + .saturating_add(Weight::from_parts(154_854_264, 0).saturating_mul(x.into())) + // Standard Error: 645_250 + .saturating_add(Weight::from_parts(4_311_218, 0).saturating_mul(y.into())) .saturating_add(RocksDbWeight::get().reads(9_u64)) .saturating_add(RocksDbWeight::get().reads((1_u64).saturating_mul(y.into()))) .saturating_add(RocksDbWeight::get().writes(8_u64)) @@ -244,10 +244,10 @@ impl WeightInfo for () { // Proof Size summary in bytes: // Measured: `1029` // Estimated: `4787 + x * (2641 ±0)` - // Minimum execution time: 52_831_436_000 picoseconds. - Weight::from_parts(37_535_482_110, 4787) - // Standard Error: 40_345_589 - .saturating_add(Weight::from_parts(14_828_490_127, 0).saturating_mul(x.into())) + // Minimum execution time: 52_109_411_000 picoseconds. + Weight::from_parts(38_584_018_164, 4787) + // Standard Error: 62_507_167 + .saturating_add(Weight::from_parts(14_381_572_906, 0).saturating_mul(x.into())) .saturating_add(RocksDbWeight::get().reads(5_u64)) .saturating_add(RocksDbWeight::get().reads((1_u64).saturating_mul(x.into()))) .saturating_add(RocksDbWeight::get().writes(1_u64)) @@ -263,10 +263,10 @@ impl WeightInfo for () { // Proof Size summary in bytes: // Measured: `590458` // Estimated: `591809` - // Minimum execution time: 135_687_885_000 picoseconds. - Weight::from_parts(136_817_290_036, 591809) - // Standard Error: 6_721_283 - .saturating_add(Weight::from_parts(172_608_712, 0).saturating_mul(x.into())) + // Minimum execution time: 131_132_781_000 picoseconds. + Weight::from_parts(132_019_264_471, 591809) + // Standard Error: 7_012_691 + .saturating_add(Weight::from_parts(195_019_609, 0).saturating_mul(x.into())) .saturating_add(RocksDbWeight::get().reads(1_u64)) .saturating_add(RocksDbWeight::get().writes(1_u64)) } @@ -276,8 +276,8 @@ impl WeightInfo for () { // Proof Size summary in bytes: // Measured: `590458` // Estimated: `591809` - // Minimum execution time: 57_089_719_000 picoseconds. - Weight::from_parts(58_050_050_000, 591809) + // Minimum execution time: 55_893_928_000 picoseconds. + Weight::from_parts(56_862_553_000, 591809) .saturating_add(RocksDbWeight::get().reads(1_u64)) } } From af3f20abc0e736522fdd8dcb440e94e2eedc5ebc Mon Sep 17 00:00:00 2001 From: Davide Galassi Date: Wed, 8 May 2024 11:55:57 +0200 Subject: [PATCH 11/42] Fix test --- substrate/frame/sassafras/src/lib.rs | 4 ++-- substrate/frame/sassafras/src/tests.rs | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/substrate/frame/sassafras/src/lib.rs b/substrate/frame/sassafras/src/lib.rs index 36787af2b757..f00a45766aaa 100644 --- a/substrate/frame/sassafras/src/lib.rs +++ b/substrate/frame/sassafras/src/lib.rs @@ -52,7 +52,7 @@ use scale_info::TypeInfo; use frame_support::{ dispatch::DispatchResult, traits::Get, weights::Weight, BoundedVec, WeakBoundedVec, }; -use frame_system::{offchain::SendTransactionTypes, pallet_prelude::BlockNumberFor}; +use frame_system::pallet_prelude::BlockNumberFor; use sp_consensus_sassafras::{ digests::{ConsensusLog, NextEpochDescriptor, SlotClaim}, vrf, AuthorityId, Configuration, Epoch, Randomness, Slot, TicketBody, TicketEnvelope, TicketId, @@ -134,7 +134,7 @@ pub mod pallet { /// Configuration parameters. #[pallet::config] - pub trait Config: frame_system::Config + SendTransactionTypes> { + pub trait Config: frame_system::Config { /// Amount of slots that each epoch should last. #[pallet::constant] type EpochLength: Get; diff --git a/substrate/frame/sassafras/src/tests.rs b/substrate/frame/sassafras/src/tests.rs index 74e7ee81ade8..aa2995921adf 100644 --- a/substrate/frame/sassafras/src/tests.rs +++ b/substrate/frame/sassafras/src/tests.rs @@ -20,8 +20,8 @@ use crate::*; use mock::*; -use frame_support::dispatch::DispatchErrorWithPostInfo; use sp_consensus_sassafras::Slot; +use sp_runtime::DispatchError; const TICKETS_FILE: &str = "src/data/tickets.bin"; @@ -630,7 +630,7 @@ fn submit_tickets_with_ring_proof_check_works() { let mut chunk = chunks[2].clone(); chunk[0].body.attempt_idx += 1; let e = Sassafras::submit_tickets(RuntimeOrigin::none(), chunk).unwrap_err(); - assert_eq!(e, DispatchErrorWithPostInfo::from(Error::::TicketBadProof)); + assert_eq!(e, DispatchError::from(Error::::TicketBadProof)); assert_eq!(TicketsAccumulator::::count(), 0); // Start submitting from the mid valued chunks. @@ -643,7 +643,7 @@ fn submit_tickets_with_ring_proof_check_works() { // Try to submit duplicates let e = Sassafras::submit_tickets(RuntimeOrigin::none(), chunks[2].clone()).unwrap_err(); - assert_eq!(e, DispatchErrorWithPostInfo::from(Error::::TicketDuplicate)); + assert_eq!(e, DispatchError::from(Error::::TicketDuplicate)); assert_eq!(TicketsAccumulator::::count(), 8); // Submit something smaller. This is accepted (2 old tickets removed). @@ -652,7 +652,7 @@ fn submit_tickets_with_ring_proof_check_works() { // Try to submit a chunk with bigger tickets. This is discarded let e = Sassafras::submit_tickets(RuntimeOrigin::none(), chunks[4].clone()).unwrap_err(); - assert_eq!(e, DispatchErrorWithPostInfo::from(Error::::TicketInvalid)); + assert_eq!(e, DispatchError::from(Error::::TicketInvalid)); assert_eq!(TicketsAccumulator::::count(), 10); // Submit the smaller candidates chunks. This is accepted (4 old tickets removed). From 7f91e7559535d457c9f36b7ce42e3d2fa92dd633 Mon Sep 17 00:00:00 2001 From: Davide Galassi Date: Wed, 8 May 2024 12:54:46 +0200 Subject: [PATCH 12/42] Submit tickets via Inherents --- substrate/frame/sassafras/src/lib.rs | 51 +++++++------------ .../primitives/consensus/sassafras/Cargo.toml | 2 + .../primitives/consensus/sassafras/src/lib.rs | 10 ++++ 3 files changed, 29 insertions(+), 34 deletions(-) diff --git a/substrate/frame/sassafras/src/lib.rs b/substrate/frame/sassafras/src/lib.rs index f00a45766aaa..63999ded7f73 100644 --- a/substrate/frame/sassafras/src/lib.rs +++ b/substrate/frame/sassafras/src/lib.rs @@ -55,8 +55,9 @@ use frame_support::{ use frame_system::pallet_prelude::BlockNumberFor; use sp_consensus_sassafras::{ digests::{ConsensusLog, NextEpochDescriptor, SlotClaim}, - vrf, AuthorityId, Configuration, Epoch, Randomness, Slot, TicketBody, TicketEnvelope, TicketId, - RANDOMNESS_LENGTH, SASSAFRAS_ENGINE_ID, + vrf, AuthorityId, Configuration, Epoch, InherentError, InherentType, Randomness, Slot, + TicketBody, TicketEnvelope, TicketId, INHERENT_IDENTIFIER, RANDOMNESS_LENGTH, + SASSAFRAS_ENGINE_ID, }; use sp_io::hashing; use sp_runtime::{ @@ -402,42 +403,24 @@ pub mod pallet { } } - #[pallet::validate_unsigned] - impl ValidateUnsigned for Pallet { + #[pallet::inherent] + impl ProvideInherent for Pallet { type Call = Call; + type Error = InherentError; + const INHERENT_IDENTIFIER: InherentIdentifier = INHERENT_IDENTIFIER; - 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() - } + fn create_inherent(data: &InherentData) -> Option { + let tickets = data + .get_data::(&INHERENT_IDENTIFIER) + .expect("Sassafras inherent data not correctly encoded") + .expect("Sassafras inherent data must be provided"); - // 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)); + let tickets = BoundedVec::truncate_from(tickets); + Some(Call::submit_tickets { tickets }) + } - ValidTransaction::with_tag_prefix("Sassafras") - .priority(TransactionPriority::max_value()) - .longevity(tickets_longevity as u64) - .and_provides(tickets_tag) - .propagate(true) - .build() + fn is_inherent(call: &Self::Call) -> bool { + matches!(call, Call::submit_tickets { .. }) } } } diff --git a/substrate/primitives/consensus/sassafras/Cargo.toml b/substrate/primitives/consensus/sassafras/Cargo.toml index 50348054da01..d513c0589c0c 100644 --- a/substrate/primitives/consensus/sassafras/Cargo.toml +++ b/substrate/primitives/consensus/sassafras/Cargo.toml @@ -25,6 +25,7 @@ sp-api = { path = "../../api", default-features = false } sp-application-crypto = { path = "../../application-crypto", default-features = false, features = ["bandersnatch-experimental"] } sp-consensus-slots = { path = "../slots", default-features = false } sp-core = { path = "../../core", default-features = false, features = ["bandersnatch-experimental"] } +sp-inherents = { path = "../../inherents", default-features = false } sp-runtime = { path = "../../runtime", default-features = false } [features] @@ -37,6 +38,7 @@ std = [ "sp-application-crypto/std", "sp-consensus-slots/std", "sp-core/std", + "sp-inherents/std", "sp-runtime/std", ] diff --git a/substrate/primitives/consensus/sassafras/src/lib.rs b/substrate/primitives/consensus/sassafras/src/lib.rs index 5ae8a79d85b5..a7c3ae40c1fa 100644 --- a/substrate/primitives/consensus/sassafras/src/lib.rs +++ b/substrate/primitives/consensus/sassafras/src/lib.rs @@ -27,6 +27,7 @@ use alloc::vec::Vec; use scale_codec::{Decode, Encode, MaxEncodedLen}; use scale_info::TypeInfo; use sp_core::crypto::KeyTypeId; +use sp_inherents::{InherentIdentifier, MakeFatalError}; use sp_runtime::{ConsensusEngineId, RuntimeDebug}; pub use sp_consensus_slots::{Slot, SlotDuration}; @@ -47,6 +48,15 @@ mod app { app_crypto!(bandersnatch, SASSAFRAS); } +/// Errors that can occur while checking the inherent. +pub type InherentError = MakeFatalError<()>; + +/// The type of the inherent +pub type InherentType = Vec; + +/// The identifier for the protocol inherent. +pub const INHERENT_IDENTIFIER: InherentIdentifier = *b"SASSAFRA"; + /// Key type identifier. pub const KEY_TYPE: KeyTypeId = sp_application_crypto::key_types::SASSAFRAS; From 457ee80a9be1c77a1424e7bc4010d9ca696716e4 Mon Sep 17 00:00:00 2001 From: Davide Galassi Date: Wed, 8 May 2024 12:56:44 +0200 Subject: [PATCH 13/42] Update lock file --- Cargo.lock | 1 + 1 file changed, 1 insertion(+) diff --git a/Cargo.lock b/Cargo.lock index 0379ba4befbe..d0613f5490c5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -19244,6 +19244,7 @@ dependencies = [ "sp-application-crypto", "sp-consensus-slots", "sp-core", + "sp-inherents", "sp-runtime", ] From 136673a79e5fa87627674b9d892cd8ef558198e5 Mon Sep 17 00:00:00 2001 From: Davide Galassi Date: Wed, 8 May 2024 12:58:37 +0200 Subject: [PATCH 14/42] Update lock --- Cargo.lock | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Cargo.lock b/Cargo.lock index 0dda623c14ab..7afeb2a00045 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -11124,6 +11124,7 @@ name = "pallet-sassafras" version = "0.3.5-dev" dependencies = [ "array-bytes", + "env_logger 0.11.3", "frame-benchmarking", "frame-support", "frame-system", @@ -19225,6 +19226,7 @@ dependencies = [ "sp-application-crypto", "sp-consensus-slots", "sp-core", + "sp-inherents", "sp-runtime", ] From a421a58c16db71aff701ef2ded4335f5794e9891 Mon Sep 17 00:00:00 2001 From: Davide Galassi Date: Wed, 8 May 2024 13:02:26 +0200 Subject: [PATCH 15/42] Add license to weights --- substrate/frame/sassafras/src/weights.rs | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/substrate/frame/sassafras/src/weights.rs b/substrate/frame/sassafras/src/weights.rs index a4f5d4aa17d5..109fe37d10f9 100644 --- a/substrate/frame/sassafras/src/weights.rs +++ b/substrate/frame/sassafras/src/weights.rs @@ -1,3 +1,19 @@ +// 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. //! Autogenerated weights for `pallet_sassafras` //! From 6d142e892ecdc07e4c61db5655aaaf59a1f5749d Mon Sep 17 00:00:00 2001 From: Davide Galassi Date: Wed, 8 May 2024 13:12:44 +0200 Subject: [PATCH 16/42] Pre-computed tickets data --- substrate/frame/sassafras/src/data/tickets.bin | Bin 0 -> 16244 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 substrate/frame/sassafras/src/data/tickets.bin diff --git a/substrate/frame/sassafras/src/data/tickets.bin b/substrate/frame/sassafras/src/data/tickets.bin new file mode 100644 index 0000000000000000000000000000000000000000..77288595374f8dc4b1cd3d88f7c63cce78fa7e5e GIT binary patch literal 16244 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 MW3h8tEkVHl115kvM*si- literal 0 HcmV?d00001 From 61a4692939c2487cd16382f4399c247ae29adde9 Mon Sep 17 00:00:00 2001 From: Davide Galassi Date: Thu, 9 May 2024 11:20:51 +0200 Subject: [PATCH 17/42] Rename TicketsMetadata to TicketsCounter --- substrate/frame/sassafras/src/lib.rs | 54 ++++++++++------------ substrate/frame/sassafras/src/tests.rs | 64 +++++++++++--------------- 2 files changed, 53 insertions(+), 65 deletions(-) diff --git a/substrate/frame/sassafras/src/lib.rs b/substrate/frame/sassafras/src/lib.rs index 63999ded7f73..2d5871afac4f 100644 --- a/substrate/frame/sassafras/src/lib.rs +++ b/substrate/frame/sassafras/src/lib.rs @@ -95,16 +95,12 @@ pub type AuthoritiesVec = WeakBoundedVec::MaxAutho /// 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 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], -} +/// Number of tickets available for current and next epoch. +/// +/// These tickets are held by the [`Tickets`] storage map. +/// +/// The entry to be used for the current epoch is computed as epoch index modulo 2. +pub type TicketsCounter = [u32; 2]; #[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Encode, Decode, MaxEncodedLen, TypeInfo)] struct TicketKey([u8; 32]); @@ -203,25 +199,27 @@ pub mod pallet { #[pallet::storage] pub(crate) type TicketsAccumulator = CountedStorageMap<_, Identity, TicketKey, TicketBody>; - /// Stored tickets metadata. + /// Tickets counters for the current and next epoch. #[pallet::storage] - pub type TicketsMeta = StorageValue<_, TicketsMetadata, ValueQuery>; + pub type TicketsCount = StorageValue<_, TicketsCounter, ValueQuery>; /// Tickets map. /// - /// The map holds tickets ids for the current and next epoch. + /// The map holds tickets identifiers for the current and next epoch. /// /// The key is a tuple composed by: - /// - `u8` equal to epoch's index modulo 2; + /// - `u8`: equal to epoch's index modulo 2; /// - `u32` equal to the ticket's index in an abstract sorted sequence of epoch's tickets. /// - /// For example, `N`-th ticket for epoch `E` has key: (E mod 2, N) + /// For example, the key for the `N`-th ticket for epoch `E` is `(E mod 2, N)` /// /// Note that the ticket's index `N` doesn't correspond to the offset of the associated - /// slot within the epoch. The assignment is computed using an *outside-in* strategy. + /// slot within the epoch. The assignment is computed using an *outside-in* strategy + /// and correctly returned by the [`slot_ticket`] method. /// /// Be aware that entries within this map are never removed, but only overwritten. - /// Last element index should be fetched from [`TicketsMeta`]. + /// The number of tickets available for epoch `E` is stored in the `E mod 2` entry + /// of [`TicketsCount`]. #[pallet::storage] pub type Tickets = StorageMap<_, Identity, (u8, u32), (TicketId, TicketBody)>; @@ -530,9 +528,9 @@ impl Pallet { Self::consume_tickets_accumulator(usize::MAX, epoch_tag); // Reset next epoch counter as we're start accumulating. - let mut metadata = TicketsMeta::::get(); - metadata.tickets_count[(epoch_tag ^ 1) as usize] = 0; - TicketsMeta::::set(metadata); + let mut tickets_count = TicketsCount::::get(); + tickets_count[(epoch_tag ^ 1) as usize] = 0; + TicketsCount::::set(tickets_count); } pub(crate) fn deposit_tickets(tickets: &[(TicketId, TicketBody)]) -> Result<(), Error> { @@ -560,15 +558,15 @@ impl Pallet { } fn consume_tickets_accumulator(max_items: usize, epoch_tag: u8) { - let mut metadata = TicketsMeta::::get(); + let mut tickets_count = TicketsCount::::get(); let mut accumulator_count = TicketsAccumulator::::count(); let mut idx = accumulator_count; for (key, body) in TicketsAccumulator::::drain().take(max_items) { idx -= 1; Tickets::::insert((epoch_tag, idx), (TicketId::from(key), body)); } - metadata.tickets_count[epoch_tag as usize] += (accumulator_count - idx); - TicketsMeta::::set(metadata); + tickets_count[epoch_tag as usize] += (accumulator_count - idx); + TicketsCount::::set(tickets_count); } // Call this function on epoch change to enact current epoch randomness. @@ -723,8 +721,8 @@ impl Pallet { return None } - let mut metadata = TicketsMeta::::get(); - let tickets_count = metadata.tickets_count[epoch_tag as usize]; + let mut tickets_count = TicketsCount::::get(); + let tickets_count = tickets_count[epoch_tag as usize]; if tickets_count <= slot_idx { return None } @@ -749,13 +747,11 @@ impl Pallet { /// Reset tickets related data. /// - /// Optimization note: tickets are left in place, only the metadata which counts - /// their number is resetted. + /// Optimization note: tickets are left in place, only the associated counters are resetted. fn reset_tickets_data() { // Reset sorted candidates (TODO: How this can fail?) + TicketsCount::::kill(); let _ = TicketsAccumulator::::clear(u32::MAX, None); - // Reset tickets metadata - TicketsMeta::::kill(); } /// Randomness buffer entries. diff --git a/substrate/frame/sassafras/src/tests.rs b/substrate/frame/sassafras/src/tests.rs index aa2995921adf..b8d2a688f767 100644 --- a/substrate/frame/sassafras/src/tests.rs +++ b/substrate/frame/sassafras/src/tests.rs @@ -326,9 +326,7 @@ fn slot_ticket_id_outside_in_fetch() { .enumerate() .for_each(|(i, t)| Tickets::::insert((1, i as u32), t)); - TicketsMeta::::set(TicketsMetadata { - tickets_count: [curr_count as u32, next_count as u32], - }); + TicketsCount::::set([curr_count as u32, next_count as u32]); EpochIndex::::set(*genesis_slot / Sassafras::epoch_length() as u64); // Before importing the first block the pallet always return `None` @@ -447,9 +445,6 @@ fn tickets_accumulator_works() { let epoch_tag = (epoch_idx % 2) as u8; let next_epoch_tag = epoch_tag ^ 1; - let mut metadata = TicketsMetadata::default(); - TicketsMeta::::set(metadata); - initialize_block(start_block, start_slot, Default::default(), &pairs[0]); // Append some tickets to the accumulator @@ -461,15 +456,15 @@ fn tickets_accumulator_works() { let end_block = start_block + epoch_length - 2; progress_to_block(end_block, &pairs[0]).unwrap(); - let metadata = TicketsMeta::::get(); - assert_eq!(metadata.tickets_count[epoch_tag as usize], 0); - assert_eq!(metadata.tickets_count[next_epoch_tag as usize], 0); + let tickets_count = TicketsCount::::get(); + assert_eq!(tickets_count[epoch_tag as usize], 0); + assert_eq!(tickets_count[next_epoch_tag as usize], 0); finalize_block(end_block); - let metadata = TicketsMeta::::get(); - assert_eq!(metadata.tickets_count[epoch_tag as usize], 0); - assert_eq!(metadata.tickets_count[next_epoch_tag as usize], e1_count as u32); + let tickets_count = TicketsCount::::get(); + assert_eq!(tickets_count[epoch_tag as usize], 0); + assert_eq!(tickets_count[next_epoch_tag as usize], e1_count as u32); // Start new epoch @@ -480,11 +475,11 @@ fn tickets_accumulator_works() { &pairs[0], ); - let metadata = TicketsMeta::::get(); let next_epoch_tag = epoch_tag; let epoch_tag = epoch_tag ^ 1; - assert_eq!(metadata.tickets_count[epoch_tag as usize], e1_count as u32); - assert_eq!(metadata.tickets_count[next_epoch_tag as usize], 0); + let tickets_count = TicketsCount::::get(); + assert_eq!(tickets_count[epoch_tag as usize], e1_count as u32); + assert_eq!(tickets_count[next_epoch_tag as usize], 0); // Append some tickets to the accumulator e2_tickets @@ -495,15 +490,15 @@ fn tickets_accumulator_works() { let end_block = end_block + epoch_length; progress_to_block(end_block, &pairs[0]).unwrap(); - let metadata = TicketsMeta::::get(); - assert_eq!(metadata.tickets_count[epoch_tag as usize], e1_count as u32); - assert_eq!(metadata.tickets_count[next_epoch_tag as usize], 0); + let tickets_count = TicketsCount::::get(); + assert_eq!(tickets_count[epoch_tag as usize], e1_count as u32); + assert_eq!(tickets_count[next_epoch_tag as usize], 0); finalize_block(end_block); - let metadata = TicketsMeta::::get(); - assert_eq!(metadata.tickets_count[epoch_tag as usize], e1_count as u32); - assert_eq!(metadata.tickets_count[next_epoch_tag as usize], e2_count as u32); + let tickets_count = TicketsCount::::get(); + assert_eq!(tickets_count[epoch_tag as usize], e1_count as u32); + assert_eq!(tickets_count[next_epoch_tag as usize], e2_count as u32); // Start new epoch initialize_block( @@ -513,11 +508,11 @@ fn tickets_accumulator_works() { &pairs[0], ); - let metadata = TicketsMeta::::get(); let next_epoch_tag = epoch_tag; let epoch_tag = epoch_tag ^ 1; - assert_eq!(metadata.tickets_count[epoch_tag as usize], e2_count as u32); - assert_eq!(metadata.tickets_count[next_epoch_tag as usize], 0); + let tickets_count = TicketsCount::::get(); + assert_eq!(tickets_count[epoch_tag as usize], e2_count as u32); + assert_eq!(tickets_count[next_epoch_tag as usize], 0); }); } @@ -531,9 +526,6 @@ fn incremental_accumulator_drain() { .collect(); new_test_ext(0).execute_with(|| { - let mut metadata = TicketsMetadata::default(); - TicketsMeta::::set(metadata); - tickets .iter() .for_each(|t| TicketsAccumulator::::insert(TicketKey::from(t.0), &t.1)); @@ -545,27 +537,27 @@ fn incremental_accumulator_drain() { assert!(accumulator.windows(2).all(|chunk| chunk[0].0 > chunk[1].0)); Sassafras::consume_tickets_accumulator(5, 0); - let meta = TicketsMeta::::get(); - assert_eq!(meta.tickets_count[0], 5); - assert_eq!(meta.tickets_count[1], 0); + let tickets_count = TicketsCount::::get(); + assert_eq!(tickets_count[0], 5); + assert_eq!(tickets_count[1], 0); tickets.iter().enumerate().skip(5).for_each(|(i, (id, _))| { let (id2, _) = Tickets::::get((0, i as u32)).unwrap(); assert_eq!(id, &id2); }); Sassafras::consume_tickets_accumulator(3, 0); - let meta = TicketsMeta::::get(); - assert_eq!(meta.tickets_count[0], 8); - assert_eq!(meta.tickets_count[1], 0); + let tickets_count = TicketsCount::::get(); + assert_eq!(tickets_count[0], 8); + assert_eq!(tickets_count[1], 0); tickets.iter().enumerate().skip(2).for_each(|(i, (id, _))| { let (id2, _) = Tickets::::get((0, i as u32)).unwrap(); assert_eq!(id, &id2); }); Sassafras::consume_tickets_accumulator(5, 0); - let meta = TicketsMeta::::get(); - assert_eq!(meta.tickets_count[0], 10); - assert_eq!(meta.tickets_count[1], 0); + let tickets_count = TicketsCount::::get(); + assert_eq!(tickets_count[0], 10); + assert_eq!(tickets_count[1], 0); tickets.iter().enumerate().for_each(|(i, (id, _))| { let (id2, _) = Tickets::::get((0, i as u32)).unwrap(); assert_eq!(id, &id2); From 5baff7b841260f7125b3e17eaee3ee5bebe83b68 Mon Sep 17 00:00:00 2001 From: Davide Galassi Date: Thu, 9 May 2024 11:25:30 +0200 Subject: [PATCH 18/42] Update weights --- substrate/frame/sassafras/src/weights.rs | 82 ++++++++++++------------ 1 file changed, 41 insertions(+), 41 deletions(-) diff --git a/substrate/frame/sassafras/src/weights.rs b/substrate/frame/sassafras/src/weights.rs index 109fe37d10f9..2d089f89a1f9 100644 --- a/substrate/frame/sassafras/src/weights.rs +++ b/substrate/frame/sassafras/src/weights.rs @@ -18,7 +18,7 @@ //! Autogenerated weights for `pallet_sassafras` //! //! THIS FILE WAS AUTO-GENERATED USING THE SUBSTRATE BENCHMARK CLI VERSION 32.0.0 -//! DATE: 2024-05-08, STEPS: `20`, REPEAT: `3`, LOW RANGE: `[]`, HIGH RANGE: `[]` +//! DATE: 2024-05-09, STEPS: `20`, REPEAT: `3`, LOW RANGE: `[]`, HIGH RANGE: `[]` //! WORST CASE MAP SIZE: `1000000` //! HOSTNAME: `behemoth`, CPU: `AMD Ryzen Threadripper 3970X 32-Core Processor` //! WASM-EXECUTION: `Compiled`, CHAIN: `Some("dev")`, DB CACHE: `1024` @@ -76,8 +76,8 @@ impl WeightInfo for SubstrateWeight { // Proof Size summary in bytes: // Measured: `270` // Estimated: `1755` - // Minimum execution time: 427_910_000 picoseconds. - Weight::from_parts(428_451_000, 1755) + // Minimum execution time: 413_947_000 picoseconds. + Weight::from_parts(414_419_000, 1755) .saturating_add(T::DbWeight::get().reads(3_u64)) .saturating_add(T::DbWeight::get().writes(3_u64)) } @@ -93,8 +93,8 @@ impl WeightInfo for SubstrateWeight { /// Proof: `Sassafras::RandomnessBuf` (`max_values`: Some(1), `max_size`: Some(128), added: 623, mode: `MaxEncodedLen`) /// Storage: `System::Digest` (r:1 w:1) /// Proof: `System::Digest` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) - /// Storage: `Sassafras::TicketsMeta` (r:1 w:1) - /// Proof: `Sassafras::TicketsMeta` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `MaxEncodedLen`) + /// Storage: `Sassafras::TicketsCount` (r:1 w:1) + /// Proof: `Sassafras::TicketsCount` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `MaxEncodedLen`) /// Storage: `Sassafras::CounterForTicketsAccumulator` (r:1 w:1) /// Proof: `Sassafras::CounterForTicketsAccumulator` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`) /// Storage: `Sassafras::TicketsAccumulator` (r:1001 w:1000) @@ -111,12 +111,12 @@ impl WeightInfo for SubstrateWeight { // Proof Size summary in bytes: // Measured: `590548 + x * (33 ±0) + y * (37 ±0)` // Estimated: `592034 + x * (33 ±0) + y * (2641 ±0)` - // Minimum execution time: 138_951_898_000 picoseconds. - Weight::from_parts(135_719_523_137, 592034) - // Standard Error: 5_842_563 - .saturating_add(Weight::from_parts(154_854_264, 0).saturating_mul(x.into())) - // Standard Error: 645_250 - .saturating_add(Weight::from_parts(4_311_218, 0).saturating_mul(y.into())) + // Minimum execution time: 137_962_332_000 picoseconds. + Weight::from_parts(133_101_423_376, 592034) + // Standard Error: 4_031_332 + .saturating_add(Weight::from_parts(155_790_673, 0).saturating_mul(x.into())) + // Standard Error: 445_218 + .saturating_add(Weight::from_parts(5_992_692, 0).saturating_mul(y.into())) .saturating_add(T::DbWeight::get().reads(9_u64)) .saturating_add(T::DbWeight::get().reads((1_u64).saturating_mul(y.into()))) .saturating_add(T::DbWeight::get().writes(8_u64)) @@ -141,10 +141,10 @@ impl WeightInfo for SubstrateWeight { // Proof Size summary in bytes: // Measured: `1029` // Estimated: `4787 + x * (2641 ±0)` - // Minimum execution time: 52_109_411_000 picoseconds. - Weight::from_parts(38_584_018_164, 4787) - // Standard Error: 62_507_167 - .saturating_add(Weight::from_parts(14_381_572_906, 0).saturating_mul(x.into())) + // Minimum execution time: 51_513_822_000 picoseconds. + Weight::from_parts(37_711_340_308, 4787) + // Standard Error: 14_384_837 + .saturating_add(Weight::from_parts(14_459_764_548, 0).saturating_mul(x.into())) .saturating_add(T::DbWeight::get().reads(5_u64)) .saturating_add(T::DbWeight::get().reads((1_u64).saturating_mul(x.into()))) .saturating_add(T::DbWeight::get().writes(1_u64)) @@ -160,10 +160,10 @@ impl WeightInfo for SubstrateWeight { // Proof Size summary in bytes: // Measured: `590458` // Estimated: `591809` - // Minimum execution time: 131_132_781_000 picoseconds. - Weight::from_parts(132_019_264_471, 591809) - // Standard Error: 7_012_691 - .saturating_add(Weight::from_parts(195_019_609, 0).saturating_mul(x.into())) + // Minimum execution time: 135_242_477_000 picoseconds. + Weight::from_parts(135_344_310_854, 591809) + // Standard Error: 3_081_978 + .saturating_add(Weight::from_parts(158_028_987, 0).saturating_mul(x.into())) .saturating_add(T::DbWeight::get().reads(1_u64)) .saturating_add(T::DbWeight::get().writes(1_u64)) } @@ -173,8 +173,8 @@ impl WeightInfo for SubstrateWeight { // Proof Size summary in bytes: // Measured: `590458` // Estimated: `591809` - // Minimum execution time: 55_893_928_000 picoseconds. - Weight::from_parts(56_862_553_000, 591809) + // Minimum execution time: 54_152_038_000 picoseconds. + Weight::from_parts(54_511_296_000, 591809) .saturating_add(T::DbWeight::get().reads(1_u64)) } } @@ -195,8 +195,8 @@ impl WeightInfo for () { // Proof Size summary in bytes: // Measured: `270` // Estimated: `1755` - // Minimum execution time: 427_910_000 picoseconds. - Weight::from_parts(428_451_000, 1755) + // Minimum execution time: 413_947_000 picoseconds. + Weight::from_parts(414_419_000, 1755) .saturating_add(RocksDbWeight::get().reads(3_u64)) .saturating_add(RocksDbWeight::get().writes(3_u64)) } @@ -212,8 +212,8 @@ impl WeightInfo for () { /// Proof: `Sassafras::RandomnessBuf` (`max_values`: Some(1), `max_size`: Some(128), added: 623, mode: `MaxEncodedLen`) /// Storage: `System::Digest` (r:1 w:1) /// Proof: `System::Digest` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) - /// Storage: `Sassafras::TicketsMeta` (r:1 w:1) - /// Proof: `Sassafras::TicketsMeta` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `MaxEncodedLen`) + /// Storage: `Sassafras::TicketsCount` (r:1 w:1) + /// Proof: `Sassafras::TicketsCount` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `MaxEncodedLen`) /// Storage: `Sassafras::CounterForTicketsAccumulator` (r:1 w:1) /// Proof: `Sassafras::CounterForTicketsAccumulator` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`) /// Storage: `Sassafras::TicketsAccumulator` (r:1001 w:1000) @@ -230,12 +230,12 @@ impl WeightInfo for () { // Proof Size summary in bytes: // Measured: `590548 + x * (33 ±0) + y * (37 ±0)` // Estimated: `592034 + x * (33 ±0) + y * (2641 ±0)` - // Minimum execution time: 138_951_898_000 picoseconds. - Weight::from_parts(135_719_523_137, 592034) - // Standard Error: 5_842_563 - .saturating_add(Weight::from_parts(154_854_264, 0).saturating_mul(x.into())) - // Standard Error: 645_250 - .saturating_add(Weight::from_parts(4_311_218, 0).saturating_mul(y.into())) + // Minimum execution time: 137_962_332_000 picoseconds. + Weight::from_parts(133_101_423_376, 592034) + // Standard Error: 4_031_332 + .saturating_add(Weight::from_parts(155_790_673, 0).saturating_mul(x.into())) + // Standard Error: 445_218 + .saturating_add(Weight::from_parts(5_992_692, 0).saturating_mul(y.into())) .saturating_add(RocksDbWeight::get().reads(9_u64)) .saturating_add(RocksDbWeight::get().reads((1_u64).saturating_mul(y.into()))) .saturating_add(RocksDbWeight::get().writes(8_u64)) @@ -260,10 +260,10 @@ impl WeightInfo for () { // Proof Size summary in bytes: // Measured: `1029` // Estimated: `4787 + x * (2641 ±0)` - // Minimum execution time: 52_109_411_000 picoseconds. - Weight::from_parts(38_584_018_164, 4787) - // Standard Error: 62_507_167 - .saturating_add(Weight::from_parts(14_381_572_906, 0).saturating_mul(x.into())) + // Minimum execution time: 51_513_822_000 picoseconds. + Weight::from_parts(37_711_340_308, 4787) + // Standard Error: 14_384_837 + .saturating_add(Weight::from_parts(14_459_764_548, 0).saturating_mul(x.into())) .saturating_add(RocksDbWeight::get().reads(5_u64)) .saturating_add(RocksDbWeight::get().reads((1_u64).saturating_mul(x.into()))) .saturating_add(RocksDbWeight::get().writes(1_u64)) @@ -279,10 +279,10 @@ impl WeightInfo for () { // Proof Size summary in bytes: // Measured: `590458` // Estimated: `591809` - // Minimum execution time: 131_132_781_000 picoseconds. - Weight::from_parts(132_019_264_471, 591809) - // Standard Error: 7_012_691 - .saturating_add(Weight::from_parts(195_019_609, 0).saturating_mul(x.into())) + // Minimum execution time: 135_242_477_000 picoseconds. + Weight::from_parts(135_344_310_854, 591809) + // Standard Error: 3_081_978 + .saturating_add(Weight::from_parts(158_028_987, 0).saturating_mul(x.into())) .saturating_add(RocksDbWeight::get().reads(1_u64)) .saturating_add(RocksDbWeight::get().writes(1_u64)) } @@ -292,8 +292,8 @@ impl WeightInfo for () { // Proof Size summary in bytes: // Measured: `590458` // Estimated: `591809` - // Minimum execution time: 55_893_928_000 picoseconds. - Weight::from_parts(56_862_553_000, 591809) + // Minimum execution time: 54_152_038_000 picoseconds. + Weight::from_parts(54_511_296_000, 591809) .saturating_add(RocksDbWeight::get().reads(1_u64)) } } From 257432f6a7149e2d9fe0a4a3d9d4892cae1e5217 Mon Sep 17 00:00:00 2001 From: Davide Galassi Date: Thu, 9 May 2024 17:15:02 +0200 Subject: [PATCH 19/42] Nits --- substrate/frame/sassafras/src/benchmarking.rs | 5 +- substrate/frame/sassafras/src/lib.rs | 160 +++++++++--------- substrate/frame/sassafras/src/tests.rs | 52 +++--- substrate/frame/sassafras/src/weights.rs | 80 ++++----- 4 files changed, 146 insertions(+), 151 deletions(-) diff --git a/substrate/frame/sassafras/src/benchmarking.rs b/substrate/frame/sassafras/src/benchmarking.rs index 6ba07dae9d40..9c03eb16274a 100644 --- a/substrate/frame/sassafras/src/benchmarking.rs +++ b/substrate/frame/sassafras/src/benchmarking.rs @@ -47,9 +47,6 @@ fn dummy_vrf_signature() -> VrfSignature { mod benchmarks { use super::*; - // For first block (#1) we do some extra operation. - // But is a one shot operation, so we don't account for it here. - // We use 0, as it will be the path used by all the blocks with n != 1 #[benchmark] fn on_initialize() { let block_num = BlockNumberFor::::from(0u32); @@ -122,7 +119,7 @@ mod benchmarks { } #[benchmark] - fn submit_tickets(x: Linear<1, 20>) { + fn submit_tickets(x: Linear<1, 16>) { let tickets_count = x as usize; let mut raw_data = TICKETS_DATA; diff --git a/substrate/frame/sassafras/src/lib.rs b/substrate/frame/sassafras/src/lib.rs index 2d5871afac4f..6f367ce9855c 100644 --- a/substrate/frame/sassafras/src/lib.rs +++ b/substrate/frame/sassafras/src/lib.rs @@ -24,21 +24,20 @@ //! 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 +//! Each validator signs some unbiasable 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. +//! We want to keep lottery winners secret, i.e. do not disclose 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. +//! A valid ticket is claimed by an honest validator 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". +//! To prevent submission of invalid tickets, resulting in empty slots, the validator +//! when submitting the ticket accompanies it with a zk-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 who I am, but my public key is among those of the nominated +//! validators". #![allow(unused)] #![deny(warnings)] @@ -50,7 +49,10 @@ use scale_codec::{Decode, Encode, MaxEncodedLen}; use scale_info::TypeInfo; use frame_support::{ - dispatch::DispatchResult, traits::Get, weights::Weight, BoundedVec, WeakBoundedVec, + dispatch::DispatchResult, + traits::{ConstU32, Get}, + weights::Weight, + BoundedVec, WeakBoundedVec, }; use frame_system::pallet_prelude::BlockNumberFor; use sp_consensus_sassafras::{ @@ -86,14 +88,17 @@ const RANDOMNESS_VRF_CONTEXT: &[u8] = b"SassafrasOnChainRandomness"; const EPOCH_TAIL_FRACTION: u32 = 6; -/// Randomness buffer type. +/// Max number of tickets that can be submitted in one block. +const TICKETS_CHUNK_MAX_LENGTH: u32 = 16; + +/// Randomness buffer. pub type RandomnessBuffer = [Randomness; 4]; -/// Authorities bounded vector convenience type. +/// Authorities sequence. pub type AuthoritiesVec = WeakBoundedVec::MaxAuthorities>; -/// Epoch length defined by the configuration. -pub type EpochLengthFor = ::EpochLength; +/// Tickets chunk. +pub type TicketsVec = BoundedVec>; /// Number of tickets available for current and next epoch. /// @@ -102,20 +107,28 @@ pub type EpochLengthFor = ::EpochLength; /// The entry to be used for the current epoch is computed as epoch index modulo 2. pub type TicketsCounter = [u32; 2]; +/// Key used for the tickets accumulator map. +/// +/// Ticket keys are constructed by taking the bitwise negation of the ticket identifier. +/// As the tickets accumulator sorts entries according to the key values from smaller +/// to larger, we end up with a sequence of tickets identifiers sorted from larger to +/// smaller. +/// +/// This strategy comes handy when we quickly need to check if a new ticket chunk has been +/// completely absorbed by the accumulator, when this is already full and without loading +/// the whole sequence in memory. #[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Encode, Decode, MaxEncodedLen, TypeInfo)] struct TicketKey([u8; 32]); impl From for TicketKey { fn from(mut value: TicketId) -> Self { - value.0.iter_mut().for_each(|b| *b ^= 0xff); - TicketKey(value.0) + TicketKey(value.0.map(|b| !b)) } } impl From for TicketId { fn from(mut value: TicketKey) -> Self { - value.0.iter_mut().for_each(|b| *b ^= 0xff); - TicketId(value.0) + TicketId(value.0.map(|b| !b)) } } @@ -336,10 +349,7 @@ pub mod pallet { T::WeightInfo::submit_tickets(tickets.len() as u32), DispatchClass::Mandatory ))] - pub fn submit_tickets( - origin: OriginFor, - tickets: BoundedVec>, - ) -> DispatchResult { + pub fn submit_tickets(origin: OriginFor, tickets: TicketsVec) -> DispatchResult { ensure_none(origin)?; debug!(target: LOG_TARGET, "Received {} tickets", tickets.len()); @@ -356,16 +366,16 @@ pub mod pallet { return Err("Ring verifier key not initialized".into()) }; - // Get next epoch params + // Get next epoch parameters let randomness = Self::next_randomness(); - let next_authorities = Self::next_authorities(); + let authorities = Self::next_authorities(); // Compute tickets threshold let ticket_threshold = sp_consensus_sassafras::ticket_id_threshold( T::RedundancyFactor::get(), epoch_length as u32, T::AttemptsNumber::get(), - next_authorities.len() as u32, + authorities.len() as u32, ); let mut candidates = Vec::new(); @@ -425,33 +435,6 @@ pub mod pallet { // 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() == 0 - } - - /// 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) - (*slot % ::EpochLength::get() as u64) as u32 - } - pub(crate) fn update_ring_verifier(authorities: &[AuthorityId]) { debug!(target: LOG_TARGET, "Loading ring context"); let Some(ring_ctx) = RingContext::::get() else { @@ -626,15 +609,12 @@ impl Pallet { // Keep track of the actual first slot used (may not be zero based). EpochIndex::::put(Self::epoch_index(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. + // Properly initialize randomness using genesis hash. + // This is important to guarantee that a different set of tickets are produced for + // different chains sharing the same ring parameters. 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 mut accumulator = RandomnessBuffer::default(); - accumulator[0] = hashing::blake2_256(buf.as_slice()); + accumulator[0] = hashing::blake2_256(genesis_hash.as_ref()); accumulator[1] = hashing::blake2_256(&accumulator[0]); accumulator[2] = hashing::blake2_256(&accumulator[1]); accumulator[3] = hashing::blake2_256(&accumulator[2]); @@ -648,28 +628,6 @@ impl Pallet { Self::deposit_next_epoch_descriptor_digest(next_epoch); } - /// Static protocol configuration. - pub fn protocol_config() -> Configuration { - Configuration { - epoch_length: T::EpochLength::get(), - epoch_tail_length: T::EpochLength::get() / 6, - max_authorities: T::MaxAuthorities::get(), - redundancy_factor: T::RedundancyFactor::get(), - attempts_number: T::AttemptsNumber::get(), - } - } - - /// Current epoch information. - pub fn current_epoch() -> Epoch { - let curr_slot = *Self::current_slot(); - let epoch_start = curr_slot - curr_slot % Self::epoch_length() as u64; - Epoch { - start: epoch_start.into(), - authorities: Self::authorities().into_inner(), - randomness: Self::randomness_buf(), - } - } - /// 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, @@ -754,6 +712,28 @@ impl Pallet { let _ = TicketsAccumulator::::clear(u32::MAX, None); } + /// Static protocol configuration. + pub fn protocol_config() -> Configuration { + Configuration { + epoch_length: T::EpochLength::get(), + epoch_tail_length: T::EpochLength::get() / 6, + max_authorities: T::MaxAuthorities::get(), + redundancy_factor: T::RedundancyFactor::get(), + attempts_number: T::AttemptsNumber::get(), + } + } + + /// Current epoch information. + pub fn current_epoch() -> Epoch { + let curr_slot = *Self::current_slot(); + let epoch_start = curr_slot - curr_slot % Self::epoch_length() as u64; + Epoch { + start: epoch_start.into(), + authorities: Self::authorities().into_inner(), + randomness: Self::randomness_buf(), + } + } + /// Randomness buffer entries. /// /// Assuming we're executing a block during epoch with index `N`. @@ -790,6 +770,24 @@ impl Pallet { Self::randomness(0) } + /// Determine whether an epoch change should take place at this block. + pub(crate) fn should_end_epoch(block_num: BlockNumberFor) -> bool { + Self::current_slot_index() == 0 && block_num != Zero::zero() + } + + /// 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) + (*slot % ::EpochLength::get() as u64) as u32 + } + /// Current epoch index. pub fn curr_epoch_index() -> u64 { Self::epoch_index(Self::current_slot()) diff --git a/substrate/frame/sassafras/src/tests.rs b/substrate/frame/sassafras/src/tests.rs index b8d2a688f767..eb0c9d8e550d 100644 --- a/substrate/frame/sassafras/src/tests.rs +++ b/substrate/frame/sassafras/src/tests.rs @@ -109,7 +109,7 @@ fn deposit_tickets_failure() { fn post_genesis_randomness_initialization() { let (pairs, mut ext) = new_test_ext_with_pairs(1, false); let pair = &pairs[0]; - let first_slot = GENESIS_SLOT.into(); + let first_slot = (GENESIS_SLOT + 1).into(); ext.execute_with(|| { let genesis_randomness = Sassafras::randomness_buf(); @@ -120,10 +120,10 @@ fn post_genesis_randomness_initialization() { let _ = initialize_block(1, first_slot, [0x00; 32].into(), pair); let randomness = Sassafras::randomness_buf(); - prefix_eq!(randomness[0], h2b("f0d42f6b")); - prefix_eq!(randomness[1], h2b("28702cc1")); - prefix_eq!(randomness[2], h2b("a2bd8b31")); - prefix_eq!(randomness[3], h2b("76d83666")); + prefix_eq!(randomness[0], h2b("89eb0d6a")); + prefix_eq!(randomness[1], h2b("4e8c71d2")); + prefix_eq!(randomness[2], h2b("3a4c0005")); + prefix_eq!(randomness[3], h2b("0dd43c54")); let (id1, _) = make_ticket_body(0, pair); @@ -135,10 +135,10 @@ fn post_genesis_randomness_initialization() { let _ = initialize_block(1, first_slot, [0xff; 32].into(), pair); let randomness = Sassafras::randomness_buf(); - prefix_eq!(randomness[0], h2b("548534cf")); - prefix_eq!(randomness[1], h2b("5b9cb838")); - prefix_eq!(randomness[2], h2b("192a2a4b")); - prefix_eq!(randomness[3], h2b("2e152bf9")); + prefix_eq!(randomness[0], h2b("e2021160")); + prefix_eq!(randomness[1], h2b("3b0c0905")); + prefix_eq!(randomness[2], h2b("632ac0d9")); + prefix_eq!(randomness[3], h2b("575088c3")); let (id2, _) = make_ticket_body(0, pair); @@ -150,13 +150,13 @@ fn post_genesis_randomness_initialization() { #[test] fn on_first_block() { let (pairs, mut ext) = new_test_ext_with_pairs(4, false); - let start_slot = GENESIS_SLOT.into(); + let start_slot = (GENESIS_SLOT + 1).into(); let start_block = 1; ext.execute_with(|| { let common_assertions = |initialized| { assert_eq!(Sassafras::current_slot(), start_slot); - assert_eq!(Sassafras::current_slot_index(), 0); + assert_eq!(Sassafras::current_slot_index(), 1); assert_eq!(PostInitCache::::exists(), initialized); }; @@ -168,10 +168,10 @@ fn on_first_block() { common_assertions(true); let post_init_randomness = Sassafras::randomness_buf(); - prefix_eq!(post_init_randomness[0], h2b("f0d42f6b")); - prefix_eq!(post_init_randomness[1], h2b("28702cc1")); - prefix_eq!(post_init_randomness[2], h2b("a2bd8b31")); - prefix_eq!(post_init_randomness[3], h2b("76d83666")); + prefix_eq!(post_init_randomness[0], h2b("89eb0d6a")); + prefix_eq!(post_init_randomness[1], h2b("4e8c71d2")); + prefix_eq!(post_init_randomness[2], h2b("3a4c0005")); + prefix_eq!(post_init_randomness[3], h2b("0dd43c54")); // // Post-finalization status @@ -179,7 +179,7 @@ fn on_first_block() { common_assertions(false); let post_fini_randomness = Sassafras::randomness_buf(); - prefix_eq!(post_fini_randomness[0], h2b("6b117a72")); + prefix_eq!(post_fini_randomness[0], h2b("173b91b3")); prefix_eq!(post_fini_randomness[1], post_init_randomness[1]); prefix_eq!(post_fini_randomness[2], post_init_randomness[2]); prefix_eq!(post_fini_randomness[3], post_init_randomness[3]); @@ -204,7 +204,7 @@ fn on_first_block() { #[test] fn on_normal_block() { let (pairs, mut ext) = new_test_ext_with_pairs(4, false); - let start_slot = GENESIS_SLOT.into(); + let start_slot = (GENESIS_SLOT + 1).into(); let start_block = 1; let end_block = start_block + 1; @@ -220,7 +220,7 @@ fn on_normal_block() { let common_assertions = |initialized| { assert_eq!(Sassafras::current_slot(), start_slot + 1); - assert_eq!(Sassafras::current_slot_index(), 1); + assert_eq!(Sassafras::current_slot_index(), 2); assert_eq!(PostInitCache::::exists(), initialized); }; @@ -228,10 +228,10 @@ fn on_normal_block() { common_assertions(true); let post_init_randomness = Sassafras::randomness_buf(); - prefix_eq!(post_init_randomness[0], h2b("6b117a72")); - prefix_eq!(post_init_randomness[1], h2b("28702cc1")); - prefix_eq!(post_init_randomness[2], h2b("a2bd8b31")); - prefix_eq!(post_init_randomness[3], h2b("76d83666")); + prefix_eq!(post_init_randomness[0], h2b("173b91b3")); + prefix_eq!(post_init_randomness[1], h2b("4e8c71d2")); + prefix_eq!(post_init_randomness[2], h2b("3a4c0005")); + prefix_eq!(post_init_randomness[3], h2b("0dd43c54")); let header = finalize_block(end_block); @@ -239,10 +239,10 @@ fn on_normal_block() { common_assertions(false); let post_fini_randomness = Sassafras::randomness_buf(); - prefix_eq!(post_fini_randomness[0], h2b("3489b933")); - prefix_eq!(post_fini_randomness[1], h2b("28702cc1")); - prefix_eq!(post_fini_randomness[2], h2b("a2bd8b31")); - prefix_eq!(post_fini_randomness[3], h2b("76d83666")); + prefix_eq!(post_fini_randomness[0], h2b("800f7825")); + prefix_eq!(post_fini_randomness[1], post_init_randomness[1]); + prefix_eq!(post_fini_randomness[2], post_init_randomness[2]); + prefix_eq!(post_fini_randomness[3], post_init_randomness[3]); // Header data check diff --git a/substrate/frame/sassafras/src/weights.rs b/substrate/frame/sassafras/src/weights.rs index 2d089f89a1f9..9f09f6e2d169 100644 --- a/substrate/frame/sassafras/src/weights.rs +++ b/substrate/frame/sassafras/src/weights.rs @@ -76,8 +76,8 @@ impl WeightInfo for SubstrateWeight { // Proof Size summary in bytes: // Measured: `270` // Estimated: `1755` - // Minimum execution time: 413_947_000 picoseconds. - Weight::from_parts(414_419_000, 1755) + // Minimum execution time: 417_410_000 picoseconds. + Weight::from_parts(419_042_000, 1755) .saturating_add(T::DbWeight::get().reads(3_u64)) .saturating_add(T::DbWeight::get().writes(3_u64)) } @@ -111,12 +111,12 @@ impl WeightInfo for SubstrateWeight { // Proof Size summary in bytes: // Measured: `590548 + x * (33 ±0) + y * (37 ±0)` // Estimated: `592034 + x * (33 ±0) + y * (2641 ±0)` - // Minimum execution time: 137_962_332_000 picoseconds. - Weight::from_parts(133_101_423_376, 592034) - // Standard Error: 4_031_332 - .saturating_add(Weight::from_parts(155_790_673, 0).saturating_mul(x.into())) - // Standard Error: 445_218 - .saturating_add(Weight::from_parts(5_992_692, 0).saturating_mul(y.into())) + // Minimum execution time: 137_787_571_000 picoseconds. + Weight::from_parts(132_666_006_991, 592034) + // Standard Error: 4_836_429 + .saturating_add(Weight::from_parts(188_608_489, 0).saturating_mul(x.into())) + // Standard Error: 534_133 + .saturating_add(Weight::from_parts(5_816_837, 0).saturating_mul(y.into())) .saturating_add(T::DbWeight::get().reads(9_u64)) .saturating_add(T::DbWeight::get().reads((1_u64).saturating_mul(y.into()))) .saturating_add(T::DbWeight::get().writes(8_u64)) @@ -134,17 +134,17 @@ impl WeightInfo for SubstrateWeight { /// Proof: `Sassafras::NextAuthorities` (`max_values`: Some(1), `max_size`: Some(3302), added: 3797, mode: `MaxEncodedLen`) /// Storage: `Sassafras::CounterForTicketsAccumulator` (r:1 w:1) /// Proof: `Sassafras::CounterForTicketsAccumulator` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`) - /// Storage: `Sassafras::TicketsAccumulator` (r:20 w:20) + /// Storage: `Sassafras::TicketsAccumulator` (r:16 w:16) /// Proof: `Sassafras::TicketsAccumulator` (`max_values`: None, `max_size`: Some(166), added: 2641, mode: `MaxEncodedLen`) - /// The range of component `x` is `[1, 20]`. + /// The range of component `x` is `[1, 16]`. fn submit_tickets(x: u32, ) -> Weight { // Proof Size summary in bytes: // Measured: `1029` // Estimated: `4787 + x * (2641 ±0)` - // Minimum execution time: 51_513_822_000 picoseconds. - Weight::from_parts(37_711_340_308, 4787) - // Standard Error: 14_384_837 - .saturating_add(Weight::from_parts(14_459_764_548, 0).saturating_mul(x.into())) + // Minimum execution time: 51_637_107_000 picoseconds. + Weight::from_parts(37_790_802_200, 4787) + // Standard Error: 26_574_579 + .saturating_add(Weight::from_parts(14_279_858_343, 0).saturating_mul(x.into())) .saturating_add(T::DbWeight::get().reads(5_u64)) .saturating_add(T::DbWeight::get().reads((1_u64).saturating_mul(x.into()))) .saturating_add(T::DbWeight::get().writes(1_u64)) @@ -160,10 +160,10 @@ impl WeightInfo for SubstrateWeight { // Proof Size summary in bytes: // Measured: `590458` // Estimated: `591809` - // Minimum execution time: 135_242_477_000 picoseconds. - Weight::from_parts(135_344_310_854, 591809) - // Standard Error: 3_081_978 - .saturating_add(Weight::from_parts(158_028_987, 0).saturating_mul(x.into())) + // Minimum execution time: 133_456_343_000 picoseconds. + Weight::from_parts(134_102_722_298, 591809) + // Standard Error: 2_598_318 + .saturating_add(Weight::from_parts(162_531_608, 0).saturating_mul(x.into())) .saturating_add(T::DbWeight::get().reads(1_u64)) .saturating_add(T::DbWeight::get().writes(1_u64)) } @@ -173,8 +173,8 @@ impl WeightInfo for SubstrateWeight { // Proof Size summary in bytes: // Measured: `590458` // Estimated: `591809` - // Minimum execution time: 54_152_038_000 picoseconds. - Weight::from_parts(54_511_296_000, 591809) + // Minimum execution time: 54_506_001_000 picoseconds. + Weight::from_parts(54_520_769_000, 591809) .saturating_add(T::DbWeight::get().reads(1_u64)) } } @@ -195,8 +195,8 @@ impl WeightInfo for () { // Proof Size summary in bytes: // Measured: `270` // Estimated: `1755` - // Minimum execution time: 413_947_000 picoseconds. - Weight::from_parts(414_419_000, 1755) + // Minimum execution time: 417_410_000 picoseconds. + Weight::from_parts(419_042_000, 1755) .saturating_add(RocksDbWeight::get().reads(3_u64)) .saturating_add(RocksDbWeight::get().writes(3_u64)) } @@ -230,12 +230,12 @@ impl WeightInfo for () { // Proof Size summary in bytes: // Measured: `590548 + x * (33 ±0) + y * (37 ±0)` // Estimated: `592034 + x * (33 ±0) + y * (2641 ±0)` - // Minimum execution time: 137_962_332_000 picoseconds. - Weight::from_parts(133_101_423_376, 592034) - // Standard Error: 4_031_332 - .saturating_add(Weight::from_parts(155_790_673, 0).saturating_mul(x.into())) - // Standard Error: 445_218 - .saturating_add(Weight::from_parts(5_992_692, 0).saturating_mul(y.into())) + // Minimum execution time: 137_787_571_000 picoseconds. + Weight::from_parts(132_666_006_991, 592034) + // Standard Error: 4_836_429 + .saturating_add(Weight::from_parts(188_608_489, 0).saturating_mul(x.into())) + // Standard Error: 534_133 + .saturating_add(Weight::from_parts(5_816_837, 0).saturating_mul(y.into())) .saturating_add(RocksDbWeight::get().reads(9_u64)) .saturating_add(RocksDbWeight::get().reads((1_u64).saturating_mul(y.into()))) .saturating_add(RocksDbWeight::get().writes(8_u64)) @@ -253,17 +253,17 @@ impl WeightInfo for () { /// Proof: `Sassafras::NextAuthorities` (`max_values`: Some(1), `max_size`: Some(3302), added: 3797, mode: `MaxEncodedLen`) /// Storage: `Sassafras::CounterForTicketsAccumulator` (r:1 w:1) /// Proof: `Sassafras::CounterForTicketsAccumulator` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`) - /// Storage: `Sassafras::TicketsAccumulator` (r:20 w:20) + /// Storage: `Sassafras::TicketsAccumulator` (r:16 w:16) /// Proof: `Sassafras::TicketsAccumulator` (`max_values`: None, `max_size`: Some(166), added: 2641, mode: `MaxEncodedLen`) - /// The range of component `x` is `[1, 20]`. + /// The range of component `x` is `[1, 16]`. fn submit_tickets(x: u32, ) -> Weight { // Proof Size summary in bytes: // Measured: `1029` // Estimated: `4787 + x * (2641 ±0)` - // Minimum execution time: 51_513_822_000 picoseconds. - Weight::from_parts(37_711_340_308, 4787) - // Standard Error: 14_384_837 - .saturating_add(Weight::from_parts(14_459_764_548, 0).saturating_mul(x.into())) + // Minimum execution time: 51_637_107_000 picoseconds. + Weight::from_parts(37_790_802_200, 4787) + // Standard Error: 26_574_579 + .saturating_add(Weight::from_parts(14_279_858_343, 0).saturating_mul(x.into())) .saturating_add(RocksDbWeight::get().reads(5_u64)) .saturating_add(RocksDbWeight::get().reads((1_u64).saturating_mul(x.into()))) .saturating_add(RocksDbWeight::get().writes(1_u64)) @@ -279,10 +279,10 @@ impl WeightInfo for () { // Proof Size summary in bytes: // Measured: `590458` // Estimated: `591809` - // Minimum execution time: 135_242_477_000 picoseconds. - Weight::from_parts(135_344_310_854, 591809) - // Standard Error: 3_081_978 - .saturating_add(Weight::from_parts(158_028_987, 0).saturating_mul(x.into())) + // Minimum execution time: 133_456_343_000 picoseconds. + Weight::from_parts(134_102_722_298, 591809) + // Standard Error: 2_598_318 + .saturating_add(Weight::from_parts(162_531_608, 0).saturating_mul(x.into())) .saturating_add(RocksDbWeight::get().reads(1_u64)) .saturating_add(RocksDbWeight::get().writes(1_u64)) } @@ -292,8 +292,8 @@ impl WeightInfo for () { // Proof Size summary in bytes: // Measured: `590458` // Estimated: `591809` - // Minimum execution time: 54_152_038_000 picoseconds. - Weight::from_parts(54_511_296_000, 591809) + // Minimum execution time: 54_506_001_000 picoseconds. + Weight::from_parts(54_520_769_000, 591809) .saturating_add(RocksDbWeight::get().reads(1_u64)) } } From 6c60e88405a1dc2e348f065c41c87145300cfe8c Mon Sep 17 00:00:00 2001 From: Davide Galassi Date: Thu, 9 May 2024 19:12:40 +0200 Subject: [PATCH 20/42] Get rid of persistent epoch index --- substrate/frame/sassafras/src/benchmarking.rs | 38 +++--- substrate/frame/sassafras/src/lib.rs | 79 ++++++------ substrate/frame/sassafras/src/tests.rs | 34 +++++- substrate/frame/sassafras/src/weights.rs | 112 +++++++++--------- 4 files changed, 152 insertions(+), 111 deletions(-) diff --git a/substrate/frame/sassafras/src/benchmarking.rs b/substrate/frame/sassafras/src/benchmarking.rs index 9c03eb16274a..f5404cee7412 100644 --- a/substrate/frame/sassafras/src/benchmarking.rs +++ b/substrate/frame/sassafras/src/benchmarking.rs @@ -18,7 +18,7 @@ //! Benchmarks for the Sassafras pallet. use crate::*; -use sp_consensus_sassafras::vrf::VrfSignature; +use sp_consensus_sassafras::vrf::{VrfPreOutput, VrfSignature}; use frame_benchmarking::v2::*; use frame_support::traits::Hooks; @@ -28,19 +28,24 @@ const LOG_TARGET: &str = "sassafras::benchmark"; const TICKETS_DATA: &[u8] = include_bytes!("data/tickets.bin"); +// This leverages our knowledge about serialized vrf signature structure. +// Mostly to avoid to import all the bandersnatch primitive just for this test. +const RAW_VRF_SIGNATURE: [u8; 99] = [ + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x04, 0xb5, 0x5f, 0x8e, 0xc7, 0x68, 0xf5, 0x05, 0x3f, 0xa9, 0x18, 0xca, 0x07, 0x13, 0xc7, + 0x4b, 0xa3, 0x9a, 0x97, 0xd3, 0x76, 0x8f, 0x0c, 0xbf, 0x2e, 0xd4, 0xf9, 0x3a, 0xae, 0xc1, 0x96, + 0x2a, 0x64, 0x80, +]; + +fn dummy_pre_output() -> VrfPreOutput { + VrfPreOutput::decode(&mut &RAW_VRF_SIGNATURE[..]).unwrap() +} + fn dummy_vrf_signature() -> VrfSignature { - // This leverages our knowledge about serialized vrf signature structure. - // Mostly to avoid to import all the bandersnatch primitive just for this test. - let buf = [ - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 0xb5, 0x5f, 0x8e, 0xc7, 0x68, 0xf5, 0x05, 0x3f, 0xa9, - 0x18, 0xca, 0x07, 0x13, 0xc7, 0x4b, 0xa3, 0x9a, 0x97, 0xd3, 0x76, 0x8f, 0x0c, 0xbf, 0x2e, - 0xd4, 0xf9, 0x3a, 0xae, 0xc1, 0x96, 0x2a, 0x64, 0x80, - ]; - VrfSignature::decode(&mut &buf[..]).unwrap() + VrfSignature::decode(&mut &RAW_VRF_SIGNATURE[..]).unwrap() } #[benchmarks] @@ -87,11 +92,16 @@ mod benchmarks { let config = Pallet::::protocol_config(); // Makes the epoch change legit + let post_init_cache = EphemeralData { + prev_slot: Slot::from(config.epoch_length as u64 - 1), + pre_output: dummy_pre_output(), + }; + TemporaryData::::put(post_init_cache); CurrentSlot::::set(Slot::from(config.epoch_length as u64)); let tickets: Vec<_> = (0..outstanding_tickets) .map(|i| { - let mut id = [0; 32]; + let mut id = [0xff; 32]; id[..4].copy_from_slice(&i.to_be_bytes()[..]); (TicketId(id), TicketBody { attempt_idx: 0, extra: Default::default() }) }) diff --git a/substrate/frame/sassafras/src/lib.rs b/substrate/frame/sassafras/src/lib.rs index 6f367ce9855c..4f94efd38a6c 100644 --- a/substrate/frame/sassafras/src/lib.rs +++ b/substrate/frame/sassafras/src/lib.rs @@ -86,6 +86,9 @@ const LOG_TARGET: &str = "sassafras::runtime"; // Contextual string used by the VRF to generate per-block randomness. const RANDOMNESS_VRF_CONTEXT: &[u8] = b"SassafrasOnChainRandomness"; +// Epoch tail is the section of the epoch when no tickets must be submitted. +// +// Length of the epoch tail is set to Config::EpochLength/EPOCH_TAIL_FRACTION const EPOCH_TAIL_FRACTION: u32 = 6; /// Max number of tickets that can be submitted in one block. @@ -107,6 +110,15 @@ pub type TicketsVec = BoundedVec for TicketKey { fn from(mut value: TicketId) -> Self { @@ -199,10 +211,6 @@ pub mod pallet { #[pallet::getter(fn current_slot)] pub type CurrentSlot = StorageValue<_, Slot, ValueQuery>; - /// Current block slot number. - #[pallet::storage] - pub type EpochIndex = StorageValue<_, u64, ValueQuery>; - /// Randomness buffer. #[pallet::storage] #[pallet::getter(fn randomness_buf)] @@ -210,10 +218,12 @@ pub mod pallet { /// Tickets accumulator. #[pallet::storage] - pub(crate) type TicketsAccumulator = CountedStorageMap<_, Identity, TicketKey, TicketBody>; + #[pallet::getter(fn tickets_accumulator)] + pub type TicketsAccumulator = CountedStorageMap<_, Identity, TicketKey, TicketBody>; /// Tickets counters for the current and next epoch. #[pallet::storage] + #[pallet::getter(fn tickets_count)] pub type TicketsCount = StorageValue<_, TicketsCounter, ValueQuery>; /// Tickets map. @@ -234,6 +244,7 @@ pub mod pallet { /// The number of tickets available for epoch `E` is stored in the `E mod 2` entry /// of [`TicketsCount`]. #[pallet::storage] + #[pallet::getter(fn tickets)] pub type Tickets = StorageMap<_, Identity, (u8, u32), (TicketId, TicketBody)>; /// Parameters used to construct the epoch's ring verifier. @@ -245,13 +256,12 @@ pub mod pallet { /// Ring verifier data for the current epoch. #[pallet::storage] + #[pallet::getter(fn ring_verifier_key)] pub type RingVerifierKey = StorageValue<_, vrf::RingVerifierKey>; - /// Slot claim VRF pre-output used to generate per-slot randomness. - /// - /// The value is ephemeral and is cleared on block finalization. + /// Ephemeral data we retain until the block finalization. #[pallet::storage] - pub(crate) type PostInitCache = StorageValue<_, vrf::VrfPreOutput>; + pub(crate) type TemporaryData = StorageValue<_, EphemeralData>; /// Genesis configuration for Sassafras protocol. #[pallet::genesis_config] @@ -290,19 +300,18 @@ pub mod pallet { .find_map(|item| item.pre_runtime_try_to::(&SASSAFRAS_ENGINE_ID)) .expect("Valid block must have a slot claim. qed"); + let ephemeral_data = EphemeralData { + prev_slot: CurrentSlot::::get(), + pre_output: claim.vrf_signature.pre_outputs[0].clone(), + }; + TemporaryData::::put(ephemeral_data); + CurrentSlot::::put(claim.slot); if block_num == One::one() { - Self::post_genesis_initialize(claim.slot); + Self::post_genesis_initialize(); } - let randomness_pre_output = claim - .vrf_signature - .pre_outputs - .get(0) - .expect("Valid claim must have VRF signature; qed"); - PostInitCache::::put(randomness_pre_output); - let trigger_weight = T::EpochChangeTrigger::trigger::(block_num); T::WeightInfo::on_initialize() + trigger_weight @@ -315,8 +324,10 @@ pub mod pallet { // (i.e. `enact_epoch_change` has already been called). let randomness_input = vrf::slot_claim_input(&Self::randomness_accumulator(), CurrentSlot::::get()); - let randomness_pre_output = PostInitCache::::take() - .expect("Unconditionally populated in `on_initialize`; `on_finalize` is always called after; qed"); + let randomness_pre_output = TemporaryData::::take() + .expect("Unconditionally populated in `on_initialize`; `on_finalize` is always called after; qed") + .pre_output; + let randomness = randomness_pre_output .make_bytes::(RANDOMNESS_VRF_CONTEXT, &randomness_input); Self::deposit_randomness(randomness); @@ -474,7 +485,9 @@ impl Pallet { NextAuthorities::::put(&next_authorities); // Update epoch index - let expected_epoch_idx = EpochIndex::::get() + 1; + let expected_epoch_idx = TemporaryData::::get() + .map(|cache| Self::epoch_index(cache.prev_slot) + 1) + .expect("Unconditionally populated in `on_initialize`; `enact_epoch_change` is always called after; qed"); let mut epoch_idx = Self::curr_epoch_index(); if epoch_idx < expected_epoch_idx { @@ -497,8 +510,6 @@ impl Pallet { ); } - EpochIndex::::put(epoch_idx); - // After we update the current epoch, we signal the *next* epoch change // so that nodes can track changes. let epoch_signal = NextEpochDescriptor { @@ -605,10 +616,7 @@ impl Pallet { } // 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). - EpochIndex::::put(Self::epoch_index(slot)); - + fn post_genesis_initialize() { // Properly initialize randomness using genesis hash. // This is important to guarantee that a different set of tickets are produced for // different chains sharing the same ring parameters. @@ -657,7 +665,7 @@ impl Pallet { return None } - let curr_epoch_idx = EpochIndex::::get(); + let curr_epoch_idx = Self::curr_epoch_index(); let slot_epoch_idx = Self::epoch_index(slot); if slot_epoch_idx < curr_epoch_idx || slot_epoch_idx > curr_epoch_idx + 1 { return None @@ -716,7 +724,7 @@ impl Pallet { pub fn protocol_config() -> Configuration { Configuration { epoch_length: T::EpochLength::get(), - epoch_tail_length: T::EpochLength::get() / 6, + epoch_tail_length: T::EpochLength::get() / EPOCH_TAIL_FRACTION, max_authorities: T::MaxAuthorities::get(), redundancy_factor: T::RedundancyFactor::get(), attempts_number: T::AttemptsNumber::get(), @@ -725,10 +733,8 @@ impl Pallet { /// Current epoch information. pub fn current_epoch() -> Epoch { - let curr_slot = *Self::current_slot(); - let epoch_start = curr_slot - curr_slot % Self::epoch_length() as u64; Epoch { - start: epoch_start.into(), + start: Self::current_epoch_start(), authorities: Self::authorities().into_inner(), randomness: Self::randomness_buf(), } @@ -805,7 +811,9 @@ impl Pallet { /// Finds the start slot of the current epoch. fn current_epoch_start() -> Slot { - Self::epoch_start(EpochIndex::::get()) + let curr_slot = *Self::current_slot(); + let epoch_start = curr_slot - curr_slot % Self::epoch_length() as u64; + Slot::from(epoch_start) } /// Get the epoch's first slot. @@ -850,10 +858,9 @@ impl EpochChangeTrigger for EpochChangeInternalTrigger { 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); - // let len = next_authorities.len() as u32; - // T::WeightInfo::enact_epoch_change(len, T::EpochLength::get()) - Weight::zero() + T::WeightInfo::enact_epoch_change(len, T::EpochLength::get()) } else { Weight::zero() } diff --git a/substrate/frame/sassafras/src/tests.rs b/substrate/frame/sassafras/src/tests.rs index eb0c9d8e550d..c60957795c35 100644 --- a/substrate/frame/sassafras/src/tests.rs +++ b/substrate/frame/sassafras/src/tests.rs @@ -68,9 +68,33 @@ fn dummy_tickets(count: usize) -> Vec<(TicketId, TicketBody)> { } #[test] -fn genesis_values_assumptions_check() { +fn assumptions_check() { + let mut tickets: Vec<_> = (0..100_u32) + .map(|i| { + let mut id = [0xff; 32]; + id[..4].copy_from_slice(&i.to_be_bytes()[..]); + (TicketId(id), TicketBody { attempt_idx: 0, extra: Default::default() }) + }) + .collect(); + shuffle(&mut tickets, 123); + new_test_ext(3).execute_with(|| { assert_eq!(Sassafras::authorities().len(), 3); + + // Check that entries are stored sorted (bigger first) + tickets + .iter() + .for_each(|t| TicketsAccumulator::::insert(TicketKey::from(t.0), &t.1)); + assert_eq!(TicketsAccumulator::::count(), 100); + tickets.sort_unstable_by(|a, b| b.0.cmp(&a.0)); + let accumulator: Vec<_> = TicketsAccumulator::::iter() + .map(|(k, b)| (TicketId::from(k), b)) + .collect(); + assert_eq!(tickets, accumulator); + + // Check accumulator clear + let _ = TicketsAccumulator::::clear(u32::MAX, None); + assert_eq!(TicketsAccumulator::::count(), 0); }); } @@ -157,7 +181,7 @@ fn on_first_block() { let common_assertions = |initialized| { assert_eq!(Sassafras::current_slot(), start_slot); assert_eq!(Sassafras::current_slot_index(), 1); - assert_eq!(PostInitCache::::exists(), initialized); + assert_eq!(TemporaryData::::exists(), initialized); }; // Post-initialization status @@ -221,7 +245,7 @@ fn on_normal_block() { let common_assertions = |initialized| { assert_eq!(Sassafras::current_slot(), start_slot + 1); assert_eq!(Sassafras::current_slot_index(), 2); - assert_eq!(PostInitCache::::exists(), initialized); + assert_eq!(TemporaryData::::exists(), initialized); }; // Post-initialization status @@ -267,7 +291,7 @@ fn produce_epoch_change_digest() { let common_assertions = |initialized| { assert_eq!(Sassafras::current_slot(), GENESIS_SLOT + epoch_length); assert_eq!(Sassafras::current_slot_index(), 0); - assert_eq!(PostInitCache::::exists(), initialized); + assert_eq!(TemporaryData::::exists(), initialized); }; let digest = progress_to_block(end_block, &pairs[0]).unwrap(); @@ -327,7 +351,7 @@ fn slot_ticket_id_outside_in_fetch() { .for_each(|(i, t)| Tickets::::insert((1, i as u32), t)); TicketsCount::::set([curr_count as u32, next_count as u32]); - EpochIndex::::set(*genesis_slot / Sassafras::epoch_length() as u64); + CurrentSlot::::set(genesis_slot); // Before importing the first block the pallet always return `None` // This is a kind of special hardcoded case that should never happen in practice diff --git a/substrate/frame/sassafras/src/weights.rs b/substrate/frame/sassafras/src/weights.rs index 9f09f6e2d169..cb661e460ae1 100644 --- a/substrate/frame/sassafras/src/weights.rs +++ b/substrate/frame/sassafras/src/weights.rs @@ -64,21 +64,21 @@ pub struct SubstrateWeight(PhantomData); impl WeightInfo for SubstrateWeight { /// Storage: `System::Digest` (r:1 w:0) /// Proof: `System::Digest` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) + /// Storage: `Sassafras::CurrentSlot` (r:1 w:1) + /// Proof: `Sassafras::CurrentSlot` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `MaxEncodedLen`) /// Storage: `Sassafras::RandomnessBuf` (r:1 w:1) /// Proof: `Sassafras::RandomnessBuf` (`max_values`: Some(1), `max_size`: Some(128), added: 623, mode: `MaxEncodedLen`) /// Storage: `Sassafras::CounterForTicketsAccumulator` (r:1 w:0) /// Proof: `Sassafras::CounterForTicketsAccumulator` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`) - /// Storage: `Sassafras::CurrentSlot` (r:0 w:1) - /// Proof: `Sassafras::CurrentSlot` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `MaxEncodedLen`) - /// Storage: `Sassafras::PostInitCache` (r:0 w:1) - /// Proof: `Sassafras::PostInitCache` (`max_values`: Some(1), `max_size`: Some(33), added: 528, mode: `MaxEncodedLen`) + /// Storage: `Sassafras::TemporaryData` (r:0 w:1) + /// Proof: `Sassafras::TemporaryData` (`max_values`: Some(1), `max_size`: Some(41), added: 536, mode: `MaxEncodedLen`) fn on_initialize() -> Weight { // Proof Size summary in bytes: // Measured: `270` // Estimated: `1755` - // Minimum execution time: 417_410_000 picoseconds. - Weight::from_parts(419_042_000, 1755) - .saturating_add(T::DbWeight::get().reads(3_u64)) + // Minimum execution time: 421_387_000 picoseconds. + Weight::from_parts(424_844_000, 1755) + .saturating_add(T::DbWeight::get().reads(4_u64)) .saturating_add(T::DbWeight::get().writes(3_u64)) } /// Storage: `Sassafras::CurrentSlot` (r:1 w:0) @@ -87,8 +87,8 @@ impl WeightInfo for SubstrateWeight { /// Proof: `Sassafras::NextAuthorities` (`max_values`: Some(1), `max_size`: Some(3302), added: 3797, mode: `MaxEncodedLen`) /// Storage: `Sassafras::RingContext` (r:1 w:0) /// Proof: `Sassafras::RingContext` (`max_values`: Some(1), `max_size`: Some(590324), added: 590819, mode: `MaxEncodedLen`) - /// Storage: `Sassafras::EpochIndex` (r:1 w:1) - /// Proof: `Sassafras::EpochIndex` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `MaxEncodedLen`) + /// Storage: `Sassafras::TemporaryData` (r:1 w:0) + /// Proof: `Sassafras::TemporaryData` (`max_values`: Some(1), `max_size`: Some(41), added: 536, mode: `MaxEncodedLen`) /// Storage: `Sassafras::RandomnessBuf` (r:1 w:1) /// Proof: `Sassafras::RandomnessBuf` (`max_values`: Some(1), `max_size`: Some(128), added: 623, mode: `MaxEncodedLen`) /// Storage: `System::Digest` (r:1 w:1) @@ -109,17 +109,17 @@ impl WeightInfo for SubstrateWeight { /// The range of component `y` is `[100, 1000]`. fn enact_epoch_change(x: u32, y: u32, ) -> Weight { // Proof Size summary in bytes: - // Measured: `590548 + x * (33 ±0) + y * (37 ±0)` - // Estimated: `592034 + x * (33 ±0) + y * (2641 ±0)` - // Minimum execution time: 137_787_571_000 picoseconds. - Weight::from_parts(132_666_006_991, 592034) - // Standard Error: 4_836_429 - .saturating_add(Weight::from_parts(188_608_489, 0).saturating_mul(x.into())) - // Standard Error: 534_133 - .saturating_add(Weight::from_parts(5_816_837, 0).saturating_mul(y.into())) + // Measured: `590614 + x * (33 ±0) + y * (37 ±0)` + // Estimated: `592100 + x * (33 ±0) + y * (2641 ±0)` + // Minimum execution time: 142_890_248_000 picoseconds. + Weight::from_parts(138_485_179_943, 592100) + // Standard Error: 6_803_050 + .saturating_add(Weight::from_parts(168_321_054, 0).saturating_mul(x.into())) + // Standard Error: 751_326 + .saturating_add(Weight::from_parts(4_848_878, 0).saturating_mul(y.into())) .saturating_add(T::DbWeight::get().reads(9_u64)) .saturating_add(T::DbWeight::get().reads((1_u64).saturating_mul(y.into()))) - .saturating_add(T::DbWeight::get().writes(8_u64)) + .saturating_add(T::DbWeight::get().writes(7_u64)) .saturating_add(T::DbWeight::get().writes((2_u64).saturating_mul(y.into()))) .saturating_add(Weight::from_parts(0, 33).saturating_mul(x.into())) .saturating_add(Weight::from_parts(0, 2641).saturating_mul(y.into())) @@ -141,10 +141,10 @@ impl WeightInfo for SubstrateWeight { // Proof Size summary in bytes: // Measured: `1029` // Estimated: `4787 + x * (2641 ±0)` - // Minimum execution time: 51_637_107_000 picoseconds. - Weight::from_parts(37_790_802_200, 4787) - // Standard Error: 26_574_579 - .saturating_add(Weight::from_parts(14_279_858_343, 0).saturating_mul(x.into())) + // Minimum execution time: 52_231_308_000 picoseconds. + Weight::from_parts(37_784_411_122, 4787) + // Standard Error: 80_093_957 + .saturating_add(Weight::from_parts(15_157_828_698, 0).saturating_mul(x.into())) .saturating_add(T::DbWeight::get().reads(5_u64)) .saturating_add(T::DbWeight::get().reads((1_u64).saturating_mul(x.into()))) .saturating_add(T::DbWeight::get().writes(1_u64)) @@ -160,10 +160,10 @@ impl WeightInfo for SubstrateWeight { // Proof Size summary in bytes: // Measured: `590458` // Estimated: `591809` - // Minimum execution time: 133_456_343_000 picoseconds. - Weight::from_parts(134_102_722_298, 591809) - // Standard Error: 2_598_318 - .saturating_add(Weight::from_parts(162_531_608, 0).saturating_mul(x.into())) + // Minimum execution time: 137_798_064_000 picoseconds. + Weight::from_parts(138_407_861_337, 591809) + // Standard Error: 4_606_141 + .saturating_add(Weight::from_parts(162_094_811, 0).saturating_mul(x.into())) .saturating_add(T::DbWeight::get().reads(1_u64)) .saturating_add(T::DbWeight::get().writes(1_u64)) } @@ -173,8 +173,8 @@ impl WeightInfo for SubstrateWeight { // Proof Size summary in bytes: // Measured: `590458` // Estimated: `591809` - // Minimum execution time: 54_506_001_000 picoseconds. - Weight::from_parts(54_520_769_000, 591809) + // Minimum execution time: 57_369_356_000 picoseconds. + Weight::from_parts(57_377_021_000, 591809) .saturating_add(T::DbWeight::get().reads(1_u64)) } } @@ -183,21 +183,21 @@ impl WeightInfo for SubstrateWeight { impl WeightInfo for () { /// Storage: `System::Digest` (r:1 w:0) /// Proof: `System::Digest` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) + /// Storage: `Sassafras::CurrentSlot` (r:1 w:1) + /// Proof: `Sassafras::CurrentSlot` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `MaxEncodedLen`) /// Storage: `Sassafras::RandomnessBuf` (r:1 w:1) /// Proof: `Sassafras::RandomnessBuf` (`max_values`: Some(1), `max_size`: Some(128), added: 623, mode: `MaxEncodedLen`) /// Storage: `Sassafras::CounterForTicketsAccumulator` (r:1 w:0) /// Proof: `Sassafras::CounterForTicketsAccumulator` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`) - /// Storage: `Sassafras::CurrentSlot` (r:0 w:1) - /// Proof: `Sassafras::CurrentSlot` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `MaxEncodedLen`) - /// Storage: `Sassafras::PostInitCache` (r:0 w:1) - /// Proof: `Sassafras::PostInitCache` (`max_values`: Some(1), `max_size`: Some(33), added: 528, mode: `MaxEncodedLen`) + /// Storage: `Sassafras::TemporaryData` (r:0 w:1) + /// Proof: `Sassafras::TemporaryData` (`max_values`: Some(1), `max_size`: Some(41), added: 536, mode: `MaxEncodedLen`) fn on_initialize() -> Weight { // Proof Size summary in bytes: // Measured: `270` // Estimated: `1755` - // Minimum execution time: 417_410_000 picoseconds. - Weight::from_parts(419_042_000, 1755) - .saturating_add(RocksDbWeight::get().reads(3_u64)) + // Minimum execution time: 421_387_000 picoseconds. + Weight::from_parts(424_844_000, 1755) + .saturating_add(RocksDbWeight::get().reads(4_u64)) .saturating_add(RocksDbWeight::get().writes(3_u64)) } /// Storage: `Sassafras::CurrentSlot` (r:1 w:0) @@ -206,8 +206,8 @@ impl WeightInfo for () { /// Proof: `Sassafras::NextAuthorities` (`max_values`: Some(1), `max_size`: Some(3302), added: 3797, mode: `MaxEncodedLen`) /// Storage: `Sassafras::RingContext` (r:1 w:0) /// Proof: `Sassafras::RingContext` (`max_values`: Some(1), `max_size`: Some(590324), added: 590819, mode: `MaxEncodedLen`) - /// Storage: `Sassafras::EpochIndex` (r:1 w:1) - /// Proof: `Sassafras::EpochIndex` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `MaxEncodedLen`) + /// Storage: `Sassafras::TemporaryData` (r:1 w:0) + /// Proof: `Sassafras::TemporaryData` (`max_values`: Some(1), `max_size`: Some(41), added: 536, mode: `MaxEncodedLen`) /// Storage: `Sassafras::RandomnessBuf` (r:1 w:1) /// Proof: `Sassafras::RandomnessBuf` (`max_values`: Some(1), `max_size`: Some(128), added: 623, mode: `MaxEncodedLen`) /// Storage: `System::Digest` (r:1 w:1) @@ -228,17 +228,17 @@ impl WeightInfo for () { /// The range of component `y` is `[100, 1000]`. fn enact_epoch_change(x: u32, y: u32, ) -> Weight { // Proof Size summary in bytes: - // Measured: `590548 + x * (33 ±0) + y * (37 ±0)` - // Estimated: `592034 + x * (33 ±0) + y * (2641 ±0)` - // Minimum execution time: 137_787_571_000 picoseconds. - Weight::from_parts(132_666_006_991, 592034) - // Standard Error: 4_836_429 - .saturating_add(Weight::from_parts(188_608_489, 0).saturating_mul(x.into())) - // Standard Error: 534_133 - .saturating_add(Weight::from_parts(5_816_837, 0).saturating_mul(y.into())) + // Measured: `590614 + x * (33 ±0) + y * (37 ±0)` + // Estimated: `592100 + x * (33 ±0) + y * (2641 ±0)` + // Minimum execution time: 142_890_248_000 picoseconds. + Weight::from_parts(138_485_179_943, 592100) + // Standard Error: 6_803_050 + .saturating_add(Weight::from_parts(168_321_054, 0).saturating_mul(x.into())) + // Standard Error: 751_326 + .saturating_add(Weight::from_parts(4_848_878, 0).saturating_mul(y.into())) .saturating_add(RocksDbWeight::get().reads(9_u64)) .saturating_add(RocksDbWeight::get().reads((1_u64).saturating_mul(y.into()))) - .saturating_add(RocksDbWeight::get().writes(8_u64)) + .saturating_add(RocksDbWeight::get().writes(7_u64)) .saturating_add(RocksDbWeight::get().writes((2_u64).saturating_mul(y.into()))) .saturating_add(Weight::from_parts(0, 33).saturating_mul(x.into())) .saturating_add(Weight::from_parts(0, 2641).saturating_mul(y.into())) @@ -260,10 +260,10 @@ impl WeightInfo for () { // Proof Size summary in bytes: // Measured: `1029` // Estimated: `4787 + x * (2641 ±0)` - // Minimum execution time: 51_637_107_000 picoseconds. - Weight::from_parts(37_790_802_200, 4787) - // Standard Error: 26_574_579 - .saturating_add(Weight::from_parts(14_279_858_343, 0).saturating_mul(x.into())) + // Minimum execution time: 52_231_308_000 picoseconds. + Weight::from_parts(37_784_411_122, 4787) + // Standard Error: 80_093_957 + .saturating_add(Weight::from_parts(15_157_828_698, 0).saturating_mul(x.into())) .saturating_add(RocksDbWeight::get().reads(5_u64)) .saturating_add(RocksDbWeight::get().reads((1_u64).saturating_mul(x.into()))) .saturating_add(RocksDbWeight::get().writes(1_u64)) @@ -279,10 +279,10 @@ impl WeightInfo for () { // Proof Size summary in bytes: // Measured: `590458` // Estimated: `591809` - // Minimum execution time: 133_456_343_000 picoseconds. - Weight::from_parts(134_102_722_298, 591809) - // Standard Error: 2_598_318 - .saturating_add(Weight::from_parts(162_531_608, 0).saturating_mul(x.into())) + // Minimum execution time: 137_798_064_000 picoseconds. + Weight::from_parts(138_407_861_337, 591809) + // Standard Error: 4_606_141 + .saturating_add(Weight::from_parts(162_094_811, 0).saturating_mul(x.into())) .saturating_add(RocksDbWeight::get().reads(1_u64)) .saturating_add(RocksDbWeight::get().writes(1_u64)) } @@ -292,8 +292,8 @@ impl WeightInfo for () { // Proof Size summary in bytes: // Measured: `590458` // Estimated: `591809` - // Minimum execution time: 54_506_001_000 picoseconds. - Weight::from_parts(54_520_769_000, 591809) + // Minimum execution time: 57_369_356_000 picoseconds. + Weight::from_parts(57_377_021_000, 591809) .saturating_add(RocksDbWeight::get().reads(1_u64)) } } From 7ab117ae899c9f07ab122b4b84804279803b8049 Mon Sep 17 00:00:00 2001 From: Davide Galassi Date: Fri, 10 May 2024 09:55:23 +0200 Subject: [PATCH 21/42] Improve docs --- substrate/frame/sassafras/src/benchmarking.rs | 72 +++++++++---------- substrate/frame/sassafras/src/lib.rs | 48 ++++++++----- substrate/primitives/core/src/bandersnatch.rs | 2 +- 3 files changed, 63 insertions(+), 59 deletions(-) diff --git a/substrate/frame/sassafras/src/benchmarking.rs b/substrate/frame/sassafras/src/benchmarking.rs index f5404cee7412..5323cea5755f 100644 --- a/substrate/frame/sassafras/src/benchmarking.rs +++ b/substrate/frame/sassafras/src/benchmarking.rs @@ -26,28 +26,28 @@ use frame_system::RawOrigin; const LOG_TARGET: &str = "sassafras::benchmark"; +// Pre-constructed tickets generated via the `generate_test_teckets` function const TICKETS_DATA: &[u8] = include_bytes!("data/tickets.bin"); -// This leverages our knowledge about serialized vrf signature structure. -// Mostly to avoid to import all the bandersnatch primitive just for this test. -const RAW_VRF_SIGNATURE: [u8; 99] = [ - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x04, 0xb5, 0x5f, 0x8e, 0xc7, 0x68, 0xf5, 0x05, 0x3f, 0xa9, 0x18, 0xca, 0x07, 0x13, 0xc7, - 0x4b, 0xa3, 0x9a, 0x97, 0xd3, 0x76, 0x8f, 0x0c, 0xbf, 0x2e, 0xd4, 0xf9, 0x3a, 0xae, 0xc1, 0x96, - 0x2a, 0x64, 0x80, -]; - -fn dummy_pre_output() -> VrfPreOutput { - VrfPreOutput::decode(&mut &RAW_VRF_SIGNATURE[..]).unwrap() -} - fn dummy_vrf_signature() -> VrfSignature { + // This leverages our knowledge about serialized vrf signature structure. + // Mostly to avoid to import all the bandersnatch primitive just for this test. + const RAW_VRF_SIGNATURE: [u8; 99] = [ + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 0xb5, 0x5f, 0x8e, 0xc7, 0x68, 0xf5, 0x05, 0x3f, 0xa9, + 0x18, 0xca, 0x07, 0x13, 0xc7, 0x4b, 0xa3, 0x9a, 0x97, 0xd3, 0x76, 0x8f, 0x0c, 0xbf, 0x2e, + 0xd4, 0xf9, 0x3a, 0xae, 0xc1, 0x96, 0x2a, 0x64, 0x80, + ]; VrfSignature::decode(&mut &RAW_VRF_SIGNATURE[..]).unwrap() } +fn dummy_pre_output() -> VrfPreOutput { + dummy_vrf_signature().pre_outputs[0] +} + #[benchmarks] mod benchmarks { use super::*; @@ -63,9 +63,6 @@ mod benchmarks { }; frame_system::Pallet::::deposit_log((&slot_claim).into()); - // We currently don't account for the potential weight added by the `on_finalize` - // incremental sorting of the tickets. - #[block] { // According to `Hooks` trait docs, `on_finalize` `Weight` should be bundled @@ -78,16 +75,16 @@ mod benchmarks { // Weight for the default internal epoch change trigger. // // Parameters: - // - `x`: number of authorities (1:100). - // - `y`: number of tickets (100:1000) + // - `x`: number of authorities [1:100]. + // - `y`: number of tickets [100:1000]; // // This accounts for the worst case which includes: - // - recompute the ring verifier key from the new authority set. - // - picking the epoch tickets from the accumulator in one shot. + // - recomputing the ring verifier key from a new authorites set. + // - picking all the tickets from the accumulator in one shot. #[benchmark] fn enact_epoch_change(x: Linear<1, 100>, y: Linear<100, 1000>) { let authorities_count = x as usize; - let outstanding_tickets = y as u32; + let accumulated_tickets = y as u32; let config = Pallet::::protocol_config(); @@ -99,28 +96,23 @@ mod benchmarks { TemporaryData::::put(post_init_cache); CurrentSlot::::set(Slot::from(config.epoch_length as u64)); - let tickets: Vec<_> = (0..outstanding_tickets) - .map(|i| { - let mut id = [0xff; 32]; - id[..4].copy_from_slice(&i.to_be_bytes()[..]); - (TicketId(id), TicketBody { attempt_idx: 0, extra: Default::default() }) - }) - .collect(); - - // Append some tickets to the accumulator - tickets - .iter() - .for_each(|t| TicketsAccumulator::::insert(TicketKey::from(t.0), &t.1)); - // Force ring verifier key re-computation let next_authorities: Vec<_> = Authorities::::get().into_iter().cycle().take(authorities_count).collect(); let next_authorities = WeakBoundedVec::force_from(next_authorities, None); - NextAuthorities::::set(next_authorities); + // Add tickets to the accumulator + (0..accumulated_tickets).for_each(|i| { + let mut id = TicketId([0xff; 32]); + id.0[..4].copy_from_slice(&i.to_be_bytes()[..]); + let body = TicketBody { attempt_idx: 0, extra: Default::default() }; + TicketsAccumulator::::insert(TicketKey::from(id), &body); + }); + #[block] { + // Also account for the call typically done in case of epoch change Pallet::::should_end_epoch(BlockNumberFor::::from(3u32)); let next_authorities = Pallet::::next_authorities(); // Using a different set of authorities triggers the recomputation of ring verifier. @@ -140,12 +132,12 @@ mod benchmarks { ) = Decode::decode(&mut raw_data).expect("Failed to decode tickets buffer"); assert!(tickets.len() >= tickets_count); - // Use the same values as the pre-built tickets + // Use the same values used for the pre-built tickets Pallet::::update_ring_verifier(&authorities); + NextAuthorities::::set(WeakBoundedVec::force_from(authorities, None)); let mut randomness_buf = RandomnessBuf::::get(); randomness_buf[2] = randomness; RandomnessBuf::::set(randomness_buf); - NextAuthorities::::set(WeakBoundedVec::force_from(authorities, None)); let tickets = tickets[..tickets_count].to_vec(); let tickets = BoundedVec::truncate_from(tickets); diff --git a/substrate/frame/sassafras/src/lib.rs b/substrate/frame/sassafras/src/lib.rs index 4f94efd38a6c..4171cafe01a5 100644 --- a/substrate/frame/sassafras/src/lib.rs +++ b/substrate/frame/sassafras/src/lib.rs @@ -21,23 +21,26 @@ //! 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. +//! We run a lottery to distribute block production slots for a *target* epoch and to fix +//! the order validators produce blocks in. //! -//! Each validator signs some unbiasable input and publishes the output on-chain. This -//! value is their lottery ticket that can be validated against their public key. +//! Each validator signs some unbiasable VRF input and publishes the VRF output on-chain. +//! This value is their lottery ticket that can be eventually validated against their +//! public key. //! //! We want to keep lottery winners secret, i.e. do not disclose their public keys. -//! At the beginning of the epoch all the validators tickets are published but not -//! their public keys. +//! At the beginning of the *target* epoch all the validators tickets are published but +//! not the corresponding author public keys. //! -//! A valid ticket is claimed by an honest validator on block production. +//! The association is revealed by the ticket's owner during block production when he will +//! claim his ticket, and thus the associated slot, by showing a proof which ships with the +//! produced block. //! //! To prevent submission of invalid tickets, resulting in empty slots, the validator -//! when submitting the ticket accompanies it with a zk-SNARK of the statement: +//! when submitting a ticket accompanies it with a zk-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 who I am, but my public key is among those of the nominated -//! validators". +//! validators for the target epoch". #![allow(unused)] #![deny(warnings)] @@ -86,28 +89,26 @@ const LOG_TARGET: &str = "sassafras::runtime"; // Contextual string used by the VRF to generate per-block randomness. const RANDOMNESS_VRF_CONTEXT: &[u8] = b"SassafrasOnChainRandomness"; -// Epoch tail is the section of the epoch when no tickets must be submitted. +// Epoch tail is the section of the epoch where no tickets are allowed to be submitted. +// As the name implies, this section is at the end of an epoch. // -// Length of the epoch tail is set to Config::EpochLength/EPOCH_TAIL_FRACTION +// Length of the epoch's tail is computed as `Config::EpochLength / EPOCH_TAIL_FRACTION` +// TODO: make this part of `Config`? const EPOCH_TAIL_FRACTION: u32 = 6; /// Max number of tickets that can be submitted in one block. +// TODO: make this part of `Config`? const TICKETS_CHUNK_MAX_LENGTH: u32 = 16; /// Randomness buffer. pub type RandomnessBuffer = [Randomness; 4]; -/// Authorities sequence. -pub type AuthoritiesVec = WeakBoundedVec::MaxAuthorities>; - -/// Tickets chunk. -pub type TicketsVec = BoundedVec>; - /// Number of tickets available for current and next epoch. /// /// These tickets are held by the [`Tickets`] storage map. /// -/// The entry to be used for the current epoch is computed as epoch index modulo 2. +/// Current counter index is computed as current epoch index modulo 2 +/// Next counter index is computed as the other entry. pub type TicketsCounter = [u32; 2]; /// Ephemeral data constructed by `on_initialize` and destroyed by `on_finalize`. @@ -115,7 +116,9 @@ pub type TicketsCounter = [u32; 2]; /// Contains some temporary data that may be useful later during code execution. #[derive(Clone, PartialEq, Eq, Encode, Decode, MaxEncodedLen, TypeInfo)] pub struct EphemeralData { + /// Previous block slot. prev_slot: Slot, + /// Cached pre-output which ships within the block's header digest. pre_output: vrf::VrfPreOutput, } @@ -144,6 +147,12 @@ impl From for TicketId { } } +/// Authorities sequence. +pub type AuthoritiesVec = WeakBoundedVec::MaxAuthorities>; + +/// Tickets sequence. +pub type TicketsVec = BoundedVec>; + #[frame_support::pallet] pub mod pallet { use super::*; @@ -328,6 +337,9 @@ pub mod pallet { .expect("Unconditionally populated in `on_initialize`; `on_finalize` is always called after; qed") .pre_output; + // TODO: debug_assert that signature is valid + // Verification has been already done by the host! + let randomness = randomness_pre_output .make_bytes::(RANDOMNESS_VRF_CONTEXT, &randomness_input); Self::deposit_randomness(randomness); diff --git a/substrate/primitives/core/src/bandersnatch.rs b/substrate/primitives/core/src/bandersnatch.rs index 9049fd208378..d70af4655e14 100644 --- a/substrate/primitives/core/src/bandersnatch.rs +++ b/substrate/primitives/core/src/bandersnatch.rs @@ -204,7 +204,7 @@ pub mod vrf { /// This object is used to produce an arbitrary number of verifiable pseudo random /// bytes and is often called pre-output to emphasize that this is not the actual /// output of the VRF but an object capable of generating the output. - #[derive(Clone, Debug, PartialEq, Eq)] + #[derive(Copy, Clone, Debug, PartialEq, Eq)] pub struct VrfPreOutput(pub(super) bandersnatch_vrfs::VrfPreOut); impl Encode for VrfPreOutput { From 2464819f3b6169f34b7bffb7c6cec0059287b1d8 Mon Sep 17 00:00:00 2001 From: Davide Galassi Date: Fri, 10 May 2024 10:06:20 +0200 Subject: [PATCH 22/42] Fix sassafras vrf inputs --- .../frame/sassafras/src/data/tickets.bin | Bin 16244 -> 16244 bytes substrate/frame/sassafras/src/lib.rs | 6 ++-- substrate/frame/sassafras/src/mock.rs | 4 +-- substrate/frame/sassafras/src/tests.rs | 6 ++-- .../primitives/consensus/sassafras/src/vrf.rs | 31 +++++------------- 5 files changed, 17 insertions(+), 30 deletions(-) diff --git a/substrate/frame/sassafras/src/data/tickets.bin b/substrate/frame/sassafras/src/data/tickets.bin index 77288595374f8dc4b1cd3d88f7c63cce78fa7e5e..96527efed64780b4ac626036df0999a264e845cc 100644 GIT binary patch delta 16041 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::get()); + let randomness_input = vrf::block_randomness_input( + &Self::randomness_accumulator(), + CurrentSlot::::get(), + ); let randomness_pre_output = TemporaryData::::take() .expect("Unconditionally populated in `on_initialize`; `on_finalize` is always called after; qed") .pre_output; diff --git a/substrate/frame/sassafras/src/mock.rs b/substrate/frame/sassafras/src/mock.rs index 1d7fb5b6b52c..78f75b256c87 100644 --- a/substrate/frame/sassafras/src/mock.rs +++ b/substrate/frame/sassafras/src/mock.rs @@ -111,8 +111,8 @@ pub fn new_test_ext_with_pairs( } fn slot_claim_vrf_signature(slot: Slot, pair: &AuthorityPair) -> VrfSignature { - let randomness = Sassafras::randomness(0); - let data = vrf::slot_claim_sign_data(&randomness, slot); + let randomness = Sassafras::randomness_accumulator(); + let data = vrf::block_randomness_sign_data(&randomness, slot); pair.as_ref().vrf_sign(&data) } diff --git a/substrate/frame/sassafras/src/tests.rs b/substrate/frame/sassafras/src/tests.rs index c60957795c35..fcd3a586caa0 100644 --- a/substrate/frame/sassafras/src/tests.rs +++ b/substrate/frame/sassafras/src/tests.rs @@ -203,7 +203,7 @@ fn on_first_block() { common_assertions(false); let post_fini_randomness = Sassafras::randomness_buf(); - prefix_eq!(post_fini_randomness[0], h2b("173b91b3")); + prefix_eq!(post_fini_randomness[0], h2b("c5caf074")); prefix_eq!(post_fini_randomness[1], post_init_randomness[1]); prefix_eq!(post_fini_randomness[2], post_init_randomness[2]); prefix_eq!(post_fini_randomness[3], post_init_randomness[3]); @@ -252,7 +252,7 @@ fn on_normal_block() { common_assertions(true); let post_init_randomness = Sassafras::randomness_buf(); - prefix_eq!(post_init_randomness[0], h2b("173b91b3")); + prefix_eq!(post_init_randomness[0], h2b("c5caf074")); prefix_eq!(post_init_randomness[1], h2b("4e8c71d2")); prefix_eq!(post_init_randomness[2], h2b("3a4c0005")); prefix_eq!(post_init_randomness[3], h2b("0dd43c54")); @@ -263,7 +263,7 @@ fn on_normal_block() { common_assertions(false); let post_fini_randomness = Sassafras::randomness_buf(); - prefix_eq!(post_fini_randomness[0], h2b("800f7825")); + prefix_eq!(post_fini_randomness[0], h2b("1b6777c9")); prefix_eq!(post_fini_randomness[1], post_init_randomness[1]); prefix_eq!(post_fini_randomness[2], post_init_randomness[2]); prefix_eq!(post_fini_randomness[3], post_init_randomness[3]); diff --git a/substrate/primitives/consensus/sassafras/src/vrf.rs b/substrate/primitives/consensus/sassafras/src/vrf.rs index b0946cb61efb..ee6dea83a201 100644 --- a/substrate/primitives/consensus/sassafras/src/vrf.rs +++ b/substrate/primitives/consensus/sassafras/src/vrf.rs @@ -48,16 +48,16 @@ fn vrf_input_from_data( VrfInput::new(domain, buf) } -/// VRF input to claim slot ownership during block production. -pub fn slot_claim_input(randomness: &Randomness, slot: Slot) -> VrfInput { - vrf_input_from_data(b"sassafras-claim-v1.0", [randomness.as_slice(), &slot.to_le_bytes()]) +/// VRF input to produce randomness. +pub fn block_randomness_input(randomness: &Randomness, slot: Slot) -> VrfInput { + vrf_input_from_data(b"sassafras-randomness", [randomness.as_slice(), &slot.to_le_bytes()]) } /// Signing-data to claim slot ownership during block production. -pub fn slot_claim_sign_data(randomness: &Randomness, slot: Slot) -> VrfSignData { - let input = slot_claim_input(randomness, slot); +pub fn block_randomness_sign_data(randomness: &Randomness, slot: Slot) -> VrfSignData { + let input = block_randomness_input(randomness, slot); VrfSignData::new_unchecked( - b"sassafras-slot-claim-transcript-v1.0", + b"sassafras-randomness-transcript", Option::<&[u8]>::None, Some(input), ) @@ -68,15 +68,10 @@ pub fn ticket_id_input(randomness: &Randomness, attempt: u32) -> VrfInput { vrf_input_from_data(b"sassafras-ticket-v1.0", [randomness.as_slice(), &attempt.to_le_bytes()]) } -/// VRF input to generate the revealed key. -pub fn revealed_key_input(randomness: &Randomness, attempt: u32) -> VrfInput { - vrf_input_from_data(b"sassafras-revealed-v1.0", [randomness.as_slice(), &attempt.to_le_bytes()]) -} - /// Data to be signed via ring-vrf. pub fn ticket_body_sign_data(ticket_body: &TicketBody, ticket_id_input: VrfInput) -> VrfSignData { VrfSignData::new_unchecked( - b"sassafras-ticket-body-transcript-v1.0", + b"sassafras-ticket-body-transcript", Some(ticket_body.encode().as_slice()), Some(ticket_id_input), ) @@ -88,15 +83,5 @@ pub fn ticket_body_sign_data(ticket_body: &TicketBody, ticket_id_input: VrfInput /// Pre-output should have been obtained from the input directly using the vrf /// secret key or from the vrf signature pre-outputs. pub fn make_ticket_id(input: &VrfInput, pre_output: &VrfPreOutput) -> TicketId { - let bytes = pre_output.make_bytes::<32>(b"ticket-id", input); - TicketId(bytes) -} - -/// Make revealed key seed from a given VRF input and pre-output. -/// -/// Input should have been obtained via [`revealed_key_input`]. -/// Pre-output should have been obtained from the input directly using the vrf -/// secret key or from the vrf signature pre-outputs. -pub fn make_revealed_key_seed(input: &VrfInput, pre_output: &VrfPreOutput) -> [u8; 32] { - pre_output.make_bytes::<32>(b"revealed-seed", input) + TicketId(pre_output.make_bytes::<32>(b"ticket-id", input)) } From dadc7d8711d356320fa6fb0a0e3d997f8eb1a0cf Mon Sep 17 00:00:00 2001 From: Davide Galassi Date: Fri, 10 May 2024 10:51:22 +0200 Subject: [PATCH 23/42] EpochTag trait --- substrate/frame/sassafras/src/lib.rs | 77 ++++++++++++++++-------- substrate/frame/sassafras/src/tests.rs | 4 +- substrate/frame/sassafras/src/weights.rs | 74 +++++++++++------------ 3 files changed, 91 insertions(+), 64 deletions(-) diff --git a/substrate/frame/sassafras/src/lib.rs b/substrate/frame/sassafras/src/lib.rs index e63855448cf0..5890396860ac 100644 --- a/substrate/frame/sassafras/src/lib.rs +++ b/substrate/frame/sassafras/src/lib.rs @@ -153,6 +153,23 @@ pub type AuthoritiesVec = WeakBoundedVec::MaxAutho /// Tickets sequence. pub type TicketsVec = BoundedVec>; +trait EpochTag { + fn tag(&self) -> u8; + fn next_tag(&self) -> u8; +} + +impl EpochTag for u64 { + #[inline(always)] + fn tag(&self) -> u8 { + (self % 2) as u8 + } + + #[inline(always)] + fn next_tag(&self) -> u8 { + self.tag() ^ 1 + } +} + #[frame_support::pallet] pub mod pallet { use super::*; @@ -358,7 +375,7 @@ pub mod pallet { if slots_left > 0 { outstanding_count = outstanding_count.div_ceil(slots_left as usize); } - let next_epoch_tag = (Self::curr_epoch_index() & 1) as u8 ^ 1; + let next_epoch_tag = Self::current_epoch_index().next_tag(); Self::consume_tickets_accumulator(outstanding_count, next_epoch_tag); } } @@ -502,7 +519,7 @@ impl Pallet { let expected_epoch_idx = TemporaryData::::get() .map(|cache| Self::epoch_index(cache.prev_slot) + 1) .expect("Unconditionally populated in `on_initialize`; `enact_epoch_change` is always called after; qed"); - let mut epoch_idx = Self::curr_epoch_index(); + let mut epoch_idx = Self::current_epoch_index(); if epoch_idx < expected_epoch_idx { panic!( @@ -532,12 +549,11 @@ impl Pallet { }; Self::deposit_next_epoch_descriptor_digest(epoch_signal); - let epoch_tag = (epoch_idx & 1) as u8; - Self::consume_tickets_accumulator(usize::MAX, epoch_tag); + Self::consume_tickets_accumulator(usize::MAX, epoch_idx.tag()); // Reset next epoch counter as we're start accumulating. let mut tickets_count = TicketsCount::::get(); - tickets_count[(epoch_tag ^ 1) as usize] = 0; + tickets_count[epoch_idx.next_tag() as usize] = 0; TicketsCount::::set(tickets_count); } @@ -679,20 +695,20 @@ impl Pallet { return None } - let curr_epoch_idx = Self::curr_epoch_index(); + let curr_epoch_idx = Self::current_epoch_index(); let slot_epoch_idx = Self::epoch_index(slot); if slot_epoch_idx < curr_epoch_idx || slot_epoch_idx > curr_epoch_idx + 1 { return None } - let mut epoch_tag = (slot_epoch_idx & 1) as u8; + let mut epoch_tag = slot_epoch_idx.tag(); let epoch_len = T::EpochLength::get(); let mut slot_idx = Self::slot_index(slot); 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; + epoch_tag = slot_epoch_idx.next_tag(); slot_idx -= epoch_len; if TicketsAccumulator::::count() != 0 { Self::consume_tickets_accumulator(usize::MAX, epoch_tag); @@ -728,13 +744,14 @@ impl Pallet { /// Reset tickets related data. /// /// Optimization note: tickets are left in place, only the associated counters are resetted. + #[inline(always)] fn reset_tickets_data() { - // Reset sorted candidates (TODO: How this can fail?) TicketsCount::::kill(); let _ = TicketsAccumulator::::clear(u32::MAX, None); } /// Static protocol configuration. + #[inline(always)] pub fn protocol_config() -> Configuration { Configuration { epoch_length: T::EpochLength::get(), @@ -746,6 +763,7 @@ impl Pallet { } /// Current epoch information. + #[inline(always)] pub fn current_epoch() -> Epoch { Epoch { start: Self::current_epoch_start(), @@ -771,71 +789,80 @@ impl Pallet { /// - 0 : accumulator for epoch `N+3` randomness /// /// If `index` is greater than 3 the `Default` is returned. - pub fn randomness(index: usize) -> Randomness { + #[inline(always)] + fn randomness(index: usize) -> Randomness { Self::randomness_buf().get(index).cloned().unwrap_or_default() } /// Current epoch's randomness. - pub fn curr_randomness() -> Randomness { + #[inline(always)] + fn current_randomness() -> Randomness { Self::randomness(3) } /// Next epoch's randomness. - pub fn next_randomness() -> Randomness { + #[inline(always)] + fn next_randomness() -> Randomness { Self::randomness(2) } /// Randomness accumulator - pub fn randomness_accumulator() -> Randomness { + #[inline(always)] + fn randomness_accumulator() -> Randomness { Self::randomness(0) } /// Determine whether an epoch change should take place at this block. - pub(crate) fn should_end_epoch(block_num: BlockNumberFor) -> bool { + #[inline(always)] + fn should_end_epoch(block_num: BlockNumberFor) -> bool { Self::current_slot_index() == 0 && block_num != Zero::zero() } /// Current slot index relative to the current epoch. + #[inline(always)] fn current_slot_index() -> u32 { Self::slot_index(CurrentSlot::::get()) } /// Slot index relative to the current epoch. + #[inline(always)] fn slot_index(slot: Slot) -> u32 { - // slot.checked_sub(*Self::current_epoch_start()) - // .and_then(|v| v.try_into().ok()) - // .unwrap_or(u32::MAX) (*slot % ::EpochLength::get() as u64) as u32 } /// Current epoch index. - pub fn curr_epoch_index() -> u64 { + #[inline(always)] + fn current_epoch_index() -> u64 { Self::epoch_index(Self::current_slot()) } /// Epoch's index from slot. - pub fn epoch_index(slot: Slot) -> u64 { + #[inline(always)] + fn epoch_index(slot: Slot) -> u64 { *slot / ::EpochLength::get() as u64 } /// Epoch length - pub fn epoch_length() -> u32 { - T::EpochLength::get() - } - - /// Finds the start slot of the current epoch. + /// Get current epoch first slot. + #[inline(always)] fn current_epoch_start() -> Slot { let curr_slot = *Self::current_slot(); - let epoch_start = curr_slot - curr_slot % Self::epoch_length() as u64; + let epoch_start = curr_slot - curr_slot % ::EpochLength::get() as u64; Slot::from(epoch_start) } /// Get the epoch's first slot. + #[inline(always)] 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."; epoch_index.checked_mul(T::EpochLength::get() as u64).expect(PROOF).into() } + + #[inline(always)] + fn epoch_length() -> u32 { + T::EpochLength::get() + } } /// Trigger an epoch change, if any should take place. diff --git a/substrate/frame/sassafras/src/tests.rs b/substrate/frame/sassafras/src/tests.rs index fcd3a586caa0..c7d9df66af0a 100644 --- a/substrate/frame/sassafras/src/tests.rs +++ b/substrate/frame/sassafras/src/tests.rs @@ -413,7 +413,7 @@ fn slot_and_epoch_helpers_works() { assert_eq!(Sassafras::current_slot(), Slot::from(slot)); assert_eq!(Sassafras::current_slot_index(), slot_idx); assert_eq!(Sassafras::current_epoch_start(), Slot::from(epoch_slot)); - assert_eq!(Sassafras::curr_epoch_index(), epoch_idx); + assert_eq!(Sassafras::current_epoch_index(), epoch_idx); }; // Post genesis state (before first initialization of epoch N) @@ -465,7 +465,7 @@ fn tickets_accumulator_works() { ext.execute_with(|| { let epoch_length = Sassafras::epoch_length() as u64; - let epoch_idx = Sassafras::curr_epoch_index(); + let epoch_idx = Sassafras::current_epoch_index(); let epoch_tag = (epoch_idx % 2) as u8; let next_epoch_tag = epoch_tag ^ 1; diff --git a/substrate/frame/sassafras/src/weights.rs b/substrate/frame/sassafras/src/weights.rs index cb661e460ae1..1d03f34dd380 100644 --- a/substrate/frame/sassafras/src/weights.rs +++ b/substrate/frame/sassafras/src/weights.rs @@ -18,7 +18,7 @@ //! Autogenerated weights for `pallet_sassafras` //! //! THIS FILE WAS AUTO-GENERATED USING THE SUBSTRATE BENCHMARK CLI VERSION 32.0.0 -//! DATE: 2024-05-09, STEPS: `20`, REPEAT: `3`, LOW RANGE: `[]`, HIGH RANGE: `[]` +//! DATE: 2024-05-10, STEPS: `20`, REPEAT: `3`, LOW RANGE: `[]`, HIGH RANGE: `[]` //! WORST CASE MAP SIZE: `1000000` //! HOSTNAME: `behemoth`, CPU: `AMD Ryzen Threadripper 3970X 32-Core Processor` //! WASM-EXECUTION: `Compiled`, CHAIN: `Some("dev")`, DB CACHE: `1024` @@ -76,8 +76,8 @@ impl WeightInfo for SubstrateWeight { // Proof Size summary in bytes: // Measured: `270` // Estimated: `1755` - // Minimum execution time: 421_387_000 picoseconds. - Weight::from_parts(424_844_000, 1755) + // Minimum execution time: 435_616_000 picoseconds. + Weight::from_parts(438_943_000, 1755) .saturating_add(T::DbWeight::get().reads(4_u64)) .saturating_add(T::DbWeight::get().writes(3_u64)) } @@ -111,12 +111,12 @@ impl WeightInfo for SubstrateWeight { // Proof Size summary in bytes: // Measured: `590614 + x * (33 ±0) + y * (37 ±0)` // Estimated: `592100 + x * (33 ±0) + y * (2641 ±0)` - // Minimum execution time: 142_890_248_000 picoseconds. - Weight::from_parts(138_485_179_943, 592100) - // Standard Error: 6_803_050 - .saturating_add(Weight::from_parts(168_321_054, 0).saturating_mul(x.into())) - // Standard Error: 751_326 - .saturating_add(Weight::from_parts(4_848_878, 0).saturating_mul(y.into())) + // Minimum execution time: 135_714_229_000 picoseconds. + Weight::from_parts(132_409_697_079, 592100) + // Standard Error: 4_688_986 + .saturating_add(Weight::from_parts(174_151_591, 0).saturating_mul(x.into())) + // Standard Error: 517_850 + .saturating_add(Weight::from_parts(6_921_142, 0).saturating_mul(y.into())) .saturating_add(T::DbWeight::get().reads(9_u64)) .saturating_add(T::DbWeight::get().reads((1_u64).saturating_mul(y.into()))) .saturating_add(T::DbWeight::get().writes(7_u64)) @@ -141,10 +141,10 @@ impl WeightInfo for SubstrateWeight { // Proof Size summary in bytes: // Measured: `1029` // Estimated: `4787 + x * (2641 ±0)` - // Minimum execution time: 52_231_308_000 picoseconds. - Weight::from_parts(37_784_411_122, 4787) - // Standard Error: 80_093_957 - .saturating_add(Weight::from_parts(15_157_828_698, 0).saturating_mul(x.into())) + // Minimum execution time: 52_011_202_000 picoseconds. + Weight::from_parts(37_697_282_724, 4787) + // Standard Error: 35_208_663 + .saturating_add(Weight::from_parts(14_794_809_485, 0).saturating_mul(x.into())) .saturating_add(T::DbWeight::get().reads(5_u64)) .saturating_add(T::DbWeight::get().reads((1_u64).saturating_mul(x.into()))) .saturating_add(T::DbWeight::get().writes(1_u64)) @@ -160,10 +160,10 @@ impl WeightInfo for SubstrateWeight { // Proof Size summary in bytes: // Measured: `590458` // Estimated: `591809` - // Minimum execution time: 137_798_064_000 picoseconds. - Weight::from_parts(138_407_861_337, 591809) - // Standard Error: 4_606_141 - .saturating_add(Weight::from_parts(162_094_811, 0).saturating_mul(x.into())) + // Minimum execution time: 133_845_298_000 picoseconds. + Weight::from_parts(135_662_928_997, 591809) + // Standard Error: 10_729_052 + .saturating_add(Weight::from_parts(151_289_127, 0).saturating_mul(x.into())) .saturating_add(T::DbWeight::get().reads(1_u64)) .saturating_add(T::DbWeight::get().writes(1_u64)) } @@ -173,8 +173,8 @@ impl WeightInfo for SubstrateWeight { // Proof Size summary in bytes: // Measured: `590458` // Estimated: `591809` - // Minimum execution time: 57_369_356_000 picoseconds. - Weight::from_parts(57_377_021_000, 591809) + // Minimum execution time: 54_932_828_000 picoseconds. + Weight::from_parts(55_457_114_000, 591809) .saturating_add(T::DbWeight::get().reads(1_u64)) } } @@ -195,8 +195,8 @@ impl WeightInfo for () { // Proof Size summary in bytes: // Measured: `270` // Estimated: `1755` - // Minimum execution time: 421_387_000 picoseconds. - Weight::from_parts(424_844_000, 1755) + // Minimum execution time: 435_616_000 picoseconds. + Weight::from_parts(438_943_000, 1755) .saturating_add(RocksDbWeight::get().reads(4_u64)) .saturating_add(RocksDbWeight::get().writes(3_u64)) } @@ -230,12 +230,12 @@ impl WeightInfo for () { // Proof Size summary in bytes: // Measured: `590614 + x * (33 ±0) + y * (37 ±0)` // Estimated: `592100 + x * (33 ±0) + y * (2641 ±0)` - // Minimum execution time: 142_890_248_000 picoseconds. - Weight::from_parts(138_485_179_943, 592100) - // Standard Error: 6_803_050 - .saturating_add(Weight::from_parts(168_321_054, 0).saturating_mul(x.into())) - // Standard Error: 751_326 - .saturating_add(Weight::from_parts(4_848_878, 0).saturating_mul(y.into())) + // Minimum execution time: 135_714_229_000 picoseconds. + Weight::from_parts(132_409_697_079, 592100) + // Standard Error: 4_688_986 + .saturating_add(Weight::from_parts(174_151_591, 0).saturating_mul(x.into())) + // Standard Error: 517_850 + .saturating_add(Weight::from_parts(6_921_142, 0).saturating_mul(y.into())) .saturating_add(RocksDbWeight::get().reads(9_u64)) .saturating_add(RocksDbWeight::get().reads((1_u64).saturating_mul(y.into()))) .saturating_add(RocksDbWeight::get().writes(7_u64)) @@ -260,10 +260,10 @@ impl WeightInfo for () { // Proof Size summary in bytes: // Measured: `1029` // Estimated: `4787 + x * (2641 ±0)` - // Minimum execution time: 52_231_308_000 picoseconds. - Weight::from_parts(37_784_411_122, 4787) - // Standard Error: 80_093_957 - .saturating_add(Weight::from_parts(15_157_828_698, 0).saturating_mul(x.into())) + // Minimum execution time: 52_011_202_000 picoseconds. + Weight::from_parts(37_697_282_724, 4787) + // Standard Error: 35_208_663 + .saturating_add(Weight::from_parts(14_794_809_485, 0).saturating_mul(x.into())) .saturating_add(RocksDbWeight::get().reads(5_u64)) .saturating_add(RocksDbWeight::get().reads((1_u64).saturating_mul(x.into()))) .saturating_add(RocksDbWeight::get().writes(1_u64)) @@ -279,10 +279,10 @@ impl WeightInfo for () { // Proof Size summary in bytes: // Measured: `590458` // Estimated: `591809` - // Minimum execution time: 137_798_064_000 picoseconds. - Weight::from_parts(138_407_861_337, 591809) - // Standard Error: 4_606_141 - .saturating_add(Weight::from_parts(162_094_811, 0).saturating_mul(x.into())) + // Minimum execution time: 133_845_298_000 picoseconds. + Weight::from_parts(135_662_928_997, 591809) + // Standard Error: 10_729_052 + .saturating_add(Weight::from_parts(151_289_127, 0).saturating_mul(x.into())) .saturating_add(RocksDbWeight::get().reads(1_u64)) .saturating_add(RocksDbWeight::get().writes(1_u64)) } @@ -292,8 +292,8 @@ impl WeightInfo for () { // Proof Size summary in bytes: // Measured: `590458` // Estimated: `591809` - // Minimum execution time: 57_369_356_000 picoseconds. - Weight::from_parts(57_377_021_000, 591809) + // Minimum execution time: 54_932_828_000 picoseconds. + Weight::from_parts(55_457_114_000, 591809) .saturating_add(RocksDbWeight::get().reads(1_u64)) } } From 0bea265025c9092a0ad96bf05e6608b95dd2a94b Mon Sep 17 00:00:00 2001 From: Davide Galassi Date: Fri, 10 May 2024 17:29:33 +0200 Subject: [PATCH 24/42] Check randomness vrf signature on debug --- substrate/frame/sassafras/Cargo.toml | 2 +- substrate/frame/sassafras/src/benchmarking.rs | 8 +--- substrate/frame/sassafras/src/lib.rs | 46 +++++++++++-------- substrate/frame/sassafras/src/tests.rs | 6 +-- 4 files changed, 32 insertions(+), 30 deletions(-) diff --git a/substrate/frame/sassafras/Cargo.toml b/substrate/frame/sassafras/Cargo.toml index 118ac8cc2b79..1ac4f500666f 100644 --- a/substrate/frame/sassafras/Cargo.toml +++ b/substrate/frame/sassafras/Cargo.toml @@ -24,6 +24,7 @@ frame-support = { path = "../support", default-features = false } frame-system = { path = "../system", default-features = false } log = { workspace = true } sp-consensus-sassafras = { path = "../../primitives/consensus/sassafras", default-features = false, features = ["serde"] } +sp-core = { path = "../../primitives/core", default-features = false } sp-io = { path = "../../primitives/io", default-features = false } sp-runtime = { path = "../../primitives/runtime", default-features = false } sp-std = { path = "../../primitives/std", default-features = false } @@ -31,7 +32,6 @@ sp-std = { path = "../../primitives/std", default-features = false } [dev-dependencies] env_logger = "0.11.3" array-bytes = "6.2.2" -sp-core = { path = "../../primitives/core" } sp-crypto-hashing = { path = "../../primitives/crypto/hashing" } [features] diff --git a/substrate/frame/sassafras/src/benchmarking.rs b/substrate/frame/sassafras/src/benchmarking.rs index 5323cea5755f..040e74d76ac9 100644 --- a/substrate/frame/sassafras/src/benchmarking.rs +++ b/substrate/frame/sassafras/src/benchmarking.rs @@ -18,7 +18,7 @@ //! Benchmarks for the Sassafras pallet. use crate::*; -use sp_consensus_sassafras::vrf::{VrfPreOutput, VrfSignature}; +use sp_consensus_sassafras::vrf::VrfSignature; use frame_benchmarking::v2::*; use frame_support::traits::Hooks; @@ -44,10 +44,6 @@ fn dummy_vrf_signature() -> VrfSignature { VrfSignature::decode(&mut &RAW_VRF_SIGNATURE[..]).unwrap() } -fn dummy_pre_output() -> VrfPreOutput { - dummy_vrf_signature().pre_outputs[0] -} - #[benchmarks] mod benchmarks { use super::*; @@ -91,7 +87,7 @@ mod benchmarks { // Makes the epoch change legit let post_init_cache = EphemeralData { prev_slot: Slot::from(config.epoch_length as u64 - 1), - pre_output: dummy_pre_output(), + block_randomness: Randomness::default(), }; TemporaryData::::put(post_init_cache); CurrentSlot::::set(Slot::from(config.epoch_length as u64)); diff --git a/substrate/frame/sassafras/src/lib.rs b/substrate/frame/sassafras/src/lib.rs index 5890396860ac..d07264805325 100644 --- a/substrate/frame/sassafras/src/lib.rs +++ b/substrate/frame/sassafras/src/lib.rs @@ -118,8 +118,8 @@ pub type TicketsCounter = [u32; 2]; pub struct EphemeralData { /// Previous block slot. prev_slot: Slot, - /// Cached pre-output which ships within the block's header digest. - pre_output: vrf::VrfPreOutput, + /// Per block randomness to be deposited after block execution (on finalization). + block_randomness: Randomness, } /// Key used for the tickets accumulator map. @@ -326,11 +326,27 @@ pub mod pallet { .find_map(|item| item.pre_runtime_try_to::(&SASSAFRAS_ENGINE_ID)) .expect("Valid block must have a slot claim. qed"); - let ephemeral_data = EphemeralData { + let randomness_accumulator = Self::randomness_accumulator(); + let randomness_input = vrf::block_randomness_input(&randomness_accumulator, claim.slot); + + // Verification has already been done by the host + debug_assert!({ + use sp_core::crypto::{VrfPublic, Wraps}; + let authorities = Authorities::::get(); + let public = authorities + .get(claim.authority_idx as usize) + .expect("Bad authority index in claim"); + let data = vrf::block_randomness_sign_data(&randomness_accumulator, claim.slot); + public.as_inner_ref().vrf_verify(&data, &claim.vrf_signature) + }); + + let block_randomness = claim.vrf_signature.pre_outputs[0] + .make_bytes::(RANDOMNESS_VRF_CONTEXT, &randomness_input); + + TemporaryData::::put(EphemeralData { prev_slot: CurrentSlot::::get(), - pre_output: claim.vrf_signature.pre_outputs[0].clone(), - }; - TemporaryData::::put(ephemeral_data); + block_randomness, + }); CurrentSlot::::put(claim.slot); @@ -344,24 +360,14 @@ pub mod pallet { } fn on_finalize(_: BlockNumberFor) { - // At the end of the block, we can safely include the current slot randomness + // At the end of the block, we can safely include the current block 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::block_randomness_input( - &Self::randomness_accumulator(), - CurrentSlot::::get(), - ); - let randomness_pre_output = TemporaryData::::take() + let block_randomness = TemporaryData::::take() .expect("Unconditionally populated in `on_initialize`; `on_finalize` is always called after; qed") - .pre_output; - - // TODO: debug_assert that signature is valid - // Verification has been already done by the host! - - let randomness = randomness_pre_output - .make_bytes::(RANDOMNESS_VRF_CONTEXT, &randomness_input); - Self::deposit_randomness(randomness); + .block_randomness; + Self::deposit_randomness(block_randomness); // Check if we are in the epoch's second half. // If so, start sorting the next epoch tickets. diff --git a/substrate/frame/sassafras/src/tests.rs b/substrate/frame/sassafras/src/tests.rs index c7d9df66af0a..cbf566ac5fb6 100644 --- a/substrate/frame/sassafras/src/tests.rs +++ b/substrate/frame/sassafras/src/tests.rs @@ -203,7 +203,7 @@ fn on_first_block() { common_assertions(false); let post_fini_randomness = Sassafras::randomness_buf(); - prefix_eq!(post_fini_randomness[0], h2b("c5caf074")); + prefix_eq!(post_fini_randomness[0], h2b("3e5bd6d3")); prefix_eq!(post_fini_randomness[1], post_init_randomness[1]); prefix_eq!(post_fini_randomness[2], post_init_randomness[2]); prefix_eq!(post_fini_randomness[3], post_init_randomness[3]); @@ -252,7 +252,7 @@ fn on_normal_block() { common_assertions(true); let post_init_randomness = Sassafras::randomness_buf(); - prefix_eq!(post_init_randomness[0], h2b("c5caf074")); + prefix_eq!(post_init_randomness[0], h2b("3e5bd6d3")); prefix_eq!(post_init_randomness[1], h2b("4e8c71d2")); prefix_eq!(post_init_randomness[2], h2b("3a4c0005")); prefix_eq!(post_init_randomness[3], h2b("0dd43c54")); @@ -263,7 +263,7 @@ fn on_normal_block() { common_assertions(false); let post_fini_randomness = Sassafras::randomness_buf(); - prefix_eq!(post_fini_randomness[0], h2b("1b6777c9")); + prefix_eq!(post_fini_randomness[0], h2b("ecff3e90")); prefix_eq!(post_fini_randomness[1], post_init_randomness[1]); prefix_eq!(post_fini_randomness[2], post_init_randomness[2]); prefix_eq!(post_fini_randomness[3], post_init_randomness[3]); From 62b544dc71a732bb1d9b6a8b0a28c45e0fc8c855 Mon Sep 17 00:00:00 2001 From: Davide Galassi Date: Fri, 10 May 2024 19:10:38 +0200 Subject: [PATCH 25/42] Deposit tickets test --- substrate/frame/sassafras/src/lib.rs | 8 +- substrate/frame/sassafras/src/tests.rs | 138 ++++++++++++++++--------- 2 files changed, 94 insertions(+), 52 deletions(-) diff --git a/substrate/frame/sassafras/src/lib.rs b/substrate/frame/sassafras/src/lib.rs index d07264805325..d59069dc1bb4 100644 --- a/substrate/frame/sassafras/src/lib.rs +++ b/substrate/frame/sassafras/src/lib.rs @@ -216,6 +216,8 @@ pub mod pallet { TicketOverThreshold, /// Duplicate ticket TicketDuplicate, + /// Bad ticket order + TicketBadOrder, /// Invalid ticket TicketInvalid, /// Invalid ticket signature @@ -565,12 +567,16 @@ impl Pallet { pub(crate) fn deposit_tickets(tickets: &[(TicketId, TicketBody)]) -> Result<(), Error> { let prev_count = TicketsAccumulator::::count(); + let mut prev_id = None; for (id, body) in tickets.iter() { + if prev_id.map(|prev| id <= prev).unwrap_or_default() { + return Err(Error::TicketBadOrder) + } TicketsAccumulator::::insert(TicketKey::from(*id), body); + prev_id = Some(id); } let count = TicketsAccumulator::::count(); if count != prev_count + tickets.len() as u32 { - // Duplicates are not allowed return Err(Error::TicketDuplicate) } let diff = count.saturating_sub(T::EpochLength::get()); diff --git a/substrate/frame/sassafras/src/tests.rs b/substrate/frame/sassafras/src/tests.rs index cbf566ac5fb6..73c02a7dd106 100644 --- a/substrate/frame/sassafras/src/tests.rs +++ b/substrate/frame/sassafras/src/tests.rs @@ -44,38 +44,32 @@ macro_rules! prefix_eq { }}; } -// Fisher Yates shuffle. +// Fisher-Yates shuffle. // -// We don't want to implement anything secure here. -// Just a trivial shuffle for the tests. +// We don't want to implement something secure here. +// Just a trivial pseudo-random shuffle for the tests. fn shuffle(vector: &mut Vec, random_seed: u64) { - let mut rng = random_seed as usize; + let mut r = random_seed as usize; for i in (1..vector.len()).rev() { - let j = rng % (i + 1); + let j = r % (i + 1); vector.swap(i, j); - rng = (rng.wrapping_mul(6364793005) + 1) as usize; // Some random number generation + r = (r.wrapping_mul(6364793005) + 1) as usize; } } fn dummy_tickets(count: usize) -> Vec<(TicketId, TicketBody)> { (0..count) - .map(|v| { - let id = TicketId([v as u8; 32]); - let body = TicketBody { attempt_idx: v as u32, extra: Default::default() }; - (id, body) + .map(|i| { + let mut id = [0xff; 32]; + id[..8].copy_from_slice(&i.to_be_bytes()[..]); + (TicketId(id), TicketBody { attempt_idx: i as u32, extra: Default::default() }) }) .collect() } #[test] fn assumptions_check() { - let mut tickets: Vec<_> = (0..100_u32) - .map(|i| { - let mut id = [0xff; 32]; - id[..4].copy_from_slice(&i.to_be_bytes()[..]); - (TicketId(id), TicketBody { attempt_idx: 0, extra: Default::default() }) - }) - .collect(); + let mut tickets = dummy_tickets(100); shuffle(&mut tickets, 123); new_test_ext(3).execute_with(|| { @@ -99,33 +93,92 @@ fn assumptions_check() { } #[test] -fn deposit_tickets_failure() { - new_test_ext(3).execute_with(|| { - let mut tickets = dummy_tickets(15); - shuffle(&mut tickets, 123); +fn deposit_tickets_works() { + let mut tickets = dummy_tickets(15); + shuffle(&mut tickets, 123); + new_test_ext(1).execute_with(|| { + // Try to append an unsorted chunk let mut candidates = tickets[..5].to_vec(); + let err = Sassafras::deposit_tickets(&candidates).unwrap_err(); + assert!(matches!(err, Error::TicketBadOrder)); + let _ = TicketsAccumulator::::clear(u32::MAX, None); + + // Correctly append the first sorted chunk + let mut candidates = tickets[..5].to_vec(); + candidates.sort_unstable_by_key(|a| a.0); Sassafras::deposit_tickets(&candidates).unwrap(); assert_eq!(TicketsAccumulator::::count(), 5); - - candidates.sort_unstable_by(|a, b| b.0.cmp(&a.0)); + // Note: internally the tickets are stored in reverse order (bigger first) let stored: Vec<_> = TicketsAccumulator::::iter() .map(|(k, b)| (TicketId::from(k), b)) .collect(); - assert_eq!(candidates, stored); - + let mut expected = tickets[..5].to_vec(); + expected.sort_unstable_by(|a, b| b.0.cmp(&a.0)); + assert_eq!(expected, stored); TicketsAccumulator::::iter().for_each(|(key, body)| { println!("{:?}, {:?}", TicketId::from(key), body); }); - Sassafras::deposit_tickets(&tickets[5..7]).unwrap(); - assert_eq!(TicketsAccumulator::::count(), 7); + // Try to append a chunk with a ticket already pushed + let mut candidates = tickets[4..10].to_vec(); + candidates.sort_unstable_by_key(|a| a.0); + let err = Sassafras::deposit_tickets(&candidates).unwrap_err(); + assert!(matches!(err, Error::TicketDuplicate)); + // Restore last correct state + let _ = TicketsAccumulator::::clear(u32::MAX, None); + let mut candidates = tickets[..5].to_vec(); + candidates.sort_unstable_by_key(|a| a.0); + Sassafras::deposit_tickets(&candidates).unwrap(); + + println!("-------------"); + // Correctly push the second sorted chunk + let mut candidates = tickets[5..10].to_vec(); + candidates.sort_unstable_by_key(|a| a.0); + Sassafras::deposit_tickets(&candidates).unwrap(); + assert_eq!(TicketsAccumulator::::count(), 10); + // Note: internally the tickets are stored in reverse order (bigger first) + let mut stored: Vec<_> = TicketsAccumulator::::iter() + .map(|(k, b)| (TicketId::from(k), b)) + .collect(); + let mut expected = tickets[..10].to_vec(); + expected.sort_unstable_by(|a, b| b.0.cmp(&a.0)); + assert_eq!(expected, stored); TicketsAccumulator::::iter().for_each(|(key, body)| { println!("{:?}, {:?}", TicketId::from(key), body); }); - assert!(Sassafras::deposit_tickets(&tickets[7..]).is_err()); + println!("-------------"); + + // Now the buffer is full, pick only the tickets that will eventually fit. + let mut candidates = tickets[10..].to_vec(); + candidates.sort_unstable_by_key(|a| a.0); + let mut eligible = Vec::new(); + for candidate in candidates { + if stored.is_empty() { + break + } + let bigger = stored.remove(0); + if bigger.0 <= candidate.0 { + break + } + eligible.push(candidate); + } + candidates = eligible; + + // Correctly push the last candidates chunk + Sassafras::deposit_tickets(&candidates).unwrap(); + assert_eq!(TicketsAccumulator::::count(), 10); + // Note: internally the tickets are stored in reverse order (bigger first) + let mut stored: Vec<_> = TicketsAccumulator::::iter() + .map(|(k, b)| (TicketId::from(k), b)) + .collect(); + tickets.sort_unstable_by(|a, b| b.0.cmp(&a.0)); + assert_eq!(tickets[5..], stored); + TicketsAccumulator::::iter().for_each(|(key, body)| { + println!("{:?}, {:?}", TicketId::from(key), body); + }); }); } @@ -328,13 +381,8 @@ fn slot_ticket_id_outside_in_fetch() { let genesis_slot = Slot::from(GENESIS_SLOT); let curr_count = 8; let next_count = 6; - let tickets_count = curr_count + next_count; + let tickets = dummy_tickets(curr_count + next_count); - let tickets: Vec<_> = (0..tickets_count) - .map(|i| { - (TicketId([i as u8; 32]), TicketBody { attempt_idx: 0, extra: Default::default() }) - }) - .collect(); // Current epoch tickets let curr_tickets = tickets[..curr_count].to_vec(); let next_tickets = tickets[curr_count..].to_vec(); @@ -445,18 +493,11 @@ fn slot_and_epoch_helpers_works() { #[test] fn tickets_accumulator_works() { - let e1_count = 6; - let e2_count = 10; - - let tot_count = e1_count + e2_count; let start_block = 1; let start_slot = (GENESIS_SLOT + 1).into(); - - let tickets: Vec<_> = (0..tot_count) - .map(|i| { - (TicketId([i as u8; 32]), TicketBody { attempt_idx: 0, extra: Default::default() }) - }) - .collect(); + let e1_count = 6; + let e2_count = 10; + let tickets = dummy_tickets(e1_count + e2_count); let e1_tickets = tickets[..e1_count].to_vec(); let e2_tickets = tickets[e1_count..].to_vec(); @@ -542,12 +583,7 @@ fn tickets_accumulator_works() { #[test] fn incremental_accumulator_drain() { - let tot_count = 10; - let tickets: Vec<_> = (0..tot_count) - .map(|i| { - (TicketId([i as u8; 32]), TicketBody { attempt_idx: 0, extra: Default::default() }) - }) - .collect(); + let tickets = dummy_tickets(10); new_test_ext(0).execute_with(|| { tickets @@ -636,7 +672,7 @@ fn submit_tickets_with_ring_proof_check_works() { // Submit the tickets let candidates_per_call = 4; - let chunks: Vec<_> = candidates + let mut chunks: Vec<_> = candidates .chunks(candidates_per_call) .map(|chunk| BoundedVec::truncate_from(chunk.to_vec())) .collect(); From c487918034ed5d64903a3377d5ecbed912fa5c90 Mon Sep 17 00:00:00 2001 From: Davide Galassi Date: Fri, 10 May 2024 19:23:41 +0200 Subject: [PATCH 26/42] Nits --- substrate/frame/sassafras/src/weights.rs | 96 +++++++++---------- substrate/primitives/core/src/bandersnatch.rs | 2 - 2 files changed, 48 insertions(+), 50 deletions(-) diff --git a/substrate/frame/sassafras/src/weights.rs b/substrate/frame/sassafras/src/weights.rs index 1d03f34dd380..04755c513c26 100644 --- a/substrate/frame/sassafras/src/weights.rs +++ b/substrate/frame/sassafras/src/weights.rs @@ -64,20 +64,20 @@ pub struct SubstrateWeight(PhantomData); impl WeightInfo for SubstrateWeight { /// Storage: `System::Digest` (r:1 w:0) /// Proof: `System::Digest` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) - /// Storage: `Sassafras::CurrentSlot` (r:1 w:1) - /// Proof: `Sassafras::CurrentSlot` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `MaxEncodedLen`) /// Storage: `Sassafras::RandomnessBuf` (r:1 w:1) /// Proof: `Sassafras::RandomnessBuf` (`max_values`: Some(1), `max_size`: Some(128), added: 623, mode: `MaxEncodedLen`) + /// Storage: `Sassafras::CurrentSlot` (r:1 w:1) + /// Proof: `Sassafras::CurrentSlot` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `MaxEncodedLen`) /// Storage: `Sassafras::CounterForTicketsAccumulator` (r:1 w:0) /// Proof: `Sassafras::CounterForTicketsAccumulator` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`) /// Storage: `Sassafras::TemporaryData` (r:0 w:1) - /// Proof: `Sassafras::TemporaryData` (`max_values`: Some(1), `max_size`: Some(41), added: 536, mode: `MaxEncodedLen`) + /// Proof: `Sassafras::TemporaryData` (`max_values`: Some(1), `max_size`: Some(40), added: 535, mode: `MaxEncodedLen`) fn on_initialize() -> Weight { // Proof Size summary in bytes: // Measured: `270` // Estimated: `1755` - // Minimum execution time: 435_616_000 picoseconds. - Weight::from_parts(438_943_000, 1755) + // Minimum execution time: 315_749_000 picoseconds. + Weight::from_parts(317_523_000, 1755) .saturating_add(T::DbWeight::get().reads(4_u64)) .saturating_add(T::DbWeight::get().writes(3_u64)) } @@ -88,7 +88,7 @@ impl WeightInfo for SubstrateWeight { /// Storage: `Sassafras::RingContext` (r:1 w:0) /// Proof: `Sassafras::RingContext` (`max_values`: Some(1), `max_size`: Some(590324), added: 590819, mode: `MaxEncodedLen`) /// Storage: `Sassafras::TemporaryData` (r:1 w:0) - /// Proof: `Sassafras::TemporaryData` (`max_values`: Some(1), `max_size`: Some(41), added: 536, mode: `MaxEncodedLen`) + /// Proof: `Sassafras::TemporaryData` (`max_values`: Some(1), `max_size`: Some(40), added: 535, mode: `MaxEncodedLen`) /// Storage: `Sassafras::RandomnessBuf` (r:1 w:1) /// Proof: `Sassafras::RandomnessBuf` (`max_values`: Some(1), `max_size`: Some(128), added: 623, mode: `MaxEncodedLen`) /// Storage: `System::Digest` (r:1 w:1) @@ -109,14 +109,14 @@ impl WeightInfo for SubstrateWeight { /// The range of component `y` is `[100, 1000]`. fn enact_epoch_change(x: u32, y: u32, ) -> Weight { // Proof Size summary in bytes: - // Measured: `590614 + x * (33 ±0) + y * (37 ±0)` - // Estimated: `592100 + x * (33 ±0) + y * (2641 ±0)` - // Minimum execution time: 135_714_229_000 picoseconds. - Weight::from_parts(132_409_697_079, 592100) - // Standard Error: 4_688_986 - .saturating_add(Weight::from_parts(174_151_591, 0).saturating_mul(x.into())) - // Standard Error: 517_850 - .saturating_add(Weight::from_parts(6_921_142, 0).saturating_mul(y.into())) + // Measured: `590613 + x * (33 ±0) + y * (37 ±0)` + // Estimated: `592099 + x * (33 ±0) + y * (2641 ±0)` + // Minimum execution time: 138_319_222_000 picoseconds. + Weight::from_parts(134_117_334_052, 592099) + // Standard Error: 5_392_475 + .saturating_add(Weight::from_parts(170_217_303, 0).saturating_mul(x.into())) + // Standard Error: 595_543 + .saturating_add(Weight::from_parts(5_308_040, 0).saturating_mul(y.into())) .saturating_add(T::DbWeight::get().reads(9_u64)) .saturating_add(T::DbWeight::get().reads((1_u64).saturating_mul(y.into()))) .saturating_add(T::DbWeight::get().writes(7_u64)) @@ -141,10 +141,10 @@ impl WeightInfo for SubstrateWeight { // Proof Size summary in bytes: // Measured: `1029` // Estimated: `4787 + x * (2641 ±0)` - // Minimum execution time: 52_011_202_000 picoseconds. - Weight::from_parts(37_697_282_724, 4787) - // Standard Error: 35_208_663 - .saturating_add(Weight::from_parts(14_794_809_485, 0).saturating_mul(x.into())) + // Minimum execution time: 51_909_043_000 picoseconds. + Weight::from_parts(37_526_932_333, 4787) + // Standard Error: 13_320_201 + .saturating_add(Weight::from_parts(14_742_687_073, 0).saturating_mul(x.into())) .saturating_add(T::DbWeight::get().reads(5_u64)) .saturating_add(T::DbWeight::get().reads((1_u64).saturating_mul(x.into()))) .saturating_add(T::DbWeight::get().writes(1_u64)) @@ -160,10 +160,10 @@ impl WeightInfo for SubstrateWeight { // Proof Size summary in bytes: // Measured: `590458` // Estimated: `591809` - // Minimum execution time: 133_845_298_000 picoseconds. - Weight::from_parts(135_662_928_997, 591809) - // Standard Error: 10_729_052 - .saturating_add(Weight::from_parts(151_289_127, 0).saturating_mul(x.into())) + // Minimum execution time: 133_728_565_000 picoseconds. + Weight::from_parts(133_908_713_941, 591809) + // Standard Error: 2_906_128 + .saturating_add(Weight::from_parts(152_929_171, 0).saturating_mul(x.into())) .saturating_add(T::DbWeight::get().reads(1_u64)) .saturating_add(T::DbWeight::get().writes(1_u64)) } @@ -173,8 +173,8 @@ impl WeightInfo for SubstrateWeight { // Proof Size summary in bytes: // Measured: `590458` // Estimated: `591809` - // Minimum execution time: 54_932_828_000 picoseconds. - Weight::from_parts(55_457_114_000, 591809) + // Minimum execution time: 54_789_232_000 picoseconds. + Weight::from_parts(54_820_282_000, 591809) .saturating_add(T::DbWeight::get().reads(1_u64)) } } @@ -183,20 +183,20 @@ impl WeightInfo for SubstrateWeight { impl WeightInfo for () { /// Storage: `System::Digest` (r:1 w:0) /// Proof: `System::Digest` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) - /// Storage: `Sassafras::CurrentSlot` (r:1 w:1) - /// Proof: `Sassafras::CurrentSlot` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `MaxEncodedLen`) /// Storage: `Sassafras::RandomnessBuf` (r:1 w:1) /// Proof: `Sassafras::RandomnessBuf` (`max_values`: Some(1), `max_size`: Some(128), added: 623, mode: `MaxEncodedLen`) + /// Storage: `Sassafras::CurrentSlot` (r:1 w:1) + /// Proof: `Sassafras::CurrentSlot` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `MaxEncodedLen`) /// Storage: `Sassafras::CounterForTicketsAccumulator` (r:1 w:0) /// Proof: `Sassafras::CounterForTicketsAccumulator` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`) /// Storage: `Sassafras::TemporaryData` (r:0 w:1) - /// Proof: `Sassafras::TemporaryData` (`max_values`: Some(1), `max_size`: Some(41), added: 536, mode: `MaxEncodedLen`) + /// Proof: `Sassafras::TemporaryData` (`max_values`: Some(1), `max_size`: Some(40), added: 535, mode: `MaxEncodedLen`) fn on_initialize() -> Weight { // Proof Size summary in bytes: // Measured: `270` // Estimated: `1755` - // Minimum execution time: 435_616_000 picoseconds. - Weight::from_parts(438_943_000, 1755) + // Minimum execution time: 315_749_000 picoseconds. + Weight::from_parts(317_523_000, 1755) .saturating_add(RocksDbWeight::get().reads(4_u64)) .saturating_add(RocksDbWeight::get().writes(3_u64)) } @@ -207,7 +207,7 @@ impl WeightInfo for () { /// Storage: `Sassafras::RingContext` (r:1 w:0) /// Proof: `Sassafras::RingContext` (`max_values`: Some(1), `max_size`: Some(590324), added: 590819, mode: `MaxEncodedLen`) /// Storage: `Sassafras::TemporaryData` (r:1 w:0) - /// Proof: `Sassafras::TemporaryData` (`max_values`: Some(1), `max_size`: Some(41), added: 536, mode: `MaxEncodedLen`) + /// Proof: `Sassafras::TemporaryData` (`max_values`: Some(1), `max_size`: Some(40), added: 535, mode: `MaxEncodedLen`) /// Storage: `Sassafras::RandomnessBuf` (r:1 w:1) /// Proof: `Sassafras::RandomnessBuf` (`max_values`: Some(1), `max_size`: Some(128), added: 623, mode: `MaxEncodedLen`) /// Storage: `System::Digest` (r:1 w:1) @@ -228,14 +228,14 @@ impl WeightInfo for () { /// The range of component `y` is `[100, 1000]`. fn enact_epoch_change(x: u32, y: u32, ) -> Weight { // Proof Size summary in bytes: - // Measured: `590614 + x * (33 ±0) + y * (37 ±0)` - // Estimated: `592100 + x * (33 ±0) + y * (2641 ±0)` - // Minimum execution time: 135_714_229_000 picoseconds. - Weight::from_parts(132_409_697_079, 592100) - // Standard Error: 4_688_986 - .saturating_add(Weight::from_parts(174_151_591, 0).saturating_mul(x.into())) - // Standard Error: 517_850 - .saturating_add(Weight::from_parts(6_921_142, 0).saturating_mul(y.into())) + // Measured: `590613 + x * (33 ±0) + y * (37 ±0)` + // Estimated: `592099 + x * (33 ±0) + y * (2641 ±0)` + // Minimum execution time: 138_319_222_000 picoseconds. + Weight::from_parts(134_117_334_052, 592099) + // Standard Error: 5_392_475 + .saturating_add(Weight::from_parts(170_217_303, 0).saturating_mul(x.into())) + // Standard Error: 595_543 + .saturating_add(Weight::from_parts(5_308_040, 0).saturating_mul(y.into())) .saturating_add(RocksDbWeight::get().reads(9_u64)) .saturating_add(RocksDbWeight::get().reads((1_u64).saturating_mul(y.into()))) .saturating_add(RocksDbWeight::get().writes(7_u64)) @@ -260,10 +260,10 @@ impl WeightInfo for () { // Proof Size summary in bytes: // Measured: `1029` // Estimated: `4787 + x * (2641 ±0)` - // Minimum execution time: 52_011_202_000 picoseconds. - Weight::from_parts(37_697_282_724, 4787) - // Standard Error: 35_208_663 - .saturating_add(Weight::from_parts(14_794_809_485, 0).saturating_mul(x.into())) + // Minimum execution time: 51_909_043_000 picoseconds. + Weight::from_parts(37_526_932_333, 4787) + // Standard Error: 13_320_201 + .saturating_add(Weight::from_parts(14_742_687_073, 0).saturating_mul(x.into())) .saturating_add(RocksDbWeight::get().reads(5_u64)) .saturating_add(RocksDbWeight::get().reads((1_u64).saturating_mul(x.into()))) .saturating_add(RocksDbWeight::get().writes(1_u64)) @@ -279,10 +279,10 @@ impl WeightInfo for () { // Proof Size summary in bytes: // Measured: `590458` // Estimated: `591809` - // Minimum execution time: 133_845_298_000 picoseconds. - Weight::from_parts(135_662_928_997, 591809) - // Standard Error: 10_729_052 - .saturating_add(Weight::from_parts(151_289_127, 0).saturating_mul(x.into())) + // Minimum execution time: 133_728_565_000 picoseconds. + Weight::from_parts(133_908_713_941, 591809) + // Standard Error: 2_906_128 + .saturating_add(Weight::from_parts(152_929_171, 0).saturating_mul(x.into())) .saturating_add(RocksDbWeight::get().reads(1_u64)) .saturating_add(RocksDbWeight::get().writes(1_u64)) } @@ -292,8 +292,8 @@ impl WeightInfo for () { // Proof Size summary in bytes: // Measured: `590458` // Estimated: `591809` - // Minimum execution time: 54_932_828_000 picoseconds. - Weight::from_parts(55_457_114_000, 591809) + // Minimum execution time: 54_789_232_000 picoseconds. + Weight::from_parts(54_820_282_000, 591809) .saturating_add(RocksDbWeight::get().reads(1_u64)) } } diff --git a/substrate/primitives/core/src/bandersnatch.rs b/substrate/primitives/core/src/bandersnatch.rs index d70af4655e14..ecc035bff101 100644 --- a/substrate/primitives/core/src/bandersnatch.rs +++ b/substrate/primitives/core/src/bandersnatch.rs @@ -227,8 +227,6 @@ pub mod vrf { } } - impl EncodeLike for VrfPreOutput {} - impl MaxEncodedLen for VrfPreOutput { fn max_encoded_len() -> usize { <[u8; PREOUT_SERIALIZED_SIZE]>::max_encoded_len() From 6d8080d7157afeca4453ed8f15954fb5965a2eaa Mon Sep 17 00:00:00 2001 From: Davide Galassi Date: Fri, 10 May 2024 19:53:53 +0200 Subject: [PATCH 27/42] Fix std feature propagation --- substrate/frame/sassafras/Cargo.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/substrate/frame/sassafras/Cargo.toml b/substrate/frame/sassafras/Cargo.toml index 1ac4f500666f..900ea1bf3d23 100644 --- a/substrate/frame/sassafras/Cargo.toml +++ b/substrate/frame/sassafras/Cargo.toml @@ -44,6 +44,7 @@ std = [ "scale-codec/std", "scale-info/std", "sp-consensus-sassafras/std", + "sp-core/std", "sp-io/std", "sp-runtime/std", "sp-std/std", From 72b1a65b0c250eb3845906fad3ec6058e72b44c8 Mon Sep 17 00:00:00 2001 From: Davide Galassi Date: Mon, 13 May 2024 14:58:45 +0200 Subject: [PATCH 28/42] Fallback to AURA if unable to construct ring verifier --- substrate/frame/sassafras/src/lib.rs | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/substrate/frame/sassafras/src/lib.rs b/substrate/frame/sassafras/src/lib.rs index d59069dc1bb4..9ad789ed8ef9 100644 --- a/substrate/frame/sassafras/src/lib.rs +++ b/substrate/frame/sassafras/src/lib.rs @@ -47,7 +47,7 @@ #![warn(unused_must_use, unsafe_code, unused_variables, unused_imports, missing_docs)] #![cfg_attr(not(feature = "std"), no_std)] -use log::{debug, trace, warn}; +use log::{debug, error, trace, warn}; use scale_codec::{Decode, Encode, MaxEncodedLen}; use scale_info::TypeInfo; @@ -495,11 +495,15 @@ impl Pallet { let pks: Vec<_> = authorities.iter().map(|auth| *auth.as_ref()).collect(); debug!(target: LOG_TARGET, "Building ring verifier (ring size: {})", pks.len()); - let verifier_key = ring_ctx - .verifier_key(&pks) - .expect("Failed to build ring verifier key. This is a bug"); - - RingVerifierKey::::put(verifier_key); + let maybe_verifier_key = ring_ctx.verifier_key(&pks); + if maybe_verifier_key.is_none() { + error!( + target: LOG_TARGET, + "Failed to build verifier key. This should never happen,\n + falling back to AURA for next epoch as last resort" + ); + } + RingVerifierKey::::set(maybe_verifier_key); } /// Enact an epoch change. From dcb90429a12e48c62ac2cd878897ecb9230d0624 Mon Sep 17 00:00:00 2001 From: Davide Galassi Date: Mon, 10 Jun 2024 16:53:06 +0200 Subject: [PATCH 29/42] Ticket body is the onchain struct --- .../frame/sassafras/src/data/tickets.bin | Bin 16244 -> 16184 bytes substrate/frame/sassafras/src/lib.rs | 73 ++++---- substrate/frame/sassafras/src/mock.rs | 40 ++-- substrate/frame/sassafras/src/tests.rs | 177 ++++++++---------- .../primitives/consensus/sassafras/src/lib.rs | 4 +- .../consensus/sassafras/src/ticket.rs | 31 ++- .../primitives/consensus/sassafras/src/vrf.rs | 53 +++--- 7 files changed, 176 insertions(+), 202 deletions(-) diff --git a/substrate/frame/sassafras/src/data/tickets.bin b/substrate/frame/sassafras/src/data/tickets.bin index 96527efed64780b4ac626036df0999a264e845cc..af6fd097b1b9167be3ce0793f1448ce8e71fd7e9 100644 GIT binary patch delta 15952 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 delta 16013 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

3mVe9K&>hvIGKu3rc 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

for TicketKey { @@ -141,12 +143,6 @@ impl From for TicketKey { } } -impl From for TicketId { - fn from(mut value: TicketKey) -> Self { - TicketId(value.0.map(|b| !b)) - } -} - /// Authorities sequence. pub type AuthoritiesVec = WeakBoundedVec::MaxAuthorities>; @@ -193,11 +189,11 @@ pub mod pallet { /// Redundancy factor #[pallet::constant] - type RedundancyFactor: Get; + type RedundancyFactor: Get; /// Max attempts number #[pallet::constant] - type AttemptsNumber: Get; + type AttemptsNumber: Get; /// Epoch change trigger. /// @@ -273,7 +269,7 @@ pub mod pallet { /// of [`TicketsCount`]. #[pallet::storage] #[pallet::getter(fn tickets)] - pub type Tickets = StorageMap<_, Identity, (u8, u32), (TicketId, TicketBody)>; + pub type Tickets = StorageMap<_, Identity, (u8, u32), TicketBody>; /// Parameters used to construct the epoch's ring verifier. /// @@ -396,13 +392,13 @@ pub mod pallet { /// 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), + T::WeightInfo::submit_tickets(envelopes.len() as u32), DispatchClass::Mandatory ))] - pub fn submit_tickets(origin: OriginFor, tickets: TicketsVec) -> DispatchResult { + pub fn submit_tickets(origin: OriginFor, envelopes: TicketsVec) -> DispatchResult { ensure_none(origin)?; - debug!(target: LOG_TARGET, "Received {} tickets", tickets.len()); + debug!(target: LOG_TARGET, "Received {} tickets", envelopes.len()); let epoch_length = T::EpochLength::get(); let current_slot_idx = Self::current_slot_index(); @@ -429,12 +425,12 @@ pub mod pallet { ); let mut candidates = Vec::new(); - for ticket in tickets { - let Some(ticket_id_pre_output) = ticket.signature.pre_outputs.get(0) else { + for envelope in envelopes { + let Some(ticket_id_pre_output) = envelope.signature.pre_outputs.get(0) else { debug!(target: LOG_TARGET, "Missing ticket VRF pre-output from ring signature"); return Err(Error::::TicketInvalid.into()) }; - let ticket_id_input = vrf::ticket_id_input(&randomness, ticket.body.attempt_idx); + let ticket_id_input = vrf::ticket_id_input(&randomness, envelope.attempt); // Check threshold constraint let ticket_id = vrf::make_ticket_id(&ticket_id_input, &ticket_id_pre_output); @@ -446,16 +442,20 @@ pub mod pallet { } // 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) { + let sign_data = vrf::ticket_id_sign_data(ticket_id_input, &envelope.extra); + if !envelope.signature.ring_vrf_verify(&sign_data, &verifier) { debug!(target: LOG_TARGET, "Proof verification failure for ticket ({:?})", ticket_id); return Err(Error::::TicketBadProof.into()) } - candidates.push((ticket_id, ticket.body)); + candidates.push(TicketBody { + id: ticket_id, + attempt: envelope.attempt, + extra: envelope.extra, + }); } - Self::deposit_tickets(&candidates)?; + Self::deposit_tickets(candidates)?; Ok(()) } @@ -468,13 +468,13 @@ pub mod pallet { const INHERENT_IDENTIFIER: InherentIdentifier = INHERENT_IDENTIFIER; fn create_inherent(data: &InherentData) -> Option { - let tickets = data + let envelopes = data .get_data::(&INHERENT_IDENTIFIER) .expect("Sassafras inherent data not correctly encoded") .expect("Sassafras inherent data must be provided"); - let tickets = BoundedVec::truncate_from(tickets); - Some(Call::submit_tickets { tickets }) + let envelopes = BoundedVec::truncate_from(envelopes); + Some(Call::submit_tickets { envelopes }) } fn is_inherent(call: &Self::Call) -> bool { @@ -569,15 +569,15 @@ impl Pallet { TicketsCount::::set(tickets_count); } - pub(crate) fn deposit_tickets(tickets: &[(TicketId, TicketBody)]) -> Result<(), Error> { + pub(crate) fn deposit_tickets(tickets: Vec) -> Result<(), Error> { let prev_count = TicketsAccumulator::::count(); let mut prev_id = None; - for (id, body) in tickets.iter() { - if prev_id.map(|prev| id <= prev).unwrap_or_default() { + for ticket in &tickets { + if prev_id.map(|prev| ticket.id <= prev).unwrap_or_default() { return Err(Error::TicketBadOrder) } - TicketsAccumulator::::insert(TicketKey::from(*id), body); - prev_id = Some(id); + prev_id = Some(ticket.id); + TicketsAccumulator::::insert(TicketKey::from(ticket.id), ticket); } let count = TicketsAccumulator::::count(); if count != prev_count + tickets.len() as u32 { @@ -585,10 +585,11 @@ impl Pallet { } let diff = count.saturating_sub(T::EpochLength::get()); if diff > 0 { - let keys: Vec<_> = TicketsAccumulator::::iter_keys().take(diff as usize).collect(); - for key in keys { - let ticket_id = TicketId::from(key); - if tickets.binary_search_by_key(&&ticket_id, |(id, _)| id).is_ok() { + let dropped_entries: Vec<_> = + TicketsAccumulator::::iter().take(diff as usize).collect(); + // Assess that no new ticket has been dropped + for (key, ticket) in dropped_entries { + if tickets.binary_search_by_key(&ticket.id, |t| t.id).is_ok() { return Err(Error::TicketInvalid) } TicketsAccumulator::::remove(key); @@ -601,9 +602,9 @@ impl Pallet { let mut tickets_count = TicketsCount::::get(); let mut accumulator_count = TicketsAccumulator::::count(); let mut idx = accumulator_count; - for (key, body) in TicketsAccumulator::::drain().take(max_items) { + for (_, ticket) in TicketsAccumulator::::drain().take(max_items) { idx -= 1; - Tickets::::insert((epoch_tag, idx), (TicketId::from(key), body)); + Tickets::::insert((epoch_tag, idx), ticket); } tickets_count[epoch_tag as usize] += (accumulator_count - idx); TicketsCount::::set(tickets_count); @@ -706,7 +707,7 @@ impl Pallet { /// relative index i > k) or if the slot falls beyond the next epoch. /// /// Before importing the first block this returns `None`. - pub fn slot_ticket(slot: Slot) -> Option<(TicketId, TicketBody)> { + pub fn slot_ticket(slot: Slot) -> Option { if frame_system::Pallet::::block_number().is_zero() { return None } diff --git a/substrate/frame/sassafras/src/mock.rs b/substrate/frame/sassafras/src/mock.rs index 78f75b256c87..820665920f2f 100644 --- a/substrate/frame/sassafras/src/mock.rs +++ b/substrate/frame/sassafras/src/mock.rs @@ -21,7 +21,7 @@ use crate::{self as pallet_sassafras, EpochChangeInternalTrigger, *}; use frame_support::{ derive_impl, - traits::{ConstU32, OnFinalize, OnInitialize}, + traits::{ConstU32, ConstU8, OnFinalize, OnInitialize}, }; use sp_consensus_sassafras::{ digests::SlotClaim, @@ -58,8 +58,8 @@ where impl pallet_sassafras::Config for Test { type EpochLength = ConstU32; type MaxAuthorities = ConstU32; - type RedundancyFactor = ConstU32<32>; - type AttemptsNumber = ConstU32<2>; + type RedundancyFactor = ConstU8<32>; + type AttemptsNumber = ConstU8<2>; type EpochChangeTrigger = EpochChangeInternalTrigger; type WeightInfo = (); } @@ -133,10 +133,10 @@ pub fn make_digest(authority_idx: AuthorityIndex, slot: Slot, pair: &AuthorityPa } /// Make a ticket which is claimable during the next epoch. -pub fn make_ticket_body(attempt_idx: u32, pair: &AuthorityPair) -> (TicketId, TicketBody) { +pub fn make_ticket_body(attempt: u8, pair: &AuthorityPair) -> TicketBody { let randomness = Sassafras::next_randomness(); - let ticket_id_input = vrf::ticket_id_input(&randomness, attempt_idx); + let ticket_id_input = vrf::ticket_id_input(&randomness, attempt); let ticket_id_pre_output = pair.as_inner_ref().vrf_pre_output(&ticket_id_input); let id = vrf::make_ticket_id(&ticket_id_input, &ticket_id_pre_output); @@ -145,25 +145,19 @@ pub fn make_ticket_body(attempt_idx: u32, pair: &AuthorityPair) -> (TicketId, Ti let mut extra = [pair.public().as_slice(), &id.0[..]].concat(); let extra = BoundedVec::truncate_from(extra); - let body = TicketBody { attempt_idx, extra }; - - (id, body) + TicketBody { id, attempt, extra } } -pub fn make_dummy_ticket_body(attempt_idx: u32) -> (TicketId, TicketBody) { - let hash = sp_crypto_hashing::blake2_256(&attempt_idx.to_le_bytes()); +pub fn make_dummy_ticket_body(attempt: u8) -> TicketBody { + let hash = sp_crypto_hashing::blake2_256(&[attempt]); let id = TicketId(hash); let hash = sp_crypto_hashing::blake2_256(&hash); let extra = BoundedVec::truncate_from(hash.to_vec()); - let body = TicketBody { attempt_idx, extra }; - (id, body) + TicketBody { id, attempt, extra } } -pub fn make_ticket_bodies( - number: u32, - pair: Option<&AuthorityPair>, -) -> Vec<(TicketId, TicketBody)> { - (0..number) +pub fn make_ticket_bodies(attempts: u8, pair: Option<&AuthorityPair>) -> Vec { + (0..attempts) .into_iter() .map(|i| match pair { Some(pair) => make_ticket_body(i, pair), @@ -218,7 +212,7 @@ pub fn progress_to_block(number: u64, pair: &AuthorityPair) -> Option { } fn make_ticket_with_prover( - attempt: u32, + attempt: u8, pair: &AuthorityPair, prover: &RingProver, ) -> (TicketId, TicketEnvelope) { @@ -228,16 +222,12 @@ fn make_ticket_with_prover( let randomness = Sassafras::next_randomness(); let ticket_id_input = vrf::ticket_id_input(&randomness, attempt); - - let body = TicketBody { attempt_idx: attempt, extra: Default::default() }; - let sign_data = vrf::ticket_body_sign_data(&body, ticket_id_input.clone()); - + let sign_data = vrf::ticket_id_sign_data(ticket_id_input.clone(), &[]); let signature = pair.as_ref().ring_vrf_sign(&sign_data, &prover); - let pre_output = &signature.pre_outputs[0]; let ticket_id = vrf::make_ticket_id(&ticket_id_input, pre_output); - let envelope = TicketEnvelope { body, signature }; + let envelope = TicketEnvelope { attempt, extra: Default::default(), signature }; (ticket_id, envelope) } @@ -269,7 +259,7 @@ pub fn make_prover(pair: &AuthorityPair) -> RingProver { /// Construct `attempts` tickets envelopes for the next epoch. /// /// E.g. by passing an optional threshold -pub fn make_tickets(attempts: u32, pair: &AuthorityPair) -> Vec<(TicketId, TicketEnvelope)> { +pub fn make_tickets(attempts: u8, pair: &AuthorityPair) -> Vec<(TicketId, TicketEnvelope)> { let prover = make_prover(pair); (0..attempts) .into_iter() diff --git a/substrate/frame/sassafras/src/tests.rs b/substrate/frame/sassafras/src/tests.rs index 73c02a7dd106..f6966fae1efe 100644 --- a/substrate/frame/sassafras/src/tests.rs +++ b/substrate/frame/sassafras/src/tests.rs @@ -57,14 +57,18 @@ fn shuffle(vector: &mut Vec, random_seed: u64) { } } -fn dummy_tickets(count: usize) -> Vec<(TicketId, TicketBody)> { - (0..count) - .map(|i| { - let mut id = [0xff; 32]; - id[..8].copy_from_slice(&i.to_be_bytes()[..]); - (TicketId(id), TicketBody { attempt_idx: i as u32, extra: Default::default() }) - }) - .collect() +// fn dummy_tickets(count: usize) -> Vec { +// (0..count) +// .map(|i| { +// let mut id = [0xff; 32]; +// id[..8].copy_from_slice(&i.to_be_bytes()[..]); +// (TicketId(id), TicketBody { attempt_idx: i as u32, extra: Default::default() }) +// }) +// .collect() +// } + +fn dummy_tickets(count: u8) -> Vec { + make_ticket_bodies(count, None) } #[test] @@ -78,12 +82,10 @@ fn assumptions_check() { // Check that entries are stored sorted (bigger first) tickets .iter() - .for_each(|t| TicketsAccumulator::::insert(TicketKey::from(t.0), &t.1)); + .for_each(|t| TicketsAccumulator::::insert(TicketKey::from(t.id), t)); assert_eq!(TicketsAccumulator::::count(), 100); - tickets.sort_unstable_by(|a, b| b.0.cmp(&a.0)); - let accumulator: Vec<_> = TicketsAccumulator::::iter() - .map(|(k, b)| (TicketId::from(k), b)) - .collect(); + tickets.sort_unstable_by_key(|t| TicketKey::from(t.id)); + let accumulator: Vec<_> = TicketsAccumulator::::iter_values().collect(); assert_eq!(tickets, accumulator); // Check accumulator clear @@ -100,67 +102,53 @@ fn deposit_tickets_works() { new_test_ext(1).execute_with(|| { // Try to append an unsorted chunk let mut candidates = tickets[..5].to_vec(); - let err = Sassafras::deposit_tickets(&candidates).unwrap_err(); + let err = Sassafras::deposit_tickets(candidates).unwrap_err(); assert!(matches!(err, Error::TicketBadOrder)); let _ = TicketsAccumulator::::clear(u32::MAX, None); // Correctly append the first sorted chunk let mut candidates = tickets[..5].to_vec(); - candidates.sort_unstable_by_key(|a| a.0); - Sassafras::deposit_tickets(&candidates).unwrap(); + candidates.sort_unstable(); + Sassafras::deposit_tickets(candidates).unwrap(); assert_eq!(TicketsAccumulator::::count(), 5); // Note: internally the tickets are stored in reverse order (bigger first) - let stored: Vec<_> = TicketsAccumulator::::iter() - .map(|(k, b)| (TicketId::from(k), b)) - .collect(); + let stored: Vec<_> = TicketsAccumulator::::iter_values().collect(); let mut expected = tickets[..5].to_vec(); - expected.sort_unstable_by(|a, b| b.0.cmp(&a.0)); + expected.sort_unstable_by_key(|t| TicketKey::from(t.id)); assert_eq!(expected, stored); - TicketsAccumulator::::iter().for_each(|(key, body)| { - println!("{:?}, {:?}", TicketId::from(key), body); - }); // Try to append a chunk with a ticket already pushed let mut candidates = tickets[4..10].to_vec(); - candidates.sort_unstable_by_key(|a| a.0); - let err = Sassafras::deposit_tickets(&candidates).unwrap_err(); + candidates.sort_unstable(); + let err = Sassafras::deposit_tickets(candidates).unwrap_err(); assert!(matches!(err, Error::TicketDuplicate)); // Restore last correct state let _ = TicketsAccumulator::::clear(u32::MAX, None); let mut candidates = tickets[..5].to_vec(); - candidates.sort_unstable_by_key(|a| a.0); - Sassafras::deposit_tickets(&candidates).unwrap(); - - println!("-------------"); + candidates.sort_unstable(); + Sassafras::deposit_tickets(candidates).unwrap(); // Correctly push the second sorted chunk let mut candidates = tickets[5..10].to_vec(); - candidates.sort_unstable_by_key(|a| a.0); - Sassafras::deposit_tickets(&candidates).unwrap(); + candidates.sort_unstable(); + Sassafras::deposit_tickets(candidates).unwrap(); assert_eq!(TicketsAccumulator::::count(), 10); // Note: internally the tickets are stored in reverse order (bigger first) - let mut stored: Vec<_> = TicketsAccumulator::::iter() - .map(|(k, b)| (TicketId::from(k), b)) - .collect(); + let mut stored: Vec<_> = TicketsAccumulator::::iter_values().collect(); let mut expected = tickets[..10].to_vec(); - expected.sort_unstable_by(|a, b| b.0.cmp(&a.0)); + expected.sort_unstable_by_key(|t| TicketKey::from(t.id)); assert_eq!(expected, stored); - TicketsAccumulator::::iter().for_each(|(key, body)| { - println!("{:?}, {:?}", TicketId::from(key), body); - }); - - println!("-------------"); // Now the buffer is full, pick only the tickets that will eventually fit. let mut candidates = tickets[10..].to_vec(); - candidates.sort_unstable_by_key(|a| a.0); + candidates.sort_unstable(); let mut eligible = Vec::new(); for candidate in candidates { if stored.is_empty() { break } let bigger = stored.remove(0); - if bigger.0 <= candidate.0 { + if bigger.id <= candidate.id { break } eligible.push(candidate); @@ -168,17 +156,14 @@ fn deposit_tickets_works() { candidates = eligible; // Correctly push the last candidates chunk - Sassafras::deposit_tickets(&candidates).unwrap(); + Sassafras::deposit_tickets(candidates).unwrap(); + assert_eq!(TicketsAccumulator::::count(), 10); // Note: internally the tickets are stored in reverse order (bigger first) - let mut stored: Vec<_> = TicketsAccumulator::::iter() - .map(|(k, b)| (TicketId::from(k), b)) - .collect(); - tickets.sort_unstable_by(|a, b| b.0.cmp(&a.0)); + let mut stored: Vec<_> = TicketsAccumulator::::iter_values().collect(); + tickets.sort_unstable_by_key(|t| TicketKey::from(t.id)); + assert_eq!(tickets[5..], stored); - TicketsAccumulator::::iter().for_each(|(key, body)| { - println!("{:?}, {:?}", TicketId::from(key), body); - }); }); } @@ -202,7 +187,7 @@ fn post_genesis_randomness_initialization() { prefix_eq!(randomness[2], h2b("3a4c0005")); prefix_eq!(randomness[3], h2b("0dd43c54")); - let (id1, _) = make_ticket_body(0, pair); + let ticket1 = make_ticket_body(0, pair); // Reset what is relevant RandomnessBuf::::set(genesis_randomness); @@ -217,10 +202,10 @@ fn post_genesis_randomness_initialization() { prefix_eq!(randomness[2], h2b("632ac0d9")); prefix_eq!(randomness[3], h2b("575088c3")); - let (id2, _) = make_ticket_body(0, pair); + let ticket2 = make_ticket_body(0, pair); // Ticket ids should be different when next epoch randomness is different - assert_ne!(id1, id2); + assert_ne!(ticket1.id, ticket2.id); }); } @@ -256,7 +241,7 @@ fn on_first_block() { common_assertions(false); let post_fini_randomness = Sassafras::randomness_buf(); - prefix_eq!(post_fini_randomness[0], h2b("3e5bd6d3")); + prefix_eq!(post_fini_randomness[0], h2b("334d1a4c")); prefix_eq!(post_fini_randomness[1], post_init_randomness[1]); prefix_eq!(post_fini_randomness[2], post_init_randomness[2]); prefix_eq!(post_fini_randomness[3], post_init_randomness[3]); @@ -305,7 +290,7 @@ fn on_normal_block() { common_assertions(true); let post_init_randomness = Sassafras::randomness_buf(); - prefix_eq!(post_init_randomness[0], h2b("3e5bd6d3")); + prefix_eq!(post_init_randomness[0], h2b("334d1a4c")); prefix_eq!(post_init_randomness[1], h2b("4e8c71d2")); prefix_eq!(post_init_randomness[2], h2b("3a4c0005")); prefix_eq!(post_init_randomness[3], h2b("0dd43c54")); @@ -316,7 +301,7 @@ fn on_normal_block() { common_assertions(false); let post_fini_randomness = Sassafras::randomness_buf(); - prefix_eq!(post_fini_randomness[0], h2b("ecff3e90")); + prefix_eq!(post_fini_randomness[0], h2b("277138ab")); prefix_eq!(post_fini_randomness[1], post_init_randomness[1]); prefix_eq!(post_fini_randomness[2], post_init_randomness[2]); prefix_eq!(post_fini_randomness[3], post_init_randomness[3]); @@ -384,8 +369,8 @@ fn slot_ticket_id_outside_in_fetch() { let tickets = dummy_tickets(curr_count + next_count); // Current epoch tickets - let curr_tickets = tickets[..curr_count].to_vec(); - let next_tickets = tickets[curr_count..].to_vec(); + let curr_tickets = tickets[..curr_count as usize].to_vec(); + let next_tickets = tickets[curr_count as usize..].to_vec(); new_test_ext(0).execute_with(|| { curr_tickets @@ -498,8 +483,8 @@ fn tickets_accumulator_works() { let e1_count = 6; let e2_count = 10; let tickets = dummy_tickets(e1_count + e2_count); - let e1_tickets = tickets[..e1_count].to_vec(); - let e2_tickets = tickets[e1_count..].to_vec(); + let e1_tickets = tickets[..e1_count as usize].to_vec(); + let e2_tickets = tickets[e1_count as usize..].to_vec(); let (pairs, mut ext) = new_test_ext_with_pairs(1, false); @@ -515,7 +500,7 @@ fn tickets_accumulator_works() { // Append some tickets to the accumulator e1_tickets .iter() - .for_each(|t| TicketsAccumulator::::insert(TicketKey::from(t.0), &t.1)); + .for_each(|t| TicketsAccumulator::::insert(TicketKey::from(t.id), t)); // Progress to epoch's last block let end_block = start_block + epoch_length - 2; @@ -549,7 +534,7 @@ fn tickets_accumulator_works() { // Append some tickets to the accumulator e2_tickets .iter() - .for_each(|t| TicketsAccumulator::::insert(TicketKey::from(t.0), &t.1)); + .for_each(|t| TicketsAccumulator::::insert(TicketKey::from(t.id), t)); // Progress to epoch's last block let end_block = end_block + epoch_length; @@ -588,58 +573,45 @@ fn incremental_accumulator_drain() { new_test_ext(0).execute_with(|| { tickets .iter() - .for_each(|t| TicketsAccumulator::::insert(TicketKey::from(t.0), &t.1)); + .for_each(|t| TicketsAccumulator::::insert(TicketKey::from(t.id), t)); - let accumulator: Vec<_> = TicketsAccumulator::::iter() - .map(|(k, b)| (TicketId::from(k), b)) - .collect(); + let accumulator: Vec<_> = TicketsAccumulator::::iter_values().collect(); // Assess accumulator expected order (bigger id first) - assert!(accumulator.windows(2).all(|chunk| chunk[0].0 > chunk[1].0)); + assert!(accumulator.windows(2).all(|chunk| chunk[0].id > chunk[1].id)); + + let mut onchain_expected = accumulator.clone(); + onchain_expected.sort_unstable(); Sassafras::consume_tickets_accumulator(5, 0); let tickets_count = TicketsCount::::get(); assert_eq!(tickets_count[0], 5); assert_eq!(tickets_count[1], 0); - tickets.iter().enumerate().skip(5).for_each(|(i, (id, _))| { - let (id2, _) = Tickets::::get((0, i as u32)).unwrap(); - assert_eq!(id, &id2); + + accumulator.iter().rev().enumerate().skip(5).for_each(|(i, t)| { + let t2 = Tickets::::get((0, i as u32)).unwrap(); + assert_eq!(t.id, t2.id); }); Sassafras::consume_tickets_accumulator(3, 0); let tickets_count = TicketsCount::::get(); assert_eq!(tickets_count[0], 8); assert_eq!(tickets_count[1], 0); - tickets.iter().enumerate().skip(2).for_each(|(i, (id, _))| { - let (id2, _) = Tickets::::get((0, i as u32)).unwrap(); - assert_eq!(id, &id2); + accumulator.iter().rev().enumerate().skip(2).for_each(|(i, t)| { + let t2 = Tickets::::get((0, i as u32)).unwrap(); + assert_eq!(t.id, t2.id); }); Sassafras::consume_tickets_accumulator(5, 0); let tickets_count = TicketsCount::::get(); assert_eq!(tickets_count[0], 10); assert_eq!(tickets_count[1], 0); - tickets.iter().enumerate().for_each(|(i, (id, _))| { - let (id2, _) = Tickets::::get((0, i as u32)).unwrap(); - assert_eq!(id, &id2); + accumulator.iter().rev().enumerate().for_each(|(i, t)| { + let t2 = Tickets::::get((0, i as u32)).unwrap(); + assert_eq!(t.id, t2.id); }); }); } -fn data_read(filename: &str) -> T { - use std::{fs::File, io::Read}; - let mut file = File::open(filename).unwrap(); - let mut buf = Vec::new(); - file.read_to_end(&mut buf).unwrap(); - T::decode(&mut &buf[..]).unwrap() -} - -fn data_write(filename: &str, data: T) { - use std::{fs::File, io::Write}; - let mut file = File::create(filename).unwrap(); - let buf = data.encode(); - file.write_all(&buf).unwrap(); -} - #[test] fn submit_tickets_with_ring_proof_check_works() { use sp_core::Pair as _; @@ -680,7 +652,7 @@ fn submit_tickets_with_ring_proof_check_works() { // Submit an invalid candidate let mut chunk = chunks[2].clone(); - chunk[0].body.attempt_idx += 1; + chunk[0].attempt += 1; let e = Sassafras::submit_tickets(RuntimeOrigin::none(), chunk).unwrap_err(); assert_eq!(e, DispatchError::from(Error::::TicketBadProof)); assert_eq!(TicketsAccumulator::::count(), 0); @@ -710,15 +682,24 @@ fn submit_tickets_with_ring_proof_check_works() { // Submit the smaller candidates chunks. This is accepted (4 old tickets removed). Sassafras::submit_tickets(RuntimeOrigin::none(), chunks[0].clone()).unwrap(); assert_eq!(TicketsAccumulator::::count(), 10); - - // NOTE (implementation detail): the accumulator internally saves bigger tickets first. - let tickets: Vec<_> = TicketsAccumulator::::iter_values().collect(); - let expected: Vec<_> = - candidates.into_iter().take(10).rev().map(|candidate| candidate.body).collect(); - assert_eq!(tickets, expected) }) } +fn data_read(filename: &str) -> T { + use std::{fs::File, io::Read}; + let mut file = File::open(filename).unwrap(); + let mut buf = Vec::new(); + file.read_to_end(&mut buf).unwrap(); + T::decode(&mut &buf[..]).unwrap() +} + +fn data_write(filename: &str, data: T) { + use std::{fs::File, io::Write}; + let mut file = File::create(filename).unwrap(); + let buf = data.encode(); + file.write_all(&buf).unwrap(); +} + #[test] #[ignore = "test tickets generator"] fn generate_test_tickets() { diff --git a/substrate/primitives/consensus/sassafras/src/lib.rs b/substrate/primitives/consensus/sassafras/src/lib.rs index a7c3ae40c1fa..ce6e283b8257 100644 --- a/substrate/primitives/consensus/sassafras/src/lib.rs +++ b/substrate/primitives/consensus/sassafras/src/lib.rs @@ -107,7 +107,7 @@ pub struct Configuration { /// /// Expected ratio between epoch's slots and the cumulative number of tickets which can /// be submitted by the set of epoch validators. - pub redundancy_factor: u32, + pub redundancy_factor: u8, /// Tickets max attempts for each validator. /// /// Influences the anonymity of block producers. As all published tickets have a public @@ -116,7 +116,7 @@ pub struct Configuration { /// we approach the epoch tail. /// /// This anonymity loss already becomes small when `attempts_number = 64` or `128`. - pub attempts_number: u32, + pub attempts_number: u8, } /// Sassafras epoch information diff --git a/substrate/primitives/consensus/sassafras/src/ticket.rs b/substrate/primitives/consensus/sassafras/src/ticket.rs index 1153675bdf24..baafaa87c977 100644 --- a/substrate/primitives/consensus/sassafras/src/ticket.rs +++ b/substrate/primitives/consensus/sassafras/src/ticket.rs @@ -60,20 +60,36 @@ impl From for U256 { /// Ticket data persisted on-chain. #[derive(Debug, Clone, PartialEq, Eq, Encode, Decode, MaxEncodedLen, TypeInfo)] pub struct TicketBody { + /// Ticket identifier. + pub id: TicketId, /// Attempt index. - pub attempt_idx: u32, - /// User opaque data. + pub attempt: u8, + /// User opaque extra data. pub extra: BoundedVec>, } +impl Ord for TicketBody { + fn cmp(&self, other: &Self) -> core::cmp::Ordering { + self.id.cmp(&other.id) + } +} + +impl PartialOrd for TicketBody { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + /// Ticket ring vrf signature. pub type TicketSignature = RingVrfSignature; /// Ticket envelope used on during submission. #[derive(Debug, Clone, PartialEq, Eq, Encode, Decode, MaxEncodedLen, TypeInfo)] pub struct TicketEnvelope { - /// Ticket body. - pub body: TicketBody, + /// Attempt index. + pub attempt: u8, + /// User opaque extra data. + pub extra: BoundedVec>, /// Ring signature. pub signature: TicketSignature, } @@ -96,12 +112,7 @@ pub struct TicketEnvelope { /// For details about the formula and implications refer to /// [*probabilities an parameters*](https://research.web3.foundation/Polkadot/protocols/block-production/SASSAFRAS#probabilities-and-parameters) /// paragraph of the w3f introduction to the protocol. -pub fn ticket_id_threshold( - redundancy: u32, - slots: u32, - attempts: u32, - validators: u32, -) -> TicketId { +pub fn ticket_id_threshold(redundancy: u8, slots: u32, attempts: u8, validators: u32) -> TicketId { let den = attempts as u64 * validators as u64; let num = redundancy as u64 * slots as u64; U256::MAX diff --git a/substrate/primitives/consensus/sassafras/src/vrf.rs b/substrate/primitives/consensus/sassafras/src/vrf.rs index ee6dea83a201..afc001318080 100644 --- a/substrate/primitives/consensus/sassafras/src/vrf.rs +++ b/substrate/primitives/consensus/sassafras/src/vrf.rs @@ -17,10 +17,7 @@ //! Utilities related to VRF input, pre-output and signatures. -use crate::{Randomness, TicketBody, TicketId}; -#[cfg(not(feature = "std"))] -use alloc::vec::Vec; -use scale_codec::Encode; +use crate::{Randomness, TicketId}; use sp_consensus_slots::Slot; pub use sp_core::bandersnatch::{ @@ -31,26 +28,34 @@ pub use sp_core::bandersnatch::{ /// Ring VRF domain size for Sassafras consensus. pub const RING_VRF_DOMAIN_SIZE: u32 = 2048; +const TICKET_SEAL_CONTEXT: &[u8] = b"sassafras_ticket_seal"; +// const FALLBACK_SEAL_CONTEXT: &[u8] = b"sassafras_fallback_seal"; +const BLOCK_ENTROPY_CONTEXT: &[u8] = b"sassafras_entropy"; + /// Bandersnatch VRF [`RingContext`] specialization for Sassafras using [`RING_VRF_DOMAIN_SIZE`]. pub type RingContext = sp_core::bandersnatch::ring_vrf::RingContext; -fn vrf_input_from_data( - domain: &[u8], - data: impl IntoIterator>, -) -> VrfInput { - let buf = data.into_iter().fold(Vec::new(), |mut buf, item| { - let bytes = item.as_ref(); - buf.extend_from_slice(bytes); - let len = u8::try_from(bytes.len()).expect("private function with well known inputs; qed"); - buf.push(len); - buf - }); - VrfInput::new(domain, buf) +/// VRF input to generate the ticket id. +pub fn ticket_id_input(randomness: &Randomness, attempt: u8) -> VrfInput { + VrfInput::new(b"sassafras", [TICKET_SEAL_CONTEXT, randomness.as_slice(), &[attempt]].concat()) +} + +/// Data to be signed via ring-vrf. +pub fn ticket_id_sign_data(ticket_id_input: VrfInput, extra_data: &[u8]) -> VrfSignData { + VrfSignData::new_unchecked( + b"sassafras-ticket-body-transcript", + Some(extra_data), + Some(ticket_id_input), + ) } /// VRF input to produce randomness. pub fn block_randomness_input(randomness: &Randomness, slot: Slot) -> VrfInput { - vrf_input_from_data(b"sassafras-randomness", [randomness.as_slice(), &slot.to_le_bytes()]) + // TODO: @davxy: implement as JAM + VrfInput::new( + b"sassafras", + [BLOCK_ENTROPY_CONTEXT, randomness.as_slice(), &slot.to_le_bytes()].concat(), + ) } /// Signing-data to claim slot ownership during block production. @@ -63,20 +68,6 @@ pub fn block_randomness_sign_data(randomness: &Randomness, slot: Slot) -> VrfSig ) } -/// VRF input to generate the ticket id. -pub fn ticket_id_input(randomness: &Randomness, attempt: u32) -> VrfInput { - vrf_input_from_data(b"sassafras-ticket-v1.0", [randomness.as_slice(), &attempt.to_le_bytes()]) -} - -/// Data to be signed via ring-vrf. -pub fn ticket_body_sign_data(ticket_body: &TicketBody, ticket_id_input: VrfInput) -> VrfSignData { - VrfSignData::new_unchecked( - b"sassafras-ticket-body-transcript", - Some(ticket_body.encode().as_slice()), - Some(ticket_id_input), - ) -} - /// Make ticket-id from the given VRF input and pre-output. /// /// Input should have been obtained via [`ticket_id_input`]. From 683b7e5e9f4fe6449c970d0b81f0041ae38d78de Mon Sep 17 00:00:00 2001 From: Davide Galassi Date: Mon, 10 Jun 2024 16:58:31 +0200 Subject: [PATCH 30/42] Nit --- substrate/frame/sassafras/src/lib.rs | 4 ++-- substrate/primitives/consensus/sassafras/src/ticket.rs | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/substrate/frame/sassafras/src/lib.rs b/substrate/frame/sassafras/src/lib.rs index 6306b6aedbab..d8ca2f3a2a1f 100644 --- a/substrate/frame/sassafras/src/lib.rs +++ b/substrate/frame/sassafras/src/lib.rs @@ -418,10 +418,10 @@ pub mod pallet { // Compute tickets threshold let ticket_threshold = sp_consensus_sassafras::ticket_id_threshold( - T::RedundancyFactor::get(), epoch_length as u32, - T::AttemptsNumber::get(), authorities.len() as u32, + T::AttemptsNumber::get(), + T::RedundancyFactor::get(), ); let mut candidates = Vec::new(); diff --git a/substrate/primitives/consensus/sassafras/src/ticket.rs b/substrate/primitives/consensus/sassafras/src/ticket.rs index baafaa87c977..9cc2f2f1101f 100644 --- a/substrate/primitives/consensus/sassafras/src/ticket.rs +++ b/substrate/primitives/consensus/sassafras/src/ticket.rs @@ -112,7 +112,7 @@ pub struct TicketEnvelope { /// For details about the formula and implications refer to /// [*probabilities an parameters*](https://research.web3.foundation/Polkadot/protocols/block-production/SASSAFRAS#probabilities-and-parameters) /// paragraph of the w3f introduction to the protocol. -pub fn ticket_id_threshold(redundancy: u8, slots: u32, attempts: u8, validators: u32) -> TicketId { +pub fn ticket_id_threshold(slots: u32, validators: u32, attempts: u8, redundancy: u8) -> TicketId { let den = attempts as u64 * validators as u64; let num = redundancy as u64 * slots as u64; U256::MAX @@ -150,7 +150,7 @@ mod tests { let attempts = 100; let validators = 500; - let threshold = ticket_id_threshold(redundancy, slots, attempts, validators); + let threshold = ticket_id_threshold(slots, validators, attempts, redundancy); println!("{:?}", threshold); let threshold = normalize_u256(threshold.0); println!("{}", threshold); From fe2f284746107726e0e3041c599b058a418b093e Mon Sep 17 00:00:00 2001 From: Davide Galassi Date: Mon, 10 Jun 2024 17:49:35 +0200 Subject: [PATCH 31/42] Update benchmarking code --- substrate/frame/sassafras/src/benchmarking.rs | 2 +- substrate/frame/sassafras/src/lib.rs | 2 +- substrate/frame/sassafras/src/weights.rs | 106 +++++++++--------- 3 files changed, 55 insertions(+), 55 deletions(-) diff --git a/substrate/frame/sassafras/src/benchmarking.rs b/substrate/frame/sassafras/src/benchmarking.rs index 040e74d76ac9..9cf0d6278810 100644 --- a/substrate/frame/sassafras/src/benchmarking.rs +++ b/substrate/frame/sassafras/src/benchmarking.rs @@ -102,7 +102,7 @@ mod benchmarks { (0..accumulated_tickets).for_each(|i| { let mut id = TicketId([0xff; 32]); id.0[..4].copy_from_slice(&i.to_be_bytes()[..]); - let body = TicketBody { attempt_idx: 0, extra: Default::default() }; + let body = TicketBody { id, attempt: 0, extra: Default::default() }; TicketsAccumulator::::insert(TicketKey::from(id), &body); }); diff --git a/substrate/frame/sassafras/src/lib.rs b/substrate/frame/sassafras/src/lib.rs index d8ca2f3a2a1f..9ae1c16ea3da 100644 --- a/substrate/frame/sassafras/src/lib.rs +++ b/substrate/frame/sassafras/src/lib.rs @@ -43,7 +43,7 @@ //! validators for the target epoch". #![allow(unused)] -// #![deny(warnings)] +#![deny(warnings)] #![warn(unused_must_use, unsafe_code, unused_variables, unused_imports, missing_docs)] #![cfg_attr(not(feature = "std"), no_std)] diff --git a/substrate/frame/sassafras/src/weights.rs b/substrate/frame/sassafras/src/weights.rs index 04755c513c26..281482016a54 100644 --- a/substrate/frame/sassafras/src/weights.rs +++ b/substrate/frame/sassafras/src/weights.rs @@ -18,7 +18,7 @@ //! Autogenerated weights for `pallet_sassafras` //! //! THIS FILE WAS AUTO-GENERATED USING THE SUBSTRATE BENCHMARK CLI VERSION 32.0.0 -//! DATE: 2024-05-10, STEPS: `20`, REPEAT: `3`, LOW RANGE: `[]`, HIGH RANGE: `[]` +//! DATE: 2024-06-10, STEPS: `20`, REPEAT: `3`, LOW RANGE: `[]`, HIGH RANGE: `[]` //! WORST CASE MAP SIZE: `1000000` //! HOSTNAME: `behemoth`, CPU: `AMD Ryzen Threadripper 3970X 32-Core Processor` //! WASM-EXECUTION: `Compiled`, CHAIN: `Some("dev")`, DB CACHE: `1024` @@ -76,8 +76,8 @@ impl WeightInfo for SubstrateWeight { // Proof Size summary in bytes: // Measured: `270` // Estimated: `1755` - // Minimum execution time: 315_749_000 picoseconds. - Weight::from_parts(317_523_000, 1755) + // Minimum execution time: 437_498_000 picoseconds. + Weight::from_parts(448_310_000, 1755) .saturating_add(T::DbWeight::get().reads(4_u64)) .saturating_add(T::DbWeight::get().writes(3_u64)) } @@ -98,9 +98,9 @@ impl WeightInfo for SubstrateWeight { /// Storage: `Sassafras::CounterForTicketsAccumulator` (r:1 w:1) /// Proof: `Sassafras::CounterForTicketsAccumulator` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`) /// Storage: `Sassafras::TicketsAccumulator` (r:1001 w:1000) - /// Proof: `Sassafras::TicketsAccumulator` (`max_values`: None, `max_size`: Some(166), added: 2641, mode: `MaxEncodedLen`) + /// Proof: `Sassafras::TicketsAccumulator` (`max_values`: None, `max_size`: Some(195), added: 2670, mode: `MaxEncodedLen`) /// Storage: `Sassafras::Tickets` (r:0 w:1000) - /// Proof: `Sassafras::Tickets` (`max_values`: None, `max_size`: Some(171), added: 2646, mode: `MaxEncodedLen`) + /// Proof: `Sassafras::Tickets` (`max_values`: None, `max_size`: Some(168), added: 2643, mode: `MaxEncodedLen`) /// Storage: `Sassafras::Authorities` (r:0 w:1) /// Proof: `Sassafras::Authorities` (`max_values`: Some(1), `max_size`: Some(3302), added: 3797, mode: `MaxEncodedLen`) /// Storage: `Sassafras::RingVerifierKey` (r:0 w:1) @@ -109,20 +109,20 @@ impl WeightInfo for SubstrateWeight { /// The range of component `y` is `[100, 1000]`. fn enact_epoch_change(x: u32, y: u32, ) -> Weight { // Proof Size summary in bytes: - // Measured: `590613 + x * (33 ±0) + y * (37 ±0)` - // Estimated: `592099 + x * (33 ±0) + y * (2641 ±0)` - // Minimum execution time: 138_319_222_000 picoseconds. - Weight::from_parts(134_117_334_052, 592099) - // Standard Error: 5_392_475 - .saturating_add(Weight::from_parts(170_217_303, 0).saturating_mul(x.into())) - // Standard Error: 595_543 - .saturating_add(Weight::from_parts(5_308_040, 0).saturating_mul(y.into())) + // Measured: `590613 + x * (33 ±0) + y * (68 ±0)` + // Estimated: `592099 + x * (33 ±0) + y * (2670 ±0)` + // Minimum execution time: 161_753_432_000 picoseconds. + Weight::from_parts(154_427_856_060, 592099) + // Standard Error: 3_252_626 + .saturating_add(Weight::from_parts(178_408_790, 0).saturating_mul(x.into())) + // Standard Error: 359_218 + .saturating_add(Weight::from_parts(8_152_285, 0).saturating_mul(y.into())) .saturating_add(T::DbWeight::get().reads(9_u64)) .saturating_add(T::DbWeight::get().reads((1_u64).saturating_mul(y.into()))) .saturating_add(T::DbWeight::get().writes(7_u64)) .saturating_add(T::DbWeight::get().writes((2_u64).saturating_mul(y.into()))) .saturating_add(Weight::from_parts(0, 33).saturating_mul(x.into())) - .saturating_add(Weight::from_parts(0, 2641).saturating_mul(y.into())) + .saturating_add(Weight::from_parts(0, 2670).saturating_mul(y.into())) } /// Storage: `Sassafras::CurrentSlot` (r:1 w:0) /// Proof: `Sassafras::CurrentSlot` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `MaxEncodedLen`) @@ -135,21 +135,21 @@ impl WeightInfo for SubstrateWeight { /// Storage: `Sassafras::CounterForTicketsAccumulator` (r:1 w:1) /// Proof: `Sassafras::CounterForTicketsAccumulator` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`) /// Storage: `Sassafras::TicketsAccumulator` (r:16 w:16) - /// Proof: `Sassafras::TicketsAccumulator` (`max_values`: None, `max_size`: Some(166), added: 2641, mode: `MaxEncodedLen`) + /// Proof: `Sassafras::TicketsAccumulator` (`max_values`: None, `max_size`: Some(195), added: 2670, mode: `MaxEncodedLen`) /// The range of component `x` is `[1, 16]`. fn submit_tickets(x: u32, ) -> Weight { // Proof Size summary in bytes: // Measured: `1029` - // Estimated: `4787 + x * (2641 ±0)` - // Minimum execution time: 51_909_043_000 picoseconds. - Weight::from_parts(37_526_932_333, 4787) - // Standard Error: 13_320_201 - .saturating_add(Weight::from_parts(14_742_687_073, 0).saturating_mul(x.into())) + // Estimated: `4787 + x * (2670 ±0)` + // Minimum execution time: 60_282_323_000 picoseconds. + Weight::from_parts(43_538_201_161, 4787) + // Standard Error: 9_492_599 + .saturating_add(Weight::from_parts(16_509_026_590, 0).saturating_mul(x.into())) .saturating_add(T::DbWeight::get().reads(5_u64)) .saturating_add(T::DbWeight::get().reads((1_u64).saturating_mul(x.into()))) .saturating_add(T::DbWeight::get().writes(1_u64)) .saturating_add(T::DbWeight::get().writes((1_u64).saturating_mul(x.into()))) - .saturating_add(Weight::from_parts(0, 2641).saturating_mul(x.into())) + .saturating_add(Weight::from_parts(0, 2670).saturating_mul(x.into())) } /// Storage: `Sassafras::RingContext` (r:1 w:0) /// Proof: `Sassafras::RingContext` (`max_values`: Some(1), `max_size`: Some(590324), added: 590819, mode: `MaxEncodedLen`) @@ -160,10 +160,10 @@ impl WeightInfo for SubstrateWeight { // Proof Size summary in bytes: // Measured: `590458` // Estimated: `591809` - // Minimum execution time: 133_728_565_000 picoseconds. - Weight::from_parts(133_908_713_941, 591809) - // Standard Error: 2_906_128 - .saturating_add(Weight::from_parts(152_929_171, 0).saturating_mul(x.into())) + // Minimum execution time: 153_738_634_000 picoseconds. + Weight::from_parts(153_793_626_658, 591809) + // Standard Error: 4_264_780 + .saturating_add(Weight::from_parts(182_398_648, 0).saturating_mul(x.into())) .saturating_add(T::DbWeight::get().reads(1_u64)) .saturating_add(T::DbWeight::get().writes(1_u64)) } @@ -173,8 +173,8 @@ impl WeightInfo for SubstrateWeight { // Proof Size summary in bytes: // Measured: `590458` // Estimated: `591809` - // Minimum execution time: 54_789_232_000 picoseconds. - Weight::from_parts(54_820_282_000, 591809) + // Minimum execution time: 63_857_510_000 picoseconds. + Weight::from_parts(64_221_658_000, 591809) .saturating_add(T::DbWeight::get().reads(1_u64)) } } @@ -195,8 +195,8 @@ impl WeightInfo for () { // Proof Size summary in bytes: // Measured: `270` // Estimated: `1755` - // Minimum execution time: 315_749_000 picoseconds. - Weight::from_parts(317_523_000, 1755) + // Minimum execution time: 437_498_000 picoseconds. + Weight::from_parts(448_310_000, 1755) .saturating_add(RocksDbWeight::get().reads(4_u64)) .saturating_add(RocksDbWeight::get().writes(3_u64)) } @@ -217,9 +217,9 @@ impl WeightInfo for () { /// Storage: `Sassafras::CounterForTicketsAccumulator` (r:1 w:1) /// Proof: `Sassafras::CounterForTicketsAccumulator` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`) /// Storage: `Sassafras::TicketsAccumulator` (r:1001 w:1000) - /// Proof: `Sassafras::TicketsAccumulator` (`max_values`: None, `max_size`: Some(166), added: 2641, mode: `MaxEncodedLen`) + /// Proof: `Sassafras::TicketsAccumulator` (`max_values`: None, `max_size`: Some(195), added: 2670, mode: `MaxEncodedLen`) /// Storage: `Sassafras::Tickets` (r:0 w:1000) - /// Proof: `Sassafras::Tickets` (`max_values`: None, `max_size`: Some(171), added: 2646, mode: `MaxEncodedLen`) + /// Proof: `Sassafras::Tickets` (`max_values`: None, `max_size`: Some(168), added: 2643, mode: `MaxEncodedLen`) /// Storage: `Sassafras::Authorities` (r:0 w:1) /// Proof: `Sassafras::Authorities` (`max_values`: Some(1), `max_size`: Some(3302), added: 3797, mode: `MaxEncodedLen`) /// Storage: `Sassafras::RingVerifierKey` (r:0 w:1) @@ -228,20 +228,20 @@ impl WeightInfo for () { /// The range of component `y` is `[100, 1000]`. fn enact_epoch_change(x: u32, y: u32, ) -> Weight { // Proof Size summary in bytes: - // Measured: `590613 + x * (33 ±0) + y * (37 ±0)` - // Estimated: `592099 + x * (33 ±0) + y * (2641 ±0)` - // Minimum execution time: 138_319_222_000 picoseconds. - Weight::from_parts(134_117_334_052, 592099) - // Standard Error: 5_392_475 - .saturating_add(Weight::from_parts(170_217_303, 0).saturating_mul(x.into())) - // Standard Error: 595_543 - .saturating_add(Weight::from_parts(5_308_040, 0).saturating_mul(y.into())) + // Measured: `590613 + x * (33 ±0) + y * (68 ±0)` + // Estimated: `592099 + x * (33 ±0) + y * (2670 ±0)` + // Minimum execution time: 161_753_432_000 picoseconds. + Weight::from_parts(154_427_856_060, 592099) + // Standard Error: 3_252_626 + .saturating_add(Weight::from_parts(178_408_790, 0).saturating_mul(x.into())) + // Standard Error: 359_218 + .saturating_add(Weight::from_parts(8_152_285, 0).saturating_mul(y.into())) .saturating_add(RocksDbWeight::get().reads(9_u64)) .saturating_add(RocksDbWeight::get().reads((1_u64).saturating_mul(y.into()))) .saturating_add(RocksDbWeight::get().writes(7_u64)) .saturating_add(RocksDbWeight::get().writes((2_u64).saturating_mul(y.into()))) .saturating_add(Weight::from_parts(0, 33).saturating_mul(x.into())) - .saturating_add(Weight::from_parts(0, 2641).saturating_mul(y.into())) + .saturating_add(Weight::from_parts(0, 2670).saturating_mul(y.into())) } /// Storage: `Sassafras::CurrentSlot` (r:1 w:0) /// Proof: `Sassafras::CurrentSlot` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `MaxEncodedLen`) @@ -254,21 +254,21 @@ impl WeightInfo for () { /// Storage: `Sassafras::CounterForTicketsAccumulator` (r:1 w:1) /// Proof: `Sassafras::CounterForTicketsAccumulator` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`) /// Storage: `Sassafras::TicketsAccumulator` (r:16 w:16) - /// Proof: `Sassafras::TicketsAccumulator` (`max_values`: None, `max_size`: Some(166), added: 2641, mode: `MaxEncodedLen`) + /// Proof: `Sassafras::TicketsAccumulator` (`max_values`: None, `max_size`: Some(195), added: 2670, mode: `MaxEncodedLen`) /// The range of component `x` is `[1, 16]`. fn submit_tickets(x: u32, ) -> Weight { // Proof Size summary in bytes: // Measured: `1029` - // Estimated: `4787 + x * (2641 ±0)` - // Minimum execution time: 51_909_043_000 picoseconds. - Weight::from_parts(37_526_932_333, 4787) - // Standard Error: 13_320_201 - .saturating_add(Weight::from_parts(14_742_687_073, 0).saturating_mul(x.into())) + // Estimated: `4787 + x * (2670 ±0)` + // Minimum execution time: 60_282_323_000 picoseconds. + Weight::from_parts(43_538_201_161, 4787) + // Standard Error: 9_492_599 + .saturating_add(Weight::from_parts(16_509_026_590, 0).saturating_mul(x.into())) .saturating_add(RocksDbWeight::get().reads(5_u64)) .saturating_add(RocksDbWeight::get().reads((1_u64).saturating_mul(x.into()))) .saturating_add(RocksDbWeight::get().writes(1_u64)) .saturating_add(RocksDbWeight::get().writes((1_u64).saturating_mul(x.into()))) - .saturating_add(Weight::from_parts(0, 2641).saturating_mul(x.into())) + .saturating_add(Weight::from_parts(0, 2670).saturating_mul(x.into())) } /// Storage: `Sassafras::RingContext` (r:1 w:0) /// Proof: `Sassafras::RingContext` (`max_values`: Some(1), `max_size`: Some(590324), added: 590819, mode: `MaxEncodedLen`) @@ -279,10 +279,10 @@ impl WeightInfo for () { // Proof Size summary in bytes: // Measured: `590458` // Estimated: `591809` - // Minimum execution time: 133_728_565_000 picoseconds. - Weight::from_parts(133_908_713_941, 591809) - // Standard Error: 2_906_128 - .saturating_add(Weight::from_parts(152_929_171, 0).saturating_mul(x.into())) + // Minimum execution time: 153_738_634_000 picoseconds. + Weight::from_parts(153_793_626_658, 591809) + // Standard Error: 4_264_780 + .saturating_add(Weight::from_parts(182_398_648, 0).saturating_mul(x.into())) .saturating_add(RocksDbWeight::get().reads(1_u64)) .saturating_add(RocksDbWeight::get().writes(1_u64)) } @@ -292,8 +292,8 @@ impl WeightInfo for () { // Proof Size summary in bytes: // Measured: `590458` // Estimated: `591809` - // Minimum execution time: 54_789_232_000 picoseconds. - Weight::from_parts(54_820_282_000, 591809) + // Minimum execution time: 63_857_510_000 picoseconds. + Weight::from_parts(64_221_658_000, 591809) .saturating_add(RocksDbWeight::get().reads(1_u64)) } } From fda29a6380755e7cadee5b36235f0f0afcbe8e19 Mon Sep 17 00:00:00 2001 From: Davide Galassi Date: Mon, 10 Jun 2024 18:04:32 +0200 Subject: [PATCH 32/42] Typo --- substrate/primitives/consensus/sassafras/src/ticket.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/substrate/primitives/consensus/sassafras/src/ticket.rs b/substrate/primitives/consensus/sassafras/src/ticket.rs index 944307c0d3fe..3524e75329ea 100644 --- a/substrate/primitives/consensus/sassafras/src/ticket.rs +++ b/substrate/primitives/consensus/sassafras/src/ticket.rs @@ -83,7 +83,7 @@ impl PartialOrd for TicketBody { /// Ticket ring vrf signature. pub type TicketSignature = RingVrfSignature; -/// Ticket envelope used on during submission. +/// Ticket envelope used during submission. #[derive(Debug, Clone, PartialEq, Eq, Encode, Decode, MaxEncodedLen, TypeInfo)] pub struct TicketEnvelope { /// Attempt index. From 7a61b1c196393d82fda67ee0431da01f322de093 Mon Sep 17 00:00:00 2001 From: Davide Galassi Date: Mon, 10 Jun 2024 18:20:40 +0200 Subject: [PATCH 33/42] Clippy --- substrate/primitives/consensus/sassafras/src/ticket.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/substrate/primitives/consensus/sassafras/src/ticket.rs b/substrate/primitives/consensus/sassafras/src/ticket.rs index 3524e75329ea..a8f8f0c8a8a4 100644 --- a/substrate/primitives/consensus/sassafras/src/ticket.rs +++ b/substrate/primitives/consensus/sassafras/src/ticket.rs @@ -156,8 +156,8 @@ mod tests { println!("{}", threshold); // We expect that the total number of tickets allowed to be submitted is slots*redundancy - let avt = ((attempts * validators) as f64 * threshold) as u32; - assert_eq!(avt, slots * redundancy); + let avt = ((attempts as u32 * validators) as f64 * threshold) as u32; + assert_eq!(avt, slots * redundancy as u32); println!("threshold: {}", threshold); println!("avt = {}", avt); From d56c1c247cf6ae660eb8734c278df2db597345c2 Mon Sep 17 00:00:00 2001 From: Davide Galassi Date: Mon, 22 Jul 2024 11:24:11 +0200 Subject: [PATCH 34/42] Remove dead code --- substrate/frame/sassafras/src/tests.rs | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/substrate/frame/sassafras/src/tests.rs b/substrate/frame/sassafras/src/tests.rs index f6966fae1efe..70f2d976cd3f 100644 --- a/substrate/frame/sassafras/src/tests.rs +++ b/substrate/frame/sassafras/src/tests.rs @@ -57,16 +57,6 @@ fn shuffle(vector: &mut Vec, random_seed: u64) { } } -// fn dummy_tickets(count: usize) -> Vec { -// (0..count) -// .map(|i| { -// let mut id = [0xff; 32]; -// id[..8].copy_from_slice(&i.to_be_bytes()[..]); -// (TicketId(id), TicketBody { attempt_idx: i as u32, extra: Default::default() }) -// }) -// .collect() -// } - fn dummy_tickets(count: u8) -> Vec { make_ticket_bodies(count, None) } From 296380d8d641d1c08c3059651455dc0247d796b9 Mon Sep 17 00:00:00 2001 From: Davide Galassi Date: Wed, 24 Jul 2024 10:48:45 +0200 Subject: [PATCH 35/42] Fix after master merge --- Cargo.lock | 1 + substrate/frame/sassafras/Cargo.toml | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/Cargo.lock b/Cargo.lock index 94681a95202f..472f0c2f5679 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -11478,6 +11478,7 @@ name = "pallet-sassafras" version = "0.3.5-dev" dependencies = [ "array-bytes", + "env_logger 0.11.3", "frame-benchmarking", "frame-support", "frame-system", diff --git a/substrate/frame/sassafras/Cargo.toml b/substrate/frame/sassafras/Cargo.toml index 0162e940ca6e..7861ad42fdd9 100644 --- a/substrate/frame/sassafras/Cargo.toml +++ b/substrate/frame/sassafras/Cargo.toml @@ -28,10 +28,10 @@ sp-consensus-sassafras = { features = ["serde"], workspace = true } sp-core = { workspace = true } sp-io = { workspace = true } sp-runtime = { workspace = true } -#sp-std = { workspace = true } [dev-dependencies] array-bytes = { workspace = true, default-features = true } +env_logger = { workspace = true } sp-crypto-hashing = { workspace = true, default-features = true } [features] From a485f8ec720ab435bdab87f63d5b1b617bb6f07c Mon Sep 17 00:00:00 2001 From: Davide Galassi Date: Wed, 24 Jul 2024 10:51:37 +0200 Subject: [PATCH 36/42] Apply suggestions from code review Co-authored-by: Sebastian Kunert --- substrate/frame/sassafras/src/lib.rs | 2 +- substrate/primitives/consensus/sassafras/src/ticket.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/substrate/frame/sassafras/src/lib.rs b/substrate/frame/sassafras/src/lib.rs index d1f459d43ff6..ef1d9457de7f 100644 --- a/substrate/frame/sassafras/src/lib.rs +++ b/substrate/frame/sassafras/src/lib.rs @@ -369,7 +369,7 @@ pub mod pallet { .block_randomness; Self::deposit_randomness(block_randomness); - // Check if we are in the epoch's second half. + // Check if we are in the epoch's tail. // If so, start sorting the next epoch tickets. let epoch_length = T::EpochLength::get(); let current_slot_idx = Self::current_slot_index(); diff --git a/substrate/primitives/consensus/sassafras/src/ticket.rs b/substrate/primitives/consensus/sassafras/src/ticket.rs index a8f8f0c8a8a4..334d8553da54 100644 --- a/substrate/primitives/consensus/sassafras/src/ticket.rs +++ b/substrate/primitives/consensus/sassafras/src/ticket.rs @@ -131,7 +131,7 @@ mod tests { let base = max_u128 + 1.0; let max = max_u128 * (base + 1.0); - // Extract four u128 segments from the byte array + // Extract two u128 segments from the byte array let h = u128::from_be_bytes(bytes[..16].try_into().unwrap()) as f64; let l = u128::from_be_bytes(bytes[16..].try_into().unwrap()) as f64; (h * base + l) / max From d3c31745679965b8212408632885623da0df4f7d Mon Sep 17 00:00:00 2001 From: Davide Galassi Date: Wed, 24 Jul 2024 15:19:13 +0200 Subject: [PATCH 37/42] Rename epoch length -> duration --- substrate/frame/sassafras/src/benchmarking.rs | 4 +- substrate/frame/sassafras/src/lib.rs | 83 +++++++++---------- substrate/frame/sassafras/src/mock.rs | 21 +++-- substrate/frame/sassafras/src/tests.rs | 34 ++++---- .../primitives/consensus/sassafras/src/lib.rs | 12 +-- 5 files changed, 84 insertions(+), 70 deletions(-) diff --git a/substrate/frame/sassafras/src/benchmarking.rs b/substrate/frame/sassafras/src/benchmarking.rs index 9cf0d6278810..e247e9612971 100644 --- a/substrate/frame/sassafras/src/benchmarking.rs +++ b/substrate/frame/sassafras/src/benchmarking.rs @@ -86,11 +86,11 @@ mod benchmarks { // Makes the epoch change legit let post_init_cache = EphemeralData { - prev_slot: Slot::from(config.epoch_length as u64 - 1), + prev_slot: Slot::from(config.epoch_duration as u64 - 1), block_randomness: Randomness::default(), }; TemporaryData::::put(post_init_cache); - CurrentSlot::::set(Slot::from(config.epoch_length as u64)); + CurrentSlot::::set(Slot::from(config.epoch_duration as u64)); // Force ring verifier key re-computation let next_authorities: Vec<_> = diff --git a/substrate/frame/sassafras/src/lib.rs b/substrate/frame/sassafras/src/lib.rs index ef1d9457de7f..be931e7678a0 100644 --- a/substrate/frame/sassafras/src/lib.rs +++ b/substrate/frame/sassafras/src/lib.rs @@ -55,10 +55,7 @@ use scale_info::TypeInfo; use alloc::vec::Vec; use frame_support::{ - dispatch::DispatchResult, - traits::{ConstU32, Get}, - weights::Weight, - BoundedVec, WeakBoundedVec, + dispatch::DispatchResult, traits::Get, weights::Weight, BoundedVec, WeakBoundedVec, }; use frame_system::pallet_prelude::BlockNumberFor; use sp_consensus_sassafras::{ @@ -71,7 +68,7 @@ use sp_io::hashing; use sp_runtime::{ generic::DigestItem, traits::{One, Zero}, - BoundToRuntimeAppPublic, + BoundToRuntimeAppPublic, Percent, }; pub use pallet::*; @@ -91,17 +88,6 @@ const LOG_TARGET: &str = "sassafras::runtime"; // Contextual string used by the VRF to generate per-block randomness. const RANDOMNESS_VRF_CONTEXT: &[u8] = b"SassafrasOnChainRandomness"; -// Epoch tail is the section of the epoch where no tickets are allowed to be submitted. -// As the name implies, this section is at the end of an epoch. -// -// Length of the epoch's tail is computed as `Config::EpochLength / EPOCH_TAIL_FRACTION` -// TODO: make this part of `Config`? -const EPOCH_TAIL_FRACTION: u32 = 6; - -/// Max number of tickets that can be submitted in one block. -// TODO: make this part of `Config`? -const TICKETS_CHUNK_MAX_LENGTH: u32 = 16; - /// Randomness buffer. pub type RandomnessBuffer = [Randomness; 4]; @@ -149,7 +135,7 @@ impl From for TicketKey { pub type AuthoritiesVec = WeakBoundedVec::MaxAuthorities>; /// Tickets sequence. -pub type TicketsVec = BoundedVec>; +pub type TicketsVec = BoundedVec::TicketsChunkLength>; trait EpochTag { fn tag(&self) -> u8; @@ -183,7 +169,7 @@ pub mod pallet { pub trait Config: frame_system::Config { /// Amount of slots that each epoch should last. #[pallet::constant] - type EpochLength: Get; + type EpochDuration: Get; /// Max number of authorities allowed. #[pallet::constant] @@ -197,6 +183,17 @@ pub mod pallet { #[pallet::constant] type AttemptsNumber: Get; + /// Max number of tickets that can be submitted in one block. + #[pallet::constant] + type TicketsChunkLength: Get; + + /// Epoch lottery duration percent relative to the epoch `EpochDuration`. + /// + /// Tickets lottery starts with the start of an epoch. + /// When epoch lottery ends no more tickets are allowed to be submitted on-chain. + #[pallet::constant] + type LotteryDurationPercent: Get; + /// Epoch change trigger. /// /// Logic to be triggered on every block to query for whether an epoch has ended @@ -285,7 +282,7 @@ pub mod pallet { #[pallet::getter(fn ring_verifier_key)] pub type RingVerifierKey = StorageValue<_, vrf::RingVerifierKey>; - /// Ephemeral data we retain until the block finalization. + /// Ephemeral data we retain until block finalization. #[pallet::storage] pub(crate) type TemporaryData = StorageValue<_, EphemeralData>; @@ -369,15 +366,13 @@ pub mod pallet { .block_randomness; Self::deposit_randomness(block_randomness); - // Check if we are in the epoch's tail. - // If so, start sorting the next epoch tickets. - let epoch_length = T::EpochLength::get(); + // Check if tickets lottery is over, and if so, start sorting the next epoch tickets. + let epoch_duration = T::EpochDuration::get(); + let lottery_over_idx = T::LotteryDurationPercent::get() * epoch_duration; let current_slot_idx = Self::current_slot_index(); let mut outstanding_count = TicketsAccumulator::::count() as usize; - if current_slot_idx >= epoch_length - epoch_length / EPOCH_TAIL_FRACTION && - outstanding_count != 0 - { - let slots_left = epoch_length.checked_sub(current_slot_idx + 1).unwrap_or(1); + if current_slot_idx >= lottery_over_idx && outstanding_count != 0 { + let slots_left = epoch_duration.checked_sub(current_slot_idx).unwrap_or(1); if slots_left > 0 { outstanding_count = outstanding_count.div_ceil(slots_left as usize); } @@ -390,21 +385,19 @@ pub mod pallet { #[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(envelopes.len() as u32), DispatchClass::Mandatory ))] - pub fn submit_tickets(origin: OriginFor, envelopes: TicketsVec) -> DispatchResult { + pub fn submit_tickets(origin: OriginFor, envelopes: TicketsVec) -> DispatchResult { ensure_none(origin)?; debug!(target: LOG_TARGET, "Received {} tickets", envelopes.len()); - let epoch_length = T::EpochLength::get(); + let epoch_duration = T::EpochDuration::get(); let current_slot_idx = Self::current_slot_index(); - if current_slot_idx > epoch_length / 2 { + if current_slot_idx > epoch_duration / 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()) } @@ -420,7 +413,7 @@ pub mod pallet { // Compute tickets threshold let ticket_threshold = sp_consensus_sassafras::ticket_id_threshold( - epoch_length as u32, + epoch_duration as u32, authorities.len() as u32, T::AttemptsNumber::get(), T::RedundancyFactor::get(), @@ -585,7 +578,7 @@ impl Pallet { if count != prev_count + tickets.len() as u32 { return Err(Error::TicketDuplicate) } - let diff = count.saturating_sub(T::EpochLength::get()); + let diff = count.saturating_sub(T::EpochDuration::get()); if diff > 0 { let dropped_entries: Vec<_> = TicketsAccumulator::::iter().take(diff as usize).collect(); @@ -721,7 +714,7 @@ impl Pallet { } let mut epoch_tag = slot_epoch_idx.tag(); - let epoch_len = T::EpochLength::get(); + let epoch_len = T::EpochDuration::get(); let mut slot_idx = Self::slot_index(slot); if epoch_len <= slot_idx && slot_idx < 2 * epoch_len { @@ -772,9 +765,11 @@ impl Pallet { /// Static protocol configuration. #[inline(always)] pub fn protocol_config() -> Configuration { + let epoch_duration = T::EpochDuration::get(); + let lottery_duration = T::LotteryDurationPercent::get() * epoch_duration; Configuration { - epoch_length: T::EpochLength::get(), - epoch_tail_length: T::EpochLength::get() / EPOCH_TAIL_FRACTION, + epoch_duration, + lottery_duration, max_authorities: T::MaxAuthorities::get(), redundancy_factor: T::RedundancyFactor::get(), attempts_number: T::AttemptsNumber::get(), @@ -846,7 +841,7 @@ impl Pallet { /// Slot index relative to the current epoch. #[inline(always)] fn slot_index(slot: Slot) -> u32 { - (*slot % ::EpochLength::get() as u64) as u32 + (*slot % ::EpochDuration::get() as u64) as u32 } /// Current epoch index. @@ -858,15 +853,14 @@ impl Pallet { /// Epoch's index from slot. #[inline(always)] fn epoch_index(slot: Slot) -> u64 { - *slot / ::EpochLength::get() as u64 + *slot / ::EpochDuration::get() as u64 } - /// Epoch length /// Get current epoch first slot. #[inline(always)] fn current_epoch_start() -> Slot { let curr_slot = *Self::current_slot(); - let epoch_start = curr_slot - curr_slot % ::EpochLength::get() as u64; + let epoch_start = curr_slot - curr_slot % ::EpochDuration::get() as u64; Slot::from(epoch_start) } @@ -875,12 +869,13 @@ impl Pallet { 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."; - epoch_index.checked_mul(T::EpochLength::get() as u64).expect(PROOF).into() + epoch_index.checked_mul(T::EpochDuration::get() as u64).expect(PROOF).into() } + /// Epoch duration. #[inline(always)] - fn epoch_length() -> u32 { - T::EpochLength::get() + fn epoch_duration() -> u32 { + T::EpochDuration::get() } } @@ -920,7 +915,7 @@ impl EpochChangeTrigger for EpochChangeInternalTrigger { 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()) + T::WeightInfo::enact_epoch_change(len, T::EpochDuration::get()) } else { Weight::zero() } diff --git a/substrate/frame/sassafras/src/mock.rs b/substrate/frame/sassafras/src/mock.rs index 820665920f2f..5d5eb68d4d6e 100644 --- a/substrate/frame/sassafras/src/mock.rs +++ b/substrate/frame/sassafras/src/mock.rs @@ -20,7 +20,7 @@ use crate::{self as pallet_sassafras, EpochChangeInternalTrigger, *}; use frame_support::{ - derive_impl, + derive_impl, parameter_types, traits::{ConstU32, ConstU8, OnFinalize, OnInitialize}, }; use sp_consensus_sassafras::{ @@ -39,8 +39,13 @@ use sp_runtime::{ const LOG_TARGET: &str = "sassafras::tests"; -const EPOCH_LENGTH: u32 = 10; +// Configuration constants +const EPOCH_DURATION: u32 = 10; +const LOTTERY_PERCENT: u8 = 85; const MAX_AUTHORITIES: u32 = 100; +const REDUNDANCY_FACTOR: u8 = 32; +const ATTEMPTS_NUMBER: u8 = 2; +const TICKETS_CHUNK_LENGTH: u32 = 16; #[derive_impl(frame_system::config_preludes::TestDefaultConfig)] impl frame_system::Config for Test { @@ -55,11 +60,17 @@ where type Extrinsic = TestXt; } +parameter_types! { + pub const LotteryPercent: Percent = Percent::from_percent(LOTTERY_PERCENT); +} + impl pallet_sassafras::Config for Test { - type EpochLength = ConstU32; + type EpochDuration = ConstU32; type MaxAuthorities = ConstU32; - type RedundancyFactor = ConstU8<32>; - type AttemptsNumber = ConstU8<2>; + type RedundancyFactor = ConstU8; + type AttemptsNumber = ConstU8; + type TicketsChunkLength = ConstU32; + type LotteryDurationPercent = LotteryPercent; type EpochChangeTrigger = EpochChangeInternalTrigger; type WeightInfo = (); } diff --git a/substrate/frame/sassafras/src/tests.rs b/substrate/frame/sassafras/src/tests.rs index 70f2d976cd3f..2276f61f03d4 100644 --- a/substrate/frame/sassafras/src/tests.rs +++ b/substrate/frame/sassafras/src/tests.rs @@ -264,8 +264,8 @@ fn on_normal_block() { initialize_block(start_block, start_slot, Default::default(), &pairs[0]); // We don't want to trigger an epoch change in this test. - let epoch_length = Sassafras::epoch_length() as u64; - assert!(epoch_length > end_block); + let epoch_duration = Sassafras::epoch_duration() as u64; + assert!(epoch_duration > end_block); // Progress to block 2 let digest = progress_to_block(end_block, &pairs[0]).unwrap(); @@ -313,11 +313,11 @@ fn produce_epoch_change_digest() { initialize_block(start_block, start_slot, Default::default(), &pairs[0]); // We want to trigger an epoch change in this test. - let epoch_length = Sassafras::epoch_length() as u64; - let end_block = start_block + epoch_length - 1; + let epoch_duration = Sassafras::epoch_duration() as u64; + let end_block = start_block + epoch_duration - 1; let common_assertions = |initialized| { - assert_eq!(Sassafras::current_slot(), GENESIS_SLOT + epoch_length); + assert_eq!(Sassafras::current_slot(), GENESIS_SLOT + epoch_duration); assert_eq!(Sassafras::current_slot_index(), 0); assert_eq!(TemporaryData::::exists(), initialized); }; @@ -429,8 +429,8 @@ fn slot_and_epoch_helpers_works() { let (pairs, mut ext) = new_test_ext_with_pairs(1, false); ext.execute_with(|| { - let epoch_length = Sassafras::epoch_length() as u64; - assert_eq!(epoch_length, 10); + let epoch_duration = Sassafras::epoch_duration() as u64; + assert_eq!(epoch_duration, 10); let check = |slot, slot_idx, epoch_slot, epoch_idx| { assert_eq!(Sassafras::current_slot(), Slot::from(slot)); @@ -447,7 +447,7 @@ fn slot_and_epoch_helpers_works() { check(101, 1, 100, 10); // Progress to epoch N last block - let end_block = start_block + epoch_length - 2; + let end_block = start_block + epoch_duration - 2; progress_to_block(end_block, &pairs[0]).unwrap(); check(109, 9, 100, 10); @@ -456,7 +456,7 @@ fn slot_and_epoch_helpers_works() { check(110, 0, 110, 11); // Progress to epoch N+1 last block - let end_block = end_block + epoch_length; + let end_block = end_block + epoch_duration; progress_to_block(end_block, &pairs[0]).unwrap(); check(119, 9, 110, 11); @@ -479,7 +479,7 @@ fn tickets_accumulator_works() { let (pairs, mut ext) = new_test_ext_with_pairs(1, false); ext.execute_with(|| { - let epoch_length = Sassafras::epoch_length() as u64; + let epoch_duration = Sassafras::epoch_duration() as u64; let epoch_idx = Sassafras::current_epoch_index(); let epoch_tag = (epoch_idx % 2) as u8; @@ -493,12 +493,15 @@ fn tickets_accumulator_works() { .for_each(|t| TicketsAccumulator::::insert(TicketKey::from(t.id), t)); // Progress to epoch's last block - let end_block = start_block + epoch_length - 2; + let end_block = start_block + epoch_duration - 2; progress_to_block(end_block, &pairs[0]).unwrap(); let tickets_count = TicketsCount::::get(); assert_eq!(tickets_count[epoch_tag as usize], 0); - assert_eq!(tickets_count[next_epoch_tag as usize], 0); + assert!( + 0 < tickets_count[next_epoch_tag as usize] && + tickets_count[next_epoch_tag as usize] < e1_count as u32 + ); finalize_block(end_block); @@ -527,12 +530,15 @@ fn tickets_accumulator_works() { .for_each(|t| TicketsAccumulator::::insert(TicketKey::from(t.id), t)); // Progress to epoch's last block - let end_block = end_block + epoch_length; + let end_block = end_block + epoch_duration; progress_to_block(end_block, &pairs[0]).unwrap(); let tickets_count = TicketsCount::::get(); assert_eq!(tickets_count[epoch_tag as usize], e1_count as u32); - assert_eq!(tickets_count[next_epoch_tag as usize], 0); + assert!( + 0 < tickets_count[next_epoch_tag as usize] && + tickets_count[next_epoch_tag as usize] < e2_count as u32 + ); finalize_block(end_block); diff --git a/substrate/primitives/consensus/sassafras/src/lib.rs b/substrate/primitives/consensus/sassafras/src/lib.rs index e05c4fd6640a..5254064f104f 100644 --- a/substrate/primitives/consensus/sassafras/src/lib.rs +++ b/substrate/primitives/consensus/sassafras/src/lib.rs @@ -97,10 +97,12 @@ pub type Randomness = [u8; RANDOMNESS_LENGTH]; )] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] pub struct Configuration { - /// Epoch length. - pub epoch_length: u32, - /// Epoch's tail duration. - pub epoch_tail_length: u32, + /// Epoch duration in slots. + pub epoch_duration: u32, + /// Epoch's tickets' lottery duration in slots. + /// + /// This determines the period during which is allowed to submit new tickets on-chain. + pub lottery_duration: u32, /// Max number of authorities allowed. pub max_authorities: u32, /// Tickets redundancy factor. @@ -113,7 +115,7 @@ pub struct Configuration { /// Influences the anonymity of block producers. As all published tickets have a public /// attempt number less than `attempts_number` if two tickets share an attempt number /// then they must belong to two different validators, which reduces anonymity late as - /// we approach the epoch tail. + /// we approach the epoch's tail. /// /// This anonymity loss already becomes small when `attempts_number = 64` or `128`. pub attempts_number: u8, From 712f307d887dd08b872f9db1712b3cf35cb93393 Mon Sep 17 00:00:00 2001 From: Davide Galassi Date: Wed, 24 Jul 2024 16:53:01 +0200 Subject: [PATCH 38/42] Update weights --- substrate/frame/sassafras/src/weights.rs | 74 ++++++++++++------------ 1 file changed, 37 insertions(+), 37 deletions(-) diff --git a/substrate/frame/sassafras/src/weights.rs b/substrate/frame/sassafras/src/weights.rs index 281482016a54..113ca5611736 100644 --- a/substrate/frame/sassafras/src/weights.rs +++ b/substrate/frame/sassafras/src/weights.rs @@ -18,7 +18,7 @@ //! Autogenerated weights for `pallet_sassafras` //! //! THIS FILE WAS AUTO-GENERATED USING THE SUBSTRATE BENCHMARK CLI VERSION 32.0.0 -//! DATE: 2024-06-10, STEPS: `20`, REPEAT: `3`, LOW RANGE: `[]`, HIGH RANGE: `[]` +//! DATE: 2024-07-24, STEPS: `20`, REPEAT: `3`, LOW RANGE: `[]`, HIGH RANGE: `[]` //! WORST CASE MAP SIZE: `1000000` //! HOSTNAME: `behemoth`, CPU: `AMD Ryzen Threadripper 3970X 32-Core Processor` //! WASM-EXECUTION: `Compiled`, CHAIN: `Some("dev")`, DB CACHE: `1024` @@ -76,8 +76,8 @@ impl WeightInfo for SubstrateWeight { // Proof Size summary in bytes: // Measured: `270` // Estimated: `1755` - // Minimum execution time: 437_498_000 picoseconds. - Weight::from_parts(448_310_000, 1755) + // Minimum execution time: 381_101_000 picoseconds. + Weight::from_parts(390_519_000, 1755) .saturating_add(T::DbWeight::get().reads(4_u64)) .saturating_add(T::DbWeight::get().writes(3_u64)) } @@ -111,12 +111,12 @@ impl WeightInfo for SubstrateWeight { // Proof Size summary in bytes: // Measured: `590613 + x * (33 ±0) + y * (68 ±0)` // Estimated: `592099 + x * (33 ±0) + y * (2670 ±0)` - // Minimum execution time: 161_753_432_000 picoseconds. - Weight::from_parts(154_427_856_060, 592099) - // Standard Error: 3_252_626 - .saturating_add(Weight::from_parts(178_408_790, 0).saturating_mul(x.into())) - // Standard Error: 359_218 - .saturating_add(Weight::from_parts(8_152_285, 0).saturating_mul(y.into())) + // Minimum execution time: 143_884_573_000 picoseconds. + Weight::from_parts(138_216_369_210, 592099) + // Standard Error: 4_421_530 + .saturating_add(Weight::from_parts(148_944_356, 0).saturating_mul(x.into())) + // Standard Error: 488_312 + .saturating_add(Weight::from_parts(6_220_397, 0).saturating_mul(y.into())) .saturating_add(T::DbWeight::get().reads(9_u64)) .saturating_add(T::DbWeight::get().reads((1_u64).saturating_mul(y.into()))) .saturating_add(T::DbWeight::get().writes(7_u64)) @@ -141,10 +141,10 @@ impl WeightInfo for SubstrateWeight { // Proof Size summary in bytes: // Measured: `1029` // Estimated: `4787 + x * (2670 ±0)` - // Minimum execution time: 60_282_323_000 picoseconds. - Weight::from_parts(43_538_201_161, 4787) - // Standard Error: 9_492_599 - .saturating_add(Weight::from_parts(16_509_026_590, 0).saturating_mul(x.into())) + // Minimum execution time: 55_610_374_000 picoseconds. + Weight::from_parts(40_968_959_771, 4787) + // Standard Error: 23_542_470 + .saturating_add(Weight::from_parts(14_769_464_075, 0).saturating_mul(x.into())) .saturating_add(T::DbWeight::get().reads(5_u64)) .saturating_add(T::DbWeight::get().reads((1_u64).saturating_mul(x.into()))) .saturating_add(T::DbWeight::get().writes(1_u64)) @@ -160,10 +160,10 @@ impl WeightInfo for SubstrateWeight { // Proof Size summary in bytes: // Measured: `590458` // Estimated: `591809` - // Minimum execution time: 153_738_634_000 picoseconds. - Weight::from_parts(153_793_626_658, 591809) - // Standard Error: 4_264_780 - .saturating_add(Weight::from_parts(182_398_648, 0).saturating_mul(x.into())) + // Minimum execution time: 137_562_164_000 picoseconds. + Weight::from_parts(138_001_039_298, 591809) + // Standard Error: 2_598_185 + .saturating_add(Weight::from_parts(159_816_096, 0).saturating_mul(x.into())) .saturating_add(T::DbWeight::get().reads(1_u64)) .saturating_add(T::DbWeight::get().writes(1_u64)) } @@ -173,8 +173,8 @@ impl WeightInfo for SubstrateWeight { // Proof Size summary in bytes: // Measured: `590458` // Estimated: `591809` - // Minimum execution time: 63_857_510_000 picoseconds. - Weight::from_parts(64_221_658_000, 591809) + // Minimum execution time: 55_076_836_000 picoseconds. + Weight::from_parts(55_414_934_000, 591809) .saturating_add(T::DbWeight::get().reads(1_u64)) } } @@ -195,8 +195,8 @@ impl WeightInfo for () { // Proof Size summary in bytes: // Measured: `270` // Estimated: `1755` - // Minimum execution time: 437_498_000 picoseconds. - Weight::from_parts(448_310_000, 1755) + // Minimum execution time: 381_101_000 picoseconds. + Weight::from_parts(390_519_000, 1755) .saturating_add(RocksDbWeight::get().reads(4_u64)) .saturating_add(RocksDbWeight::get().writes(3_u64)) } @@ -230,12 +230,12 @@ impl WeightInfo for () { // Proof Size summary in bytes: // Measured: `590613 + x * (33 ±0) + y * (68 ±0)` // Estimated: `592099 + x * (33 ±0) + y * (2670 ±0)` - // Minimum execution time: 161_753_432_000 picoseconds. - Weight::from_parts(154_427_856_060, 592099) - // Standard Error: 3_252_626 - .saturating_add(Weight::from_parts(178_408_790, 0).saturating_mul(x.into())) - // Standard Error: 359_218 - .saturating_add(Weight::from_parts(8_152_285, 0).saturating_mul(y.into())) + // Minimum execution time: 143_884_573_000 picoseconds. + Weight::from_parts(138_216_369_210, 592099) + // Standard Error: 4_421_530 + .saturating_add(Weight::from_parts(148_944_356, 0).saturating_mul(x.into())) + // Standard Error: 488_312 + .saturating_add(Weight::from_parts(6_220_397, 0).saturating_mul(y.into())) .saturating_add(RocksDbWeight::get().reads(9_u64)) .saturating_add(RocksDbWeight::get().reads((1_u64).saturating_mul(y.into()))) .saturating_add(RocksDbWeight::get().writes(7_u64)) @@ -260,10 +260,10 @@ impl WeightInfo for () { // Proof Size summary in bytes: // Measured: `1029` // Estimated: `4787 + x * (2670 ±0)` - // Minimum execution time: 60_282_323_000 picoseconds. - Weight::from_parts(43_538_201_161, 4787) - // Standard Error: 9_492_599 - .saturating_add(Weight::from_parts(16_509_026_590, 0).saturating_mul(x.into())) + // Minimum execution time: 55_610_374_000 picoseconds. + Weight::from_parts(40_968_959_771, 4787) + // Standard Error: 23_542_470 + .saturating_add(Weight::from_parts(14_769_464_075, 0).saturating_mul(x.into())) .saturating_add(RocksDbWeight::get().reads(5_u64)) .saturating_add(RocksDbWeight::get().reads((1_u64).saturating_mul(x.into()))) .saturating_add(RocksDbWeight::get().writes(1_u64)) @@ -279,10 +279,10 @@ impl WeightInfo for () { // Proof Size summary in bytes: // Measured: `590458` // Estimated: `591809` - // Minimum execution time: 153_738_634_000 picoseconds. - Weight::from_parts(153_793_626_658, 591809) - // Standard Error: 4_264_780 - .saturating_add(Weight::from_parts(182_398_648, 0).saturating_mul(x.into())) + // Minimum execution time: 137_562_164_000 picoseconds. + Weight::from_parts(138_001_039_298, 591809) + // Standard Error: 2_598_185 + .saturating_add(Weight::from_parts(159_816_096, 0).saturating_mul(x.into())) .saturating_add(RocksDbWeight::get().reads(1_u64)) .saturating_add(RocksDbWeight::get().writes(1_u64)) } @@ -292,8 +292,8 @@ impl WeightInfo for () { // Proof Size summary in bytes: // Measured: `590458` // Estimated: `591809` - // Minimum execution time: 63_857_510_000 picoseconds. - Weight::from_parts(64_221_658_000, 591809) + // Minimum execution time: 55_076_836_000 picoseconds. + Weight::from_parts(55_414_934_000, 591809) .saturating_add(RocksDbWeight::get().reads(1_u64)) } } From f4ab66ca3461f2b300f46ea630f5750c859c677f Mon Sep 17 00:00:00 2001 From: Davide Galassi Date: Wed, 24 Jul 2024 18:46:17 +0200 Subject: [PATCH 39/42] Simplify logic --- substrate/frame/sassafras/src/lib.rs | 41 +++++------- substrate/frame/sassafras/src/mock.rs | 14 ++++- substrate/frame/sassafras/src/tests.rs | 87 +++++++++++++++----------- 3 files changed, 77 insertions(+), 65 deletions(-) diff --git a/substrate/frame/sassafras/src/lib.rs b/substrate/frame/sassafras/src/lib.rs index be931e7678a0..fe465a5cdacc 100644 --- a/substrate/frame/sassafras/src/lib.rs +++ b/substrate/frame/sassafras/src/lib.rs @@ -593,6 +593,9 @@ impl Pallet { Ok(()) } + // Consumes the tickets accumulator relative to `epoch_tag` by depositing at most + // `max_items` into the `Tickets` map. Ticket bodies are stored in the `Tickets` + // map from smaller to bigger wrt ticket identifier (as required by the protocol). fn consume_tickets_accumulator(max_items: usize, epoch_tag: u8) { let mut tickets_count = TicketsCount::::get(); let mut accumulator_count = TicketsAccumulator::::count(); @@ -616,7 +619,7 @@ impl Pallet { announce } - // Deposit per-slot randomness. + // Deposit per-block randomness. fn deposit_randomness(randomness: Randomness) { let mut accumulator = RandomnessBuf::::get(); let mut buf = [0; 2 * RANDOMNESS_LENGTH]; @@ -714,36 +717,26 @@ impl Pallet { } let mut epoch_tag = slot_epoch_idx.tag(); - let epoch_len = T::EpochDuration::get(); - let mut slot_idx = Self::slot_index(slot); - - 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 = slot_epoch_idx.next_tag(); - slot_idx -= epoch_len; - if TicketsAccumulator::::count() != 0 { - Self::consume_tickets_accumulator(usize::MAX, epoch_tag); - } - } else if slot_idx >= 2 * epoch_len { - return None + let slot_idx = Self::slot_index(slot); + + if slot_epoch_idx == curr_epoch_idx + 1 && TicketsAccumulator::::count() != 0 { + // JIT enactment of next epoch tickets when the accumulator has not been + // fully consumed yet. Drain and enact the accumulator for next epoch. + Self::consume_tickets_accumulator(usize::MAX, epoch_tag); } - let mut tickets_count = TicketsCount::::get(); - let tickets_count = tickets_count[epoch_tag as usize]; + let tickets_count = TicketsCount::::get()[epoch_tag as usize]; if tickets_count <= slot_idx { + // Slot not bound to a ticket. return None } - let get_ticket_index = |slot_index: u32| { - let mut ticket_index = slot_idx / 2; - if slot_index & 1 != 0 { - ticket_index = tickets_count - (ticket_index + 1); - } - ticket_index as u32 - }; + // Outside-in sort. + let mut ticket_idx = slot_idx / 2; + if slot_idx & 1 != 0 { + ticket_idx = tickets_count - (ticket_idx + 1); + } - let ticket_idx = get_ticket_index(slot_idx); debug!( target: LOG_TARGET, "slot-idx {} <-> ticket-idx {}", diff --git a/substrate/frame/sassafras/src/mock.rs b/substrate/frame/sassafras/src/mock.rs index 5d5eb68d4d6e..e260748ec6a1 100644 --- a/substrate/frame/sassafras/src/mock.rs +++ b/substrate/frame/sassafras/src/mock.rs @@ -167,14 +167,22 @@ pub fn make_dummy_ticket_body(attempt: u8) -> TicketBody { TicketBody { id, attempt, extra } } -pub fn make_ticket_bodies(attempts: u8, pair: Option<&AuthorityPair>) -> Vec { - (0..attempts) +pub fn make_ticket_bodies( + attempts: u8, + pair: Option<&AuthorityPair>, + sort: bool, +) -> Vec { + let mut bodies: Vec<_> = (0..attempts) .into_iter() .map(|i| match pair { Some(pair) => make_ticket_body(i, pair), None => make_dummy_ticket_body(i), }) - .collect() + .collect(); + if sort { + bodies.sort_unstable(); + } + bodies } pub fn initialize_block( diff --git a/substrate/frame/sassafras/src/tests.rs b/substrate/frame/sassafras/src/tests.rs index 2276f61f03d4..81c241322c41 100644 --- a/substrate/frame/sassafras/src/tests.rs +++ b/substrate/frame/sassafras/src/tests.rs @@ -44,27 +44,12 @@ macro_rules! prefix_eq { }}; } -// Fisher-Yates shuffle. -// -// We don't want to implement something secure here. -// Just a trivial pseudo-random shuffle for the tests. -fn shuffle(vector: &mut Vec, random_seed: u64) { - let mut r = random_seed as usize; - for i in (1..vector.len()).rev() { - let j = r % (i + 1); - vector.swap(i, j); - r = (r.wrapping_mul(6364793005) + 1) as usize; - } -} - -fn dummy_tickets(count: u8) -> Vec { - make_ticket_bodies(count, None) -} - #[test] fn assumptions_check() { - let mut tickets = dummy_tickets(100); - shuffle(&mut tickets, 123); + let mut tickets = make_ticket_bodies(100, None, false); + + // Check that the returned tickets are not sorted to start with. + assert!(tickets.windows(2).any(|w| w[0] > w[1])); new_test_ext(3).execute_with(|| { assert_eq!(Sassafras::authorities().len(), 3); @@ -86,8 +71,7 @@ fn assumptions_check() { #[test] fn deposit_tickets_works() { - let mut tickets = dummy_tickets(15); - shuffle(&mut tickets, 123); + let mut tickets = make_ticket_bodies(15, None, false); new_test_ext(1).execute_with(|| { // Try to append an unsorted chunk @@ -351,41 +335,54 @@ fn produce_epoch_change_digest() { } // Tests if the sorted tickets are assigned to each slot outside-in. -#[test] -fn slot_ticket_id_outside_in_fetch() { +fn slot_ticket_id_outside_in_fetch(jit_accumulator_drain: bool) { let genesis_slot = Slot::from(GENESIS_SLOT); let curr_count = 8; let next_count = 6; - let tickets = dummy_tickets(curr_count + next_count); + let tickets = make_ticket_bodies(curr_count + next_count, None, false); - // Current epoch tickets - let curr_tickets = tickets[..curr_count as usize].to_vec(); - let next_tickets = tickets[curr_count as usize..].to_vec(); + // Current epoch tickets (incrementally sorted as expected by the protocol) + let mut curr_tickets = tickets[..curr_count as usize].to_vec(); + curr_tickets.sort_unstable(); + // Next epoch tickets (incrementally sorted as expected by the protocol) + let mut next_tickets = tickets[curr_count as usize..].to_vec(); + next_tickets.sort_unstable(); new_test_ext(0).execute_with(|| { + // Store current epoch tickets in place. curr_tickets .iter() .enumerate() .for_each(|(i, t)| Tickets::::insert((0, i as u32), t)); - next_tickets - .iter() - .enumerate() - .for_each(|(i, t)| Tickets::::insert((1, i as u32), t)); + if jit_accumulator_drain { + // Store next epoch tickets in the accumulator (to test the JIT sorting logic as well) + next_tickets + .iter() + .for_each(|t| TicketsAccumulator::::insert(TicketKey::from(t.id), t)); + TicketsCount::::set([curr_count as u32, 0]); + } else { + // Directly store in the tickets buffer + next_tickets + .iter() + .enumerate() + .for_each(|(i, t)| Tickets::::insert((1, i as u32), t)); + TicketsCount::::set([curr_count as u32, next_count as u32]); + } - TicketsCount::::set([curr_count as u32, next_count as u32]); CurrentSlot::::set(genesis_slot); - // Before importing the first block the pallet always return `None` - // This is a kind of special hardcoded case that should never happen in practice - // as the first thing the pallet does is to initialize the genesis slot. + // Before importing the first block (on frame System pallet) `slot_ticket` always + // returns `None`. This is a kind of special hardcoded case that should never happen + // in practice as the first thing the pallet does is to initialize the genesis slot. assert_eq!(Sassafras::slot_ticket(0.into()), None); assert_eq!(Sassafras::slot_ticket(genesis_slot + 0), None); assert_eq!(Sassafras::slot_ticket(genesis_slot + 1), None); assert_eq!(Sassafras::slot_ticket(genesis_slot + 100), None); - // Reset block number.. + // Manually set block number to simulate that frame system initialize has been + // called for the first block. frame_system::Pallet::::set_block_number(One::one()); // Try to fetch a ticket for a slot before current epoch. @@ -405,6 +402,10 @@ fn slot_ticket_id_outside_in_fetch() { // Next epoch tickets. assert_eq!(Sassafras::slot_ticket(genesis_slot + 10).unwrap(), next_tickets[0]); + if jit_accumulator_drain { + // After first fetch tickets are moved to the buffer + assert_eq!(TicketsCount::::get()[1], 6); + } assert_eq!(Sassafras::slot_ticket(genesis_slot + 11).unwrap(), next_tickets[5]); assert_eq!(Sassafras::slot_ticket(genesis_slot + 12).unwrap(), next_tickets[1]); assert_eq!(Sassafras::slot_ticket(genesis_slot + 13).unwrap(), next_tickets[4]); @@ -421,6 +422,16 @@ fn slot_ticket_id_outside_in_fetch() { }); } +#[test] +fn slot_ticket_id_outside_in_fetch_jit_accumulator_drain() { + slot_ticket_id_outside_in_fetch(true); +} + +#[test] +fn slot_ticket_id_outside_in_fetch_no_jit_accumulator_drain() { + slot_ticket_id_outside_in_fetch(false); +} + #[test] fn slot_and_epoch_helpers_works() { let start_block = 1; @@ -472,7 +483,7 @@ fn tickets_accumulator_works() { let start_slot = (GENESIS_SLOT + 1).into(); let e1_count = 6; let e2_count = 10; - let tickets = dummy_tickets(e1_count + e2_count); + let tickets = make_ticket_bodies(e1_count + e2_count, None, false); let e1_tickets = tickets[..e1_count as usize].to_vec(); let e2_tickets = tickets[e1_count as usize..].to_vec(); @@ -564,7 +575,7 @@ fn tickets_accumulator_works() { #[test] fn incremental_accumulator_drain() { - let tickets = dummy_tickets(10); + let tickets = make_ticket_bodies(10, None, false); new_test_ext(0).execute_with(|| { tickets From 476b188f4b936e6b338ebdf6e77d4649e54f138a Mon Sep 17 00:00:00 2001 From: Davide Galassi Date: Wed, 24 Jul 2024 19:23:53 +0200 Subject: [PATCH 40/42] Excercise all failure paths during tickets submission --- substrate/frame/sassafras/src/lib.rs | 41 ++++++++++++++++++-------- substrate/frame/sassafras/src/tests.rs | 24 +++++++++++---- 2 files changed, 48 insertions(+), 17 deletions(-) diff --git a/substrate/frame/sassafras/src/lib.rs b/substrate/frame/sassafras/src/lib.rs index fe465a5cdacc..2be97a309336 100644 --- a/substrate/frame/sassafras/src/lib.rs +++ b/substrate/frame/sassafras/src/lib.rs @@ -207,16 +207,24 @@ pub mod pallet { /// Sassafras runtime errors. #[pallet::error] pub enum Error { + /// Tickets were found after the lottery is over. + TicketUnexpected, /// Ticket identifier is too big. TicketOverThreshold, - /// Duplicate ticket - TicketDuplicate, - /// Bad ticket order + /// Bad ticket order. TicketBadOrder, - /// Invalid ticket - TicketInvalid, - /// Invalid ticket signature + /// Invalid ticket signature. TicketBadProof, + /// Invalid ticket attempt number. + TicketBadAttempt, + /// Some submitted ticket has not been persisted because of its score. + TicketDropped, + /// Duplicate ticket. + TicketDuplicate, + /// Invalid VRF output. + TicketBadVrfOutput, + /// Uninitialized Ring Verifier + TicketVerifierNotInitialized, } /// Current epoch authorities. @@ -397,14 +405,16 @@ pub mod pallet { let epoch_duration = T::EpochDuration::get(); let current_slot_idx = Self::current_slot_index(); - if current_slot_idx > epoch_duration / 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 lottery_over_idx = T::LotteryDurationPercent::get() * epoch_duration; + + if current_slot_idx >= lottery_over_idx { + warn!(target: LOG_TARGET, "Lottery is over, tickets must be submitted before slot index {}", lottery_over_idx); + return Err(Error::::TicketUnexpected.into()) } let Some(verifier) = RingVerifierKey::::get().map(|v| v.into()) else { warn!(target: LOG_TARGET, "Ring verifier key not initialized"); - return Err("Ring verifier key not initialized".into()) + return Err(Error::::TicketVerifierNotInitialized.into()) }; // Get next epoch parameters @@ -419,11 +429,18 @@ pub mod pallet { T::RedundancyFactor::get(), ); + let attempts_num = T::AttemptsNumber::get(); + let mut candidates = Vec::new(); for envelope in envelopes { + if envelope.attempt >= attempts_num { + debug!(target: LOG_TARGET, "Bad ticket attempt"); + return Err(Error::::TicketBadAttempt.into()) + } + let Some(ticket_id_pre_output) = envelope.signature.pre_outputs.get(0) else { debug!(target: LOG_TARGET, "Missing ticket VRF pre-output from ring signature"); - return Err(Error::::TicketInvalid.into()) + return Err(Error::::TicketBadVrfOutput.into()) }; let ticket_id_input = vrf::ticket_id_input(&randomness, envelope.attempt); @@ -585,7 +602,7 @@ impl Pallet { // Assess that no new ticket has been dropped for (key, ticket) in dropped_entries { if tickets.binary_search_by_key(&ticket.id, |t| t.id).is_ok() { - return Err(Error::TicketInvalid) + return Err(Error::TicketDropped) } TicketsAccumulator::::remove(key); } diff --git a/substrate/frame/sassafras/src/tests.rs b/substrate/frame/sassafras/src/tests.rs index 81c241322c41..68c981768414 100644 --- a/substrate/frame/sassafras/src/tests.rs +++ b/substrate/frame/sassafras/src/tests.rs @@ -620,7 +620,7 @@ fn incremental_accumulator_drain() { } #[test] -fn submit_tickets_with_ring_proof_check_works() { +fn submit_tickets_works() { use sp_core::Pair as _; let _ = env_logger::try_init(); let start_block = 1; @@ -632,6 +632,8 @@ fn submit_tickets_with_ring_proof_check_works() { Vec, ) = data_read(TICKETS_FILE); + let config = Sassafras::protocol_config(); + // Also checks that duplicates are discarded let (pairs, mut ext) = new_test_ext_with_pairs(authorities.len(), true); @@ -657,13 +659,20 @@ fn submit_tickets_with_ring_proof_check_works() { .collect(); assert_eq!(chunks.len(), 5); - // Submit an invalid candidate + // Try to submit a candidate with an invalid signature. let mut chunk = chunks[2].clone(); - chunk[0].attempt += 1; + chunk[0].signature.signature[0] ^= 1; let e = Sassafras::submit_tickets(RuntimeOrigin::none(), chunk).unwrap_err(); assert_eq!(e, DispatchError::from(Error::::TicketBadProof)); assert_eq!(TicketsAccumulator::::count(), 0); + // Try to submit with invalid attempt number. + let mut chunk = chunks[2].clone(); + chunk[0].attempt = u8::MAX; + let e = Sassafras::submit_tickets(RuntimeOrigin::none(), chunk).unwrap_err(); + assert_eq!(e, DispatchError::from(Error::::TicketBadAttempt)); + assert_eq!(TicketsAccumulator::::count(), 0); + // Start submitting from the mid valued chunks. Sassafras::submit_tickets(RuntimeOrigin::none(), chunks[2].clone()).unwrap(); assert_eq!(TicketsAccumulator::::count(), 4); @@ -672,7 +681,7 @@ fn submit_tickets_with_ring_proof_check_works() { Sassafras::submit_tickets(RuntimeOrigin::none(), chunks[3].clone()).unwrap(); assert_eq!(TicketsAccumulator::::count(), 8); - // Try to submit duplicates + // Try to submit a ticket duplicate let e = Sassafras::submit_tickets(RuntimeOrigin::none(), chunks[2].clone()).unwrap_err(); assert_eq!(e, DispatchError::from(Error::::TicketDuplicate)); assert_eq!(TicketsAccumulator::::count(), 8); @@ -683,12 +692,17 @@ fn submit_tickets_with_ring_proof_check_works() { // Try to submit a chunk with bigger tickets. This is discarded let e = Sassafras::submit_tickets(RuntimeOrigin::none(), chunks[4].clone()).unwrap_err(); - assert_eq!(e, DispatchError::from(Error::::TicketInvalid)); + assert_eq!(e, DispatchError::from(Error::::TicketDropped)); assert_eq!(TicketsAccumulator::::count(), 10); // Submit the smaller candidates chunks. This is accepted (4 old tickets removed). Sassafras::submit_tickets(RuntimeOrigin::none(), chunks[0].clone()).unwrap(); assert_eq!(TicketsAccumulator::::count(), 10); + + // Try to submit a chunk after when the contest is over. + progress_to_block(start_block + (config.epoch_duration as u64 - 2), &pairs[0]).unwrap(); + let e = Sassafras::submit_tickets(RuntimeOrigin::none(), chunks[0].clone()).unwrap_err(); + assert_eq!(e, DispatchError::from(Error::::TicketUnexpected)); }) } From 20f57fe7d4518e871d01669f4d75203a9af1b892 Mon Sep 17 00:00:00 2001 From: Davide Galassi Date: Wed, 24 Jul 2024 19:35:09 +0200 Subject: [PATCH 41/42] Update weights --- substrate/frame/sassafras/src/weights.rs | 72 ++++++++++++------------ 1 file changed, 36 insertions(+), 36 deletions(-) diff --git a/substrate/frame/sassafras/src/weights.rs b/substrate/frame/sassafras/src/weights.rs index 113ca5611736..41a005b26cc5 100644 --- a/substrate/frame/sassafras/src/weights.rs +++ b/substrate/frame/sassafras/src/weights.rs @@ -76,8 +76,8 @@ impl WeightInfo for SubstrateWeight { // Proof Size summary in bytes: // Measured: `270` // Estimated: `1755` - // Minimum execution time: 381_101_000 picoseconds. - Weight::from_parts(390_519_000, 1755) + // Minimum execution time: 382_223_000 picoseconds. + Weight::from_parts(383_656_000, 1755) .saturating_add(T::DbWeight::get().reads(4_u64)) .saturating_add(T::DbWeight::get().writes(3_u64)) } @@ -111,12 +111,12 @@ impl WeightInfo for SubstrateWeight { // Proof Size summary in bytes: // Measured: `590613 + x * (33 ±0) + y * (68 ±0)` // Estimated: `592099 + x * (33 ±0) + y * (2670 ±0)` - // Minimum execution time: 143_884_573_000 picoseconds. - Weight::from_parts(138_216_369_210, 592099) - // Standard Error: 4_421_530 - .saturating_add(Weight::from_parts(148_944_356, 0).saturating_mul(x.into())) - // Standard Error: 488_312 - .saturating_add(Weight::from_parts(6_220_397, 0).saturating_mul(y.into())) + // Minimum execution time: 142_623_107_000 picoseconds. + Weight::from_parts(135_944_664_003, 592099) + // Standard Error: 3_660_095 + .saturating_add(Weight::from_parts(174_904_510, 0).saturating_mul(x.into())) + // Standard Error: 404_219 + .saturating_add(Weight::from_parts(7_440_688, 0).saturating_mul(y.into())) .saturating_add(T::DbWeight::get().reads(9_u64)) .saturating_add(T::DbWeight::get().reads((1_u64).saturating_mul(y.into()))) .saturating_add(T::DbWeight::get().writes(7_u64)) @@ -141,10 +141,10 @@ impl WeightInfo for SubstrateWeight { // Proof Size summary in bytes: // Measured: `1029` // Estimated: `4787 + x * (2670 ±0)` - // Minimum execution time: 55_610_374_000 picoseconds. - Weight::from_parts(40_968_959_771, 4787) - // Standard Error: 23_542_470 - .saturating_add(Weight::from_parts(14_769_464_075, 0).saturating_mul(x.into())) + // Minimum execution time: 52_363_693_000 picoseconds. + Weight::from_parts(38_029_460_770, 4787) + // Standard Error: 15_839_361 + .saturating_add(Weight::from_parts(14_567_084_979, 0).saturating_mul(x.into())) .saturating_add(T::DbWeight::get().reads(5_u64)) .saturating_add(T::DbWeight::get().reads((1_u64).saturating_mul(x.into()))) .saturating_add(T::DbWeight::get().writes(1_u64)) @@ -160,10 +160,10 @@ impl WeightInfo for SubstrateWeight { // Proof Size summary in bytes: // Measured: `590458` // Estimated: `591809` - // Minimum execution time: 137_562_164_000 picoseconds. - Weight::from_parts(138_001_039_298, 591809) - // Standard Error: 2_598_185 - .saturating_add(Weight::from_parts(159_816_096, 0).saturating_mul(x.into())) + // Minimum execution time: 135_738_430_000 picoseconds. + Weight::from_parts(135_840_809_672, 591809) + // Standard Error: 3_319_979 + .saturating_add(Weight::from_parts(173_092_727, 0).saturating_mul(x.into())) .saturating_add(T::DbWeight::get().reads(1_u64)) .saturating_add(T::DbWeight::get().writes(1_u64)) } @@ -173,8 +173,8 @@ impl WeightInfo for SubstrateWeight { // Proof Size summary in bytes: // Measured: `590458` // Estimated: `591809` - // Minimum execution time: 55_076_836_000 picoseconds. - Weight::from_parts(55_414_934_000, 591809) + // Minimum execution time: 55_326_215_000 picoseconds. + Weight::from_parts(55_332_809_000, 591809) .saturating_add(T::DbWeight::get().reads(1_u64)) } } @@ -195,8 +195,8 @@ impl WeightInfo for () { // Proof Size summary in bytes: // Measured: `270` // Estimated: `1755` - // Minimum execution time: 381_101_000 picoseconds. - Weight::from_parts(390_519_000, 1755) + // Minimum execution time: 382_223_000 picoseconds. + Weight::from_parts(383_656_000, 1755) .saturating_add(RocksDbWeight::get().reads(4_u64)) .saturating_add(RocksDbWeight::get().writes(3_u64)) } @@ -230,12 +230,12 @@ impl WeightInfo for () { // Proof Size summary in bytes: // Measured: `590613 + x * (33 ±0) + y * (68 ±0)` // Estimated: `592099 + x * (33 ±0) + y * (2670 ±0)` - // Minimum execution time: 143_884_573_000 picoseconds. - Weight::from_parts(138_216_369_210, 592099) - // Standard Error: 4_421_530 - .saturating_add(Weight::from_parts(148_944_356, 0).saturating_mul(x.into())) - // Standard Error: 488_312 - .saturating_add(Weight::from_parts(6_220_397, 0).saturating_mul(y.into())) + // Minimum execution time: 142_623_107_000 picoseconds. + Weight::from_parts(135_944_664_003, 592099) + // Standard Error: 3_660_095 + .saturating_add(Weight::from_parts(174_904_510, 0).saturating_mul(x.into())) + // Standard Error: 404_219 + .saturating_add(Weight::from_parts(7_440_688, 0).saturating_mul(y.into())) .saturating_add(RocksDbWeight::get().reads(9_u64)) .saturating_add(RocksDbWeight::get().reads((1_u64).saturating_mul(y.into()))) .saturating_add(RocksDbWeight::get().writes(7_u64)) @@ -260,10 +260,10 @@ impl WeightInfo for () { // Proof Size summary in bytes: // Measured: `1029` // Estimated: `4787 + x * (2670 ±0)` - // Minimum execution time: 55_610_374_000 picoseconds. - Weight::from_parts(40_968_959_771, 4787) - // Standard Error: 23_542_470 - .saturating_add(Weight::from_parts(14_769_464_075, 0).saturating_mul(x.into())) + // Minimum execution time: 52_363_693_000 picoseconds. + Weight::from_parts(38_029_460_770, 4787) + // Standard Error: 15_839_361 + .saturating_add(Weight::from_parts(14_567_084_979, 0).saturating_mul(x.into())) .saturating_add(RocksDbWeight::get().reads(5_u64)) .saturating_add(RocksDbWeight::get().reads((1_u64).saturating_mul(x.into()))) .saturating_add(RocksDbWeight::get().writes(1_u64)) @@ -279,10 +279,10 @@ impl WeightInfo for () { // Proof Size summary in bytes: // Measured: `590458` // Estimated: `591809` - // Minimum execution time: 137_562_164_000 picoseconds. - Weight::from_parts(138_001_039_298, 591809) - // Standard Error: 2_598_185 - .saturating_add(Weight::from_parts(159_816_096, 0).saturating_mul(x.into())) + // Minimum execution time: 135_738_430_000 picoseconds. + Weight::from_parts(135_840_809_672, 591809) + // Standard Error: 3_319_979 + .saturating_add(Weight::from_parts(173_092_727, 0).saturating_mul(x.into())) .saturating_add(RocksDbWeight::get().reads(1_u64)) .saturating_add(RocksDbWeight::get().writes(1_u64)) } @@ -292,8 +292,8 @@ impl WeightInfo for () { // Proof Size summary in bytes: // Measured: `590458` // Estimated: `591809` - // Minimum execution time: 55_076_836_000 picoseconds. - Weight::from_parts(55_414_934_000, 591809) + // Minimum execution time: 55_326_215_000 picoseconds. + Weight::from_parts(55_332_809_000, 591809) .saturating_add(RocksDbWeight::get().reads(1_u64)) } } From bcfdd09a8946cc010c9ab92a6e7ba4b81f69278d Mon Sep 17 00:00:00 2001 From: Davide Galassi Date: Sun, 28 Jul 2024 09:44:13 +0200 Subject: [PATCH 42/42] Add note about epoch duration --- substrate/frame/sassafras/src/lib.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/substrate/frame/sassafras/src/lib.rs b/substrate/frame/sassafras/src/lib.rs index 2be97a309336..1a21d8932093 100644 --- a/substrate/frame/sassafras/src/lib.rs +++ b/substrate/frame/sassafras/src/lib.rs @@ -168,6 +168,9 @@ pub mod pallet { #[pallet::config] pub trait Config: frame_system::Config { /// Amount of slots that each epoch should last. + /// + /// NOTE: Currently it is not possible to change the epoch duration after + /// the chain has started. Attempting to do so will brick block production. #[pallet::constant] type EpochDuration: Get;