Sassafras primitives (#1249)

* Introduce Sassafras primitives

* Keystore workaround

* Fix doc

* Use  in keystore

* Improve bandersnatch vrf docs

* Apply review suggestions

* Update README

* Docs improvement

* Docs fix
This commit is contained in:
Davide Galassi
2023-08-31 13:38:11 +02:00
committed by GitHub
parent bdbe982970
commit f1f793718a
12 changed files with 802 additions and 206 deletions
@@ -0,0 +1,98 @@
// 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 digests structures and helpers.
use crate::{
ticket::TicketClaim, vrf::VrfSignature, AuthorityId, AuthorityIndex, AuthoritySignature,
EpochConfiguration, Randomness, Slot, SASSAFRAS_ENGINE_ID,
};
use scale_codec::{Decode, Encode, MaxEncodedLen};
use scale_info::TypeInfo;
use sp_runtime::{DigestItem, RuntimeDebug};
use sp_std::vec::Vec;
/// Epoch slot claim digest entry.
///
/// This is mandatory for each block.
#[derive(Clone, RuntimeDebug, Encode, Decode, MaxEncodedLen, TypeInfo)]
pub struct SlotClaim {
/// Authority index that claimed the slot.
pub authority_idx: AuthorityIndex,
/// Corresponding slot number.
pub slot: Slot,
/// Slot claim VRF signature.
pub vrf_signature: VrfSignature,
/// Ticket auxiliary information for claim check.
pub ticket_claim: Option<TicketClaim>,
}
/// Information about the next 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<AuthorityId>,
/// Epoch randomness.
pub randomness: Randomness,
/// Epoch configurable parameters.
///
/// If not present previous epoch parameters are used.
pub config: Option<EpochConfiguration>,
}
/// 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.
#[codec(index = 1)]
NextEpochData(NextEpochDescriptor),
/// Disable the authority with given index.
#[codec(index = 2)]
OnDisabled(AuthorityIndex),
}
impl TryFrom<&DigestItem> for SlotClaim {
type Error = ();
fn try_from(item: &DigestItem) -> Result<Self, Self::Error> {
item.pre_runtime_try_to(&SASSAFRAS_ENGINE_ID).ok_or(())
}
}
impl From<&SlotClaim> for DigestItem {
fn from(claim: &SlotClaim) -> Self {
DigestItem::PreRuntime(SASSAFRAS_ENGINE_ID, claim.encode())
}
}
impl TryFrom<&DigestItem> for AuthoritySignature {
type Error = ();
fn try_from(item: &DigestItem) -> Result<Self, Self::Error> {
item.seal_try_to(&SASSAFRAS_ENGINE_ID).ok_or(())
}
}
impl From<&AuthoritySignature> for DigestItem {
fn from(signature: &AuthoritySignature) -> Self {
DigestItem::Seal(SASSAFRAS_ENGINE_ID, signature.encode())
}
}
@@ -0,0 +1,169 @@
// 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.
//! Primitives for Sassafras consensus.
#![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;
use sp_core::crypto::KeyTypeId;
use sp_runtime::{ConsensusEngineId, RuntimeDebug};
use sp_std::vec::Vec;
pub use sp_consensus_slots::{Slot, SlotDuration};
#[cfg(feature = "serde")]
use serde::{Deserialize, Serialize};
pub mod digests;
pub mod ticket;
pub mod vrf;
pub use ticket::{
ticket_id_threshold, EphemeralPublic, EphemeralSignature, TicketBody, TicketClaim,
TicketEnvelope, TicketId,
};
mod app {
use sp_application_crypto::{app_crypto, bandersnatch, key_types::SASSAFRAS};
app_crypto!(bandersnatch, SASSAFRAS);
}
/// Key type identifier.
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;
/// 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<H> = sp_consensus_slots::EquivocationProof<H, AuthorityId>;
/// Randomness required by some protocol's operations.
pub type Randomness = [u8; RANDOMNESS_LENGTH];
/// Configuration data that can be modified on epoch change.
#[derive(
Copy, Clone, PartialEq, Eq, Encode, Decode, RuntimeDebug, MaxEncodedLen, TypeInfo, Default,
)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct EpochConfiguration {
/// Tickets threshold redundancy factor.
pub redundancy_factor: u32,
/// Tickets attempts for each validator.
pub attempts_number: u32,
}
/// Sassafras epoch information
#[derive(Debug, Clone, PartialEq, Eq, Encode, Decode, TypeInfo)]
pub struct Epoch {
/// The epoch index.
pub epoch_idx: u64,
/// The starting slot of the epoch.
pub start_slot: Slot,
/// Slot duration in milliseconds.
pub slot_duration: SlotDuration,
/// Duration of epoch in slots.
pub epoch_duration: u64,
/// Authorities for the epoch.
pub authorities: Vec<AuthorityId>,
/// Randomness for the epoch.
pub randomness: Randomness,
/// Epoch configuration.
pub config: EpochConfiguration,
}
/// 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)]
#[repr(transparent)]
pub struct OpaqueKeyOwnershipProof(Vec<u8>);
// 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<vrf::RingContext>;
/// 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<TicketEnvelope>) -> bool;
/// Get ticket id associated to the given slot.
fn slot_ticket_id(slot: Slot) -> Option<TicketId>;
/// 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.
fn generate_key_ownership_proof(authority_id: AuthorityId) -> Option<OpaqueKeyOwnershipProof>;
/// 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 `false` when creation of the extrinsic fails.
///
/// Only useful in an offchain context.
fn submit_report_equivocation_unsigned_extrinsic(
equivocation_proof: EquivocationProof<Block::Header>,
key_owner_proof: OpaqueKeyOwnershipProof,
) -> bool;
}
}
@@ -0,0 +1,91 @@
// 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.
//! Primitives related to tickets.
use crate::vrf::RingVrfSignature;
use scale_codec::{Decode, Encode, MaxEncodedLen};
use scale_info::TypeInfo;
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
/// 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 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.
#[derive(Debug, Clone, PartialEq, Eq, Encode, Decode, MaxEncodedLen, TypeInfo)]
pub struct TicketBody {
/// Attempt index.
pub attempt_idx: u32,
/// Ephemeral public key which gets erased when the ticket is claimed.
pub erased_public: EphemeralPublic,
/// Ephemeral public key which gets exposed when the ticket is claimed.
pub revealed_public: EphemeralPublic,
}
/// Ticket ring vrf signature.
pub type TicketSignature = RingVrfSignature;
/// Ticket envelope used on during submission.
#[derive(Debug, Clone, PartialEq, Eq, Encode, Decode, MaxEncodedLen, TypeInfo)]
pub struct TicketEnvelope {
/// Ticket body.
pub body: TicketBody,
/// Ring signature.
pub signature: TicketSignature,
}
/// Ticket claim information filled by the block author.
#[derive(Debug, Clone, PartialEq, Eq, Encode, Decode, MaxEncodedLen, TypeInfo)]
pub struct TicketClaim {
/// Signature verified via `TicketBody::erased_public`.
pub erased_signature: EphemeralSignature,
}
/// Computes 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 `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 return 0.
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())
}
@@ -0,0 +1,104 @@
// 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};
use scale_codec::Encode;
use sp_consensus_slots::Slot;
use sp_std::vec::Vec;
pub use sp_core::bandersnatch::{
ring_vrf::{RingContext, RingProver, RingVerifier, RingVrfSignature},
vrf::{VrfInput, VrfOutput, VrfSignData, VrfSignature},
};
fn vrf_input_from_data(
domain: &[u8],
data: impl IntoIterator<Item = impl AsRef<[u8]>>,
) -> VrfInput {
let buf = data.into_iter().fold(Vec::new(), |mut buf, item| {
let bytes = item.as_ref();
buf.extend_from_slice(bytes);
let len = u8::try_from(bytes.len()).expect("private function with well known inputs; qed");
buf.push(len);
buf
});
VrfInput::new(domain, buf)
}
/// VRF input to 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 input = slot_claim_input(randomness, slot, epoch);
VrfSignData::new_unchecked(
b"sassafras-slot-claim-transcript-v1.0",
Option::<&[u8]>::None,
Some(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(
b"sassafras-ticket-body-transcript-v1.0",
Some(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(input: &VrfInput, output: &VrfOutput) -> TicketId {
let bytes = output.make_bytes::<16>(b"ticket-id", 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(input: &VrfInput, output: &VrfOutput) -> [u8; 32] {
output.make_bytes::<32>(b"revealed-seed", input)
}