V1.0.0-LOGOS-PROOF-OF-QUOTA
| Field | Value |
|---|---|
| Name | [1.0.0] Proof of Quota |
| Slug | 217 |
| Status | deprecated |
| Type | RFC |
| Category | Standards Track |
| Editor | Mehmet Gonen [email protected] |
| Contributors | Filip Dimitrijevic [email protected] |
Timeline
- 2026-05-28 —
d45eed2— Chore: mirror blochain specs into github/mdbook (#347)
Owner: @Mehmet @Marcin Pawlowski @Thomas Lavaur
Reviewers: @Youngjoon Lee @David Rusu @lvaro Castro-Castilla
Revision History
| Version | Changes | Date |
|---|---|---|
| 1.0.0 | Initial revision. | 2026-04-09 |
Introduction
This document defines an implementation-friendly specification of the Proof of Quota (PoQ), which is introduced in [1.0.0] Blend Protocol - Proof of Quota.
Overview
The PoQ ensures that there is a limited number of message encapsulations that a node can perform. This constrains the number of messages a node can introduce to the Blend network. The mechanism regulating these messages is similar to rate-limiting nullifiers.
Construction
The Proof of Quota (PoQ) verifies that a node's public key is within a limit for either a core node or a leader node. It consists of two parts:
- Proof of Core Quota (PoQ_C): Ensures that the core node is declared and hasnt already produced more keys than the core quota Q_C.
- Proof of Leadership Quota (PoQ_L): Ensures that the leader node would win the proof of stake for current Cryptarchia epoch and hasnt already produced more keys than the leadership quota Q_L. That doesnt guarantee that the node is indeed winning because the PoQ doesnt check if the note is unspent enabling generation of the proof ahead of time preventing extreme delays.
The final proof PoQ is valid if either PoQ_C or PoQ_L holds.
Zero-Knowledge Proof Statement
Public values
A proof attesting that for the following public values derived from blockchain parameters:
class ProofOfQuotaPublic:
session: int # Session number (uint64)
core_quota: int # Allowed messages per session for core nodes (20 bits)
leader_quota: int # Allowed messages per session for potential leaders (20 bits)
core_root: zkhash # Merkle root of zk_id of the core nodes
K_part_one: int # First part of the signature public key (16 bytes)
K_part_two: int # Second part of the signature public key (16 bytes)
pol_epoch_nonce: int # PoL Epoch nonce
pol_t0: int # PoL constant t0
pol_t1: int # PoL constant t1
pol_ledger_aged: zkhash # Merkle root of the PoL eligible notes
# Outputs:
key_nullifier: zkhash # derived from session, private index and private sk
Witness
The prover knows a witness:
class ProofOfQuotaWitness:
index: int # This is the index of the generated key. Limiting this index limits the maximum number of key generated. (20 bits)
selector: bool # Indicates if it's a leader (=1) or a core node (=0)
# This part is filled randomly by potential leaders
core_sk: zkhash # sk corresponding to the zk_id of the core node
core_path: list[zkhash] # Merkle path proving zk_id membership (len = 20)
core_path_selectors: list[bool] # Indicates how to read the core_path (if Merkle nodes are left or right in the path)
# This part is filled randomly by core nodes
pol_sl: int # PoL slot
pol_sk_starting_slot: int # PoL starting slot of the slot secrets
pol_note_value: int # PoL note value
pol_note_tx_hash: zkhash # PoL note transaction
pol_note_output_number: int # PoL note transaction output number
pol_noteid_path: list[zkhash] # PoL Merkle path proving noteID membership in ledger aged (len = 32)
pol_noteid_path_selectors: list[bool] # Indicates how to read the note_path (if Merkle nodes are left or right in the path)
pol_slot_secret: int # PoL slot secret corresponding to sl
pol_slot_secret_path: list[zkhash] # PoL slot secret Merkle path to sk_secrets_root (len = 25)
Note that every inputs and outputs of zero-knowledge proofs are all scalar field elements.
Constraints
Such that the following constraints hold:
Step 1: The prover selects an index for the chosen key. This index must be lower than the allowed quota and not already used. This index is used to derive the key nullifier in step 4. Limiting the possible values of this index also limit the possible nullifier created which produce the desired effect: limiting the generation of keys to a certain quota. index will be on 20 bits enabling up to messages per node per session.
Step 2: If the prover indicated that the node is a core node for the proof, the proof checks that:
-
The core node is registered in the set N = SDP(session). This is proven by demonstrating knowledge of a core_sk that corresponds to a declared zk_id, which is a valid SDP registry for the current session. The zk_id values are stored in a Merkle tree with a fixed depth of 20, with the root provided as a public input. To build the Merkle tree, zk_id are ordered from the smallest to the biggest (when seen as natural numbers between 0 and ) and remaining empty leaves are represented by the 0 after the sorting (appended at the end of the vector). This structure supports up to 1M validators.
-
The index is valid: index < core_quota. Step 3: If the prover indicated that the node is a potential leader node for the proof, the proof checks that:
-
The leader node possesses a note that would win a slot in the consensus lottery. Unlike leadership conditions, the proof of quota doesn't verify that the note is unspent. This enables potential provers to generate the PoQ well in advance. All other lottery constraints are the same as in [1.0.0] Proof of Leadership - Circuit Constraints.
-
The index is valid: index < leader_quota. Step 4: The prover derives a key_nullifier maintained by blend nodes during the session for message deduplication purpose.
selection_randomness = zkhash(b"SELECTION_RANDOMNESS_V1", sk, index, session)
key_nullifier = zkhash(b"KEY_NULLIFIER_V1", selection_randomness)
Where sk is:
- The core_sk as defined in the Mantle specification if the node is a core node.
- The secret key of the PoL note if its a leader node derived from inputs. Here we use two hashes because the selection randomness is used in the Proof of Selection in order to prove the ownership of a valid PoQ (see [1.0.0] Blend Protocol - Proof of Selection).
Step 5: The prover attaches a one-time signature key used in the blend protocol. This public key is split into two 16-byte parts: K_part_one and K_part_two. When written in little-endian byte order, the complete public key equals the concatenation K_part_one||K_part_two.
Pseudocode
# Verify selector is a boolean
# selector = 1 if it's a potential leader and 0 if it's a core node
selector * (1 - selector) == 0 # to check that selector is indeed a bit.
# Verify index is lower than quota. It's exactly like saying index < leader_quota
# if selector == 1 or index < core_quota if selector == 0
index < selector * (leader_quota - core_quota) + core_quota
# Check if it's a registered core node
zk_id = zkhash(b"LOGOS_KDF", core_sk)
is_registered = merkle_verify(core_root, core_path, core_path_selectors, zk_id)
# Check if it's a potential leader
is_leader = would_win_leadership(pol_epoch_nonce,
pol_t0,
pol_t1,
pol_ledger_aged,
pol_sl,
pol_sk_starting_slot,
pol_sk_secrets_root,
pol_note_value,
pol_note_tx_hash,
pol_note_output_number,
pol_noteid_path,
pol_noteid_path_selectors,
pol_slot_secret,
pol_slot_secret_path)
# Verify that it's a core node or a leader
assert( selector * (is_leader - is_registered) + is_registered == 1)
# get leader note secret key
pol_sk_secrets_root = get_merkle_root(pol_sk_starting_slot, sl, pol_slot_secret_path)
pol_note_sk = zkhash(b"LOGOS_POL_SK_V1",pol_sk_starting_slot,pol_sk_secrets_root)
# Derive nullifier
selection_randomness = zkhash(
b"SELECTION_RANDOMNESS_V1",
selector * (pol_note_sk - core_sk) + core_sk,
index,
session)
key_nullifier = zkhash(b"KEY_NULLIFIER_V1", selection_randomness)
Proof Compression
The proof confirming that the PoQ is correct must be compressed to a size of 128 bytes, where the UncompressedProof is comprising of 2 and 1 BN256 elements as presented below.
class UncompressedProof:
pi_a: G1 # BN256 element
pi_b: G2 # BN256 element
pi_c: G1 # BN256 element
Proof Serialization
The ProofOfQuota structure contains key_nullifier and the compressed proof transformed in bytes according [1.0.2] Common Cryptographic Components - Use in the Logos Blockchain:. The key_nullifier must be transformed into bytes. The bytes of the compressed proof are then concatenated together with the bytes representing the key_nullifier, with the encoded key_nullifier preceding the encoded compressed proof. Reconstruction of a serialized ProofOfQuota interpreting the bytes as the concatenation of the key_nullifier and of the compressed proof following the same rule of conversion.
class ProofOfQuota:
key_nullifier: zkhash # 32 bytes
proof: bytes # 128 bytes
Appendix
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
