From 2aa26df2c7a7b427cd37b66bb5443ace21037744 Mon Sep 17 00:00:00 2001 From: Davide Galassi Date: Thu, 24 Aug 2023 21:22:52 +0200 Subject: [PATCH 01/26] Sassafras RFC draft --- text/0000-sassafras-consensus.md | 785 +++++++++++++++++++++++++++++++ 1 file changed, 785 insertions(+) create mode 100644 text/0000-sassafras-consensus.md diff --git a/text/0000-sassafras-consensus.md b/text/0000-sassafras-consensus.md new file mode 100644 index 000000000..8fb540c0a --- /dev/null +++ b/text/0000-sassafras-consensus.md @@ -0,0 +1,785 @@ +# RFC-xx: Sassafras Consensus Protocol + +| | | +| --------------- | ------------------------------------------------------------------------------------------- | +| **Start Date** | September 03, 2023 | +| **Description** | Sassafras consensus protocol description and structures | +| **Authors** | Davide Galassi | + + +## Abstract + +Sassafras is a novel consensus protocol designed to address the recurring +fork-related challenges encountered in other lottery-based protocols, such as +Babe. + +Sassafras aims to establish a unique association between each epoch's slots +and the validators, ensuring that there will be one and only one validator +per slot. + +The protocol ensures the anonymity of the validator associated to a slot until +the slot is not claimed at block production time. + + +## 1. Motivation + + +Sassafras Protocol has been extensively documented in a comprehensive +[research paper](https://eprint.iacr.org/2023/031.pdf). However, this RFC serves +the purpose of conveying essential implementation details that are crucial for +interoperability and clarifying aspects left open for implementation discretion. + +### 1.1. Relevance to Implementors + +This RFC focuses on providing implementors with the necessary insights into the +protocol's operation. It takes precedence over the research paper in cases where +discrepancies arise between the two documents. + +(TODO: remove this) +Example: In this RFC, ticket claims occur in the epoch immediately following +issuance, whereas the original protocol description specifies a two-epoch gap. +The approach outlined in this RFC should be followed. + +### 1.2. Supporting Sassafras for Polkadot + +In addition to fostering interoperability, another objective of this RFC is to +facilitate the implementation of Sassafras within the Polkadot ecosystem. While +the specifics of deployment mechanics are beyond the scope of this document, it +paves the way for integrating Sassafras into the Polkadot network. + + +## 2. Stakeholders + +### 2.1 Developers of Relay-Chains and Para-Chains + +Developers responsible for creating relay-chains and para-chains within the +Polkadot ecosystem who intend to leverage the benefits offered by the Sassafras +Protocol. + +### 2.2 Developers of Polkadot Relay-Chain + +Developers contributing to the Polkadot relay-chain, which plays a pivotal role +in facilitating the interoperability and functionality of the Sassafras Protocol +within the broader Polkadot network. + +## 3. Notation and Convention + +This section outlines the notation and conventions used throughout the document +to ensure clarity and consistency. + +### 3.1. Data Structure Definitions + +Data structures are primarily defined using [ASN.1](https://en.wikipedia.org/wiki/ASN.1), +with a few exceptions: +- Integer types are not explicitly defined in ASN.1 and `Un` types should be + interpreted as n-bit unsigned integers. + +To ensure that serialized data structures can be used interchangeably, it's +important to consider the arrangement of fields in their definitions. + +In cases where no specific instructions are given, structures should be +serialized using [SCALE](https://github.com/paritytech/parity-scale-codec). + +### 3.2. Pseudo-Code + +Through this document it is advantageous to make use of code snippets as part +of the comprehensive description. These code snippets shall adhere to the +subsequent conventions: + +- Code snippets are presented in a Rust-like pseudo-code format. + +- The function `SCALE(x: T)` returns the `SCALE` encoding of the variable `x` + with type `T`. + +- The function `BYTES(x: T)` returns an `OCTET STRING` which represents the raw + byte array representation of the object `x` with type `T`. + - if `T` is `VisibleString` (aka *ascii* string): it returns the sequence of + bytes of its *ascii* representation. + - if `T` is `Un`: it returns the little-endian encoding of the integer `Un` + as `n` bytes. + +- The function `Un(x: OCTET STRING)` returns a `Un` using the little-endian + encoding of the integer `x` + +- The function `TRANSCRIPT(x)` represents the construction of a transcript + object as defined by [`ark-transcript`](https://docs.rs/ark-transcript/0.0.1/ark_transcript/) + library. + +### 3.3. Incremental Introduction of Types and Functions + +Types and helper functions will be 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 extensive or overly complex. + +This incremental presentation enhances readability and comprehension. + + +## 4. Protocol Introduction + +The Sassafras Protocol employs a binding mechanism between validators and slots +through the use of a **ticket**. + +The protocol is organized into five discrete and asynchronous phases: + +### 4.1. Submission of Candidate Tickets + +Validators generate and submit their candidate tickets along with validity +proofs to the blockchain. The validity proof ensures that the tickets are +legitimate while maintaining the anonymity of the authorship. + +### 4.2. Validation of Candidate Tickets + +Submitted candidate tickets undergo a validation process to verify their +authenticity, integrity and compliance with other protocol-specific rules. + +### 4.3. Tickets and Slots Binding + +After collecting all candidate tickets, a deterministic method is employed to +associate some of these tickets with specific slots. + +### 4.4. Claim of Ticket Ownership + +Validators assert ownership of tickets during the block production phase. This +step establishes a secure binding between validators and their respective slots. + +### 4.5. Validation of Ticket Ownership + +During block verification, the claims of ticket ownership are rigorously +validated to uphold the protocol's integrity. + + +## 5. Bandernatch VRFs Cryptographic Primitives + +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. Instead, its primary purpose is to offer a concise and +comprehensible interpretation of the primitive within the context of this RFC. + +For a more detailed and mathematical understanding of Ring-VRFs, we recommend +referring to the Ring-VRF research [paper](https://eprint.iacr.org/2023/002.pdf). + +### 5.1. VRF Input + +The VRF Input, denoted as `VrfInput`, is constructed using a domain and some +arbitrary data using the `vrf_input(domain: OCTET STRING, buf: OCTET STRING)` +function. This function is left opaque here and can be read in the actual +reference implementation. + +The VRF Input, denoted as `VrfInput`, is constructed by combining a domain identifier +with arbitrary data using the `vrf_input` function: + +```rust + fn vrf_input(domain: OCTET_STRING, buf: OCTET_STRING) -> VrfInput; +``` + +While the specific implementation details of this function are intentionally +omitted here, you can find the complete implementation in the +[`bandersnatch_vrfs`](https://github.com/w3f/ring-vrf/blob/18614458ca4cb335c88d4e710c13906a76f51e43/bandersnatch_vrfs/src/lib.rs#L57) +reference implementation. + +Helper function to construct a `VrfInput` from a sequence of `data` items: + +```rust + fn vrf_input_from_items(domain: OCTET_STRING, data: SEQUENCE_OF OCTET_STRING) -> VrfInput { + buf = OCTET_STRING(SIZE(0)); + for item in data { + buf.append(item); + buf.append(length(item) as U8); + } + return vrf_input(domain, buf); + } +``` + +### 5.2. VRF Output + +Given a `VrfInput` object, the corresponding `VrfOutput`, also referred to as +`VrfPreOutput`, is computed using a Bandersnatch secret key. + +A `VrfOutput` can be created in two ways: as a standalone object or as part of a +VRF signature. In both scenarios, the resulting `VrfOutput` remains the same, but +the primary difference lies in the inclusion of a signature in the latter, which +serves to confirm its validity. + +In practice, the `VrfOutput` functions as a *seed* to produce a variable number +of pseudo-random bytes. These bytes are considered 'verified' when `VrfOutput` is +accompanied by a signature. + +When used as a standalone object, `VrfOutput` is primarily employed in situations +where the protocol necessitates to check the generated bytes according to some +protocol specific criteria before applying the signature. + +To facilitate the construction of `VrfOutput` from a secret key and `VrfInput`, +the following helper function is used: + +```rust + fn vrf_output(secret: BandernatchSecretKey, input: VrfInput) -> VrfOutput; +``` + +Additionally, a helper function is provided for producing `len` bytes from +`VrfInput` and `VrfOutput`: + +```rust + fn vrf_bytes(vrf_input: VrfInput, vrf_output: VrfOuput, len: U32) -> OCTET_STRING; +``` + +Just like the `VrfInput` support function, we have intentionally excluded the +detailed implementation of this function in this document. However, you can +see the complete implementation in the `dleq_vrfs` reference implementation: +- [`vrf_output`](https://github.com/w3f/ring-vrf/blob/18614458ca4cb335c88d4e710c13906a76f51e43/dleq_vrf/src/traits.rs#L75-L77) +- [`vrf_bytes`](https://github.com/w3f/ring-vrf/blob/18614458ca4cb335c88d4e710c13906a76f51e43/dleq_vrf/src/vrf.rs#L211-L214) + +### 5.3. VRF Signature Data + +This section defines the data that is to be signed using the VRF primitive: + +```rust + VrfSignatureData ::= SEQUENCE { + transcript: Transcript, + vrf_input: SEQUENCE_OF VrfInput + } +``` + +- `transcript`: represents a `ark-transcript` object. +- `vrf_input`: sequence of `VrfInputs` to be signed. + +To simplify the construction of a `VrfSignatureData` object, a helper function is provided: + +```rust + TranscriptData ::= OCTET_STRING; + + fn vrf_signature_data( + transcript_label: OCTET_STRING, + transcript_data: SEQUENCE_OF TranscriptData, + vrf_inputs: SEQUENCE_OF VrfInput + ) -> VrfSignatureData { + transcript = TRANSCRIPT(transcript_label); + for data in transcript_data { + transcript.append(data); + } + return VrfSignatureData { transcript, vrf_inputs } + } +``` + +### 5.4. VRF Signature + +Bandersnatch VRF offers two signature options: plain signature, which in +practice is like a *Schnorr* signature, or Ring signature. The Ring signature +option allows for anonymous signatures using a key from a predefined set of +enabled keys, known as the ring. + +#### 5.4.1. Plain VRF Signature + +This section describes the signature process for `VrfSignatureData` using the +plain Bandersnatch signature flavor. + +```rust + PlainSignature ::= OCTET_STRING; + + VrfSignature ::= SEQUENCE { + signature: PlainSignature, + outputs: SEQUENCE-OF VrfOutput + } +``` + +- `signature`: represents the actual signature (opaque). +- `outputs`: a sequence of `VrfOutput`s corresponding to the `VrfInput`s values. + +Helper function to create a `VrfSignature` from `VrfSignatureData`: + +```rust + fn vrf_sign( + secret: BandernatchSecretKey, + signature_data: VrfSignatureData + ) -> VrfSignature +``` + +Helper function for validating the signature and returning a Boolean value +indicating the validity of the signature (`True` if it's valid): + +```rust + fn vrf_verify( + public: BandersnatchPublicKey, + signature: VrfSignature + ) -> Boolean; +``` + +In this document, the types `BandersnatchSecretKey`, `BandersnatchPublicKey` +and `PlainSignature` are intentionally left undefined as are not relevant. Their +definitions can be found in the `bandersnatch_vrfs` reference implementation. + +#### 5.3.2. Ring VRF Signature + +This section deals with the signature process for `VrfSignatureData` using the +Bandersnatch Ring signature flavor. + +```rust + RingSignature ::= OCTET_STRING; + + RingVrfSignature ::= SEQUENCE { + signature: RingSignature, + ring_proof: RingProof, + outputs: SEQUENCE_OF VrfOutput + } +``` + +- `signature`: represents the actual signature (opaque). +- `ring_proof`: denotes the proof of ring membership. +- `outputs`: sequence of `VrfOutput` objects corresponding to the `VrfInput` values. + +Helper function to create a `RingVrfSignature` from `VrfSignatureData`: + +```rust + fn ring_vrf_sign( + secret: BandersnatchSecretKey, + signature_data: VrfSignatureData, + prover: RingProver + ) -> RingVrfSignature; +``` + +Helper function for validating the signature and returning a Boolean value +indicating the validity of the signature (`True` if it's valid). +It's important to note that this function does not require the signer's public key. + +```rust + fn ring_vrf_verify( + signature: RingVrfSignature, + verifier: RingVerifier + ) -> Boolean; +``` + +In this document, the types `BandersnatchSecretKey`, `BandersnatchPublicKey`, +`RingSignature`, `RingProof`, `RingProver` and `RingVerifier` are intentionally +left undefined as are not relevant. Their definitions can be found in the +`bandersnatch_vrfs` reference implementation. + + +## 6. Sassafras Protocol + +### 6.1 Epoch's First Block + +The first block produced for an epoch `N` is required to include the descriptor +for the next epoch `N+1`. + +The descriptor of next epoch is the `NextEpochDescriptor`. + +```rust + AuthorityId ::= BandersnatchPublicKey; + + Randomness ::= OCTET_STRING(SIZE(32)); + + NextEpochDescriptor ::= SEQUENCE { + authorities: SEQUENCE_OF AuthorityId, + randomness: Randomness, + configuration: ProtocolConfiguration OPTIONAL + } +``` + +- `authorities`: list of authorities for the next epoch. +- `randomness`: randomness value associated to the next epoch. +- `configuration`: next epoch optional protocol configuration. + +The `NextEpochDescriptor` must be `SCALE` encoded and embedded in the block +header digest data. + +The identifier for the digest element is `BYTES("SASS")`. + +**Security Consideration**: Instances of `NextEpochDescriptor` are generated through +on-chain code whenever a block is identified as the first of an epoch. +Consequently, every node executing the block has the capability to verify if +the descriptor generated during block execution matches the one produced by the +block author, which is stored in the digest data. + +#### 6.1.1 Epoch Randomness + +Each epoch has an associated randomness value defined by the +`NextEpochDescriptor` `Randomness` element. + +The first block of epoch `N` contains the randomness associated to the next epoch `N+1`. + +Randomness for epoch `N+1` is computed using the VRF output values which are associated +to each block of epoch `N-1`. + +Assuming block `Bi` is produced during epoch `N-1`. Then the randomness for epoch `N+1` +is incrementally generated as `RandomnessAccumulator = Blake2(Bi.VrfOut)'`. + +The first block produced for epoch `N` will propose as epoch `N+1` randomness the value +of `RandomnessAccumulator` as found after the production of the last block associated +to epoch `N-1`. + +#### 6.1.2. Protocol Configuration + +The `ProtocolConfiguration` primarily influences certain checks carried out +during ticket validation. It is defined as follows: + +```rust + ProtocolConfiguration ::= SEQUENCE { + attempts_number: U32, + redundancy_factor: U32 + } +``` + +- `attempts_number`: number of tickets that can be submitted by each next-epoch authority. +- `redundancy_factor`: factor that enhances the likelihood of a candidate ticket + being deemed valid according to some protocol specific checks. + +Further details regarding this configuration can be found in the section +dedicated to candidate ticket validation (ticket-id threshold computation). + +`ProtocolConfiguration` values can be adjusted via a dedicated extrinsic which should have origin set to `Root`. +A valid configuration proposal submitted on epoch `K` will be propagated in +the `NextEpochDescriptor` at the begin of epoch `K+1` and will be effectively +enacted on epoch `K+2`. + +A new `ProtocolConfiguration` can be submitted using a dedicated extrinsic, +with the requirement that its origin is set to `Root`. If a valid proposal is +submitted during epoch `N-1`, it will be embedded into the `NextEpochDescriptor` +at the beginning of epoch `N`. + +### 6.2. Creation and Submission of Candidate Tickets + +As a shorthand notation, in this section we will refer to one of the next epoch +validators simply as 'the validator'. + +Upon the beginning of a new epoch `N`, the validator will create a set of +'tickets' to be submitted on-chain. These tickets aim to secure ownership of one +of the slots in the upcoming epoch `N+1`. + +Each validator is allowed to submit a maximum number of tickets whose value is +found in the next epoch `ProtocolConfiguration` `attempts_number` field. + +Each ticket has an associated unique identifier, denoted as `TicketId`. + +```rust + TicketId ::= U128 +``` + +#### 6.2.1. Ticket Identifier Value + +The value of the `TicketId` is determined through the output of the Bandersnatch +VRF, using the following inputs: + +- Epoch `N+1` randomness: obtained from the `NextEpochDescriptor`. +- Epoch `N+1` index: tracked by the Sassafras on-chain code. +- Attempt index: a value from `0` to `attempts_number`. + +```rust + randomness = Randomness(..) + epoch_index = U64(..); + attempt_index = U32(..); + + ticket_id_vrf_input = vrf_input_from_items( + BYTES("sassafras-ticket-v1.0"), + [ randomness, BYTES(epoch), BYTES(attempt) ] + ); + + bytes = vrf_bytes(SECRET_KEY, ticket_id_vrf_input, 16); + ticket_id = U128(bytes); +``` + +#### 6.2.2. Tickets Threshold + +A `TicketId` value is considered valid if it is less than the ticket threshold. + +Parameters: +- `v`: the number of authorities (aka validators) in the epoch +- `s`: number of slots in the epoch +- `r`: the redundancy factor +- `a`: number of attempts +- `T`: ticket threshold value (`0 ≤ T ≤ 1`) + +For an epoch of `s` slots we want to have a number of tickets in expectation for +block production equal to the `r·s`. + +We need that there is a very small probability of their being less than `s` +winning tickets, even if up to `1/3` of authorities are offline. + +First we set the probability of a ticket winning as `T = (r·s)/(a·v)`. + +Let `n` be the number of validators who actually participate and so `v·2/3 ≤ n ≤ v`. + +These `n` validators make `a` attempts each, 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] ≤ exp(-s/21)`. + +For `s = 600` this gives `Pr[X < s] < 4·10⁻¹³`, and thus we end up with a great +tolerance over offline nodes and we end-up filling all the slots with tickets +with high probability. + +For more details about threshold formula please refer to the +[Probabilities and parameters](https://research.web3.foundation/Polkadot/protocols/block-production/SASSAFRAS#probabilities-and-parameters) +paragraph of the *layman description* of Sassafras protocol. + +#### 6.2.3. Ticket Body + +For every candidate ticket an associated ticket-body is constructed. + +```rust + EphemeralPublicKey ::= Ed25519PublicKey + + TicketBody ::= SEQUENCE { + attempt_index: AttemptIndex, + erased_pub: EphemeralPublicKey, + revealed_pub: EphemeralPublicKey + } +``` + +- `attempt_index`: attempt index used to generate the associated ticket_id. +- `erased_pub`: Ed25519 ephemeral public key which is invalidated as soon as the + ticket is claimed. +- `revealed_pub`: Ed25519 ephemeral public key which is enabled as soon as the + ticket is claimed. + +Erased key pair generation is left unspecified and free to the implementor. + +Revealed key pair is instead generated using bytes produced by the VRF with +input similar to the one used for ticket-id generation, only the label is different. + +```rust + vrf_input = vrf_input_from_items( + BYTES("sassafras-erased-seed-v1.0"), + [ randomness, BYTES(epoch), BYTES(attempt) ] + ); + + seed = vrf_bytes(SECRET_KEY, vrf_input, 32); + revealed_pub = EphemeralSecret(seed).public(); +``` + +The usage of the `EphemeralPublicKey`s will be clarified in the ticket claiming section + +#### 6.2.4. Ring Signature Production + +`TicketBody` must be signed using the Bandersnatch Ring-VRF flavor. + +```rust + sign_data = vrf_signature_data( + transcript_label: BYTES("sassafras-ticket-body-v1.0"), + transcript_data: [ SCALE(ticket_body) ], + vrf_inputs: [ ticket_id_vrf_input ] + ) + + ring_signature = ring_vrf_sign(SECRET_KEY, sign_data, RING_PROVER) +``` + +The body and the ring signature are bundled together in the `TicketEnvelope` + +```rust + TicketEnvelope ::= SEQUENCE { + ticket_body: TicketBody, + ring_signature: RingVrfSignature + } +``` + +All the ticket envelopes corresponding to valid tickets are submitted on-chain +via a dedicated unsigned extrinsic. + +### 6.3. Validation of candidate tickets + +All the actions in the steps described by this paragraph are executed by +on-chain code. + +The tickets are received via a dedicated unsigned extrinsic call. + +Generic validation rules: +- Tickets submissions must occur within the first half of the epoch. + (TODO: I expect this is to give time to the chain finality consensus to finalize + the on-chain tickets before next epoch starts) +- The transaction source must be one of the current session validators. + +Ticket specific validation rules: +- Ring signature is verified using the on-chain `RingVerifier`. +- Ticket identifier is computed from the (verified) `VrfOutput` contained in the + ring signature and its value is checked to be less than the ticket-threshold. + +Valid tickets bodies are persisted on-chain. + +### 6.4. Ticket-Slot assignment + +Before the beginning of the next epoch, i.e. the epoch in which the tickets are +supposed to be claimed, the on-chain list of tickets must be associated with the +next epoch's slots. + +The assignment process can happen any time in the second half of the submission +epoch, before the beginning of the next epoch. + +There must be at most one ticket per slot. + +- Initially, the complete list of tickets is sorted based on their ticket-id, + with smaller values coming first. +- In cases where there are more tickets than available slots, the list is pruned + by removing the larger value. +- Tickets are then assigned to the slots using an outside-in assignment strategy. + +**Outside-In Assignment** + +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 `epoch_start_slot - epoch_slot`. + +#### 6.4.1 Fallback Assignment + +In cases where `k` is less than `n`, some (*orphan*) slots in the middle of the +epoch will remain unassigned by any ticket. In such instances, these slots are +allocated using a fallback assignment method resembling the *AURA* protocol. + +The authorities set which is persisted on-chain stores the authorities with some +order. The authority index which has the right to claim a slot is computed as: + +```rust + authority_index = blake2_64(SCALE(epoch_randomness, slot)) mod authorities_number; +``` + +### 6.5. Claim of ticket ownership during block production + +Once that tickets are bounded to the epoch slots, each validator becomes aware +for which slots he can author a block. + +Slots are primarily claimed via VRF output. + +The author of a block must embed in the block header digest a `SlotClaim` entry which +has enough information to claim the ownership of the slot. + +```rust + SlotClaim ::= SEQUENCE { + authority_index: U32, + slot: U64, + vrf_signature: VrfSignature, + ticket_claim: EphemeralSignature OPTIONAL + } +``` + +- `authority_index`: index of the block author in the on-chain authorities list. +- `slot`: absolute monotonic slot number from genesis (note this is not the index for a slot in the epoch) +- `vrf_signature`: signature embedding one or two `VrfOutputs`. + One is always present and is used to generate per-block randomness. + The second is present if the slot has an associated ticket and is used to claim ticket ownership. +- `ticket_claim`: optional additional proof of ticket ownership. + Is a signature of some challenge which can be verified using the `erased_public` `EphemeralPublicKey` + committed via the `TicketBody`. + +TODO: what is the point of ticket_claim if we already have a method to claim the ticket? + +#### 6.5.1. Primary Claim Method + +The `VrfSignatureData` to produce the `SlotClaim` signature is constructed as: + +```rust + fn slot_claim_vrf_input = vrf_input_from_items( + domain: BYTES("sassafras-slot-claim-v1.0"), + data: [randomness, BYTES(epoch), BYTES(attempt)] + ); + + fn revealed_key_vrf_input = vrf_input_from_items( + domain: BYTES("sassafras-revealed-key-v1.0"), + data: [ TODO ] + ); + + signature_data = vrf_signature_data( + transcript_label: BYTES("sassafras-slot-claim-transcript-v1.0"), + transcript_data: [ SCALE(ticket_body) ], + vrf_inputs: [ slot_claim_vrf_input, revealed_key_vrf_input ] + ); +``` + +#### 6.5.2. Secondary Claim Method + +If the slot doesn't have any associated ticket then the validator is the one +with index equal to the rule exposed in paragraph [6.4.1]. + +The `VrfSignatureData` to produce the `SlotClaim` signature is constructed as: + +```rust + signature_data = vrf_signature_data( + transcript_label: BYTES("sassafras-slot-claim-transcript-v1.0"), + transcript_data: [ ], + vrf_inputs: [ slot_claim_vrf_input ] + ) +``` + +### 6.6. Validation of the claim during block verification + +TODO + + +## Drawbacks + +TODO + +Description of recognized drawbacks to the approach given in the RFC. +Non-exhaustively, drawbacks relating to performance, ergonomics, user experience, security, or privacy. + +## Testing, Security, and Privacy + +TODO + +Describe the the impact of the proposal on these three high-importance areas - how implementations can be tested for adherence, effects that the proposal has on security and privacy per-se, as well as any possible implementation pitfalls which should be clearly avoided. + +## Performance, Ergonomics, and Compatibility + +TODO + +Describe the impact of the proposal on the exposed functionality of Polkadot. + +### Performance + +TODO. Is this an optimization or a necessary pessimization? What steps have been taken to minimize additional overhead? + +Sassafras consensus usage is a huge step forward in short-lived forks reduction. + +### Ergonomics + +If the proposal alters exposed interfaces to developers or end-users, which types of usage patterns have been optimized for? + +### Compatibility + +The introduction of this consensus mechanism impacts native client code and thus can't be introduced via a runtime upgrade. + +Upgrading strategy should not be discussed here and must be discussed in a separately RFC. + + +## Prior Art and References + +- Web3 Foundation research page: https://research.web3.foundation/Polkadot/protocols/block-production/SASSAFRAS +- Sassafras whitepaper: https://eprint.iacr.org/2023/031.pdf +- Ring-VRF whitepaper: https://eprint.iacr.org/2023/002.pdf +- Sassafras reference implementation tracking issue: https://github.com/paritytech/substrate/issues/11515 +- Sassafras reference implementation main PR: https://github.com/paritytech/substrate/pull/11879 + +- TODO: ASN.1 + +## Unresolved Questions + +None + +## Future Directions and Related Material + +This RFC covers the foundations and the core of the protocol. + +Future RFCs should cover the following additional topics which makes the protocol complete and secure. + +- Deployment strategy. + - How this protocol can replace an already running instance of another one? +- ZK-SNARK SRS initialization ceremony. + - Should this process performed before sassafras deployment and how? + - As the process is quite involved, is the SRS shared with the parachains? +- Anonymous submission of on-chain tickets. + - Are we going to leverage Mixnet? + - ... From 2d26dd7b976fe81046f3565b26379d902c42f258 Mon Sep 17 00:00:00 2001 From: Davide Galassi Date: Mon, 4 Sep 2023 19:43:33 +0200 Subject: [PATCH 02/26] Further work --- text/0000-sassafras-consensus.md | 255 +++++++++++++++++++++---------- 1 file changed, 171 insertions(+), 84 deletions(-) diff --git a/text/0000-sassafras-consensus.md b/text/0000-sassafras-consensus.md index 8fb540c0a..88eae038d 100644 --- a/text/0000-sassafras-consensus.md +++ b/text/0000-sassafras-consensus.md @@ -463,18 +463,20 @@ Each ticket has an associated unique identifier, denoted as `TicketId`. The value of the `TicketId` is determined through the output of the Bandersnatch VRF, using the following inputs: -- Epoch `N+1` randomness: obtained from the `NextEpochDescriptor`. -- Epoch `N+1` index: tracked by the Sassafras on-chain code. -- Attempt index: a value from `0` to `attempts_number`. +- Epoch `N+1` randomness: a `Randomness` obtained from the `NextEpochDescriptor`. +- Epoch `N+1` index: a `U64` value tracked by the Sassafras on-chain code. +- Attempt index: a `U32` value from `0` to `attempts_number`. -```rust - randomness = Randomness(..) - epoch_index = U64(..); - attempt_index = U32(..); +Let `next_epoch` be an object with the information associated to the next epoch. +```rust ticket_id_vrf_input = vrf_input_from_items( BYTES("sassafras-ticket-v1.0"), - [ randomness, BYTES(epoch), BYTES(attempt) ] + [ + next_epoch.randomness, + BYTES(next_epoch.epoch_index), + BYTES(attempt_index) + ] ); bytes = vrf_bytes(SECRET_KEY, ticket_id_vrf_input, 16); @@ -528,12 +530,10 @@ paragraph of the *layman description* of Sassafras protocol. For every candidate ticket an associated ticket-body is constructed. ```rust - EphemeralPublicKey ::= Ed25519PublicKey - TicketBody ::= SEQUENCE { - attempt_index: AttemptIndex, - erased_pub: EphemeralPublicKey, - revealed_pub: EphemeralPublicKey + attempt_index: U32, + erased_pub: Ed25519PublicKey, + revealed_pub: Ed25519PublicKey } ``` @@ -543,22 +543,30 @@ For every candidate ticket an associated ticket-body is constructed. - `revealed_pub`: Ed25519 ephemeral public key which is enabled as soon as the ticket is claimed. -Erased key pair generation is left unspecified and free to the implementor. +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 bytes produced by the VRF with input +parameters equal to those employed in ticket-id generation, only the label is +different. -Revealed key pair is instead generated using bytes produced by the VRF with -input similar to the one used for ticket-id generation, only the label is different. +Let `next_epoch` be an object with the information associated to the next epoch. ```rust - vrf_input = vrf_input_from_items( - BYTES("sassafras-erased-seed-v1.0"), - [ randomness, BYTES(epoch), BYTES(attempt) ] + revealed_vrf_input = vrf_input_from_items( + domain: BYTES("sassafras-revealed-v1.0"), + data: [ + next_epoch.randomness, + BYTES(next_epoch.epoch_index), + BYTES(attempt_index) + ] ); - seed = vrf_bytes(SECRET_KEY, vrf_input, 32); - revealed_pub = EphemeralSecret(seed).public(); + revealed_seed = vrf_bytes(SECRET_KEY, revealed_vrf_input, 32); + revealed_pub = ed25519_secret_from_seed(revealed_seed).public(); ``` -The usage of the `EphemeralPublicKey`s will be clarified in the ticket claiming section +The usage of the `EphemeralPublicKey`s will be clarified in the ticket claiming section. #### 6.2.4. Ring Signature Production @@ -567,14 +575,22 @@ The usage of the `EphemeralPublicKey`s will be clarified in the ticket claiming ```rust sign_data = vrf_signature_data( transcript_label: BYTES("sassafras-ticket-body-v1.0"), - transcript_data: [ SCALE(ticket_body) ], - vrf_inputs: [ ticket_id_vrf_input ] + transcript_data: [ + SCALE(ticket_body) + ], + vrf_inputs: [ + ticket_id_vrf_input + ] ) - ring_signature = ring_vrf_sign(SECRET_KEY, sign_data, RING_PROVER) + ring_signature = ring_vrf_sign(SECRET_KEY, RING_PROVER, sign_data) ``` -The body and the ring signature are bundled together in the `TicketEnvelope` +`RING_PROVER` object is constructed using the set of public keys which belong to +the next epoch authorities and the *ZK-SNARK* initialization parameters +(more details in the [bandersnatch_vrfs](https://github.com/w3f/ring-vrf/blob/18614458ca4cb335c88d4e710c13906a76f51e43/bandersnatch_vrfs/src/ring.rs#L91-L93) reference implementation). + +The body and the ring signature are combined in the `TicketEnvelope`: ```rust TicketEnvelope ::= SEQUENCE { @@ -597,7 +613,7 @@ Generic validation rules: - Tickets submissions must occur within the first half of the epoch. (TODO: I expect this is to give time to the chain finality consensus to finalize the on-chain tickets before next epoch starts) -- The transaction source must be one of the current session validators. +- The transaction must be submitted by one of the current session validators. Ticket specific validation rules: - Ring signature is verified using the on-chain `RingVerifier`. @@ -634,6 +650,13 @@ strategy: Here `slot-index` is a relative value computed as `epoch_start_slot - epoch_slot`. +The association between each ticket and its corresponding slot is recorded on +the blockchain and is publicly visible to all. What remains confidential is the +identity of the ticket *owner*, and consequently, who possesses the authority to +claim the corresponding slot. This information is known only to the author of +the ticket. + + #### 6.4.1 Fallback Assignment In cases where `k` is less than `n`, some (*orphan*) slots in the middle of the @@ -649,113 +672,159 @@ order. The authority index which has the right to claim a slot is computed as: ### 6.5. Claim of ticket ownership during block production -Once that tickets are bounded to the epoch slots, each validator becomes aware -for which slots he can author a block. +Once that tickets are associated with epoch slots, each validator gains +knowledge of the slots for which they can author a block. -Slots are primarily claimed via VRF output. +The primary method for claiming slots is through the use of the VRF output. -The author of a block must embed in the block header digest a `SlotClaim` entry which -has enough information to claim the ownership of the slot. +To establish ownership of a slot, the block author must include a SCALE encoded +`SlotClaim` item in the block header digest. This structure contains the +necessary information to assert ownership of the slot. ```rust SlotClaim ::= SEQUENCE { authority_index: U32, slot: U64, - vrf_signature: VrfSignature, + signature: VrfSignature, ticket_claim: EphemeralSignature OPTIONAL } ``` - `authority_index`: index of the block author in the on-chain authorities list. -- `slot`: absolute monotonic slot number from genesis (note this is not the index for a slot in the epoch) -- `vrf_signature`: signature embedding one or two `VrfOutputs`. - One is always present and is used to generate per-block randomness. - The second is present if the slot has an associated ticket and is used to claim ticket ownership. -- `ticket_claim`: optional additional proof of ticket ownership. - Is a signature of some challenge which can be verified using the `erased_public` `EphemeralPublicKey` - committed via the `TicketBody`. -TODO: what is the point of ticket_claim if we already have a method to claim the ticket? +- `slot`: absolute slot number (this is not the relative index within the epoch) + +- `vrf_signature`: This is a signature that includes one or two `VrfOutputs`. + - The first `VrfOutput` is always present and is used to generate per-block + randomness. This is not relevant to claim ticket ownership. + - The second `VrfOutput` is included if the slot is associated with a ticket. + This is relevant to claim ticket ownership. + +- `ticket_claim`: optional element providing an additional proof of ticket + ownership. It comprises a signature of the challenge derived from the signed + data transcript. This signature is verified using the `erased_public` + `EphemeralPublicKey` that was committed via the `TicketBody`. + +TODO: what is the point of claiming the ticket via the `ticket_claim` if we already +have a method to claim the ticket via the VRF output? #### 6.5.1. Primary Claim Method -The `VrfSignatureData` to produce the `SlotClaim` signature is constructed as: +This claim mechanism is employed when the slot is bounded to a ticket, as +determined by the chain's current state. + +In this case the `VrfSignature` within the `SlotClaim` contains two `VrfOutput` +elements. + +Let `ticket_body` represent the `TicketBody` that has been committed to the +on-chain state, `curr_epoch` denote an object containing information about +the current epoch and `slot` the absolute monotonic slot number. + +The construction of the `VrfSignatureData` for generating the `SlotClaim` +signature is as follows: + ```rust - fn slot_claim_vrf_input = vrf_input_from_items( - domain: BYTES("sassafras-slot-claim-v1.0"), - data: [randomness, BYTES(epoch), BYTES(attempt)] + randomness_vrf_input = vrf_input_from_items( + domain: BYTES("sassafras-randomness-v1.0"), + data: [ + curr_epoch.randomness, + BYTES(curr_epoch.epoch_index), + BYTES(slot) + ] ); - fn revealed_key_vrf_input = vrf_input_from_items( - domain: BYTES("sassafras-revealed-key-v1.0"), - data: [ TODO ] + revealed_vrf_input = vrf_input_from_items( + domain: BYTES("sassafras-revealed-v1.0"), + data: [ + curr_epoch.randomness, + BYTES(curr_epoch.epoch_index), + BYTES(ticket_body.attempt_index) + ] ); signature_data = vrf_signature_data( - transcript_label: BYTES("sassafras-slot-claim-transcript-v1.0"), - transcript_data: [ SCALE(ticket_body) ], - vrf_inputs: [ slot_claim_vrf_input, revealed_key_vrf_input ] + transcript_label: BYTES("sassafras-claim-v1.0"), + transcript_data: [ + SCALE(ticket_body) + ], + vrf_inputs: [ + randomness_vrf_input, + revealed_vrf_input + ] ); ``` #### 6.5.2. Secondary Claim Method If the slot doesn't have any associated ticket then the validator is the one -with index equal to the rule exposed in paragraph [6.4.1]. +with index equal to the rule exposed in paragraph `6.4.1`. -The `VrfSignatureData` to produce the `SlotClaim` signature is constructed as: +Given `randomness_vrf_input` constructed as shown for the primary method, the +`VrfSignatureData` to produce the `SlotClaim` signature is constructed as: ```rust signature_data = vrf_signature_data( transcript_label: BYTES("sassafras-slot-claim-transcript-v1.0"), transcript_data: [ ], - vrf_inputs: [ slot_claim_vrf_input ] + vrf_inputs: [ + randomness_vrf_input + ] ) ``` ### 6.6. Validation of the claim during block verification -TODO +Ticket ownership claims should be validated. +Validation is performed using the second `VrfOutput` found in the SlotClaim signature. +Using this object the verifier is able to reconstruct the val -## Drawbacks -TODO -Description of recognized drawbacks to the approach given in the RFC. -Non-exhaustively, drawbacks relating to performance, ergonomics, user experience, security, or privacy. +## 7. Drawbacks + +None -## Testing, Security, and Privacy +## 8. Testing, Security, and Privacy -TODO +TODO ? -Describe the the impact of the proposal on these three high-importance areas - how implementations can be tested for adherence, effects that the proposal has on security and privacy per-se, as well as any possible implementation pitfalls which should be clearly avoided. +Describe the impact of the proposal on these three high-importance areas +- how implementations can be tested for adherence, +- effects that the proposal has on security and +- privacy per-se, as well as any possible implementation pitfalls which should be clearly avoided. -## Performance, Ergonomics, and Compatibility +## 9. Performance, Ergonomics, and Compatibility -TODO +TODO ? Describe the impact of the proposal on the exposed functionality of Polkadot. -### Performance +### 9.1. Performance -TODO. Is this an optimization or a necessary pessimization? What steps have been taken to minimize additional overhead? +TODO ? + +Is this an optimization or a necessary pessimization? What steps have been taken to minimize additional overhead? Sassafras consensus usage is a huge step forward in short-lived forks reduction. -### Ergonomics +### 9.2 Ergonomics + +TODO ? If the proposal alters exposed interfaces to developers or end-users, which types of usage patterns have been optimized for? -### Compatibility +### 9.3 Compatibility -The introduction of this consensus mechanism impacts native client code and thus can't be introduced via a runtime upgrade. +The introduction of this consensus mechanism impacts native client code and thus +can't be introduced via a runtime upgrade. -Upgrading strategy should not be discussed here and must be discussed in a separately RFC. +Upgrading strategy should not be discussed here and must be discussed in a +separately RFC. -## Prior Art and References +## 10. Prior Art and References - Web3 Foundation research page: https://research.web3.foundation/Polkadot/protocols/block-production/SASSAFRAS - Sassafras whitepaper: https://eprint.iacr.org/2023/031.pdf @@ -763,23 +832,41 @@ Upgrading strategy should not be discussed here and must be discussed in a separ - Sassafras reference implementation tracking issue: https://github.com/paritytech/substrate/issues/11515 - Sassafras reference implementation main PR: https://github.com/paritytech/substrate/pull/11879 -- TODO: ASN.1 -## Unresolved Questions +## 11. Unresolved Questions None -## Future Directions and Related Material -This RFC covers the foundations and the core of the protocol. +## 12. Future Directions and Related Material + +While this RFC lays the groundwork and outlines the core aspects of the +protocol, several crucial topics remain to be addressed in future RFCs to ensure +the protocol's completeness and security. + +These topics include: + +### 12.1. Deployment Strategies + +- **Protocol Migration**. Exploring how this protocol can seamlessly replace + an already operational instance of another protocol is essential. Future RFCs + should delve into the deployment strategy, including considerations for a smooth + transition process. + +### 12.2. ZK-SNARK SRS Initialization Ceremony. + +- **Timing and Procedure**: Determining the timing and procedure for the ZK-SNARK + SRS (Structured Reference String) initialization ceremony. Future RFCs should + provide insights into whether this process should be performed before the + deployment of Sassafras and the steps involved. + +- **Sharing with Parachains**: Considering the complexity of the ceremony, we + must understand whether the SRS is shared with parachains or maintained + independently. -Future RFCs should cover the following additional topics which makes the protocol complete and secure. +### 12.3. Anonymous Submission of Tickets. -- Deployment strategy. - - How this protocol can replace an already running instance of another one? -- ZK-SNARK SRS initialization ceremony. - - Should this process performed before sassafras deployment and how? - - As the process is quite involved, is the SRS shared with the parachains? -- Anonymous submission of on-chain tickets. - - Are we going to leverage Mixnet? - - ... +- **Mixnet Integration**: Submitting tickets directly can pose a risk of + potential deanonymization through traffic analysis. Subsequent RFCs should + investigate the potential for incorporating Mixnet technology or other + privacy-enhancing mechanisms to address this concern. From cda64a88560dba94dbc8e81e17712b4e234e58c8 Mon Sep 17 00:00:00 2001 From: Davide Galassi Date: Wed, 6 Sep 2023 16:02:55 +0200 Subject: [PATCH 03/26] Finish draft --- text/0000-sassafras-consensus.md | 313 +++++++++++++++++++++---------- 1 file changed, 215 insertions(+), 98 deletions(-) diff --git a/text/0000-sassafras-consensus.md b/text/0000-sassafras-consensus.md index 88eae038d..5480363ed 100644 --- a/text/0000-sassafras-consensus.md +++ b/text/0000-sassafras-consensus.md @@ -1,10 +1,10 @@ # RFC-xx: Sassafras Consensus Protocol -| | | -| --------------- | ------------------------------------------------------------------------------------------- | -| **Start Date** | September 03, 2023 | -| **Description** | Sassafras consensus protocol description and structures | -| **Authors** | Davide Galassi | +| | | +| --------------- | ---------------------------------------------------------------- | +| **Start Date** | September 03, 2023 | +| **Description** | Sassafras consensus protocol description and structures | +| **Authors** | Davide Galassi | ## Abstract @@ -71,14 +71,15 @@ to ensure clarity and consistency. Data structures are primarily defined using [ASN.1](https://en.wikipedia.org/wiki/ASN.1), with a few exceptions: -- Integer types are not explicitly defined in ASN.1 and `Un` types should be - interpreted as n-bit unsigned integers. +- Integer types are not explicitly defined in ASN.1 and in the context of + this document `U` should be interpreted as `n`-bit unsigned integers + (e.g. `U32`) -To ensure that serialized data structures can be used interchangeably, it's -important to consider the arrangement of fields in their definitions. +To ensure interoperability of serialized structures, it's important to consider +the order of fields in their definitions. In cases where no specific instructions are given, structures should be -serialized using [SCALE](https://github.com/paritytech/parity-scale-codec). +serialized using [SCALE](). ### 3.2. Pseudo-Code @@ -86,24 +87,23 @@ Through this document it is advantageous to make use of code snippets as part of the comprehensive description. These code snippets shall adhere to the subsequent conventions: -- Code snippets are presented in a Rust-like pseudo-code format. +- For simplicity, code snippets are presented in a *Rust-like* pseudo-code format. -- The function `SCALE(x: T)` returns the `SCALE` encoding of the variable `x` - with type `T`. - -- The function `BYTES(x: T)` returns an `OCTET STRING` which represents the raw +- The function `BYTES(x: T)` returns an `OCTET STRING` representing the raw byte array representation of the object `x` with type `T`. - if `T` is `VisibleString` (aka *ascii* string): it returns the sequence of bytes of its *ascii* representation. - - if `T` is `Un`: it returns the little-endian encoding of the integer `Un` - as `n` bytes. + - if `T` is `U`: it returns the little-endian encoding of the integer + `U` as `n/8` bytes. + +- The function `SCALE(x: T)` returns an `OCTET_STRING` representing the + [`SCALE`](https://github.com/paritytech/parity-scale-codec) encoding of the + variable `x` with type `T`. -- The function `Un(x: OCTET STRING)` returns a `Un` using the little-endian - encoding of the integer `x` +- The function `U(x: OCTET STRING)` returns a `U` interpreting `x` as + the little-endian encoding of a `n` bits unsigned integer. -- The function `TRANSCRIPT(x)` represents the construction of a transcript - object as defined by [`ark-transcript`](https://docs.rs/ark-transcript/0.0.1/ark_transcript/) - library. +- The function `BLAKE2_` returns bytes of the standard *blake2b* hash. ### 3.3. Incremental Introduction of Types and Functions @@ -292,7 +292,7 @@ plain Bandersnatch signature flavor. Helper function to create a `VrfSignature` from `VrfSignatureData`: ```rust - fn vrf_sign( + fn plain_vrf_sign( secret: BandernatchSecretKey, signature_data: VrfSignatureData ) -> VrfSignature @@ -302,7 +302,7 @@ Helper function for validating the signature and returning a Boolean value indicating the validity of the signature (`True` if it's valid): ```rust - fn vrf_verify( + fn plain_vrf_verify( public: BandersnatchPublicKey, signature: VrfSignature ) -> Boolean; @@ -479,8 +479,10 @@ Let `next_epoch` be an object with the information associated to the next epoch. ] ); - bytes = vrf_bytes(SECRET_KEY, ticket_id_vrf_input, 16); - ticket_id = U128(bytes); + ticket_id_vrf_output = vrf_output(AUTHORITY_SECRET_KEY, ticket_id_vrf_input); + + ticket_bytes = vrf_bytes(ticket_id_vrf_input, ticket_id_vrf_output, 16); + ticket_id = U128(ticket_bytes); ``` #### 6.2.2. Tickets Threshold @@ -562,7 +564,9 @@ Let `next_epoch` be an object with the information associated to the next epoch. ] ); - revealed_seed = vrf_bytes(SECRET_KEY, revealed_vrf_input, 32); + revealed_vrf_output = vrf_output(AUTHORITY_SECRET_KEY, ticket_id_vrf_input); + + revealed_seed = vrf_bytes(revealed_vrf_input, revealed_vrf_output, 32); revealed_pub = ed25519_secret_from_seed(revealed_seed).public(); ``` @@ -583,7 +587,7 @@ The usage of the `EphemeralPublicKey`s will be clarified in the ticket claiming ] ) - ring_signature = ring_vrf_sign(SECRET_KEY, RING_PROVER, sign_data) + ring_signature = ring_vrf_sign(AUTHORITY_SECRET_KEY, RING_PROVER, sign_data) ``` `RING_PROVER` object is constructed using the set of public keys which belong to @@ -656,73 +660,48 @@ identity of the ticket *owner*, and consequently, who possesses the authority to claim the corresponding slot. This information is known only to the author of the ticket. - #### 6.4.1 Fallback Assignment -In cases where `k` is less than `n`, some (*orphan*) slots in the middle of the -epoch will remain unassigned by any ticket. In such instances, these slots are -allocated using a fallback assignment method resembling the *AURA* protocol. - -The authorities set which is persisted on-chain stores the authorities with some -order. The authority index which has the right to claim a slot is computed as: - -```rust - authority_index = blake2_64(SCALE(epoch_randomness, slot)) mod authorities_number; -``` - -### 6.5. Claim of ticket ownership during block production +In cases where 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. -Once that tickets are associated with epoch slots, each validator gains -knowledge of the slots for which they can author a block. +In such situations, these unassigned slots are allocated using a fallback +assignment method. -The primary method for claiming slots is through the use of the VRF output. - -To establish ownership of a slot, the block author must include a SCALE encoded -`SlotClaim` item in the block header digest. This structure contains the -necessary information to assert ownership of the slot. +The on-chain authority set contains the authorities for the current epoch in a +specific order. The index of the authority which has the privilege to claim a +slot is calculated as follows: ```rust - SlotClaim ::= SEQUENCE { - authority_index: U32, - slot: U64, - signature: VrfSignature, - ticket_claim: EphemeralSignature OPTIONAL - } + index = blake2b_64(SCALE((epoch_randomness, slot))) mod authorities_number; ``` -- `authority_index`: index of the block author in the on-chain authorities list. +TODO: what about using `epoch_randomness_accumulator` instead of `epoch_randomness`? +The accumulator is updated using the randomness which ships with every block, thus +we know who is the author of block N only after block N-1 has been imported. +Is a bit more resistant to DoS. But given the sporadic nature of secondary method +maybe this is not a bit deal anyway. -- `slot`: absolute slot number (this is not the relative index within the epoch) +### 6.5. Claim of ticket ownership during block production -- `vrf_signature`: This is a signature that includes one or two `VrfOutputs`. - - The first `VrfOutput` is always present and is used to generate per-block - randomness. This is not relevant to claim ticket ownership. - - The second `VrfOutput` is included if the slot is associated with a ticket. - This is relevant to claim ticket ownership. +With tickets bound to epoch slots, every validator acquires information about +the slots for which they are eligible to produce a block. -- `ticket_claim`: optional element providing an additional proof of ticket - ownership. It comprises a signature of the challenge derived from the signed - data transcript. This signature is verified using the `erased_public` - `EphemeralPublicKey` that was committed via the `TicketBody`. +The procedure for block authoring varies based on whether a given slot has an +associated ticket according to the on-chain state. -TODO: what is the point of claiming the ticket via the `ticket_claim` if we already -have a method to claim the ticket via the VRF output? +If a slot is associated with a ticket, we will employ the primary authoring +method. Conversely, if the slot lacks an associated ticket, we will resort to +the secondary authoring method as a fallback. #### 6.5.1. Primary Claim Method -This claim mechanism is employed when the slot is bounded to a ticket, as -determined by the chain's current state. - -In this case the `VrfSignature` within the `SlotClaim` contains two `VrfOutput` -elements. - -Let `ticket_body` represent the `TicketBody` that has been committed to the -on-chain state, `curr_epoch` denote an object containing information about -the current epoch and `slot` the absolute monotonic slot number. - -The construction of the `VrfSignatureData` for generating the `SlotClaim` -signature is as follows: +Let `ticket_body` represent 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 absolute monotonic slot number. +Follows the construction of the `VrfSignatureData`: ```rust randomness_vrf_input = vrf_input_from_items( @@ -743,7 +722,7 @@ signature is as follows: ] ); - signature_data = vrf_signature_data( + sign_data = vrf_signature_data( transcript_label: BYTES("sassafras-claim-v1.0"), transcript_data: [ SCALE(ticket_body) @@ -755,16 +734,39 @@ signature is as follows: ); ``` +The inclusion of `revealed_vrf_input` allows the verifier to reconstruct the +`revealed_pub` key which has been committed into the `TicketBody`. + +##### 6.5.1.1 (Optional) Ed25519 Erased Ephemeral Key Claim + +As the ticket ownership was already checked using the primary method, this +step is purely optional and serves only to enforce the claim. + +TODO: 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. + +The *Fiat-Shamir* transform is utilized 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 this challenge. + +```rust + challenge = sign_data.transcript.challenge(); + erased_signature = ed25519_sign(ERASED_SECRET_KEY, challenge) +``` + #### 6.5.2. Secondary Claim Method If the slot doesn't have any associated ticket then the validator is the one with index equal to the rule exposed in paragraph `6.4.1`. Given `randomness_vrf_input` constructed as shown for the primary method, the -`VrfSignatureData` to produce the `SlotClaim` signature is constructed as: +`VrfSignatureData` is constructed as: ```rust - signature_data = vrf_signature_data( + sign_data = vrf_signature_data( transcript_label: BYTES("sassafras-slot-claim-transcript-v1.0"), transcript_data: [ ], vrf_inputs: [ @@ -773,13 +775,134 @@ Given `randomness_vrf_input` constructed as shown for the primary method, the ) ``` +#### 6.5.3. Slot Claim object + +To establish ownership of a slot, the block author must construct a `SlotClaim` object +which contains all the necessary information to assert ownership of the slot. + +```rust + SlotClaim ::= SEQUENCE { + authority_index: U32, + slot: U64, + signature: VrfSignature, + erased_signature: Ed25519Signature OPTIONAL + } +``` + +- `authority_index`: index of the block author in the on-chain authorities list. + +- `slot`: absolute slot number (this is not the relative index within the epoch) + +- `signature`: This is a signature that includes one or two `VrfOutputs`. + - The first `VrfOutput` is always present and is used to generate per-block + randomness. This is not relevant to claim ticket ownership. + - The second `VrfOutput` is included if the slot is associated with a ticket. + This is relevant to claim ticket ownership using primary method. + +- `erased_signature`: optional signature providing an additional proof of ticket + ownership (see 6.5.1.1). + +```rust + signature = plain_vrf_sign(AUTHORITY_SECRET_KEY, sign_data); + + claim = SlotClaim { + authority_index, + slot, + signature, + erased_signature + } +``` + +The `claim` object is *SCALE* encoded and sent as a block's header digest log +item. + ### 6.6. Validation of the claim during block verification -Ticket ownership claims should be validated. +Validation of `SlotClaim` object as found in the block's header. + +The procedure depends on whether the slot has an associated ticket or not +according to the on-chain state. + +If there is a ticket linked to the slot, we will utilize the primary +verification method; otherwise, the protocol resorts to the secondary one. + +In both scenarios, the signature within the `SlotClaim` is verified using a +`VrfSignData` that matches the one according to paragraph 6.5. If signature +verification fails then the claim is not legit. + +Given `claim` the instance of `SlotClaim` within the block header. + +```rust + plain_vrf_verify(AUTHORITY_PUBLIC_KEY, sign_data, claim.signature); +``` + +### 6.6.1. Primary Claim Method Verification + +This verification is performed to confirm ticket ownership and is performed +utilizing the second `VrfOutput` contained within the `SlotClaim` `signature`. + +By using the `VrfOutput` object together with the corresponding expected +`VrfInput` the verifier should be able to reconstruct the `revealed_pub` key +committed in the `TicketBody`. If there is a mismatch, the claim is not legit. + +```rust + reveled_vrf_output = claim.signature.vrf_outputs[1]; + + revealed_seed = vrf_bytes(revealed_vrf_input, revealed_vrf_output, 32); + revealed_pub = ed25519_secret_from_seed(revealed_seed).public(); +``` + +##### 6.6.1.1 (Optional) Ephemeral Key Signature Check + +If the `erased_signature` element within the `SlotClaim` is present the +`erased_pub` key is used to verify it. + +The signed challenge is generated through the identical steps as outlined in +section 6.5.1.1. -Validation is performed using the second `VrfOutput` found in the SlotClaim signature. -Using this object the verifier is able to reconstruct the val +#### 6.6.2. Secondary Claim Method Verification +If the slot doesn't have any associated ticket then the validator index which signed +the claim should match the one given by the rule outlined in section 6.4.1. + +### 6.7. Randomness Accumulator + +The first `VrfOutput` which ships with the `SlotClaim` `signature` is mandatory and +is always used to provide some randomness which gets accumulated in on-chain state +after block processing. + +Given `claim` the instance of `SlotClaim` within the block header, and +`accumulator` the current value for current epoch randomness accumulator, +the `accumulator` value is updated as follows: + +```rust + randomness_vrf_input = vrf_input_from_items( + domain: BYTES("sassafras-randomness-v1.0"), + data: [ + curr_epoch.randomness, + BYTES(curr_epoch.epoch_index), + BYTES(slot) + ] + ); + + randomness_vrf_output = claim.signature.vrf_outputs[0]; + + randomness = vrf_bytes(randomness_vrf_input, randomness_vrf_output, 32); + + accumulator = BLAKE2_256(CONCATENATE(accumulator, randomness)); +``` + +The updated `accumulator` value is stored on-chain. + +The randomess accumulated during epoch `N` will be used, at the start of the +next epoch (`N+1`), as the value to be stored within the `NextEpochDescriptor` +`randomness` element (see section 6.1) to be used as the epoch `N+2` randomness +value. + +As outlined throughout the document, epoch randomness value secures various +protocol-specific functions, including ticket generation and assignment of +fallback slots (refer to section 6.4.1). Additionally, users may utilize this +value for other purposes as needed. ## 7. Drawbacks @@ -797,17 +920,14 @@ Describe the impact of the proposal on these three high-importance areas ## 9. Performance, Ergonomics, and Compatibility -TODO ? - -Describe the impact of the proposal on the exposed functionality of Polkadot. - ### 9.1. Performance -TODO ? - -Is this an optimization or a necessary pessimization? What steps have been taken to minimize additional overhead? +The utilization of Sassafras consensus represents a significant advancement in +the mitigation of short-lived fork occurrences. -Sassafras consensus usage is a huge step forward in short-lived forks reduction. +Generation of forks are not possible when following the protocol and the only source +of forks is network partitioning. In this case, on recovery, the decision of +which fork to follow is not opinionated and there is only one choice. ### 9.2 Ergonomics @@ -817,11 +937,8 @@ If the proposal alters exposed interfaces to developers or end-users, which type ### 9.3 Compatibility -The introduction of this consensus mechanism impacts native client code and thus -can't be introduced via a runtime upgrade. - -Upgrading strategy should not be discussed here and must be discussed in a -separately RFC. +The adoption of Sassafras impacts native client code and thus can't be +introduced just via a runtime upgrade. ## 10. Prior Art and References From ec1fade8080f436597f2f36c4822901c95ae967b Mon Sep 17 00:00:00 2001 From: Davide Galassi Date: Wed, 6 Sep 2023 16:05:28 +0200 Subject: [PATCH 04/26] Rename RFC --- ...000-sassafras-consensus.md => 0026-sassafras-consensus.md} | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) rename text/{0000-sassafras-consensus.md => 0026-sassafras-consensus.md} (99%) diff --git a/text/0000-sassafras-consensus.md b/text/0026-sassafras-consensus.md similarity index 99% rename from text/0000-sassafras-consensus.md rename to text/0026-sassafras-consensus.md index 5480363ed..95b9a89e6 100644 --- a/text/0000-sassafras-consensus.md +++ b/text/0026-sassafras-consensus.md @@ -1,8 +1,8 @@ -# RFC-xx: Sassafras Consensus Protocol +# RFC-26: Sassafras Consensus Protocol | | | | --------------- | ---------------------------------------------------------------- | -| **Start Date** | September 03, 2023 | +| **Start Date** | September 06, 2023 | | **Description** | Sassafras consensus protocol description and structures | | **Authors** | Davide Galassi | From 639ccd077f0edca0b41a6a119a664e46a7c51070 Mon Sep 17 00:00:00 2001 From: Davide Galassi Date: Wed, 6 Sep 2023 16:07:49 +0200 Subject: [PATCH 05/26] Nit --- text/0026-sassafras-consensus.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/text/0026-sassafras-consensus.md b/text/0026-sassafras-consensus.md index 95b9a89e6..1b6c76401 100644 --- a/text/0026-sassafras-consensus.md +++ b/text/0026-sassafras-consensus.md @@ -1,4 +1,4 @@ -# RFC-26: Sassafras Consensus Protocol +# RFC-0026: Sassafras Consensus Protocol | | | | --------------- | ---------------------------------------------------------------- | From 2bd3b7f43fedb20c03709546b3dc6ec2f48f75ad Mon Sep 17 00:00:00 2001 From: Davide Galassi Date: Wed, 6 Sep 2023 16:18:09 +0200 Subject: [PATCH 06/26] Nit Nit --- text/0026-sassafras-consensus.md | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/text/0026-sassafras-consensus.md b/text/0026-sassafras-consensus.md index 1b6c76401..d74419c69 100644 --- a/text/0026-sassafras-consensus.md +++ b/text/0026-sassafras-consensus.md @@ -540,9 +540,9 @@ For every candidate ticket an associated ticket-body is constructed. ``` - `attempt_index`: attempt index used to generate the associated ticket_id. -- `erased_pub`: Ed25519 ephemeral public key which is invalidated as soon as the +- `erased_pub`: Ed25519 ephemeral public key which gets erased as soon as the ticket is claimed. -- `revealed_pub`: Ed25519 ephemeral public key which is enabled as soon as the +- `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, @@ -649,8 +649,10 @@ 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 ]. +``` + slot-index : [ 0, 1, 2, ............ , n ] + tickets : [ t1, t3, t5, ... , t4, t2, t0 ] +``` Here `slot-index` is a relative value computed as `epoch_start_slot - epoch_slot`. From d960fafc856f3ece99605285ce2c85b904a482af Mon Sep 17 00:00:00 2001 From: Davide Galassi Date: Wed, 6 Sep 2023 16:28:41 +0200 Subject: [PATCH 07/26] Nit Nit Nit --- text/0026-sassafras-consensus.md | 27 ++++++++++++--------------- 1 file changed, 12 insertions(+), 15 deletions(-) diff --git a/text/0026-sassafras-consensus.md b/text/0026-sassafras-consensus.md index d74419c69..2aac4808e 100644 --- a/text/0026-sassafras-consensus.md +++ b/text/0026-sassafras-consensus.md @@ -32,13 +32,10 @@ interoperability and clarifying aspects left open for implementation discretion. ### 1.1. Relevance to Implementors This RFC focuses on providing implementors with the necessary insights into the -protocol's operation. It takes precedence over the research paper in cases where -discrepancies arise between the two documents. +protocol's operation. -(TODO: remove this) -Example: In this RFC, ticket claims occur in the epoch immediately following -issuance, whereas the original protocol description specifies a two-epoch gap. -The approach outlined in this RFC should be followed. +To avoid ambiguities and interoperability issues, this document takes precedence +over the research paper in cases where discrepancies arise between the two. ### 1.2. Supporting Sassafras for Polkadot @@ -50,13 +47,13 @@ paves the way for integrating Sassafras into the Polkadot network. ## 2. Stakeholders -### 2.1 Developers of Relay-Chains and Para-Chains +### 2.1. Developers of Relay-Chains and Para-Chains Developers responsible for creating relay-chains and para-chains within the Polkadot ecosystem who intend to leverage the benefits offered by the Sassafras Protocol. -### 2.2 Developers of Polkadot Relay-Chain +### 2.2. Developers of Polkadot Relay-Chain Developers contributing to the Polkadot relay-chain, which plays a pivotal role in facilitating the interoperability and functionality of the Sassafras Protocol @@ -360,7 +357,7 @@ left undefined as are not relevant. Their definitions can be found in the ## 6. Sassafras Protocol -### 6.1 Epoch's First Block +### 6.1. Epoch's First Block The first block produced for an epoch `N` is required to include the descriptor for the next epoch `N+1`. @@ -394,7 +391,7 @@ Consequently, every node executing the block has the capability to verify if the descriptor generated during block execution matches the one produced by the block author, which is stored in the digest data. -#### 6.1.1 Epoch Randomness +#### 6.1.1. Epoch Randomness Each epoch has an associated randomness value defined by the `NextEpochDescriptor` `Randomness` element. @@ -662,7 +659,7 @@ identity of the ticket *owner*, and consequently, who possesses the authority to claim the corresponding slot. This information is known only to the author of the ticket. -#### 6.4.1 Fallback Assignment +#### 6.4.1. Fallback Assignment In cases where 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 @@ -739,7 +736,7 @@ Follows the construction of the `VrfSignatureData`: The inclusion of `revealed_vrf_input` allows the verifier to reconstruct the `revealed_pub` key which has been committed into the `TicketBody`. -##### 6.5.1.1 (Optional) Ed25519 Erased Ephemeral Key Claim +##### 6.5.1.1. (Optional) Ed25519 Erased Ephemeral Key Claim As the ticket ownership was already checked using the primary method, this step is purely optional and serves only to enforce the claim. @@ -854,7 +851,7 @@ committed in the `TicketBody`. If there is a mismatch, the claim is not legit. revealed_pub = ed25519_secret_from_seed(revealed_seed).public(); ``` -##### 6.6.1.1 (Optional) Ephemeral Key Signature Check +##### 6.6.1.1. (Optional) Ephemeral Key Signature Check If the `erased_signature` element within the `SlotClaim` is present the `erased_pub` key is used to verify it. @@ -931,13 +928,13 @@ Generation of forks are not possible when following the protocol and the only so of forks is network partitioning. In this case, on recovery, the decision of which fork to follow is not opinionated and there is only one choice. -### 9.2 Ergonomics +### 9.2. Ergonomics TODO ? If the proposal alters exposed interfaces to developers or end-users, which types of usage patterns have been optimized for? -### 9.3 Compatibility +### 9.3. Compatibility The adoption of Sassafras impacts native client code and thus can't be introduced just via a runtime upgrade. From 80637e976294d245d3325f3132f717e09b7b6228 Mon Sep 17 00:00:00 2001 From: Davide Galassi Date: Wed, 6 Sep 2023 16:40:44 +0200 Subject: [PATCH 08/26] Typos --- text/0026-sassafras-consensus.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/text/0026-sassafras-consensus.md b/text/0026-sassafras-consensus.md index 2aac4808e..4d4f9f2bc 100644 --- a/text/0026-sassafras-consensus.md +++ b/text/0026-sassafras-consensus.md @@ -86,7 +86,7 @@ subsequent conventions: - For simplicity, code snippets are presented in a *Rust-like* pseudo-code format. -- The function `BYTES(x: T)` returns an `OCTET STRING` representing the raw +- The function `BYTES(x: T)` returns an `OCTET_STRING` representing the raw byte array representation of the object `x` with type `T`. - if `T` is `VisibleString` (aka *ascii* string): it returns the sequence of bytes of its *ascii* representation. @@ -97,7 +97,7 @@ subsequent conventions: [`SCALE`](https://github.com/paritytech/parity-scale-codec) encoding of the variable `x` with type `T`. -- The function `U(x: OCTET STRING)` returns a `U` interpreting `x` as +- The function `U(x: OCTET_STRING)` returns a `U` interpreting `x` as the little-endian encoding of a `n` bits unsigned integer. - The function `BLAKE2_` returns bytes of the standard *blake2b* hash. @@ -163,7 +163,7 @@ referring to the Ring-VRF research [paper](https://eprint.iacr.org/2023/002.pdf) ### 5.1. VRF Input The VRF Input, denoted as `VrfInput`, is constructed using a domain and some -arbitrary data using the `vrf_input(domain: OCTET STRING, buf: OCTET STRING)` +arbitrary data using the `vrf_input(domain: OCTET_STRING, buf: OCTET_STRING)` function. This function is left opaque here and can be read in the actual reference implementation. @@ -673,7 +673,7 @@ specific order. The index of the authority which has the privilege to claim a slot is calculated as follows: ```rust - index = blake2b_64(SCALE((epoch_randomness, slot))) mod authorities_number; + index = BLAKE2_64(SCALE((epoch_randomness, slot))) mod authorities_number; ``` TODO: what about using `epoch_randomness_accumulator` instead of `epoch_randomness`? From 94304d31c617b7f1236ffa1fe5b4c9138c7dc851 Mon Sep 17 00:00:00 2001 From: Davide Galassi Date: Fri, 10 Nov 2023 10:02:21 +0100 Subject: [PATCH 09/26] Genesis params --- text/0026-sassafras-consensus.md | 81 ++++++++++++++++++++++++-------- 1 file changed, 61 insertions(+), 20 deletions(-) diff --git a/text/0026-sassafras-consensus.md b/text/0026-sassafras-consensus.md index 4d4f9f2bc..f4c0b7ee2 100644 --- a/text/0026-sassafras-consensus.md +++ b/text/0026-sassafras-consensus.md @@ -23,7 +23,6 @@ the slot is not claimed at block production time. ## 1. Motivation - Sassafras Protocol has been extensively documented in a comprehensive [research paper](https://eprint.iacr.org/2023/031.pdf). However, this RFC serves the purpose of conveying essential implementation details that are crucial for @@ -94,13 +93,17 @@ subsequent conventions: `U` as `n/8` bytes. - The function `SCALE(x: T)` returns an `OCTET_STRING` representing the - [`SCALE`](https://github.com/paritytech/parity-scale-codec) encoding of the - variable `x` with type `T`. + [`SCALE`](https://github.com/paritytech/parity-scale-codec) encoding of + `x` with type `T`. - The function `U(x: OCTET_STRING)` returns a `U` interpreting `x` as the little-endian encoding of a `n` bits unsigned integer. -- The function `BLAKE2_` returns bytes of the standard *blake2b* hash. +- The function `BLAKE2(n: U32, x: OCTET_STRING)` returns `n` bytes of the + standard *blake2b* hash of `x`. + +- The function `CONCAT(x₀: OCTET_STRING, ..., xₖ)` returns the concatenation of + the `k` octet strings `xᵢ`. ### 3.3. Incremental Introduction of Types and Functions @@ -370,8 +373,8 @@ The descriptor of next epoch is the `NextEpochDescriptor`. Randomness ::= OCTET_STRING(SIZE(32)); NextEpochDescriptor ::= SEQUENCE { - authorities: SEQUENCE_OF AuthorityId, randomness: Randomness, + authorities: SEQUENCE_OF AuthorityId, configuration: ProtocolConfiguration OPTIONAL } ``` @@ -393,20 +396,40 @@ block author, which is stored in the digest data. #### 6.1.1. Epoch Randomness -Each epoch has an associated randomness value defined by the -`NextEpochDescriptor` `Randomness` element. +Each block ships with some entropy source in the form of bandersnatch VRF +output. Per block randomness gets accumulated in the protocol on-chain state +**after** block import. + +The exact procedure to accumulate per-block randomness is described in detail +later, in the [Randomness Accumulator] paragraph. + +Next epoch `randomness` is computed as: + +```rust + next_epoch_randomness = BLAKE2(32, CONCAT( + accumulator, + curr_epoch_randomness, + next_epoch_index + )); +``` + + +Why we need to add `curr_epoch_randomness` to the hash? -The first block of epoch `N` contains the randomness associated to the next epoch `N+1`. +IMO this doesn't add any extra entropy to the `next_epoch_randomness` as we +are using `accumulator` -Randomness for epoch `N+1` is computed using the VRF output values which are associated -to each block of epoch `N-1`. + curr_epoch_randomness = H(prev_accumulator ++ ... ++ curr_epoch_index) + next_epoch_randomness = H(curr_accumulator ++ curr_epoch_randomness ++ next_epoch_index) -Assuming block `Bi` is produced during epoch `N-1`. Then the randomness for epoch `N+1` -is incrementally generated as `RandomnessAccumulator = Blake2(Bi.VrfOut)'`. + With: + - curr_accumulator depending on prev_accumulator + - next_epoch_index depending on curr_epoch_index -The first block produced for epoch `N` will propose as epoch `N+1` randomness the value -of `RandomnessAccumulator` as found after the production of the last block associated -to epoch `N-1`. + So the only extra thing curr_epoch_randomness brings to the table is `...`. + But if we recursively unroll this we end up `...` being the genesis randomness + which is always 0. + #### 6.1.2. Protocol Configuration @@ -437,6 +460,23 @@ with the requirement that its origin is set to `Root`. If a valid proposal is submitted during epoch `N-1`, it will be embedded into the `NextEpochDescriptor` at the beginning of epoch `N`. +#### 6.1.3. Startup Parameters + +Some parameters for first epoch (index = 0) are configurable via genesis configuration. + +```rust + GenesisConfig ::= SEQUENCE { + authorities: SEQUENCE_OF AuthorityId, + configuration: ProtocolConfiguration OPTIONAL + } +``` + +Randomness for first epoch is set to all zeros. + +The first block produced (i.e. block #1) MUST ship with a `NextEpochDescriptor` +for next epoch. This is constructed re-using the same values used for the first +epoch. + ### 6.2. Creation and Submission of Candidate Tickets As a shorthand notation, in this section we will refer to one of the next epoch @@ -673,7 +713,8 @@ specific order. The index of the authority which has the privilege to claim a slot is calculated as follows: ```rust - index = BLAKE2_64(SCALE((epoch_randomness, slot))) mod authorities_number; + index_bytes = BLAKE2(8, SCALE( (epoch_randomness, slot) )); + index = U64(index_bytes) mod authorities_number; ``` TODO: what about using `epoch_randomness_accumulator` instead of `epoch_randomness`? @@ -866,9 +907,9 @@ the claim should match the one given by the rule outlined in section 6.4.1. ### 6.7. Randomness Accumulator -The first `VrfOutput` which ships with the `SlotClaim` `signature` is mandatory and -is always used to provide some randomness which gets accumulated in on-chain state -after block processing. +The first `VrfOutput` which ships with the block's `SlotClaim` `signature` +is mandatory and MUST be used as the entropy source for the randomness which +gets accumulated on-chain **after** block processing. Given `claim` the instance of `SlotClaim` within the block header, and `accumulator` the current value for current epoch randomness accumulator, @@ -888,7 +929,7 @@ the `accumulator` value is updated as follows: randomness = vrf_bytes(randomness_vrf_input, randomness_vrf_output, 32); - accumulator = BLAKE2_256(CONCATENATE(accumulator, randomness)); + accumulator = BLAKE2(32, CONCAT(accumulator, randomness)); ``` The updated `accumulator` value is stored on-chain. From c35f18b275aefb4361374dc1406169209d6029ee Mon Sep 17 00:00:00 2001 From: Davide Galassi Date: Fri, 10 Nov 2023 10:45:27 +0100 Subject: [PATCH 10/26] Some details about ask-transcripts --- text/0026-sassafras-consensus.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/text/0026-sassafras-consensus.md b/text/0026-sassafras-consensus.md index f4c0b7ee2..71d2024b1 100644 --- a/text/0026-sassafras-consensus.md +++ b/text/0026-sassafras-consensus.md @@ -244,20 +244,20 @@ This section defines the data that is to be signed using the VRF primitive: } ``` -- `transcript`: represents a `ark-transcript` object. +- `transcript`: an [`ark-transcript`](https://docs.rs/ark-transcript/latest/ark_transcript/) + object. In practice, this is a *special* hash of the data to sign which should not + influence the `VrfOutput`. - `vrf_input`: sequence of `VrfInputs` to be signed. To simplify the construction of a `VrfSignatureData` object, a helper function is provided: -```rust - TranscriptData ::= OCTET_STRING; - +```rust fn vrf_signature_data( transcript_label: OCTET_STRING, - transcript_data: SEQUENCE_OF TranscriptData, + transcript_data: SEQUENCE_OF OCTET_STRING, vrf_inputs: SEQUENCE_OF VrfInput ) -> VrfSignatureData { - transcript = TRANSCRIPT(transcript_label); + let mut transcript = Tanscript::new_labeled(transcript_label); for data in transcript_data { transcript.append(data); } From c2dd679bf9f17033bfc039c92de833c1cf95e737 Mon Sep 17 00:00:00 2001 From: Davide Galassi Date: Fri, 10 Nov 2023 12:56:48 +0100 Subject: [PATCH 11/26] Better specify what are the consequences of some threshold function parameters --- text/0026-sassafras-consensus.md | 35 +++++++++++++++++++++----------- 1 file changed, 23 insertions(+), 12 deletions(-) diff --git a/text/0026-sassafras-consensus.md b/text/0026-sassafras-consensus.md index 71d2024b1..6f52ef1b3 100644 --- a/text/0026-sassafras-consensus.md +++ b/text/0026-sassafras-consensus.md @@ -257,11 +257,11 @@ To simplify the construction of a `VrfSignatureData` object, a helper function i transcript_data: SEQUENCE_OF OCTET_STRING, vrf_inputs: SEQUENCE_OF VrfInput ) -> VrfSignatureData { - let mut transcript = Tanscript::new_labeled(transcript_label); + let mut transcript = Transcript::new_labeled(transcript_label); for data in transcript_data { transcript.append(data); } - return VrfSignatureData { transcript, vrf_inputs } + VrfSignatureData { transcript, vrf_inputs } } ``` @@ -434,7 +434,7 @@ are using `accumulator` #### 6.1.2. Protocol Configuration The `ProtocolConfiguration` primarily influences certain checks carried out -during ticket validation. It is defined as follows: +during tickets validation. It is defined as follows: ```rust ProtocolConfiguration ::= SEQUENCE { @@ -443,17 +443,28 @@ during ticket validation. It is defined as follows: } ``` -- `attempts_number`: number of tickets that can be submitted by each next-epoch authority. -- `redundancy_factor`: factor that enhances the likelihood of a candidate ticket - being deemed valid according to some protocol specific checks. +- `attempts_number`: max number of tickets that can be submitted by each + next-epoch authority. +- `redundancy_factor`: controls the expected number of extra tickets produced + beyond `epoch_length`. -Further details regarding this configuration can be found in the section -dedicated to candidate ticket validation (ticket-id threshold computation). +The max attempts number influences the anonymity of block producers. As all +published tickets have a public attempt number less than `attempts_number`, +if two tickets share an attempt number then they must be by different block +producers, which reduces anonymity late in the epoch. -`ProtocolConfiguration` values can be adjusted via a dedicated extrinsic which should have origin set to `Root`. -A valid configuration proposal submitted on epoch `K` will be propagated in -the `NextEpochDescriptor` at the begin of epoch `K+1` and will be effectively -enacted on epoch `K+2`. +We do not mind `max_attempts < epoch_length` though because this loss already +becomes small when `attempts_number = 64` or `128` and larger values requires +more computation. + +Details about how these parameters drives the ticket validity probability +can be found in the section dedicated to candidate ticket validation +([ticket threshold](622-tickets-threshold)). + +`ProtocolConfiguration` values can be adjusted via a dedicated extrinsic which +should have origin set to `Root`. A valid configuration proposal submitted on +epoch `K` will be propagated in the `NextEpochDescriptor` at the begin of epoch +`K+1` and will be effectively enacted on epoch `K+2`. A new `ProtocolConfiguration` can be submitted using a dedicated extrinsic, with the requirement that its origin is set to `Root`. If a valid proposal is From 90f432de943429c88cc3ee4028755fd5f0f14078 Mon Sep 17 00:00:00 2001 From: Davide Galassi Date: Fri, 10 Nov 2023 12:58:48 +0100 Subject: [PATCH 12/26] Fix link --- text/0026-sassafras-consensus.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/text/0026-sassafras-consensus.md b/text/0026-sassafras-consensus.md index 6f52ef1b3..a78f6368c 100644 --- a/text/0026-sassafras-consensus.md +++ b/text/0026-sassafras-consensus.md @@ -459,7 +459,7 @@ more computation. Details about how these parameters drives the ticket validity probability can be found in the section dedicated to candidate ticket validation -([ticket threshold](622-tickets-threshold)). +([ticket threshold](0026-sassafras-consensus.md#622-tickets-threshold)). `ProtocolConfiguration` values can be adjusted via a dedicated extrinsic which should have origin set to `Root`. A valid configuration proposal submitted on From 950ffbe3f8aabe26c187ac33a9c4c7c3380e2953 Mon Sep 17 00:00:00 2001 From: Davide Galassi Date: Sat, 11 Nov 2023 19:20:58 +0100 Subject: [PATCH 13/26] Content review --- text/0026-sassafras-consensus.md | 529 +++++++++++++++++-------------- 1 file changed, 284 insertions(+), 245 deletions(-) diff --git a/text/0026-sassafras-consensus.md b/text/0026-sassafras-consensus.md index a78f6368c..45dcc3afc 100644 --- a/text/0026-sassafras-consensus.md +++ b/text/0026-sassafras-consensus.md @@ -58,52 +58,55 @@ Developers contributing to the Polkadot relay-chain, which plays a pivotal role in facilitating the interoperability and functionality of the Sassafras Protocol within the broader Polkadot network. + ## 3. Notation and Convention This section outlines the notation and conventions used throughout the document to ensure clarity and consistency. -### 3.1. Data Structure Definitions +### 3.1. Data Structures Definitions and Encoding Data structures are primarily defined using [ASN.1](https://en.wikipedia.org/wiki/ASN.1), with a few exceptions: - Integer types are not explicitly defined in ASN.1 and in the context of - this document `U` should be interpreted as `n`-bit unsigned integers - (e.g. `U32`) + this document `U` should be interpreted as a `n`-bit unsigned integers -To ensure interoperability of serialized structures, it's important to consider -the order of fields in their definitions. +If no context-specific instructions are given, all types must be serialized +using [SCALE](https://github.com/paritytech/parity-scale-codec) codec. -In cases where no specific instructions are given, structures should be -serialized using [SCALE](). +To ensure interoperability of serialized structures, the order of the single +fields is required to match the structures definitions found in this document. ### 3.2. Pseudo-Code Through this document it is advantageous to make use of code snippets as part -of the comprehensive description. These code snippets shall adhere to the -subsequent conventions: +of the comprehensive description. These snippets shall adhere to the subsequent +conventions: - For simplicity, code snippets are presented in a *Rust-like* pseudo-code format. - The function `BYTES(x: T)` returns an `OCTET_STRING` representing the raw byte array representation of the object `x` with type `T`. - - if `T` is `VisibleString` (aka *ascii* string): it returns the sequence of - bytes of its *ascii* representation. + - if `T` is `VisibleString` (i.e. an *ascii* string): it returns the sequence + of octets of its *ascii* representation. - if `T` is `U`: it returns the little-endian encoding of the integer - `U` as `n/8` bytes. + `U` as `n/8` octets. + +- The function `U(x: OCTET_STRING)` returns a `U` interpreting `x` as + the little-endian encoding of a `n` bits unsigned integer. - The function `SCALE(x: T)` returns an `OCTET_STRING` representing the [`SCALE`](https://github.com/paritytech/parity-scale-codec) encoding of `x` with type `T`. -- The function `U(x: OCTET_STRING)` returns a `U` interpreting `x` as - the little-endian encoding of a `n` bits unsigned integer. - - The function `BLAKE2(n: U32, x: OCTET_STRING)` returns `n` bytes of the - standard *blake2b* hash of `x`. + standard *blake2b* hash of `x` as an `OCTET_STRING`. + +- The function `CONCAT(x₀: OCTET_STRING, ..., xₖ: OCTET_STRING)` returns the + concatenation of the inputs as an `OCTET_STRING`. -- The function `CONCAT(x₀: OCTET_STRING, ..., xₖ)` returns the concatenation of - the `k` octet strings `xᵢ`. +- The function `LENGTH(x: OCTET_STRING)` returns a `U32` representing the + number of octets in `x`. ### 3.3. Incremental Introduction of Types and Functions @@ -118,36 +121,37 @@ This incremental presentation enhances readability and comprehension. ## 4. Protocol Introduction -The Sassafras Protocol employs a binding mechanism between validators and slots -through the use of a **ticket**. +Timeline is partitioned in epochs, epochs are partitioned in slots. + +The Sassafras protocol employs a binding mechanism between validators and slots +through the use of a ticketing system. -The protocol is organized into five discrete and asynchronous phases: +The protocol can be divided into five discrete and asynchronous phases: ### 4.1. Submission of Candidate Tickets -Validators generate and submit their candidate tickets along with validity -proofs to the blockchain. The validity proof ensures that the tickets are -legitimate while maintaining the anonymity of the authorship. +Validators generate and submit their candidate tickets to the blockchain. Each +ticket comes with an anonymous validity proof. ### 4.2. Validation of Candidate Tickets -Submitted candidate tickets undergo a validation process to verify their -authenticity, integrity and compliance with other protocol-specific rules. +Each candidate tickets undergo a validation process for the associated validity +proof and compliance with other protocol-specific constraints. ### 4.3. Tickets and Slots Binding After collecting all candidate tickets, a deterministic method is employed to -associate some of these tickets with specific slots. +uniquely associate a subset of these tickets to the next epoch slots. ### 4.4. Claim of Ticket Ownership -Validators assert ownership of tickets during the block production phase. This +Validators prove ownership of tickets during the block production phase. This step establishes a secure binding between validators and their respective slots. ### 4.5. Validation of Ticket Ownership -During block verification, the claims of ticket ownership are rigorously -validated to uphold the protocol's integrity. +During block verification, the claims of ticket ownership are validated to +uphold the protocol's integrity. ## 5. Bandernatch VRFs Cryptographic Primitives @@ -160,16 +164,11 @@ exhaustive exploration of the mathematically intensive foundations of the cryptographic primitive. Instead, its primary purpose is to offer a concise and comprehensible interpretation of the primitive within the context of this RFC. -For a more detailed and mathematical understanding of Ring-VRFs, we recommend -referring to the Ring-VRF research [paper](https://eprint.iacr.org/2023/002.pdf). +For a more detailed understanding we recommend referring to the Ring-VRF +research [paper](https://eprint.iacr.org/2023/002.pdf) from W3F. ### 5.1. VRF Input -The VRF Input, denoted as `VrfInput`, is constructed using a domain and some -arbitrary data using the `vrf_input(domain: OCTET_STRING, buf: OCTET_STRING)` -function. This function is left opaque here and can be read in the actual -reference implementation. - The VRF Input, denoted as `VrfInput`, is constructed by combining a domain identifier with arbitrary data using the `vrf_input` function: @@ -177,10 +176,10 @@ with arbitrary data using the `vrf_input` function: fn vrf_input(domain: OCTET_STRING, buf: OCTET_STRING) -> VrfInput; ``` -While the specific implementation details of this function are intentionally -omitted here, you can find the complete implementation in the +The specific implementation details of this function are intentionally omitted +here, you can find a complete reference implementation in the [`bandersnatch_vrfs`](https://github.com/w3f/ring-vrf/blob/18614458ca4cb335c88d4e710c13906a76f51e43/bandersnatch_vrfs/src/lib.rs#L57) -reference implementation. +project. Helper function to construct a `VrfInput` from a sequence of `data` items: @@ -189,32 +188,41 @@ Helper function to construct a `VrfInput` from a sequence of `data` items: buf = OCTET_STRING(SIZE(0)); for item in data { buf.append(item); - buf.append(length(item) as U8); + buf.append(LENGTH(item) as U8); } return vrf_input(domain, buf); } ``` +Note that we cast the length of each item to a `U8`. In the context of the +protocol we never have to append strings longer than 255. The function is +internal and not designed to be generic. + + +Or we should provide a generic one in bandersnatch primitive wrapper to be +used in other contexts? + + ### 5.2. VRF Output -Given a `VrfInput` object, the corresponding `VrfOutput`, also referred to as -`VrfPreOutput`, is computed using a Bandersnatch secret key. +A `VrfOutput` in this context is computed in function of a `VrfInput` and a +`BandersnatchSecretKey`. A `VrfOutput` can be created in two ways: as a standalone object or as part of a VRF signature. In both scenarios, the resulting `VrfOutput` remains the same, but the primary difference lies in the inclusion of a signature in the latter, which serves to confirm its validity. -In practice, the `VrfOutput` functions as a *seed* to produce a variable number -of pseudo-random bytes. These bytes are considered 'verified' when `VrfOutput` is -accompanied by a signature. +In practice, the `VrfOutput` is a verifiable *seed* to produce a variable number +of pseudo-random bytes. These bytes are considered valid when `VrfOutput` is +accompanied by a valid signature. -When used as a standalone object, `VrfOutput` is primarily employed in situations -where the protocol necessitates to check the generated bytes according to some -protocol specific criteria before applying the signature. +When constructed as a standalone object, `VrfOutput` is primarily employed +in situations where the secret key owner needs to check if the generated +pseudo-random bytes fulfill some criteria before applying the signature. To facilitate the construction of `VrfOutput` from a secret key and `VrfInput`, -the following helper function is used: +the following helper function is provided: ```rust fn vrf_output(secret: BandernatchSecretKey, input: VrfInput) -> VrfOutput; @@ -224,30 +232,30 @@ Additionally, a helper function is provided for producing `len` bytes from `VrfInput` and `VrfOutput`: ```rust - fn vrf_bytes(vrf_input: VrfInput, vrf_output: VrfOuput, len: U32) -> OCTET_STRING; + fn vrf_bytes(len: U32, input: VrfInput, output: VrfOuput) -> OCTET_STRING; ``` Just like the `VrfInput` support function, we have intentionally excluded the -detailed implementation of this function in this document. However, you can -see the complete implementation in the `dleq_vrfs` reference implementation: +detailed implementation of this function in this document. A reference implementation +is provided in the `dleq_vrfs` library: - [`vrf_output`](https://github.com/w3f/ring-vrf/blob/18614458ca4cb335c88d4e710c13906a76f51e43/dleq_vrf/src/traits.rs#L75-L77) - [`vrf_bytes`](https://github.com/w3f/ring-vrf/blob/18614458ca4cb335c88d4e710c13906a76f51e43/dleq_vrf/src/vrf.rs#L211-L214) ### 5.3. VRF Signature Data -This section defines the data that is to be signed using the VRF primitive: +This section defines the data to be signed using the VRF primitive: ```rust VrfSignatureData ::= SEQUENCE { transcript: Transcript, - vrf_input: SEQUENCE_OF VrfInput + inputs: SEQUENCE_OF VrfInput } ``` - `transcript`: an [`ark-transcript`](https://docs.rs/ark-transcript/latest/ark_transcript/) - object. In practice, this is a *special* hash of the data to sign which should not - influence the `VrfOutput`. -- `vrf_input`: sequence of `VrfInputs` to be signed. + object. In practice, this is a *special* hash of some protocol-specific data + to sign which should not influence the `VrfOutput`. +- `inputs`: sequence of `VrfInputs` to be signed. To simplify the construction of a `VrfSignatureData` object, a helper function is provided: @@ -255,22 +263,22 @@ To simplify the construction of a `VrfSignatureData` object, a helper function i fn vrf_signature_data( transcript_label: OCTET_STRING, transcript_data: SEQUENCE_OF OCTET_STRING, - vrf_inputs: SEQUENCE_OF VrfInput + inputs: SEQUENCE_OF VrfInput ) -> VrfSignatureData { let mut transcript = Transcript::new_labeled(transcript_label); for data in transcript_data { transcript.append(data); } - VrfSignatureData { transcript, vrf_inputs } + VrfSignatureData { transcript, inputs } } ``` ### 5.4. VRF Signature -Bandersnatch VRF offers two signature options: plain signature, which in -practice is like a *Schnorr* signature, or Ring signature. The Ring signature -option allows for anonymous signatures using a key from a predefined set of -enabled keys, known as the ring. +Bandersnatch VRF offers two signature flavors: +- *plain* signature, which is much like a traditional *Schnorr* signature, +- *ring* signature which leverages a *zk-SNARK* to allows for anonymous signatures + using a key from a predefined set of enabled keys, known as the ring. #### 5.4.1. Plain VRF Signature @@ -286,86 +294,91 @@ plain Bandersnatch signature flavor. } ``` -- `signature`: represents the actual signature (opaque). +- `signature`: the actual signature. - `outputs`: a sequence of `VrfOutput`s corresponding to the `VrfInput`s values. -Helper function to create a `VrfSignature` from `VrfSignatureData`: +Helper function to create a `VrfPlainSignature` from `VrfSignatureData`: ```rust - fn plain_vrf_sign( + BandersnatchSecretKey ::= OCTET_STRING; + + fn vrf_sign( secret: BandernatchSecretKey, signature_data: VrfSignatureData ) -> VrfSignature ``` -Helper function for validating the signature and returning a Boolean value -indicating the validity of the signature (`True` if it's valid): +Helper function for validating the signature and returning a `BOOLEAN` value +indicating the validity of the signature. ```rust - fn plain_vrf_verify( + BandersnatchPublicKey ::= OCTET_STRING; + + fn vrf_verify( public: BandersnatchPublicKey, signature: VrfSignature - ) -> Boolean; + ) -> BOOLEAN; ``` In this document, the types `BandersnatchSecretKey`, `BandersnatchPublicKey` -and `PlainSignature` are intentionally left undefined as are not relevant. Their -definitions can be found in the `bandersnatch_vrfs` reference implementation. +and `PlainSignature` are intentionally left undefined. Their definitions can be +found in the `bandersnatch_vrfs` reference implementation. #### 5.3.2. Ring VRF Signature This section deals with the signature process for `VrfSignatureData` using the -Bandersnatch Ring signature flavor. +Bandersnatch ring signature flavor. ```rust RingSignature ::= OCTET_STRING; RingVrfSignature ::= SEQUENCE { signature: RingSignature, - ring_proof: RingProof, outputs: SEQUENCE_OF VrfOutput } ``` -- `signature`: represents the actual signature (opaque). -- `ring_proof`: denotes the proof of ring membership. +- `signature`: the actual signature. - `outputs`: sequence of `VrfOutput` objects corresponding to the `VrfInput` values. Helper function to create a `RingVrfSignature` from `VrfSignatureData`: ```rust + BandersnatchRingProverKey ::= OCTET_STRING; + fn ring_vrf_sign( - secret: BandersnatchSecretKey, + secret: BandersnatchRingProverKey, signature_data: VrfSignatureData, - prover: RingProver ) -> RingVrfSignature; ``` -Helper function for validating the signature and returning a Boolean value -indicating the validity of the signature (`True` if it's valid). -It's important to note that this function does not require the signer's public key. +Helper function for validating the signature and returning a `BOOLEAN` +indicating the validity of the signature (`True` if it's valid). It's important +to note that this function does not require the signer's public key. ```rust + BandersnatchRingVerifierKey ::= OCTET_STRING; + fn ring_vrf_verify( + verifier: BandersnatchRingVerifierKey, signature: RingVrfSignature, - verifier: RingVerifier - ) -> Boolean; + ) -> BOOLEAN; ``` -In this document, the types `BandersnatchSecretKey`, `BandersnatchPublicKey`, -`RingSignature`, `RingProof`, `RingProver` and `RingVerifier` are intentionally -left undefined as are not relevant. Their definitions can be found in the -`bandersnatch_vrfs` reference implementation. +In this document, the types `BandersnatchRingProverKey`, +`BandersnatchRingVerifierKey`, and `RingSignature` are intentionally left +undefined. Their definitions can be found in the `bandersnatch_vrfs` reference +implementation. ## 6. Sassafras Protocol ### 6.1. Epoch's First Block -The first block produced for an epoch `N` is required to include the descriptor -for the next epoch `N+1`. +The first block produced for epoch `N` is required to include the descriptor for +the next epoch `N+1`. -The descriptor of next epoch is the `NextEpochDescriptor`. +The descriptor for next epoch is `NextEpochDescriptor`. ```rust AuthorityId ::= BandersnatchPublicKey; @@ -379,26 +392,26 @@ The descriptor of next epoch is the `NextEpochDescriptor`. } ``` -- `authorities`: list of authorities for the next epoch. -- `randomness`: randomness value associated to the next epoch. -- `configuration`: next epoch optional protocol configuration. +- `randomness`: randomness value. +- `authorities`: list of authorities. +- `configuration`: optional protocol configuration. The `NextEpochDescriptor` must be `SCALE` encoded and embedded in the block -header digest data. +header digest log. The identifier for the digest element is `BYTES("SASS")`. -**Security Consideration**: Instances of `NextEpochDescriptor` are generated through -on-chain code whenever a block is identified as the first of an epoch. -Consequently, every node executing the block has the capability to verify if -the descriptor generated during block execution matches the one produced by the -block author, which is stored in the digest data. +**Security Consideration**: Instances of `NextEpochDescriptor` are generated +through on-chain code whenever a block is identified as the first of an epoch. +Consequently, every node executing the block should verify that the descriptor +locally generated during block execution matches the one produced by the block +author, which is found in the digest data before block import. #### 6.1.1. Epoch Randomness -Each block ships with some entropy source in the form of bandersnatch VRF -output. Per block randomness gets accumulated in the protocol on-chain state -**after** block import. +Each block ships with some entropy source in the form of bandersnatch +`VrfOutput`. Per block randomness is accumulated in the protocol's on-chain +state **after** block import. The exact procedure to accumulate per-block randomness is described in detail later, in the [Randomness Accumulator] paragraph. @@ -406,18 +419,14 @@ later, in the [Randomness Accumulator] paragraph. Next epoch `randomness` is computed as: ```rust - next_epoch_randomness = BLAKE2(32, CONCAT( - accumulator, - curr_epoch_randomness, - next_epoch_index - )); + BLAKE2(32, CONCAT(accumulator, curr_epoch_randomness, next_epoch_index)); ``` - -Why we need to add `curr_epoch_randomness` to the hash? + +Do we really need need to add `curr_epoch_randomness` to the hash? -IMO this doesn't add any extra entropy to the `next_epoch_randomness` as we -are using `accumulator` +Looks like it doesn't add any extra entropy to the `next_epoch_randomness` as we +are already using `accumulator` curr_epoch_randomness = H(prev_accumulator ++ ... ++ curr_epoch_index) next_epoch_randomness = H(curr_accumulator ++ curr_epoch_randomness ++ next_epoch_index) @@ -429,7 +438,7 @@ are using `accumulator` So the only extra thing curr_epoch_randomness brings to the table is `...`. But if we recursively unroll this we end up `...` being the genesis randomness which is always 0. - + #### 6.1.2. Protocol Configuration @@ -444,32 +453,27 @@ during tickets validation. It is defined as follows: ``` - `attempts_number`: max number of tickets that can be submitted by each - next-epoch authority. + next epoch authority. - `redundancy_factor`: controls the expected number of extra tickets produced beyond `epoch_length`. -The max attempts number influences the anonymity of block producers. As all -published tickets have a public attempt number less than `attempts_number`, -if two tickets share an attempt number then they must be by different block -producers, which reduces anonymity late in the epoch. +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 in the epoch. -We do not mind `max_attempts < epoch_length` though because this loss already -becomes small when `attempts_number = 64` or `128` and larger values requires -more computation. +We do not mind `max_attempts < epoch_length` though because this loss of +anonymity already becomes small when `attempts_number = 64` or `128` and larger +values requires more computation. -Details about how these parameters drives the ticket validity probability -can be found in the section dedicated to candidate ticket validation +Details about how exactly these parameters drives the ticket validity +probability can be found in the section dedicated to candidate ticket validation ([ticket threshold](0026-sassafras-consensus.md#622-tickets-threshold)). `ProtocolConfiguration` values can be adjusted via a dedicated extrinsic which should have origin set to `Root`. A valid configuration proposal submitted on -epoch `K` will be propagated in the `NextEpochDescriptor` at the begin of epoch -`K+1` and will be effectively enacted on epoch `K+2`. - -A new `ProtocolConfiguration` can be submitted using a dedicated extrinsic, -with the requirement that its origin is set to `Root`. If a valid proposal is -submitted during epoch `N-1`, it will be embedded into the `NextEpochDescriptor` -at the beginning of epoch `N`. +epoch `K` will be propagated in the `NextEpochDescriptor` at the beginning of +epoch `K+1` and will be effectively enacted on epoch `K+2`. #### 6.1.3. Startup Parameters @@ -484,22 +488,28 @@ Some parameters for first epoch (index = 0) are configurable via genesis configu Randomness for first epoch is set to all zeros. -The first block produced (i.e. block #1) MUST ship with a `NextEpochDescriptor` -for next epoch. This is constructed re-using the same values used for the first -epoch. +As block #0 is locally produced by every node by processing the genesis configuration, +the first block explicitly produced by a validator for the first epoch is block #1. + +Block #1 must embed the `NextEpochDescriptor` for next epoch. This is +constructed re-using the same values used for the first epoch. ### 6.2. Creation and Submission of Candidate Tickets -As a shorthand notation, in this section we will refer to one of the next epoch -validators simply as 'the validator'. +As a shorthand notation, in this section we refer to one of the next epoch +validators as 'the validator'. -Upon the beginning of a new epoch `N`, the validator will create a set of +Upon the beginning of a new epoch `N`, the validator will construct a set of 'tickets' to be submitted on-chain. These tickets aim to secure ownership of one -of the slots in the upcoming epoch `N+1`. +or more slots in the upcoming epoch `N+1`. Each validator is allowed to submit a maximum number of tickets whose value is found in the next epoch `ProtocolConfiguration` `attempts_number` field. +The expected ratio between the attempts and the number of tickets which are +assigned to the next epoch slots is driven by the +[ticket threshold](0026-sassafras-consensus.md#622-tickets-threshold). + Each ticket has an associated unique identifier, denoted as `TicketId`. ```rust @@ -508,12 +518,12 @@ Each ticket has an associated unique identifier, denoted as `TicketId`. #### 6.2.1. Ticket Identifier Value -The value of the `TicketId` is determined through the output of the Bandersnatch -VRF, using the following inputs: +The value of the `TicketId` is determined by the output of the Bandersnatch VRF +when using the following inputs: -- Epoch `N+1` randomness: a `Randomness` obtained from the `NextEpochDescriptor`. -- Epoch `N+1` index: a `U64` value tracked by the Sassafras on-chain code. -- Attempt index: a `U32` value from `0` to `attempts_number`. +- Next epoch randomness: `Randomness` obtained from the `NextEpochDescriptor`. +- Next epoch index: `U64` computed as epoch start slot divided epoch duration. +- Attempt index: `U32` value going from `0` to `attempts_number`. Let `next_epoch` be an object with the information associated to the next epoch. @@ -529,21 +539,25 @@ Let `next_epoch` be an object with the information associated to the next epoch. ticket_id_vrf_output = vrf_output(AUTHORITY_SECRET_KEY, ticket_id_vrf_input); - ticket_bytes = vrf_bytes(ticket_id_vrf_input, ticket_id_vrf_output, 16); + ticket_bytes = vrf_bytes(16, ticket_id_vrf_input, ticket_id_vrf_output); ticket_id = U128(ticket_bytes); ``` #### 6.2.2. Tickets Threshold -A `TicketId` value is considered valid if it is less than the ticket threshold. +A `TicketId` value is valid if its value is less than the ticket threshold. + + T = (r·s)/(a·v) -Parameters: +Where: - `v`: the number of authorities (aka validators) in the epoch - `s`: number of slots in the epoch - `r`: the redundancy factor - `a`: number of attempts - `T`: ticket threshold value (`0 ≤ T ≤ 1`) +##### 6.2.2.1 Formula Derivation + For an epoch of `s` slots we want to have a number of tickets in expectation for block production equal to the `r·s`. @@ -577,7 +591,7 @@ paragraph of the *layman description* of Sassafras protocol. #### 6.2.3. Ticket Body -For every candidate ticket an associated ticket-body is constructed. +Every candidate ticket identifier has an associated body. ```rust TicketBody ::= SEQUENCE { @@ -587,7 +601,7 @@ For every candidate ticket an associated ticket-body is constructed. } ``` -- `attempt_index`: attempt index used to generate the associated ticket_id. +- `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. - `revealed_pub`: Ed25519 ephemeral public key which gets exposed as soon as the @@ -597,10 +611,10 @@ 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 bytes produced by the VRF with input -parameters equal to those employed in ticket-id generation, only the label is -different. +parameters equal to those employed in `TicketId` generation, only the label +is different. -Let `next_epoch` be an object with the information associated to the next epoch. +Let `next_epoch` be an object with the information associated to the next epoch: ```rust revealed_vrf_input = vrf_input_from_items( @@ -612,17 +626,17 @@ Let `next_epoch` be an object with the information associated to the next epoch. ] ); - revealed_vrf_output = vrf_output(AUTHORITY_SECRET_KEY, ticket_id_vrf_input); + revealed_vrf_output = vrf_output(AUTHORITY_SECRET_KEY, revealed_vrf_input); - revealed_seed = vrf_bytes(revealed_vrf_input, revealed_vrf_output, 32); + revealed_seed = vrf_bytes(32, revealed_vrf_input, revealed_vrf_output); revealed_pub = ed25519_secret_from_seed(revealed_seed).public(); ``` -The usage of the `EphemeralPublicKey`s will be clarified in the ticket claiming section. +The usage of the ephemeral public keys will be clarified in the [ticket claiming](0026-sassafras-consensus.md#65-claim-of-ticket-ownership-during-block-production) section. #### 6.2.4. Ring Signature Production -`TicketBody` must be signed using the Bandersnatch Ring-VRF flavor. +`TicketBody` must be signed using the Bandersnatch [ring VRF](532-ring-vrf-signature) flavor. ```rust sign_data = vrf_signature_data( @@ -630,17 +644,19 @@ The usage of the `EphemeralPublicKey`s will be clarified in the ticket claiming transcript_data: [ SCALE(ticket_body) ], - vrf_inputs: [ + inputs: [ ticket_id_vrf_input ] ) - ring_signature = ring_vrf_sign(AUTHORITY_SECRET_KEY, RING_PROVER, sign_data) + ring_signature = ring_vrf_sign(RING_PROVER_KEY, sign_data) ``` -`RING_PROVER` object is constructed using the set of public keys which belong to -the next epoch authorities and the *ZK-SNARK* initialization parameters -(more details in the [bandersnatch_vrfs](https://github.com/w3f/ring-vrf/blob/18614458ca4cb335c88d4e710c13906a76f51e43/bandersnatch_vrfs/src/ring.rs#L91-L93) reference implementation). +`RING_PROVER` object is constructed using the authority secret key, the set +public keys which belong to the next epoch authorities and the *zk-SNARK* +context parameters (more details in the +[bandersnatch_vrfs](https://github.com/w3f/ring-vrf/blob/18614458ca4cb335c88d4e710c13906a76f51e43/bandersnatch_vrfs/src/ring.rs#L91-L93) +reference implementation). The body and the ring signature are combined in the `TicketEnvelope`: @@ -659,39 +675,38 @@ via a dedicated unsigned extrinsic. All the actions in the steps described by this paragraph are executed by on-chain code. -The tickets are received via a dedicated unsigned extrinsic call. +The tickets are received via a dedicated extrinsic call. Generic validation rules: - Tickets submissions must occur within the first half of the epoch. (TODO: I expect this is to give time to the chain finality consensus to finalize - the on-chain tickets before next epoch starts) -- The transaction must be submitted by one of the current session validators. + the on-chain tickets before next epoch starts) +- For unsigned extrinsics, it must be submitted by one of the current session + validators. Ticket specific validation rules: -- Ring signature is verified using the on-chain `RingVerifier`. -- Ticket identifier is computed from the (verified) `VrfOutput` contained in the - ring signature and its value is checked to be less than the ticket-threshold. +- Ring signature is verified using the on-chain `BandersnatchRingVerifierKey`. +- Ticket identifier is locally computed from the `VrfOutput` contained in the + `RingVrfSignature` and its value is checked to be less than the ticket-threshold. Valid tickets bodies are persisted on-chain. ### 6.4. Ticket-Slot assignment -Before the beginning of the next epoch, i.e. the epoch in which the tickets are -supposed to be claimed, the on-chain list of tickets must be associated with the -next epoch's slots. +Before the beginning of the next epoch, the on-chain list of tickets must be +associated with the next epoch's slots. -The assignment process can happen any time in the second half of the submission -epoch, before the beginning of the next epoch. +The assignment process happens in the second half of the submission epoch. -There must be at most one ticket per slot. +In the end, there must be at most one ticket per slot. - Initially, the complete list of tickets is sorted based on their ticket-id, with smaller values coming first. - In cases where there are more tickets than available slots, the list is pruned by removing the larger value. -- Tickets are then assigned to the slots using an outside-in assignment strategy. +- Tickets are then assigned to the slots using an *outside-in* assignment strategy. -**Outside-In Assignment** +#### 6.4.1. Outside-In Assignment 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 @@ -702,42 +717,44 @@ strategy: tickets : [ t1, t3, t5, ... , t4, t2, t0 ] ``` -Here `slot-index` is a relative value computed as `epoch_start_slot - epoch_slot`. +Here `slot-index` is a relative value computed as: -The association between each ticket and its corresponding slot is recorded on -the blockchain and is publicly visible to all. What remains confidential is the -identity of the ticket *owner*, and consequently, who possesses the authority to -claim the corresponding slot. This information is known only to the author of -the ticket. + slot-index = absolute_slot_index - epoch_start_slot -#### 6.4.1. Fallback Assignment +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 *owner*, and +consequently, who possesses the authority to claim the corresponding slot. This +information is known only to the author of the ticket. -In cases where 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. +#### 6.4.2. Fallback Assignment -In such situations, these unassigned slots are allocated using a fallback -assignment method. +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. -The on-chain authority set contains the authorities for the current epoch in a -specific order. The index of the authority which has the privilege to claim a -slot is calculated as follows: +In such situation, these unassigned slots are allocated using a fallback +assignment strategy. + +The authorities registered on-chain are kept in a sorted buffer. The index of +the authority which has the privilege to claim an unbounded slot is calculated +as follows: ```rust - index_bytes = BLAKE2(8, SCALE( (epoch_randomness, slot) )); - index = U64(index_bytes) mod authorities_number; + index_bytes = BLAKE2(4, SCALE( (epoch_randomness, slot) )); + index = U32(index_bytes) mod authorities_number; ``` -TODO: what about using `epoch_randomness_accumulator` instead of `epoch_randomness`? + +What about using `epoch_randomness_accumulator` instead of `epoch_randomness`? The accumulator is updated using the randomness which ships with every block, thus we know who is the author of block N only after block N-1 has been imported. -Is a bit more resistant to DoS. But given the sporadic nature of secondary method -maybe this is not a bit deal anyway. +Is a bit more Dos resistant. + ### 6.5. Claim of ticket ownership during block production With tickets bound to epoch slots, every validator acquires information about -the slots for which they are eligible to produce a block. +the slots for which they are supposed to produce a block. The procedure for block authoring varies based on whether a given slot has an associated ticket according to the on-chain state. @@ -752,7 +769,7 @@ Let `ticket_body` represent 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 absolute monotonic slot number. -Follows the construction of the `VrfSignatureData`: +Follows the construction of `VrfSignatureData`: ```rust randomness_vrf_input = vrf_input_from_items( @@ -778,26 +795,30 @@ Follows the construction of the `VrfSignatureData`: transcript_data: [ SCALE(ticket_body) ], - vrf_inputs: [ + inputs: [ randomness_vrf_input, revealed_vrf_input ] ); ``` -The inclusion of `revealed_vrf_input` allows the verifier to reconstruct the -`revealed_pub` key which has been committed into the `TicketBody`. +The inclusion of `revealed_vrf_input` will generate a `VrfSignature` with a +`VrfOutput` allowing the verifier to reconstruct a `revealed_pub` key +which is expected to be equal to the one committed into the `TicketBody`. ##### 6.5.1.1. (Optional) Ed25519 Erased Ephemeral Key Claim -As the ticket ownership was already checked using the primary method, this -step is purely optional and serves only to enforce the claim. +As the ticket ownership can be claimed by reconstructing the `revealed_pub` +entry of the ticket, this step is purely optional and serves only to enforce +the claim. -TODO: is this step really necessary? + +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. + -The *Fiat-Shamir* transform is utilized to obtain a 32-byte challenge associated +The *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 @@ -811,7 +832,7 @@ committed in the `TicketBody`, to sign this challenge. #### 6.5.2. Secondary Claim Method If the slot doesn't have any associated ticket then the validator is the one -with index equal to the rule exposed in paragraph `6.4.1`. +with index equal to the rule exposed in paragraph [6.4.2](642-fallback-assignment). Given `randomness_vrf_input` constructed as shown for the primary method, the `VrfSignatureData` is constructed as: @@ -820,7 +841,7 @@ Given `randomness_vrf_input` constructed as shown for the primary method, the sign_data = vrf_signature_data( transcript_label: BYTES("sassafras-slot-claim-transcript-v1.0"), transcript_data: [ ], - vrf_inputs: [ + inputs: [ randomness_vrf_input ] ) @@ -842,19 +863,19 @@ which contains all the necessary information to assert ownership of the slot. - `authority_index`: index of the block author in the on-chain authorities list. -- `slot`: absolute slot number (this is not the relative index within the epoch) +- `slot`: absolute slot number (not relative with respect to the epoch start) -- `signature`: This is a signature that includes one or two `VrfOutputs`. +- `signature`: signature that includes one or two `VrfOutputs`. - The first `VrfOutput` is always present and is used to generate per-block - randomness. This is not relevant to claim ticket ownership. + randomness. This is used to claim ticket ownership. - The second `VrfOutput` is included if the slot is associated with a ticket. - This is relevant to claim ticket ownership using primary method. + This is relevant to claim ticket ownership. - `erased_signature`: optional signature providing an additional proof of ticket ownership (see 6.5.1.1). ```rust - signature = plain_vrf_sign(AUTHORITY_SECRET_KEY, sign_data); + signature = vrf_sign(AUTHORITY_SECRET_KEY, sign_data); claim = SlotClaim { authority_index, @@ -864,43 +885,56 @@ which contains all the necessary information to assert ownership of the slot. } ``` -The `claim` object is *SCALE* encoded and sent as a block's header digest log -item. +The `claim` object is *SCALE* encoded and sent in the block's header digest log. ### 6.6. Validation of the claim during block verification -Validation of `SlotClaim` object as found in the block's header. +Validation of `SlotClaim` object found in the block's header. The procedure depends on whether the slot has an associated ticket or not according to the on-chain state. -If there is a ticket linked to the slot, we will utilize the primary -verification method; otherwise, the protocol resorts to the secondary one. +If there is a ticket linked to the slot, the primary verification method will be +used; otherwise, the protocol resorts to the secondary one. -In both scenarios, the signature within the `SlotClaim` is verified using a -`VrfSignData` that matches the one according to paragraph 6.5. If signature -verification fails then the claim is not legit. +In both scenarios, the signature within the `SlotClaim` is verified using +a `VrfSignData` constructed as specified by paragraph 6.5. -Given `claim` the instance of `SlotClaim` within the block header. +Given `claim` an instance of `SlotClaim`: ```rust - plain_vrf_verify(AUTHORITY_PUBLIC_KEY, sign_data, claim.signature); + public_key = AUTHORITIES[claim.authority_index]; + + vrf_verify(public_key, sign_data, claim.signature); ``` +If signature verification fails then the claim is not legit. + ### 6.6.1. Primary Claim Method Verification This verification is performed to confirm ticket ownership and is performed utilizing the second `VrfOutput` contained within the `SlotClaim` `signature`. -By using the `VrfOutput` object together with the corresponding expected -`VrfInput` the verifier should be able to reconstruct the `revealed_pub` key -committed in the `TicketBody`. If there is a mismatch, the claim is not legit. +By using the `VrfOutput` object together with the associated expected `VrfInput` +the verifier should be able to reconstruct the `revealed_pub` key committed in +the `TicketBody`. If there is a mismatch, the claim is not legit. ```rust - reveled_vrf_output = claim.signature.vrf_outputs[1]; + revealed_vrf_input = vrf_input_from_items( + domain: BYTES("sassafras-revealed-v1.0"), + data: [ + curr_epoch.randomness, + BYTES(curr_epoch.epoch_index), + BYTES(ticket_body.attempt_index) + ] + ); - revealed_seed = vrf_bytes(revealed_vrf_input, revealed_vrf_output, 32); + reveled_vrf_output = claim.signature.outputs[1]; + + revealed_seed = vrf_bytes(32, revealed_vrf_input, revealed_vrf_output); revealed_pub = ed25519_secret_from_seed(revealed_seed).public(); + + assert(revealed_pub == ticket_body.revealed_pub); ``` ##### 6.6.1.1. (Optional) Ephemeral Key Signature Check @@ -908,22 +942,29 @@ committed in the `TicketBody`. If there is a mismatch, the claim is not legit. If the `erased_signature` element within the `SlotClaim` is present the `erased_pub` key is used to verify it. -The signed challenge is generated through the identical steps as outlined in -section 6.5.1.1. +The signed challenge is generated with identical steps as outlined in section +6.5.1.1. + +```rust + challenge = sign_data.transcript.challenge(); + result = ed25519_verify(ticket_body.erased_pub, challenge, claim.erased_signature); + + assert(result == true); +``` #### 6.6.2. Secondary Claim Method Verification -If the slot doesn't have any associated ticket then the validator index which signed -the claim should match the one given by the rule outlined in section 6.4.1. +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.4.2](642-fallback-assignment). ### 6.7. Randomness Accumulator The first `VrfOutput` which ships with the block's `SlotClaim` `signature` -is mandatory and MUST be used as the entropy source for the randomness which +is mandatory and must be used as the entropy source for the randomness which gets accumulated on-chain **after** block processing. Given `claim` the instance of `SlotClaim` within the block header, and -`accumulator` the current value for current epoch randomness accumulator, +`accumulator` the current value for the current epoch randomness accumulator, the `accumulator` value is updated as follows: ```rust @@ -936,9 +977,9 @@ the `accumulator` value is updated as follows: ] ); - randomness_vrf_output = claim.signature.vrf_outputs[0]; + randomness_vrf_output = claim.signature.outputs[0]; - randomness = vrf_bytes(randomness_vrf_input, randomness_vrf_output, 32); + randomness = vrf_bytes(32, randomness_vrf_input, randomness_vrf_output); accumulator = BLAKE2(32, CONCAT(accumulator, randomness)); ``` @@ -946,13 +987,12 @@ the `accumulator` value is updated as follows: The updated `accumulator` value is stored on-chain. The randomess accumulated during epoch `N` will be used, at the start of the -next epoch (`N+1`), as the value to be stored within the `NextEpochDescriptor` -`randomness` element (see section 6.1) to be used as the epoch `N+2` randomness -value. +next epoch (`N+1`), as an input to compute the `NextEpochDescriptor` +`randomness` element (see section 6.1). As outlined throughout the document, epoch randomness value secures various protocol-specific functions, including ticket generation and assignment of -fallback slots (refer to section 6.4.1). Additionally, users may utilize this +fallback slots (refer to section 6.4.2). Additionally, users may utilize this value for other purposes as needed. @@ -962,12 +1002,9 @@ None ## 8. Testing, Security, and Privacy -TODO ? +The reference implementation for this RFC will be tested on testnets first. -Describe the impact of the proposal on these three high-importance areas -- how implementations can be tested for adherence, -- effects that the proposal has on security and -- privacy per-se, as well as any possible implementation pitfalls which should be clearly avoided. +An audit may be required to ensure the implementation does not introduce unwanted side effects ## 9. Performance, Ergonomics, and Compatibility @@ -982,14 +1019,16 @@ which fork to follow is not opinionated and there is only one choice. ### 9.2. Ergonomics -TODO ? - -If the proposal alters exposed interfaces to developers or end-users, which types of usage patterns have been optimized for? +No specific considerations. ### 9.3. Compatibility The adoption of Sassafras impacts native client code and thus can't be -introduced just via a runtime upgrade. +introduced via a simple runtime upgrade. + +A deployment strategy should be carefully engineered for live networks. + +This subject is left open for a dedicated RFC. ## 10. Prior Art and References From d6aff755c9faceb26c3516ccb72aef2b0dbef9b6 Mon Sep 17 00:00:00 2001 From: Davide Galassi Date: Tue, 21 Nov 2023 11:58:26 +0100 Subject: [PATCH 14/26] Notes about runtime interfaces --- text/0026-sassafras-consensus.md | 47 +++++++++++++++++++------------- 1 file changed, 28 insertions(+), 19 deletions(-) diff --git a/text/0026-sassafras-consensus.md b/text/0026-sassafras-consensus.md index 45dcc3afc..9523d3937 100644 --- a/text/0026-sassafras-consensus.md +++ b/text/0026-sassafras-consensus.md @@ -10,12 +10,10 @@ ## Abstract Sassafras is a novel consensus protocol designed to address the recurring -fork-related challenges encountered in other lottery-based protocols, such as -Babe. +fork-related challenges encountered in other lottery-based protocols. -Sassafras aims to establish a unique association between each epoch's slots -and the validators, ensuring that there will be one and only one validator -per slot. +Sassafras aims to establish a unique association between each epoch's slots and +the validators, ensuring that there is one and only one validator per slot. The protocol ensures the anonymity of the validator associated to a slot until the slot is not claimed at block production time. @@ -24,9 +22,10 @@ the slot is not claimed at block production time. ## 1. Motivation Sassafras Protocol has been extensively documented in a comprehensive -[research paper](https://eprint.iacr.org/2023/031.pdf). However, this RFC serves -the purpose of conveying essential implementation details that are crucial for -interoperability and clarifying aspects left open for implementation discretion. +[research paper](https://eprint.iacr.org/2023/031.pdf). This RFC serves the +purpose of conveying most of the essential implementation details that are +crucial for interoperability and clarifying aspects left open for implementation +discretion. ### 1.1. Relevance to Implementors @@ -46,17 +45,16 @@ paves the way for integrating Sassafras into the Polkadot network. ## 2. Stakeholders -### 2.1. Developers of Relay-Chains and Para-Chains +### 2.1. Developers of Blockchains -Developers responsible for creating relay-chains and para-chains within the -Polkadot ecosystem who intend to leverage the benefits offered by the Sassafras -Protocol. +Developers responsible for creating blockchains who intend to leverage the +benefits offered by the Sassafras Protocol. -### 2.2. Developers of Polkadot Relay-Chain +### 2.2. Contributors to the Polkadot Ecosystem -Developers contributing to the Polkadot relay-chain, which plays a pivotal role -in facilitating the interoperability and functionality of the Sassafras Protocol -within the broader Polkadot network. +Developers contributing to the Polkadot ecosystem, both relay-chain and para-chains. +The protocol will have a central role in the next generation Polkadot relay chain +block authoring system. ## 3. Notation and Convention @@ -1053,14 +1051,25 @@ the protocol's completeness and security. These topics include: -### 12.1. Deployment Strategies +### 12.1. Interactions with the Runtime + +- **Outbound Interface**. Interfaces exposed by the host which are required by the runtime. + These are commonly dubbed *Host Functions*. + +- **Unrecorded Inboud Interfaces**. Interfaces exposed by the runtime which are required by the host. + These are commonly dubbed *Runtime APIs*. + +- **Transactional Inboud Interfaces**. Interfaces exposed by the runtime which alter the state. + These are commonly dubbed *Extrinsics* and *Inherents*. + +### 12.2. Deployment Strategies - **Protocol Migration**. Exploring how this protocol can seamlessly replace an already operational instance of another protocol is essential. Future RFCs should delve into the deployment strategy, including considerations for a smooth transition process. -### 12.2. ZK-SNARK SRS Initialization Ceremony. +### 12.3. ZK-SNARK SRS Initialization Ceremony. - **Timing and Procedure**: Determining the timing and procedure for the ZK-SNARK SRS (Structured Reference String) initialization ceremony. Future RFCs should @@ -1071,7 +1080,7 @@ These topics include: must understand whether the SRS is shared with parachains or maintained independently. -### 12.3. Anonymous Submission of Tickets. +### 12.4. Anonymous Submission of Tickets. - **Mixnet Integration**: Submitting tickets directly can pose a risk of potential deanonymization through traffic analysis. Subsequent RFCs should From 51aa3631435404df5a4756afc48881c2c092de55 Mon Sep 17 00:00:00 2001 From: Davide Galassi Date: Wed, 22 Nov 2023 19:45:48 +0100 Subject: [PATCH 15/26] Simpler next epoch randomness computation --- text/0026-sassafras-consensus.md | 20 +------------------- 1 file changed, 1 insertion(+), 19 deletions(-) diff --git a/text/0026-sassafras-consensus.md b/text/0026-sassafras-consensus.md index 9523d3937..f3c4eef39 100644 --- a/text/0026-sassafras-consensus.md +++ b/text/0026-sassafras-consensus.md @@ -409,7 +409,7 @@ author, which is found in the digest data before block import. Each block ships with some entropy source in the form of bandersnatch `VrfOutput`. Per block randomness is accumulated in the protocol's on-chain -state **after** block import. +`accumulator` **after** block import. The exact procedure to accumulate per-block randomness is described in detail later, in the [Randomness Accumulator] paragraph. @@ -420,24 +420,6 @@ Next epoch `randomness` is computed as: BLAKE2(32, CONCAT(accumulator, curr_epoch_randomness, next_epoch_index)); ``` - -Do we really need need to add `curr_epoch_randomness` to the hash? - -Looks like it doesn't add any extra entropy to the `next_epoch_randomness` as we -are already using `accumulator` - - curr_epoch_randomness = H(prev_accumulator ++ ... ++ curr_epoch_index) - next_epoch_randomness = H(curr_accumulator ++ curr_epoch_randomness ++ next_epoch_index) - - With: - - curr_accumulator depending on prev_accumulator - - next_epoch_index depending on curr_epoch_index - - So the only extra thing curr_epoch_randomness brings to the table is `...`. - But if we recursively unroll this we end up `...` being the genesis randomness - which is always 0. - - #### 6.1.2. Protocol Configuration The `ProtocolConfiguration` primarily influences certain checks carried out From 10b33e5e115c0fb202c90b60d8ade59f8fca3762 Mon Sep 17 00:00:00 2001 From: Davide Galassi Date: Wed, 22 Nov 2023 19:46:34 +0100 Subject: [PATCH 16/26] Fix internal links --- text/0026-sassafras-consensus.md | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/text/0026-sassafras-consensus.md b/text/0026-sassafras-consensus.md index f3c4eef39..6547e90e8 100644 --- a/text/0026-sassafras-consensus.md +++ b/text/0026-sassafras-consensus.md @@ -322,7 +322,7 @@ In this document, the types `BandersnatchSecretKey`, `BandersnatchPublicKey` and `PlainSignature` are intentionally left undefined. Their definitions can be found in the `bandersnatch_vrfs` reference implementation. -#### 5.3.2. Ring VRF Signature +#### 5.4.2. Ring VRF Signature This section deals with the signature process for `VrfSignatureData` using the Bandersnatch ring signature flavor. @@ -412,12 +412,12 @@ Each block ships with some entropy source in the form of bandersnatch `accumulator` **after** block import. The exact procedure to accumulate per-block randomness is described in detail -later, in the [Randomness Accumulator] paragraph. +later, in the [randomness accumulator](#67-randomness-accumulator) paragraph. Next epoch `randomness` is computed as: ```rust - BLAKE2(32, CONCAT(accumulator, curr_epoch_randomness, next_epoch_index)); + next_epoch_randomness = BLAKE2(32, CONCAT(accumulator, next_epoch_index)); ``` #### 6.1.2. Protocol Configuration @@ -448,7 +448,7 @@ values requires more computation. Details about how exactly these parameters drives the ticket validity probability can be found in the section dedicated to candidate ticket validation -([ticket threshold](0026-sassafras-consensus.md#622-tickets-threshold)). +against [threshold](0026-sassafras-consensus.md#622-tickets-threshold). `ProtocolConfiguration` values can be adjusted via a dedicated extrinsic which should have origin set to `Root`. A valid configuration proposal submitted on @@ -566,8 +566,8 @@ tolerance over offline nodes and we end-up filling all the slots with tickets with high probability. For more details about threshold formula please refer to the -[Probabilities and parameters](https://research.web3.foundation/Polkadot/protocols/block-production/SASSAFRAS#probabilities-and-parameters) -paragraph of the *layman description* of Sassafras protocol. +[probabilities and parameters](https://research.web3.foundation/Polkadot/protocols/block-production/SASSAFRAS#probabilities-and-parameters) +paragraph of the w3f description of the protocol. #### 6.2.3. Ticket Body @@ -612,11 +612,11 @@ Let `next_epoch` be an object with the information associated to the next epoch: revealed_pub = ed25519_secret_from_seed(revealed_seed).public(); ``` -The usage of the ephemeral public keys will be clarified in the [ticket claiming](0026-sassafras-consensus.md#65-claim-of-ticket-ownership-during-block-production) section. +The usage of the ephemeral public keys will be clarified in the [ticket claiming](#65-claim-of-ticket-ownership-during-block-production) section. #### 6.2.4. Ring Signature Production -`TicketBody` must be signed using the Bandersnatch [ring VRF](532-ring-vrf-signature) flavor. +`TicketBody` must be signed using the Bandersnatch [ring VRF](#542-ring-vrf-signature) flavor. ```rust sign_data = vrf_signature_data( From 07bb84c224258017da91140ac169dc0a224d6e88 Mon Sep 17 00:00:00 2001 From: Davide Galassi Date: Wed, 22 Nov 2023 19:57:17 +0100 Subject: [PATCH 17/26] Try new refs style --- text/0026-sassafras-consensus.md | 24 +++++++++++++----------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/text/0026-sassafras-consensus.md b/text/0026-sassafras-consensus.md index 6547e90e8..9b9d85aad 100644 --- a/text/0026-sassafras-consensus.md +++ b/text/0026-sassafras-consensus.md @@ -21,11 +21,13 @@ the slot is not claimed at block production time. ## 1. Motivation -Sassafras Protocol has been extensively documented in a comprehensive -[research paper](https://eprint.iacr.org/2023/031.pdf). This RFC serves the -purpose of conveying most of the essential implementation details that are -crucial for interoperability and clarifying aspects left open for implementation -discretion. +Sassafras Protocol has been extensively documented in a comprehensive [research +paper](https://eprint.iacr.org/2023/031.pdf) from the [Web3 foundation](https://web3.foundation) +research team. + +This RFC serves the purpose of conveying most of the essential implementation +details that are crucial for interoperability and clarifying aspects left open +for implementation discretion. ### 1.1. Relevance to Implementors @@ -152,7 +154,7 @@ During block verification, the claims of ticket ownership are validated to uphold the protocol's integrity. -## 5. Bandernatch VRFs Cryptographic Primitives +## 5. Bandersnatch VRFs Cryptographic Primitives This chapter provides a high-level overview of the Bandersnatch VRF primitive as it relates to the Sassafras protocol. @@ -163,7 +165,7 @@ cryptographic primitive. Instead, its primary purpose is to offer a concise and comprehensible interpretation of the primitive within the context of this RFC. For a more detailed understanding we recommend referring to the Ring-VRF -research [paper](https://eprint.iacr.org/2023/002.pdf) from W3F. +[research paper](https://eprint.iacr.org/2023/002.pdf) from W3F. ### 5.1. VRF Input @@ -412,7 +414,7 @@ Each block ships with some entropy source in the form of bandersnatch `accumulator` **after** block import. The exact procedure to accumulate per-block randomness is described in detail -later, in the [randomness accumulator](#67-randomness-accumulator) paragraph. +later, in the randomness accumulator paragraph ([6.7](#67-randomness-accumulator)). Next epoch `randomness` is computed as: @@ -448,7 +450,7 @@ values requires more computation. Details about how exactly these parameters drives the ticket validity probability can be found in the section dedicated to candidate ticket validation -against [threshold](0026-sassafras-consensus.md#622-tickets-threshold). +([6.2.2](#622-tickets-threshold)). `ProtocolConfiguration` values can be adjusted via a dedicated extrinsic which should have origin set to `Root`. A valid configuration proposal submitted on @@ -487,8 +489,8 @@ Each validator is allowed to submit a maximum number of tickets whose value is found in the next epoch `ProtocolConfiguration` `attempts_number` field. The expected ratio between the attempts and the number of tickets which are -assigned to the next epoch slots is driven by the -[ticket threshold](0026-sassafras-consensus.md#622-tickets-threshold). +assigned to the next epoch slots is driven by the ticket threshold +([6.2.2](#622-tickets-threshold)). Each ticket has an associated unique identifier, denoted as `TicketId`. From 1d709ff35c99e0d677c57293b5f011fd01c2e7b3 Mon Sep 17 00:00:00 2001 From: Davide Galassi Date: Wed, 22 Nov 2023 20:04:01 +0100 Subject: [PATCH 18/26] Fix more internal references --- text/0026-sassafras-consensus.md | 29 ++++++++++++++++------------- 1 file changed, 16 insertions(+), 13 deletions(-) diff --git a/text/0026-sassafras-consensus.md b/text/0026-sassafras-consensus.md index 9b9d85aad..2473f5fe2 100644 --- a/text/0026-sassafras-consensus.md +++ b/text/0026-sassafras-consensus.md @@ -83,13 +83,13 @@ Through this document it is advantageous to make use of code snippets as part of the comprehensive description. These snippets shall adhere to the subsequent conventions: -- For simplicity, code snippets are presented in a *Rust-like* pseudo-code format. +- For simplicity, code snippets are presented in a *Rust-like* pseudocode format. - The function `BYTES(x: T)` returns an `OCTET_STRING` representing the raw byte array representation of the object `x` with type `T`. - - if `T` is `VisibleString` (i.e. an *ascii* string): it returns the sequence - of octets of its *ascii* representation. - - if `T` is `U`: it returns the little-endian encoding of the integer + - If `T` is `VisibleString` (i.e. an ASCII string): it returns the sequence + of octets of its ASCII representation. + - If `T` is `U`: it returns the little-endian encoding of the integer `U` as `n/8` octets. - The function `U(x: OCTET_STRING)` returns a `U` interpreting `x` as @@ -614,11 +614,12 @@ Let `next_epoch` be an object with the information associated to the next epoch: revealed_pub = ed25519_secret_from_seed(revealed_seed).public(); ``` -The usage of the ephemeral public keys will be clarified in the [ticket claiming](#65-claim-of-ticket-ownership-during-block-production) section. +The usage of the ephemeral public keys will be clarified in the ticket claiming +section ([6.5](#65-claim-of-ticket-ownership-during-block-production)). #### 6.2.4. Ring Signature Production -`TicketBody` must be signed using the Bandersnatch [ring VRF](#542-ring-vrf-signature) flavor. +`TicketBody` must be signed using the Bandersnatch ring VRF flavor ([5.4.2](#542-ring-vrf-signature)). ```rust sign_data = vrf_signature_data( @@ -814,7 +815,8 @@ committed in the `TicketBody`, to sign this challenge. #### 6.5.2. Secondary Claim Method If the slot doesn't have any associated ticket then the validator is the one -with index equal to the rule exposed in paragraph [6.4.2](642-fallback-assignment). +with index equal to the rule exposed in the fallback assignment section +([6.4.2](#642-fallback-assignment)). Given `randomness_vrf_input` constructed as shown for the primary method, the `VrfSignatureData` is constructed as: @@ -937,7 +939,8 @@ The signed challenge is generated with identical steps as outlined in section #### 6.6.2. Secondary Claim Method Verification 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.4.2](642-fallback-assignment). +the claim should match the one given by the rule outlined in the fallback assignment +section ([6.4.2](#642-fallback-assignment)) ### 6.7. Randomness Accumulator @@ -968,7 +971,7 @@ the `accumulator` value is updated as follows: The updated `accumulator` value is stored on-chain. -The randomess accumulated during epoch `N` will be used, at the start of the +The randomness accumulated during epoch `N` will be used, at the start of the next epoch (`N+1`), as an input to compute the `NextEpochDescriptor` `randomness` element (see section 6.1). @@ -1040,10 +1043,10 @@ These topics include: - **Outbound Interface**. Interfaces exposed by the host which are required by the runtime. These are commonly dubbed *Host Functions*. -- **Unrecorded Inboud Interfaces**. Interfaces exposed by the runtime which are required by the host. +- **Unrecorded Inbound Interfaces**. Interfaces exposed by the runtime which are required by the host. These are commonly dubbed *Runtime APIs*. -- **Transactional Inboud Interfaces**. Interfaces exposed by the runtime which alter the state. +- **Transactional Inbound Interfaces**. Interfaces exposed by the runtime which alter the state. These are commonly dubbed *Extrinsics* and *Inherents*. ### 12.2. Deployment Strategies @@ -1060,8 +1063,8 @@ These topics include: provide insights into whether this process should be performed before the deployment of Sassafras and the steps involved. -- **Sharing with Parachains**: Considering the complexity of the ceremony, we - must understand whether the SRS is shared with parachains or maintained +- **Sharing with Para-chains**: Considering the complexity of the ceremony, we + must understand whether the SRS is shared with para-chains or maintained independently. ### 12.4. Anonymous Submission of Tickets. From 962b7f50873cd1101fd71b90cfef48b31e2020e1 Mon Sep 17 00:00:00 2001 From: Davide Galassi Date: Fri, 24 Nov 2023 13:07:36 +0100 Subject: [PATCH 19/26] Remove proposal --- text/0026-sassafras-consensus.md | 24 ++++++++---------------- 1 file changed, 8 insertions(+), 16 deletions(-) diff --git a/text/0026-sassafras-consensus.md b/text/0026-sassafras-consensus.md index 2473f5fe2..36b1c4461 100644 --- a/text/0026-sassafras-consensus.md +++ b/text/0026-sassafras-consensus.md @@ -411,7 +411,7 @@ author, which is found in the digest data before block import. Each block ships with some entropy source in the form of bandersnatch `VrfOutput`. Per block randomness is accumulated in the protocol's on-chain -`accumulator` **after** block import. +randomness accumulator **after** block import. The exact procedure to accumulate per-block randomness is described in detail later, in the randomness accumulator paragraph ([6.7](#67-randomness-accumulator)). @@ -419,7 +419,7 @@ later, in the randomness accumulator paragraph ([6.7](#67-randomness-accumulator Next epoch `randomness` is computed as: ```rust - next_epoch_randomness = BLAKE2(32, CONCAT(accumulator, next_epoch_index)); + next_epoch_randomness = BLAKE2(32, CONCAT(randomness_accumulator, BYTES(next_epoch_index))); ``` #### 6.1.2. Protocol Configuration @@ -662,8 +662,6 @@ The tickets are received via a dedicated extrinsic call. Generic validation rules: - Tickets submissions must occur within the first half of the epoch. - (TODO: I expect this is to give time to the chain finality consensus to finalize - the on-chain tickets before next epoch starts) - For unsigned extrinsics, it must be submitted by one of the current session validators. @@ -718,22 +716,16 @@ ticket. In such situation, these unassigned slots are allocated using a fallback assignment strategy. -The authorities registered on-chain are kept in a sorted buffer. The index of -the authority which has the privilege to claim an unbounded slot is calculated -as follows: +The authorities registered on-chain are kept in a sorted buffer. + +The index of the authority which has the privilege to claim an unbounded slot is +calculated as follows: ```rust - index_bytes = BLAKE2(4, SCALE( (epoch_randomness, slot) )); + index_bytes = BLAKE2(4, CONCAT(epoch_randomness, BYTES(slot))); index = U32(index_bytes) mod authorities_number; ``` - -What about using `epoch_randomness_accumulator` instead of `epoch_randomness`? -The accumulator is updated using the randomness which ships with every block, thus -we know who is the author of block N only after block N-1 has been imported. -Is a bit more Dos resistant. - - ### 6.5. Claim of ticket ownership during block production With tickets bound to epoch slots, every validator acquires information about @@ -966,7 +958,7 @@ the `accumulator` value is updated as follows: randomness = vrf_bytes(32, randomness_vrf_input, randomness_vrf_output); - accumulator = BLAKE2(32, CONCAT(accumulator, randomness)); + randomness_accumulator = BLAKE2(32, CONCAT(randomness_accumulator, randomness)); ``` The updated `accumulator` value is stored on-chain. From 9a3e3ba41a5ee1f0e3b0375de0e34a71828cf1d2 Mon Sep 17 00:00:00 2001 From: Davide Galassi Date: Wed, 20 Dec 2023 18:27:15 +0100 Subject: [PATCH 20/26] Complete overhaul --- text/0026-sassafras-consensus.md | 865 +++++++++++++++---------------- 1 file changed, 429 insertions(+), 436 deletions(-) diff --git a/text/0026-sassafras-consensus.md b/text/0026-sassafras-consensus.md index 36b1c4461..bf68ba742 100644 --- a/text/0026-sassafras-consensus.md +++ b/text/0026-sassafras-consensus.md @@ -12,146 +12,148 @@ Sassafras is a novel consensus protocol designed to address the recurring fork-related challenges encountered in other lottery-based protocols. -Sassafras aims to establish a unique association between each epoch's slots and -the validators, ensuring that there is one and only one validator per slot. - -The protocol ensures the anonymity of the validator associated to a slot until -the slot is not claimed at block production time. - +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. ## 1. Motivation -Sassafras Protocol has been extensively documented in a comprehensive [research -paper](https://eprint.iacr.org/2023/031.pdf) from the [Web3 foundation](https://web3.foundation) -research team. +Sassafras Protocol has been rigorously detailed in a comprehensive +[research paper](https://eprint.iacr.org/2023/031.pdf) authored by the +[Web3 foundation](https://web3.foundation) research team. -This RFC serves the purpose of conveying most of the essential implementation -details that are crucial for interoperability and clarifying aspects left open -for implementation discretion. +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. ### 1.1. Relevance to Implementors This RFC focuses on providing implementors with the necessary insights into the protocol's operation. -To avoid ambiguities and interoperability issues, this document takes precedence -over the research paper in cases where discrepancies arise between the two. +In instances of inconsistency between this document and the research paper, +this RFC should be considered authoritative to eliminate ambiguities and ensure +interoperability. ### 1.2. Supporting Sassafras for Polkadot -In addition to fostering interoperability, another objective of this RFC is to -facilitate the implementation of Sassafras within the Polkadot ecosystem. While -the specifics of deployment mechanics are beyond the scope of this document, it -paves the way for integrating Sassafras into the Polkadot network. +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. ## 2. Stakeholders -### 2.1. Developers of Blockchains +### 2.1. Blockchain Developers Developers responsible for creating blockchains who intend to leverage the benefits offered by the Sassafras Protocol. -### 2.2. Contributors to the Polkadot Ecosystem +### 2.2. Polkadot Ecosystem Contributors + +Developers contributing to the Polkadot ecosystem, both relay-chain and +para-chains. -Developers contributing to the Polkadot ecosystem, both relay-chain and para-chains. -The protocol will have a central role in the next generation Polkadot relay chain -block authoring system. +The protocol will have a central role in the next generation block authoring +consensus systems. ## 3. Notation and Convention -This section outlines the notation and conventions used throughout the document -to ensure clarity and consistency. +This section outlines the notation and conventions adopted throughout this +document to ensure clarity and consistency. ### 3.1. Data Structures Definitions and Encoding -Data structures are primarily defined using [ASN.1](https://en.wikipedia.org/wiki/ASN.1), -with a few exceptions: -- Integer types are not explicitly defined in ASN.1 and in the context of - this document `U` should be interpreted as a `n`-bit unsigned integers +Data structures are primarily defined using standard [ASN.1](https://en.wikipedia.org/wiki/ASN.1), +syntax with few exceptions: +- Fixed width integer types are not explicitly defined by ASN.1 standard. + Within this document, `U` denotes a `n`-bit unsigned integer. -If no context-specific instructions are given, all types must be serialized -using [SCALE](https://github.com/paritytech/parity-scale-codec) codec. +Unless explicitly noted, all types must be serialized using +[SCALE](https://github.com/paritytech/parity-scale-codec) codec. -To ensure interoperability of serialized structures, the order of the single -fields is required to match the structures definitions found in this document. +To ensure interoperability of serialized structures, the order of the fields +must match the structures definitions found within this document. ### 3.2. Pseudo-Code -Through this document it is advantageous to make use of code snippets as part -of the comprehensive description. These snippets shall adhere to the subsequent -conventions: +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: -- For simplicity, code snippets are presented in a *Rust-like* pseudocode format. - -- The function `BYTES(x: T)` returns an `OCTET_STRING` representing the raw - byte array representation of the object `x` with type `T`. - - If `T` is `VisibleString` (i.e. an ASCII string): it returns the sequence +- `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`: it returns the little-endian encoding of the integer + - If `T` is `U`, it returns the little-endian encoding of the integer `U` as `n/8` octets. -- The function `U(x: OCTET_STRING)` returns a `U` interpreting `x` as - the little-endian encoding of a `n` bits unsigned integer. +- `U(x: OCTET_STRING)`: returns a `U` interpreting `x` as the + little-endian encoding of a `n` bits unsigned integer. -- The function `SCALE(x: T)` returns an `OCTET_STRING` representing the - [`SCALE`](https://github.com/paritytech/parity-scale-codec) encoding of +- `SCALE(x: T)`: returns an `OCTET_STRING` representing the SCALE encoding of `x` with type `T`. -- The function `BLAKE2(n: U32, x: OCTET_STRING)` returns `n` bytes of the - standard *blake2b* hash of `x` as an `OCTET_STRING`. +- `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). -- The function `CONCAT(x₀: OCTET_STRING, ..., xₖ: OCTET_STRING)` returns the - concatenation of the inputs as an `OCTET_STRING`. +- `CONCAT(x₀: OCTET_STRING, ..., xₖ: OCTET_STRING)`: returns the concatenation + of the inputs as an `OCTET_STRING`. -- The function `LENGTH(x: OCTET_STRING)` returns a `U32` representing the - number of octets in `x`. +- `LENGTH(x: OCTET_STRING)`: returns the number of octets in `x` as an `U32`. ### 3.3. Incremental Introduction of Types and Functions -Types and helper functions will be introduced incrementally as they become +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 extensive or overly complex. - -This incremental presentation enhances readability and comprehension. +not overly complex. ## 4. Protocol Introduction -Timeline is partitioned in epochs, epochs are partitioned in slots. +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 employs a binding mechanism between validators and slots -through the use of a ticketing system. +The Sassafras protocol aims to map each slot within an epoch to the designated +validators for that epoch, utilizing a ticketing system. -The protocol can be divided into five discrete and asynchronous phases: +The protocol operation can be roughly divided into five phases: ### 4.1. Submission of Candidate Tickets -Validators generate and submit their candidate tickets to the blockchain. Each -ticket comes with an anonymous validity proof. +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. ### 4.2. Validation of Candidate Tickets -Each candidate tickets undergo a validation process for the associated validity +Each candidate ticket undergoes a validation process for the associated validity proof and compliance with other protocol-specific constraints. ### 4.3. Tickets and Slots Binding -After collecting all candidate tickets, a deterministic method is employed to -uniquely associate a subset of these tickets to the next epoch slots. +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. ### 4.4. Claim of Ticket Ownership -Validators prove ownership of tickets during the block production phase. This -step establishes a secure binding between validators and their respective slots. +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. ### 4.5. Validation of Ticket Ownership -During block verification, the claims of ticket ownership are validated to -uphold the protocol's integrity. +During block verification, the claim of ticket ownership is validated. ## 5. Bandersnatch VRFs Cryptographic Primitives @@ -161,89 +163,88 @@ 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. Instead, its primary purpose is to offer a concise and -comprehensible interpretation of the primitive within the context of this RFC. +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 a more detailed understanding we recommend referring to the Ring-VRF -[research paper](https://eprint.iacr.org/2023/002.pdf) from W3F. +For an in-depth explanation, refer to the Ring-VRF +[paper](https://eprint.iacr.org/2023/002.pdf) authored by the Web3 foundation +research team. ### 5.1. VRF Input -The VRF Input, denoted as `VrfInput`, is constructed by combining a domain identifier -with arbitrary data using the `vrf_input` function: +The VRF Input, denoted as `VrfInput`, is constructed by combining a domain +identifier with arbitrary data through the `vrf_input` function: ```rust - fn vrf_input(domain: OCTET_STRING, buf: OCTET_STRING) -> VrfInput; + fn vrf_input(domain: OCTET_STRING, data: OCTET_STRING) -> VrfInput; ``` -The specific implementation details of this function are intentionally omitted -here, you can find a complete reference implementation in the -[`bandersnatch_vrfs`](https://github.com/w3f/ring-vrf/blob/18614458ca4cb335c88d4e710c13906a76f51e43/bandersnatch_vrfs/src/lib.rs#L57) +The specific implementation details of this function are intentionally omitted. +A reference implementation is provided by the +[`bandersnatch_vrfs`](https://github.com/w3f/ring-vrf/tree/master/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: ```rust - fn vrf_input_from_items(domain: OCTET_STRING, data: SEQUENCE_OF OCTET_STRING) -> VrfInput { - buf = OCTET_STRING(SIZE(0)); - for item in data { - buf.append(item); - buf.append(LENGTH(item) as U8); + 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, buf); + return vrf_input(domain, data); } ``` -Note that we cast the length of each item to a `U8`. In the context of the -protocol we never have to append strings longer than 255. The function is -internal and not designed to be generic. +Note that each item length is safely casted to an `U8` as: +1. In the context of this protocol all items lengths are less than 256. +2. The function is internal and not designed for generic use. - -Or we should provide a generic one in bandersnatch primitive wrapper to be -used in other contexts? - - -### 5.2. VRF Output +### 5.2. VRF PreOutput -A `VrfOutput` in this context is computed in function of a `VrfInput` and a -`BandersnatchSecretKey`. +Functionally, the `VrfPreOutput` can be considered as a *seed* for a PRNG to +produce an arbitrary number of output bytes. -A `VrfOutput` can be created in two ways: as a standalone object or as part of a -VRF signature. In both scenarios, the resulting `VrfOutput` remains the same, but -the primary difference lies in the inclusion of a signature in the latter, which -serves to confirm its validity. +It is computed as function of a `VrfInput` and a `BandersnatchSecretKey`. -In practice, the `VrfOutput` is a verifiable *seed* to produce a variable number -of pseudo-random bytes. These bytes are considered valid when `VrfOutput` is -accompanied by a valid signature. +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, `VrfOutput` is primarily employed -in situations where the secret key owner needs to check if the generated -pseudo-random bytes fulfill some criteria before applying the signature. +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 of `VrfOutput` from a secret key and `VrfInput`, -the following helper function is provided: +To facilitate the construction, the following helper function is provided: ```rust - fn vrf_output(secret: BandernatchSecretKey, input: VrfInput) -> VrfOutput; + fn vrf_pre_output(secret: BandernatchSecretKey, input: VrfInput) -> VrfPreOutput; ``` -Additionally, a helper function is provided for producing `len` bytes from -`VrfInput` and `VrfOutput`: +An additional helper function is provided for producing an arbitrary number of +output bytes from `VrfInput` and `VrfPreOutput`: ```rust - fn vrf_bytes(len: U32, input: VrfInput, output: VrfOuput) -> OCTET_STRING; + fn vrf_bytes(len: U32, input: VrfInput, pre_output: VrfPreOuput) -> OCTET_STRING; ``` -Just like the `VrfInput` support function, we have intentionally excluded the -detailed implementation of this function in this document. A reference implementation -is provided in the `dleq_vrfs` library: -- [`vrf_output`](https://github.com/w3f/ring-vrf/blob/18614458ca4cb335c88d4e710c13906a76f51e43/dleq_vrf/src/traits.rs#L75-L77) -- [`vrf_bytes`](https://github.com/w3f/ring-vrf/blob/18614458ca4cb335c88d4e710c13906a76f51e43/dleq_vrf/src/vrf.rs#L211-L214) +Similar to the `vrf_input` function, the details about the implementation +of these functions is omitted. Reference implementations are provided by the +[`dleq_vrfs`](https://github.com/w3f/ring-vrf/tree/master/dleq_vrfs) project +- [`vrf_pre_output`](https://docs.rs/dleq_vrf/0.0.1/dleq_vrf/keys/struct.SecretKey.html#method.vrf_preout) +- [`vrf_bytes`](https://docs.rs/dleq_vrf/0.0.1/dleq_vrf/vrf/struct.VrfInOut.html#method.vrf_preoutput_bytes) ### 5.3. VRF Signature Data -This section defines the data to be signed using the VRF primitive: +This section outlines the data to be signed utilizing the VRF primitive: ```rust VrfSignatureData ::= SEQUENCE { @@ -252,12 +253,13 @@ This section defines the data to be signed using the VRF primitive: } ``` -- `transcript`: an [`ark-transcript`](https://docs.rs/ark-transcript/latest/ark_transcript/) - object. In practice, this is a *special* hash of some protocol-specific data - to sign which should not influence the `VrfOutput`. +Where: +- `transcript`: a [`Transcript`](https://docs.rs/ark-transcript/0.0.1/ark_transcript/struct.Transcript.html) + 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 a `VrfSignatureData` object, a helper function is provided: +To simplify the construction of `VrfSignatureData` objects, a helper function is defined: ```rust fn vrf_signature_data( @@ -276,28 +278,30 @@ To simplify the construction of a `VrfSignatureData` object, a helper function i ### 5.4. VRF Signature Bandersnatch VRF offers two signature flavors: -- *plain* signature, which is much like a traditional *Schnorr* signature, -- *ring* signature which leverages a *zk-SNARK* to allows for anonymous signatures +- *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. #### 5.4.1. Plain VRF Signature This section describes the signature process for `VrfSignatureData` using the -plain Bandersnatch signature flavor. +*plain* signature flavor. ```rust PlainSignature ::= OCTET_STRING; VrfSignature ::= SEQUENCE { signature: PlainSignature, - outputs: SEQUENCE-OF VrfOutput + pre_outputs: SEQUENCE-OF VrfPreOutput } ``` -- `signature`: the actual signature. -- `outputs`: a sequence of `VrfOutput`s corresponding to the `VrfInput`s values. +Where: +- `signature`: the actual plain signature. +- `pre_outputs`: sequence of `VrfPreOutput`s corresponding to the `VrfInput`s + found within the `VrfSignatureData`. -Helper function to create a `VrfPlainSignature` from `VrfSignatureData`: +Helper function to construct `VrfPlainSignature` from `VrfSignatureData`: ```rust BandersnatchSecretKey ::= OCTET_STRING; @@ -308,8 +312,8 @@ Helper function to create a `VrfPlainSignature` from `VrfSignatureData`: ) -> VrfSignature ``` -Helper function for validating the signature and returning a `BOOLEAN` value -indicating the validity of the signature. +Helper function for signature verification returning a `BOOLEAN` value +indicating the validity of the signature (`true` on success): ```rust BandersnatchPublicKey ::= OCTET_STRING; @@ -326,22 +330,23 @@ found in the `bandersnatch_vrfs` reference implementation. #### 5.4.2. Ring VRF Signature -This section deals with the signature process for `VrfSignatureData` using the -Bandersnatch ring signature flavor. +This section describes the signature process for `VrfSignatureData` using the +*ring* signature flavor. ```rust RingSignature ::= OCTET_STRING; RingVrfSignature ::= SEQUENCE { signature: RingSignature, - outputs: SEQUENCE_OF VrfOutput + pre_outputs: SEQUENCE_OF VrfPreOutput } ``` -- `signature`: the actual signature. -- `outputs`: sequence of `VrfOutput` objects corresponding to the `VrfInput` values. +- `signature`: the actual ring signature. +- `pre_outputs`: sequence of `VrfPreOutput`s corresponding to the `VrfInput`s + found within the `VrfSignatureData`. -Helper function to create a `RingVrfSignature` from `VrfSignatureData`: +Helper function to construct `RingVrfSignature` from `VrfSignatureData`: ```rust BandersnatchRingProverKey ::= OCTET_STRING; @@ -352,9 +357,8 @@ Helper function to create a `RingVrfSignature` from `VrfSignatureData`: ) -> RingVrfSignature; ``` -Helper function for validating the signature and returning a `BOOLEAN` -indicating the validity of the signature (`True` if it's valid). It's important -to note that this function does not require the signer's public key. +Helper function for signature verification returning a `BOOLEAN` value +indicating the validity of the signature (`true` on success). ```rust BandersnatchRingVerifierKey ::= OCTET_STRING; @@ -365,6 +369,8 @@ to note that this function does not require the signer's public key. ) -> 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 @@ -375,57 +381,48 @@ implementation. ### 6.1. Epoch's First Block -The first block produced for epoch `N` is required to include the descriptor for -the next epoch `N+1`. +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: -The descriptor for next epoch is `NextEpochDescriptor`. - ```rust - AuthorityId ::= BandersnatchPublicKey; - - Randomness ::= OCTET_STRING(SIZE(32)); - NextEpochDescriptor ::= SEQUENCE { - randomness: Randomness, - authorities: SEQUENCE_OF AuthorityId, + randomness: OCTET_STRING(SIZE(32)), + authorities: SEQUENCE_OF BandersnatchPublicKey, configuration: ProtocolConfiguration OPTIONAL } ``` -- `randomness`: randomness value. +Where: +- `randomness`: 32-bytes pseudo random value. - `authorities`: list of authorities. - `configuration`: optional protocol configuration. -The `NextEpochDescriptor` must be `SCALE` encoded and embedded in the block -header digest log. - -The identifier for the digest element is `BYTES("SASS")`. +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")`. -**Security Consideration**: Instances of `NextEpochDescriptor` are generated -through on-chain code whenever a block is identified as the first of an epoch. -Consequently, every node executing the block should verify that the descriptor -locally generated during block execution matches the one produced by the block -author, which is found in the digest data before block import. +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](#613-startup-parameters). #### 6.1.1. Epoch Randomness -Each block ships with some entropy source in the form of bandersnatch -`VrfOutput`. Per block randomness is accumulated in the protocol's on-chain -randomness accumulator **after** block import. - -The exact procedure to accumulate per-block randomness is described in detail -later, in the randomness accumulator paragraph ([6.7](#67-randomness-accumulator)). - -Next epoch `randomness` is computed as: +The randomness in the `NextEpochDescriptor` `randomness` is computed as: ```rust - next_epoch_randomness = BLAKE2(32, CONCAT(randomness_accumulator, BYTES(next_epoch_index))); + 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](#67-randomness-accumulator). + #### 6.1.2. Protocol Configuration The `ProtocolConfiguration` primarily influences certain checks carried out -during tickets validation. It is defined as follows: +during tickets validation. It is defined as: ```rust ProtocolConfiguration ::= SEQUENCE { @@ -434,123 +431,138 @@ during tickets validation. It is defined as follows: } ``` -- `attempts_number`: max number of tickets that can be submitted by each - next epoch authority. -- `redundancy_factor`: controls the expected number of extra tickets produced - beyond `epoch_length`. +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 +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 in the epoch. - -We do not mind `max_attempts < epoch_length` though because this loss of -anonymity already becomes small when `attempts_number = 64` or `128` and larger -values requires more computation. +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 the section dedicated to candidate ticket validation -([6.2.2](#622-tickets-threshold)). +probability can be found in section [6.2.2](#622-tickets-threshold). -`ProtocolConfiguration` values can be adjusted via a dedicated extrinsic which -should have origin set to `Root`. A valid configuration proposal submitted on -epoch `K` will be propagated in the `NextEpochDescriptor` at the beginning of -epoch `K+1` and will be effectively enacted on epoch `K+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`. #### 6.1.3. Startup Parameters -Some parameters for first epoch (index = 0) are configurable via genesis configuration. +Some of the initial parameters for the first epoch, Epoch `#0`, are set through +the genesis configuration, which is defined as: ```rust GenesisConfig ::= SEQUENCE { - authorities: SEQUENCE_OF AuthorityId, - configuration: ProtocolConfiguration OPTIONAL + authorities: SEQUENCE_OF BandersnatchPublicKey, + configuration: ProtocolConfiguration, } ``` -Randomness for first epoch is set to all zeros. +The on-chain randomness accumulator is initialized only **after** the genesis +block is produced. It starts with the hash of the genesis block: + +```rust + randomness_accumulator = genesis_hash +``` -As block #0 is locally produced by every node by processing the genesis configuration, -the first block explicitly produced by a validator for the first epoch is block #1. +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`. -Block #1 must embed the `NextEpochDescriptor` for next epoch. This is -constructed re-using the same values used for the first epoch. +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. ### 6.2. Creation and Submission of Candidate Tickets -As a shorthand notation, in this section we refer to one of the next epoch -validators as 'the validator'. +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](#622-tickets-threshold)) +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. -Upon the beginning of a new epoch `N`, the validator will construct a set of -'tickets' to be submitted on-chain. These tickets aim to secure ownership of one -or more slots in the upcoming epoch `N+1`. +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. -Each validator is allowed to submit a maximum number of tickets whose value is -found in the next epoch `ProtocolConfiguration` `attempts_number` field. +However, validators are also advised to avoid submitting tickets too late, +as tickets submitted during the second half of the epoch must be discarded. -The expected ratio between the attempts and the number of tickets which are -assigned to the next epoch slots is driven by the ticket threshold -([6.2.2](#622-tickets-threshold)). +#### 6.2.1. Ticket Identifier Value -Each ticket has an associated unique identifier, denoted as `TicketId`. +Each ticket has an associated 128-bit unique identifier defined as: ```rust - TicketId ::= U128 + TicketId ::= U128; ``` -#### 6.2.1. Ticket Identifier Value - The value of the `TicketId` is determined by the output of the Bandersnatch VRF -when using the following inputs: - -- Next epoch randomness: `Randomness` obtained from the `NextEpochDescriptor`. -- Next epoch index: `U64` computed as epoch start slot divided epoch duration. -- Attempt index: `U32` value going from `0` to `attempts_number`. - -Let `next_epoch` be an object with the information associated to the next epoch. +with the following input: ```rust ticket_id_vrf_input = vrf_input_from_items( BYTES("sassafras-ticket-v1.0"), [ next_epoch.randomness, - BYTES(next_epoch.epoch_index), + BYTES(next_epoch.index), BYTES(attempt_index) ] ); - ticket_id_vrf_output = vrf_output(AUTHORITY_SECRET_KEY, ticket_id_vrf_input); + 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_output); + 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`. + #### 6.2.2. Tickets Threshold -A `TicketId` value is valid if its value is less than the ticket threshold. +A `TicketId` value is valid if its value is less than the ticket threshold: T = (r·s)/(a·v) Where: -- `v`: the number of authorities (aka validators) in the epoch -- `s`: number of slots in the epoch -- `r`: the redundancy factor -- `a`: number of attempts +- `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`) ##### 6.2.2.1 Formula Derivation -For an epoch of `s` slots we want to have a number of tickets in expectation for -block production equal to the `r·s`. +In an epoch with `s` slots, the goal is to achieve an expected number of tickets +for block production equal to `r·s`. -We need that there is a very small probability of their being less than `s` -winning tickets, even if up to `1/3` of authorities are offline. +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. -First we set the probability of a ticket winning as `T = (r·s)/(a·v)`. +To accomplish this, we first define the winning probability of a single ticket +as `T = (r·s)/(a·v)`. -Let `n` be the number of validators who actually participate and so `v·2/3 ≤ n ≤ v`. +Let `n` be the actual number of participating validators, where `v·2/3 ≤ n ≤ v`. -These `n` validators make `a` attempts each, for a total of `a·n` attempts. +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: @@ -561,19 +573,19 @@ By setting `r = 2`, we get s·4/3 ≤ E[X] ≤ s·2 -Using *Bernestein's inequality* we get `Pr[X < s] ≤ exp(-s/21)`. +Using *Bernestein's inequality* we get `Pr[X < s] ≤ e^(-s/21)`. -For `s = 600` this gives `Pr[X < s] < 4·10⁻¹³`, and thus we end up with a great -tolerance over offline nodes and we end-up filling all the slots with tickets -with high probability. +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](https://research.web3.foundation/Polkadot/protocols/block-production/SASSAFRAS#probabilities-and-parameters) -paragraph of the w3f description of the protocol. +paragraph in the Web3 foundation description of the protocol. #### 6.2.3. Ticket Body -Every candidate ticket identifier has an associated body. +Every candidate ticket identifier has an associated body, defined as: ```rust TicketBody ::= SEQUENCE { @@ -583,39 +595,43 @@ Every candidate ticket identifier has an associated body. } ``` +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. + 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 bytes produced by the VRF with input +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. -Let `next_epoch` be an object with the information associated to the next epoch: - ```rust revealed_vrf_input = vrf_input_from_items( domain: BYTES("sassafras-revealed-v1.0"), data: [ next_epoch.randomness, - BYTES(next_epoch.epoch_index), + BYTES(next_epoch.index), BYTES(attempt_index) ] ); - revealed_vrf_output = vrf_output(AUTHORITY_SECRET_KEY, revealed_vrf_input); + revealed_vrf_pre_output = vrf_pre_output(AUTHORITY_SECRET_KEY, revealed_vrf_input); - revealed_seed = vrf_bytes(32, revealed_vrf_input, revealed_vrf_output); + revealed_seed = vrf_bytes(32, revealed_vrf_input, revealed_vrf_pre_output); revealed_pub = ed25519_secret_from_seed(revealed_seed).public(); ``` -The usage of the ephemeral public keys will be clarified in the ticket claiming -section ([6.5](#65-claim-of-ticket-ownership-during-block-production)). +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](#65-claim-of-ticket-ownership-during-block-production) for details. #### 6.2.4. Ring Signature Production @@ -632,16 +648,16 @@ section ([6.5](#65-claim-of-ticket-ownership-during-block-production)). ] ) - ring_signature = ring_vrf_sign(RING_PROVER_KEY, sign_data) + ring_signature = ring_vrf_sign(AUTHORITY_SECRET_KEY, RING_PROVER_KEY, sign_data) ``` -`RING_PROVER` object is constructed using the authority secret key, the set -public keys which belong to the next epoch authorities and the *zk-SNARK* -context parameters (more details in the +`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](https://github.com/w3f/ring-vrf/blob/18614458ca4cb335c88d4e710c13906a76f51e43/bandersnatch_vrfs/src/ring.rs#L91-L93) reference implementation). -The body and the ring signature are combined in the `TicketEnvelope`: +The body and the ring signature are combined in the `TicketEnvelope` structure: ```rust TicketEnvelope ::= SEQUENCE { @@ -650,42 +666,36 @@ The body and the ring signature are combined in the `TicketEnvelope`: } ``` -All the ticket envelopes corresponding to valid tickets are submitted on-chain -via a dedicated unsigned extrinsic. +All the envelopes corresponding to valid tickets can be submitted on-chain via a +dedicated on-chain call (extrinsic). ### 6.3. Validation of candidate tickets All the actions in the steps described by this paragraph are executed by on-chain code. -The tickets are received via a dedicated extrinsic call. - -Generic validation rules: -- Tickets submissions must occur within the first half of the epoch. -- For unsigned extrinsics, it must be submitted by one of the current session - validators. - -Ticket specific validation rules: -- Ring signature is verified using the on-chain `BandersnatchRingVerifierKey`. -- Ticket identifier is locally computed from the `VrfOutput` contained in the - `RingVrfSignature` and its value is checked to be less than the ticket-threshold. +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 persisted on-chain. +Valid tickets bodies are all persisted on-chain. -### 6.4. Ticket-Slot assignment +### 6.4. Ticket-Slot Binding Before the beginning of the next epoch, the on-chain list of tickets must be -associated with the next epoch's slots. - -The assignment process happens in the second half of the submission epoch. - -In the end, there must be at most one ticket per slot. - -- Initially, the complete list of tickets is sorted based on their ticket-id, - with smaller values coming first. -- In cases where there are more tickets than available slots, the list is pruned - by removing the larger value. -- Tickets are then assigned to the slots using an *outside-in* assignment strategy. +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. #### 6.4.1. Outside-In Assignment @@ -700,49 +710,33 @@ strategy: Here `slot-index` is a relative value computed as: - slot-index = absolute_slot_index - epoch_start_slot + 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 *owner*, and +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. -#### 6.4.2. Fallback Assignment - 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. - -In such situation, these unassigned slots are allocated using a fallback -assignment strategy. - -The authorities registered on-chain are kept in a sorted buffer. - -The index of the authority which has the privilege to claim an unbounded slot is -calculated as follows: - -```rust - index_bytes = BLAKE2(4, CONCAT(epoch_randomness, BYTES(slot))); - index = U32(index_bytes) mod authorities_number; -``` +some *orphan* slots in the middle of the epoch will remain unbounded to any +ticket. For claiming strategy refer to [6.5.2](652-secondary-claim-method). -### 6.5. Claim of ticket ownership during block production +### 6.5. Slot Claim Production 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 block authoring varies based on whether a given slot has an +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, we will employ the primary authoring -method. Conversely, if the slot lacks an associated ticket, we will resort to -the secondary authoring method as a fallback. +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. -#### 6.5.1. Primary Claim Method +#### 6.5.1. Primary Method -Let `ticket_body` represent 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 absolute monotonic slot number. +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`: @@ -751,7 +745,7 @@ Follows the construction of `VrfSignatureData`: domain: BYTES("sassafras-randomness-v1.0"), data: [ curr_epoch.randomness, - BYTES(curr_epoch.epoch_index), + BYTES(curr_epoch.index), BYTES(slot) ] ); @@ -760,7 +754,7 @@ Follows the construction of `VrfSignatureData`: domain: BYTES("sassafras-revealed-v1.0"), data: [ curr_epoch.randomness, - BYTES(curr_epoch.epoch_index), + BYTES(curr_epoch.index), BYTES(ticket_body.attempt_index) ] ); @@ -777,45 +771,50 @@ Follows the construction of `VrfSignatureData`: ); ``` -The inclusion of `revealed_vrf_input` will generate a `VrfSignature` with a -`VrfOutput` allowing the verifier to reconstruct a `revealed_pub` key -which is expected to be equal to the one committed into the `TicketBody`. +##### 6.5.1.1. Ephemeral Key Claim -##### 6.5.1.1. (Optional) Ed25519 Erased Ephemeral Key Claim +*Fiat-Shamir* transform is used to obtain a 32-byte challenge associated with +the `VrfSignData` transcript. -As the ticket ownership can be claimed by reconstructing the `revealed_pub` -entry of the ticket, this step is purely optional and serves only to enforce -the claim. +Validators employ the secret key associated with `erased_pub`, which has been +committed in the `TicketBody`, to sign the challenge. + +```rust + 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. +- 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 -The *Fiat-Shamir* transform is used to obtain a 32-byte challenge associated -with the `VrfSignData` transcript. +#### 6.5.2. Secondary Method -Validators employ the secret key associated with `erased_pub`, which has been -committed in the `TicketBody`, to sign this challenge. +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: ```rust - challenge = sign_data.transcript.challenge(); - erased_signature = ed25519_sign(ERASED_SECRET_KEY, challenge) + index_bytes = BLAKE2(4, CONCAT(epoch_randomness, BYTES(slot))); + index = U32(index_bytes) mod authorities_number; ``` -#### 6.5.2. Secondary Claim Method - -If the slot doesn't have any associated ticket then the validator is the one -with index equal to the rule exposed in the fallback assignment section -([6.4.2](#642-fallback-assignment)). - -Given `randomness_vrf_input` constructed as shown for the primary method, the -`VrfSignatureData` is constructed as: +Given `randomness_vrf_input` constructed as shown for the primary method ([6.5.1](#primary-method)), +the `VrfSignatureData` is constructed as: ```rust sign_data = vrf_signature_data( - transcript_label: BYTES("sassafras-slot-claim-transcript-v1.0"), + transcript_label: BYTES("sassafras-claim-v1.0"), transcript_data: [ ], inputs: [ randomness_vrf_input @@ -823,10 +822,10 @@ Given `randomness_vrf_input` constructed as shown for the primary method, the ) ``` -#### 6.5.3. Slot Claim object +#### 6.5.3. Slot Claim Object -To establish ownership of a slot, the block author must construct a `SlotClaim` object -which contains all the necessary information to assert ownership of the slot. +The `SlotClaim` structure is used to contain all the necessary information to +assess ownership of a slot. ```rust SlotClaim ::= SEQUENCE { @@ -837,18 +836,7 @@ which contains all the necessary information to assert ownership of the slot. } ``` -- `authority_index`: index of the block author in the on-chain authorities list. - -- `slot`: absolute slot number (not relative with respect to the epoch start) - -- `signature`: signature that includes one or two `VrfOutputs`. - - The first `VrfOutput` is always present and is used to generate per-block - randomness. This is used to claim ticket ownership. - - The second `VrfOutput` is included if the slot is associated with a ticket. - This is relevant to claim ticket ownership. - -- `erased_signature`: optional signature providing an additional proof of ticket - ownership (see 6.5.1.1). +The claim is constructed as follows: ```rust signature = vrf_sign(AUTHORITY_SECRET_KEY, sign_data); @@ -861,116 +849,119 @@ which contains all the necessary information to assert ownership of the slot. } ``` -The `claim` object is *SCALE* encoded and sent in the block's header digest log. - -### 6.6. Validation of the claim during block verification - -Validation of `SlotClaim` object found in the block's header. +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](#primary-method) or secondary ([6.5.2](#secondary-method)) method. +- `erased_signature`: optional signature providing an additional proof of ticket + ownership ([6.5.1.1](#6511-ed25519-erased-ephemeral-key-claim). -The procedure depends on whether the slot has an associated ticket or not -according to the on-chain state. +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](#67-randomness-accumulator)). +- The second is included if the slot is bound to a ticket. This is relevant to + claim ticket ownership ([6.6.1](#661-primary-method)). -If there is a ticket linked to the slot, the primary verification method will be -used; otherwise, the protocol resorts to the secondary one. +The `claim` object is *SCALE* encoded and sent in the block's header digest log. -In both scenarios, the signature within the `SlotClaim` is verified using -a `VrfSignData` constructed as specified by paragraph 6.5. +### 6.6. Slot Claim Verification -Given `claim` an instance of `SlotClaim`: +The signature within the `SlotClaim` is verified using a `VrfSignData` +constructed as specified in [6.5](#65-slot-claim-production). ```rust - public_key = AUTHORITIES[claim.authority_index]; + public_key = authorities[claim.authority_index]; - vrf_verify(public_key, sign_data, claim.signature); + result = vrf_verify(public_key, sign_data, claim.signature); + assert(result == true); ``` -If signature verification fails then the claim is not legit. +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](#65-slot-claim-production). -### 6.6.1. Primary Claim Method Verification +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. -This verification is performed to confirm ticket ownership and is performed -utilizing the second `VrfOutput` contained within the `SlotClaim` `signature`. +For slots tied to a ticket, the primary verification method is employed. Otherwise, +the secondary method is utilized. -By using the `VrfOutput` object together with the associated expected `VrfInput` -the verifier should be able to reconstruct the `revealed_pub` key committed in -the `TicketBody`. If there is a mismatch, the claim is not legit. +### 6.6.1. Primary Method + +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. ```rust revealed_vrf_input = vrf_input_from_items( domain: BYTES("sassafras-revealed-v1.0"), data: [ curr_epoch.randomness, - BYTES(curr_epoch.epoch_index), + BYTES(curr_epoch.index), BYTES(ticket_body.attempt_index) ] ); - reveled_vrf_output = claim.signature.outputs[1]; + reveled_vrf_pre_output = claim.signature.pre_outputs[1]; - revealed_seed = vrf_bytes(32, revealed_vrf_input, revealed_vrf_output); + 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); ``` -##### 6.6.1.1. (Optional) Ephemeral Key Signature Check +##### 6.6.1.1. Ephemeral Key Signature Check -If the `erased_signature` element within the `SlotClaim` is present the -`erased_pub` key is used to verify it. +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 with identical steps as outlined in section -6.5.1.1. +The signed challenge is generated as outlined in section [6.5.1.1](#6511-ephemeral-key-claim). ```rust challenge = sign_data.transcript.challenge(); result = ed25519_verify(ticket_body.erased_pub, challenge, claim.erased_signature); - assert(result == true); ``` -#### 6.6.2. Secondary Claim Method Verification +#### 6.6.2. Secondary Method 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 the fallback assignment -section ([6.4.2](#642-fallback-assignment)) +the claim should match the one given by the rule outlined in section [6.5.2](#652-secondary-method). ### 6.7. Randomness Accumulator -The first `VrfOutput` which ships with the block's `SlotClaim` `signature` -is mandatory and must be used as the entropy source for the randomness which -gets accumulated on-chain **after** block processing. +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` within the block header, and -`accumulator` the current value for the current epoch randomness accumulator, -the `accumulator` value is updated as follows: +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: ```rust randomness_vrf_input = vrf_input_from_items( domain: BYTES("sassafras-randomness-v1.0"), data: [ curr_epoch.randomness, - BYTES(curr_epoch.epoch_index), + BYTES(curr_epoch.index), BYTES(slot) ] ); - randomness_vrf_output = claim.signature.outputs[0]; - - randomness = vrf_bytes(32, randomness_vrf_input, randomness_vrf_output); + 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 updated `accumulator` value is stored on-chain. - -The randomness accumulated during epoch `N` will be used, at the start of the -next epoch (`N+1`), as an input to compute the `NextEpochDescriptor` -`randomness` element (see section 6.1). - -As outlined throughout the document, epoch randomness value secures various -protocol-specific functions, including ticket generation and assignment of -fallback slots (refer to section 6.4.2). Additionally, users may utilize this -value for other purposes as needed. +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](#61-epochs-first-block), but custom usages +from the user are not excluded. ## 7. Drawbacks @@ -979,20 +970,22 @@ None ## 8. Testing, Security, and Privacy -The reference implementation for this RFC will be tested on testnets first. +It is critical that implementations of this RFC undergo thorough testing on +test networks. -An audit may be required to ensure the implementation does not introduce unwanted side effects +A security audit may be desirable to ensure the implementation does not +introduce unwanted side effects. ## 9. Performance, Ergonomics, and Compatibility ### 9.1. Performance -The utilization of Sassafras consensus represents a significant advancement in -the mitigation of short-lived fork occurrences. +Adopting Sassafras consensus marks a significant improvement in reducing the +frequency of short-lived forks. -Generation of forks are not possible when following the protocol and the only source -of forks is network partitioning. In this case, on recovery, the decision of -which fork to follow is not opinionated and there is only one choice. +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. ### 9.2. Ergonomics @@ -1000,8 +993,8 @@ No specific considerations. ### 9.3. Compatibility -The adoption of Sassafras impacts native client code and thus can't be -introduced via a simple runtime upgrade. +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. @@ -1010,12 +1003,14 @@ This subject is left open for a dedicated RFC. ## 10. Prior Art and References -- Web3 Foundation research page: https://research.web3.foundation/Polkadot/protocols/block-production/SASSAFRAS -- Sassafras whitepaper: https://eprint.iacr.org/2023/031.pdf -- Ring-VRF whitepaper: https://eprint.iacr.org/2023/002.pdf -- Sassafras reference implementation tracking issue: https://github.com/paritytech/substrate/issues/11515 -- Sassafras reference implementation main PR: https://github.com/paritytech/substrate/pull/11879 +- [Web3 Foundation research page](https://research.web3.foundation/Polkadot/protocols/block-production/SASSAFRAS) +- [Sassafras research paper](https://eprint.iacr.org/2023/031.pdf) +- [Ring-VRF research paper](https://eprint.iacr.org/2023/002.pdf) +- [Sassafras reference implementation tracking issue](https://github.com/paritytech/substrate/issues/11515) +- [Sassafras reference implementation main PR](https://github.com/paritytech/substrate/pull/11879) +- [Bandersnatch VRFS reference implementation](https://github.com/w3f/ring-vrf/tree/master/bandersnatch_vrfs) + ## 11. Unresolved Questions @@ -1025,43 +1020,41 @@ None ## 12. Future Directions and Related Material While this RFC lays the groundwork and outlines the core aspects of the -protocol, several crucial topics remain to be addressed in future RFCs to ensure -the protocol's completeness and security. - -These topics include: +protocol, several crucial topics remain to be addressed in future RFCs. +These include: -### 12.1. Interactions with the Runtime +### 12.1. Interactions with On-Chain Code -- **Outbound Interface**. Interfaces exposed by the host which are required by the runtime. - These are commonly dubbed *Host Functions*. +- **Outbound Interfaces**: Interfaces that the host environment provides to the + on-chain code, typically known as *Host Functions*. -- **Unrecorded Inbound Interfaces**. Interfaces exposed by the runtime which are required by the host. - These are commonly dubbed *Runtime APIs*. +- **Unrecorded Inbound Interfaces**. Interfaces that the on-chain code provides + to the host code, typically known as *Runtime APIs*. -- **Transactional Inbound Interfaces**. Interfaces exposed by the runtime which alter the state. - These are commonly dubbed *Extrinsics* and *Inherents*. +- **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) ### 12.2. Deployment Strategies -- **Protocol Migration**. Exploring how this protocol can seamlessly replace - an already operational instance of another protocol is essential. Future RFCs - should delve into the deployment strategy, including considerations for a smooth - transition process. +- **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. -### 12.3. ZK-SNARK SRS Initialization Ceremony. +### 12.3. ZK-SNARK SRS Initialization -- **Timing and Procedure**: Determining the timing and procedure for the ZK-SNARK - SRS (Structured Reference String) initialization ceremony. Future RFCs should - provide insights into whether this process should be performed before the - deployment of Sassafras and the steps involved. +- **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 ceremony, we - must understand whether the SRS is shared with para-chains or maintained - independently. +- **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. ### 12.4. Anonymous Submission of Tickets. - **Mixnet Integration**: Submitting tickets directly can pose a risk of potential deanonymization through traffic analysis. Subsequent RFCs should - investigate the potential for incorporating Mixnet technology or other + investigate the potential for incorporating Mixnet protocol or other privacy-enhancing mechanisms to address this concern. From 58020faf57a01fd29e103448e0fa38b76a89877b Mon Sep 17 00:00:00 2001 From: Davide Galassi Date: Thu, 4 Jan 2024 17:49:54 +0100 Subject: [PATCH 21/26] Fix broken links --- text/0026-sassafras-consensus.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/text/0026-sassafras-consensus.md b/text/0026-sassafras-consensus.md index bf68ba742..8f526491e 100644 --- a/text/0026-sassafras-consensus.md +++ b/text/0026-sassafras-consensus.md @@ -631,7 +631,7 @@ Where: - `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](#65-claim-of-ticket-ownership-during-block-production) for details. +Refer to section [6.5](#65-slot-claim-production) for details. #### 6.2.4. Ring Signature Production @@ -719,7 +719,7 @@ 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](652-secondary-claim-method). +ticket. For claiming strategy refer to [6.5.2](#652-secondary-claim-method). ### 6.5. Slot Claim Production @@ -802,14 +802,14 @@ Is this step really necessary? #### 6.5.2. Secondary Method 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: +the index of the authority which has the privilege to claim an *orphan* slot is: ```rust 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](#primary-method)), +Given `randomness_vrf_input` constructed as shown for the primary method ([6.5.1](#651-primary-method)), the `VrfSignatureData` is constructed as: ```rust @@ -853,9 +853,9 @@ 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](#primary-method) or secondary ([6.5.2](#secondary-method)) method. + primary [6.5.1](#651-primary-method) or secondary ([6.5.2](#652-secondary-method)) method. - `erased_signature`: optional signature providing an additional proof of ticket - ownership ([6.5.1.1](#6511-ed25519-erased-ephemeral-key-claim). + ownership ([6.5.1.1](#6511-ephemeral-key-claim)). The signature includes one or two `VrfPreOutputs`. - The first is always present and is used to generate per-block randomness From 2610f547004607feb78a2743d06242b959406d85 Mon Sep 17 00:00:00 2001 From: Davide Galassi Date: Thu, 4 Jan 2024 17:54:07 +0100 Subject: [PATCH 22/26] One more broken link --- text/0026-sassafras-consensus.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/text/0026-sassafras-consensus.md b/text/0026-sassafras-consensus.md index 8f526491e..f9cd02d29 100644 --- a/text/0026-sassafras-consensus.md +++ b/text/0026-sassafras-consensus.md @@ -719,7 +719,7 @@ 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](#652-secondary-claim-method). +ticket. For claiming strategy refer to [6.5.2](#652-secondary-method). ### 6.5. Slot Claim Production From bcb569b7b2308407761367b7f80d43bb0577ef30 Mon Sep 17 00:00:00 2001 From: Davide Galassi Date: Wed, 1 May 2024 18:59:34 +0200 Subject: [PATCH 23/26] Major protocol revision Pick the good bits from Safrole protocol. --- text/0026-sassafras-consensus.md | 1003 +++++++++++++----------------- 1 file changed, 432 insertions(+), 571 deletions(-) diff --git a/text/0026-sassafras-consensus.md b/text/0026-sassafras-consensus.md index f9cd02d29..e915a5a89 100644 --- a/text/0026-sassafras-consensus.md +++ b/text/0026-sassafras-consensus.md @@ -3,7 +3,7 @@ | | | | --------------- | ---------------------------------------------------------------- | | **Start Date** | September 06, 2023 | -| **Description** | Sassafras consensus protocol description and structures | +| **Description** | Sassafras consensus protocol specification | | **Authors** | Davide Galassi | @@ -63,60 +63,68 @@ The protocol will have a central role in the next generation block authoring consensus systems. -## 3. Notation and Convention +## 3. Notation This section outlines the notation and conventions adopted throughout this document to ensure clarity and consistency. -### 3.1. Data Structures Definitions and Encoding +### 3.1. Data Structures Definitions Data structures are primarily defined using standard [ASN.1](https://en.wikipedia.org/wiki/ASN.1), -syntax with few exceptions: -- Fixed width integer types are not explicitly defined by ASN.1 standard. - Within this document, `U` denotes a `n`-bit unsigned integer. - -Unless explicitly noted, all types must be serialized using -[SCALE](https://github.com/paritytech/parity-scale-codec) codec. +syntax with few exceptions To ensure interoperability of serialized structures, the order of the fields must match the structures definitions found within this document. +### 3.2. Types Alias + +We define some type alias to make ASN.1 syntax more intuitive. + +- Unsigned integer: `Unsigned ::= INTEGER (0..MAX)` +- n bits unsigned integer: `Unsigned ::= 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 ::= SEQUENCE OF T` + E.g. `Sequence ::= SEQUENCE OF Unsigned` +- Fixed length homogeneous sequence: `Sequence ::= Sequence (SIZE(n))` +- Octet string alias: `OctetString ::= Sequence` +- Fixed length octet string: `OctetString ::= Sequence` +- Optional value: `Option ::= T OPTIONAL` + ### 3.2. Pseudo-Code 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`, it returns the little-endian encoding of the integer - `U` as `n/8` octets. +Syntax: + +- `ENCODE(x: T) -> OctetString`: encodes `x` as an `OctetString` using + [SCALE](https://github.com/paritytech/parity-scale-codec) codec. + +- `DECODE(x: OctetString) -> T`: decodes `x` as a value with type `T` using + [SCALE](https://github.com/paritytech/parity-scale-codec) codec. -- `U(x: OCTET_STRING)`: returns a `U` interpreting `x` as the - little-endian encoding of a `n` bits unsigned integer. +- `BLAKE2(n: Unsigned, x: OctetString) -> OctetString`: standard *Blake2b* hash. -- `SCALE(x: T)`: returns an `OCTET_STRING` representing the SCALE encoding of - `x` with type `T`. +- `CONCAT(x₀: OctetString, ..., xₖ: OctetString) -> OctetString`: concatenate the + inputs octets. -- `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). +- `LENGTH(x: Sequence) -> Unsigned`: returns the number of elements in `x`. -- `CONCAT(x₀: OCTET_STRING, ..., xₖ: OCTET_STRING)`: returns the concatenation - of the inputs as an `OCTET_STRING`. +- `GET(seq: Sequence, i: Unsigned) -> T`: returns the i-th element of a sequence. -- `LENGTH(x: OCTET_STRING)`: returns the number of octets in `x` as an `U32`. +- `PUSH(seq: Sequence, x: T)`: append `x` as the new last element of the sequence. + +- `POP(seq: Sequence) -> T`: extract and returns the last element of a sequence. ### 3.3. Incremental Introduction of Types and Functions 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. - ## 4. Protocol Introduction @@ -151,325 +159,239 @@ 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. -### 4.5. Validation of Ticket Ownership - -During block verification, the claim of ticket ownership is validated. - ## 5. Bandersnatch VRFs Cryptographic Primitives -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](https://eprint.iacr.org/2023/002.pdf) authored by the Web3 foundation -research team. +For an in-depth explanation, refer to the Bandersnatch VRF +[spec](https://github.com/davxy/bandersnatch-vrfs-spec) -### 5.1. VRF Input +Bandersnatch VRF can be used in two flavors: +- *Bare* VRF: extends the IETF ECVRF [RFC 9381](https://datatracker.ietf.org/doc/rfc9381/), +- *Ring* VRF: provides anonymous signatures by leveraging a *zk-SNARK*. -The VRF Input, denoted as `VrfInput`, is constructed by combining a domain -identifier with arbitrary data through the `vrf_input` function: +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. -```rust - 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`](https://github.com/w3f/ring-vrf/tree/master/bandersnatch_vrfs) -project. +### 5.1 Plain VRF Interface - -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: +Function to construct a `VrfSignature`. ```rust - 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); - } + fn vrf_sign( + secret: BandernatchSecretKey, + input: OctetString, + extra: OctetString, + ) -> VrfSignature ``` -Note that each item length is safely casted to an `U8` as: -1. In the context of this protocol all items lengths are less than 256. -2. The function is internal and not designed for generic use. - -### 5.2. VRF PreOutput - -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: +Function for signature verification returning a Boolean value indicating the +validity of the signature (`1` on success): ```rust - fn vrf_pre_output(secret: BandernatchSecretKey, input: VrfInput) -> VrfPreOutput; + fn vrf_verify( + public: PublicKey, + input: OctetString, + extra: OctetString, + signature: VrfSignature + ) -> Unsigned<1>; ``` -An additional helper function is provided for producing an arbitrary number of -output bytes from `VrfInput` and `VrfPreOutput`: +Function to derive the VRF output from input and secret: ```rust - fn vrf_bytes(len: U32, input: VrfInput, pre_output: VrfPreOuput) -> OCTET_STRING; + fn vrf_output( + secret: BandernatchSecretKey, + input: OctetString, + ) -> OctetString<32>; ``` -Similar to the `vrf_input` function, the details about the implementation -of these functions is omitted. Reference implementations are provided by the -[`dleq_vrfs`](https://github.com/w3f/ring-vrf/tree/master/dleq_vrfs) project -- [`vrf_pre_output`](https://docs.rs/dleq_vrf/0.0.1/dleq_vrf/keys/struct.SecretKey.html#method.vrf_preout) -- [`vrf_bytes`](https://docs.rs/dleq_vrf/0.0.1/dleq_vrf/vrf/struct.VrfInOut.html#method.vrf_preoutput_bytes) +Function to derive the VRF output from a signature: -### 5.3. VRF Signature Data +```rust + fn vrf_signed_output( + signature: VrfSignature, + ) -> OctetString<32>; +``` -This section outlines the data to be signed utilizing the VRF primitive: +Note that the following condition is always satisfied: ```rust - VrfSignatureData ::= SEQUENCE { - transcript: Transcript, - inputs: SEQUENCE_OF VrfInput - } + let signature = vrf_sign(secret, input, extra); + vrf_output(secret, input) == vrf_signed_output(signature) ``` -Where: -- `transcript`: a [`Transcript`](https://docs.rs/ark-transcript/0.0.1/ark_transcript/struct.Transcript.html) - 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: - -```rust - 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 } - } -``` +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. -### 5.4. VRF Signature +#### 5.4.2. Ring VRF Interface -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. +Function to construct `RingVrfSignature`. -#### 5.4.1. Plain VRF Signature +```rust + fn ring_vrf_sign( + secret: SecretKey, + prover: RingProverKey, + input: OctetString, + extra: OctetString, + ) -> RingVrfSignature; +``` -This section describes the signature process for `VrfSignatureData` using the -*plain* signature flavor. +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. ```rust - PlainSignature ::= OCTET_STRING; - - VrfSignature ::= SEQUENCE { - signature: PlainSignature, - pre_outputs: SEQUENCE-OF VrfPreOutput - } + fn ring_vrf_verify( + verifier: RingVerifierKey, + input: OctetString, + extra: OctetString, + signature: RingVrfSignature, + ) -> Unsigned<1>; ``` -Where: -- `signature`: the actual plain signature. -- `pre_outputs`: sequence of `VrfPreOutput`s corresponding to the `VrfInput`s - found within the `VrfSignatureData`. - -Helper function to construct `VrfPlainSignature` from `VrfSignatureData`: +Function to derive the VRF output from a ring signature: ```rust - BandersnatchSecretKey ::= OCTET_STRING; - - fn vrf_sign( - secret: BandernatchSecretKey, - signature_data: VrfSignatureData - ) -> VrfSignature + fn ring_vrf_signed_output( + signature: RingVrfSignature, + ) -> OctetString<32>; ``` -Helper function for signature verification returning a `BOOLEAN` value -indicating the validity of the signature (`true` on success): +Note that the following condition is always satisfied: ```rust - BandersnatchPublicKey ::= OCTET_STRING; - - fn vrf_verify( - public: BandersnatchPublicKey, - signature: VrfSignature - ) -> BOOLEAN; + 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 `BandersnatchSecretKey`, `BandersnatchPublicKey` -and `PlainSignature` are intentionally left undefined. Their definitions can be -found in the `bandersnatch_vrfs` reference implementation. +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. -#### 5.4.2. Ring VRF Signature -This section describes the signature process for `VrfSignatureData` using the -*ring* signature flavor. +## 6. Sassafras Protocol -```rust - RingSignature ::= OCTET_STRING; +#### 6.1. Protocol Configuration + +The `ProtocolConfiguration` is constant and primarily influences certain checks +carried out during tickets validation. It is defined as: - RingVrfSignature ::= SEQUENCE { - signature: RingSignature, - pre_outputs: SEQUENCE_OF VrfPreOutput +```rust + ProtocolConfiguration ::= Sequence { + epoch_length: Unsigned32, + attempts_number: Unsigned8, + redundancy_factor: Unsigned8, } ``` -- `signature`: the actual ring signature. -- `pre_outputs`: sequence of `VrfPreOutput`s corresponding to the `VrfInput`s - found within the `VrfSignatureData`. - -Helper function to construct `RingVrfSignature` from `VrfSignatureData`: +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. -```rust - BandersnatchRingProverKey ::= OCTET_STRING; - - fn ring_vrf_sign( - secret: BandersnatchRingProverKey, - signature_data: VrfSignatureData, - ) -> RingVrfSignature; -``` +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. -Helper function for signature verification returning a `BOOLEAN` value -indicating the validity of the signature (`true` on success). +Details about how exactly these parameters drives the ticket validity +probability can be found in section [6.2.2](#622-tickets-threshold). -```rust - BandersnatchRingVerifierKey ::= OCTET_STRING; +### 6.2. Header Digest Log - fn ring_vrf_verify( - verifier: BandersnatchRingVerifierKey, - signature: RingVrfSignature, - ) -> BOOLEAN; -``` +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. -Note that this function doesn't require the signer's public key. +The structures are defined to be quite generic and usable by other subsystems: -In this document, the types `BandersnatchRingProverKey`, -`BandersnatchRingVerifierKey`, and `RingSignature` are intentionally left -undefined. Their definitions can be found in the `bandersnatch_vrfs` reference -implementation. +```rust + DigestItem ::= Sequence { + id: OctetString<4>, + data: OctetString + } + Digest ::= Sequence +``` -## 6. Sassafras Protocol +For Sassafras related `DiegestItem`s the `id` is set to the constant ASCII string `"SASS"`. -### 6.1. Epoch's First Block +### 6.3. On-Chain Randomness -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: +On-Chain, we maintain a sequence with four randomness entries. ```rust - NextEpochDescriptor ::= SEQUENCE { - randomness: OCTET_STRING(SIZE(32)), - authorities: SEQUENCE_OF BandersnatchPublicKey, - configuration: ProtocolConfiguration OPTIONAL - } + RandomnessBuffer ::= Sequence, 4> ``` -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")`. +During epoch `N` -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](#613-startup-parameters). +- 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](#67-randomness-accumulator). -#### 6.1.1. Epoch Randomness +- The second entry of the buffer is the snapshot of the accumulator after the + execution of the last block of epoch `N-1`. -The randomness in the `NextEpochDescriptor` `randomness` is computed as: +- The third entry of the buffer is the snapshot of the accumulator after the + execution of the last block of epoch `N-2`. -```rust - randomness = BLAKE2(32, CONCAT(randomness_accumulator, BYTES(next_epoch.index))); -``` +- The fourth entry of the buffer is the snapshot of the accumulator after the + execution of the last block of epoch `N-3`. -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](#67-randomness-accumulator). +The buffer is entries are updated **after** block execution. -#### 6.1.2. Protocol Configuration +### 6.4. Epoch's First Block -The `ProtocolConfiguration` primarily influences certain checks carried out -during tickets validation. It is defined as: +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: ```rust - ProtocolConfiguration ::= SEQUENCE { - attempts_number: U32, - redundancy_factor: U32 + NextEpochDescriptor ::= Sequence { + randomness: OctetString<32>, + authorities: Sequence, } ``` 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. +- `randomness`: last randomness accumulator snapshot, which must be equivalent + to `GET(RandomnessBuffer, 1)` **after** block execution. +- `authorities`: list of validators scheduled for next epoch. -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](#622-tickets-threshold). +This descriptor is `SCALE` encoded and embedded in the block header's digest +log. -`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`. +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](#641-startup-parameters). -#### 6.1.3. Startup Parameters +#### 6.4.1. Startup Parameters Some of the initial parameters for the first epoch, Epoch `#0`, are set through the genesis configuration, which is defined as: ```rust - GenesisConfig ::= SEQUENCE { - authorities: SEQUENCE_OF BandersnatchPublicKey, - configuration: ProtocolConfiguration, + GenesisConfig ::= Sequence { + authorities: Sequence, } ``` The on-chain randomness accumulator is initialized only **after** the genesis -block is produced. It starts with the hash of the genesis block: - -```rust - randomness_accumulator = genesis_hash -``` +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 @@ -480,76 +402,72 @@ 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. -### 6.2. Creation and Submission of Candidate Tickets +### 6.5. Offchain Tickets Creation and Submission + +During epoch `N`, each validator associated to epoch `N+2` constructs a set of +tickets which may be eligible ([6.5.2](#652-tickets-threshold)) to be delivered +to on-chain proxies, which are the validators scheduled for epoch `N+1`. -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](#622-tickets-threshold)) -to be submitted on-chain. These tickets aim to secure ownership of one or more -slots in the upcoming 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, as specified by -the `attempts_number` field in the `ProtocolConfiguration` for the next epoch. +Each validator is allowed to submit a maximum number of tickets, constrained by +`attempts_number` field of the `ProtocolConfiguration`. -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. +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. -However, validators are also advised to avoid submitting tickets too late, -as tickets submitted during the second half of the epoch must be discarded. +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?). -#### 6.2.1. Ticket Identifier Value +#### 6.5.1. Ticket Identifier -Each ticket has an associated 128-bit unique identifier defined as: +Each ticket has an associated identifier defined as: ```rust - TicketId ::= U128; + TicketId ::= OctetString<32>; ``` -The value of the `TicketId` is determined by the output of the Bandersnatch VRF -with the following input: +The value of the `TicketId` is completely determined by the output of the +Bandersnatch VRF with the following **unbiasable** input: ```rust - ticket_id_vrf_input = vrf_input_from_items( - BYTES("sassafras-ticket-v1.0"), - [ - next_epoch.randomness, - BYTES(next_epoch.index), - BYTES(attempt_index) - ] + let ticket_vrf_input = CONCAT( + BYTES("sassafras_ticket"), + GET(randomness_buffer, 1), + 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); + let ticket_id = vrf_output(AUTHORITY_SECRET_KEY, ticket_vrf_input); ``` 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`. +- `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`. -#### 6.2.2. Tickets Threshold +#### 6.5.2. Tickets Threshold -A `TicketId` value is valid if its value is less than the ticket threshold: +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 authorities (aka validators) number +- `v`: epoch's validators number - `s`: epoch's slots number - `r`: redundancy factor - `a`: attempts number - `T`: ticket threshold value (`0 ≤ T ≤ 1`) -##### 6.2.2.1 Formula Derivation - In an epoch with `s` slots, the goal is to achieve an expected number of tickets for block production equal to `r·s`. @@ -583,145 +501,129 @@ For more details about threshold formula please refer to the [probabilities and parameters](https://research.web3.foundation/Polkadot/protocols/block-production/SASSAFRAS#probabilities-and-parameters) paragraph in the Web3 foundation description of the protocol. -#### 6.2.3. Ticket Body +#### 6.5.3. Ticket Body -Every candidate ticket identifier has an associated body, defined as: +Every ticket candidate has an associated body, defined as: ```rust - TicketBody ::= SEQUENCE { - attempt_index: U32, - erased_pub: Ed25519PublicKey, - revealed_pub: Ed25519PublicKey + TicketBody ::= Sequence { + attempt_index: Unsigned8, + opaque: OctetString, } ``` 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. +- `attempt_index`: index used to generate the associated `TicketId`. +- `opaque`: additional data for user-defined applications. -The process of generating an erased key pair is intentionally left undefined, -allowing the implementor the freedom to choose the most suitable strategy. +#### 6.5.4. Ticket Signature -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. +`TicketBody` must be signed using the Bandersnatch Ring VRF flavor ([5.4.2](#542-ring-vrf-interface)). ```rust - revealed_vrf_input = vrf_input_from_items( - domain: BYTES("sassafras-revealed-v1.0"), - data: [ - next_epoch.randomness, - BYTES(next_epoch.index), - BYTES(attempt_index) - ] + let signature = ring_vrf_sign( + secret_key, + ring_prover_key + ticket_vrf_input, + ENCODE(ticket_body), ); - - 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](#65-slot-claim-production) for details. - -#### 6.2.4. Ring Signature Production +`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). -`TicketBody` must be signed using the Bandersnatch ring VRF flavor ([5.4.2](#542-ring-vrf-signature)). +Finally, the body and the ring signature are combined within the `TicketEnvelope`: ```rust - 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](https://github.com/w3f/ring-vrf/blob/18614458ca4cb335c88d4e710c13906a76f51e43/bandersnatch_vrfs/src/ring.rs#L91-L93) -reference implementation). - -The body and the ring signature are combined in the `TicketEnvelope` structure: - -```rust - TicketEnvelope ::= SEQUENCE { + 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). - -### 6.3. Validation of candidate tickets +### 6.6. Onchain Tickets Validation 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. +1. 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. + +2. Ticket identifier is locally recomputed from the `RingVrfSignature` and its value + is checked to be less than the tickets' threshold. + +3. 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. + +4. 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. + +5. 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. -### 6.4. Ticket-Slot Binding +Pseudo-code for ticket validation for steps 1 and 2: -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. +```rust + 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); +``` -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. +### 6.7. Ticket-Slot Binding -#### 6.4.1. Outside-In Assignment +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 `[t0, t1, t2, ..., tk]` to be assigned to -`n` slots, where `n ≥ k`, the tickets are allocated according to the following +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, ............ , n ] - tickets : [ t1, t3, t5, ... , t4, t2, t0 ] + slot_index : [ 0, 1, 2, 3 , ... ] + tickets : [ t₀, tₙ, t₁, tₙ₋₁, ... ] ``` Here `slot-index` is a relative value computed as: - slot-index = absolute_slot - epoch_start_slot + 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 authority to claim the corresponding slot. This +consequently, who possesses the validator 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](#652-secondary-method). +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](#682-secondary-method). +Note that this situation always apply to the first epochs after genesis. -### 6.5. Slot Claim Production +### 6.8. Slot Claim With tickets bound to epoch slots, every validator acquires information about the slots for which they are supposed to produce a block. @@ -732,237 +634,201 @@ 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. -#### 6.5.1. Primary Method +#### 6.8.1. Primary Method -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). +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. -Follows the construction of `VrfSignatureData`: +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: ```rust - 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 - ] + let seal_vrf_input = CONCAT( + BYTES("sassafras_ticket"), + GET(randomness_buffer, 3), + BYTES(ticket_body.attempt_index) ); ``` -##### 6.5.1.1. Ephemeral Key Claim +This `seal_vrf_input`, when signed with the correct validator secret key must +generate the same `TicketId` associated on-chain to the target slot. -*Fiat-Shamir* transform is used to obtain a 32-byte challenge associated with -the `VrfSignData` transcript. +#### 6.8.2. Secondary Method -Validators employ the secret key associated with `erased_pub`, which has been -committed in the `TicketBody`, to sign the challenge. +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: ```rust - challenge = sign_data.transcript.challenge(); - erased_signature = ed25519_sign(ERASED_SECRET_KEY, challenge); + 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(index_bytes) % LENGTH(authorities); ``` -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 - +With `relative_slot_index` the slot offset relative to the epoch's start and `authorities` +the `Sequence` of current epoch validators. -#### 6.5.2. Secondary Method - -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: - -```rust - 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](#651-primary-method)), -the `VrfSignatureData` is constructed as: +Let `randomness_buffer` be the instance of `RandomnessBuffer` stored in on-chain state +then the VRF input for slot claiming is constructed as: ```rust - sign_data = vrf_signature_data( - transcript_label: BYTES("sassafras-claim-v1.0"), - transcript_data: [ ], - inputs: [ - randomness_vrf_input - ] - ) + let seal_vrf_input = CONCAT( + BYTES("sassafras_fallback"), + GET(randomness_buffer, 3), + ); ``` -#### 6.5.3. Slot Claim Object +#### 6.8.3. Claim Data -The `SlotClaim` structure is used to contain all the necessary information to -assess ownership of a slot. +The slot claim data is a digest entry which contains additional information +which is required by the protocol in order to verify the block: ```rust - SlotClaim ::= SEQUENCE { - authority_index: U32, - slot: U64, - signature: VrfSignature, - erased_signature: Ed25519Signature OPTIONAL + ClaimData ::= Sequence { + slot: Unsigned32, + validator_index: Unsigned32, + randomness_source: VrfSignature, } ``` -The claim is constructed as follows: +- `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: ```rust - signature = vrf_sign(AUTHORITY_SECRET_KEY, sign_data); + 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, + [] + ); - claim = SlotClaim { - authority_index, + let claim = ClaimData { slot, - signature, - erased_signature + validator_index, + randomness_source, } ``` -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](#651-primary-method) or secondary ([6.5.2](#652-secondary-method)) method. -- `erased_signature`: optional signature providing an additional proof of ticket - ownership ([6.5.1.1](#6511-ephemeral-key-claim)). - -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](#67-randomness-accumulator)). -- The second is included if the slot is bound to a ticket. This is relevant to - claim ticket ownership ([6.6.1](#661-primary-method)). +The `claim` object is *SCALE* encoded and pushed into the header digest log. -The `claim` object is *SCALE* encoded and sent in the block's header digest log. +#### 6.8.4. Block Seal -### 6.6. Slot Claim Verification - -The signature within the `SlotClaim` is verified using a `VrfSignData` -constructed as specified in [6.5](#65-slot-claim-production). +A block is sealed as follows: ```rust - public_key = authorities[claim.authority_index]; + let unsealed_header_bytes = ENCODE(header); + + let seal = vrf_sign( + AUTHORITY_SECRET_KEY, + seal_vrf_input, + unsealed_header_bytes + ); - result = vrf_verify(public_key, sign_data, claim.signature); - assert(result == true); + PUSH(header.digest, ENCODE(seal)); ``` -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](#65-slot-claim-production). +With `header` the block's header without the seal digest log entry. -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. +The `seal` object is a `VrfSignature` instance, which is *SCALE* encoded and +pushed as the last entry of the block's header digest log. -For slots tied to a ticket, the primary verification method is employed. Otherwise, -the secondary method is utilized. +### 6.9. Slot Claim Verification -### 6.6.1. Primary Method +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. -This method verifies ticket ownership using the second `VrfPreOutput` from the -`SlotClaim` signature +The next entry is extracted from the header digest log, and is interpreted as a +`ClaimData` instance. -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. +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. ```rust - 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) - ] + let seal_signature = DECODE(POP(header.digest)); + let unsealed_header_bytes = ENCODE(header); + let claim_data = DECODE(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); - reveled_vrf_pre_output = claim.signature.pre_outputs[1]; + let randomness_vrf_input = vrf_signed_output(seal_signature); - 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); + let result = vrf_verify( + public_key, + randomness_vrf_input, + [], + claim_data.randomness_source + ); + assert(result == 1); ``` -##### 6.6.1.1. Ephemeral Key Signature Check +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](#68-slot-claiming). + +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. -If the `erased_signature` is present in `SlotClaim`, the `erased_pub` within the -committed `TicketBody` key is used to verify it. +### 6.9.1. Primary Method -The signed challenge is generated as outlined in section [6.5.1.1](#6511-ephemeral-key-claim). +For slots tied to a ticket, the primary verification method is employed. +This method verifies ticket ownership using the `TicketId` associated to the slot. ```rust - challenge = sign_data.transcript.challenge(); - result = ed25519_verify(ticket_body.erased_pub, challenge, claim.erased_signature); - assert(result == true); + let ticket_id = vrf_signed_output(seal_signature); + assert(ticket_id == expected_ticket_id); ``` -#### 6.6.2. Secondary Method +With `expected_ticket_id` the ticket identifier committed on-chain together +with the associated `ticket_body`. + +#### 6.9.2. Secondary Method 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](#652-secondary-method). +the claim data must match the one given by the procedure outlined in section +[6.8.2](#682-secondary-method). -### 6.7. Randomness Accumulator +### 6.10. Randomness Accumulator -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. +The randomness accumulator is updated using the `randomness_source` signature found +within the `ClaimData` object. -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: +In particular, fresh randomness is derived and accumulated **after** block +execution as follows: ```rust - 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); + let fresh_randomness = vrf_signed_output(claim.randomness_source); - randomness_accumulator = BLAKE2(32, CONCAT(randomness_accumulator, randomness)); + let prev_accumulator = POP(randomness_buffer); + let curr_accumulator = BLAKE2(CONCAT(randomness_accumulator, fresh_randomness)); + PUSH(randomness_buffer, curr_accumulator); ``` -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](#61-epochs-first-block), but custom usages -from the user are not excluded. - ## 7. Drawbacks @@ -1003,14 +869,14 @@ This subject is left open for a dedicated RFC. ## 10. Prior Art and References -- [Web3 Foundation research page](https://research.web3.foundation/Polkadot/protocols/block-production/SASSAFRAS) +- [Sassafras layman introduction](https://research.web3.foundation/Polkadot/protocols/block-production/SASSAFRAS) - [Sassafras research paper](https://eprint.iacr.org/2023/031.pdf) -- [Ring-VRF research paper](https://eprint.iacr.org/2023/002.pdf) +- [Bandersnatch VRFs specification](https://github.com/davxy/bandersnatch-vrfs-spec). +- [Bandersnatch VRFs reference implementation](https://github.com/davxy/ark-ec-vrfs). +- [W3F Ring VRF research paper](https://eprint.iacr.org/2023/002.pdf) - [Sassafras reference implementation tracking issue](https://github.com/paritytech/substrate/issues/11515) - [Sassafras reference implementation main PR](https://github.com/paritytech/substrate/pull/11879) -- [Bandersnatch VRFS reference implementation](https://github.com/w3f/ring-vrf/tree/master/bandersnatch_vrfs) - ## 11. Unresolved Questions @@ -1021,7 +887,6 @@ 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: ### 12.1. Interactions with On-Chain Code @@ -1029,7 +894,7 @@ These include: 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*. + 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* @@ -1038,23 +903,19 @@ These include: ### 12.2. Deployment Strategies - **Protocol Migration**. Exploring how this protocol can seamlessly replace an - already operational instance of another protocol. Future RFCs should focus on + already operational instance of another protocol. Future RFCs may focus on deployment strategies to facilitate a smooth transition. -### 12.3. ZK-SNARK SRS Initialization +### 12.3. ZK-SNARK URS Initialization -- **Procedure**: Determining the procedure for the *zk-SNARK* SRS (Structured - Reference String) initialization. Future RFCs should provide insights into +- **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). -- **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. - ### 12.4. Anonymous Submission of Tickets. -- **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. +- **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. From 7ae319ad7d4b064a5a93a5ebfc76bf003a0e42c4 Mon Sep 17 00:00:00 2001 From: Davide Galassi Date: Mon, 10 Jun 2024 18:00:39 +0200 Subject: [PATCH 24/26] One more review iteration --- text/0026-sassafras-consensus.md | 380 ++++++++++++++++--------------- 1 file changed, 202 insertions(+), 178 deletions(-) diff --git a/text/0026-sassafras-consensus.md b/text/0026-sassafras-consensus.md index e915a5a89..bdcaf6b45 100644 --- a/text/0026-sassafras-consensus.md +++ b/text/0026-sassafras-consensus.md @@ -17,6 +17,7 @@ validators set while ensuring that the identity of validators assigned to the slots remains undisclosed until the slot is actively claimed during block production. + ## 1. Motivation Sassafras Protocol has been rigorously detailed in a comprehensive @@ -31,7 +32,7 @@ implementation. ### 1.1. Relevance to Implementors This RFC focuses on providing implementors with the necessary insights into the -protocol's operation. +core 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 @@ -71,14 +72,14 @@ document to ensure clarity and consistency. ### 3.1. Data Structures Definitions Data structures are primarily defined using standard [ASN.1](https://en.wikipedia.org/wiki/ASN.1), -syntax with few exceptions +syntax with few exceptions. To ensure interoperability of serialized structures, the order of the fields must match the structures definitions found within this document. ### 3.2. Types Alias -We define some type alias to make ASN.1 syntax more intuitive. +We define some types alias to make ASN.1 syntax more intuitive. - Unsigned integer: `Unsigned ::= INTEGER (0..MAX)` - n bits unsigned integer: `Unsigned ::= INTEGER (0..2^n - 1)` @@ -88,29 +89,28 @@ We define some type alias to make ASN.1 syntax more intuitive. - Non-homogeneous sequence (struct/tuple): `Sequence ::= SEQUENCE` - Homogeneous sequence (vector): `Sequence ::= SEQUENCE OF T` E.g. `Sequence ::= SEQUENCE OF Unsigned` -- Fixed length homogeneous sequence: `Sequence ::= Sequence (SIZE(n))` +- Fixed length homogeneous sequence (array): `Sequence ::= Sequence (SIZE(n))` - Octet string alias: `OctetString ::= Sequence` - Fixed length octet string: `OctetString ::= Sequence` - Optional value: `Option ::= T OPTIONAL` ### 3.2. Pseudo-Code -It is advantageous to make use of code snippets as part of the protocol +It is convenient 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](https://github.com/paritytech/parity-scale-codec) codec. -- `DECODE(x: OctetString) -> T`: decodes `x` as a value with type `T` using +- `DECODE(x: OctetString) -> T`: decodes `x` as an object with type `T` using [SCALE](https://github.com/paritytech/parity-scale-codec) codec. -- `BLAKE2(n: Unsigned, x: OctetString) -> OctetString`: standard *Blake2b* hash. +- `BLAKE2(n: Unsigned, x: OctetString) -> OctetString`: standard *Blake2b* hash + with output truncated to n bytes. - `CONCAT(x₀: OctetString, ..., xₖ: OctetString) -> OctetString`: concatenate the - inputs octets. + inputs octets as a new octet string. - `LENGTH(x: Sequence) -> Unsigned`: returns the number of elements in `x`. @@ -132,32 +132,34 @@ 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 +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: +The core protocol operation can be roughly divided into four phases: ### 4.1. Submission of Candidate Tickets 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. +a set of candidate tickets. Every ticket is bundled with an anonymous proof +of validity. ### 4.2. Validation of Candidate Tickets Each candidate ticket undergoes a validation process for the associated validity -proof and compliance with other protocol-specific constraints. +proof and compliance with other protocol-specific constraints. Valid blocks +are persisted on-chain. ### 4.3. Tickets and Slots Binding -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. +After collecting all valid candidate tickets and before the beginning of the +target epoch, a deterministic method is used to uniquely associate a subset of +these tickets with the slots of the target epoch. ### 4.4. Claim of Ticket Ownership -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 production phase of the target epoch, block's author is required +to prove ownership of the ticket associated to the block's slot. This step +discloses the identity of the ticket owners. ## 5. Bandersnatch VRFs Cryptographic Primitives @@ -166,26 +168,26 @@ 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. +within the scope of the protocol. -For an in-depth explanation, refer to the Bandersnatch VRF -[spec](https://github.com/davxy/bandersnatch-vrfs-spec) +For an in-depth explanation, refer to the [Bandersnatch VRF](https://github.com/davxy/bandersnatch-vrfs-spec) +technical specification -Bandersnatch VRF can be used in two flavors: +Bandersnatch VRF comes in two flavors: - *Bare* VRF: extends the IETF ECVRF [RFC 9381](https://datatracker.ietf.org/doc/rfc9381/), - *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`) +flavors offer the capability to sign some arbitrary additional data (*extra*) which doesn't contribute to the VRF output. -### 5.1 Plain VRF Interface +### 5.1 Bare VRF Interface Function to construct a `VrfSignature`. ```rust fn vrf_sign( - secret: BandernatchSecretKey, + secret: SecretKey, input: OctetString, extra: OctetString, ) -> VrfSignature @@ -207,7 +209,7 @@ Function to derive the VRF output from input and secret: ```rust fn vrf_output( - secret: BandernatchSecretKey, + secret: SecretKey, input: OctetString, ) -> OctetString<32>; ``` @@ -227,7 +229,7 @@ Note that the following condition is always satisfied: vrf_output(secret, input) == vrf_signed_output(signature) ``` -In this document, the types `SecretKey`, `PublicKey` and `VrfSignature` are +In this document, `SecretKey`, `PublicKey` and `VrfSignature` types are intentionally left undefined. Their definitions can be found in the Bandersnatch VRF specification and related documents. @@ -238,7 +240,7 @@ Function to construct `RingVrfSignature`. ```rust fn ring_vrf_sign( secret: SecretKey, - prover: RingProverKey, + prover: RingProver, input: OctetString, extra: OctetString, ) -> RingVrfSignature; @@ -246,11 +248,11 @@ Function to construct `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. +Note that verification this doesn't require the signer's public key. ```rust fn ring_vrf_verify( - verifier: RingVerifierKey, + verifier: RingVerifier, input: OctetString, extra: OctetString, signature: RingVrfSignature, @@ -270,20 +272,21 @@ Note that the following condition is always satisfied: ```rust 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); + vrf_signed_output(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. +In this document, the types `RingProver`, `RingVerifier`, and `RingSignature` +are intentionally left undefined. Their definitions can be found in the +Bandersnatch VRF specification and related documents. ## 6. Sassafras Protocol #### 6.1. Protocol Configuration -The `ProtocolConfiguration` is constant and primarily influences certain checks -carried out during tickets validation. It is defined as: +The `ProtocolConfiguration` contains some parameters to tweak the protocol +behavior and primarily influences certain checks carried out during tickets +validation. It is defined as: ```rust ProtocolConfiguration ::= Sequence { @@ -295,10 +298,9 @@ carried out during tickets validation. It is defined as: 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. +- `attempts_number`: maximum number of tickets that each validator 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. + number of valid 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`, @@ -307,15 +309,12 @@ 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](#622-tickets-threshold). +probability can be found in section [6.5.2](#622-tickets-threshold). ### 6.2. Header Digest Log -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: +Each block's header contains a `Digest` log, which is defined as an ordered +sequence of `DigestItem`s: ```rust DigestItem ::= Sequence { @@ -326,7 +325,22 @@ The structures are defined to be quite generic and usable by other subsystems: Digest ::= Sequence ``` -For Sassafras related `DiegestItem`s the `id` is set to the constant ASCII string `"SASS"`. +The `Digest` sequence is used to propagate information required for the +correct protocol progress. The information within each `DigestItem` is opaque +outside the protocol's context and is represented as a SCALE-encoded version of +protocol-specific structures. + +For Sassafras related entries the `DiegestItem`s the `id` is set to the ASCII +string `"SASS"`. + +The possible digest entries for Sassafras are: +- Seal: A signature added by the block author. It is mandatory and must be the + last entry in the log. +- Slot claim info: Additional data required for block verification and is mandatory. +- Epoch change signal: Contains information about the next epoch and is + mandatory for the first block of a new epoch. +- Epoch tickets signal: Contains tickets for claiming slots in the next epoch + and is mandatory for the first block in the *epoch's tail* (more later). ### 6.3. On-Chain Randomness @@ -336,28 +350,33 @@ On-Chain, we maintain a sequence with four randomness entries. RandomnessBuffer ::= Sequence, 4> ``` -During epoch `N` +During epoch `N`: + +- The first entry is the current randomness accumulator value and incorporates + verifiable random elements from all previously executed blocks. The + exact randomness accumulation procedure is described in section + [6.10](#610-randomness-accumulator). -- 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](#67-randomness-accumulator). +- The second entry is the snapshot of the accumulator **before** the execution + of the first block of epoch `N`. This is the randomness to be used by tickets + targeting epoch `N+2`. -- 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 is the snapshot of the accumulator **before** the execution + of the first block of epoch `N-1`. This is the randomness to be used by tickets + targeting epoch `N+1` (the next epoch). -- The third entry of the buffer is the snapshot of the accumulator after the - execution of the last block of epoch `N-2`. +- The third entry is the snapshot of the accumulator **before** the execution + of the first block of epoch `N-2`. This is the randomness to be used by tickets + targeting epoch `N` (the current epoch). -- 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's entries are updated **after** block execution. -The buffer is entries are updated **after** block execution. +### 6.4. Epoch Change Signal -### 6.4. Epoch's First Block +The first block produced during epoch `N` must include a descriptor for some +of the parameters to be used during the subsequent epoch (`N+1`). -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: +This descriptor is defined as: ```rust NextEpochDescriptor ::= Sequence { @@ -367,17 +386,12 @@ of the subsequent epoch (`N+1`) parameters. This descriptor is defined as: ``` 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. +- `randomness`: Randomness accumulator snapshot relevant for validation of + next epoch blocks. In other words, the randomness used to construct the tickets. +- `authorities`: List of validators scheduled for next epoch. -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](#641-startup-parameters). +This descriptor is `SCALE` encoded and embedded in a `DigestItem` of the +`Digest` log. #### 6.4.1. Startup Parameters @@ -391,26 +405,28 @@ the genesis configuration, which is defined as: ``` 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. +block is produced, and its value is set to the Blake2b hash of the genesis +block. Each following entry of the randomness buffer is set to the Blake2b hash +of the previous entry. 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. +`NextEpochDescriptor` for Epoch `#1`: +- `randomness`: randomness for the validation of next epoch slots computed + as post-genesis as specified above. +- `authorities`: the same sequence as specified in the genesis configuration. -### 6.5. Offchain Tickets Creation and Submission +### 6.5. Tickets Creation and Submission -During epoch `N`, each validator associated to epoch `N+2` constructs a set of -tickets which may be eligible ([6.5.2](#652-tickets-threshold)) to be delivered -to on-chain proxies, which are the validators scheduled for epoch `N+1`. +During epoch `N`, each validator enabled for to epoch `N+2` constructs a set +of tickets which may be eligible ([6.5.2](#652-tickets-threshold)) for on-chain +submission via the relayers, 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 +**before** the execution of the first block of epoch `N` 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 @@ -420,12 +436,14 @@ 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. +resources creating tickets that will be unusable if a different chain branch is +chosen as canonical. -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?). +As said, relays collect tickets during epoch `N` and when epoch `N+1` begins +the collected tickets are submitted on-chain. As the relayers are validators, +tickets are submitted on-chain as an "*inherent extrinsic*", a special type +of mandatory transaction inserted at the beginning of the block's transactions +sequence. #### 6.5.1. Ticket Identifier @@ -436,12 +454,12 @@ Each ticket has an associated identifier defined as: ``` The value of the `TicketId` is completely determined by the output of the -Bandersnatch VRF with the following **unbiasable** input: +Bandersnatch VRFs with the following **unbiasable** input: ```rust let ticket_vrf_input = CONCAT( - BYTES("sassafras_ticket"), - GET(randomness_buffer, 1), + BYTES("sassafras_ticket_seal"), + target_randomness, BYTES(attempt_index) ); @@ -449,8 +467,8 @@ Bandersnatch VRF with the following **unbiasable** input: ``` Where: -- `randomness_buffer`: on-chain `RandomnessBuffer` instance, in particular we - use the snapshot after the execution of previous epoch's last block. +- `target_randomness`: element of `RandomnessBuffer` which contains the randomness + for the epoch the ticket is targeting. - `attempt_index`: value going from `0` to the configuration `attempts_number - 1`. #### 6.5.2. Tickets Threshold @@ -501,78 +519,62 @@ For more details about threshold formula please refer to the [probabilities and parameters](https://research.web3.foundation/Polkadot/protocols/block-production/SASSAFRAS#probabilities-and-parameters) paragraph in the Web3 foundation description of the protocol. -#### 6.5.3. Ticket Body +#### 6.5.3. Ticket Envelope -Every ticket candidate has an associated body, defined as: +Every ticket candidate is represented by the `TicketEnvelope`: ```rust - TicketBody ::= Sequence { - attempt_index: Unsigned8, - opaque: OctetString, - } + TicketEnvelope ::= Sequence { + attempt: Unsigned8, + extra: OctetString, + signature: RingVrfSignature + } ``` Where: -- `attempt_index`: index used to generate the associated `TicketId`. -- `opaque`: additional data for user-defined applications. - -#### 6.5.4. Ticket Signature +- `attempt`: index used to keep track of the number of tickets generated by a single authority. +- `extra`: additional data for user-defined applications. +- `signature`: ring signature of the other envelope data. -`TicketBody` must be signed using the Bandersnatch Ring VRF flavor ([5.4.2](#542-ring-vrf-interface)). +Ticket envelope data must be signed using the Bandersnatch Ring VRF flavor ([5.4.2](#542-ring-vrf-interface)). ```rust let signature = ring_vrf_sign( secret_key, - ring_prover_key + ring_prover ticket_vrf_input, - ENCODE(ticket_body), + extra, ); ``` -`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). +With `ticket_vrf_input` defined as in [6.5.1](#651-ticket-identifier). -Finally, the body and the ring signature are combined within the `TicketEnvelope`: - -```rust - TicketEnvelope ::= Sequence { - ticket_body: TicketBody, - ring_signature: RingVrfSignature - } -``` - -### 6.6. Onchain Tickets Validation +### 6.6. On-chain Tickets Validation All the actions in the steps described by this paragraph are executed by on-chain code. Validation rules: -1. Ring signature is verified using the on-chain `ring_verifier_key` derived by the +1. Ring signature is verified using the on-chain `ring_verifier` derived by the static ring context parameters and the next epoch validators public keys. -2. Ticket identifier is locally recomputed from the `RingVrfSignature` and its value +2. Ticket identifier is locally computed from the `RingVrfSignature` and its value is checked to be less than the tickets' threshold. -3. 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. +3. On-chain tickets submission can't occur within a block part of the so called + *epoch's tail*, which encompass a configurable number of the slots at the end + of the epoch. 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 at the beginning of the target epoch. 4. 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. + must end up in the on-chain queue. -5. No duplicates are allowed. +5. No tickets 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: ```rust @@ -594,6 +596,23 @@ Pseudo-code for ticket validation for steps 1 and 2: assert(ticket_id < ticket_threshold); ``` +Valid tickets are persisted on-chain as a bounded sorted sequence of +`TicketBody` objects. Items are sorted according to `TicketId`, interpreted as a +256-bit big-endian unsigned integer. + +```rust + TicketBody ::= Sequence { + id: TicketId, + attempt: Unsigned8, + extra: OctetString, + } + + Tickets ::= Sequence +``` + +The tickets bound is set equal to the epoch length according to the protocol +configuration. + ### 6.7. Ticket-Slot Binding Before the beginning of the claiming phase (i.e. what we've called the target @@ -621,37 +640,37 @@ 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](#682-secondary-method). -Note that this situation always apply to the first epochs after genesis. +Note that this fallback situation always apply to the first two epochs after genesis. ### 6.8. Slot Claim -With tickets bound to epoch slots, every validator acquires information about -the slots for which they are supposed to produce a block. +With tickets bound to epoch slots, every designed 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. +If a slot is associated to a ticket, then the primary authoring method is used. Conversely, the protocol resorts to the secondary method as a fallback. #### 6.8.1. Primary Method -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. +An authority, can 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: +Let `target_randomness` be the entry in `RandomnessBuffer` relative to the epoch +the block is targeting and `ticket_body` be the `TicketBody` that is associated +to the slot to claim, the VRF input for slot claiming is constructed as: ```rust let seal_vrf_input = CONCAT( - BYTES("sassafras_ticket"), - GET(randomness_buffer, 3), - BYTES(ticket_body.attempt_index) + BYTES("sassafras_ticket_seal"), + target_randomness, + BYTES(attempt_index) ); ``` -This `seal_vrf_input`, when signed with the correct validator secret key must +The `seal_vrf_input`, when signed with the correct validator secret key, must generate the same `TicketId` associated on-chain to the target slot. #### 6.8.2. Secondary Method @@ -662,7 +681,7 @@ is given by the following procedure: ```rust let hash_input = CONCAT( - GET(randomness_buffer, 2), + target_randomness, relative_slot_index, ); let hash = BLAKE2(hash_input); @@ -670,16 +689,16 @@ is given by the following procedure: let index = DECODE(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. +With `relative_slot_index` the slot offset relative to the target epoch's start +and `authorities` the `Sequence` of target 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: ```rust let seal_vrf_input = CONCAT( - BYTES("sassafras_fallback"), - GET(randomness_buffer, 3), + BYTES("sassafras_fallback_seal"), + target_randomness ); ``` @@ -696,12 +715,12 @@ which is required by the protocol in order to verify the block: } ``` -- `slot`: the slot number -- `validator_index`: block's author index relative to the on-chain validators sequence. +- `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: +the randomness source signature is generated as follows: ```rust let randomness_vrf_input = CONCAT( @@ -715,21 +734,24 @@ the claim is derived as follows: [] ); - let claim = ClaimData { + let claim = SlotClaim { slot, validator_index, - randomness_source, - } + randomness_source + }; + + PUSH(block_header.digest, ENCODE(claim)); ``` -The `claim` object is *SCALE* encoded and pushed into the header digest log. +The `ClaimData` instance is *SCALE* encoded and pushed as the second-to-last +element of the header digest log. #### 6.8.4. Block Seal -A block is sealed as follows: +A block is finally sealed as follows: ```rust - let unsealed_header_bytes = ENCODE(header); + let unsealed_header_bytes = ENCODE(block_header); let seal = vrf_sign( AUTHORITY_SECRET_KEY, @@ -737,13 +759,13 @@ A block is sealed as follows: unsealed_header_bytes ); - PUSH(header.digest, ENCODE(seal)); + PUSH(block_header.digest, ENCODE(seal)); ``` -With `header` the block's header without the seal digest log entry. +With `block_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 `seal` object is thus a `VrfSignature` instance, which is *SCALE* encoded and +pushed as the **last** entry of the block's header digest log. ### 6.9. Slot Claim Verification @@ -754,10 +776,10 @@ 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 +The validity of the signatures are 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. +additional data used by the block author. ```rust let seal_signature = DECODE(POP(header.digest)); @@ -766,29 +788,31 @@ additional data expected to have been used by the block author. let public_key = GET(authorities, claim_data.validator_index); + // Verify seal signature let result = vrf_verify( public_key, seal_vrf_input, unsealed_header_bytes, seal_signature ); - assert(result == 1); + if result != 1 { return Err }; let randomness_vrf_input = vrf_signed_output(seal_signature); + // Verify per-block entropy source signature let result = vrf_verify( public_key, randomness_vrf_input, [], claim_data.randomness_source ); - assert(result == 1); + if result != 1 { return Err }; ``` 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](#68-slot-claiming). +- `header`: The block's header. +- `authorities`: Sequence of authorities for the target epoch, as recorded on-chain. +- `seal_vrf_input`: VRF input data constructed as specified in [6.8](#68-slot-claim). 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 @@ -804,13 +828,13 @@ This method verifies ticket ownership using the `TicketId` associated to the slo assert(ticket_id == expected_ticket_id); ``` -With `expected_ticket_id` the ticket identifier committed on-chain together -with the associated `ticket_body`. +With `expected_ticket_id` the ticket identifier committed on-chain within the +associated `TicketBody`. #### 6.9.2. Secondary Method 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 +the `ClaimData` must match the one given by the procedure outlined in section [6.8.2](#682-secondary-method). ### 6.10. Randomness Accumulator @@ -915,7 +939,7 @@ protocol, several crucial topics remain to be addressed in future RFCs. ### 12.4. Anonymous Submission of Tickets. -- **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. +- **Mixnet Integration**: Submitting tickets directly to the relay 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. From 3abba554bbf6a71e094ac9f6cc74309ca06d3c1a Mon Sep 17 00:00:00 2001 From: Davide Galassi Date: Mon, 10 Jun 2024 20:06:52 +0200 Subject: [PATCH 25/26] Nitpicks --- text/0026-sassafras-consensus.md | 335 +++++++++++++++---------------- 1 file changed, 165 insertions(+), 170 deletions(-) diff --git a/text/0026-sassafras-consensus.md b/text/0026-sassafras-consensus.md index bdcaf6b45..3b9dbe785 100644 --- a/text/0026-sassafras-consensus.md +++ b/text/0026-sassafras-consensus.md @@ -13,7 +13,7 @@ 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 +authorities set while ensuring that the identity of authorities assigned to the slots remains undisclosed until the slot is actively claimed during block production. @@ -45,7 +45,7 @@ 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. +greater Polkadot ecosystem. ## 2. Stakeholders @@ -71,11 +71,11 @@ document to ensure clarity and consistency. ### 3.1. Data Structures Definitions -Data structures are primarily defined using standard [ASN.1](https://en.wikipedia.org/wiki/ASN.1), +Data structures are mostly defined using standard [ASN.1](https://en.wikipedia.org/wiki/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. +must match the definitions found within this specification. ### 3.2. Types Alias @@ -92,7 +92,6 @@ We define some types alias to make ASN.1 syntax more intuitive. - Fixed length homogeneous sequence (array): `Sequence ::= Sequence (SIZE(n))` - Octet string alias: `OctetString ::= Sequence` - Fixed length octet string: `OctetString ::= Sequence` -- Optional value: `Option ::= T OPTIONAL` ### 3.2. Pseudo-Code @@ -100,25 +99,25 @@ It is convenient 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: -- `ENCODE(x: T) -> OctetString`: encodes `x` as an `OctetString` using +- `ENCODE(x: T) -> OctetString`: Encodes `x` as an `OctetString` according to [SCALE](https://github.com/paritytech/parity-scale-codec) codec. -- `DECODE(x: OctetString) -> T`: decodes `x` as an object with type `T` using - [SCALE](https://github.com/paritytech/parity-scale-codec) codec. +- `DECODE(x: OctetString) -> T`: Decodes `x` as a type `T` object according + to [SCALE](https://github.com/paritytech/parity-scale-codec) codec. -- `BLAKE2(n: Unsigned, x: OctetString) -> OctetString`: standard *Blake2b* hash - with output truncated to n bytes. +- `BLAKE2(n: Unsigned, x: OctetString) -> OctetString`: Standard *Blake2b* hash + of `x` with output truncated to `n` bytes. -- `CONCAT(x₀: OctetString, ..., xₖ: OctetString) -> OctetString`: concatenate the +- `CONCAT(x₀: OctetString, ..., xₖ: OctetString) -> OctetString`: Concatenate the inputs octets as a new octet string. -- `LENGTH(x: Sequence) -> Unsigned`: returns the number of elements in `x`. +- `LENGTH(s: Sequence) -> Unsigned`: The number of elements in the sequence `s`. -- `GET(seq: Sequence, i: Unsigned) -> T`: returns the i-th element of a sequence. +- `GET(s: Sequence, i: Unsigned) -> T`: The `i`-th element of the sequence `s`. -- `PUSH(seq: Sequence, x: T)`: append `x` as the new last element of the sequence. +- `PUSH(s: Sequence, x: T)`: Appends `x` as the new last element of the sequence `s`. -- `POP(seq: Sequence) -> T`: extract and returns the last element of a sequence. +- `POP(s: Sequence) -> T`: extract and returns the last element of the sequence `s`. ### 3.3. Incremental Introduction of Types and Functions @@ -132,24 +131,24 @@ 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**. -Sassafras protocol aims to map each slot within an epoch to the designated -validators for that epoch, utilizing a ticketing system. +Sassafras protocol aims to map each slot within a *target* epoch to the +designated authorities for that epoch, utilizing a ticketing system. -The core protocol operation can be roughly divided into four phases: +The core protocol operation can be roughly divided into four phases. ### 4.1. Submission of Candidate Tickets -Each of the validators associated to the target epoch generates and submits -a set of candidate tickets. Every ticket is bundled with an anonymous proof -of validity. +Each of the authorities scheduled for the target epoch generate and submits +a set of candidate tickets. Every ticket has an unbiasable pseudo random score +and is bundled with an anonymous proof of validity. ### 4.2. Validation of Candidate Tickets Each candidate ticket undergoes a validation process for the associated validity -proof and compliance with other protocol-specific constraints. Valid blocks +proof and compliance with other protocol-specific constraints. Valid tickets are persisted on-chain. -### 4.3. Tickets and Slots Binding +### 4.3. Tickets Slots Binding After collecting all valid candidate tickets and before the beginning of the target epoch, a deterministic method is used to uniquely associate a subset of @@ -159,23 +158,22 @@ these tickets with the slots of the target epoch. During block production phase of the target epoch, block's author is required to prove ownership of the ticket associated to the block's slot. This step -discloses the identity of the ticket owners. +discloses the identity of the ticket owner. ## 5. Bandersnatch VRFs Cryptographic Primitives -It's important to note that this section is not intended to serve as an +It is 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 the protocol. - -For an in-depth explanation, refer to the [Bandersnatch VRF](https://github.com/davxy/bandersnatch-vrfs-spec) +within the scope of the protocol. For a more detailed explanation, refer to +the [Bandersnatch VRF](https://github.com/davxy/bandersnatch-vrfs-spec) technical specification Bandersnatch VRF comes in two flavors: -- *Bare* VRF: extends the IETF ECVRF [RFC 9381](https://datatracker.ietf.org/doc/rfc9381/), -- *Ring* VRF: provides anonymous signatures by leveraging a *zk-SNARK*. +- *Bare* VRF: Extension to the IETF ECVRF [RFC 9381](https://datatracker.ietf.org/doc/rfc9381/), +- *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*) @@ -205,7 +203,7 @@ validity of the signature (`1` on success): ) -> Unsigned<1>; ``` -Function to derive the VRF output from input and secret: +Function to derive the VRF *output* from *input* and *secret*: ```rust fn vrf_output( @@ -214,7 +212,7 @@ Function to derive the VRF output from input and secret: ) -> OctetString<32>; ``` -Function to derive the VRF output from a signature: +Function to derive the VRF *output* from a *signature*: ```rust fn vrf_signed_output( @@ -231,7 +229,7 @@ Note that the following condition is always satisfied: In this document, `SecretKey`, `PublicKey` and `VrfSignature` types are intentionally left undefined. Their definitions can be found in the Bandersnatch -VRF specification and related documents. +VRF specification. #### 5.4.2. Ring VRF Interface @@ -248,7 +246,7 @@ Function to construct `RingVrfSignature`. Function for signature verification returning a Boolean value indicating the validity of the signature (`1` on success). -Note that verification this doesn't require the signer's public key. +Note that verification doesn't require the signer's public key. ```rust fn ring_vrf_verify( @@ -259,7 +257,7 @@ Note that verification this doesn't require the signer's public key. ) -> Unsigned<1>; ``` -Function to derive the VRF output from a ring signature: +Function to derive the VRF *output* from a ring *signature*: ```rust fn ring_vrf_signed_output( @@ -275,7 +273,7 @@ Note that the following condition is always satisfied: vrf_signed_output(signature) == ring_vrf_signed_output(ring_signature); ``` -In this document, the types `RingProver`, `RingVerifier`, and `RingSignature` +In this document, the types `RingProver`, `RingVerifier`, and `RingVrfSignature` are intentionally left undefined. Their definitions can be found in the Bandersnatch VRF specification and related documents. @@ -284,9 +282,9 @@ Bandersnatch VRF specification and related documents. #### 6.1. Protocol Configuration -The `ProtocolConfiguration` contains some parameters to tweak the protocol -behavior and primarily influences certain checks carried out during tickets -validation. It is defined as: +The `ProtocolConfiguration` type contains some parameters to tweak the +protocol behavior and primarily influences certain checks carried out during +tickets validation. It is defined as: ```rust ProtocolConfiguration ::= Sequence { @@ -298,9 +296,9 @@ validation. It is defined as: Where: - `epoch_length`: number of slots for each epoch. -- `attempts_number`: maximum number of tickets that each validator allowed to submit. +- `attempts_number`: maximum number of tickets that each authority is allowed to submit. - `redundancy_factor`: expected ratio between epoch's slots and the cumulative - number of valid tickets which can be submitted by the set of epoch validators. + number of valid tickets which can be submitted by the set of epoch authorities. The `attempts_number` influences the anonymity of block producers. As all published tickets have a **public** attempt number less than `attempts_number`, @@ -309,7 +307,7 @@ 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.5.2](#622-tickets-threshold). +probability can be found in section [6.5.2](#652-tickets-threshold). ### 6.2. Header Digest Log @@ -330,21 +328,25 @@ correct protocol progress. The information within each `DigestItem` is opaque outside the protocol's context and is represented as a SCALE-encoded version of protocol-specific structures. -For Sassafras related entries the `DiegestItem`s the `id` is set to the ASCII +For Sassafras related entries, the `DiegestItem`s `id` is set to the ASCII string `"SASS"`. -The possible digest entries for Sassafras are: -- Seal: A signature added by the block author. It is mandatory and must be the - last entry in the log. -- Slot claim info: Additional data required for block verification and is mandatory. -- Epoch change signal: Contains information about the next epoch and is +Possible digest entries for Sassafras: +- Epoch change signal: Contains information about the next epoch. This is mandatory for the first block of a new epoch. -- Epoch tickets signal: Contains tickets for claiming slots in the next epoch - and is mandatory for the first block in the *epoch's tail* (more later). +- Epoch tickets signal: Contains the sequence of tickets for claiming slots + in the next epoch. This is mandatory for the first block in the *epoch's tail* +- Slot claim info: Additional data required for block verification. This is mandatory + and must be the second-to-last entry in the log. +- Seal: Block signature added by the block author. This is mandatory and must be + the last entry in the log. + +If any of the digest entries are found in the wrong place or in a block where +they are not specified as mandatory, then the block is considered invalid. ### 6.3. On-Chain Randomness -On-Chain, we maintain a sequence with four randomness entries. +On-Chain, we maintain a sequence of four randomness entries. ```rust RandomnessBuffer ::= Sequence, 4> @@ -358,15 +360,15 @@ During epoch `N`: [6.10](#610-randomness-accumulator). - The second entry is the snapshot of the accumulator **before** the execution - of the first block of epoch `N`. This is the randomness to be used by tickets + of the first block of epoch `N`. This is the randomness to be used for tickets targeting epoch `N+2`. - The third entry is the snapshot of the accumulator **before** the execution - of the first block of epoch `N-1`. This is the randomness to be used by tickets + of the first block of epoch `N-1`. This is the randomness to be used for tickets targeting epoch `N+1` (the next epoch). - The third entry is the snapshot of the accumulator **before** the execution - of the first block of epoch `N-2`. This is the randomness to be used by tickets + of the first block of epoch `N-2`. This is the randomness to be used for tickets targeting epoch `N` (the current epoch). The buffer's entries are updated **after** block execution. @@ -374,7 +376,7 @@ The buffer's entries are updated **after** block execution. ### 6.4. Epoch Change Signal The first block produced during epoch `N` must include a descriptor for some -of the parameters to be used during the subsequent epoch (`N+1`). +of the parameters to be used by the subsequent epoch (`N+1`). This descriptor is defined as: @@ -387,15 +389,16 @@ This descriptor is defined as: Where: - `randomness`: Randomness accumulator snapshot relevant for validation of - next epoch blocks. In other words, the randomness used to construct the tickets. -- `authorities`: List of validators scheduled for next epoch. + next epoch blocks. In other words, the randomness used to construct the tickets + targeting epoch `N+1`. +- `authorities`: List of authorities scheduled for next epoch. This descriptor is `SCALE` encoded and embedded in a `DigestItem` of the `Digest` log. #### 6.4.1. Startup Parameters -Some of the initial parameters for the first epoch, Epoch `#0`, are set through +Some of the initial parameters by the first epoch (`#0`), are set through the genesis configuration, which is defined as: ```rust @@ -404,46 +407,44 @@ the genesis configuration, which is defined as: } ``` -The on-chain randomness accumulator is initialized only **after** the genesis -block is produced, and its value is set to the Blake2b hash of the genesis -block. Each following entry of the randomness buffer is set to the Blake2b hash -of the previous entry. +The on-chain `RandomnessBuffer` is initialized **after** the genesis block. +The first entry is set as the *Blake2b* hash of the genesis block, each of +the following entry is set as the *Blake2b* hash of the previous entry. -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`. +Since block `#0` is generated by each node as part of the genesis process, the +first block that an authority explicitly produces for epoch `#0` is block `#1`. +Therefore, block `#1` is required to contain the `NextEpochDescriptor` for the +following epoch. -`NextEpochDescriptor` for Epoch `#1`: -- `randomness`: randomness for the validation of next epoch slots computed - as post-genesis as specified above. -- `authorities`: the same sequence as specified in the genesis configuration. +`NextEpochDescriptor` for epoch `#1`: +- `randomness`: Third entry (index 2) of the randomness buffer. +- `authorities`: The same sequence as specified in the genesis configuration. ### 6.5. Tickets Creation and Submission -During epoch `N`, each validator enabled for to epoch `N+2` constructs a set +During epoch `N`, each authority scheduled for epoch `N+2` constructs a set of tickets which may be eligible ([6.5.2](#652-tickets-threshold)) for on-chain -submission via the relayers, which are the validators scheduled for epoch `N+1`. +submission via the relayers, which are the authorities scheduled for epoch `N+1`. These tickets are constructed using the on-chain randomness snapshot taken **before** the execution of the first block of epoch `N` 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 +Each authority 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 +The ideal timing for the candidate authority 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 will be unusable if a different chain branch is chosen as canonical. -As said, relays collect tickets during epoch `N` and when epoch `N+1` begins -the collected tickets are submitted on-chain. As the relayers are validators, -tickets are submitted on-chain as an "*inherent extrinsic*", a special type -of mandatory transaction inserted at the beginning of the block's transactions -sequence. +As said, during epoch `N`, tickets relayers collect (offchain) tickets targeting +epoch `N+2`. When epoch `N+1` starts, the collected tickets are submitted +on-chain by relayers (which are the authorities scheduled for epoch `N+1`) as +"*inherent extrinsic*"s, a special type of mandatory transaction inserted by the +block author at the beginning of the block's transactions sequence. #### 6.5.1. Ticket Identifier @@ -453,14 +454,14 @@ Each ticket has an associated identifier defined as: TicketId ::= OctetString<32>; ``` -The value of the `TicketId` is completely determined by the output of the -Bandersnatch VRFs with the following **unbiasable** input: +The value of `TicketId` is completely determined by the output of Bandersnatch +VRFs with the following **unbiasable** input: ```rust let ticket_vrf_input = CONCAT( BYTES("sassafras_ticket_seal"), target_randomness, - BYTES(attempt_index) + BYTES(attempt) ); let ticket_id = vrf_output(AUTHORITY_SECRET_KEY, ticket_vrf_input); @@ -469,7 +470,7 @@ Bandersnatch VRFs with the following **unbiasable** input: Where: - `target_randomness`: element of `RandomnessBuffer` which contains the randomness for the epoch the ticket is targeting. -- `attempt_index`: value going from `0` to the configuration `attempts_number - 1`. +- `attempt`: value going from `0` to the configured `attempts_number - 1`. #### 6.5.2. Tickets Threshold @@ -480,7 +481,7 @@ is less than the ticket threshold computed as: T = (r·s)/(a·v) Where: -- `v`: epoch's validators number +- `v`: epoch's authorities number - `s`: epoch's slots number - `r`: redundancy factor - `a`: attempts number @@ -496,9 +497,9 @@ 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`. +Let `n` be the actual number of participating authorities, where `v·2/3 ≤ n ≤ v`. -These `n` validators each make `a` attempts, for a total of `a·n` attempts. +These `n` authorities 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: @@ -521,7 +522,7 @@ paragraph in the Web3 foundation description of the protocol. #### 6.5.3. Ticket Envelope -Every ticket candidate is represented by the `TicketEnvelope`: +Each ticket candidate is represented by a `TicketEnvelope`: ```rust TicketEnvelope ::= Sequence { @@ -532,11 +533,11 @@ Every ticket candidate is represented by the `TicketEnvelope`: ``` Where: -- `attempt`: index used to keep track of the number of tickets generated by a single authority. +- `attempt`: Index associated to the ticket. - `extra`: additional data for user-defined applications. -- `signature`: ring signature of the other envelope data. +- `signature`: ring signature of the envelope data. -Ticket envelope data must be signed using the Bandersnatch Ring VRF flavor ([5.4.2](#542-ring-vrf-interface)). +The envelope data must be signed using Bandersnatch Ring VRF ([5.4.2](#542-ring-vrf-interface)). ```rust let signature = ring_vrf_sign( @@ -556,14 +557,14 @@ on-chain code. Validation rules: -1. Ring signature is verified using the on-chain `ring_verifier` derived by the - static ring context parameters and the next epoch validators public keys. +1. Ring signature is verified using the `ring_verifier` derived by the static + ring context parameters and the next epoch authorities public keys. -2. Ticket identifier is locally computed from the `RingVrfSignature` and its value - is checked to be less than the tickets' threshold. +2. `TicketId` is locally computed from the `RingVrfSignature` and its value + is checked to be less than tickets' threshold. -3. On-chain tickets submission can't occur within a block part of the so called - *epoch's tail*, which encompass a configurable number of the slots at the end +3. On-chain tickets submission can't occur within a block part of the + *epoch's tail*, which encompasses a configurable number of the slots at the end of the epoch. 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 at the beginning of the target epoch. @@ -573,32 +574,32 @@ Validation rules: 5. No tickets duplicates are allowed. -If at least one of the checks fails then the block must be discarded. +If at least one of the checks fails then the block must be considered invalid. Pseudo-code for ticket validation for steps 1 and 2: ```rust let ticket_vrf_input = CONCAT( - BYTES("sassafras_ticket"), - GET(randomness_buffer, 2), - BYTES(envelope.body.attempt_index) + BYTES("sassafras_ticket_seal"), + target_randomness, + BYTES(envelope.attempt) ); let result = ring_vrf_verify( verifier, ticket_vrf_input, - ENCODE(ticket_body), + envelope.extra, envelope.ring_signature ); - assert(result == 1); + ASSERT(result == 1); let ticket_id = ring_vrf_signed_output(envelope.ring_signature); - assert(ticket_id < ticket_threshold); + ASSERT(ticket_id < ticket_threshold); ``` -Valid tickets are persisted on-chain as a bounded sorted sequence of -`TicketBody` objects. Items are sorted according to `TicketId`, interpreted as a -256-bit big-endian unsigned integer. +Valid tickets are persisted on-chain in a bounded sorted sequence of +`TicketBody` objects. Items within this sequence are sorted according to +their `TicketId`, interpreted as a 256-bit big-endian unsigned integer. ```rust TicketBody ::= Sequence { @@ -610,74 +611,71 @@ Valid tickets are persisted on-chain as a bounded sorted sequence of Tickets ::= Sequence ``` -The tickets bound is set equal to the epoch length according to the protocol -configuration. +The on-chain tickets sequence bound is set as the epoch length according to the +protocol configuration. ### 6.7. Ticket-Slot Binding 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. +epoch), the on-chain list of tickets must be associated to the next epoch's +slots such that there is 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: +Given an ordered sequence of tickets `[t₀, t₁, ..., tₙ]`, the tickets are +associated 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: +Here `slot-index` is a relative value computed as: `slot_index = slot - epoch_start_slot`. - slot_index = slot - epoch_start_slot - -The association between each ticket and a slot is recorded on-chain and thus +The association between tickets and a slots 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. +consequently, who is enabled to claim the corresponding slot. This information +is known only to the ticket's author. -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](#682-secondary-method). +If the number of published tickets is less than the number of epoch's slots, +some *orphan* slots at the end of the epoch will remain unbounded to any ticket. +For *orphan* slots claiming strategy refer to [6.8.2](#682-secondary-method). Note that this fallback situation always apply to the first two epochs after genesis. ### 6.8. Slot Claim -With tickets bound to epoch slots, every designed validator acquires information -about the slots for which they are supposed to produce a block. +With tickets bounded to the target epoch slots, every designated authority +acquires the information about the slots for which they are required 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 to a ticket, then the primary authoring method is used. -Conversely, the protocol resorts to the secondary method as a fallback. +associated ticket according to the on-chain state. If a slot has an associated +ticket, then the primary authoring method is used. Conversely, the protocol +resorts to the secondary method as a fallback. #### 6.8.1. Primary Method -An authority, can claim a slot using the primary method if we are the legit +An authority, can claim a slot using the primary method if it is the legit owner of the ticket associated to the given slot. Let `target_randomness` be the entry in `RandomnessBuffer` relative to the epoch -the block is targeting and `ticket_body` be the `TicketBody` that is associated -to the slot to claim, the VRF input for slot claiming is constructed as: +the block is targeting and `attempt` be the attempt used to construct the ticket +associated to the slot to claim, the VRF input for slot claiming is constructed as: ```rust let seal_vrf_input = CONCAT( BYTES("sassafras_ticket_seal"), target_randomness, - BYTES(attempt_index) + BYTES(attempt) ); ``` -The `seal_vrf_input`, when signed with the correct validator secret key, must +The `seal_vrf_input`, when signed with the correct authority secret key, must generate the same `TicketId` associated on-chain to the target slot. #### 6.8.2. Secondary Method -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: +Given that the authorities registered on-chain are kept on-chain in an ordered +list, the index of the authority which has the privilege to claim an *orphan* +slot is given by the following procedure: ```rust let hash_input = CONCAT( @@ -690,10 +688,10 @@ is given by the following procedure: ``` With `relative_slot_index` the slot offset relative to the target epoch's start -and `authorities` the `Sequence` of target epoch validators. +and `authorities` the sequence of target epoch authorities. Let `randomness_buffer` be the instance of `RandomnessBuffer` stored in on-chain state -then the VRF input for slot claiming is constructed as: +then the VRF input for secondary slot claiming is constructed as: ```rust let seal_vrf_input = CONCAT( @@ -704,19 +702,19 @@ then the VRF input for slot claiming is constructed as: #### 6.8.3. Claim Data -The slot claim data is a digest entry which contains additional information -which is required by the protocol in order to verify the block: +`ClaimData` is a digest entry which contains additional information required by +the protocol to verify the block: ```rust ClaimData ::= Sequence { slot: Unsigned32, - validator_index: Unsigned32, + authority_index: Unsigned32, randomness_source: VrfSignature, } ``` - `slot`: The slot number -- `validator_index`: Block's author index relative to the on-chain validators sequence. +- `authority_index`: Block's author index relative to the on-chain authorities sequence. - `randomness_source`: VRF signature used to generate per-block randomness. Given the `seal_vrf_input` constructed using the primary or secondary method, @@ -736,7 +734,7 @@ the randomness source signature is generated as follows: let claim = SlotClaim { slot, - validator_index, + authority_index, randomness_source }; @@ -751,12 +749,10 @@ element of the header digest log. A block is finally sealed as follows: ```rust - let unsealed_header_bytes = ENCODE(block_header); - let seal = vrf_sign( AUTHORITY_SECRET_KEY, seal_vrf_input, - unsealed_header_bytes + ENCODE(block_header), ); PUSH(block_header.digest, ENCODE(seal)); @@ -764,29 +760,29 @@ A block is finally sealed as follows: With `block_header` the block's header without the seal digest log entry. -The `seal` object is thus a `VrfSignature` instance, which is *SCALE* encoded and -pushed as the **last** entry of the block's header digest log. +The `seal` object is a `VrfSignature`, which is *SCALE* encoded and pushed as +the **last** entry of the block's header digest log. ### 6.9. Slot Claim Verification -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 last entry is extracted from the header digest log, and is SCALE decoded as +a `VrfSignature` object. 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 next entry is extracted from the header digest log, and is SCALE decoded as +a `ClaimData` object. -The validity of the signatures are 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 used by the block author. +The validity of the two signatures is assessed using as the authority public key +corresponding to the `authority_index` found in the `ClaimData`, together with +the VRF input (which depends on primary/secondary method) and additional data +used by the block author. ```rust let seal_signature = DECODE(POP(header.digest)); let unsealed_header_bytes = ENCODE(header); let claim_data = DECODE(POP(header.digest)); - let public_key = GET(authorities, claim_data.validator_index); + let public_key = GET(authorities, claim_data.authority_index); // Verify seal signature let result = vrf_verify( @@ -795,7 +791,7 @@ additional data used by the block author. unsealed_header_bytes, seal_signature ); - if result != 1 { return Err }; + ASSERT(result == 1); let randomness_vrf_input = vrf_signed_output(seal_signature); @@ -806,7 +802,7 @@ additional data used by the block author. [], claim_data.randomness_source ); - if result != 1 { return Err }; + ASSERT(result == 1); ``` With: @@ -825,16 +821,16 @@ This method verifies ticket ownership using the `TicketId` associated to the slo ```rust let ticket_id = vrf_signed_output(seal_signature); - assert(ticket_id == expected_ticket_id); + ASSERT(ticket_id == expected_ticket_id); ``` -With `expected_ticket_id` the ticket identifier committed on-chain within the +With `expected_ticket_id` the ticket identifier committed on-chain in the associated `TicketBody`. #### 6.9.2. Secondary Method -If the slot doesn't have any associated ticket then the validator index contained in -the `ClaimData` must match the one given by the procedure outlined in section +If the slot doesn't have any associated ticket then the `authority_index` contained in +the `ClaimData` must match the one returned by the procedure outlined in section [6.8.2](#682-secondary-method). ### 6.10. Randomness Accumulator @@ -847,10 +843,7 @@ execution as follows: ```rust 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); + randomness_buffer[0] = BLAKE2(CONCAT(randomness_buffer[0], fresh_randomness)); ``` @@ -914,6 +907,8 @@ protocol, several crucial topics remain to be addressed in future RFCs. ### 12.1. Interactions with On-Chain Code +- **Storage** organization and static configuration. + - **Outbound Interfaces**: Interfaces that the host environment provides to the on-chain code, typically known as *Host Functions*. @@ -930,9 +925,9 @@ protocol, several crucial topics remain to be addressed in future RFCs. already operational instance of another protocol. Future RFCs may focus on deployment strategies to facilitate a smooth transition. -### 12.3. ZK-SNARK URS Initialization +### 12.3. ZK-SNARK SRS -- **Procedure**: Determining the procedure for the *zk-SNARK* URS (Universal +- **Procedure**: Determining the procedure for the *zk-SNARK* SRS (Structured 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). From 6cc45d5577f82afb330b16ea84c48a8e2ca3738a Mon Sep 17 00:00:00 2001 From: Davide Galassi Date: Tue, 18 Jun 2024 18:04:04 +0200 Subject: [PATCH 26/26] Ready to go --- text/0026-sassafras-consensus.md | 468 +++++++++++++++---------------- 1 file changed, 231 insertions(+), 237 deletions(-) diff --git a/text/0026-sassafras-consensus.md b/text/0026-sassafras-consensus.md index 3b9dbe785..2aceff37f 100644 --- a/text/0026-sassafras-consensus.md +++ b/text/0026-sassafras-consensus.md @@ -20,9 +20,9 @@ production. ## 1. Motivation -Sassafras Protocol has been rigorously detailed in a comprehensive +Sassafras Protocol has been rigorously described in a comprehensive [research paper](https://eprint.iacr.org/2023/031.pdf) authored by the -[Web3 foundation](https://web3.foundation) research team. +[Web3 Foundation](https://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 @@ -41,16 +41,18 @@ interoperability. ### 1.2. Supporting Sassafras for Polkadot Beyond promoting interoperability, this RFC also aims to facilitate the -implementation of Sassafras within the Polkadot ecosystem. +implementation of Sassafras within the greater 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 -greater Polkadot ecosystem. +document, it lays the groundwork for the integration of Sassafras. ## 2. Stakeholders -### 2.1. Blockchain Developers +The protocol has a central role in the next generation block authoring consensus +systems. + +### 2.1. Blockchain Core Developers Developers responsible for creating blockchains who intend to leverage the benefits offered by the Sassafras Protocol. @@ -60,18 +62,15 @@ 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. - ## 3. Notation -This section outlines the notation and conventions adopted throughout this -document to ensure clarity and consistency. +This section outlines the notation adopted throughout this document to ensure +clarity and consistency. ### 3.1. Data Structures Definitions -Data structures are mostly defined using standard [ASN.1](https://en.wikipedia.org/wiki/ASN.1), +Data structures are mostly defined using standard [ASN.1](https://www.itu.int/en/ITU-T/asn1/Pages/introduction.aspx) syntax with few exceptions. To ensure interoperability of serialized structures, the order of the fields @@ -79,37 +78,27 @@ must match the definitions found within this specification. ### 3.2. Types Alias -We define some types alias to make ASN.1 syntax more intuitive. - - Unsigned integer: `Unsigned ::= INTEGER (0..MAX)` -- n bits unsigned integer: `Unsigned ::= 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>` +- n-bit unsigned integer: `Unsigned ::= INTEGER (0..2^n - 1)` + - 8-bit unsigned integer (octet) `Unsigned8 ::= Unsigned<8>` + - 32-bit unsigned integer: `Unsigned32 ::= Unsigned<32>` + - 64-bit unsigned integer: `Unsigned64 ::= Unsigned<64>` - Non-homogeneous sequence (struct/tuple): `Sequence ::= SEQUENCE` -- Homogeneous sequence (vector): `Sequence ::= SEQUENCE OF T` - E.g. `Sequence ::= SEQUENCE OF Unsigned` +- Variable length homogeneous sequence (vector): `Sequence ::= SEQUENCE OF T` - Fixed length homogeneous sequence (array): `Sequence ::= Sequence (SIZE(n))` -- Octet string alias: `OctetString ::= Sequence` -- Fixed length octet string: `OctetString ::= Sequence` +- Variable length octet-string: `OctetString ::= Sequence` +- Fixed length octet-string: `OctetString ::= Sequence` ### 3.2. Pseudo-Code It is convenient 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: - -- `ENCODE(x: T) -> OctetString`: Encodes `x` as an `OctetString` according to - [SCALE](https://github.com/paritytech/parity-scale-codec) codec. - -- `DECODE(x: OctetString) -> T`: Decodes `x` as a type `T` object according - to [SCALE](https://github.com/paritytech/parity-scale-codec) codec. +*Rust*, and can make use of the following set of predefined procedures: -- `BLAKE2(n: Unsigned, x: OctetString) -> OctetString`: Standard *Blake2b* hash - of `x` with output truncated to `n` bytes. +#### Sequences -- `CONCAT(x₀: OctetString, ..., xₖ: OctetString) -> OctetString`: Concatenate the - inputs octets as a new octet string. +- `CONCAT(x₀: OctetString, ..., xₖ: OctetString) -> OctetString`: Concatenates the + input octet-strings as a new octet string. - `LENGTH(s: Sequence) -> Unsigned`: The number of elements in the sequence `s`. @@ -119,6 +108,19 @@ description. As a convention, the code is formatted in a style similar to - `POP(s: Sequence) -> T`: extract and returns the last element of the sequence `s`. +#### Codec + +- `ENCODE(x: T) -> OctetString`: Encodes `x` as an `OctetString` according to + [SCALE](https://github.com/paritytech/parity-scale-codec) codec. + +- `DECODE(x: OctetString) -> T`: Decodes `x` as a type `T` object according + to [SCALE](https://github.com/paritytech/parity-scale-codec) codec. + +#### Other + +- `BLAKE2(x: OctetString) -> OctetString<32>`: Standard *Blake2b* hash + of `x` with 256-bit digest. + ### 3.3. Incremental Introduction of Types and Functions More types and helper functions are introduced incrementally as they become @@ -128,19 +130,19 @@ relevant within the document's context. ## 4. Protocol Introduction The timeline is segmented into a sequentially ordered sequence of **slots**. -This entire sequence of slots is then further partitioned into distinct segments +This entire sequence of slots is further partitioned into distinct segments known as **epochs**. -Sassafras protocol aims to map each slot within a *target* epoch to the -designated authorities for that epoch, utilizing a ticketing system. +Sassafras aims to map each slot within a *target epoch* to the authorities +scheduled for that epoch, utilizing a ticketing system. The core protocol operation can be roughly divided into four phases. ### 4.1. Submission of Candidate Tickets -Each of the authorities scheduled for the target epoch generate and submits -a set of candidate tickets. Every ticket has an unbiasable pseudo random score -and is bundled with an anonymous proof of validity. +Each authority scheduled for the target epoch generates and shares a set of +candidate tickets. Every ticket has an *unbiasable* pseudo random score and is +bundled with an anonymous proof of validity. ### 4.2. Validation of Candidate Tickets @@ -151,37 +153,36 @@ are persisted on-chain. ### 4.3. Tickets Slots Binding After collecting all valid candidate tickets and before the beginning of the -target epoch, a deterministic method is used to uniquely associate a subset of -these tickets with the slots of the target epoch. +*target epoch*, a deterministic method is used to uniquely associate a subset of +these tickets to the slots of the *target epoch*. ### 4.4. Claim of Ticket Ownership -During block production phase of the target epoch, block's author is required -to prove ownership of the ticket associated to the block's slot. This step -discloses the identity of the ticket owner. +During block production phase of *target epoch*, the author is required to prove +ownership of the ticket associated to the block's slot. This step discloses the +identity of the ticket owner. ## 5. Bandersnatch VRFs Cryptographic Primitives -It is 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 the protocol. For a more detailed explanation, refer to -the [Bandersnatch VRF](https://github.com/davxy/bandersnatch-vrfs-spec) +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 primitives +role and interface which is relevant within the scope of the protocol. For a more +detailed explanation, refer to the [Bandersnatch VRFs](https://github.com/davxy/bandersnatch-vrfs-spec) technical specification -Bandersnatch VRF comes in two flavors: +Bandersnatch VRF comes in two variants: - *Bare* VRF: Extension to the IETF ECVRF [RFC 9381](https://datatracker.ietf.org/doc/rfc9381/), -- *Ring* VRF: Provides anonymous signatures by leveraging a *zk-SNARK*. +- *Ring* VRF: Anonymous signatures leveraging *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. +Together with the *input*, which determines the VRF *output*, both variants +offer the capability to sign some arbitrary additional data (*extra*) which +doesn't contribute to the VRF output. ### 5.1 Bare VRF Interface -Function to construct a `VrfSignature`. +VRF signature construction. ```rust fn vrf_sign( @@ -191,8 +192,8 @@ Function to construct a `VrfSignature`. ) -> VrfSignature ``` -Function for signature verification returning a Boolean value indicating the -validity of the signature (`1` on success): +VRF signature verification. Returns a Boolean indicating the validity of the +signature (`1` on success). ```rust fn vrf_verify( @@ -203,7 +204,7 @@ validity of the signature (`1` on success): ) -> Unsigned<1>; ``` -Function to derive the VRF *output* from *input* and *secret*: +VRF *output* derivation from *input* and *secret*. ```rust fn vrf_output( @@ -212,7 +213,7 @@ Function to derive the VRF *output* from *input* and *secret*: ) -> OctetString<32>; ``` -Function to derive the VRF *output* from a *signature*: +VRF *output* derivation from a VRF signature. ```rust fn vrf_signed_output( @@ -220,20 +221,20 @@ Function to derive the VRF *output* from a *signature*: ) -> OctetString<32>; ``` -Note that the following condition is always satisfied: +The following condition is always satisfied: ```rust let signature = vrf_sign(secret, input, extra); vrf_output(secret, input) == vrf_signed_output(signature) ``` -In this document, `SecretKey`, `PublicKey` and `VrfSignature` types are -intentionally left undefined. Their definitions can be found in the Bandersnatch -VRF specification. +`SecretKey`, `PublicKey` and `VrfSignature` types are intentionally left +undefined. Their definitions can be found in the Bandersnatch VRF specification +and related documents. #### 5.4.2. Ring VRF Interface -Function to construct `RingVrfSignature`. +Ring VRF signature construction. ```rust fn ring_vrf_sign( @@ -244,9 +245,9 @@ Function to construct `RingVrfSignature`. ) -> RingVrfSignature; ``` -Function for signature verification returning a Boolean value -indicating the validity of the signature (`1` on success). -Note that verification doesn't require the signer's public key. +Ring VRF signature verification. Returns a Boolean indicating the validity +of the signature (`1` on success). Note that verification doesn't require the +signer's public key. ```rust fn ring_vrf_verify( @@ -257,7 +258,7 @@ Note that verification doesn't require the signer's public key. ) -> Unsigned<1>; ``` -Function to derive the VRF *output* from a ring *signature*: +VRF *output* derivation from a ring VRF *signature*. ```rust fn ring_vrf_signed_output( @@ -265,7 +266,7 @@ Function to derive the VRF *output* from a ring *signature*: ) -> OctetString<32>; ``` -Note that the following condition is always satisfied: +The following condition is always satisfied: ```rust let signature = vrf_sign(secret, input, extra); @@ -273,9 +274,9 @@ Note that the following condition is always satisfied: vrf_signed_output(signature) == ring_vrf_signed_output(ring_signature); ``` -In this document, the types `RingProver`, `RingVerifier`, and `RingVrfSignature` -are intentionally left undefined. Their definitions can be found in the -Bandersnatch VRF specification and related documents. +`RingProver`, `RingVerifier`, and `RingVrfSignature` are intentionally left +undefined. Their definitions can be found in the Bandersnatch VRF specification +and related documents. ## 6. Sassafras Protocol @@ -295,10 +296,11 @@ tickets validation. It is defined as: ``` Where: -- `epoch_length`: number of slots for each epoch. -- `attempts_number`: maximum number of tickets that each authority is allowed to submit. -- `redundancy_factor`: expected ratio between epoch's slots and the cumulative - number of valid tickets which can be submitted by the set of epoch authorities. +- `epoch_length`: Number of slots for each epoch. +- `attempts_number`: Maximum number of tickets that each authority is allowed to submit. +- `redundancy_factor`: Expected ratio between the cumulative number of valid + tickets which can be submitted by the scheduled authorities and the epoch's + duration in slots. The `attempts_number` influences the anonymity of block producers. As all published tickets have a **public** attempt number less than `attempts_number`, @@ -306,12 +308,12 @@ 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.5.2](#652-tickets-threshold). +Details about how these parameters drive the tickets validity probability can be +found in section [6.5.2](#652-tickets-threshold). ### 6.2. Header Digest Log -Each block's header contains a `Digest` log, which is defined as an ordered +Each block header contains a `Digest` log, which is defined as an ordered sequence of `DigestItem`s: ```rust @@ -324,29 +326,29 @@ sequence of `DigestItem`s: ``` The `Digest` sequence is used to propagate information required for the -correct protocol progress. The information within each `DigestItem` is opaque -outside the protocol's context and is represented as a SCALE-encoded version of -protocol-specific structures. - -For Sassafras related entries, the `DiegestItem`s `id` is set to the ASCII -string `"SASS"`. - -Possible digest entries for Sassafras: -- Epoch change signal: Contains information about the next epoch. This is - mandatory for the first block of a new epoch. -- Epoch tickets signal: Contains the sequence of tickets for claiming slots - in the next epoch. This is mandatory for the first block in the *epoch's tail* +correct protocol progress. Outside the protocol's context, the information +within each `DigestItem` is opaque and maps to some SCALE-encoded +protocol-specific structure. + +For Sassafras related items, the `DiegestItem`s `id` is set to the ASCII +string `"SASS"` + +Possible digest items for Sassafras: +- Epoch change signal: Information about next epoch. This is mandatory for the + first block of a new epoch. +- Epoch tickets signal: Sequence of tickets for claiming slots in the next + epoch. This is mandatory for the first block in the *epoch's tail* - Slot claim info: Additional data required for block verification. This is mandatory - and must be the second-to-last entry in the log. -- Seal: Block signature added by the block author. This is mandatory and must be - the last entry in the log. + for each block and must be the second-to-last entry in the log. +- Seal: Block signature added by the block author. This is mandatory for each block + and must be the last entry in the log. -If any of the digest entries are found in the wrong place or in a block where -they are not specified as mandatory, then the block is considered invalid. +If any digest entry is unexpected, not found where mandatory or found in the +wrong position, then the block is considered invalid. ### 6.3. On-Chain Randomness -On-Chain, we maintain a sequence of four randomness entries. +A sequence of four randomness entries is maintained on-chain. ```rust RandomnessBuffer ::= Sequence, 4> @@ -354,31 +356,30 @@ On-Chain, we maintain a sequence of four randomness entries. During epoch `N`: -- The first entry is the current randomness accumulator value and incorporates +- The first entry is the current *randomness accumulator* and incorporates verifiable random elements from all previously executed blocks. The - exact randomness accumulation procedure is described in section - [6.10](#610-randomness-accumulator). + accumulation procedure is described in section [6.10](#610-randomness-accumulator). - The second entry is the snapshot of the accumulator **before** the execution - of the first block of epoch `N`. This is the randomness to be used for tickets + of the first block of epoch `N`. This is the randomness used for tickets targeting epoch `N+2`. - The third entry is the snapshot of the accumulator **before** the execution - of the first block of epoch `N-1`. This is the randomness to be used for tickets + of the first block of epoch `N-1`. This is the randomness used for tickets targeting epoch `N+1` (the next epoch). - The third entry is the snapshot of the accumulator **before** the execution - of the first block of epoch `N-2`. This is the randomness to be used for tickets + of the first block of epoch `N-2`. This is the randomness used for tickets targeting epoch `N` (the current epoch). -The buffer's entries are updated **after** block execution. +The buffer's entries are updated **after** each block execution. ### 6.4. Epoch Change Signal The first block produced during epoch `N` must include a descriptor for some of the parameters to be used by the subsequent epoch (`N+1`). -This descriptor is defined as: +This signal descriptor is defined as: ```rust NextEpochDescriptor ::= Sequence { @@ -389,16 +390,15 @@ This descriptor is defined as: Where: - `randomness`: Randomness accumulator snapshot relevant for validation of - next epoch blocks. In other words, the randomness used to construct the tickets + next epoch blocks. In other words, randomness used to construct the tickets targeting epoch `N+1`. - `authorities`: List of authorities scheduled for next epoch. -This descriptor is `SCALE` encoded and embedded in a `DigestItem` of the -`Digest` log. +This descriptor is `SCALE` encoded and embedded in a `DigestItem`. #### 6.4.1. Startup Parameters -Some of the initial parameters by the first epoch (`#0`), are set through +Some of the initial parameters used by the first epoch (`#0`), are set through the genesis configuration, which is defined as: ```rust @@ -407,9 +407,9 @@ the genesis configuration, which is defined as: } ``` -The on-chain `RandomnessBuffer` is initialized **after** the genesis block. -The first entry is set as the *Blake2b* hash of the genesis block, each of -the following entry is set as the *Blake2b* hash of the previous entry. +The on-chain `RandomnessBuffer` is initialized **after** the genesis block +construction. The first buffer entry is set as the *Blake2b* hash of the genesis +block, each of the other entries is set as the *Blake2b* hash of the previous entry. Since block `#0` is generated by each node as part of the genesis process, the first block that an authority explicitly produces for epoch `#0` is block `#1`. @@ -424,11 +424,12 @@ following epoch. During epoch `N`, each authority scheduled for epoch `N+2` constructs a set of tickets which may be eligible ([6.5.2](#652-tickets-threshold)) for on-chain -submission via the relayers, which are the authorities scheduled for epoch `N+1`. +submission. These tickets are constructed using the on-chain randomness snapshot taken **before** the execution of the first block of epoch `N` together with other -parameters and aims to secure ownership of one or more slots of epoch `N+2`. +parameters and aims to secure ownership of one or more slots of epoch `N+2` +(*target epoch*). Each authority is allowed to submit a maximum number of tickets, constrained by `attempts_number` field of the `ProtocolConfiguration`. @@ -440,11 +441,13 @@ deterministically finalized. This delay is suggested to prevent wasting resources creating tickets that will be unusable if a different chain branch is chosen as canonical. -As said, during epoch `N`, tickets relayers collect (offchain) tickets targeting -epoch `N+2`. When epoch `N+1` starts, the collected tickets are submitted -on-chain by relayers (which are the authorities scheduled for epoch `N+1`) as -"*inherent extrinsic*"s, a special type of mandatory transaction inserted by the -block author at the beginning of the block's transactions sequence. +Tickets generated during epoch `N` are shared with the *tickets relayers*, +which are the authorities scheduled for epoch `N+1`. Relayers validate and +collect (off-chain) the tickets targeting epoch `N+2`. + +When epoch `N+1` starts, collected tickets are submitted on-chain by relayers +as *inherent extrinsics*, a special type of transaction inserted by the block +author at the beginning of the block's transactions sequence. #### 6.5.1. Ticket Identifier @@ -455,28 +458,28 @@ Each ticket has an associated identifier defined as: ``` The value of `TicketId` is completely determined by the output of Bandersnatch -VRFs with the following **unbiasable** input: +VRFs given the following **unbiasable** input: ```rust let ticket_vrf_input = CONCAT( BYTES("sassafras_ticket_seal"), - target_randomness, + target_epoch_randomness, BYTES(attempt) ); - let ticket_id = vrf_output(AUTHORITY_SECRET_KEY, ticket_vrf_input); + let ticket_id = vrf_output(authority_secret_key, ticket_vrf_input); ``` Where: -- `target_randomness`: element of `RandomnessBuffer` which contains the randomness - for the epoch the ticket is targeting. +- `target_epoch_randomness`: element of `RandomnessBuffer` which contains the + randomness for the epoch the ticket is targeting. - `attempt`: value going from `0` to the configured `attempts_number - 1`. #### 6.5.2. Tickets Threshold -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: +A ticket is valid for on-chain submission if its `TicketId` 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) @@ -485,40 +488,29 @@ Where: - `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`. +In an epoch with `s` slots, the goal is to achieve an expected number of valid +tickets 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 authorities, where `v·2/3 ≤ n ≤ v`. +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 authorities, where `v·2/3 ≤ n ≤ v`. These `n` authorities 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)`. +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 +For more details about threshold formula refer to [probabilities and parameters](https://research.web3.foundation/Polkadot/protocols/block-production/SASSAFRAS#probabilities-and-parameters) -paragraph in the Web3 foundation description of the protocol. +paragraph in the Web3 Foundation description of the protocol. #### 6.5.3. Ticket Envelope @@ -534,10 +526,10 @@ Each ticket candidate is represented by a `TicketEnvelope`: Where: - `attempt`: Index associated to the ticket. -- `extra`: additional data for user-defined applications. -- `signature`: ring signature of the envelope data. +- `extra`: Additional data available for user-defined applications. +- `signature`: Ring VRF signature of the envelope data (`attempt` and `extra`). -The envelope data must be signed using Bandersnatch Ring VRF ([5.4.2](#542-ring-vrf-interface)). +Envelope data is signed using Bandersnatch Ring VRF ([5.4.2](#542-ring-vrf-interface)). ```rust let signature = ring_vrf_sign( @@ -552,25 +544,26 @@ With `ticket_vrf_input` defined as in [6.5.1](#651-ticket-identifier). ### 6.6. On-chain Tickets Validation -All the actions in the steps described by this paragraph are executed by -on-chain code. - Validation rules: -1. Ring signature is verified using the `ring_verifier` derived by the static - ring context parameters and the next epoch authorities public keys. +1. Ring VRF signature is verified using the `ring_verifier` derived by the + constant ring context parameters (SNARK SRS) and the next epoch authorities + public keys. 2. `TicketId` is locally computed from the `RingVrfSignature` and its value is checked to be less than tickets' threshold. 3. On-chain tickets submission can't occur within a block part of the - *epoch's tail*, which encompasses a configurable number of the slots at the end - of the epoch. 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 at the beginning of the target epoch. + *epoch's tail*, which encompasses a configurable number of slots at the end + of the epoch. This constraint is to give time to persisted on-chain tickets + to be probabilistically (or even better deterministically) finalized and thus + to further reduce the fork chances at the beginning of the target epoch. 4. All tickets which are proposed within a block must be valid and all of them - must end up in the on-chain queue. + must end up being persisted on-chain. Because the total number of tickets + persisted on-chain is limited by to the epoch's length, this may require to + drop some of the previously persisted tickets. We remove tickets with greater + `TicketId` value first. 5. No tickets duplicates are allowed. @@ -581,12 +574,12 @@ Pseudo-code for ticket validation for steps 1 and 2: ```rust let ticket_vrf_input = CONCAT( BYTES("sassafras_ticket_seal"), - target_randomness, + target_epoch_randomness, BYTES(envelope.attempt) ); let result = ring_vrf_verify( - verifier, + ring_verifier, ticket_vrf_input, envelope.extra, envelope.ring_signature @@ -611,14 +604,14 @@ their `TicketId`, interpreted as a 256-bit big-endian unsigned integer. Tickets ::= Sequence ``` -The on-chain tickets sequence bound is set as the epoch length according to the -protocol configuration. +The on-chain tickets sequence length bound is set equal to the epoch length +in slots according to the protocol configuration. ### 6.7. Ticket-Slot Binding -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 to the next epoch's -slots such that there is at most one ticket per slot. +Before the beginning of the *target epoch*, the on-chain sequence of tickets +must be associated to epoch's slots such that there is at most one ticket per +slot. Given an ordered sequence of tickets `[t₀, t₁, ..., tₙ]`, the tickets are associated according to the following **outside-in** strategy: @@ -628,10 +621,11 @@ associated according to the following **outside-in** strategy: tickets : [ t₀, tₙ, t₁, tₙ₋₁, ... ] ``` -Here `slot-index` is a relative value computed as: `slot_index = slot - epoch_start_slot`. +Here `slot_index` is the slot number relative to the epoch's first slot: +`slot_index = slot - epoch_first_slot`. The association between tickets and a slots is recorded on-chain and thus -is public. What remains confidential is the identity of the ticket's author, and +is public. What remains confidential is the ticket's author identity, and consequently, who is enabled to claim the corresponding slot. This information is known only to the ticket's author. @@ -642,7 +636,7 @@ Note that this fallback situation always apply to the first two epochs after gen ### 6.8. Slot Claim -With tickets bounded to the target epoch slots, every designated authority +With tickets bounded to the *target epoch* slots, every designated authority acquires the information about the slots for which they are required to produce a block. @@ -656,31 +650,33 @@ resorts to the secondary method as a fallback. An authority, can claim a slot using the primary method if it is the legit owner of the ticket associated to the given slot. -Let `target_randomness` be the entry in `RandomnessBuffer` relative to the epoch -the block is targeting and `attempt` be the attempt used to construct the ticket -associated to the slot to claim, the VRF input for slot claiming is constructed as: +Let `target_epoch_randomness` be the entry in `RandomnessBuffer` relative to +the epoch the block is targeting and `attempt` be the attempt used to construct +the ticket associated to the slot to claim, the VRF input for slot claiming is +constructed as: ```rust let seal_vrf_input = CONCAT( BYTES("sassafras_ticket_seal"), - target_randomness, + target_epoch_randomness, BYTES(attempt) ); ``` The `seal_vrf_input`, when signed with the correct authority secret key, must -generate the same `TicketId` associated on-chain to the target slot. +generate the same `TicketId` which has been associated to the target slot +according to the on-chain state. #### 6.8.2. Secondary Method -Given that the authorities registered on-chain are kept on-chain in an ordered -list, the index of the authority which has the privilege to claim an *orphan* -slot is given by the following procedure: +Given that the authorities scheduled for the *target epoch* are kept on-chain in +an ordered sequence, the index of the authority which has the privilege to claim an +*orphan* slot is given by the following procedure: ```rust let hash_input = CONCAT( - target_randomness, - relative_slot_index, + target_epoch_randomness, + ENCODE(relative_slot_index), ); let hash = BLAKE2(hash_input); let index_bytes = CONCAT(GET(hash, 0), GET(hash, 1), GET(hash, 2), GET(hash, 3)); @@ -690,13 +686,10 @@ slot is given by the following procedure: With `relative_slot_index` the slot offset relative to the target epoch's start and `authorities` the sequence of target epoch authorities. -Let `randomness_buffer` be the instance of `RandomnessBuffer` stored in on-chain state -then the VRF input for secondary slot claiming is constructed as: - ```rust let seal_vrf_input = CONCAT( BYTES("sassafras_fallback_seal"), - target_randomness + target_epoch_randomness ); ``` @@ -723,11 +716,11 @@ the randomness source signature is generated as follows: ```rust let randomness_vrf_input = CONCAT( BYTES("sassafras_randomness"), - vrf_output(AUTHORITY_SECRET_KEY, seal_vrf_input) + vrf_output(authority_secret_key, seal_vrf_input) ); let randomness_source = vrf_sign( - AUTHORITY_SECRET_KEY, + authority_secret_key, randomness_vrf_input, [] ); @@ -741,7 +734,7 @@ the randomness source signature is generated as follows: PUSH(block_header.digest, ENCODE(claim)); ``` -The `ClaimData` instance is *SCALE* encoded and pushed as the second-to-last +The `ClaimData` object is *SCALE* encoded and pushed as the second-to-last element of the header digest log. #### 6.8.4. Block Seal @@ -749,10 +742,12 @@ element of the header digest log. A block is finally sealed as follows: ```rust + let unsealed_header_byets = ENCODE(block_header); + let seal = vrf_sign( - AUTHORITY_SECRET_KEY, + authority_secret_key, seal_vrf_input, - ENCODE(block_header), + unsealed_header_bytes ); PUSH(block_header.digest, ENCODE(seal)); @@ -761,7 +756,7 @@ A block is finally sealed as follows: With `block_header` the block's header without the seal digest log entry. The `seal` object is a `VrfSignature`, which is *SCALE* encoded and pushed as -the **last** entry of the block's header digest log. +the **last** entry of the header digest log. ### 6.9. Slot Claim Verification @@ -782,22 +777,25 @@ used by the block author. let unsealed_header_bytes = ENCODE(header); let claim_data = DECODE(POP(header.digest)); - let public_key = GET(authorities, claim_data.authority_index); + let authority_public_key = GET(authorities, claim_data.authority_index); // Verify seal signature let result = vrf_verify( - public_key, + authority_public_key, seal_vrf_input, unsealed_header_bytes, seal_signature ); ASSERT(result == 1); - let randomness_vrf_input = vrf_signed_output(seal_signature); + let randomness_vrf_input = CONCAT( + BYTES("sassafras_randomness"), + vrf_signed_output(seal_signature) + ); // Verify per-block entropy source signature let result = vrf_verify( - public_key, + authority_public_key, randomness_vrf_input, [], claim_data.randomness_source @@ -817,7 +815,8 @@ state. ### 6.9.1. Primary Method For slots tied to a ticket, the primary verification method is employed. -This method verifies ticket ownership using the `TicketId` associated to the slot. +This method verifies ticket ownership using the `TicketId` associated to the +slot. ```rust let ticket_id = vrf_signed_output(seal_signature); @@ -829,17 +828,15 @@ associated `TicketBody`. #### 6.9.2. Secondary Method -If the slot doesn't have any associated ticket then the `authority_index` contained in -the `ClaimData` must match the one returned by the procedure outlined in section -[6.8.2](#682-secondary-method). +If the slot doesn't have any associated ticket, then the `authority_index` +contained in the `ClaimData` must match the one returned by the procedure +outlined in section [6.8.2](#682-secondary-method). ### 6.10. Randomness Accumulator -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: +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: ```rust let fresh_randomness = vrf_signed_output(claim.randomness_source); @@ -853,22 +850,20 @@ None ## 8. Testing, Security, and Privacy -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. +It is critical that implementations of this RFC undergo thorough rigorous +testing. A security audit may be desirable to ensure the implementation does not +introduce emergent side effects. ## 9. Performance, Ergonomics, and Compatibility ### 9.1. Performance Adopting Sassafras consensus marks a significant improvement in reducing the -frequency of short-lived forks. +frequency of short-lived forks which are eliminated by design. -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. +Forks may only result from network disruption or protocol attacks. In such +cases, the choice of which fork to follow upon recovery is clear-cut, with only +one valid option. ### 9.2. Ergonomics @@ -877,19 +872,18 @@ No specific considerations. ### 9.3. Compatibility 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. +via a "simple" runtime upgrade. -This subject is left open for a dedicated RFC. +A deployment strategy should be carefully engineered for live networks. This +subject is left open for a dedicated RFC. ## 10. Prior Art and References - [Sassafras layman introduction](https://research.web3.foundation/Polkadot/protocols/block-production/SASSAFRAS) - [Sassafras research paper](https://eprint.iacr.org/2023/031.pdf) -- [Bandersnatch VRFs specification](https://github.com/davxy/bandersnatch-vrfs-spec). -- [Bandersnatch VRFs reference implementation](https://github.com/davxy/ark-ec-vrfs). +- [Bandersnatch VRFs specification](https://github.com/davxy/bandersnatch-vrfs-spec) +- [Bandersnatch VRFs reference implementation](https://github.com/davxy/ark-ec-vrfs) - [W3F Ring VRF research paper](https://eprint.iacr.org/2023/002.pdf) - [Sassafras reference implementation tracking issue](https://github.com/paritytech/substrate/issues/11515) - [Sassafras reference implementation main PR](https://github.com/paritytech/substrate/pull/11879) @@ -907,34 +901,34 @@ protocol, several crucial topics remain to be addressed in future RFCs. ### 12.1. Interactions with On-Chain Code -- **Storage** organization and static configuration. +- **Storage**: Types, organization and genesis configuration. -- **Outbound Interfaces**: Interfaces that the host environment provides to the - on-chain code, typically known as *Host Functions*. +- **Host interface**: Interface that the hosting environment exposes to on-chain + code (also known as *host functions*). -- **Unrecorded Inbound Interfaces**. Interfaces that the on-chain code provides - to the host environment, typically known as *Runtime APIs*. +- **Unrecorded on-chain interface**. Interface that on-chain code exposes to the + hosting environment (also known as *runtime API*). -- **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) +- **Transactional on-chain interface**. Interface that on-chain code exposes + to the World to alter the state (also known as *transactions* or + *extrinsics* in the *Polkadot* ecosystem). ### 12.2. Deployment Strategies -- **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. +- **Protocol Migration**. Investigate of how Sassafras can seamlessly replace +an already operational instance of another protocol. Future RFCs may focus on +deployment strategies to facilitate a smooth transition. -### 12.3. ZK-SNARK SRS +### 12.3. ZK-SNARK Parameters -- **Procedure**: Determining the procedure for the *zk-SNARK* SRS (Structured - 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). +- **Parameters Setup**: Determine the setup procedure for the *zk-SNARK* SRS + (Structured 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). ### 12.4. Anonymous Submission of Tickets. - **Mixnet Integration**: Submitting tickets directly to the relay 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. + may investigate the potential for incorporating *mix network* protocol or + other privacy-enhancing mechanisms to address this concern.