---
# System prepended metadata

title: Decentralised Validator Network

---

# Codex - Decentralised Validator Network

## Background

Codex uses on chain proof validation and off chain sentinel nodes (i.e. "watchtowers") detect missing proofs and trigger on chain adjudication. Sentinel are necessary, because the EVM is inherently event driven - execution must be externally triggered, which makes their role unavoidable under current constraints. While future EVM changes may allow autonomous behavior, current systems require explicit triggering.

Additionally, on chain proof validation is extremely expensive and imposes strict storage semantics - for example, only proofs over very large datasets (e.g., hundreds of gigabytes) are economically viable. This significantly limits general-purpose usability.

One way to improve the current model is to rely entirely on sentinel validators for both proof validation and missed proof detection, similar to optimistic rollup designs. This addresses the cost issue described above, however the sentinel model is vulnerable to the tragedy-of-the-commons problem. Sentinel nodes profit from failures and the absence of such might make it irrational to monitor the network all the time, or at all. In other words, in the absence of deterministic incentives, rational actors tend to abstain from participation, leading to decreased system security.

The result is an asymmetric security model - robust in the all-honest ("happy-path") regime, but brittle under faulty conditions, with diminished guarantees when providers deviate. Generally speaking, **storage systems operate under tighter timing constraints than typical execution environments, rendering purely optimistic approaches insufficient for ensuring reliability**.

This proposal addresses the fundamental shortcomings of the current sentinel-based architecture by introducing a deterministic compensation mechanism. This directly mitigates the tragedy-of-the-commons issue, without introducing the complexity of consensus protocols or zero-knowledge proof systems. Additionally, it also makes the system significantly more scalable, allowing it to process several orders of magnitude more storage proofs and relieving it from the scalability limitations of on chain proof validation.

## Goals

* Achieve robust verification under adversarial conditions
* Eliminate on chain verification costs for storage providers
* Minimize system complexity - avoid full consensus protocols and heavy zk machinery

## High-Level Design Overview

This design leverages standard primitives: BLS signatures and aggregation, Merkle hashing, and gossip-based messaging (e.g., gossipsub), and draws inspiration from existing validator architectures, most notably the Ethereum beacon chain’s validation and signature aggregation model.

A set of validator nodes, continuously monitor the network for storage proofs, verify them, and deterministically aggregate proofs into Merkle Trees, whose roots serve as canonical bundle commitments. Each bundle is signed using BLS, enabling efficient aggregate attestation. To scale and avoid bottlenecks validators are partitioned into periodically rotated committees. Validators are staked and profit from performing their assigned functions and are penalized otherwise.

### Design Objectives

* **Eliminate single‑validator bottlenecks** while keeping on‑chain proofs constant‑size.
* **Rotate participation** so no entity can monopolise aggregation over time.
* **Keep bandwidth and gas per period bounded** - to keep the system both cheap and nimble

Subsequent sections describe bundle formation, incentive flow, failure handling, and committee assignment.

## System Overview

Each *audit period* (every $T$ minutes) challenges a subset of storage providers to prove possession via a succinct (Groth16) Proof Of Retrievability (PoR) proof (or an aggregate of several slot proofs). A pool of $V$ self‑selected **validator nodes** is deterministically assigned every epoch (every $N$ periods) into $K = \lceil V / G \rceil$ committees of fixed size $G$ (where $G$ can be 31, 67, 97, etc...). Committees operate on separate gossip topics, aggregate proofs from providers assigned to their period, BLS‑sign a deterministic bundle root and reveal a leader who submits the attested bundle root on chain.

```
R_epoch  --> H(R_epoch || pk) mod K --> Committees 0 … K‑1  (size≈G)
slotId s   --> c_i = s modK --> gossip topic /codex/validate/c_i
```

## Epoch Mechanics

Epoch rotation ensures liveness and validators fairness. Furthermore, it allows rebalancing the validator set, by incorporating new validators and ejecting exited ones.

An epoch $E$ is defined as $N$ consecutive periods, where a period $p$ is some number of seconds. We use time instead of block numbers due to the fact that some L2s do not guarantee stable block production cadence.

At each epoch boundary:

- Any node can invoke the $newEpoch$ contract call to produce an epoch seed from the latest block hash (or prevrandao if available in the execution environment) and domain - $R_{epoch} = H(blockhash(block.number‑1)||epochDomain)$. This seed is used to perform validator-to-committee assignments. 
- Each epoch seed is stored in a mapping - $epoch[id] = R_{epoch}$, and emitted as a an event $Epoch(id, R_{epoch})$ to the rest of the validators. Invoking $newEpoch$ has a reward that pays for the invocation plus an additional fee.
- If a $newEpoch$ is invoked before the previous epoch has ended, the contract will reject the transaction, avoiding spamming and premature reassignments.

