22 KiB
title, name, status, category, tags, editor, contributors
| title | name | status | category | tags | editor | contributors | ||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| CRYPTARCHIA-PROOF-OF-LEADERSHIP | Cryptarchia Proof of Leadership Specification | raw | Standards Track | nomos, cryptarchia, proof-of-leadership, zero-knowledge, consensus | Thomas Lavaur <thomas@status.im> |
|
Abstract
The Proof of Leadership (PoL) enables a leader to produce a zero-knowledge proof attesting to the fact that they have an eligible note that has won the leadership lottery. This proof is designed to be as lightweight as possible to generate and verify, to impose minimal restrictions on access to the role of leader and maximize the decentralization of that role. This document specifies the PoL mechanism for Cryptarchia, extending the work presented in the Ouroboros Crypsinous paper with recent cryptographic developments.
Semantics
The keywords "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", "MAY", and "OPTIONAL" in this document are to be interpreted as described in RFC 2119.
Overview
Overview of the Protocol
The PoL mechanism ensures that a note has legitimately won the leadership election while protecting the leader's privacy. The protocol is comprised of two parts: setup and PoL generation.
Setup:
- Draw uniformly a random seed.
- Construct a Merkle tree composed of slot secrets derived from the seed.
- Use the root of the tree and the starting slot to get the leader's secret key. The starting slot is when the note can start to be used for PoL.
- The leader receives their stake in a note that uses this generated secret key. The leader either transfers this stake to themselves or obtains it from a different user.
- The note becomes eligible for PoS when it has aged sufficiently, and the actual slot number is greater than or equal to the starting slot of the note.
PoL generation:
- First, check if the note is winning by simulating the lottery.
- Prove the membership of the note identifier in an old snapshot of the Mantle Ledger, proving its age and its existence.
- Prove the membership of the note identifier in the most recent Mantle ledger, proving it's unspent.
- Prove that the note won the PoS lottery.
- Prove the knowledge of the slot secret for the winning slot.
- The proof is bound to a cryptographic public key used for signing the leader's proposed blocks.
Comparison with Original Crypsinous PoL
Our description differs from the original paper proposition, proving that a note is unspent directly instead of delegating the verification to validators. This design choice brings the following tradeoffs:
Advantages:
-
The ledger isn't required to be private using shielded notes.
- Validators don't need to maintain a nullifier list.
- Leaders keep their privacy unlinking their stake, block and PoL.
-
There is no leader note evolution mechanism anymore (see the paper for details).
- There are no orphan proofs anymore, removing the need to include valid PoL proofs from abandoned forks.
- Crypsinous forced us to maintain a parallel note commitment set integrating evolving notes over time. This requirement is removed now.
- The derivation of the slot secret and its Merkle proof can be done locally without connection to the Nomos chain.
Disadvantages:
- We cannot compute the PoL far in advance because the leader MUST know the latest ledger state of Mantle.
Protocol
Protection Against Adaptive Adversaries
The Ouroboros Crypsinous paper integrates protection against adaptive adversaries:
The design has several subtleties since a critical consideration in the PoS setting is tolerating adaptive corruptions: this ensures that even if the adversary can corrupt parties in the course of the protocol execution in an adaptive manner, it does not gain any non-negligible advantage by e.g., re-issuing past PoS blocks. (p. 2)
To avoid a leaked note being reused to maliciously regenerate past PoLs, we adopt the solution proposed in the paper using slightly different parameters.
We recall here the solution proposed in the paper:
We solve the former issue, by adding a cheap key-erasure scheme into the NIZK for leadership proofs. Specifically, parties have a Merkle tree of secret keys, the root of which is hashed to create the corresponding public key. The Merkle tree roots acts like a Zerocash coin secret key, and can be used to spend coins. For leadership however, parties also must prove knowledge of a path in the Merkle tree to a leaf at the index of the slot they are claiming to lead. After a slot passes, honest parties erase their preimages of this part of that path in the tree. As the size of this tree is linear with the number of slots, we allow parties to keep it small, by restricting its size. (p. 5)
The paper proposed a tree of depth 24.
- This implies that the note is usable for PoS for only 194 days approximately (because 1 slot is 1 second).
- After this period, the note MUST be refreshed to include new randomness. We will keep it simple and design the refresh mechanism as a classical transaction modifying the nullifier secret key.
- This solution has good performance:
For a reasonable value of
R = 2^{24}, this is of little practical concern. Public keys are valid for2^{24}slots and employing standard space/time trade-offs, key updates take under 10,000 hashes, with less than 500kB storage requirement. The most expensive part of the process, key generation, still takes less than a minute on a modern CPU. (p. 29)
The disadvantages of this solution are that:
- The public key of the note will change periodically (each time all slot secrets are consumed) for the ones participating in PoL.
- The note will not be reusable directly after refresh as only old enough notes are usable for PoS.
We propose a tree with a depth of 25, extending the note's eligibility to around 388 days, with a maximum of two epochs remaining ineligible not counted in these days. Note that this requirement applies specifically to proving leadership in PoS and is not needed for every note. While any note can be used for PoL, the knowledge of the secret slots behind the public key is only necessary to demonstrate that you are a leader.
Setup: When refreshing their notes, potential leaders will:
-
Uniformly randomly draw a seed
r_1 \stackrel{\$}{\leftarrow} \mathbb{F}_p. -
Construct a Merkle Tree of root
Rcontaining2^{25}slot secrets (that are random numbers). One way to efficiently construct the tree is to:-
Derive the slot secrets using a zkhash chain:
\forall i \in [2, 2^{25}], r_i := \text{hash}(r_{i-1}).- More concretely, each leaf is the zkhash of the previous leaf (slot secret).
-
This reduces storage requirements compared to directly randomly drawn independently
2^{25}slot secrets.- The first generation of the Merkle tree should be fast enough
as it only requires hashing data.
A correct implementation that erases data over time
could maintain an upper bound in memory usage during the generation
of the tree to only
\log_2(2^{25}) = 25zk hashes which is 800 bytes. - Leaders are only required to maintain the MMR up to the current slot. This means at minimum, leaders hold only 25 hashes in memory at any point in time.
- After the first generation, the wallets optimize their storage by holding only the necessary information to maintain a correct Merkle path, deriving the next one over time using the fact that slot secrets were derived from the previous ones.
- The first generation of the Merkle tree should be fast enough
as it only requires hashing data.
A correct implementation that erases data over time
could maintain an upper bound in memory usage during the generation
of the tree to only
-
It guarantees protection against adaptive adversaries.
- Thanks to the pseudo-random properties of the hash function, slot secrets are indistinguishable from true randomness.
- The one-way property of the hash function guarantees that an adaptive adversary cannot retrieve past slot secrets using a fresher one.
-
-
The user chooses a starting slot
sl_{start}from which their note will be eligible for PoS.The note MUST be on-chain by the start of epoch
ep - 1to be eligible for PoL in epochepbecause of the age requirement. Based on this, we suggestsl_{start}to not be earlier than the start of the epoch following the one after the transaction is emitted. This prevents the inclusion of unusable slot secrets in the tree (because the note would not be aged enough), optimizing the PoL lifetime of the note. -
Finally, they derive their secret key
sk := \text{hash}(\text{NOMOS\_POL\_SK\_V1}||sl_{start}||R), binding the starting slot and the Merkle tree of slot secret to the note secret key. This is verified in Circuit Constraints.
These four steps are summarized in the following pseudo-code:
def pol_sk_gen(sl_start, seed):
frontier_nodes = MMR()
path = MerkleProof()
# Generate 2^25 slot secrets using a hash chain initialized with `seed`.
r = zkhash(seed)
for i in range(2**25):
frontier_nodes.append(r) # Append the slot secret to the MMR
path.update(frontier_nodes) # Update Merkle path of this slot secret
r = zkhash(r) # Derive the next slot secret
# Derive the root of the MMR
root = frontier_nodes.get_root()
# Finally, derive the final PoL secret key.
# Return the secret key and the Merkle proof of seed.
return (zkhash(b"NOMOS_POL_SK_V1" + sl_start + root), path)
def update_secret_and_path(r, path):
r = zkhash(r) # Derive next slot secret
path.update(r) # Update the path for the Merkle proof of the new slot secret
return (r, path)
Note that the generation of the slot secret tree is not constrained by proofs or at consensus level and can be adapted by the node as long as they are able to derive the merkle proof of their slot secret.
PoL: When proving the leadership election,
note owners will prove knowledge of the slot secret corresponding to the slot sl.
- To do that, they will give a Merkle path from the leaf at index
sl - sl_{start}. - The root of the tree hashed with
sl_{start}MUST be the secret keysk, which will be used for public key derivation.
Protection against adaptive adversaries: Since each slot has its own slot secret, requiring wallets to delete slot secrets used for previous slots avoids the risk of corruption that leads to the creation of PoL for previous blocks.
- The slot secret is derived from the previous one but the opposite is impossible.
- An adaptive adversary corrupting the node would not have access to previous slot secrets if correctly deleted. Therefore, an adversary would not be able to generate the PoL for previous slots.
Ledger Root
In order to prove that the winning note exists in the ledger
and existed at the start of the previous epoch,
every node MUST compute two ledger commitments.
These commitments ledger_{AGED} and ledger_{LATEST}
are Merkle roots constructed over the Note IDs.
The trees have a depth of 32 and are populated with note IDs.
The value 0 represents an empty leaf.
When the set is updated, during insertion,
the first empty leaf is replaced with the new note ID,
and during deletion, the leaf containing the deleted note ID is replaced with 0.
The following pseudo-code shows how the tree is managed:
def insert_new_note(note_set: list[NoteId], new_note: NoteId):
i = 0
while i < len(note_set) and note_set[i] != 0:
i += 1
if i < len(note_set):
note_set[i] = new_note
else:
note_set.append(new_note)
return note_set
def delete_note(note_set: list[NoteId], note: NoteId):
i = 0
while i < len(note_set) and note_set[i] != note:
i += 1
if i == len(note_set):
# note not in the set
return note_set
note_set[i] = 0
return note_set
def empty_tree_root(depth: int):
root = 0
for i in range(depth):
h = hasher() # zk hash
h.update(root)
h.update(root)
root = h.digest()
return root
def get_ledger_root(note_set: list[NoteId]):
assert(len(note_set) < 2**32)
ledger_root = get_merkle_root(note_set)
# return the Merkle root of the set padded with 0 to next power of 2
ledger_root_height = len(note_set).bit_length()
for height in range(ledger_root_height, 32):
h = Hasher() # zk hash
h.update(ledger_root)
h.update(empty_tree_root(height))
ledger_root = h.digest()
return ledger_root
The ledger root may not be unique because the note IDs set can cycle. Indeed, even if it's not possible to insert the same note ID twice, it's possible to cycle on a previous set state by removing notes. However, note IDs uniqueness guarantees protection against attacks on note aging.
Zero-knowledge Proof Statement
Circuit Public Inputs
The prover (the leader) and the verifiers (nodes of the chain) MUST agree on these values:
-
The slot number:
sl. -
The epoch nonce:
\eta.- For details see Cryptarchia v1 Protocol Specification - Epoch Nonce.
-
The lottery function constants:
t_0 = -\frac{\text{VRF\_order} \cdot \ln(1-f)}{\text{inferred\_total\_stake}}andt_1 = -\frac{\text{VRF\_order} \cdot \ln^2(1-f)}{2 \cdot \text{inferred\_total\_stake}^2}.- For details see Lottery Approximation.
- These numbers MUST be computed with high precision outside the proof.
-
The root of the note Merkle tree when the stake distribution was frozen:
ledger_{AGED}.- For details see Cryptarchia v1 Protocol Specification - Epoch State Pseudocode.
-
The latest root of the note Merkle tree:
ledger_{LATEST}.- Used to ensure the leadership note has not been spent.
-
The leader's one-time public key
P_{LEAD}represented by 2 public inputs, each of 16 bytes in little endian. This key is needed to sign the proposed block.- For details see Linking the Proof of Leadership to a Block.
-
The entropy contribution
\rho_{LEAD}verified to be correctly derived.- This is the epoch nonce entropy contribution. See Cryptarchia v1 Protocol Specification - Epoch Nonce.
Circuit Private Inputs
The prover has to provide these values, but they remain secret:
-
The slot secret and the related information used for the slot
slas described in Protection Against Adaptive Adversaries:- The slot secret
r_{sl}. - The Merkle path
slot\_secret\_pathofr_{sl}leading to the rootR. - The starting secret slot
sl_{start}.
- The slot secret
-
The eligible note and its related information used to derive the
noteID(the secret key is derived for the previous step):- The note value:
v. - The note transaction zk hash:
note\_tx\_hash. - The note outputs number:
note\_output\_number.
- The note value:
-
The proof of membership of the note identifier in the zone ledgers
ledger_{AGED}andledger_{LATEST}. This is done by providing the complementary Merkle nodes and indicating whether they are left (0) or right (1) through boolean selectors:- The aged ledger complementary nodes:
noteid\_aged\_path. - The aged ledger complementary node selectors:
note\_id\_aged\_selectors. - The latest ledger complementary nodes:
noteid\_latest\_path. - The latest ledger complementary node selectors:
note\_id\_latest\_selectors.
- The aged ledger complementary nodes:
Circuit Constraints
The proof confirms the following relations:
-
The derivation of the Merkle tree root
Rusing the slot secretr_{sl}as the $sl - sl_{start}$'s leaf of the Merkle tree using the Merkle path.This is a proof of knowledge of the secret slot
r_{sl}guaranteeing protection against adaptive adversaries. -
The derivation of
sk = \text{hash}(\text{NOMOS\_POL\_SK\_V1}||sl_{start}||R), as documented in Protection Against Adaptive Adversaries. -
The computation of the note identifier.
-
The note identifier is in
ledger_{AGED}andledger_{LATEST}. -
The computation of the lottery ticket:
ticket := \text{hash}(\text{LEAD\_V1}||\eta||sl||noteID||sk)using Poseidon2. -
The computation of the threshold:
t := v(t_0 + t_1 \cdot v). The ticket MUST be lower than this threshold to win the lottery. -
The check that indeed
ticket < t. -
Compute and output the entropy contribution
\rho_{LEAD} := \text{hash}(\text{NOMOS\_NONCE\_CONTRIB\_V1}||sl||noteID||sk).
Linking the Proof of Leadership to a Block
The PoL is bound to a public key from an asymmetric signature scheme.
This public key P_{LEAD} is given as two public inputs during the PoL proof generation,
binding the proof to the key.
- The public key is represented by two public inputs of 16 bytes to guarantee the support of every possible EdDSA25519 public key.
- This public key is later used to verify the signature
\sigmaof a block when it is dispersed. This ensures that the PoL is tied to a specific block, and only the entity creating the proof can perform this binding. - The key is single-use, as reusing the same one could allow multiple PoLs to be linked to the same identity. An observer could then infer the stake of that identity by observing the frequency at which it emits a PoL.
Appendix
Lottery Approximation
- The
\phi_f(\alpha) = 1 - (1-f)^\alphafunction of Ouroboros Crypsinous cannot be computed in a hand-written circuit as it can only operate on elements of\mathbb{F}_pfor a certain prime numberp. - Managing floating point numbers and mathematical functions involving floating points like exponentiations or logarithms in circuits is very inefficient.
- We compared the Taylor expansion of order 1 and 2
and used the Taylor expansion of order 2 method
to approximate the Ouroboros Genesis (and Crypsinous) function
by the following linear function:
\stackrel{0}{\sim}means nearly equal in the neighborhood of 0fis the probability that at least one leader wins the lottery on each slotxis the stake of the proven note
1 - (1-f)^x = 1 - e^{x \ln(1-f)}
1 - e^{x \ln(1-f)} \stackrel{0}{\sim} x(-\ln(1-f) - 0.5 \ln^2(1-f)x)
Then the threshold is stake(t_0 + t_1 \cdot stake) with
t_0 := -\frac{\text{VRF\_order} \cdot \ln(1-f)}{\text{inferred\_total\_stake}}
and
t_1 := -\frac{\text{VRF\_order} \cdot \ln^2(1-f)}{2 \cdot \text{inferred\_total\_stake}^2}.
Since everything is known by every node except the value of the staked note,
we pre-compute t_0 and t_1 outside of the circuit.
- The Hash functions used to derive the lottery ticket is Poseidon2
so the VRF_order is
pthe order of the scalar field of the BN254 elliptic curve. - To compute
t_0andt_1, we precomputed the constant parts using sagemath and real number of 512 bits precision. In the implementation,t_0andt_1should then be derived using 256-bit precision integers following:
| Variable | Formula |
|---|---|
p |
0x30644e72e131a029b85045b68181585d2833e84879b9709143e1f593f0000001 |
t_0\_constant |
0x1a3fb997fd58374772808c13d1c2ddacb5ab3ea77413f86fd6e0d3d978e5438 |
t_1\_constant |
0x71e790b41991052e30c93934b5612412e7958837bac8b1c524c24d84cc7d0 |
t_0 |
\frac{t_0\_constant}{\text{inferred\_total\_stake}} |
t_1 |
p - \lfloor\frac{t_1\_constant}{\text{inferred\_total\_stake}^2}\rfloor |
Error Analysis
- For
f = 0.05. The error percentage is computed with100 \cdot \frac{estimation - real\_value}{real\_value}. - We will consider that inferred_total_stake is 23.5B as in Cardano.
- Original function:
1 - (1-f)^{\frac{stake}{\text{inferred\_total\_stake}}} - Taylor expansion of order 1:
-\frac{stake}{\text{inferred\_total\_stake}} \ln(1-f) := stake \cdot t_0 - Taylor expansion of order 2:
\frac{stake}{\text{inferred\_total\_stake}}(-\ln(1-f) - 0.5 \ln^2(1-f)(\frac{stake}{\text{inferred\_total\_stake}})) := stake(t_0 + stake \cdot t_1)
| stake (%) | order 1 error | order 2 error |
|---|---|---|
| 5% | 0.13% | -0.0001% |
| 10% | 0.26% | -0.0004% |
| 15% | 0.39% | -0.0010% |
| 20% | 0.51% | -0.0018% |
| 25% | 0.64% | -0.0027% |
| 30% | 0.77% | -0.0040% |
| 35% | 0.90% | -0.0054% |
| 40% | 1.03% | -0.0071% |
| 45% | 1.16% | -0.0089% |
| 50% | 1.29% | -0.0110% |
| 55% | 1.42% | -0.0134% |
| 60% | 1.55% | -0.0159% |
| 65% | 1.68% | -0.0187% |
| 70% | 1.81% | -0.0217% |
| 75% | 1.94% | -0.0249% |
| 80% | 2.07% | -0.0284% |
| 85% | 2.20% | -0.0320% |
| 90% | 2.33% | -0.0359% |
| 95% | 2.46% | -0.0406% |
| 100% | 2.59% | -0.0444% |
Benchmarks
The material used for the benchmarks is the following:
- CPU: 13th Gen Intel(R) Core(TM) i9-13980HX (24 cores / 32 threads)
- RAM: 32GB - Speed: 5600 MT/s
- Motherboard: Micro-Star International Co., Ltd. MS-17S1
- OS: Ubuntu 22.04.5 LTS
- Kernel: 6.8.0-59-generic
References
Normative
- Cryptarchia v1 Protocol Specification - Parent protocol specification
Informative
- Proof of Leadership Specification - Original Proof of Leadership documentation
- Ouroboros Crypsinous: Privacy-Preserving Proof-of-Stake - Foundation for the PoL design
Copyright
Copyright and related rights waived via CC0.

