diff --git a/404.html b/404.html
index 91a3c53..7c653f4 100644
--- a/404.html
+++ b/404.html
@@ -91,7 +91,7 @@
diff --git a/approved/0001-agile-coretime.html b/approved/0001-agile-coretime.html
index 0824fa4..d686f48 100644
--- a/approved/0001-agile-coretime.html
+++ b/approved/0001-agile-coretime.html
@@ -90,7 +90,7 @@
diff --git a/approved/0005-coretime-interface.html b/approved/0005-coretime-interface.html
index 6aa2e25..b1a59c4 100644
--- a/approved/0005-coretime-interface.html
+++ b/approved/0005-coretime-interface.html
@@ -90,7 +90,7 @@
diff --git a/approved/0007-system-collator-selection.html b/approved/0007-system-collator-selection.html
index 08c7cfe..824975e 100644
--- a/approved/0007-system-collator-selection.html
+++ b/approved/0007-system-collator-selection.html
@@ -90,7 +90,7 @@
diff --git a/approved/0008-parachain-bootnodes-dht.html b/approved/0008-parachain-bootnodes-dht.html
index 6d3dfec..119eb1c 100644
--- a/approved/0008-parachain-bootnodes-dht.html
+++ b/approved/0008-parachain-bootnodes-dht.html
@@ -90,7 +90,7 @@
diff --git a/approved/0010-burn-coretime-revenue.html b/approved/0010-burn-coretime-revenue.html
index 70c8d86..a7fd98e 100644
--- a/approved/0010-burn-coretime-revenue.html
+++ b/approved/0010-burn-coretime-revenue.html
@@ -90,7 +90,7 @@
diff --git a/approved/0012-process-for-adding-new-collectives.html b/approved/0012-process-for-adding-new-collectives.html
index d260af9..395f689 100644
--- a/approved/0012-process-for-adding-new-collectives.html
+++ b/approved/0012-process-for-adding-new-collectives.html
@@ -90,7 +90,7 @@
diff --git a/approved/0013-prepare-blockbuilder-and-core-runtime-apis-for-mbms.html b/approved/0013-prepare-blockbuilder-and-core-runtime-apis-for-mbms.html
index 3cf734a..79153b8 100644
--- a/approved/0013-prepare-blockbuilder-and-core-runtime-apis-for-mbms.html
+++ b/approved/0013-prepare-blockbuilder-and-core-runtime-apis-for-mbms.html
@@ -90,7 +90,7 @@
diff --git a/approved/0014-improve-locking-mechanism-for-parachains.html b/approved/0014-improve-locking-mechanism-for-parachains.html
index cf4eb81..9963d20 100644
--- a/approved/0014-improve-locking-mechanism-for-parachains.html
+++ b/approved/0014-improve-locking-mechanism-for-parachains.html
@@ -90,7 +90,7 @@
diff --git a/approved/0022-adopt-encointer-runtime.html b/approved/0022-adopt-encointer-runtime.html
index 69ab0fb..43674c2 100644
--- a/approved/0022-adopt-encointer-runtime.html
+++ b/approved/0022-adopt-encointer-runtime.html
@@ -90,7 +90,7 @@
diff --git a/approved/0032-minimal-relay.html b/approved/0032-minimal-relay.html
index 564c5e8..89e08d8 100644
--- a/approved/0032-minimal-relay.html
+++ b/approved/0032-minimal-relay.html
@@ -90,7 +90,7 @@
diff --git a/approved/0042-extrinsics-state-version.html b/approved/0042-extrinsics-state-version.html
index ee3f885..b62c8e6 100644
--- a/approved/0042-extrinsics-state-version.html
+++ b/approved/0042-extrinsics-state-version.html
@@ -90,7 +90,7 @@
diff --git a/approved/0043-storage-proof-size-hostfunction.html b/approved/0043-storage-proof-size-hostfunction.html
index 1753a4f..72ea14e 100644
--- a/approved/0043-storage-proof-size-hostfunction.html
+++ b/approved/0043-storage-proof-size-hostfunction.html
@@ -90,7 +90,7 @@
diff --git a/approved/0045-nft-deposits-asset-hub.html b/approved/0045-nft-deposits-asset-hub.html
index e6ef5f6..48aceed 100644
--- a/approved/0045-nft-deposits-asset-hub.html
+++ b/approved/0045-nft-deposits-asset-hub.html
@@ -90,7 +90,7 @@
diff --git a/approved/0047-assignment-of-availability-chunks.html b/approved/0047-assignment-of-availability-chunks.html
index 3ff2e7b..9532891 100644
--- a/approved/0047-assignment-of-availability-chunks.html
+++ b/approved/0047-assignment-of-availability-chunks.html
@@ -90,7 +90,7 @@
diff --git a/approved/0050-fellowship-salaries.html b/approved/0050-fellowship-salaries.html
index c34d2ad..760cc52 100644
--- a/approved/0050-fellowship-salaries.html
+++ b/approved/0050-fellowship-salaries.html
@@ -90,7 +90,7 @@
diff --git a/approved/0056-one-transaction-per-notification.html b/approved/0056-one-transaction-per-notification.html
index fb61752..a6ed1d6 100644
--- a/approved/0056-one-transaction-per-notification.html
+++ b/approved/0056-one-transaction-per-notification.html
@@ -90,7 +90,7 @@
diff --git a/approved/0059-nodes-capabilities-discovery.html b/approved/0059-nodes-capabilities-discovery.html
index 9a9b2ed..9ce51a1 100644
--- a/approved/0059-nodes-capabilities-discovery.html
+++ b/approved/0059-nodes-capabilities-discovery.html
@@ -90,7 +90,7 @@
diff --git a/approved/0078-merkleized-metadata.html b/approved/0078-merkleized-metadata.html
index 7ad6b1b..e165aed 100644
--- a/approved/0078-merkleized-metadata.html
+++ b/approved/0078-merkleized-metadata.html
@@ -90,7 +90,7 @@
diff --git a/approved/0084-general-transaction-extrinsic-format.html b/approved/0084-general-transaction-extrinsic-format.html
index 9ba21f1..d1c516e 100644
--- a/approved/0084-general-transaction-extrinsic-format.html
+++ b/approved/0084-general-transaction-extrinsic-format.html
@@ -90,7 +90,7 @@
diff --git a/index.html b/index.html
index 9245bf5..ba72c60 100644
--- a/index.html
+++ b/index.html
@@ -90,7 +90,7 @@
diff --git a/introduction.html b/introduction.html
index 9245bf5..ba72c60 100644
--- a/introduction.html
+++ b/introduction.html
@@ -90,7 +90,7 @@
diff --git a/new/0088-broker-pallet-slashable-deposit-purchaser-reputation-reserved-cores.html b/new/0088-broker-pallet-slashable-deposit-purchaser-reputation-reserved-cores.html
index 7ada5ea..d8d2c93 100644
--- a/new/0088-broker-pallet-slashable-deposit-purchaser-reputation-reserved-cores.html
+++ b/new/0088-broker-pallet-slashable-deposit-purchaser-reputation-reserved-cores.html
@@ -90,7 +90,7 @@
@@ -291,7 +291,7 @@
-
+
@@ -305,7 +305,7 @@
-
+
diff --git a/print.html b/print.html
index 918fffc..9d46588 100644
--- a/print.html
+++ b/print.html
@@ -91,7 +91,7 @@
@@ -288,6 +288,859 @@ detailing proposed changes to the technical implementation of the Polkadot netwo
None
None
+(source)
+Table of Contents
+
+
+ | |
+| Start Date | September 06, 2023 |
+| Description | Sassafras consensus protocol specification |
+| Authors | Davide Galassi |
+
+
+
+Sassafras is a novel consensus protocol designed to address the recurring
+fork-related challenges encountered in other lottery-based protocols.
+The protocol aims to create a mapping between each epoch's slots and the
+validators set while ensuring that the identity of validators assigned to
+the slots remains undisclosed until the slot is actively claimed during block
+production.
+
+Sassafras Protocol has been rigorously detailed in a comprehensive
+research paper authored by the
+Web3 foundation research team.
+This RFC is primarily intended to detail the critical implementation aspects
+vital for ensuring interoperability and to clarify certain aspects that are
+left open by the research paper and thus subject to interpretation during
+implementation.
+
+This RFC focuses on providing implementors with the necessary insights into the
+protocol's operation.
+In instances of inconsistency between this document and the research paper,
+this RFC should be considered authoritative to eliminate ambiguities and ensure
+interoperability.
+
+Beyond promoting interoperability, this RFC also aims to facilitate the
+implementation of Sassafras within the Polkadot ecosystem.
+Although the specifics of deployment strategies are beyond the scope of this
+document, it lays the groundwork for the integration of Sassafras into the
+Polkadot network.
+
+
+Developers responsible for creating blockchains who intend to leverage the
+benefits offered by the Sassafras Protocol.
+
+Developers contributing to the Polkadot ecosystem, both relay-chain and
+para-chains.
+The protocol will have a central role in the next generation block authoring
+consensus systems.
+
+This section outlines the notation and conventions adopted throughout this
+document to ensure clarity and consistency.
+
+Data structures are primarily defined using standard ASN.1,
+syntax with few exceptions
+To ensure interoperability of serialized structures, the order of the fields
+must match the structures definitions found within this document.
+
+We define some type alias to make ASN.1 syntax more intuitive.
+
+- Unsigned integer:
Unsigned ::= INTEGER (0..MAX)
+- n bits unsigned integer:
Unsigned<n> ::= INTEGER (0..2^n - 1)
+
+- 8 bits unsigned integer (octet)
Unsigned8 ::= Unsigned<8>
+- 32 bits unsigned integer:
Unsigned32 ::= Unsigned<32>
+- 64 bits unsigned integer:
Unsigned64 ::= Unsigned<64>
+
+
+- Non-homogeneous sequence (struct/tuple):
Sequence ::= SEQUENCE
+- Homogeneous sequence (vector):
Sequence<T> ::= SEQUENCE OF T
+E.g. Sequence<Unsigned> ::= SEQUENCE OF Unsigned
+- Fixed length homogeneous sequence:
Sequence<T,n> ::= Sequence<T> (SIZE(n))
+- Octet string alias:
OctetString ::= Sequence<Unsigned8>
+- Fixed length octet string:
OctetString<n> ::= Sequence<Unsigned8, n>
+- Optional value:
Option<T> ::= T OPTIONAL
+
+
+It is advantageous to make use of code snippets as part of the protocol
+description. As a convention, the code is formatted in a style similar to
+Rust, and can make use of the following set of predefined functions:
+Syntax:
+
+-
+
ENCODE(x: T) -> OctetString: encodes x as an OctetString using
+SCALE codec.
+
+-
+
DECODE<T>(x: OctetString) -> T: decodes x as a value with type T using
+SCALE codec.
+
+-
+
BLAKE2(n: Unsigned, x: OctetString) -> OctetString<n>: standard Blake2b hash.
+
+-
+
CONCAT(x₀: OctetString, ..., xₖ: OctetString) -> OctetString: concatenate the
+inputs octets.
+
+-
+
LENGTH(x: Sequence) -> Unsigned: returns the number of elements in x.
+
+-
+
GET(seq: Sequence<T>, i: Unsigned) -> T: returns the i-th element of a sequence.
+
+-
+
PUSH(seq: Sequence<T>, x: T): append x as the new last element of the sequence.
+
+-
+
POP(seq: Sequence<T>) -> T: extract and returns the last element of a sequence.
+
+
+
+More types and helper functions are introduced incrementally as they become
+relevant within the document's context.
+
+The timeline is segmented into a sequentially ordered sequence of slots.
+This entire sequence of slots is then further partitioned into distinct segments
+known as epochs.
+The Sassafras protocol aims to map each slot within an epoch to the designated
+validators for that epoch, utilizing a ticketing system.
+The protocol operation can be roughly divided into five phases:
+
+Each of the validators associated to the target epoch generates and submits
+a set of candidate tickets to the blockchain. Every ticket is bundled with an
+anonymous proof of validity.
+
+Each candidate ticket undergoes a validation process for the associated validity
+proof and compliance with other protocol-specific constraints.
+
+After collecting all valid candidate tickets, a deterministic method is used to
+uniquely associate a subset of these tickets with the slots of the target epoch.
+
+During the block production phase of the target epoch, validators are required
+to demonstrate their ownership of tickets. This step discloses the identity of
+the ticket owners.
+
+It's important to note that this section is not intended to serve as an
+exhaustive exploration of the mathematically intensive foundations of the
+cryptographic primitive. Rather, its primary aim is to offer a concise and
+accessible explanation of the primitive's role and usage which is relevant
+within the scope of this RFC.
+For an in-depth explanation, refer to the Bandersnatch VRF
+spec
+Bandersnatch VRF can be used in two flavors:
+
+- Bare VRF: extends the IETF ECVRF RFC 9381,
+- Ring VRF: provides anonymous signatures by leveraging a zk-SNARK.
+
+Together with the input, which determines the signed VRF output, both the
+flavors offer the capability to sign some arbitrary additional data (extra)
+which doesn't contribute to the VRF output.
+
+Function to construct a VrfSignature.
+#![allow(unused)]
+fn main() {
+ fn vrf_sign(
+ secret: BandernatchSecretKey,
+ input: OctetString,
+ extra: OctetString,
+ ) -> VrfSignature
+}
+Function for signature verification returning a Boolean value indicating the
+validity of the signature (1 on success):
+#![allow(unused)]
+fn main() {
+ fn vrf_verify(
+ public: PublicKey,
+ input: OctetString,
+ extra: OctetString,
+ signature: VrfSignature
+ ) -> Unsigned<1>;
+}
+Function to derive the VRF output from input and secret:
+#![allow(unused)]
+fn main() {
+ fn vrf_output(
+ secret: BandernatchSecretKey,
+ input: OctetString,
+ ) -> OctetString<32>;
+}
+Function to derive the VRF output from a signature:
+#![allow(unused)]
+fn main() {
+ fn vrf_signed_output(
+ signature: VrfSignature,
+ ) -> OctetString<32>;
+}
+Note that the following condition is always satisfied:
+#![allow(unused)]
+fn main() {
+ let signature = vrf_sign(secret, input, extra);
+ vrf_output(secret, input) == vrf_signed_output(signature)
+}
+In this document, the types SecretKey, PublicKey and VrfSignature are
+intentionally left undefined. Their definitions can be found in the Bandersnatch
+VRF specification and related documents.
+
+Function to construct RingVrfSignature.
+#![allow(unused)]
+fn main() {
+ fn ring_vrf_sign(
+ secret: SecretKey,
+ prover: RingProverKey,
+ input: OctetString,
+ extra: OctetString,
+ ) -> RingVrfSignature;
+}
+Function for signature verification returning a Boolean value
+indicating the validity of the signature (1 on success).
+Note that this function doesn't require the signer's public key.
+#![allow(unused)]
+fn main() {
+ fn ring_vrf_verify(
+ verifier: RingVerifierKey,
+ input: OctetString,
+ extra: OctetString,
+ signature: RingVrfSignature,
+ ) -> Unsigned<1>;
+}
+Function to derive the VRF output from a ring signature:
+#![allow(unused)]
+fn main() {
+ fn ring_vrf_signed_output(
+ signature: RingVrfSignature,
+ ) -> OctetString<32>;
+}
+Note that the following condition is always satisfied:
+#![allow(unused)]
+fn main() {
+ let signature = vrf_sign(secret, input, extra);
+ let ring_signature = ring_vrf_sign(secret, prover, input, extra);
+ vrf_signed_output(plain_signature) == ring_vrf_signed_output(ring_signature);
+}
+In this document, the types RingProverKey, RingVerifierKey, and
+RingSignature are intentionally left undefined. Their definitions can be found
+in the Bandersnatch VRF specification and related documents.
+
+
+The ProtocolConfiguration is constant and primarily influences certain checks
+carried out during tickets validation. It is defined as:
+#![allow(unused)]
+fn main() {
+ ProtocolConfiguration ::= Sequence {
+ epoch_length: Unsigned32,
+ attempts_number: Unsigned8,
+ redundancy_factor: Unsigned8,
+ }
+}
+Where:
+
+epoch_length: number of slots for each epoch.
+attempts_number: maximum number of tickets that each validator for the next
+epoch is allowed to submit.
+redundancy_factor: expected ratio between epoch's slots and the cumulative
+number of tickets which can be submitted by the set of epoch validators.
+
+The attempts_number influences the anonymity of block producers. As all
+published tickets have a public attempt number less than attempts_number,
+all the tickets which share the attempt number value must belong to different
+block producers, which reduces anonymity late as we approach the epoch tail.
+Bigger values guarantee more anonymity but also more computation.
+Details about how exactly these parameters drives the ticket validity
+probability can be found in section 6.2.2.
+
+Each block's header contains a Digest, which is a sequence of DigestItems
+where the protocol is allowed to append any information required for correct
+progress.
+The structures are defined to be quite generic and usable by other subsystems:
+#![allow(unused)]
+fn main() {
+ DigestItem ::= Sequence {
+ id: OctetString<4>,
+ data: OctetString
+ }
+
+ Digest ::= Sequence<DigestItem>
+}
+For Sassafras related DiegestItems the id is set to the constant ASCII string "SASS".
+
+On-Chain, we maintain a sequence with four randomness entries.
+#![allow(unused)]
+fn main() {
+ RandomnessBuffer ::= Sequence<OctetString<32>, 4>
+}
+During epoch N
+
+-
+
The first entry of the buffer is the current randomness accumulator value
+and incorporates verifiable random elements from all previously executed
+blocks. The exact accumulation procedure is described in section
+6.7.
+
+-
+
The second entry of the buffer is the snapshot of the accumulator after the
+execution of the last block of epoch N-1.
+
+-
+
The third entry of the buffer is the snapshot of the accumulator after the
+execution of the last block of epoch N-2.
+
+-
+
The fourth entry of the buffer is the snapshot of the accumulator after the
+execution of the last block of epoch N-3.
+
+
+The buffer is entries are updated after block execution.
+
+The first block produced during an epoch N must include a descriptor for some
+of the subsequent epoch (N+1) parameters. This descriptor is defined as:
+#![allow(unused)]
+fn main() {
+ NextEpochDescriptor ::= Sequence {
+ randomness: OctetString<32>,
+ authorities: Sequence<PublicKey>,
+ }
+}
+Where:
+
+randomness: last randomness accumulator snapshot, which must be equivalent
+to GET(RandomnessBuffer, 1) after block execution.
+authorities: list of validators scheduled for next epoch.
+
+This descriptor is SCALE encoded and embedded in the block header's digest
+log.
+A special case arises for the first block of epoch 0, which each node produces
+independently during the genesis phase. In this case, the NextEpochDescriptor
+relative to epoch 1 is shared within the second block, as outlined in section
+6.4.1.
+
+Some of the initial parameters for the first epoch, Epoch #0, are set through
+the genesis configuration, which is defined as:
+#![allow(unused)]
+fn main() {
+ GenesisConfig ::= Sequence {
+ authorities: Sequence<PublicKey>,
+ }
+}
+The on-chain randomness accumulator is initialized only after the genesis
+block is produced, and its value is set to the hash of the genesis block.
+Since block #0 is generated locally by each node as part of the genesis
+process, the first block that a validator explicitly produces for Epoch
+#0 is block #1. Therefore, block #1 is required to contain the
+NextEpochDescriptor for the following epoch, Epoch #1.
+The NextEpochDescriptor for Epoch #1:
+
+randomness: computed using the randomness_accumulator established
+post-genesis, as mentioned above.
+authorities: the same as those specified in the genesis configuration.
+
+
+During epoch N, each validator associated to epoch N+2 constructs a set of
+tickets which may be eligible (6.5.2) to be delivered
+to on-chain proxies, which are the validators scheduled for epoch N+1.
+These tickets are constructed using the on-chain randomness snapshot taken
+after the execution of the last block of epoch N-1 together with other
+parameters and aims to secure ownership of one or more slots of epoch N+2.
+Each validator is allowed to submit a maximum number of tickets, constrained by
+attempts_number field of the ProtocolConfiguration.
+The ideal timing for the candidate validator to start constructing the tickets
+is subject to strategy. A recommended approach is to initiate tickets creation
+once the last block of epoch N-1 is either probabilistically or, even better,
+deterministically finalized. This delay is suggested to prevent wasting
+resources creating tickets that might become unusable if a different chain
+branch is chosen as the canonical one.
+As said, proxies collect tickets during epoch N and when epoch N+1 begins
+the collected tickets are submitted on-chain.
+TODO (inherents/ unsigned ext?).
+
+Each ticket has an associated identifier defined as:
+#![allow(unused)]
+fn main() {
+ TicketId ::= OctetString<32>;
+}
+The value of the TicketId is completely determined by the output of the
+Bandersnatch VRF with the following unbiasable input:
+#![allow(unused)]
+fn main() {
+ let ticket_vrf_input = CONCAT(
+ BYTES("sassafras_ticket"),
+ GET(randomness_buffer, 1),
+ BYTES(attempt_index)
+ );
+
+ let ticket_id = vrf_output(AUTHORITY_SECRET_KEY, ticket_vrf_input);
+}
+Where:
+
+randomness_buffer: on-chain RandomnessBuffer instance, in particular we
+use the snapshot after the execution of previous epoch's last block.
+attempt_index: value going from 0 to the configuration attempts_number - 1.
+
+
+A TicketId value is valid for on-chain submission if its value, when interpreted
+as a big-endian 256-bit integer normalized as a float within the range [0..1],
+is less than the ticket threshold computed as:
+T = (r·s)/(a·v)
+
+Where:
+
+v: epoch's validators number
+s: epoch's slots number
+r: redundancy factor
+a: attempts number
+T: ticket threshold value (0 ≤ T ≤ 1)
+
+In an epoch with s slots, the goal is to achieve an expected number of tickets
+for block production equal to r·s.
+It's crucial to ensure that the probability of having fewer than s winning
+tickets is very low, even in scenarios where up to 1/3 of the authorities
+might be offline.
+To accomplish this, we first define the winning probability of a single ticket
+as T = (r·s)/(a·v).
+Let n be the actual number of participating validators, where v·2/3 ≤ n ≤ v.
+These n validators each make a attempts, for a total of a·n attempts.
+Let X be the random variable associated to the number of winning tickets, then
+its expected value is:
+E[X] = T·a·n = (r·s·n)/v
+
+By setting r = 2, we get
+s·4/3 ≤ E[X] ≤ s·2
+
+Using Bernestein's inequality we get Pr[X < s] ≤ e^(-s/21).
+For instance, with s = 600 this results in Pr[X < s] < 4·10⁻¹³.
+Consequently, this approach offers considerable tolerance for offline nodes and
+ensures that all slots are likely to be filled with tickets.
+For more details about threshold formula please refer to the
+probabilities and parameters
+paragraph in the Web3 foundation description of the protocol.
+
+Every ticket candidate has an associated body, defined as:
+#![allow(unused)]
+fn main() {
+ TicketBody ::= Sequence {
+ attempt_index: Unsigned8,
+ opaque: OctetString,
+ }
+}
+Where:
+
+attempt_index: index used to generate the associated TicketId.
+opaque: additional data for user-defined applications.
+
+
+TicketBody must be signed using the Bandersnatch Ring VRF flavor (5.4.2).
+#![allow(unused)]
+fn main() {
+ let signature = ring_vrf_sign(
+ secret_key,
+ ring_prover_key
+ ticket_vrf_input,
+ ENCODE(ticket_body),
+ );
+}
+ring_prover_key object is constructed using the set of public keys which
+belong to the target epoch's validators and the zk-SNARK context parameters
+(for more details refer to the Bandersnatch VRFs specification).
+Finally, the body and the ring signature are combined within the TicketEnvelope:
+#![allow(unused)]
+fn main() {
+ TicketEnvelope ::= Sequence {
+ ticket_body: TicketBody,
+ ring_signature: RingVrfSignature
+ }
+}
+
+All the actions in the steps described by this paragraph are executed by
+on-chain code.
+Validation rules:
+
+-
+
Ring signature is verified using the on-chain ring_verifier_key derived by the
+static ring context parameters and the next epoch validators public keys.
+
+-
+
Ticket identifier is locally recomputed from the RingVrfSignature and its value
+is checked to be less than the tickets' threshold.
+
+-
+
Tickets submissions can't occur within a block part of the epoch's tail, which
+are a given number of the slots at the end of the epoch. The tail length is a
+configuration value (e.g. 1/6 of epoch length) part of the configuration.
+This constraint is to give time to the on-chain tickets to be probabilistically
+(or even better deterministically) finalized and thus further reduce the fork chances.
+
+-
+
All tickets which are proposed within a block must be valid and all of them
+must end up in the on-chain queue. That is, no submitted ticket should be
+discarded.
+
+-
+
No duplicates are allowed.
+
+
+If at least one of the checks fails then the block must be discarded.
+Valid tickets bodies, together with the ticket identifiers, are all persisted on-chain
+and kept incrementally sorted according to the TicketId interpreted as a 256-bit
+big-endian unsigned integer.
+Pseudo-code for ticket validation for steps 1 and 2:
+#![allow(unused)]
+fn main() {
+ let ticket_vrf_input = CONCAT(
+ BYTES("sassafras_ticket"),
+ GET(randomness_buffer, 2),
+ BYTES(envelope.body.attempt_index)
+ );
+
+ let result = ring_vrf_verify(
+ verifier,
+ ticket_vrf_input,
+ ENCODE(ticket_body),
+ envelope.ring_signature
+ );
+ assert(result == 1);
+
+ let ticket_id = ring_vrf_signed_output(envelope.ring_signature);
+ assert(ticket_id < ticket_threshold);
+}
+
+Before the beginning of the claiming phase (i.e. what we've called the target
+epoch), the on-chain list of tickets must be associated with the next epoch's
+slots such that there must be at most one ticket per slot.
+Given an ordered sequence of tickets [t₀, t₁, ..., tₙ] to be assigned to
+n slots, the tickets are allocated according to the following outside-in
+strategy:
+ slot_index : [ 0, 1, 2, 3 , ... ]
+ tickets : [ t₀, tₙ, t₁, tₙ₋₁, ... ]
+
+Here slot-index is a relative value computed as:
+slot_index = slot - epoch_start_slot
+
+The association between each ticket and a slot is recorded on-chain and thus
+is public. What remains confidential is the identity of the ticket's author, and
+consequently, who possesses the validator to claim the corresponding slot. This
+information is known only to the author of the ticket.
+If the number of published tickets is less than the number of epoch slots,
+some orphan slots in the end of the epoch will remain unbounded to any ticket.
+For claiming strategy refer to 6.8.2.
+Note that this situation always apply to the first epochs after genesis.
+
+With tickets bound to epoch slots, every validator acquires information about
+the slots for which they are supposed to produce a block.
+The procedure for slot claiming depends on whether a given slot has an
+associated ticket according to the on-chain state.
+If a slot is associated with a ticket, the primary authoring method is used.
+Conversely, the protocol resorts to the secondary method as a fallback.
+
+We can proceed to claim a slot using the primary method if we are the
+legit owner of the ticket associated to the given slot.
+Let randomness_buffer be the instance of RandomnessBuffer stored in the
+chain state and ticket_body be the TicketBody that is associated to the
+slot to claim, the VRF input for slot claiming is constructed as:
+#![allow(unused)]
+fn main() {
+ let seal_vrf_input = CONCAT(
+ BYTES("sassafras_ticket"),
+ GET(randomness_buffer, 3),
+ BYTES(ticket_body.attempt_index)
+ );
+}
+This seal_vrf_input, when signed with the correct validator secret key must
+generate the same TicketId associated on-chain to the target slot.
+
+Given that the authorities registered on-chain are kept in an ordered list,
+the index of the validator which has the privilege to claim an orphan slot
+is given by the following procedure:
+#![allow(unused)]
+fn main() {
+ let hash_input = CONCAT(
+ GET(randomness_buffer, 2),
+ relative_slot_index,
+ );
+ let hash = BLAKE2(hash_input);
+ let index_bytes = CONCAT(GET(hash, 0), GET(hash, 1), GET(hash, 2), GET(hash, 3));
+ let index = DECODE<Unsigned32>(index_bytes) % LENGTH(authorities);
+}
+With relative_slot_index the slot offset relative to the epoch's start and authorities
+the Sequence of current epoch validators.
+Let randomness_buffer be the instance of RandomnessBuffer stored in on-chain state
+then the VRF input for slot claiming is constructed as:
+#![allow(unused)]
+fn main() {
+ let seal_vrf_input = CONCAT(
+ BYTES("sassafras_fallback"),
+ GET(randomness_buffer, 3),
+ );
+}
+
+The slot claim data is a digest entry which contains additional information
+which is required by the protocol in order to verify the block:
+#![allow(unused)]
+fn main() {
+ ClaimData ::= Sequence {
+ slot: Unsigned32,
+ validator_index: Unsigned32,
+ randomness_source: VrfSignature,
+ }
+}
+
+slot: the slot number
+validator_index: block's author index relative to the on-chain validators sequence.
+randomness_source: VRF signature used to generate per-block randomness.
+
+Given the seal_vrf_input constructed using the primary or secondary method,
+the claim is derived as follows:
+#![allow(unused)]
+fn main() {
+ let randomness_vrf_input = CONCAT(
+ BYTES("sassafras_randomness"),
+ vrf_output(AUTHORITY_SECRET_KEY, seal_vrf_input)
+ );
+
+ let randomness_source = vrf_sign(
+ AUTHORITY_SECRET_KEY,
+ randomness_vrf_input,
+ []
+ );
+
+ let claim = ClaimData {
+ slot,
+ validator_index,
+ randomness_source,
+ }
+}
+The claim object is SCALE encoded and pushed into the header digest log.
+
+A block is sealed as follows:
+#![allow(unused)]
+fn main() {
+ let unsealed_header_bytes = ENCODE(header);
+
+ let seal = vrf_sign(
+ AUTHORITY_SECRET_KEY,
+ seal_vrf_input,
+ unsealed_header_bytes
+ );
+
+ PUSH(header.digest, ENCODE(seal));
+}
+With header the block's header without the seal digest log entry.
+The seal object is a VrfSignature instance, which is SCALE encoded and
+pushed as the last entry of the block's header digest log.
+
+The last entry is extracted from the header digest log, and is interpreted as
+the seal VrfSignature. The unsealed header is then SCALE encoded in order to
+be verified.
+The next entry is extracted from the header digest log, and is interpreted as a
+ClaimData instance.
+The validity of the signatures is then verified using as the public key the
+validator key corresponding to the validator_index found in the ClaimData,
+together with the VRF input (which depends on primary/secondary method) and
+additional data expected to have been used by the block author.
+#![allow(unused)]
+fn main() {
+ let seal_signature = DECODE<VrfSignature>(POP(header.digest));
+ let unsealed_header_bytes = ENCODE(header);
+ let claim_data = DECODE<ClaimData>(POP(header.digest));
+
+ let public_key = GET(authorities, claim_data.validator_index);
+
+ let result = vrf_verify(
+ public_key,
+ seal_vrf_input,
+ unsealed_header_bytes,
+ seal_signature
+ );
+ assert(result == 1);
+
+ let randomness_vrf_input = vrf_signed_output(seal_signature);
+
+ let result = vrf_verify(
+ public_key,
+ randomness_vrf_input,
+ [],
+ claim_data.randomness_source
+ );
+ assert(result == 1);
+}
+With:
+
+header: the block's header.
+authorities: sequence of authorities for the epoch, as recorded on-chain.
+seal_vrf_input: VRF seal input data constructed as specified in 6.8.
+
+If signatures verification is successful, then the verification process diverges
+based on whether the slot is associated with a ticket according to the on-chain
+state.
+
+For slots tied to a ticket, the primary verification method is employed.
+This method verifies ticket ownership using the TicketId associated to the slot.
+#![allow(unused)]
+fn main() {
+ let ticket_id = vrf_signed_output(seal_signature);
+ assert(ticket_id == expected_ticket_id);
+}
+With expected_ticket_id the ticket identifier committed on-chain together
+with the associated ticket_body.
+
+If the slot doesn't have any associated ticket then the validator index contained in
+the claim data must match the one given by the procedure outlined in section
+6.8.2.
+
+The randomness accumulator is updated using the randomness_source signature found
+within the ClaimData object.
+In particular, fresh randomness is derived and accumulated after block
+execution as follows:
+#![allow(unused)]
+fn main() {
+ let fresh_randomness = vrf_signed_output(claim.randomness_source);
+
+ let prev_accumulator = POP(randomness_buffer);
+ let curr_accumulator = BLAKE2(CONCAT(randomness_accumulator, fresh_randomness));
+ PUSH(randomness_buffer, curr_accumulator);
+}
+
+None
+
+It is critical that implementations of this RFC undergo thorough testing on
+test networks.
+A security audit may be desirable to ensure the implementation does not
+introduce unwanted side effects.
+
+
+Adopting Sassafras consensus marks a significant improvement in reducing the
+frequency of short-lived forks.
+Forks are eliminated by design. Forks may only result from network disruptions
+or protocol attacks. In such cases, the choice of which fork to follow upon
+recovery is clear-cut, with only one valid option.
+
+No specific considerations.
+
+The adoption of Sassafras affects the native client and thus can't be introduced
+just via a runtime upgrade.
+A deployment strategy should be carefully engineered for live networks.
+This subject is left open for a dedicated RFC.
+
+
+
+None
+
+While this RFC lays the groundwork and outlines the core aspects of the
+protocol, several crucial topics remain to be addressed in future RFCs.
+
+
+-
+
Outbound Interfaces: Interfaces that the host environment provides to the
+on-chain code, typically known as Host Functions.
+
+-
+
Unrecorded Inbound Interfaces. Interfaces that the on-chain code provides
+to the host environment, typically known as Runtime APIs.
+
+-
+
Transactional Inbound Interfaces. Interfaces that the on-chain code provides
+to the world to alter the chain state, typically known as Transactions
+(or extrinsics in the Polkadot ecosystem)
+
+
+
+
+- Protocol Migration. Exploring how this protocol can seamlessly replace an
+already operational instance of another protocol. Future RFCs may focus on
+deployment strategies to facilitate a smooth transition.
+
+
+
+- Procedure: Determining the procedure for the zk-SNARK URS (Universal
+Reference String) initialization. Future RFCs may provide insights into
+whether this process should include an ad-hoc initialization ceremony or if
+we can reuse an SRS from another ecosystem (e.g. Zcash or Ethereum).
+
+
+
+- Mixnet Integration: Submitting tickets directly to the relay/proxy can
+pose a risk of potential deanonymization through traffic analysis. Subsequent
+RFCs may investigate the potential for incorporating Mixnet protocol or
+other privacy-enhancing mechanisms to address this concern.
+
(source)
Table of Contents
@@ -4276,981 +5129,6 @@ Also note that child tries aren't considered as descendants of the main trie whe
As a side note, the root track confirmation period is 24 hours.
This RFC hopefully reminds the greater Polkadot community that it is possible to submit changes to the parameters of Polkadot OpenGov, and the greater protocol as a whole through the RFC process.
-(source)
-Table of Contents
-
-
- | |
-| Start Date | September 06, 2023 |
-| Description | Sassafras consensus protocol description and structures |
-| Authors | Davide Galassi |
-
-
-
-Sassafras is a novel consensus protocol designed to address the recurring
-fork-related challenges encountered in other lottery-based protocols.
-The protocol aims to create a mapping between each epoch's slots and the
-validators set while ensuring that the identity of validators assigned to
-the slots remains undisclosed until the slot is actively claimed during block
-production.
-
-Sassafras Protocol has been rigorously detailed in a comprehensive
-research paper authored by the
-Web3 foundation research team.
-This RFC is primarily intended to detail the critical implementation aspects
-vital for ensuring interoperability and to clarify certain aspects that are
-left open by the research paper and thus subject to interpretation during
-implementation.
-
-This RFC focuses on providing implementors with the necessary insights into the
-protocol's operation.
-In instances of inconsistency between this document and the research paper,
-this RFC should be considered authoritative to eliminate ambiguities and ensure
-interoperability.
-
-Beyond promoting interoperability, this RFC also aims to facilitate the
-implementation of Sassafras within the Polkadot ecosystem.
-Although the specifics of deployment strategies are beyond the scope of this
-document, it lays the groundwork for the integration of Sassafras into the
-Polkadot network.
-
-
-Developers responsible for creating blockchains who intend to leverage the
-benefits offered by the Sassafras Protocol.
-
-Developers contributing to the Polkadot ecosystem, both relay-chain and
-para-chains.
-The protocol will have a central role in the next generation block authoring
-consensus systems.
-
-This section outlines the notation and conventions adopted throughout this
-document to ensure clarity and consistency.
-
-Data structures are primarily defined using standard ASN.1,
-syntax with few exceptions:
-
-- Fixed width integer types are not explicitly defined by ASN.1 standard.
-Within this document,
U<n> denotes a n-bit unsigned integer.
-
-Unless explicitly noted, all types must be serialized using
-SCALE codec.
-To ensure interoperability of serialized structures, the order of the fields
-must match the structures definitions found within this document.
-
-It is advantageous to make use of code snippets as part of the protocol
-description. As a convention, the code is formatted in a style similar to
-Rust, and can make use of the following set of predefined functions:
-
--
-
BYTES(x: T): returns an OCTET_STRING that represents the raw byte array of
-the object x with type T.
-
-- If
T is a VisibleString (ASCII string), it returns the sequence
-of octets of its ASCII representation.
-- If
T is U<n>, it returns the little-endian encoding of the integer
-U<n> as n/8 octets.
-
-
--
-
U<n>(x: OCTET_STRING): returns a U<n> interpreting x as the
-little-endian encoding of a n bits unsigned integer.
-
--
-
SCALE(x: T): returns an OCTET_STRING representing the SCALE encoding of
-x with type T.
-
--
-
BLAKE2(n: U32, x: OCTET_STRING): returns the standard Blake2b n
-bytes hash of x as an OCTET_STRING (note this is not equivalent to the
-truncation of the full 64 bytes Blake2b hash).
-
--
-
CONCAT(x₀: OCTET_STRING, ..., xₖ: OCTET_STRING): returns the concatenation
-of the inputs as an OCTET_STRING.
-
--
-
LENGTH(x: OCTET_STRING): returns the number of octets in x as an U32.
-
-
-
-More types and helper functions are introduced incrementally as they become
-relevant within the document's context.
-We find this approach more agile, especially given that the set of types used is
-not overly complex.
-
-The timeline is segmented into a sequentially ordered sequence of slots.
-This entire sequence of slots is then further partitioned into distinct segments
-known as epochs.
-The Sassafras protocol aims to map each slot within an epoch to the designated
-validators for that epoch, utilizing a ticketing system.
-The protocol operation can be roughly divided into five phases:
-
-Each of the validators associated to the target epoch generates and submits
-a set of candidate tickets to the blockchain. Every ticket is bundled with an
-anonymous proof of validity.
-
-Each candidate ticket undergoes a validation process for the associated validity
-proof and compliance with other protocol-specific constraints.
-
-After collecting all valid candidate tickets, a deterministic method is used to
-uniquely associate a subset of these tickets with the slots of the target epoch.
-
-During the block production phase of the target epoch, validators are required
-to demonstrate their ownership of tickets. This step discloses the identity of
-the ticket owners.
-
-During block verification, the claim of ticket ownership is validated.
-
-This chapter provides a high-level overview of the Bandersnatch VRF primitive as
-it relates to the Sassafras protocol.
-It's important to note that this section is not intended to serve as an
-exhaustive exploration of the mathematically intensive foundations of the
-cryptographic primitive. Rather, its primary aim is to offer a concise and
-accessible explanation of the primitive's role and usage which is relevant
-within the scope of this RFC.
-For an in-depth explanation, refer to the Ring-VRF
-paper authored by the Web3 foundation
-research team.
-
-The VRF Input, denoted as VrfInput, is constructed by combining a domain
-identifier with arbitrary data through the vrf_input function:
-
#![allow(unused)]
-fn main() {
- fn vrf_input(domain: OCTET_STRING, data: OCTET_STRING) -> VrfInput;
-}
-The specific implementation details of this function are intentionally omitted.
-A reference implementation is provided by the
-bandersnatch_vrfs
-project.
-
-The above link points to some temporary code (Transcript label set to "TemporaryDoNotDeploy").
-Also replace with docs.rs link once published to crates.io.
-
-Helper function to construct a VrfInput from a sequence of data items:
-#![allow(unused)]
-fn main() {
- fn vrf_input_from_items(domain: OCTET_STRING, items: SEQUENCE_OF OCTET_STRING) -> VrfInput {
- let data = OCTET_STRING(SIZE = 0); // empty octet string
- for item in items {
- data.append(item);
- data.append(LENGTH(item) as U8);
- }
- return vrf_input(domain, data);
- }
-}
-Note that each item length is safely casted to an U8 as:
-
-- In the context of this protocol all items lengths are less than 256.
-- The function is internal and not designed for generic use.
-
-
-Functionally, the VrfPreOutput can be considered as a seed for a PRNG to
-produce an arbitrary number of output bytes.
-It is computed as function of a VrfInput and a BandersnatchSecretKey.
-Two different approaches can be used to generate it: as a standalone object
-or as part of a signature. While the resulting VrfPreOutput is identical
-in both cases, the legitimacy of the latter can be confirmed by verifying the
-signature using the BandersnatchPublicKey of the expected signer.
-When constructed as a standalone object, VrfPreOutput is primarily employed
-in situations where the secret key owner needs to check if the generated output
-bytes fulfill some context specific criteria before applying the signature.
-To facilitate the construction, the following helper function is provided:
-#![allow(unused)]
-fn main() {
- fn vrf_pre_output(secret: BandernatchSecretKey, input: VrfInput) -> VrfPreOutput;
-}
-An additional helper function is provided for producing an arbitrary number of
-output bytes from VrfInput and VrfPreOutput:
-#![allow(unused)]
-fn main() {
- fn vrf_bytes(len: U32, input: VrfInput, pre_output: VrfPreOuput) -> OCTET_STRING;
-}
-Similar to the vrf_input function, the details about the implementation
-of these functions is omitted. Reference implementations are provided by the
-dleq_vrfs project
-
-
-This section outlines the data to be signed utilizing the VRF primitive:
-#![allow(unused)]
-fn main() {
- VrfSignatureData ::= SEQUENCE {
- transcript: Transcript,
- inputs: SEQUENCE_OF VrfInput
- }
-}
-Where:
-
-transcript: a Transcript
-instance. In practice, this is a special hash of some protocol-specific data
-to sign which doesn't influence the VrfPreOutput.
-inputs: sequence of VrfInputs to be signed.
-
-To simplify the construction of VrfSignatureData objects, a helper function is defined:
-#![allow(unused)]
-fn main() {
- fn vrf_signature_data(
- transcript_label: OCTET_STRING,
- transcript_data: SEQUENCE_OF OCTET_STRING,
- inputs: SEQUENCE_OF VrfInput
- ) -> VrfSignatureData {
- let mut transcript = Transcript::new_labeled(transcript_label);
- for data in transcript_data {
- transcript.append(data);
- }
- VrfSignatureData { transcript, inputs }
- }
-}
-
-Bandersnatch VRF offers two signature flavors:
-
-- plain signature: much like a traditional Schnorr signature,
-- ring signature: leverages a zk-SNARK to allows for anonymous signatures
-using a key from a predefined set of enabled keys, known as the ring.
-
-
-This section describes the signature process for VrfSignatureData using the
-plain signature flavor.
-#![allow(unused)]
-fn main() {
- PlainSignature ::= OCTET_STRING;
-
- VrfSignature ::= SEQUENCE {
- signature: PlainSignature,
- pre_outputs: SEQUENCE-OF VrfPreOutput
- }
-}
-Where:
-
-signature: the actual plain signature.
-pre_outputs: sequence of VrfPreOutputs corresponding to the VrfInputs
-found within the VrfSignatureData.
-
-Helper function to construct VrfPlainSignature from VrfSignatureData:
-#![allow(unused)]
-fn main() {
- BandersnatchSecretKey ::= OCTET_STRING;
-
- fn vrf_sign(
- secret: BandernatchSecretKey,
- signature_data: VrfSignatureData
- ) -> VrfSignature
-}
-Helper function for signature verification returning a BOOLEAN value
-indicating the validity of the signature (true on success):
-#![allow(unused)]
-fn main() {
- BandersnatchPublicKey ::= OCTET_STRING;
-
- fn vrf_verify(
- public: BandersnatchPublicKey,
- signature: VrfSignature
- ) -> BOOLEAN;
-}
-In this document, the types BandersnatchSecretKey, BandersnatchPublicKey
-and PlainSignature are intentionally left undefined. Their definitions can be
-found in the bandersnatch_vrfs reference implementation.
-
-This section describes the signature process for VrfSignatureData using the
-ring signature flavor.
-#![allow(unused)]
-fn main() {
- RingSignature ::= OCTET_STRING;
-
- RingVrfSignature ::= SEQUENCE {
- signature: RingSignature,
- pre_outputs: SEQUENCE_OF VrfPreOutput
- }
-}
-
-signature: the actual ring signature.
-pre_outputs: sequence of VrfPreOutputs corresponding to the VrfInputs
-found within the VrfSignatureData.
-
-Helper function to construct RingVrfSignature from VrfSignatureData:
-#![allow(unused)]
-fn main() {
- BandersnatchRingProverKey ::= OCTET_STRING;
-
- fn ring_vrf_sign(
- secret: BandersnatchRingProverKey,
- signature_data: VrfSignatureData,
- ) -> RingVrfSignature;
-}
-Helper function for signature verification returning a BOOLEAN value
-indicating the validity of the signature (true on success).
-#![allow(unused)]
-fn main() {
- BandersnatchRingVerifierKey ::= OCTET_STRING;
-
- fn ring_vrf_verify(
- verifier: BandersnatchRingVerifierKey,
- signature: RingVrfSignature,
- ) -> BOOLEAN;
-}
-Note that this function doesn't require the signer's public key.
-In this document, the types BandersnatchRingProverKey,
-BandersnatchRingVerifierKey, and RingSignature are intentionally left
-undefined. Their definitions can be found in the bandersnatch_vrfs reference
-implementation.
-
-
-For epoch N, the first block produced must include a descriptor for some of
-the subsequent epoch (N+1) parameters. This descriptor is defined as:
-#![allow(unused)]
-fn main() {
- NextEpochDescriptor ::= SEQUENCE {
- randomness: OCTET_STRING(SIZE(32)),
- authorities: SEQUENCE_OF BandersnatchPublicKey,
- configuration: ProtocolConfiguration OPTIONAL
- }
-}
-Where:
-
-randomness: 32-bytes pseudo random value.
-authorities: list of authorities.
-configuration: optional protocol configuration.
-
-This descriptor must be encoded using the SCALE encoding system and embedded
-in the block header's digest log. The identifier for the digest element is
-BYTES("SASS").
-A special case arises for the first block for epoch 0, which each node produces
-independently during the genesis phase. In this case, the NextEpochDescriptor
-relative to epoch 1 is shared within the second block, as outlined in section
-6.1.3.
-
-The randomness in the NextEpochDescriptor randomness is computed as:
-#![allow(unused)]
-fn main() {
- randomness = BLAKE2(32, CONCAT(randomness_accumulator, BYTES(next_epoch.index)));
-}
-Here, randomness_accumulator refers to a 32-byte OCTET_STRING stored
-on-chain and computed through a process that incorporates verifiable random
-elements from all previously imported blocks. The exact procedure is described
-in section 6.7.
-
-The ProtocolConfiguration primarily influences certain checks carried out
-during tickets validation. It is defined as:
-#![allow(unused)]
-fn main() {
- ProtocolConfiguration ::= SEQUENCE {
- attempts_number: U32,
- redundancy_factor: U32
- }
-}
-Where:
-
-attempts_number: maximum number of tickets that each authority for the next
-epoch is allowed to submit.
-redundancy_factor: expected ratio between epoch's slots and the cumulative
-number of tickets which can be submitted by the set of epoch validators.
-
-The attempts_number influences the anonymity of block producers. As all
-published tickets have a public attempt number less than attempts_number,
-all the tickets which share the attempt number value must belong to different
-block producers, which reduces anonymity late as we approach the epoch tail.
-Bigger values guarantee more anonymity but also more computation.
-Details about how exactly these parameters drives the ticket validity
-probability can be found in section 6.2.2.
-ProtocolConfiguration values can be adjusted via a dedicated on-chain call
-which should have origin set to Root. Any proposed changes to
-ProtocolConfiguration that are submitted in epoch K will be included in the
-NextEpochDescriptor at the start of epoch K+1 and will come into effect in
-epoch K+2.
-
-Some of the initial parameters for the first epoch, Epoch #0, are set through
-the genesis configuration, which is defined as:
-#![allow(unused)]
-fn main() {
- GenesisConfig ::= SEQUENCE {
- authorities: SEQUENCE_OF BandersnatchPublicKey,
- configuration: ProtocolConfiguration,
- }
-}
-The on-chain randomness accumulator is initialized only after the genesis
-block is produced. It starts with the hash of the genesis block:
-#![allow(unused)]
-fn main() {
- randomness_accumulator = genesis_hash
-}
-Since block #0 is generated locally by each node as part of the genesis
-process, the first block that a validator explicitly produces for Epoch
-#0 is block #1. Therefore, block #1 is required to contain the
-NextEpochDescriptor for the following epoch, Epoch #1.
-The NextEpochDescriptor for Epoch #1:
-
-randomness: computed using the randomness_accumulator established
-post-genesis, as mentioned above.
-authorities: the same as those specified in the genesis configuration.
-configuration: not set (i.e., None), implying the reuse of the
-one found in the genesis configuration.
-
-
-After the beginning of a new epoch N, each validator associated to the next
-epoch (N+1) constructs a set of tickets which may be eligible (6.2.2)
-to be submitted on-chain. These tickets aim to secure ownership of one or more
-slots in the upcoming epoch N+1.
-Each validator is allowed to submit a maximum number of tickets, as specified by
-the attempts_number field in the ProtocolConfiguration for the next epoch.
-The ideal timing for a validator to start creating the tickets is subject to
-strategy. A recommended approach is to initiate tickets creation once the block
-containing the NextEpochDescriptor is either probabilistically or, preferably,
-deterministically finalized. This timing is suggested to prevent to waste
-resources on tickets that might become obsolete if a different chain branch
-is finally chosen as the best one by the distributed system.
-However, validators are also advised to avoid submitting tickets too late,
-as tickets submitted during the second half of the epoch must be discarded.
-
-Each ticket has an associated 128-bit unique identifier defined as:
-#![allow(unused)]
-fn main() {
- TicketId ::= U128;
-}
-The value of the TicketId is determined by the output of the Bandersnatch VRF
-with the following input:
-#![allow(unused)]
-fn main() {
- ticket_id_vrf_input = vrf_input_from_items(
- BYTES("sassafras-ticket-v1.0"),
- [
- next_epoch.randomness,
- BYTES(next_epoch.index),
- BYTES(attempt_index)
- ]
- );
-
- ticket_id_vrf_pre_output = vrf_pre_output(AUTHORITY_SECRET_KEY, ticket_id_vrf_input);
-
- ticket_bytes = vrf_bytes(16, ticket_id_vrf_input, ticket_id_vrf_pre_output);
- ticket_id = U128(ticket_bytes);
-}
-Where:
-
-next_epoch.randomness: randomness associated to the target epoch.
-next_epoch.index: index of the target epoch as a U64.
-attempt_index: value going from 0 to attempts_number as a U32.
-
-
-A TicketId value is valid if its value is less than the ticket threshold:
-T = (r·s)/(a·v)
-
-Where:
-
-v: epoch's authorities (aka validators) number
-s: epoch's slots number
-r: redundancy factor
-a: attempts number
-T: ticket threshold value (0 ≤ T ≤ 1)
-
-
-In an epoch with s slots, the goal is to achieve an expected number of tickets
-for block production equal to r·s.
-It's crucial to ensure that the probability of having fewer than s winning
-tickets is very low, even in scenarios where up to 1/3 of the authorities
-might be offline.
-To accomplish this, we first define the winning probability of a single ticket
-as T = (r·s)/(a·v).
-Let n be the actual number of participating validators, where v·2/3 ≤ n ≤ v.
-These n validators each make a attempts, for a total of a·n attempts.
-Let X be the random variable associated to the number of winning tickets, then
-its expected value is:
-E[X] = T·a·n = (r·s·n)/v
-
-By setting r = 2, we get
-s·4/3 ≤ E[X] ≤ s·2
-
-Using Bernestein's inequality we get Pr[X < s] ≤ e^(-s/21).
-For instance, with s = 600 this results in Pr[X < s] < 4·10⁻¹³.
-Consequently, this approach offers considerable tolerance for offline nodes and
-ensures that all slots are likely to be filled with tickets.
-For more details about threshold formula please refer to the
-probabilities and parameters
-paragraph in the Web3 foundation description of the protocol.
-
-Every candidate ticket identifier has an associated body, defined as:
-#![allow(unused)]
-fn main() {
- TicketBody ::= SEQUENCE {
- attempt_index: U32,
- erased_pub: Ed25519PublicKey,
- revealed_pub: Ed25519PublicKey
- }
-}
-Where:
-
-attempt_index: attempt index used to generate the associated TicketId.
-erased_pub: Ed25519 ephemeral public key which gets erased as soon as the
-ticket is claimed. This key can be used to encrypt data for the validator.
-revealed_pub: Ed25519 ephemeral public key which gets exposed as soon as the
-ticket is claimed.
-
-The process of generating an erased key pair is intentionally left undefined,
-allowing the implementor the freedom to choose the most suitable strategy.
-Revealed key pair is generated using the bytes produced by the VRF with input
-parameters equal to those employed in TicketId generation, only the label
-is different.
-#![allow(unused)]
-fn main() {
- revealed_vrf_input = vrf_input_from_items(
- domain: BYTES("sassafras-revealed-v1.0"),
- data: [
- next_epoch.randomness,
- BYTES(next_epoch.index),
- BYTES(attempt_index)
- ]
- );
-
- revealed_vrf_pre_output = vrf_pre_output(AUTHORITY_SECRET_KEY, revealed_vrf_input);
-
- revealed_seed = vrf_bytes(32, revealed_vrf_input, revealed_vrf_pre_output);
- revealed_pub = ed25519_secret_from_seed(revealed_seed).public();
-}
-Where:
-
-next_epoch.randomness: randomness associated to the target epoch.
-next_epoch.index: index of the target epoch as a U64.
-attempt_index: value going from 0 to attempts_number as a U32.
-
-The ephemeral public keys are also used for claiming the tickets on block production.
-Refer to section 6.5 for details.
-
-TicketBody must be signed using the Bandersnatch ring VRF flavor (5.4.2).
-#![allow(unused)]
-fn main() {
- sign_data = vrf_signature_data(
- transcript_label: BYTES("sassafras-ticket-body-v1.0"),
- transcript_data: [
- SCALE(ticket_body)
- ],
- inputs: [
- ticket_id_vrf_input
- ]
- )
-
- ring_signature = ring_vrf_sign(AUTHORITY_SECRET_KEY, RING_PROVER_KEY, sign_data)
-}
-RING_PROVER_KEY object is constructed using the set of public keys which
-belong to the target epoch's authorities and the zk-SNARK context parameters
-(for more details refer to the
-bandersnatch_vrfs
-reference implementation).
-The body and the ring signature are combined in the TicketEnvelope structure:
-#![allow(unused)]
-fn main() {
- TicketEnvelope ::= SEQUENCE {
- ticket_body: TicketBody,
- ring_signature: RingVrfSignature
- }
-}
-All the envelopes corresponding to valid tickets can be submitted on-chain via a
-dedicated on-chain call (extrinsic).
-
-All the actions in the steps described by this paragraph are executed by
-on-chain code.
-Validation rules:
-
-- Tickets submissions must occur within a block part of the first half of the epoch.
-- Ring signature is verified using the on-chain
RING_VERIFIER_KEY.
-- Ticket identifier is locally (re)computed from the
VrfPreOutput contained in the
-RingVrfSignature and its value is checked to be less than the tickets' threshold.
-
-Valid tickets bodies are all persisted on-chain.
-
-Before the beginning of the next epoch, the on-chain list of tickets must be
-associated with the next epoch's slots such that there must be at most one
-ticket per slot.
-The assignment process happens in the second half of the submission epoch and
-follows these steps:
-
-- Sorting: The complete list of tickets is sorted based on their
TicketId
-value, with smaller values coming first.
-- Trimming: In scenarios where there are more tickets than available slots, the
-list is trimmed to fit the epoch's slots by removing the larger value.
-- Assignment: Tickets are assigned to the epoch's slots following an
-outside-in strategy.
-
-
-Given an ordered sequence of tickets [t0, t1, t2, ..., tk] to be assigned to
-n slots, where n ≥ k, the tickets are allocated according to the following
-strategy:
- slot-index : [ 0, 1, 2, ............ , n ]
- tickets : [ t1, t3, t5, ... , t4, t2, t0 ]
-
-Here slot-index is a relative value computed as:
-slot-index = absolute_slot - epoch_start_slot
-
-The association between each ticket and a slot is recorded on-chain and thus
-is public. What remains confidential is the identity of the ticket's author, and
-consequently, who possesses the authority to claim the corresponding slot. This
-information is known only to the author of the ticket.
-In case the number of available tickets is less than the number of epoch slots,
-some orphan slots in the middle of the epoch will remain unbounded to any
-ticket. For claiming strategy refer to 6.5.2.
-
-With tickets bound to epoch slots, every validator acquires information about
-the slots for which they are supposed to produce a block.
-The procedure for slot claiming depends on whether a given slot has an
-associated ticket according to the on-chain state.
-If a slot is associated with a ticket, the primary authoring method is used.
-Conversely, the protocol resorts to the secondary method as a fallback.
-
-Let ticket_body be the TicketBody that has been committed to the on-chain
-state, curr_epoch denote an object containing information about the current
-epoch, and slot represent the slot number (absolute).
-Follows the construction of VrfSignatureData:
-#![allow(unused)]
-fn main() {
- randomness_vrf_input = vrf_input_from_items(
- domain: BYTES("sassafras-randomness-v1.0"),
- data: [
- curr_epoch.randomness,
- BYTES(curr_epoch.index),
- BYTES(slot)
- ]
- );
-
- revealed_vrf_input = vrf_input_from_items(
- domain: BYTES("sassafras-revealed-v1.0"),
- data: [
- curr_epoch.randomness,
- BYTES(curr_epoch.index),
- BYTES(ticket_body.attempt_index)
- ]
- );
-
- sign_data = vrf_signature_data(
- transcript_label: BYTES("sassafras-claim-v1.0"),
- transcript_data: [
- SCALE(ticket_body)
- ],
- inputs: [
- randomness_vrf_input,
- revealed_vrf_input
- ]
- );
-}
-
-Fiat-Shamir transform is used to obtain a 32-byte challenge associated with
-the VrfSignData transcript.
-Validators employ the secret key associated with erased_pub, which has been
-committed in the TicketBody, to sign the challenge.
-#![allow(unused)]
-fn main() {
- challenge = sign_data.transcript.challenge();
- erased_signature = ed25519_sign(ERASED_SECRET_KEY, challenge);
-}
-As ticket's ownership can be claimed by reconstructing the revealed_pub entry
-of the committed TicketBody, this step is considered optional.
-
-Is this step really necessary?
-- Isn't better to keep it simple if this step doesn't offer any extra security?
-- We already have a strong method to claim ticket ownership using the vrf output
-- What if a validator provides both the proofs?
- More weight for the branch (i.e. used to decide what is the best branch by validators)?
- E.g.
- - primary method + ed25519 erased signature => score 2
- - primary method => score 1
- - fallback method => score 0
-
-
-By noting that the authorities registered on-chain are kept in an ordered list,
-the index of the authority which has the privilege to claim an orphan slot is:
-#![allow(unused)]
-fn main() {
- index_bytes = BLAKE2(4, CONCAT(epoch_randomness, BYTES(slot)));
- index = U32(index_bytes) mod authorities_number;
-}
-Given randomness_vrf_input constructed as shown for the primary method (6.5.1),
-the VrfSignatureData is constructed as:
-#![allow(unused)]
-fn main() {
- sign_data = vrf_signature_data(
- transcript_label: BYTES("sassafras-claim-v1.0"),
- transcript_data: [ ],
- inputs: [
- randomness_vrf_input
- ]
- )
-}
-
-The SlotClaim structure is used to contain all the necessary information to
-assess ownership of a slot.
-#![allow(unused)]
-fn main() {
- SlotClaim ::= SEQUENCE {
- authority_index: U32,
- slot: U64,
- signature: VrfSignature,
- erased_signature: Ed25519Signature OPTIONAL
- }
-}
-The claim is constructed as follows:
-#![allow(unused)]
-fn main() {
- signature = vrf_sign(AUTHORITY_SECRET_KEY, sign_data);
-
- claim = SlotClaim {
- authority_index,
- slot,
- signature,
- erased_signature
- }
-}
-Where:
-
-authority_index: index of the block author in the on-chain authorities list.
-slot: slot number (absolute, not relative to the epoch start)
-signature: signature relative to the sign_data constructed via the
-primary 6.5.1 or secondary (6.5.2) method.
-erased_signature: optional signature providing an additional proof of ticket
-ownership (6.5.1.1).
-
-The signature includes one or two VrfPreOutputs.
-
-- The first is always present and is used to generate per-block randomness
-to feed the randomness accumulator (6.7).
-- The second is included if the slot is bound to a ticket. This is relevant to
-claim ticket ownership (6.6.1).
-
-The claim object is SCALE encoded and sent in the block's header digest log.
-
-The signature within the SlotClaim is verified using a VrfSignData
-constructed as specified in 6.5.
-#![allow(unused)]
-fn main() {
- public_key = authorities[claim.authority_index];
-
- result = vrf_verify(public_key, sign_data, claim.signature);
- assert(result == true);
-}
-With:
-
-authorities: list of authorities for the epoch, as recorded on-chain.
-sign_data: data that has been signed, constructed as specified in 6.5.
-
-If signature verification is successful, the validation process then diverges
-based on whether the slot is associated with a ticket according to the on-chain
-state.
-For slots tied to a ticket, the primary verification method is employed. Otherwise,
-the secondary method is utilized.
-
-This method verifies ticket ownership using the second VrfPreOutput from the
-SlotClaim signature
-The process involves comparing the revealed_pub key from the committed
-TicketBody with a reconstructed key using the VrfPreOutput and the expected
-VrfInput. A mismatch indicates an illegitimate claim.
-#![allow(unused)]
-fn main() {
- revealed_vrf_input = vrf_input_from_items(
- domain: BYTES("sassafras-revealed-v1.0"),
- data: [
- curr_epoch.randomness,
- BYTES(curr_epoch.index),
- BYTES(ticket_body.attempt_index)
- ]
- );
-
- reveled_vrf_pre_output = claim.signature.pre_outputs[1];
-
- revealed_seed = vrf_bytes(32, revealed_vrf_input, revealed_vrf_pre_output);
- revealed_pub = ed25519_secret_from_seed(revealed_seed).public();
- assert(revealed_pub == ticket_body.revealed_pub);
-}
-
-If the erased_signature is present in SlotClaim, the erased_pub within the
-committed TicketBody key is used to verify it.
-The signed challenge is generated as outlined in section 6.5.1.1.
-#![allow(unused)]
-fn main() {
- challenge = sign_data.transcript.challenge();
- result = ed25519_verify(ticket_body.erased_pub, challenge, claim.erased_signature);
- assert(result == true);
-}
-
-If the slot doesn't have any associated ticket then the validator index contained in
-the claim should match the one given by the rule outlined in section 6.5.2.
-
-The first VrfPreOutput which ships within the block's SlotClaim signature
-is mandatory and must be used as entropy source for the randomness which gets
-accumulated on-chain after block transactions execution.
-Given claim the instance of SlotClaim found within the block header, and
-randomness_accumulator the current value for the randomness accumulator, the
-randomness_accumulator value is updated as follows:
-#![allow(unused)]
-fn main() {
- randomness_vrf_input = vrf_input_from_items(
- domain: BYTES("sassafras-randomness-v1.0"),
- data: [
- curr_epoch.randomness,
- BYTES(curr_epoch.index),
- BYTES(slot)
- ]
- );
-
- randomness_vrf_pre_output = claim.signature.pre_outputs[0];
- randomness = vrf_bytes(32, randomness_vrf_input, randomness_vrf_pre_output);
-
- randomness_accumulator = BLAKE2(32, CONCAT(randomness_accumulator, randomness));
-}
-The randomness_accumulator never resets and is a continuously evolving value.
-It primarily serves as a basis for calculating the randomness associated to the
-epochs as outlined on section 6.1, but custom usages
-from the user are not excluded.
-
-None
-
-It is critical that implementations of this RFC undergo thorough testing on
-test networks.
-A security audit may be desirable to ensure the implementation does not
-introduce unwanted side effects.
-
-
-Adopting Sassafras consensus marks a significant improvement in reducing the
-frequency of short-lived forks.
-Forks are eliminated by design. Forks may only result from network disruptions
-or protocol attacks. In such cases, the choice of which fork to follow upon
-recovery is clear-cut, with only one valid option.
-
-No specific considerations.
-
-The adoption of Sassafras affects the native client and thus can't be introduced
-just via a runtime upgrade.
-A deployment strategy should be carefully engineered for live networks.
-This subject is left open for a dedicated RFC.
-
-
-
-
-None
-
-While this RFC lays the groundwork and outlines the core aspects of the
-protocol, several crucial topics remain to be addressed in future RFCs.
-These include:
-
-
--
-
Outbound Interfaces: Interfaces that the host environment provides to the
-on-chain code, typically known as Host Functions.
-
--
-
Unrecorded Inbound Interfaces. Interfaces that the on-chain code provides
-to the host code, typically known as Runtime APIs.
-
--
-
Transactional Inbound Interfaces. Interfaces that the on-chain code provides
-to the world to alter the chain state, typically known as Transactions
-(or extrinsics in the Polkadot ecosystem)
-
-
-
-
-- Protocol Migration. Exploring how this protocol can seamlessly replace an
-already operational instance of another protocol. Future RFCs should focus on
-deployment strategies to facilitate a smooth transition.
-
-
-
--
-
Procedure: Determining the procedure for the zk-SNARK SRS (Structured
-Reference String) initialization. Future RFCs should provide insights into
-whether this process should include an ad-hoc initialization ceremony or if
-we can reuse an SRS from another ecosystem (e.g. Zcash or Ethereum).
-
--
-
Sharing with Para-chains: Considering the complexity of the process, we
-must understand whether the SRS is shared with system para-chains or
-maintained independently.
-
-
-
-
-- Mixnet Integration: Submitting tickets directly can pose a risk of
-potential deanonymization through traffic analysis. Subsequent RFCs should
-investigate the potential for incorporating Mixnet protocol or other
-privacy-enhancing mechanisms to address this concern.
-
(source)
Table of Contents