### Committee assignment

Committees are used to partition the validator set into subsets that coordinate across attestation and submission duties. Each validator deterministically computes its own committee by hashing its public key with the epoch seed: $c = H(R_{epoch} || pk || committeeDomain) \bmod K$. 

> **Security Analysis of mod K**
> 

This eliminates the need for costly full-list shuffling like Fisher-Yates and allows the assignment to be computed and verified incrementally both on and off chain. During bundle submission, the contract checks each validator’s committee membership by computing the same hash mapping. Validators signing bundles outside their assigned committee are slashed.

Each validator listens on two dedicated committee gossip topics keyed by the committee index - $/validate/c_{i}$ and $/aggregate/c_{i}$. Storage providers gossip proofs over the $/validate/c_{i}$ topic and committees attest to the bundle root on the $/aggregate/c_{i}$ topics. The separation aims to minimize unrelated traffic across the mesh.

In case there are more validators than committees, excess validators are assigned to their respective committees using the same modulus mapping. However, this does not affect the committee quorum ($\ge \frac{2}{3}$) threshold and their attestations are counted equally against the quorum. Having slightly uneven committees is shouldn't affect the committees functionality in any drastic manner, and committees should rebalance on next epoch. (Will this affect the committees payout? this needs to be clarified/decided)

## In‑Committee Operation

**Validation and bundle construction**

A committee validates and aggregates proofs submitted by storage providers: 

- Storage providers sign the tuple $proofTuple = (slotId, proofBytes)$ and broadcast $signedMsg = (proofTuple, signature)$ over the proof-submission $/validate/c_{i}$ topic using the $c = slotId \mod K$ to map the message to the correct committee. 
- Each validator first verifies that the message is routed to the correct committee, then checks the signature and verifies the proof against public inputs available on chain.

> **Implementation Note**: All required information should be available on chain, but given that the number of slots required to be validated per period might be in the order of hundreds of thousands (plus the public inputs per slot), some special considerations are required when designing the calls, for example there might be limits that will need to be considered (rpc.gascap) or the retrieval time might be too long, these needs to be taken into account.

When the proof submission deadline $(\Phi)$ elapses, every validator builds:

- $proofsList$ – a delta-LEB128 byte string of $uint32$ global slot indices whose proofs were _present and valid_in this period.
- $validatorsList$ – a delta-LEB128 byte string of $uint16$ _global_ validator indices that have signed so far.

Both sequences must decode to strictly increasing, duplicate-free lists. The message $proofBundle = (proofsList, blockHash, validatorsList, signatures)$ is then gossiped on $/aggregate/c_i$.

For each valid proof a validator appends the slot index (delta-encoded) to $proofsList$, where each entry is a mapping between the index (or index delta) and a flag indicating whether the proof is present, invalid or absent. It also appends its own global validator index to $validatorsList$, signing only the $bundle = (proofsList, blockHash)$ tuple, and broadcasts over the $/aggregate/c_{i}$ topic.

After $(\Omega)$ gossip rounds and before the bundle submission deadline ($\aleph$ - periods), a leader elected using the mechanism described in the next section, aggregates, signs and submits the $aggregateBundle = (bundle, signature)$.

If quorum - $|validatorsList| \ge \frac{2}{3}$, is not reached before the deadline, the bundle is missed and _all_ committee members are penalised (should we still compensate signing validators with a portion of the reward?). Validators that withheld a signature from a quorum bundle are penalised individually.

**Leader election**

To avoid redundant on chain submissions and avoid gossiping redundant messages, a simple leader election mechanism is used. The leader is in charge of aggregating all the signatures and is selected by computing a deterministic minimum over the hash of the proof bundle tuple and each participating validator’s public key:

$$
\text{leader} = \underset{s \in \mathcal{{PK_{i}..PK{n}}}}{argmin} ; H(s \parallel \text{bundle})
$$

On chain, this is checked by performing the same $argmin$ procedure over the list of submitted signers, not the full committee. If quorum is reached and the submitter has the lowest hash out of the list of signers, the bundle is accepted. If another bundle appears before the submission deadline and with a lower $argmin$ and same or higher number of signers, it will be accepted instead.

This should incentivize only a few validators with the lowest hashes to perform aggregation and submission, and at the same time avoid complex on-chain flow logic to prevent submission from "invalid" leaders.

**On chain validation**

Upon receipt, the contract:

1. Check submitter is in committee $H(R_{epoch}||signerKeys[msg.sender]||committeeDomain)\bmod K = committeeIndex$
2. Check $bundleAggregate$ is signed by submitter
3. Decodes $validatorsList$ and checks each BLS public key with $H(R_{epoch}||signerKeys[signersMapping[idx]]||committeeDomain)\bmod K = committeeIndex$ only signatures from the correct committee count toward quorum.
4. Check submitter is the leader
5. Verifies the aggregate BLS signature against the decoded signer set.
6. Decodes and parses $proofsList$, which contains the global index and success flag (0|1) indicating if proof was valid or invalid/missing.
7. Proceed with reward distribution/penalization.

