Created nomos/raw/nomos-proof-of-quota.md file

This commit is contained in:
Cofson
2025-11-28 09:08:08 +01:00
parent d2df7e0c2d
commit 47c7a01ef1

View File

@@ -0,0 +1,437 @@
---
title: NOMOS-PROOF-OF-QUOTA
name: Nomos Proof of Quota Specification
status: raw
category: Standards Track
tags: cryptography, zero-knowledge, blend, quota, rate-limiting
editor: Mehmet Gonen <mehmet@status.im>
contributors:
- Marcin Pawlowski <marcin@status.im>
- Thomas Lavaur <thomaslavaur@status.im>
- Youngjoon Lee <youngjoon@status.im>
- David Rusu <davidrusu@status.im>
- Álvaro Castro-Castilla <alvaro@status.im>
- Filip Dimitrijevic <filip@status.im>
---
## Introduction
This document defines an implementation-friendly specification of the Proof of Quota (PoQ), which ensures that there is a limited number of message encapsulations that a node can perform, thereby constraining the number of messages a node can introduce to the Blend network.
## 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.
The proof verifies that a node's public key is within a limit for either a core node or a leader node.
## Document Structure
This specification is organized into two distinct parts to serve different audiences and use cases:
**Protocol Specification** contains the normative requirements necessary for implementing an interoperable Blend Protocol node. This section defines the cryptographic primitives, message formats, network protocols, and behavioral requirements that all implementations MUST follow to ensure compatibility and maintain the protocol's privacy guarantees. Protocol designers, auditors, and those seeking to understand the core mechanisms should focus on this part.
**Implementation Considerations** provides non-normative guidance for implementers. This section offers practical recommendations, optimization strategies, and detailed examples that help developers build efficient and robust implementations. While these details are not required for interoperability, they represent best practices learned from reference implementations and can significantly improve performance and reliability.
## Protocol Specification
This section defines the normative cryptographic protocol requirements for the Proof of Quota.
### Construction
The Proof of Quota (PoQ) consists of two parts:
1. **Proof of Core Quota (PoQ_C)**: Ensures that the core node is declared and hasn't already produced more keys than the core quota Q_C.
2. **Proof of Leadership Quota (PoQ_L)**: Ensures that the leader node would win the proof of stake for current Cryptarchia epoch and hasn't already produced more keys than the leadership quota Q_L. This doesn't guarantee that the node is indeed winning because the PoQ doesn't check if the note is unspent, enabling generation of the proof ahead of time preventing extreme delays.
**Validity**: 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:
```python
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
```
**Field Descriptions:**
- `session`: Unique session identifier for temporal partitioning
- `core_quota`: Maximum number of message encapsulations allowed per session for core nodes (20-bit value)
- `leader_quota`: Maximum number of message encapsulations allowed per session for potential leaders (20-bit value)
- `core_root`: Root of Merkle tree containing zk_id values of all declared core nodes
- `K_part_one`, `K_part_two`: Split representation of one-time signature public key (32 bytes total)
- `pol_epoch_nonce`: Proof of Leadership epoch nonce for lottery
- `pol_t0`, `pol_t1`: Proof of Leadership threshold constants
- `pol_ledger_aged`: Root of Merkle tree containing eligible Proof of Leadership notes
- `key_nullifier`: Output nullifier preventing key reuse within a session
#### Witness
The prover knows a witness:
```python
class ProofOfQuotaWitness:
index: int # Index of the generated key (20 bits)
selector: bool # Indicates if it's a leader (=1) or 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
# 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 (len = 32)
pol_noteid_path_selectors: list[bool] # Indicates how to read the note_path
pol_slot_secret: int # PoL slot secret corresponding to sl
pol_slot_secret_path: list[zkhash] # PoL slot secret Merkle path (len = 25)
```
**Witness Field Descriptions:**
- `index`: The index of the generated key. Limiting this index limits the maximum number of keys generated (20 bits enables up to 2^20 = 1,048,576 messages per node per session)
- `selector`: Boolean flag indicating node type (1 for leader, 0 for core node)
- `core_sk`: Secret key corresponding to the core node's zk_id
- `core_path`: Merkle authentication path for core node membership
- `core_path_selectors`: Navigation bits for Merkle path (left/right)
- `pol_*`: Proof of Leadership witness fields (filled randomly by core nodes)
**Note**: All inputs and outputs of zero-knowledge proofs are scalar field elements.
### Constraints
The following constraints MUST hold for a valid proof:
#### Step 1: Index Selection and Quota Limitation
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.
**Purpose**: Limiting the possible values of this index limits the possible nullifiers created, which produces the desired effect of limiting the generation of keys to a certain quota.
**Specification**: `index` is 20 bits, enabling up to 2^20 messages per node per session.
#### Step 2: Core Node Verification
If the prover indicated that the node is a core node for the proof (`selector = 0`), the proof checks that:
1. **Core Node Registration**: 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
- The root is provided as a public input
- To build the Merkle tree, `zk_id` values are ordered from smallest to biggest (when seen as natural numbers between 0 and p)
- Remaining empty leaves are represented by 0 after the sorting (appended at the end of the vector)
- This structure supports up to 1M validators
2. **Index Validity**: The index MUST satisfy: `index < core_quota`
#### Step 3: Leader Node Verification
If the prover indicated that the node is a potential leader node for the proof (`selector = 1`), the proof checks that:
1. **Leadership Lottery**: 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 Circuit Constraints.
2. **Index Validity**: The index MUST satisfy: `index < leader_quota`
#### Step 4: Key Nullifier Derivation
The prover derives a `key_nullifier` maintained by blend nodes during the session for message deduplication purposes:
```python
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 it's a leader node derived from inputs
**Rationale**: Two hashes are used because the selection randomness is used in the Proof of Selection to prove the ownership of a valid PoQ.
#### Step 5: One-Time Signature Key Attachment
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`.
**Encoding**: When written in little-endian byte order, the complete public key equals the concatenation `K_part_one || K_part_two`.
### Circuit Implementation
```python
# 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 # Check that selector is indeed a bit
# Verify index is lower than quota
# Equivalent to: 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"NOMOS_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"NOMOS_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.
**Uncompressed Format**: The UncompressedProof comprises 2 G1 and 1 G2 BN256 elements:
```python
class UncompressedProof:
pi_a: G1 # BN256 element
pi_b: G2 # BN256 element
pi_c: G1 # BN256 element
```
**Compression Requirements**:
- Compressed size: 128 bytes
- Curve: BN256 (also known as BN254 or alt_bn128)
- Compression MUST preserve proof validity
### Proof Serialization
The ProofOfQuota structure contains `key_nullifier` and the compressed proof transformed into bytes.
```python
class ProofOfQuota:
key_nullifier: zkhash # 32 bytes
proof: bytes # 128 bytes
```
**Serialization Format**:
1. Transform `key_nullifier` into 32 bytes
2. Compress proof to 128 bytes
3. Concatenate: `key_nullifier || proof`
4. Total size: 160 bytes
**Deserialization**:
Interpret the 160-byte sequence as:
- Bytes 0-31: `key_nullifier`
- Bytes 32-159: `proof`
### Security Considerations
#### Quota Enforcement
- Implementations MUST track `key_nullifier` values during each session
- Duplicate `key_nullifier` values MUST be rejected
- Session transitions MUST clear the nullifier set
#### Proof Verification
- All Merkle path verifications MUST be performed
- The `selector` bit MUST be verified as boolean (0 or 1)
- Index bounds MUST be strictly enforced
- Implementations MUST reject proofs where neither core nor leader conditions hold
#### Cryptographic Assumptions
- Relies on soundness of the underlying zk-SNARK system
- Assumes collision resistance of `zkhash` function
- Assumes computational Diffie-Hellman assumption on BN256 curve
#### Note Unspent Condition
- **Critical**: The PoQ does NOT verify that Proof of Leadership notes are unspent
- This allows pre-generation of proofs to avoid delays
- Implementations SHOULD implement additional checks for actual leadership
## Implementation Considerations
This section provides guidance for implementing the Proof of Quota protocol.
### Proof Generation
**Performance Characteristics**:
Implementations SHOULD consider:
- Proof generation is computationally intensive
- Pre-generation is recommended for leader nodes
- Witness preparation involves Merkle path computation
### Proof Verification Implementation
**Verification Steps**:
1. Deserialize proof into `key_nullifier` and `proof` components
2. Verify proof size (160 bytes total)
3. Check `key_nullifier` against session nullifier set
4. Verify zk-SNARK proof with public inputs
5. Add `key_nullifier` to session set if valid
### Merkle Tree Construction
#### Core Nodes Merkle Tree
**Specification**:
- Depth: 20 levels
- Leaf values: `zk_id` of declared core nodes
- Ordering: Ascending numerical order (as natural numbers 0 to p)
- Empty leaves: Represented by 0, appended after sorted values
- Capacity: 2^20 = 1,048,576 validators
**Construction Algorithm**:
```python
def build_core_tree(zk_ids: list[int]) -> MerkleTree:
# Sort zk_ids in ascending order
sorted_ids = sorted(zk_ids)
# Pad to 2^20 with zeros
padded = sorted_ids + [0] * (2**20 - len(sorted_ids))
# Build Merkle tree
return MerkleTree(padded, depth=20)
```
#### PoL Ledger Merkle Tree
**Specification**:
- Depth: 32 levels
- Leaf values: Note IDs of eligible PoL notes
- Purpose: Prove note membership in aged ledger
### Session Management
**Session Lifecycle**:
1. **Session Start**:
- Initialize empty nullifier set
- Load current session parameters (quotas, roots)
- Prepare session number for proofs
2. **During Session**:
- Verify incoming proofs
- Track nullifiers in set
- Reject duplicate nullifiers
3. **Session End**:
- Clear nullifier set
- Archive session data
- Transition to next session
### Best Practices
#### Nullifier Set Management
- Use efficient data structure (hash set or Bloom filter with fallback)
- Implement atomic operations for nullifier insertion
- Consider memory constraints for long sessions
#### Pre-Generation Strategy
For leader nodes:
- Generate proofs before slot assignment
- Cache proofs for multiple indices
- Monitor note status separately from PoQ
#### Error Handling
Implementations SHOULD handle:
- Invalid proof format
- Duplicate nullifiers
- Index out of bounds
- Merkle path verification failures
- Invalid selector values
### Performance Benchmarks
**Reference Hardware**:
- 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
**Expected Performance** (implementation-dependent):
- Proof generation: TBD
- Proof verification: TBD
- Merkle path computation: TBD
## References
### Normative
- Proof of Quota (original paper/specification)
- Service Declaration Protocol (SDP)
- Mantle Specification
- Circuit Constraints (Cryptarchia)
- Proof of Selection
- rate-limiting nullifiers
### Informative
- BN256 Curve Specification
- zk-SNARKs (Zero-Knowledge Succinct Non-Interactive Arguments of Knowledge)
- [Cryptarchia Consensus](https://arxiv.org/abs/2402.06408)
- Merkle Trees and Authentication Paths
## Copyright
Copyright and related rights waived via [CC0](https://creativecommons.org/publicdomain/zero/1.0/).