From 69683763097c95cd7c85119dacb29f12e9e9b0b0 Mon Sep 17 00:00:00 2001 From: Davide Galassi Date: Sat, 12 Aug 2023 18:27:00 +0200 Subject: [PATCH 01/22] Introduce sassafras primitives --- Cargo.lock | 20 ++ Cargo.toml | 1 + primitives/consensus/sassafras/Cargo.toml | 59 ++++++ primitives/consensus/sassafras/src/digests.rs | 101 +++++++++ .../consensus/sassafras/src/inherents.rs | 102 +++++++++ primitives/consensus/sassafras/src/lib.rs | 193 ++++++++++++++++++ primitives/consensus/sassafras/src/ticket.rs | 149 ++++++++++++++ primitives/core/src/bandersnatch.rs | 51 ++++- primitives/core/src/crypto.rs | 2 + 9 files changed, 668 insertions(+), 10 deletions(-) create mode 100644 primitives/consensus/sassafras/Cargo.toml create mode 100644 primitives/consensus/sassafras/src/digests.rs create mode 100644 primitives/consensus/sassafras/src/inherents.rs create mode 100644 primitives/consensus/sassafras/src/lib.rs create mode 100644 primitives/consensus/sassafras/src/ticket.rs diff --git a/Cargo.lock b/Cargo.lock index 0289ea601eab0..0fb1366dd5d08 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -10736,6 +10736,26 @@ dependencies = [ "sp-std", ] +[[package]] +name = "sp-consensus-sassafras" +version = "0.3.4-dev" +dependencies = [ + "async-trait", + "merlin 2.0.1", + "parity-scale-codec", + "scale-info", + "serde", + "sp-api", + "sp-application-crypto", + "sp-consensus-slots", + "sp-core", + "sp-inherents", + "sp-keystore", + "sp-runtime", + "sp-std", + "sp-timestamp", +] + [[package]] name = "sp-consensus-slots" version = "0.10.0-dev" diff --git a/Cargo.toml b/Cargo.toml index 9ee8142e23e76..4743f1b30da54 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -243,6 +243,7 @@ members = [ "primitives/consensus/common", "primitives/consensus/grandpa", "primitives/consensus/pow", + "primitives/consensus/sassafras", "primitives/consensus/slots", "primitives/core", "primitives/core/hashing", diff --git a/primitives/consensus/sassafras/Cargo.toml b/primitives/consensus/sassafras/Cargo.toml new file mode 100644 index 0000000000000..b60aee644c6cb --- /dev/null +++ b/primitives/consensus/sassafras/Cargo.toml @@ -0,0 +1,59 @@ +[package] +name = "sp-consensus-sassafras" +version = "0.3.4-dev" +authors = ["Parity Technologies "] +description = "Primitives for Sassafras consensus" +edition = "2021" +license = "Apache-2.0" +homepage = "https://substrate.io" +repository = "https://github.com/paritytech/substrate/" +documentation = "https://docs.rs/sp-consensus-sassafras" +readme = "README.md" + +[package.metadata.docs.rs] +targets = ["x86_64-unknown-linux-gnu"] + +[dependencies] +async-trait = { version = "0.1.50", optional = true } +merlin = { version = "2.0", default-features = false } +scale-codec = { package = "parity-scale-codec", version = "3.2.2", default-features = false } +scale-info = { version = "2.5.0", default-features = false, features = ["derive"] } +serde = { version = "1.0.163", default-features = false, features = ["derive", "alloc"], optional = true } +sp-api = { version = "4.0.0-dev", default-features = false, path = "../../api" } +sp-application-crypto = { version = "23.0.0", default-features = false, path = "../../application-crypto", features = ["bandersnatch-experimental"] } +sp-consensus-slots = { version = "0.10.0-dev", default-features = false, path = "../slots" } +sp-core = { version = "21.0.0", default-features = false, path = "../../core", features = ["bandersnatch-experimental"] } +sp-inherents = { version = "4.0.0-dev", default-features = false, path = "../../inherents" } +sp-keystore = { version = "0.27.0", default-features = false, optional = true, path = "../../keystore", features = ["bandersnatch-experimental"] } +sp-runtime = { version = "24.0.0", default-features = false, path = "../../runtime" } +sp-std = { version = "8.0.0", default-features = false, path = "../../std" } +sp-timestamp = { version = "4.0.0-dev", optional = true, path = "../../timestamp" } + +[features] +default = ["std"] +std = [ + "async-trait", + "merlin/std", + "scale-codec/std", + "scale-info/std", + "serde/std", + "sp-api/std", + "sp-application-crypto/std", + "sp-consensus-slots/std", + "sp-core/std", + "sp-inherents/std", + "sp-keystore/std", + "sp-runtime/std", + "sp-std/std", + "sp-timestamp", +] + +# Serde support without relying on std features. +serde = [ + "dep:serde", + "scale-info/serde", + "sp-application-crypto/serde", + "sp-consensus-slots/serde", + "sp-core/serde", + "sp-runtime/serde", +] diff --git a/primitives/consensus/sassafras/src/digests.rs b/primitives/consensus/sassafras/src/digests.rs new file mode 100644 index 0000000000000..966220c0f83df --- /dev/null +++ b/primitives/consensus/sassafras/src/digests.rs @@ -0,0 +1,101 @@ +// This file is part of Substrate. + +// Copyright (C) 2022 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. + +//! Private implementation details of Sassafras digests. + +use super::{ + ticket::TicketClaim, AuthorityId, AuthorityIndex, AuthoritySignature, Randomness, + SassafrasEpochConfiguration, Slot, VrfSignature, SASSAFRAS_ENGINE_ID, +}; + +use scale_codec::{Decode, Encode, MaxEncodedLen}; +use scale_info::TypeInfo; + +use sp_runtime::{DigestItem, RuntimeDebug}; +use sp_std::vec::Vec; + +/// Sassafras primary slot assignment pre-digest. +#[derive(Clone, RuntimeDebug, Encode, Decode, MaxEncodedLen, TypeInfo)] +pub struct PreDigest { + /// Authority index that claimed the slot. + pub authority_idx: AuthorityIndex, + /// Corresponding slot number. + pub slot: Slot, + /// Slot claim VRF signature. + /// TODO DAVXY we can store this Signature as a Seal DigestItem + pub vrf_signature: VrfSignature, + /// Ticket auxiliary information for claim check. + pub ticket_claim: Option, +} + +/// Information about the next epoch. This is broadcast in the first block +/// of the epoch. +#[derive(Clone, PartialEq, Eq, Encode, Decode, RuntimeDebug)] +pub struct NextEpochDescriptor { + /// The authorities. + pub authorities: Vec, + /// The value of randomness to use for the slot-assignment. + pub randomness: Randomness, + /// Algorithm parameters. If not present, previous epoch parameters are used. + pub config: Option, +} + +/// An consensus log item for BABE. +#[derive(Decode, Encode, Clone, PartialEq, Eq)] +pub enum ConsensusLog { + /// The epoch has changed. This provides information about the _next_ + /// epoch - information about the _current_ epoch (i.e. the one we've just + /// entered) should already be available earlier in the chain. + #[codec(index = 1)] + NextEpochData(NextEpochDescriptor), + /// Disable the authority with given index. + #[codec(index = 2)] + OnDisabled(AuthorityIndex), +} + +/// A digest item which is usable with Sassafras consensus. +pub trait CompatibleDigestItem: Sized { + /// Construct a digest item which contains a Sassafras pre-digest. + fn sassafras_pre_digest(seal: PreDigest) -> Self; + + /// If this item is an Sassafras pre-digest, return it. + fn as_sassafras_pre_digest(&self) -> Option; + + /// Construct a digest item which contains a Sassafras seal. + fn sassafras_seal(signature: AuthoritySignature) -> Self; + + /// If this item is a Sassafras signature, return the signature. + fn as_sassafras_seal(&self) -> Option; +} + +impl CompatibleDigestItem for DigestItem { + fn sassafras_pre_digest(digest: PreDigest) -> Self { + DigestItem::PreRuntime(SASSAFRAS_ENGINE_ID, digest.encode()) + } + + fn as_sassafras_pre_digest(&self) -> Option { + self.pre_runtime_try_to(&SASSAFRAS_ENGINE_ID) + } + + fn sassafras_seal(signature: AuthoritySignature) -> Self { + DigestItem::Seal(SASSAFRAS_ENGINE_ID, signature.encode()) + } + + fn as_sassafras_seal(&self) -> Option { + self.seal_try_to(&SASSAFRAS_ENGINE_ID) + } +} diff --git a/primitives/consensus/sassafras/src/inherents.rs b/primitives/consensus/sassafras/src/inherents.rs new file mode 100644 index 0000000000000..d6254a80a16e8 --- /dev/null +++ b/primitives/consensus/sassafras/src/inherents.rs @@ -0,0 +1,102 @@ +// This file is part of Substrate. + +// Copyright (C) 2022 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. + +//! Inherents for Sassafras + +use sp_inherents::{Error, InherentData, InherentIdentifier}; +use sp_std::result::Result; + +/// The Sassafras inherent identifier. +pub const INHERENT_IDENTIFIER: InherentIdentifier = *b"sassslot"; + +/// The type of the Sassafras inherent. +pub type InherentType = sp_consensus_slots::Slot; + +/// Auxiliary trait to extract Sassafras inherent data. +pub trait SassafrasInherentData { + /// Get Sassafras inherent data. + fn sassafras_inherent_data(&self) -> Result, Error>; + /// Replace Sassafras inherent data. + fn sassafras_replace_inherent_data(&mut self, new: InherentType); +} + +impl SassafrasInherentData for InherentData { + fn sassafras_inherent_data(&self) -> Result, Error> { + self.get_data(&INHERENT_IDENTIFIER) + } + + fn sassafras_replace_inherent_data(&mut self, new: InherentType) { + self.replace_data(INHERENT_IDENTIFIER, &new); + } +} + +/// Provides the slot duration inherent data for Sassafras. +// TODO: Remove in the future. https://github.com/paritytech/substrate/issues/8029 +#[cfg(feature = "std")] +pub struct InherentDataProvider { + slot: InherentType, +} + +#[cfg(feature = "std")] +impl InherentDataProvider { + /// Create new inherent data provider from the given `slot`. + pub fn new(slot: InherentType) -> Self { + Self { slot } + } + + /// Creates the inherent data provider by calculating the slot from the given + /// `timestamp` and `duration`. + pub fn from_timestamp_and_slot_duration( + timestamp: sp_timestamp::Timestamp, + slot_duration: sp_consensus_slots::SlotDuration, + ) -> Self { + let slot = InherentType::from_timestamp(timestamp, slot_duration); + + Self { slot } + } + + /// Returns the `slot` of this inherent data provider. + pub fn slot(&self) -> InherentType { + self.slot + } +} + +#[cfg(feature = "std")] +impl sp_std::ops::Deref for InherentDataProvider { + type Target = InherentType; + + fn deref(&self) -> &Self::Target { + &self.slot + } +} + +#[cfg(feature = "std")] +#[async_trait::async_trait] +impl sp_inherents::InherentDataProvider for InherentDataProvider { + async fn provide_inherent_data(&self, inherent_data: &mut InherentData) -> Result<(), Error> { + inherent_data.put_data(INHERENT_IDENTIFIER, &self.slot) + } + + async fn try_handle_error( + &self, + _: &InherentIdentifier, + _: &[u8], + ) -> Option> { + // There is no error anymore + None + } +} diff --git a/primitives/consensus/sassafras/src/lib.rs b/primitives/consensus/sassafras/src/lib.rs new file mode 100644 index 0000000000000..c3df3ab675bcf --- /dev/null +++ b/primitives/consensus/sassafras/src/lib.rs @@ -0,0 +1,193 @@ +// This file is part of Substrate. + +// Copyright (C) 2022 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. + +//! Primitives for Sassafras +//! TODO-SASS-P2 : write proper docs + +// TODO davxy enable warnings +// #![deny(warnings)] +// #![forbid(unsafe_code, missing_docs, unused_variables, unused_imports)] +#![cfg_attr(not(feature = "std"), no_std)] + +use scale_codec::{Decode, Encode, MaxEncodedLen}; +use scale_info::TypeInfo; +#[cfg(feature = "serde")] +use serde::{Deserialize, Serialize}; +use sp_core::crypto::KeyTypeId; +use sp_runtime::{ConsensusEngineId, RuntimeDebug}; +use sp_std::vec::Vec; + +pub use sp_consensus_slots::{Slot, SlotDuration}; +pub use sp_core::bandersnatch::{ + ring_vrf::{RingContext, RingProver, RingVerifier, RingVrfSignature}, + vrf::{VrfInput, VrfOutput, VrfSignData, VrfSignature}, +}; + +pub mod digests; +pub mod inherents; +pub mod ticket; + +pub use ticket::{ + slot_claim_sign_data, slot_claim_vrf_input, ticket_body_sign_data, ticket_id, + ticket_id_threshold, ticket_id_vrf_input, TicketBody, TicketClaim, TicketEnvelope, TicketId, + TicketSecret, +}; + +mod app { + use sp_application_crypto::{app_crypto, bandersnatch, key_types::SASSAFRAS}; + app_crypto!(bandersnatch, SASSAFRAS); +} + +/// Key type for Sassafras protocol. +pub const KEY_TYPE: KeyTypeId = sp_application_crypto::key_types::SASSAFRAS; + +/// Consensus engine identifier. +pub const SASSAFRAS_ENGINE_ID: ConsensusEngineId = *b"SASS"; + +/// VRF output length for per-slot randomness. +pub const RANDOMNESS_LENGTH: usize = 32; + +/// The index of an authority. +pub type AuthorityIndex = u32; + +/// Sassafras authority keypair. Necessarily equivalent to the schnorrkel public key used in +/// the main Sassafras module. If that ever changes, then this must, too. +#[cfg(feature = "std")] +pub type AuthorityPair = app::Pair; + +/// Sassafras authority signature. +pub type AuthoritySignature = app::Signature; + +/// Sassafras authority identifier. Necessarily equivalent to the schnorrkel public key used in +/// the main Sassafras module. If that ever changes, then this must, too. +pub type AuthorityId = app::Public; + +/// Weight of a Sassafras block. +/// Primary blocks have a weight of 1 whereas secondary blocks have a weight of 0. +pub type SassafrasBlockWeight = u32; + +/// An equivocation proof for multiple block authorships on the same slot (i.e. double vote). +pub type EquivocationProof = sp_consensus_slots::EquivocationProof; + +/// Randomness required by some SASSAFRAS operations. +pub type Randomness = [u8; RANDOMNESS_LENGTH]; + +/// Configuration data used by the Sassafras consensus engine. +#[derive(Clone, Encode, Decode, RuntimeDebug, PartialEq, Eq, TypeInfo)] +pub struct SassafrasConfiguration { + /// The slot duration in milliseconds. + pub slot_duration: u64, + /// The duration of epoch in slots. + pub epoch_duration: u64, + /// The authorities for the epoch. + pub authorities: Vec, + /// The randomness for the epoch. + pub randomness: Randomness, + /// Tickets threshold parameters. + pub threshold_params: SassafrasEpochConfiguration, +} + +impl SassafrasConfiguration { + /// Get the slot duration defined in the genesis configuration. + pub fn slot_duration(&self) -> SlotDuration { + SlotDuration::from_millis(self.slot_duration) + } +} + +/// Sassafras epoch information +#[derive(Encode, Decode, PartialEq, Eq, Clone, Debug, TypeInfo)] +pub struct Epoch { + /// The epoch index. + pub epoch_idx: u64, + /// The starting slot of the epoch. + pub start_slot: Slot, + /// Epoch configuration. + pub config: SassafrasConfiguration, +} + +/// Configuration data used by the Sassafras consensus engine that can be modified on epoch change. +// TODO-SASS-P3: rename to something better... like LotteryConfig +#[derive(Clone, PartialEq, Eq, Encode, Decode, RuntimeDebug, MaxEncodedLen, TypeInfo, Default)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +pub struct SassafrasEpochConfiguration { + /// Redundancy factor. + pub redundancy_factor: u32, + /// Number of attempts for tickets generation. + pub attempts_number: u32, +} + +/// An opaque type used to represent the key ownership proof at the runtime API boundary. +/// The inner value is an encoded representation of the actual key ownership proof which will be +/// parameterized when defining the runtime. At the runtime API boundary this type is unknown and +/// as such we keep this opaque representation, implementors of the runtime API will have to make +/// sure that all usages of `OpaqueKeyOwnershipProof` refer to the same type. +#[derive(Decode, Encode, PartialEq, TypeInfo)] +pub struct OpaqueKeyOwnershipProof(Vec); + +// Runtime API. +sp_api::decl_runtime_apis! { + /// API necessary for block authorship with Sassafras. + pub trait SassafrasApi { + /// Get ring context to be used for ticket construction and verification. + fn ring_context() -> Option; + + /// Submit next epoch validator tickets via an unsigned extrinsic. + /// This method returns `false` when creation of the extrinsics fails. + fn submit_tickets_unsigned_extrinsic(tickets: Vec) -> bool; + + /// Get ticket id associated to the given slot. + fn slot_ticket_id(slot: Slot) -> Option; + + /// Get ticket id and data associated to the given slot. + fn slot_ticket(slot: Slot) -> Option<(TicketId, TicketBody)>; + + /// Current epoch information. + fn current_epoch() -> Epoch; + + /// Next epoch information. + fn next_epoch() -> Epoch; + + /// Generates a proof of key ownership for the given authority in the current epoch. + /// + /// An example usage of this module is coupled with the session historical module to prove + /// that a given authority key is tied to a given staking identity during a specific + /// session. Proofs of key ownership are necessary for submitting equivocation reports. + /// + /// NOTE: even though the API takes a `slot` as parameter the current implementations + /// ignores this parameter and instead relies on this method being called at the correct + /// block height, i.e. any point at which the epoch for the given slot is live on-chain. + /// Future implementations will instead use indexed data through an offchain worker, not + /// requiring older states to be available. + fn generate_key_ownership_proof( + slot: Slot, + authority_id: AuthorityId, + ) -> Option; + + /// Submits an unsigned extrinsic to report an equivocation. + /// + /// The caller must provide the equivocation proof and a key ownership proof (should be + /// obtained using `generate_key_ownership_proof`). The extrinsic will be unsigned and + /// should only be accepted for local authorship (not to be broadcast to the network). This + /// method returns `None` when creation of the extrinsic fails, e.g. if equivocation + /// reporting is disabled for the given runtime (i.e. this method is hardcoded to return + /// `None`). Only useful in an offchain context. + fn submit_report_equivocation_unsigned_extrinsic( + equivocation_proof: EquivocationProof, + key_owner_proof: OpaqueKeyOwnershipProof, + ) -> bool; + } +} diff --git a/primitives/consensus/sassafras/src/ticket.rs b/primitives/consensus/sassafras/src/ticket.rs new file mode 100644 index 0000000000000..2bd24a8b16a28 --- /dev/null +++ b/primitives/consensus/sassafras/src/ticket.rs @@ -0,0 +1,149 @@ +// This file is part of Substrate. + +// Copyright (C) 2022 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. + +//! Primitives related to tickets. + +use crate::{Randomness, RingVrfSignature, VrfInput, VrfOutput, VrfSignData, SASSAFRAS_ENGINE_ID}; +use scale_codec::{Decode, Encode, MaxEncodedLen}; +use scale_info::TypeInfo; +use sp_consensus_slots::Slot; +use sp_std::vec::Vec; + +/// Ticket identifier. +/// +/// Within the algorithm this is also used as a ticket score applied to bound +/// the ticket to a epoch's slot. +pub type TicketId = u128; + +/// Ticket data persisted on-chain. +#[derive(Debug, Clone, PartialEq, Eq, Encode, Decode, MaxEncodedLen, TypeInfo)] +pub struct TicketBody { + /// Attempt index. + pub attempt_idx: u32, + /// Ed25519 public key which gets erased when claiming the ticket. + pub erased_public: [u8; 32], +} + +/// Ticket ring vrf signature. +pub type TicketRingSignature = RingVrfSignature; + +/// Ticket envelope used on during submission. +#[derive(Debug, Clone, PartialEq, Eq, Encode, Decode, MaxEncodedLen, TypeInfo)] +pub struct TicketEnvelope { + /// Ticket body. + pub body: TicketBody, + /// Ring signature. + pub ring_signature: TicketRingSignature, +} + +/// Ticket auxiliary information used to claim the ticket ownership. +#[derive(Debug, Clone, PartialEq, Eq, Encode, Decode, MaxEncodedLen, TypeInfo)] +pub struct TicketSecret { + /// Attempt index. + pub attempt_idx: u32, + /// Ed25519 used to claim ticket ownership. + pub erased_secret: [u8; 32], +} + +/// Ticket claim information filled by the block author. +#[derive(Debug, Clone, PartialEq, Eq, Encode, Decode, MaxEncodedLen, TypeInfo)] +pub struct TicketClaim { + pub erased_signature: [u8; 64], +} + +fn vrf_input_from_data( + domain: &[u8], + data: impl IntoIterator>, +) -> VrfInput { + let raw = data.into_iter().fold(Vec::new(), |mut v, e| { + let bytes = e.as_ref(); + v.extend_from_slice(bytes); + let len = u8::try_from(bytes.len()).expect("private function with well known inputs; qed"); + v.extend_from_slice(&len.to_le_bytes()); + v + }); + VrfInput::new(domain, raw) +} + +/// VRF input to claim slot ownership during block production. +/// +/// Input randomness is current epoch randomness. +pub fn slot_claim_vrf_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()], + ) +} + +/// Signing-data to claim slot ownership during block production. +/// +/// Input randomness is current epoch randomness. +pub fn slot_claim_sign_data(randomness: &Randomness, slot: Slot, epoch: u64) -> VrfSignData { + let vrf_input = slot_claim_vrf_input(randomness, slot, epoch); + VrfSignData::new_unchecked(&SASSAFRAS_ENGINE_ID, Some("slot-claim-transcript"), Some(vrf_input)) +} + +/// VRF input to generate the ticket id. +/// +/// Input randomness is current epoch randomness. +pub fn ticket_id_vrf_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()], + ) +} + +/// Data to be signed via ring-vrf. +/// TODO davxy: ticket_body is not a vrf input??? +pub fn ticket_body_sign_data(ticket_body: &TicketBody) -> VrfSignData { + VrfSignData::new_unchecked( + &SASSAFRAS_ENGINE_ID, + &[b"ticket-body-transcript", ticket_body.encode().as_slice()], + [], + ) +} + +/// Get ticket-id for a given vrf input and output. +/// +/// Input generally obtained via `ticket_id_vrf_input`. +/// Output can be obtained directly using the vrf secret key or from the signature. +pub fn ticket_id(vrf_input: &VrfInput, vrf_output: &VrfOutput) -> TicketId { + let bytes = vrf_output.make_bytes::<16>(b"vrf-out", vrf_input); + u128::from_le_bytes(bytes) +} + +/// Computes the threshold for a given epoch as T = (x*s)/(a*v), where: +/// - x: redundancy factor; +/// - s: number of slots in epoch; +/// - a: max number of attempts; +/// - v: number of validator in epoch. +/// The parameters should be chosen such that T <= 1. +/// If `attempts * validators` is zero then we fallback to T = 0 +// TODO-SASS-P3: this formula must be double-checked... +pub fn ticket_id_threshold( + redundancy: u32, + slots: u32, + attempts: u32, + validators: u32, +) -> TicketId { + let den = attempts as u64 * validators as u64; + let num = redundancy as u64 * slots as u64; + TicketId::max_value() + .checked_div(den.into()) + .unwrap_or_default() + .saturating_mul(num.into()) +} diff --git a/primitives/core/src/bandersnatch.rs b/primitives/core/src/bandersnatch.rs index c3ba7f41058e9..d87b3ee232df9 100644 --- a/primitives/core/src/bandersnatch.rs +++ b/primitives/core/src/bandersnatch.rs @@ -31,7 +31,7 @@ use crate::crypto::{DeriveError, DeriveJunction, Pair as TraitPair, SecretString use bandersnatch_vrfs::CanonicalSerialize; #[cfg(feature = "full_crypto")] use bandersnatch_vrfs::SecretKey; -use codec::{Decode, Encode, MaxEncodedLen}; +use codec::{Decode, Encode, EncodeLike, MaxEncodedLen}; use scale_info::TypeInfo; use sp_runtime_interface::pass_by::PassByInner; @@ -294,7 +294,7 @@ impl TraitPair for Pair { fn verify>(signature: &Signature, data: M, public: &Public) -> bool { let data = vrf::VrfSignData::new_unchecked(SIGNING_CTX, &[data.as_ref()], None); let signature = - vrf::VrfSignature { signature: *signature, vrf_outputs: vrf::VrfIosVec::default() }; + vrf::VrfSignature { signature: *signature, outputs: vrf::VrfIosVec::default() }; public.vrf_verify(&data, &signature) } @@ -463,7 +463,7 @@ pub mod vrf { #[derive(Clone, Debug, PartialEq, Eq, Encode, Decode, MaxEncodedLen, TypeInfo)] pub struct VrfSignature { /// VRF (pre)outputs. - pub vrf_outputs: VrfIosVec, + pub outputs: VrfIosVec, /// VRF signature. pub signature: Signature, } @@ -506,12 +506,12 @@ pub mod vrf { impl VrfPublic for Public { fn vrf_verify(&self, data: &Self::VrfSignData, signature: &Self::VrfSignature) -> bool { const _: () = assert!(MAX_VRF_IOS == 3, "`MAX_VRF_IOS` expected to be 3"); - let preouts_len = signature.vrf_outputs.len(); - if preouts_len != data.vrf_inputs.len() { + let outputs_len = signature.outputs.len(); + if outputs_len != data.vrf_inputs.len() { return false } // Workaround to overcome backend signature generic over the number of IOs. - match preouts_len { + match outputs_len { 0 => self.vrf_verify_gen::<0>(data, signature), 1 => self.vrf_verify_gen::<1>(data, signature), 2 => self.vrf_verify_gen::<2>(data, signature), @@ -541,7 +541,7 @@ pub mod vrf { let outputs: Vec<_> = signature.preoutputs.into_iter().map(VrfOutput).collect(); let outputs = VrfIosVec::truncate_from(outputs); - VrfSignature { signature: Signature(sign_bytes), vrf_outputs: outputs } + VrfSignature { signature: Signature(sign_bytes), outputs } } /// Generate an arbitrary number of bytes from the given `context` and VRF `input`. @@ -567,7 +567,7 @@ pub mod vrf { }; let Ok(preouts) = signature - .vrf_outputs + .outputs .iter() .map(|o| o.0.clone()) .collect::>() @@ -675,6 +675,8 @@ pub mod ring_vrf { } } + impl EncodeLike for RingContext {} + impl MaxEncodedLen for RingContext { fn max_encoded_len() -> usize { <[u8; RING_CONTEXT_SERIALIZED_LEN]>::max_encoded_len() @@ -910,11 +912,11 @@ mod tests { let signature = pair.vrf_sign(&data); let o10 = pair.make_bytes::<32>(b"ctx1", &i1); - let o11 = signature.vrf_outputs[0].make_bytes::<32>(b"ctx1", &i1); + let o11 = signature.outputs[0].make_bytes::<32>(b"ctx1", &i1); assert_eq!(o10, o11); let o20 = pair.make_bytes::<48>(b"ctx2", &i2); - let o21 = signature.vrf_outputs[1].make_bytes::<48>(b"ctx2", &i2); + let o21 = signature.outputs[1].make_bytes::<48>(b"ctx2", &i2); assert_eq!(o20, o21); } @@ -993,6 +995,35 @@ mod tests { assert!(!signature.verify(&data, &verifier)); } + #[test] + fn ring_vrf_make_bytes_matches() { + let ring_ctx = RingContext::new_testing(); + + let mut pks: Vec<_> = (0..16).map(|i| Pair::from_seed(&[i as u8; 32]).public()).collect(); + assert!(pks.len() <= ring_ctx.max_keyset_size()); + + let pair = Pair::from_seed(DEV_SEED); + + // Just pick one index to patch with the actual public key + let prover_idx = 3; + pks[prover_idx] = pair.public(); + + let i1 = VrfInput::new(b"dom1", b"foo"); + let i2 = VrfInput::new(b"dom2", b"bar"); + let data = VrfSignData::new_unchecked(b"mydata", &[b"tdata"], [i1.clone(), i2.clone()]); + + let prover = ring_ctx.prover(&pks, prover_idx).unwrap(); + let signature = pair.ring_vrf_sign(&data, &prover); + + let o10 = pair.make_bytes::<32>(b"ctx1", &i1); + let o11 = signature.outputs[0].make_bytes::<32>(b"ctx1", &i1); + assert_eq!(o10, o11); + + let o20 = pair.make_bytes::<48>(b"ctx2", &i2); + let o21 = signature.outputs[1].make_bytes::<48>(b"ctx2", &i2); + assert_eq!(o20, o21); + } + #[test] fn encode_decode_ring_vrf_signature() { let ring_ctx = RingContext::new_testing(); diff --git a/primitives/core/src/crypto.rs b/primitives/core/src/crypto.rs index 6afe4b752a690..8c7d98f00cd89 100644 --- a/primitives/core/src/crypto.rs +++ b/primitives/core/src/crypto.rs @@ -1136,6 +1136,8 @@ pub mod key_types { /// Key type for Babe module, built-in. Identified as `babe`. pub const BABE: KeyTypeId = KeyTypeId(*b"babe"); + /// Key type for Sassafras module, built-in. Identified as `sass`. + pub const SASSAFRAS: KeyTypeId = KeyTypeId(*b"sass"); /// Key type for Grandpa module, built-in. Identified as `gran`. pub const GRANDPA: KeyTypeId = KeyTypeId(*b"gran"); /// Key type for controlling an account in a Substrate runtime, built-in. Identified as `acco`. From 1cd3c18e68355d3b7a4fa569cf7b5ca9c317e6b8 Mon Sep 17 00:00:00 2001 From: Davide Galassi Date: Sat, 12 Aug 2023 18:39:25 +0200 Subject: [PATCH 02/22] Primitives README --- primitives/consensus/sassafras/Cargo.toml | 1 + primitives/consensus/sassafras/README.md | 10 ++++++++++ 2 files changed, 11 insertions(+) create mode 100644 primitives/consensus/sassafras/README.md diff --git a/primitives/consensus/sassafras/Cargo.toml b/primitives/consensus/sassafras/Cargo.toml index b60aee644c6cb..79137022f7086 100644 --- a/primitives/consensus/sassafras/Cargo.toml +++ b/primitives/consensus/sassafras/Cargo.toml @@ -9,6 +9,7 @@ homepage = "https://substrate.io" repository = "https://github.com/paritytech/substrate/" documentation = "https://docs.rs/sp-consensus-sassafras" readme = "README.md" +publish = false [package.metadata.docs.rs] targets = ["x86_64-unknown-linux-gnu"] diff --git a/primitives/consensus/sassafras/README.md b/primitives/consensus/sassafras/README.md new file mode 100644 index 0000000000000..9875b725b8f75 --- /dev/null +++ b/primitives/consensus/sassafras/README.md @@ -0,0 +1,10 @@ +Primitives for SASSAFRAS. + +# ⚠️ WARNING ⚠️ + +The crate interfaces and structures are highly experimental and may be subject +to significant changes. + +These structs were mostly extracted from the main SASSAFRAS protocol PR: https://github.com/paritytech/substrate/pull/11879. + +Tracking issue: https://github.com/paritytech/substrate/issues/11515. From 382b8751a30404b6ccea4d2e4ccdb1aa910371dc Mon Sep 17 00:00:00 2001 From: Davide Galassi Date: Sat, 12 Aug 2023 18:42:41 +0200 Subject: [PATCH 03/22] README update --- primitives/consensus/sassafras/README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/primitives/consensus/sassafras/README.md b/primitives/consensus/sassafras/README.md index 9875b725b8f75..f632ce5ba534d 100644 --- a/primitives/consensus/sassafras/README.md +++ b/primitives/consensus/sassafras/README.md @@ -5,6 +5,8 @@ Primitives for SASSAFRAS. The crate interfaces and structures are highly experimental and may be subject to significant changes. +Depends on upstream experimental feature: `bandersnatch-experimental`. + These structs were mostly extracted from the main SASSAFRAS protocol PR: https://github.com/paritytech/substrate/pull/11879. Tracking issue: https://github.com/paritytech/substrate/issues/11515. From f178ecf0e202dfad1871f731c5abdd681fd79e8f Mon Sep 17 00:00:00 2001 From: Davide Galassi Date: Sat, 12 Aug 2023 18:51:49 +0200 Subject: [PATCH 04/22] Dependencies cleanup --- Cargo.lock | 3 --- primitives/consensus/sassafras/Cargo.toml | 7 ------- primitives/keystore/src/lib.rs | 1 + 3 files changed, 1 insertion(+), 10 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 0fb1366dd5d08..5e5bab1061668 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -10741,16 +10741,13 @@ name = "sp-consensus-sassafras" version = "0.3.4-dev" dependencies = [ "async-trait", - "merlin 2.0.1", "parity-scale-codec", "scale-info", - "serde", "sp-api", "sp-application-crypto", "sp-consensus-slots", "sp-core", "sp-inherents", - "sp-keystore", "sp-runtime", "sp-std", "sp-timestamp", diff --git a/primitives/consensus/sassafras/Cargo.toml b/primitives/consensus/sassafras/Cargo.toml index 79137022f7086..494074a2b64a5 100644 --- a/primitives/consensus/sassafras/Cargo.toml +++ b/primitives/consensus/sassafras/Cargo.toml @@ -16,16 +16,13 @@ targets = ["x86_64-unknown-linux-gnu"] [dependencies] async-trait = { version = "0.1.50", optional = true } -merlin = { version = "2.0", default-features = false } scale-codec = { package = "parity-scale-codec", version = "3.2.2", default-features = false } scale-info = { version = "2.5.0", default-features = false, features = ["derive"] } -serde = { version = "1.0.163", default-features = false, features = ["derive", "alloc"], optional = true } sp-api = { version = "4.0.0-dev", default-features = false, path = "../../api" } sp-application-crypto = { version = "23.0.0", default-features = false, path = "../../application-crypto", features = ["bandersnatch-experimental"] } sp-consensus-slots = { version = "0.10.0-dev", default-features = false, path = "../slots" } sp-core = { version = "21.0.0", default-features = false, path = "../../core", features = ["bandersnatch-experimental"] } sp-inherents = { version = "4.0.0-dev", default-features = false, path = "../../inherents" } -sp-keystore = { version = "0.27.0", default-features = false, optional = true, path = "../../keystore", features = ["bandersnatch-experimental"] } sp-runtime = { version = "24.0.0", default-features = false, path = "../../runtime" } sp-std = { version = "8.0.0", default-features = false, path = "../../std" } sp-timestamp = { version = "4.0.0-dev", optional = true, path = "../../timestamp" } @@ -34,16 +31,13 @@ sp-timestamp = { version = "4.0.0-dev", optional = true, path = "../../timestamp default = ["std"] std = [ "async-trait", - "merlin/std", "scale-codec/std", "scale-info/std", - "serde/std", "sp-api/std", "sp-application-crypto/std", "sp-consensus-slots/std", "sp-core/std", "sp-inherents/std", - "sp-keystore/std", "sp-runtime/std", "sp-std/std", "sp-timestamp", @@ -51,7 +45,6 @@ std = [ # Serde support without relying on std features. serde = [ - "dep:serde", "scale-info/serde", "sp-application-crypto/serde", "sp-consensus-slots/serde", diff --git a/primitives/keystore/src/lib.rs b/primitives/keystore/src/lib.rs index 82062fe7b40a7..b388362ecb898 100644 --- a/primitives/keystore/src/lib.rs +++ b/primitives/keystore/src/lib.rs @@ -17,6 +17,7 @@ //! Keystore traits +#[cfg(feature = "std")] pub mod testing; #[cfg(feature = "bandersnatch-experimental")] From 963cb0ce81810331d2015a381d58c44d280e5c8d Mon Sep 17 00:00:00 2001 From: Davide Galassi Date: Sat, 12 Aug 2023 19:01:50 +0200 Subject: [PATCH 05/22] Remove leftovers --- primitives/consensus/sassafras/src/digests.rs | 3 +-- primitives/consensus/sassafras/src/ticket.rs | 1 - 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/primitives/consensus/sassafras/src/digests.rs b/primitives/consensus/sassafras/src/digests.rs index 966220c0f83df..09f8d8abb97be 100644 --- a/primitives/consensus/sassafras/src/digests.rs +++ b/primitives/consensus/sassafras/src/digests.rs @@ -28,7 +28,7 @@ use scale_info::TypeInfo; use sp_runtime::{DigestItem, RuntimeDebug}; use sp_std::vec::Vec; -/// Sassafras primary slot assignment pre-digest. +/// Sassafras slot assignment pre-digest. #[derive(Clone, RuntimeDebug, Encode, Decode, MaxEncodedLen, TypeInfo)] pub struct PreDigest { /// Authority index that claimed the slot. @@ -36,7 +36,6 @@ pub struct PreDigest { /// Corresponding slot number. pub slot: Slot, /// Slot claim VRF signature. - /// TODO DAVXY we can store this Signature as a Seal DigestItem pub vrf_signature: VrfSignature, /// Ticket auxiliary information for claim check. pub ticket_claim: Option, diff --git a/primitives/consensus/sassafras/src/ticket.rs b/primitives/consensus/sassafras/src/ticket.rs index 2bd24a8b16a28..1d1f161b3718c 100644 --- a/primitives/consensus/sassafras/src/ticket.rs +++ b/primitives/consensus/sassafras/src/ticket.rs @@ -108,7 +108,6 @@ pub fn ticket_id_vrf_input(randomness: &Randomness, attempt: u32, epoch: u64) -> } /// Data to be signed via ring-vrf. -/// TODO davxy: ticket_body is not a vrf input??? pub fn ticket_body_sign_data(ticket_body: &TicketBody) -> VrfSignData { VrfSignData::new_unchecked( &SASSAFRAS_ENGINE_ID, From f6af20baf223524b9ccaa84147a70a8805682235 Mon Sep 17 00:00:00 2001 From: Davide Galassi Date: Sat, 12 Aug 2023 20:14:42 +0200 Subject: [PATCH 06/22] Gate sassafras behind optional feature --- Cargo.lock | 2 ++ Cargo.toml | 16 +++++++++++++++- 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/Cargo.lock b/Cargo.lock index 5e5bab1061668..e28f00b8373a8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -11529,7 +11529,9 @@ dependencies = [ "sc-consensus-grandpa", "sc-consensus-manual-seal", "sc-consensus-pow", + "sc-keystore", "sc-service", + "sp-consensus-sassafras", "sp-runtime", "subkey", ] diff --git a/Cargo.toml b/Cargo.toml index 4743f1b30da54..22bdcd4c44f1c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -29,6 +29,21 @@ frame-support = { path = "frame/support" } node-cli = { path = "bin/node/cli" } +# This list of dependencies is necessary to build sassafras primitives +# `bandersnatch-experimental` is transitifely passed between: +# sp-consensus-sassafras → sp-application-crypto → sp-io → sp-keystore +# This last thus defines the bandersnatch functions for its trait `Keystore`. +# We end up requiring bandersnatch implementations in the client keystore as well. +sp-consensus-sassafras = { path = "primitives/consensus/sassafras", optional = true } +sc-keystore = { path = "client/keystore" } + +[features] +sassafras = [ + "sp-consensus-sassafras", + "sc-keystore/bandersnatch-experimental", +] + + # Exists here to be backwards compatible and to support `cargo run` in the workspace. # # Just uses the `node-cli` main binary. `node-cli` itself also again exposes the node as @@ -243,7 +258,6 @@ members = [ "primitives/consensus/common", "primitives/consensus/grandpa", "primitives/consensus/pow", - "primitives/consensus/sassafras", "primitives/consensus/slots", "primitives/core", "primitives/core/hashing", From a99b39f61c77abf4708db1cc47d9fc889e17faf3 Mon Sep 17 00:00:00 2001 From: Davide Galassi Date: Sat, 12 Aug 2023 20:21:38 +0200 Subject: [PATCH 07/22] Rename sassafras-experimental feature --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 22bdcd4c44f1c..d063f9f3c6426 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -38,7 +38,7 @@ sp-consensus-sassafras = { path = "primitives/consensus/sassafras", optional = t sc-keystore = { path = "client/keystore" } [features] -sassafras = [ +sassafras-experimental = [ "sp-consensus-sassafras", "sc-keystore/bandersnatch-experimental", ] From 989960703960c971b5334173103c14375b1fe9b8 Mon Sep 17 00:00:00 2001 From: Davide Galassi Date: Sat, 12 Aug 2023 20:23:19 +0200 Subject: [PATCH 08/22] Remove copyright year --- primitives/consensus/sassafras/src/digests.rs | 2 +- primitives/consensus/sassafras/src/inherents.rs | 2 +- primitives/consensus/sassafras/src/lib.rs | 2 +- primitives/consensus/sassafras/src/ticket.rs | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/primitives/consensus/sassafras/src/digests.rs b/primitives/consensus/sassafras/src/digests.rs index 09f8d8abb97be..dd2e9cb9efc3e 100644 --- a/primitives/consensus/sassafras/src/digests.rs +++ b/primitives/consensus/sassafras/src/digests.rs @@ -1,6 +1,6 @@ // This file is part of Substrate. -// Copyright (C) 2022 Parity Technologies (UK) Ltd. +// Copyright (C) Parity Technologies (UK) Ltd. // SPDX-License-Identifier: Apache-2.0 // Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/primitives/consensus/sassafras/src/inherents.rs b/primitives/consensus/sassafras/src/inherents.rs index d6254a80a16e8..3183afee8ab84 100644 --- a/primitives/consensus/sassafras/src/inherents.rs +++ b/primitives/consensus/sassafras/src/inherents.rs @@ -1,6 +1,6 @@ // This file is part of Substrate. -// Copyright (C) 2022 Parity Technologies (UK) Ltd. +// Copyright (C) Parity Technologies (UK) Ltd. // SPDX-License-Identifier: Apache-2.0 // Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/primitives/consensus/sassafras/src/lib.rs b/primitives/consensus/sassafras/src/lib.rs index c3df3ab675bcf..334e2bd4824f2 100644 --- a/primitives/consensus/sassafras/src/lib.rs +++ b/primitives/consensus/sassafras/src/lib.rs @@ -1,6 +1,6 @@ // This file is part of Substrate. -// Copyright (C) 2022 Parity Technologies (UK) Ltd. +// Copyright (C) Parity Technologies (UK) Ltd. // SPDX-License-Identifier: Apache-2.0 // Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/primitives/consensus/sassafras/src/ticket.rs b/primitives/consensus/sassafras/src/ticket.rs index 1d1f161b3718c..46b614a6d0fc6 100644 --- a/primitives/consensus/sassafras/src/ticket.rs +++ b/primitives/consensus/sassafras/src/ticket.rs @@ -1,6 +1,6 @@ // This file is part of Substrate. -// Copyright (C) 2022 Parity Technologies (UK) Ltd. +// Copyright (C) Parity Technologies (UK) Ltd. // SPDX-License-Identifier: Apache-2.0 // Licensed under the Apache License, Version 2.0 (the "License"); From 517b3c9411fbff4396b174d81a4778cef0efa03d Mon Sep 17 00:00:00 2001 From: Davide Galassi Date: Sat, 12 Aug 2023 20:34:22 +0200 Subject: [PATCH 09/22] Refine digest module --- primitives/consensus/sassafras/src/digests.rs | 33 +++++++++---------- 1 file changed, 16 insertions(+), 17 deletions(-) diff --git a/primitives/consensus/sassafras/src/digests.rs b/primitives/consensus/sassafras/src/digests.rs index dd2e9cb9efc3e..6df16a2ce4178 100644 --- a/primitives/consensus/sassafras/src/digests.rs +++ b/primitives/consensus/sassafras/src/digests.rs @@ -17,7 +17,7 @@ //! Private implementation details of Sassafras digests. -use super::{ +use crate::{ ticket::TicketClaim, AuthorityId, AuthorityIndex, AuthoritySignature, Randomness, SassafrasEpochConfiguration, Slot, VrfSignature, SASSAFRAS_ENGINE_ID, }; @@ -41,43 +41,42 @@ pub struct PreDigest { pub ticket_claim: Option, } -/// Information about the next epoch. This is broadcast in the first block -/// of the epoch. +/// Information about the next epoch. +/// +/// This is broadcast in the first block of each epoch. #[derive(Clone, PartialEq, Eq, Encode, Decode, RuntimeDebug)] pub struct NextEpochDescriptor { - /// The authorities. + /// Authorities list. pub authorities: Vec, - /// The value of randomness to use for the slot-assignment. + /// Epoch randomness. pub randomness: Randomness, - /// Algorithm parameters. If not present, previous epoch parameters are used. + /// Mutable protocol parameters. If not present previous epoch parameters are used. pub config: Option, } -/// An consensus log item for BABE. +/// Consensus log item. #[derive(Decode, Encode, Clone, PartialEq, Eq)] pub enum ConsensusLog { - /// The epoch has changed. This provides information about the _next_ - /// epoch - information about the _current_ epoch (i.e. the one we've just - /// entered) should already be available earlier in the chain. + /// Provides information about the next epoch parameters. #[codec(index = 1)] NextEpochData(NextEpochDescriptor), - /// Disable the authority with given index. + /// Disable the authority with given index (@davxy: todo). #[codec(index = 2)] OnDisabled(AuthorityIndex), } -/// A digest item which is usable with Sassafras consensus. -pub trait CompatibleDigestItem: Sized { - /// Construct a digest item which contains a Sassafras pre-digest. +/// A digest item which is usable by Sassafras. +pub trait CompatibleDigestItem { + /// Construct a digest item which contains a `PreDigest`. fn sassafras_pre_digest(seal: PreDigest) -> Self; - /// If this item is an Sassafras pre-digest, return it. + /// If this item is a `PreDigest`, return it. fn as_sassafras_pre_digest(&self) -> Option; - /// Construct a digest item which contains a Sassafras seal. + /// Construct a digest item which contains an `AuthoritySignature`. fn sassafras_seal(signature: AuthoritySignature) -> Self; - /// If this item is a Sassafras signature, return the signature. + /// If this item is an `AuthoritySignature`, return it. fn as_sassafras_seal(&self) -> Option; } From cf1d0bf25deee42dc34b888ab89ef132f888b609 Mon Sep 17 00:00:00 2001 From: Davide Galassi Date: Sun, 13 Aug 2023 15:49:09 +0200 Subject: [PATCH 10/22] Cleanup iteration --- primitives/consensus/sassafras/src/digests.rs | 8 +- .../consensus/sassafras/src/inherents.rs | 60 ++++------ primitives/consensus/sassafras/src/lib.rs | 104 ++++++------------ primitives/consensus/sassafras/src/ticket.rs | 28 ++--- primitives/inherents/src/client_side.rs | 8 +- 5 files changed, 77 insertions(+), 131 deletions(-) diff --git a/primitives/consensus/sassafras/src/digests.rs b/primitives/consensus/sassafras/src/digests.rs index 6df16a2ce4178..3d063445facfa 100644 --- a/primitives/consensus/sassafras/src/digests.rs +++ b/primitives/consensus/sassafras/src/digests.rs @@ -15,11 +15,11 @@ // See the License for the specific language governing permissions and // limitations under the License. -//! Private implementation details of Sassafras digests. +//! Sassafras digests structures and helpers. use crate::{ - ticket::TicketClaim, AuthorityId, AuthorityIndex, AuthoritySignature, Randomness, - SassafrasEpochConfiguration, Slot, VrfSignature, SASSAFRAS_ENGINE_ID, + ticket::TicketClaim, AuthorityId, AuthorityIndex, AuthoritySignature, EpochConfiguration, + Randomness, Slot, VrfSignature, SASSAFRAS_ENGINE_ID, }; use scale_codec::{Decode, Encode, MaxEncodedLen}; @@ -51,7 +51,7 @@ pub struct NextEpochDescriptor { /// Epoch randomness. pub randomness: Randomness, /// Mutable protocol parameters. If not present previous epoch parameters are used. - pub config: Option, + pub config: Option, } /// Consensus log item. diff --git a/primitives/consensus/sassafras/src/inherents.rs b/primitives/consensus/sassafras/src/inherents.rs index 3183afee8ab84..cf229091b4ff9 100644 --- a/primitives/consensus/sassafras/src/inherents.rs +++ b/primitives/consensus/sassafras/src/inherents.rs @@ -15,47 +15,48 @@ // See the License for the specific language governing permissions and // limitations under the License. -//! Inherents for Sassafras +//! Sassafras inherents structures and helpers. use sp_inherents::{Error, InherentData, InherentIdentifier}; use sp_std::result::Result; -/// The Sassafras inherent identifier. +/// Inherent identifier. pub const INHERENT_IDENTIFIER: InherentIdentifier = *b"sassslot"; -/// The type of the Sassafras inherent. +/// The type of inherent. pub type InherentType = sp_consensus_slots::Slot; -/// Auxiliary trait to extract Sassafras inherent data. +/// Auxiliary trait to extract inherent data. pub trait SassafrasInherentData { - /// Get Sassafras inherent data. - fn sassafras_inherent_data(&self) -> Result, Error>; - /// Replace Sassafras inherent data. - fn sassafras_replace_inherent_data(&mut self, new: InherentType); + /// Get inherent data. + fn sassafras_get_inherent_data(&self) -> Result, Error>; + /// Put inherent data. + fn sassafras_put_inherent_data(&mut self, data: &InherentType) -> Result<(), Error>; + /// Replace inherent data. + fn sassafras_replace_inherent_data(&mut self, data: &InherentType); } impl SassafrasInherentData for InherentData { - fn sassafras_inherent_data(&self) -> Result, Error> { + fn sassafras_get_inherent_data(&self) -> Result, Error> { self.get_data(&INHERENT_IDENTIFIER) } - fn sassafras_replace_inherent_data(&mut self, new: InherentType) { - self.replace_data(INHERENT_IDENTIFIER, &new); + fn sassafras_put_inherent_data(&mut self, data: &InherentType) -> Result<(), Error> { + self.put_data(INHERENT_IDENTIFIER, data) } -} -/// Provides the slot duration inherent data for Sassafras. -// TODO: Remove in the future. https://github.com/paritytech/substrate/issues/8029 -#[cfg(feature = "std")] -pub struct InherentDataProvider { - slot: InherentType, + fn sassafras_replace_inherent_data(&mut self, data: &InherentType) { + self.replace_data(INHERENT_IDENTIFIER, data); + } } -#[cfg(feature = "std")] +/// Provides the slot duration inherent data. +pub struct InherentDataProvider(InherentType); + impl InherentDataProvider { /// Create new inherent data provider from the given `slot`. pub fn new(slot: InherentType) -> Self { - Self { slot } + Self(slot) } /// Creates the inherent data provider by calculating the slot from the given @@ -64,39 +65,26 @@ impl InherentDataProvider { timestamp: sp_timestamp::Timestamp, slot_duration: sp_consensus_slots::SlotDuration, ) -> Self { - let slot = InherentType::from_timestamp(timestamp, slot_duration); - - Self { slot } + Self(InherentType::from_timestamp(timestamp, slot_duration)) } /// Returns the `slot` of this inherent data provider. pub fn slot(&self) -> InherentType { - self.slot + self.0 } } -#[cfg(feature = "std")] impl sp_std::ops::Deref for InherentDataProvider { type Target = InherentType; fn deref(&self) -> &Self::Target { - &self.slot + &self.0 } } -#[cfg(feature = "std")] #[async_trait::async_trait] impl sp_inherents::InherentDataProvider for InherentDataProvider { async fn provide_inherent_data(&self, inherent_data: &mut InherentData) -> Result<(), Error> { - inherent_data.put_data(INHERENT_IDENTIFIER, &self.slot) - } - - async fn try_handle_error( - &self, - _: &InherentIdentifier, - _: &[u8], - ) -> Option> { - // There is no error anymore - None + inherent_data.sassafras_put_inherent_data(&self.0) } } diff --git a/primitives/consensus/sassafras/src/lib.rs b/primitives/consensus/sassafras/src/lib.rs index 334e2bd4824f2..910d56df1e839 100644 --- a/primitives/consensus/sassafras/src/lib.rs +++ b/primitives/consensus/sassafras/src/lib.rs @@ -15,18 +15,14 @@ // See the License for the specific language governing permissions and // limitations under the License. -//! Primitives for Sassafras -//! TODO-SASS-P2 : write proper docs +//! Primitives for Sassafras consensus. -// TODO davxy enable warnings -// #![deny(warnings)] -// #![forbid(unsafe_code, missing_docs, unused_variables, unused_imports)] +#![deny(warnings)] +#![forbid(unsafe_code, missing_docs, unused_variables, unused_imports)] #![cfg_attr(not(feature = "std"), no_std)] use scale_codec::{Decode, Encode, MaxEncodedLen}; use scale_info::TypeInfo; -#[cfg(feature = "serde")] -use serde::{Deserialize, Serialize}; use sp_core::crypto::KeyTypeId; use sp_runtime::{ConsensusEngineId, RuntimeDebug}; use sp_std::vec::Vec; @@ -44,7 +40,6 @@ pub mod ticket; pub use ticket::{ slot_claim_sign_data, slot_claim_vrf_input, ticket_body_sign_data, ticket_id, ticket_id_threshold, ticket_id_vrf_input, TicketBody, TicketClaim, TicketEnvelope, TicketId, - TicketSecret, }; mod app { @@ -52,7 +47,7 @@ mod app { app_crypto!(bandersnatch, SASSAFRAS); } -/// Key type for Sassafras protocol. +/// Key type identifier. pub const KEY_TYPE: KeyTypeId = sp_application_crypto::key_types::SASSAFRAS; /// Consensus engine identifier. @@ -61,7 +56,7 @@ pub const SASSAFRAS_ENGINE_ID: ConsensusEngineId = *b"SASS"; /// VRF output length for per-slot randomness. pub const RANDOMNESS_LENGTH: usize = 32; -/// The index of an authority. +/// Index of an authority. pub type AuthorityIndex = u32; /// Sassafras authority keypair. Necessarily equivalent to the schnorrkel public key used in @@ -83,29 +78,32 @@ pub type SassafrasBlockWeight = u32; /// An equivocation proof for multiple block authorships on the same slot (i.e. double vote). pub type EquivocationProof = sp_consensus_slots::EquivocationProof; -/// Randomness required by some SASSAFRAS operations. +/// Randomness required by some protocol's operations. pub type Randomness = [u8; RANDOMNESS_LENGTH]; -/// Configuration data used by the Sassafras consensus engine. +/// Configuration data that can be modified on epoch change. +#[derive(Clone, PartialEq, Eq, Encode, Decode, RuntimeDebug, MaxEncodedLen, TypeInfo, Default)] +pub struct EpochConfiguration { + /// Redundancy factor. + pub redundancy_factor: u32, + /// Number of attempts for tickets generation. + pub attempts_number: u32, +} + +/// Information attached to an epoch. +// TODO @davxy. Why not embed this information in `Epoch` structure below? #[derive(Clone, Encode, Decode, RuntimeDebug, PartialEq, Eq, TypeInfo)] -pub struct SassafrasConfiguration { - /// The slot duration in milliseconds. - pub slot_duration: u64, - /// The duration of epoch in slots. +pub struct EpochData { + /// Slot duration in milliseconds. + pub slot_duration: SlotDuration, + /// Duration of epoch in slots. pub epoch_duration: u64, - /// The authorities for the epoch. + /// Authorities for the epoch. pub authorities: Vec, - /// The randomness for the epoch. + /// Randomness for the epoch. pub randomness: Randomness, /// Tickets threshold parameters. - pub threshold_params: SassafrasEpochConfiguration, -} - -impl SassafrasConfiguration { - /// Get the slot duration defined in the genesis configuration. - pub fn slot_duration(&self) -> SlotDuration { - SlotDuration::from_millis(self.slot_duration) - } + pub threshold_params: EpochConfiguration, } /// Sassafras epoch information @@ -116,21 +114,11 @@ pub struct Epoch { /// The starting slot of the epoch. pub start_slot: Slot, /// Epoch configuration. - pub config: SassafrasConfiguration, -} - -/// Configuration data used by the Sassafras consensus engine that can be modified on epoch change. -// TODO-SASS-P3: rename to something better... like LotteryConfig -#[derive(Clone, PartialEq, Eq, Encode, Decode, RuntimeDebug, MaxEncodedLen, TypeInfo, Default)] -#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] -pub struct SassafrasEpochConfiguration { - /// Redundancy factor. - pub redundancy_factor: u32, - /// Number of attempts for tickets generation. - pub attempts_number: u32, + pub config: EpochData, } /// An opaque type used to represent the key ownership proof at the runtime API boundary. +/// /// The inner value is an encoded representation of the actual key ownership proof which will be /// parameterized when defining the runtime. At the runtime API boundary this type is unknown and /// as such we keep this opaque representation, implementors of the runtime API will have to make @@ -142,13 +130,6 @@ pub struct OpaqueKeyOwnershipProof(Vec); sp_api::decl_runtime_apis! { /// API necessary for block authorship with Sassafras. pub trait SassafrasApi { - /// Get ring context to be used for ticket construction and verification. - fn ring_context() -> Option; - - /// Submit next epoch validator tickets via an unsigned extrinsic. - /// This method returns `false` when creation of the extrinsics fails. - fn submit_tickets_unsigned_extrinsic(tickets: Vec) -> bool; - /// Get ticket id associated to the given slot. fn slot_ticket_id(slot: Slot) -> Option; @@ -161,33 +142,12 @@ sp_api::decl_runtime_apis! { /// Next epoch information. fn next_epoch() -> Epoch; - /// Generates a proof of key ownership for the given authority in the current epoch. - /// - /// An example usage of this module is coupled with the session historical module to prove - /// that a given authority key is tied to a given staking identity during a specific - /// session. Proofs of key ownership are necessary for submitting equivocation reports. - /// - /// NOTE: even though the API takes a `slot` as parameter the current implementations - /// ignores this parameter and instead relies on this method being called at the correct - /// block height, i.e. any point at which the epoch for the given slot is live on-chain. - /// Future implementations will instead use indexed data through an offchain worker, not - /// requiring older states to be available. - fn generate_key_ownership_proof( - slot: Slot, - authority_id: AuthorityId, - ) -> Option; - - /// Submits an unsigned extrinsic to report an equivocation. + /// Get ring context to be used for ticket proof construction. + fn ring_context() -> Option; + + /// Submit next epoch validator tickets via an unsigned extrinsic. /// - /// The caller must provide the equivocation proof and a key ownership proof (should be - /// obtained using `generate_key_ownership_proof`). The extrinsic will be unsigned and - /// should only be accepted for local authorship (not to be broadcast to the network). This - /// method returns `None` when creation of the extrinsic fails, e.g. if equivocation - /// reporting is disabled for the given runtime (i.e. this method is hardcoded to return - /// `None`). Only useful in an offchain context. - fn submit_report_equivocation_unsigned_extrinsic( - equivocation_proof: EquivocationProof, - key_owner_proof: OpaqueKeyOwnershipProof, - ) -> bool; + /// This method returns `false` when creation of the extrinsics fails. + fn submit_tickets_unsigned_extrinsic(tickets: Vec) -> bool; } } diff --git a/primitives/consensus/sassafras/src/ticket.rs b/primitives/consensus/sassafras/src/ticket.rs index 46b614a6d0fc6..515d5eedb78c1 100644 --- a/primitives/consensus/sassafras/src/ticket.rs +++ b/primitives/consensus/sassafras/src/ticket.rs @@ -21,12 +21,15 @@ use crate::{Randomness, RingVrfSignature, VrfInput, VrfOutput, VrfSignData, SASS use scale_codec::{Decode, Encode, MaxEncodedLen}; use scale_info::TypeInfo; use sp_consensus_slots::Slot; +use sp_core::ed25519::{Public as EphemeralPublic, Signature as EphemeralSignature}; use sp_std::vec::Vec; /// Ticket identifier. /// -/// Within the algorithm this is also used as a ticket score applied to bound -/// the ticket to a epoch's slot. +/// Its value is the output of a VRF whose inputs cannot be controlled by the +/// creator of the ticket (refer to [`ticket_id_vrf_input`] parameters). +/// Because of this, it is also used as the ticket score to compare against +/// the epoch ticket's threshold. pub type TicketId = u128; /// Ticket data persisted on-chain. @@ -34,12 +37,13 @@ pub type TicketId = u128; pub struct TicketBody { /// Attempt index. pub attempt_idx: u32, - /// Ed25519 public key which gets erased when claiming the ticket. - pub erased_public: [u8; 32], + /// Ed25519 ephemeral public key representing ticket ownersip. + /// (i.e. whoever has the secret, is the owner) + pub erased_public: EphemeralPublic, } /// Ticket ring vrf signature. -pub type TicketRingSignature = RingVrfSignature; +pub type TicketSignature = RingVrfSignature; /// Ticket envelope used on during submission. #[derive(Debug, Clone, PartialEq, Eq, Encode, Decode, MaxEncodedLen, TypeInfo)] @@ -47,22 +51,14 @@ pub struct TicketEnvelope { /// Ticket body. pub body: TicketBody, /// Ring signature. - pub ring_signature: TicketRingSignature, -} - -/// Ticket auxiliary information used to claim the ticket ownership. -#[derive(Debug, Clone, PartialEq, Eq, Encode, Decode, MaxEncodedLen, TypeInfo)] -pub struct TicketSecret { - /// Attempt index. - pub attempt_idx: u32, - /// Ed25519 used to claim ticket ownership. - pub erased_secret: [u8; 32], + pub signature: TicketSignature, } /// Ticket claim information filled by the block author. #[derive(Debug, Clone, PartialEq, Eq, Encode, Decode, MaxEncodedLen, TypeInfo)] pub struct TicketClaim { - pub erased_signature: [u8; 64], + /// Signature to claim ownership of `TicketBody::erased_public`. + pub erased_signature: EphemeralSignature, } fn vrf_input_from_data( diff --git a/primitives/inherents/src/client_side.rs b/primitives/inherents/src/client_side.rs index 27479de136f2d..2e23221261336 100644 --- a/primitives/inherents/src/client_side.rs +++ b/primitives/inherents/src/client_side.rs @@ -99,9 +99,11 @@ pub trait InherentDataProvider: Send + Sync { /// If the given error could not be decoded, `None` should be returned. async fn try_handle_error( &self, - identifier: &InherentIdentifier, - error: &[u8], - ) -> Option>; + _identifier: &InherentIdentifier, + _error: &[u8], + ) -> Option> { + None + } } #[impl_trait_for_tuples::impl_for_tuples(30)] From afd6e8df5ebca5f837169f5caa5dd8486ae862c2 Mon Sep 17 00:00:00 2001 From: Davide Galassi Date: Mon, 14 Aug 2023 13:20:49 +0200 Subject: [PATCH 11/22] Align to main sassafras PR --- Cargo.lock | 1 + Cargo.toml | 1 - primitives/consensus/sassafras/Cargo.toml | 3 + primitives/consensus/sassafras/src/digests.rs | 4 +- .../consensus/sassafras/src/inherents.rs | 3 + primitives/consensus/sassafras/src/lib.rs | 79 +++++++++++++------ primitives/consensus/sassafras/src/ticket.rs | 3 +- 7 files changed, 64 insertions(+), 30 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index e28f00b8373a8..493d496cda72f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -10743,6 +10743,7 @@ dependencies = [ "async-trait", "parity-scale-codec", "scale-info", + "serde", "sp-api", "sp-application-crypto", "sp-consensus-slots", diff --git a/Cargo.toml b/Cargo.toml index d063f9f3c6426..ea512269d39d4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -43,7 +43,6 @@ sassafras-experimental = [ "sc-keystore/bandersnatch-experimental", ] - # Exists here to be backwards compatible and to support `cargo run` in the workspace. # # Just uses the `node-cli` main binary. `node-cli` itself also again exposes the node as diff --git a/primitives/consensus/sassafras/Cargo.toml b/primitives/consensus/sassafras/Cargo.toml index 494074a2b64a5..8d687aabee00b 100644 --- a/primitives/consensus/sassafras/Cargo.toml +++ b/primitives/consensus/sassafras/Cargo.toml @@ -18,6 +18,7 @@ targets = ["x86_64-unknown-linux-gnu"] async-trait = { version = "0.1.50", optional = true } scale-codec = { package = "parity-scale-codec", version = "3.2.2", default-features = false } scale-info = { version = "2.5.0", default-features = false, features = ["derive"] } +serde = { version = "1.0.163", default-features = false, features = ["derive"], optional = true } sp-api = { version = "4.0.0-dev", default-features = false, path = "../../api" } sp-application-crypto = { version = "23.0.0", default-features = false, path = "../../application-crypto", features = ["bandersnatch-experimental"] } sp-consensus-slots = { version = "0.10.0-dev", default-features = false, path = "../slots" } @@ -33,6 +34,7 @@ std = [ "async-trait", "scale-codec/std", "scale-info/std", + "serde/std", "sp-api/std", "sp-application-crypto/std", "sp-consensus-slots/std", @@ -45,6 +47,7 @@ std = [ # Serde support without relying on std features. serde = [ + "dep:serde", "scale-info/serde", "sp-application-crypto/serde", "sp-consensus-slots/serde", diff --git a/primitives/consensus/sassafras/src/digests.rs b/primitives/consensus/sassafras/src/digests.rs index 3d063445facfa..1971540351d3c 100644 --- a/primitives/consensus/sassafras/src/digests.rs +++ b/primitives/consensus/sassafras/src/digests.rs @@ -50,7 +50,7 @@ pub struct NextEpochDescriptor { pub authorities: Vec, /// Epoch randomness. pub randomness: Randomness, - /// Mutable protocol parameters. If not present previous epoch parameters are used. + /// Mutable epoch parameters. If not present previous epoch parameters are used. pub config: Option, } @@ -60,7 +60,7 @@ pub enum ConsensusLog { /// Provides information about the next epoch parameters. #[codec(index = 1)] NextEpochData(NextEpochDescriptor), - /// Disable the authority with given index (@davxy: todo). + /// Disable the authority with given index (TODO @davxy). #[codec(index = 2)] OnDisabled(AuthorityIndex), } diff --git a/primitives/consensus/sassafras/src/inherents.rs b/primitives/consensus/sassafras/src/inherents.rs index cf229091b4ff9..70025267fa6b4 100644 --- a/primitives/consensus/sassafras/src/inherents.rs +++ b/primitives/consensus/sassafras/src/inherents.rs @@ -53,6 +53,7 @@ impl SassafrasInherentData for InherentData { /// Provides the slot duration inherent data. pub struct InherentDataProvider(InherentType); +#[cfg(feature = "std")] impl InherentDataProvider { /// Create new inherent data provider from the given `slot`. pub fn new(slot: InherentType) -> Self { @@ -74,6 +75,7 @@ impl InherentDataProvider { } } +#[cfg(feature = "std")] impl sp_std::ops::Deref for InherentDataProvider { type Target = InherentType; @@ -82,6 +84,7 @@ impl sp_std::ops::Deref for InherentDataProvider { } } +#[cfg(feature = "std")] #[async_trait::async_trait] impl sp_inherents::InherentDataProvider for InherentDataProvider { async fn provide_inherent_data(&self, inherent_data: &mut InherentData) -> Result<(), Error> { diff --git a/primitives/consensus/sassafras/src/lib.rs b/primitives/consensus/sassafras/src/lib.rs index 910d56df1e839..c0d3725ea7154 100644 --- a/primitives/consensus/sassafras/src/lib.rs +++ b/primitives/consensus/sassafras/src/lib.rs @@ -33,13 +33,17 @@ pub use sp_core::bandersnatch::{ vrf::{VrfInput, VrfOutput, VrfSignData, VrfSignature}, }; +#[cfg(feature = "serde")] +use serde::{Deserialize, Serialize}; + pub mod digests; pub mod inherents; pub mod ticket; pub use ticket::{ slot_claim_sign_data, slot_claim_vrf_input, ticket_body_sign_data, ticket_id, - ticket_id_threshold, ticket_id_vrf_input, TicketBody, TicketClaim, TicketEnvelope, TicketId, + ticket_id_threshold, ticket_id_vrf_input, EphemeralPublic, EphemeralSignature, TicketBody, + TicketClaim, TicketEnvelope, TicketId, }; mod app { @@ -82,18 +86,24 @@ pub type EquivocationProof = sp_consensus_slots::EquivocationProof, /// Randomness for the epoch. pub randomness: Randomness, - /// Tickets threshold parameters. - pub threshold_params: EpochConfiguration, -} - -/// Sassafras epoch information -#[derive(Encode, Decode, PartialEq, Eq, Clone, Debug, TypeInfo)] -pub struct Epoch { - /// The epoch index. - pub epoch_idx: u64, - /// The starting slot of the epoch. - pub start_slot: Slot, /// Epoch configuration. - pub config: EpochData, + pub config: EpochConfiguration, } /// An opaque type used to represent the key ownership proof at the runtime API boundary. @@ -130,6 +129,13 @@ pub struct OpaqueKeyOwnershipProof(Vec); sp_api::decl_runtime_apis! { /// API necessary for block authorship with Sassafras. pub trait SassafrasApi { + /// Get ring context to be used for ticket construction and verification. + fn ring_context() -> Option; + + /// Submit next epoch validator tickets via an unsigned extrinsic. + /// This method returns `false` when creation of the extrinsics fails. + fn submit_tickets_unsigned_extrinsic(tickets: Vec) -> bool; + /// Get ticket id associated to the given slot. fn slot_ticket_id(slot: Slot) -> Option; @@ -142,12 +148,33 @@ sp_api::decl_runtime_apis! { /// Next epoch information. fn next_epoch() -> Epoch; - /// Get ring context to be used for ticket proof construction. - fn ring_context() -> Option; - - /// Submit next epoch validator tickets via an unsigned extrinsic. + /// Generates a proof of key ownership for the given authority in the current epoch. /// - /// This method returns `false` when creation of the extrinsics fails. - fn submit_tickets_unsigned_extrinsic(tickets: Vec) -> bool; + /// An example usage of this module is coupled with the session historical module to prove + /// that a given authority key is tied to a given staking identity during a specific + /// session. Proofs of key ownership are necessary for submitting equivocation reports. + /// + /// NOTE: even though the API takes a `slot` as parameter the current implementations + /// ignores this parameter and instead relies on this method being called at the correct + /// block height, i.e. any point at which the epoch for the given slot is live on-chain. + /// Future implementations will instead use indexed data through an offchain worker, not + /// requiring older states to be available. + fn generate_key_ownership_proof( + slot: Slot, + authority_id: AuthorityId, + ) -> Option; + + /// Submits an unsigned extrinsic to report an equivocation. + /// + /// The caller must provide the equivocation proof and a key ownership proof (should be + /// obtained using `generate_key_ownership_proof`). The extrinsic will be unsigned and + /// should only be accepted for local authorship (not to be broadcast to the network). This + /// method returns `None` when creation of the extrinsic fails, e.g. if equivocation + /// reporting is disabled for the given runtime (i.e. this method is hardcoded to return + /// `None`). Only useful in an offchain context. + fn submit_report_equivocation_unsigned_extrinsic( + equivocation_proof: EquivocationProof, + key_owner_proof: OpaqueKeyOwnershipProof, + ) -> bool; } } diff --git a/primitives/consensus/sassafras/src/ticket.rs b/primitives/consensus/sassafras/src/ticket.rs index 515d5eedb78c1..62624676c3df4 100644 --- a/primitives/consensus/sassafras/src/ticket.rs +++ b/primitives/consensus/sassafras/src/ticket.rs @@ -21,9 +21,10 @@ use crate::{Randomness, RingVrfSignature, VrfInput, VrfOutput, VrfSignData, SASS use scale_codec::{Decode, Encode, MaxEncodedLen}; use scale_info::TypeInfo; use sp_consensus_slots::Slot; -use sp_core::ed25519::{Public as EphemeralPublic, Signature as EphemeralSignature}; use sp_std::vec::Vec; +pub use sp_core::ed25519::{Public as EphemeralPublic, Signature as EphemeralSignature}; + /// Ticket identifier. /// /// Its value is the output of a VRF whose inputs cannot be controlled by the From e37713c9ee8bbaa7a8e126da53b0815743460cd0 Mon Sep 17 00:00:00 2001 From: Davide Galassi Date: Mon, 14 Aug 2023 17:24:09 +0200 Subject: [PATCH 12/22] Improve docs --- Cargo.toml | 9 +++++---- primitives/consensus/sassafras/src/digests.rs | 4 ++-- .../consensus/sassafras/src/inherents.rs | 2 +- primitives/consensus/sassafras/src/ticket.rs | 20 +++++++++++-------- 4 files changed, 20 insertions(+), 15 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index ea512269d39d4..3ddb5ca53d325 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -29,14 +29,15 @@ frame-support = { path = "frame/support" } node-cli = { path = "bin/node/cli" } -# This list of dependencies is necessary to build sassafras primitives -# `bandersnatch-experimental` is transitifely passed between: +# This list of dependencies is necessary to build sassafras primitives. +# `bandersnatch-experimental` is transitively passed between: # sp-consensus-sassafras → sp-application-crypto → sp-io → sp-keystore -# This last thus defines the bandersnatch functions for its trait `Keystore`. -# We end up requiring bandersnatch implementations in the client keystore as well. +# Thus we end up having the bandersnatch functions "unlocked" in the `Keystore` trait. +# Follow that we require bandersnatch implementations in the client keystore as well. sp-consensus-sassafras = { path = "primitives/consensus/sassafras", optional = true } sc-keystore = { path = "client/keystore" } +# Feature to unlock "Sassafras" experimental crates [features] sassafras-experimental = [ "sp-consensus-sassafras", diff --git a/primitives/consensus/sassafras/src/digests.rs b/primitives/consensus/sassafras/src/digests.rs index 1971540351d3c..9953b1fa5e23b 100644 --- a/primitives/consensus/sassafras/src/digests.rs +++ b/primitives/consensus/sassafras/src/digests.rs @@ -50,7 +50,7 @@ pub struct NextEpochDescriptor { pub authorities: Vec, /// Epoch randomness. pub randomness: Randomness, - /// Mutable epoch parameters. If not present previous epoch parameters are used. + /// Configurable parameters. If not present previous epoch parameters are used. pub config: Option, } @@ -60,7 +60,7 @@ pub enum ConsensusLog { /// Provides information about the next epoch parameters. #[codec(index = 1)] NextEpochData(NextEpochDescriptor), - /// Disable the authority with given index (TODO @davxy). + /// Disable the authority with given index. #[codec(index = 2)] OnDisabled(AuthorityIndex), } diff --git a/primitives/consensus/sassafras/src/inherents.rs b/primitives/consensus/sassafras/src/inherents.rs index 70025267fa6b4..dc9bca31b9ac8 100644 --- a/primitives/consensus/sassafras/src/inherents.rs +++ b/primitives/consensus/sassafras/src/inherents.rs @@ -26,7 +26,7 @@ pub const INHERENT_IDENTIFIER: InherentIdentifier = *b"sassslot"; /// The type of inherent. pub type InherentType = sp_consensus_slots::Slot; -/// Auxiliary trait to extract inherent data. +/// Convenience trait to manipulate protocol-specific inherent data. pub trait SassafrasInherentData { /// Get inherent data. fn sassafras_get_inherent_data(&self) -> Result, Error>; diff --git a/primitives/consensus/sassafras/src/ticket.rs b/primitives/consensus/sassafras/src/ticket.rs index 62624676c3df4..ce73fd11b2aaa 100644 --- a/primitives/consensus/sassafras/src/ticket.rs +++ b/primitives/consensus/sassafras/src/ticket.rs @@ -122,14 +122,18 @@ pub fn ticket_id(vrf_input: &VrfInput, vrf_output: &VrfOutput) -> TicketId { u128::from_le_bytes(bytes) } -/// Computes the threshold for a given epoch as T = (x*s)/(a*v), where: -/// - x: redundancy factor; -/// - s: number of slots in epoch; -/// - a: max number of attempts; -/// - v: number of validator in epoch. -/// The parameters should be chosen such that T <= 1. -/// If `attempts * validators` is zero then we fallback to T = 0 -// TODO-SASS-P3: this formula must be double-checked... +/// Computes the ticket-id maximum allowed value for a given epoch. +/// +/// Only ticket identifiers below this threshold should be considered for +/// slot assignment. +/// +/// The value is computed as `T = TicketId::MAX·(r·s)/(a·v)`, where: +/// - `r` = redundancy factor; +/// - `s` = number of slots in epoch; +/// - `a` = maximum number of tickets attempts per validator; +/// - `v` = number of validator in epoch. +/// +/// If `a·v = 0` then we fallback to `T = 0`. pub fn ticket_id_threshold( redundancy: u32, slots: u32, From a6dfd28cbbced20ec507fd20f71ccd52ab76ff87 Mon Sep 17 00:00:00 2001 From: Davide Galassi Date: Mon, 14 Aug 2023 18:26:19 +0200 Subject: [PATCH 13/22] Remove inherents (moved to client code) --- primitives/consensus/sassafras/Cargo.toml | 6 -- primitives/consensus/sassafras/src/digests.rs | 2 +- .../consensus/sassafras/src/inherents.rs | 93 ------------------- primitives/consensus/sassafras/src/lib.rs | 1 - primitives/consensus/sassafras/src/ticket.rs | 20 ++-- 5 files changed, 9 insertions(+), 113 deletions(-) delete mode 100644 primitives/consensus/sassafras/src/inherents.rs diff --git a/primitives/consensus/sassafras/Cargo.toml b/primitives/consensus/sassafras/Cargo.toml index 8d687aabee00b..14de323c73617 100644 --- a/primitives/consensus/sassafras/Cargo.toml +++ b/primitives/consensus/sassafras/Cargo.toml @@ -15,7 +15,6 @@ publish = false targets = ["x86_64-unknown-linux-gnu"] [dependencies] -async-trait = { version = "0.1.50", optional = true } scale-codec = { package = "parity-scale-codec", version = "3.2.2", default-features = false } scale-info = { version = "2.5.0", default-features = false, features = ["derive"] } serde = { version = "1.0.163", default-features = false, features = ["derive"], optional = true } @@ -23,15 +22,12 @@ sp-api = { version = "4.0.0-dev", default-features = false, path = "../../api" } sp-application-crypto = { version = "23.0.0", default-features = false, path = "../../application-crypto", features = ["bandersnatch-experimental"] } sp-consensus-slots = { version = "0.10.0-dev", default-features = false, path = "../slots" } sp-core = { version = "21.0.0", default-features = false, path = "../../core", features = ["bandersnatch-experimental"] } -sp-inherents = { version = "4.0.0-dev", default-features = false, path = "../../inherents" } sp-runtime = { version = "24.0.0", default-features = false, path = "../../runtime" } sp-std = { version = "8.0.0", default-features = false, path = "../../std" } -sp-timestamp = { version = "4.0.0-dev", optional = true, path = "../../timestamp" } [features] default = ["std"] std = [ - "async-trait", "scale-codec/std", "scale-info/std", "serde/std", @@ -39,10 +35,8 @@ std = [ "sp-application-crypto/std", "sp-consensus-slots/std", "sp-core/std", - "sp-inherents/std", "sp-runtime/std", "sp-std/std", - "sp-timestamp", ] # Serde support without relying on std features. diff --git a/primitives/consensus/sassafras/src/digests.rs b/primitives/consensus/sassafras/src/digests.rs index 9953b1fa5e23b..454bf58507d12 100644 --- a/primitives/consensus/sassafras/src/digests.rs +++ b/primitives/consensus/sassafras/src/digests.rs @@ -50,7 +50,7 @@ pub struct NextEpochDescriptor { pub authorities: Vec, /// Epoch randomness. pub randomness: Randomness, - /// Configurable parameters. If not present previous epoch parameters are used. + /// Mutable epoch parameters. If not present previous epoch parameters are used. pub config: Option, } diff --git a/primitives/consensus/sassafras/src/inherents.rs b/primitives/consensus/sassafras/src/inherents.rs deleted file mode 100644 index dc9bca31b9ac8..0000000000000 --- a/primitives/consensus/sassafras/src/inherents.rs +++ /dev/null @@ -1,93 +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. - -//! Sassafras inherents structures and helpers. - -use sp_inherents::{Error, InherentData, InherentIdentifier}; -use sp_std::result::Result; - -/// Inherent identifier. -pub const INHERENT_IDENTIFIER: InherentIdentifier = *b"sassslot"; - -/// The type of inherent. -pub type InherentType = sp_consensus_slots::Slot; - -/// Convenience trait to manipulate protocol-specific inherent data. -pub trait SassafrasInherentData { - /// Get inherent data. - fn sassafras_get_inherent_data(&self) -> Result, Error>; - /// Put inherent data. - fn sassafras_put_inherent_data(&mut self, data: &InherentType) -> Result<(), Error>; - /// Replace inherent data. - fn sassafras_replace_inherent_data(&mut self, data: &InherentType); -} - -impl SassafrasInherentData for InherentData { - fn sassafras_get_inherent_data(&self) -> Result, Error> { - self.get_data(&INHERENT_IDENTIFIER) - } - - fn sassafras_put_inherent_data(&mut self, data: &InherentType) -> Result<(), Error> { - self.put_data(INHERENT_IDENTIFIER, data) - } - - fn sassafras_replace_inherent_data(&mut self, data: &InherentType) { - self.replace_data(INHERENT_IDENTIFIER, data); - } -} - -/// Provides the slot duration inherent data. -pub struct InherentDataProvider(InherentType); - -#[cfg(feature = "std")] -impl InherentDataProvider { - /// Create new inherent data provider from the given `slot`. - pub fn new(slot: InherentType) -> Self { - Self(slot) - } - - /// Creates the inherent data provider by calculating the slot from the given - /// `timestamp` and `duration`. - pub fn from_timestamp_and_slot_duration( - timestamp: sp_timestamp::Timestamp, - slot_duration: sp_consensus_slots::SlotDuration, - ) -> Self { - Self(InherentType::from_timestamp(timestamp, slot_duration)) - } - - /// Returns the `slot` of this inherent data provider. - pub fn slot(&self) -> InherentType { - self.0 - } -} - -#[cfg(feature = "std")] -impl sp_std::ops::Deref for InherentDataProvider { - type Target = InherentType; - - fn deref(&self) -> &Self::Target { - &self.0 - } -} - -#[cfg(feature = "std")] -#[async_trait::async_trait] -impl sp_inherents::InherentDataProvider for InherentDataProvider { - async fn provide_inherent_data(&self, inherent_data: &mut InherentData) -> Result<(), Error> { - inherent_data.sassafras_put_inherent_data(&self.0) - } -} diff --git a/primitives/consensus/sassafras/src/lib.rs b/primitives/consensus/sassafras/src/lib.rs index c0d3725ea7154..c900ea87d09ff 100644 --- a/primitives/consensus/sassafras/src/lib.rs +++ b/primitives/consensus/sassafras/src/lib.rs @@ -37,7 +37,6 @@ pub use sp_core::bandersnatch::{ use serde::{Deserialize, Serialize}; pub mod digests; -pub mod inherents; pub mod ticket; pub use ticket::{ diff --git a/primitives/consensus/sassafras/src/ticket.rs b/primitives/consensus/sassafras/src/ticket.rs index ce73fd11b2aaa..62624676c3df4 100644 --- a/primitives/consensus/sassafras/src/ticket.rs +++ b/primitives/consensus/sassafras/src/ticket.rs @@ -122,18 +122,14 @@ pub fn ticket_id(vrf_input: &VrfInput, vrf_output: &VrfOutput) -> TicketId { u128::from_le_bytes(bytes) } -/// Computes the ticket-id maximum allowed value for a given epoch. -/// -/// Only ticket identifiers below this threshold should be considered for -/// slot assignment. -/// -/// The value is computed as `T = TicketId::MAX·(r·s)/(a·v)`, where: -/// - `r` = redundancy factor; -/// - `s` = number of slots in epoch; -/// - `a` = maximum number of tickets attempts per validator; -/// - `v` = number of validator in epoch. -/// -/// If `a·v = 0` then we fallback to `T = 0`. +/// Computes the threshold for a given epoch as T = (x*s)/(a*v), where: +/// - x: redundancy factor; +/// - s: number of slots in epoch; +/// - a: max number of attempts; +/// - v: number of validator in epoch. +/// The parameters should be chosen such that T <= 1. +/// If `attempts * validators` is zero then we fallback to T = 0 +// TODO-SASS-P3: this formula must be double-checked... pub fn ticket_id_threshold( redundancy: u32, slots: u32, From 21299ea5936eb02ab7081fba44cfc77737451f82 Mon Sep 17 00:00:00 2001 From: Davide Galassi Date: Mon, 14 Aug 2023 19:07:23 +0200 Subject: [PATCH 14/22] Improve docs --- Cargo.lock | 3 --- primitives/consensus/sassafras/src/digests.rs | 2 +- primitives/consensus/sassafras/src/ticket.rs | 20 +++++++++++-------- 3 files changed, 13 insertions(+), 12 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 493d496cda72f..cfa42be3db1a1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -10740,7 +10740,6 @@ dependencies = [ name = "sp-consensus-sassafras" version = "0.3.4-dev" dependencies = [ - "async-trait", "parity-scale-codec", "scale-info", "serde", @@ -10748,10 +10747,8 @@ dependencies = [ "sp-application-crypto", "sp-consensus-slots", "sp-core", - "sp-inherents", "sp-runtime", "sp-std", - "sp-timestamp", ] [[package]] diff --git a/primitives/consensus/sassafras/src/digests.rs b/primitives/consensus/sassafras/src/digests.rs index 454bf58507d12..9953b1fa5e23b 100644 --- a/primitives/consensus/sassafras/src/digests.rs +++ b/primitives/consensus/sassafras/src/digests.rs @@ -50,7 +50,7 @@ pub struct NextEpochDescriptor { pub authorities: Vec, /// Epoch randomness. pub randomness: Randomness, - /// Mutable epoch parameters. If not present previous epoch parameters are used. + /// Configurable parameters. If not present previous epoch parameters are used. pub config: Option, } diff --git a/primitives/consensus/sassafras/src/ticket.rs b/primitives/consensus/sassafras/src/ticket.rs index 62624676c3df4..e4bc0a072d9af 100644 --- a/primitives/consensus/sassafras/src/ticket.rs +++ b/primitives/consensus/sassafras/src/ticket.rs @@ -122,14 +122,18 @@ pub fn ticket_id(vrf_input: &VrfInput, vrf_output: &VrfOutput) -> TicketId { u128::from_le_bytes(bytes) } -/// Computes the threshold for a given epoch as T = (x*s)/(a*v), where: -/// - x: redundancy factor; -/// - s: number of slots in epoch; -/// - a: max number of attempts; -/// - v: number of validator in epoch. -/// The parameters should be chosen such that T <= 1. -/// If `attempts * validators` is zero then we fallback to T = 0 -// TODO-SASS-P3: this formula must be double-checked... +/// Computes the ticket-id maximum allowed values for a given epoch. +/// +/// Only ticket identifiers below this threshold should be considered for slot +/// assignment. +/// +/// The value is computed as `T = TicketId::MAX·(redundancy*slots)/(attempts*validators)`, where: +/// - `redundancy`: redundancy factor; +/// - `slots`: number of slots in epoch; +/// - `attempts`: max number of tickets attempts per validator; +/// - `validators`: number of validators in epoch. +/// +/// If `attempts * validators = 0` then we fallback to 0. pub fn ticket_id_threshold( redundancy: u32, slots: u32, From 827b2a7544a446c6bc89729f432f85cb5e8e39b3 Mon Sep 17 00:00:00 2001 From: Davide Galassi Date: Mon, 14 Aug 2023 19:18:17 +0200 Subject: [PATCH 15/22] Rollback tweak to inherent data provider --- primitives/inherents/src/client_side.rs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/primitives/inherents/src/client_side.rs b/primitives/inherents/src/client_side.rs index 2e23221261336..e23d7576bf60d 100644 --- a/primitives/inherents/src/client_side.rs +++ b/primitives/inherents/src/client_side.rs @@ -101,9 +101,7 @@ pub trait InherentDataProvider: Send + Sync { &self, _identifier: &InherentIdentifier, _error: &[u8], - ) -> Option> { - None - } + ) -> Option>; } #[impl_trait_for_tuples::impl_for_tuples(30)] From ddef0b22a1817adb74054f623ad4a6e3e371a219 Mon Sep 17 00:00:00 2001 From: Davide Galassi Date: Mon, 14 Aug 2023 19:21:02 +0200 Subject: [PATCH 16/22] Leftover --- primitives/inherents/src/client_side.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/primitives/inherents/src/client_side.rs b/primitives/inherents/src/client_side.rs index e23d7576bf60d..27479de136f2d 100644 --- a/primitives/inherents/src/client_side.rs +++ b/primitives/inherents/src/client_side.rs @@ -99,8 +99,8 @@ pub trait InherentDataProvider: Send + Sync { /// If the given error could not be decoded, `None` should be returned. async fn try_handle_error( &self, - _identifier: &InherentIdentifier, - _error: &[u8], + identifier: &InherentIdentifier, + error: &[u8], ) -> Option>; } From b938ffc4f471a12d7e0e76a2f3105817a65983f5 Mon Sep 17 00:00:00 2001 From: Davide Galassi Date: Wed, 16 Aug 2023 17:35:22 +0200 Subject: [PATCH 17/22] Update ticket body with 'revealed_public' key --- primitives/consensus/sassafras/src/lib.rs | 6 +- primitives/consensus/sassafras/src/ticket.rs | 73 ++------------- primitives/consensus/sassafras/src/vrf.rs | 99 ++++++++++++++++++++ 3 files changed, 108 insertions(+), 70 deletions(-) create mode 100644 primitives/consensus/sassafras/src/vrf.rs diff --git a/primitives/consensus/sassafras/src/lib.rs b/primitives/consensus/sassafras/src/lib.rs index c900ea87d09ff..ef4a1f2606a06 100644 --- a/primitives/consensus/sassafras/src/lib.rs +++ b/primitives/consensus/sassafras/src/lib.rs @@ -38,11 +38,11 @@ use serde::{Deserialize, Serialize}; pub mod digests; pub mod ticket; +pub mod vrf; pub use ticket::{ - slot_claim_sign_data, slot_claim_vrf_input, ticket_body_sign_data, ticket_id, - ticket_id_threshold, ticket_id_vrf_input, EphemeralPublic, EphemeralSignature, TicketBody, - TicketClaim, TicketEnvelope, TicketId, + ticket_id_threshold, EphemeralPublic, EphemeralSignature, TicketBody, TicketClaim, + TicketEnvelope, TicketId, }; mod app { diff --git a/primitives/consensus/sassafras/src/ticket.rs b/primitives/consensus/sassafras/src/ticket.rs index e4bc0a072d9af..84b485aa8d7e3 100644 --- a/primitives/consensus/sassafras/src/ticket.rs +++ b/primitives/consensus/sassafras/src/ticket.rs @@ -17,11 +17,9 @@ //! Primitives related to tickets. -use crate::{Randomness, RingVrfSignature, VrfInput, VrfOutput, VrfSignData, SASSAFRAS_ENGINE_ID}; +use crate::RingVrfSignature; use scale_codec::{Decode, Encode, MaxEncodedLen}; use scale_info::TypeInfo; -use sp_consensus_slots::Slot; -use sp_std::vec::Vec; pub use sp_core::ed25519::{Public as EphemeralPublic, Signature as EphemeralSignature}; @@ -38,9 +36,10 @@ pub type TicketId = u128; pub struct TicketBody { /// Attempt index. pub attempt_idx: u32, - /// Ed25519 ephemeral public key representing ticket ownersip. - /// (i.e. whoever has the secret, is the owner) + /// 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, } /// Ticket ring vrf signature. @@ -58,76 +57,16 @@ pub struct TicketEnvelope { /// Ticket claim information filled by the block author. #[derive(Debug, Clone, PartialEq, Eq, Encode, Decode, MaxEncodedLen, TypeInfo)] pub struct TicketClaim { - /// Signature to claim ownership of `TicketBody::erased_public`. + /// Signature verified via `TicketBody::erased_public`. pub erased_signature: EphemeralSignature, } -fn vrf_input_from_data( - domain: &[u8], - data: impl IntoIterator>, -) -> VrfInput { - let raw = data.into_iter().fold(Vec::new(), |mut v, e| { - let bytes = e.as_ref(); - v.extend_from_slice(bytes); - let len = u8::try_from(bytes.len()).expect("private function with well known inputs; qed"); - v.extend_from_slice(&len.to_le_bytes()); - v - }); - VrfInput::new(domain, raw) -} - -/// VRF input to claim slot ownership during block production. -/// -/// Input randomness is current epoch randomness. -pub fn slot_claim_vrf_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()], - ) -} - -/// Signing-data to claim slot ownership during block production. -/// -/// Input randomness is current epoch randomness. -pub fn slot_claim_sign_data(randomness: &Randomness, slot: Slot, epoch: u64) -> VrfSignData { - let vrf_input = slot_claim_vrf_input(randomness, slot, epoch); - VrfSignData::new_unchecked(&SASSAFRAS_ENGINE_ID, Some("slot-claim-transcript"), Some(vrf_input)) -} - -/// VRF input to generate the ticket id. -/// -/// Input randomness is current epoch randomness. -pub fn ticket_id_vrf_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()], - ) -} - -/// Data to be signed via ring-vrf. -pub fn ticket_body_sign_data(ticket_body: &TicketBody) -> VrfSignData { - VrfSignData::new_unchecked( - &SASSAFRAS_ENGINE_ID, - &[b"ticket-body-transcript", ticket_body.encode().as_slice()], - [], - ) -} - -/// Get ticket-id for a given vrf input and output. -/// -/// Input generally obtained via `ticket_id_vrf_input`. -/// Output can be obtained directly using the vrf secret key or from the signature. -pub fn ticket_id(vrf_input: &VrfInput, vrf_output: &VrfOutput) -> TicketId { - let bytes = vrf_output.make_bytes::<16>(b"vrf-out", vrf_input); - u128::from_le_bytes(bytes) -} - /// Computes the ticket-id maximum allowed values for a given epoch. /// /// Only ticket identifiers below this threshold should be considered for slot /// assignment. /// -/// The value is computed as `T = TicketId::MAX·(redundancy*slots)/(attempts*validators)`, where: +/// The value is computed as `T = TicketId::MAX*(redundancy*slots)/(attempts*validators)`, where: /// - `redundancy`: redundancy factor; /// - `slots`: number of slots in epoch; /// - `attempts`: max number of tickets attempts per validator; diff --git a/primitives/consensus/sassafras/src/vrf.rs b/primitives/consensus/sassafras/src/vrf.rs new file mode 100644 index 0000000000000..e32e739a53c87 --- /dev/null +++ b/primitives/consensus/sassafras/src/vrf.rs @@ -0,0 +1,99 @@ +// 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. + +//! Utilities related to VRF input, output and signatures. + +use crate::{ + Randomness, TicketBody, TicketId, VrfInput, VrfOutput, VrfSignData, SASSAFRAS_ENGINE_ID, +}; +use scale_codec::Encode; +use sp_consensus_slots::Slot; +use sp_std::vec::Vec; + +pub use sp_core::ed25519::{Public as EphemeralPublic, Signature as EphemeralSignature}; + +fn vrf_input_from_data( + domain: &[u8], + data: impl IntoIterator>, +) -> VrfInput { + let raw = data.into_iter().fold(Vec::new(), |mut v, e| { + let bytes = e.as_ref(); + v.extend_from_slice(bytes); + let len = u8::try_from(bytes.len()).expect("private function with well known inputs; qed"); + v.extend_from_slice(&len.to_le_bytes()); + v + }); + VrfInput::new(domain, raw) +} + +/// 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()], + ) +} + +/// Signing-data to claim slot ownership during block production. +pub fn slot_claim_sign_data(randomness: &Randomness, slot: Slot, epoch: u64) -> VrfSignData { + let vrf_input = slot_claim_input(randomness, slot, epoch); + VrfSignData::new_unchecked(&SASSAFRAS_ENGINE_ID, Some("slot-claim-transcript"), Some(vrf_input)) +} + +/// 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()], + ) +} + +/// 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()], + ) +} + +/// Data to be signed via ring-vrf. +pub fn ticket_body_sign_data(ticket_body: &TicketBody, ticket_id_input: VrfInput) -> VrfSignData { + VrfSignData::new_unchecked( + &SASSAFRAS_ENGINE_ID, + &[b"ticket-body-transcript", ticket_body.encode().as_slice()], + Some(ticket_id_input), + ) +} + +/// Make ticket-id from the given VRF input and output. +/// +/// Input should have been obtained via [`ticket_id_input`]. +/// Output should have been obtained from the input directly using the vrf secret key +/// or from the vrf signature outputs. +pub fn make_ticket_id(vrf_input: &VrfInput, vrf_output: &VrfOutput) -> TicketId { + let bytes = vrf_output.make_bytes::<16>(b"ticket-id", vrf_input); + u128::from_le_bytes(bytes) +} + +/// Make revealed key seed from a given VRF input and ouput. +/// +/// Input should have been obtained via [`revealed_key_input`]. +/// Output should have been obtained from the input directly using the vrf secret key +/// or from the vrf signature outputs. +pub fn make_revealed_key_seed(vrf_input: &VrfInput, vrf_output: &VrfOutput) -> [u8; 32] { + vrf_output.make_bytes::<32>(b"revealed-seed", vrf_input) +} From 0488013e13ceae6b335986dbedfc9f9b77577a8d Mon Sep 17 00:00:00 2001 From: Davide Galassi Date: Fri, 18 Aug 2023 09:25:57 +0200 Subject: [PATCH 18/22] Fix Cargo toml --- Cargo.toml | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 3ddb5ca53d325..da7243e67dd5d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -29,11 +29,16 @@ frame-support = { path = "frame/support" } node-cli = { path = "bin/node/cli" } -# This list of dependencies is necessary to build sassafras primitives. -# `bandersnatch-experimental` is transitively passed between: -# sp-consensus-sassafras → sp-application-crypto → sp-io → sp-keystore -# Thus we end up having the bandersnatch functions "unlocked" in the `Keystore` trait. -# Follow that we require bandersnatch implementations in the client keystore as well. +# Dependencies necessary to optionaly build Sassafras related crates. +# +# Sassafras requires to enable `bandersnatch-experimental` feature. +# This feature ends up being transitively enabled in: +# sp-consensus-sassafras → sp-application-crypto → sp-io → sp-keystore +# Thus the bandersnatch experimental api is exposed in the `Keystore` trait. +# Follow that we require to expose these functions in the client keystore as well. +# At this stage, by default, we don't want this. +# We prefer instead to keep sassafras crates (and thus bandersnatch feature) hidden +# and optionally enabled via the `sassafras-experimental` feature. sp-consensus-sassafras = { path = "primitives/consensus/sassafras", optional = true } sc-keystore = { path = "client/keystore" } @@ -327,6 +332,10 @@ members = [ "utils/binary-merkle-tree", ] +exclude = [ + "primitives/consensus/sassafras", +] + # The list of dependencies below (which can be both direct and indirect dependencies) are crates # that are suspected to be CPU-intensive, and that are unlikely to require debugging (as some of # their debug info might be missing) or to require to be frequently recompiled. We compile these From 35b310a8a794f8bd2cf008eee661ea74126eb417 Mon Sep 17 00:00:00 2001 From: Davide Galassi Date: Fri, 18 Aug 2023 09:33:41 +0200 Subject: [PATCH 19/22] Better doc --- Cargo.toml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index da7243e67dd5d..11ce10f87b218 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -31,14 +31,14 @@ node-cli = { path = "bin/node/cli" } # Dependencies necessary to optionaly build Sassafras related crates. # -# Sassafras requires to enable `bandersnatch-experimental` feature. -# This feature ends up being transitively enabled in: +# Sassafras requires to enable `bandersnatch-experimental` feature and this ends up +# being transitively enabled in: # sp-consensus-sassafras → sp-application-crypto → sp-io → sp-keystore -# Thus the bandersnatch experimental api is exposed in the `Keystore` trait. -# Follow that we require to expose these functions in the client keystore as well. +# Bandersnatch experimental api is thus exposed in the `Keystore` trait and we require +# to expose these functions implementations in the client keystore as well. # At this stage, by default, we don't want this. -# We prefer instead to keep sassafras crates (and thus bandersnatch feature) hidden -# and optionally enabled via the `sassafras-experimental` feature. +# We prefer instead to keep sassafras crates (and thus bandersnatch feature) out of +# workspace and optionally enabled via the `sassafras-experimental` feature. sp-consensus-sassafras = { path = "primitives/consensus/sassafras", optional = true } sc-keystore = { path = "client/keystore" } From 72a1aa08da88e136c878e3136e84555f4f133fa9 Mon Sep 17 00:00:00 2001 From: Davide Galassi Date: Fri, 18 Aug 2023 10:19:17 +0200 Subject: [PATCH 20/22] Cleanup --- primitives/consensus/sassafras/src/digests.rs | 4 ++-- primitives/consensus/sassafras/src/lib.rs | 6 +----- primitives/consensus/sassafras/src/ticket.rs | 17 +++++++++++------ primitives/consensus/sassafras/src/vrf.rs | 9 +++++---- 4 files changed, 19 insertions(+), 17 deletions(-) diff --git a/primitives/consensus/sassafras/src/digests.rs b/primitives/consensus/sassafras/src/digests.rs index 9953b1fa5e23b..0c77fe8f95fcc 100644 --- a/primitives/consensus/sassafras/src/digests.rs +++ b/primitives/consensus/sassafras/src/digests.rs @@ -18,8 +18,8 @@ //! Sassafras digests structures and helpers. use crate::{ - ticket::TicketClaim, AuthorityId, AuthorityIndex, AuthoritySignature, EpochConfiguration, - Randomness, Slot, VrfSignature, SASSAFRAS_ENGINE_ID, + ticket::TicketClaim, vrf::VrfSignature, AuthorityId, AuthorityIndex, AuthoritySignature, + EpochConfiguration, Randomness, Slot, SASSAFRAS_ENGINE_ID, }; use scale_codec::{Decode, Encode, MaxEncodedLen}; diff --git a/primitives/consensus/sassafras/src/lib.rs b/primitives/consensus/sassafras/src/lib.rs index ef4a1f2606a06..651e97850b756 100644 --- a/primitives/consensus/sassafras/src/lib.rs +++ b/primitives/consensus/sassafras/src/lib.rs @@ -28,10 +28,6 @@ use sp_runtime::{ConsensusEngineId, RuntimeDebug}; use sp_std::vec::Vec; pub use sp_consensus_slots::{Slot, SlotDuration}; -pub use sp_core::bandersnatch::{ - ring_vrf::{RingContext, RingProver, RingVerifier, RingVrfSignature}, - vrf::{VrfInput, VrfOutput, VrfSignData, VrfSignature}, -}; #[cfg(feature = "serde")] use serde::{Deserialize, Serialize}; @@ -129,7 +125,7 @@ sp_api::decl_runtime_apis! { /// API necessary for block authorship with Sassafras. pub trait SassafrasApi { /// Get ring context to be used for ticket construction and verification. - fn ring_context() -> Option; + fn ring_context() -> Option; /// Submit next epoch validator tickets via an unsigned extrinsic. /// This method returns `false` when creation of the extrinsics fails. diff --git a/primitives/consensus/sassafras/src/ticket.rs b/primitives/consensus/sassafras/src/ticket.rs index 84b485aa8d7e3..42d9d64434dd8 100644 --- a/primitives/consensus/sassafras/src/ticket.rs +++ b/primitives/consensus/sassafras/src/ticket.rs @@ -17,7 +17,7 @@ //! Primitives related to tickets. -use crate::RingVrfSignature; +use crate::vrf::RingVrfSignature; use scale_codec::{Decode, Encode, MaxEncodedLen}; use scale_info::TypeInfo; @@ -26,9 +26,10 @@ pub use sp_core::ed25519::{Public as EphemeralPublic, Signature as EphemeralSign /// Ticket identifier. /// /// Its value is the output of a VRF whose inputs cannot be controlled by the -/// creator of the ticket (refer to [`ticket_id_vrf_input`] parameters). +/// ticket's creator (refer to [`crate::vrf::ticket_id_input`] parameters). /// Because of this, it is also used as the ticket score to compare against -/// the epoch ticket's threshold. +/// 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; /// Ticket data persisted on-chain. @@ -61,18 +62,22 @@ pub struct TicketClaim { pub erased_signature: EphemeralSignature, } -/// Computes the ticket-id maximum allowed values for a given epoch. +/// Computes ticket-id maximum allowed value for a given epoch. /// /// Only ticket identifiers below this threshold should be considered for slot /// assignment. /// -/// The value is computed as `T = TicketId::MAX*(redundancy*slots)/(attempts*validators)`, where: +/// The value is computed as +/// +/// TicketId::MAX*(redundancy*slots)/(attempts*validators) +/// +/// Where: /// - `redundancy`: redundancy factor; /// - `slots`: number of slots in epoch; /// - `attempts`: max number of tickets attempts per validator; /// - `validators`: number of validators in epoch. /// -/// If `attempts * validators = 0` then we fallback to 0. +/// If `attempts * validators = 0` then we return 0. pub fn ticket_id_threshold( redundancy: u32, slots: u32, diff --git a/primitives/consensus/sassafras/src/vrf.rs b/primitives/consensus/sassafras/src/vrf.rs index e32e739a53c87..e26679ba29236 100644 --- a/primitives/consensus/sassafras/src/vrf.rs +++ b/primitives/consensus/sassafras/src/vrf.rs @@ -17,14 +17,15 @@ //! Utilities related to VRF input, output and signatures. -use crate::{ - Randomness, TicketBody, TicketId, VrfInput, VrfOutput, VrfSignData, SASSAFRAS_ENGINE_ID, -}; +use crate::{Randomness, TicketBody, TicketId, SASSAFRAS_ENGINE_ID}; use scale_codec::Encode; use sp_consensus_slots::Slot; use sp_std::vec::Vec; -pub use sp_core::ed25519::{Public as EphemeralPublic, Signature as EphemeralSignature}; +pub use sp_core::bandersnatch::{ + ring_vrf::{RingContext, RingProver, RingVerifier, RingVrfSignature}, + vrf::{VrfInput, VrfOutput, VrfSignData, VrfSignature}, +}; fn vrf_input_from_data( domain: &[u8], From bc4a6976d33b1b179c45adec55e2abbe78cb4791 Mon Sep 17 00:00:00 2001 From: Davide Galassi Date: Thu, 24 Aug 2023 20:34:06 +0200 Subject: [PATCH 21/22] More ergonomic digest items --- primitives/consensus/sassafras/src/digests.rs | 55 +++++++++---------- primitives/consensus/sassafras/src/vrf.rs | 12 ++-- 2 files changed, 35 insertions(+), 32 deletions(-) diff --git a/primitives/consensus/sassafras/src/digests.rs b/primitives/consensus/sassafras/src/digests.rs index 0c77fe8f95fcc..95a305099de55 100644 --- a/primitives/consensus/sassafras/src/digests.rs +++ b/primitives/consensus/sassafras/src/digests.rs @@ -28,9 +28,11 @@ use scale_info::TypeInfo; use sp_runtime::{DigestItem, RuntimeDebug}; use sp_std::vec::Vec; -/// Sassafras slot assignment pre-digest. +/// Epoch slot claim digest entry. +/// +/// This is mandatory for each block. #[derive(Clone, RuntimeDebug, Encode, Decode, MaxEncodedLen, TypeInfo)] -pub struct PreDigest { +pub struct SlotClaim { /// Authority index that claimed the slot. pub authority_idx: AuthorityIndex, /// Corresponding slot number. @@ -43,18 +45,22 @@ pub struct PreDigest { /// Information about the next epoch. /// -/// This is broadcast in the first block of each epoch. +/// This is mandatory in the first block of each epoch. #[derive(Clone, PartialEq, Eq, Encode, Decode, RuntimeDebug)] pub struct NextEpochDescriptor { /// Authorities list. pub authorities: Vec, /// Epoch randomness. pub randomness: Randomness, - /// Configurable parameters. If not present previous epoch parameters are used. + /// Epoch configurable parameters. + /// + /// If not present previous epoch parameters are used. pub config: Option, } -/// Consensus log item. +/// Runtime digest entries. +/// +/// Entries which may be generated by on-chain code. #[derive(Decode, Encode, Clone, PartialEq, Eq)] pub enum ConsensusLog { /// Provides information about the next epoch parameters. @@ -65,35 +71,28 @@ pub enum ConsensusLog { OnDisabled(AuthorityIndex), } -/// A digest item which is usable by Sassafras. -pub trait CompatibleDigestItem { - /// Construct a digest item which contains a `PreDigest`. - fn sassafras_pre_digest(seal: PreDigest) -> Self; - - /// If this item is a `PreDigest`, return it. - fn as_sassafras_pre_digest(&self) -> Option; - - /// Construct a digest item which contains an `AuthoritySignature`. - fn sassafras_seal(signature: AuthoritySignature) -> Self; - - /// If this item is an `AuthoritySignature`, return it. - fn as_sassafras_seal(&self) -> Option; +impl TryFrom<&DigestItem> for SlotClaim { + type Error = (); + fn try_from(item: &DigestItem) -> Result { + item.pre_runtime_try_to(&SASSAFRAS_ENGINE_ID).ok_or(()) + } } -impl CompatibleDigestItem for DigestItem { - fn sassafras_pre_digest(digest: PreDigest) -> Self { - DigestItem::PreRuntime(SASSAFRAS_ENGINE_ID, digest.encode()) +impl From<&SlotClaim> for DigestItem { + fn from(claim: &SlotClaim) -> Self { + DigestItem::PreRuntime(SASSAFRAS_ENGINE_ID, claim.encode()) } +} - fn as_sassafras_pre_digest(&self) -> Option { - self.pre_runtime_try_to(&SASSAFRAS_ENGINE_ID) +impl TryFrom<&DigestItem> for AuthoritySignature { + type Error = (); + fn try_from(item: &DigestItem) -> Result { + item.seal_try_to(&SASSAFRAS_ENGINE_ID).ok_or(()) } +} - fn sassafras_seal(signature: AuthoritySignature) -> Self { +impl From<&AuthoritySignature> for DigestItem { + fn from(signature: &AuthoritySignature) -> Self { DigestItem::Seal(SASSAFRAS_ENGINE_ID, signature.encode()) } - - fn as_sassafras_seal(&self) -> Option { - self.seal_try_to(&SASSAFRAS_ENGINE_ID) - } } diff --git a/primitives/consensus/sassafras/src/vrf.rs b/primitives/consensus/sassafras/src/vrf.rs index e26679ba29236..52f9d45e5ab57 100644 --- a/primitives/consensus/sassafras/src/vrf.rs +++ b/primitives/consensus/sassafras/src/vrf.rs @@ -17,7 +17,7 @@ //! Utilities related to VRF input, output and signatures. -use crate::{Randomness, TicketBody, TicketId, SASSAFRAS_ENGINE_ID}; +use crate::{Randomness, TicketBody, TicketId}; use scale_codec::Encode; use sp_consensus_slots::Slot; use sp_std::vec::Vec; @@ -52,7 +52,11 @@ pub fn slot_claim_input(randomness: &Randomness, slot: Slot, epoch: u64) -> VrfI /// Signing-data to claim slot ownership during block production. pub fn slot_claim_sign_data(randomness: &Randomness, slot: Slot, epoch: u64) -> VrfSignData { let vrf_input = slot_claim_input(randomness, slot, epoch); - VrfSignData::new_unchecked(&SASSAFRAS_ENGINE_ID, Some("slot-claim-transcript"), Some(vrf_input)) + VrfSignData::new_unchecked( + b"sassafras-slot-claim-transcript-v1.0", + Option::<&[u8]>::None, + Some(vrf_input), + ) } /// VRF input to generate the ticket id. @@ -74,8 +78,8 @@ pub fn revealed_key_input(randomness: &Randomness, attempt: u32, epoch: u64) -> /// Data to be signed via ring-vrf. pub fn ticket_body_sign_data(ticket_body: &TicketBody, ticket_id_input: VrfInput) -> VrfSignData { VrfSignData::new_unchecked( - &SASSAFRAS_ENGINE_ID, - &[b"ticket-body-transcript", ticket_body.encode().as_slice()], + b"sassafras-ticket-body-transcript-v1.0", + Some(ticket_body.encode().as_slice()), Some(ticket_id_input), ) } From f62e9c93134f17ba6f413a59b2e84ca7dca048eb Mon Sep 17 00:00:00 2001 From: Davide Galassi Date: Thu, 24 Aug 2023 21:41:36 +0200 Subject: [PATCH 22/22] Zepter format --- Cargo.toml | 4 ++-- primitives/consensus/sassafras/Cargo.toml | 18 +++++++++--------- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 11ce10f87b218..7f7ee8abd7191 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -44,9 +44,9 @@ sc-keystore = { path = "client/keystore" } # Feature to unlock "Sassafras" experimental crates [features] -sassafras-experimental = [ - "sp-consensus-sassafras", +sassafras-experimental = [ "sc-keystore/bandersnatch-experimental", + "sp-consensus-sassafras", ] # Exists here to be backwards compatible and to support `cargo run` in the workspace. diff --git a/primitives/consensus/sassafras/Cargo.toml b/primitives/consensus/sassafras/Cargo.toml index 14de323c73617..56ae0087099e1 100644 --- a/primitives/consensus/sassafras/Cargo.toml +++ b/primitives/consensus/sassafras/Cargo.toml @@ -26,17 +26,17 @@ sp-runtime = { version = "24.0.0", default-features = false, path = "../../runti sp-std = { version = "8.0.0", default-features = false, path = "../../std" } [features] -default = ["std"] +default = [ "std" ] std = [ - "scale-codec/std", - "scale-info/std", - "serde/std", - "sp-api/std", - "sp-application-crypto/std", + "scale-codec/std", + "scale-info/std", + "serde/std", + "sp-api/std", + "sp-application-crypto/std", "sp-consensus-slots/std", - "sp-core/std", - "sp-runtime/std", - "sp-std/std", + "sp-core/std", + "sp-runtime/std", + "sp-std/std", ] # Serde support without relying on std features.