```sol
function submitBundle(
    uint256 period,
    uint256 committeeIndex,
    bytes   calldata packedSlotIdx,      // delta-LEB128 uint32 list
    bytes   calldata packedSignerIdx,    // delta-LEB128 uint16 list
    bytes   calldata aggregateSignature, // aggregate signature
    bytes   calldata bundleSigner        // bundle signature
) external {
    /* 1. caller must belong to committee and properly sign the bundle*/
    require(isValidatorInCommittee(msg.sender, committeeIndex), "not in committee");

    require(blsVerifySigner(msg.sender), "not signed by submitter")

    /* 2. derive epoch seed */
    bytes32 seed = epochSeedForPeriod(period);

    /* 3. decode signer list, validate submitter is the leader, validate committee membership of signers */
    uint16[] memory signerIndices = packedSignerIdx.decodeU16();

    require(isLeader(signerIndices, msg.sender), "submitter is not the bundle leader")

    for (uint i = 0; i < signerIndices.length; ++i) {
        uint16 gIdx = signerIndices[i];
        bytes32 pk  = blsPubkeys[gIdx];
        uint assigned = uint256(keccak256(abi.encodePacked(seed, pk))) % K; // check committee
        require(assigned == committeeIndex, "signer out of committee");
    }

    /* 4. quorum */
    require(signerIndices.length >= quorumThreshold, "quorum");

    /* 5. aggregate signature check */
    require(blsAggregateVerify(packedSlotIdx, signerIndices, aggregateSignature), "bad agg-sig");

    /* 6. decode slot indices and mark proven */
    uint32[] memory slotIndices = packedSlotIdx.decodeU32();
    for (uint i = 0; i < slotIndices.length; ++i) {
        uint32 (localIdx, valid) = slotIndices[i];
        uint256 slotId  = slotIdForIndex(committeeIndex, period, localIdx);
        require(!slotAlreadyProven(slotId, period), "dup slot");
        if (valid & 0x1) {
	        markSlotAsProven(slotId, period);
        } else {
	        markSlotAsFailed(slotId, period);
        }
    }

    /* 7. reward + event */
    distributeRewards(signerIndices);
    emit BundleSubmitted(period, committeeIndex, packedSlotIdx, packedSignerIdx, aggregateSignature);
}
```

TODO:

* Describe BLS validation

## Validator staking and incentives

Validators are expected to put a stake as collateral in order to participate in validator duties and in return they are compensated for performing according to protocol and penalized otherwise.

**Registration**

When a validator wants to join the validators network, it needs to register itself with the validator manager contract on chain, by calling the $register(blsPK, signature)$. First, the contract verifies the validator's BLS PK against the signature (this is a proof of possesion to prevent BLS key spoofing) and then attempts to transfer the collateral from the callers wallet. If both of this succeed, it will store the validator's PK and registration time in a mapping. The PK is used to verify validator signature and the time is used to determine which epoch the validator becomes active, not earlier than $\Gamma$ epochs to thwart a class of attacks related to key grinding and prevent churn.

**Incentives**

Validators are compensated when they perform their duties according to the protocol and penalized otherwise, with the possibility of loosing all or part of their collateral for certain offenses. 

**Validator should be compensated for:**

- Triggering a new epoch by calling $newEpoch$ 
	- The validator should eventually get the gas cost for the call refunded as well as receive a reward for triggering the new epoch
- Signing the correct bundle root
	- If the bundle is accepted the validator will receive a reward proportional to the number of validated slots, rewards are fixed to prevent premature submissions, meaning that every validator gets an equal share of rewards regardless of how many nodes signed the root as long as quorum is reached before the deadline expires
- Submitting the bundle root on chain
	- The final submitter of a correct bundle will get its gas cost eventually covered plus a reward fee for triggering validation

**Validators get penalized or slashed for:**

- Not sign the bundle root
	- The validator gets penalized for inactivity
- Signing the wrong bundle root
	- Signing a root from the wrong committee or one that doesn't reach quorum
- Signing more than one bundle root
	- A validator is only allowed to sign a single bundle root, signing more than one bundle root is penalized (should we compensate honest but wrong validators, if a validator signed a bundle that failed to reach quorum, we might reward that with a % of the base reward?)
- Missing the submission deadline
	- If the bundle root is never submitted all validators from the committee are equally penalized
- ??

## Parameters table

| Parameter              | Symbol       | Value               | Notes                                                                                                                                                                                                                                                                                                          |
| ---------------------- | ------------ | ------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Audit period           | $T$          | 10 minutes          | Frequency of PoR challenges                                                                                                                                                                                                                                                                                    |
| Committee size         | $G$          | 50                  | ~f = 3 Byzantine faults                                                                                                                                                                                                                                                                                        |
| Epoch duration         | $N \times T$ | 6 × 10 min = 1 hour | Balance churn and responsiveness                                                                                                                                                                                                                                                                               |
| Challenge deadline     | $\Phi$       | 2 × T (20 min)      | After which proofVector is finalized                                                                                                                                                                                                                                                                           |
| Churn activation delay | $\Gamma$     | 4 epochs (~2 h)     | Prevents key-grinding ([link.springer.com](https://link.springer.com/article/10.1007/s44227-024-00050-z "Model Checking of Rewards and Penalties in Beacon Chain"), [liquidcollective.io](https://liquidcollective.io/eth-activations-and-exits/ "Ethereum's activation and exit queues - Liquid Collective")) |
| Quorum size            | ⌈2G/3⌉       | 8                   | Consensus on PoR bundle                                                                                                                                                                                                                                                                                        |
| Registration bond      | $B$          | 32 ETH (or CODX)    | Discourages Sybil stakes                                                                                                                                                                                                                                                                                       |

## Cost breakdown

**Detailed gas-cost table**

| #                | Operation                                                 | Gas (cold)    | Payer            | Frequency  | Source                                                                                                                                                                                                                                                                                        |
| ---------------- | --------------------------------------------------------- | ------------- | ---------------- | ---------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| 1                | `newEpoch()` seed + event                                 | **≈ 45 000**  | first caller     | 1 × epoch  | `BLOCKHASH` 20 g + `KECCAK256` 36 g + `SSTORE` 20 000 g + LOG ([ethereum.org](https://ethereum.org/en/developers/docs/evm/opcodes/ "Opcodes for the EVM - Ethereum.org"), [eips.ethereum.org](https://eips.ethereum.org/EIPS/eip-2537 "EIP-2537: Precompile for BLS12-381 curve operations")) |
| 2                | Committee check _per signer_ (`KECCAK256`)                | **36 g**      | bundle submitter | G (50)     | Base 30 g + 1-word 6 g ([eips.ethereum.org](https://eips.ethereum.org/EIPS/eip-7904 "EIP-7904: General Repricing - Ethereum Improvement Proposals"))                                                                                                                                          |
|                  | - subtotal for 11 signers                                 | **396**       | -                | per bundle | -                                                                                                                                                                                                                                                                                             |
| 3                | BLS12-381 aggregate verify (2 pairs)                      | **≈ 161 000** | bundle submitter | per bundle | EIP-2537: 115 k + 23 k × 2 ([eips.ethereum.org](https://eips.ethereum.org/EIPS/eip-2537 "EIP-2537: Precompile for BLS12-381 curve operations"))                                                                                                                                               |
| 4                | Calldata (root 32 B + sig 192 B + two bit-vectors ~ 34 B) | **≈ 4 100**   | bundle submitter | per bundle | 16 g per non-zero byte ([ethereum.org](https://ethereum.org/en/developers/tutorials/short-abi/ "Short ABIs for Calldata Optimization - Ethereum.org"), [eips.ethereum.org](https://eips.ethereum.org/EIPS/eip-2028 "EIP-2028: Transaction data gas cost reduction"))                          |
| 5                | Misc. calldata decode / control flow                      | **≈ 15 000**  | bundle submitter | per bundle | empirical (Hardhat baseline)                                                                                                                                                                                                                                                                  |
| **Bundle total** | -                                                         | **≈ 181 k**   | -                | per bundle | dominated by #3                                                                                                                                                                                                                                                                               |
| 6                | Provider `commit()` (hash + store)                        | **≈ 25 000**  | provider         | on dispute | `KECCAK256` 36 g + `SSTORE` 20 k g ([ethereum.org](https://ethereum.org/en/developers/docs/evm/opcodes/  "Opcodes for the EVM - Ethereum.org"))                                                                                                                                               |
| 7                | Provider `submitProof()` (Groth16 verify)                 | **≈ 145 000** | provider         | on dispute | Groth16 verifier cost range ([github.com](https://github.com/Consensys/gnark/discussions/1188 "why does gnark groth16 verify need to cost about 390000 gas for 4 ..."))                                                                                                                       |

**USD equivalents @ 20 gwei, 2 000 USD / ETH**

| Item                    | USD            |
| ----------------------- | -------------- |
| Epoch seed tx           | **0.0018 USD** |
| Bundle (181 k gas)      | **≈ 7.2 USD**  |
| Provider fallback proof | **≈ 5.8 USD**  |
