diff --git a/EIPS/eip-7666.md b/EIPS/eip-7666.md index ec776aa2607011..2ee805be0bd093 100644 --- a/EIPS/eip-7666.md +++ b/EIPS/eip-7666.md @@ -2,9 +2,9 @@ eip: 7666 title: EVM-ify the identity precompile description: Remove the identity precompile, and put into place a piece of EVM code that has equivalent functionality -author: Vitalik Buterin (@vbuterin) +author: Vitalik Buterin (@vbuterin), Kevaundray Wedderburn (@kevaundray) discussions-to: https://ethereum-magicians.org/t/eip-7561-evm-ify-the-identity-precompile/19445 -status: Stagnant +status: Draft type: Standards Track category: Core created: 2024-03-31 diff --git a/EIPS/eip-8321.md b/EIPS/eip-8321.md new file mode 100644 index 00000000000000..5f7c43156745a3 --- /dev/null +++ b/EIPS/eip-8321.md @@ -0,0 +1,419 @@ +--- +eip: 8321 +title: Hash-Chain RANDAO +description: Replace the BLS-signature RANDAO reveal with a post-quantum hash-chain commit-reveal scheme +author: Kevaundray Wedderburn (@kevaundray), Benedikt Wagner (@benedikt-wagner), Tom Wambsgans (@TomWambsgans), Justin Drake (@JustinDrake), Thomas Coratger (@tcoratger) +discussions-to: https://ethereum-magicians.org/t/eip-8321-hash-chain-randao/28942 +status: Draft +type: Standards Track +category: Core +created: 2026-07-05 +requires: 7916 +--- + +## Abstract + +Replace the BLS-signature-based Random Decentralized Autonomous Organization (RANDAO) reveal with a hash-chain commit-reveal scheme. RANDAO's resistance to grinding currently relies on BLS signatures being *unique*, so that a proposer cannot bias its contribution. Since BLS relies on pre-quantum hardness assumptions, a quantum computer can recover a validator's key and predict its future reveals. + +A hash chain relies only on standard hash-function security: collision resistance to prevent grinding (the property that replaces BLS's uniqueness) and preimage resistance for unpredictability. Both are believed to hold against quantum attack, and this removes the dependency on the signature scheme entirely. Each validator commits to the tip of a generated hash chain. When proposing a block, the validator reveals the preimage of its currently stored commitment; the protocol verifies that the revealed value is indeed the preimage to the current stored commitment for that validator, folds the preimage into the RANDAO accumulator, and stores the preimage as the validator's new commitment. + +A commitment is registered once, via a new per-block-capped beacon operation (similar to `BLSToExecutionChange`); it cannot be updated in place, so a validator that ever needs a new chain exits and re-enters. Validators that have not yet registered a commitment continue to use the legacy BLS reveal; this is a transitional path intended for removal in a later fork. + +We note the simplicity: the protocol holds only the current commitment (32 bytes) and walks one link back per proposal for that validator. + +## Motivation + +The primary driver is post-quantum readiness. + +Today's RANDAO contribution is a BLS signature over the epoch number. Its security rests on the *uniqueness* of BLS signatures: given the message and public key, exactly one valid signature exists, so a proposer cannot grind their own contribution to bias the randomness. + +BLS is not post-quantum safe, so a cryptographically relevant quantum computer (CRQC) can recover a validator's secret key from its public key. Uniqueness still holds, but the attacker can compute the validator's reveals itself, predicting the chain's future randomness, and thus the proposer schedule, far in advance. + +Prediction also amplifies bias, beyond merely leaking the schedule. The standard reveal-or-withhold attack requires the attacker to control `k` *consecutive* proposer slots at the tail of an epoch to choose among `2**k` candidate mixes, because it cannot evaluate a candidate without knowing the contributions that land after its own. An attacker that can predict every honest contribution evaluates each candidate outright, so (assuming honest proposers always reveal) any `k` of its slots in the mixing period (the span of slots whose reveals feed the target seed) work; they need not be consecutive or at the end of the epoch. + +Most uses of signatures, such as block proposals and attestations, can move to any secure post-quantum scheme. RANDAO, however, additionally depends on signature *uniqueness*, and most post-quantum schemes do not provide it, allowing one to grind for a favorable RANDAO contribution. The hash-based signature scheme currently planned for the consensus layer (a generalized eXtended Merkle Signature Scheme (XMSS) ([RFC 8391](https://www.rfc-editor.org/rfc/rfc8391)), commonly referred to as the lean signature scheme) is grindable in exactly this way, since it includes a *salt* component. + +A hash chain sidesteps both problems in a simple way. There is no signature, it is just chaining hashes together. Grinding would require building a chain around a hash collision: if a proposer could find two values that hash to the same word, it could place that word in its chain and later choose which of the two preimages to reveal, biasing its contribution. This is exactly the freedom BLS's uniqueness denies, and collision resistance denies it here. Preimage resistance separately keeps each reveal unpredictable to others until it is published. Both properties are believed to hold even against a quantum attacker, with reduced but adequate security. + +The commit-reveal structure also preserves RANDAO's existing security model, since the whole chain is fixed at commitment time, long before the validator knows its proposal slots or the mixes it might want to bias, so the proposer's only remaining lever is the same as the one it has today: reveal, or withhold and forfeit the block. + +Note: This EIP does not make beacon chain randomness post-quantum secure end to end; block signatures and the registration operation's signature still use BLS. It incrementally introduces a post-quantum version of RANDAO, so that a later post-quantum fork which changes the signature-scheme and sets the initial hash-chain commitment at deposit time can complete the transition. + +## Specification + +The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", "NOT RECOMMENDED", "MAY", and "OPTIONAL" in this document are to be interpreted as described in [RFC 2119](https://www.rfc-editor.org/rfc/rfc2119) and [RFC 8174](https://www.rfc-editor.org/rfc/rfc8174). + +These changes are applied to the consensus specifications (`ethereum/consensus-specs`) at a fork to be scheduled; functions and constants not defined here retain their meaning from those specifications. + +### Cryptographic Functions + +`blake3(data: bytes) -> Bytes32` is the BLAKE3 hash function (version 1) in its default unkeyed hash mode, with no derive-key context, restricted to its default 32-byte output. All hashing introduced by this EIP uses `blake3` (see Rationale); the consensus specifications' `hash` helper continues to serve the legacy reveal path. + +### Constants + +| Name | Value | Description | +| - | - | - | +| `DOMAIN_RANDAO_COMMITMENT_REGISTRATION` | `DomainType('0x0F000000')` | Domain for signed commitment registrations | +| `HASH_CHAIN_RANDAO_DST` | `b'HASH_CHAIN_RANDAO'` | Domain-separation tag prefixed to each hash-chain link | + +### Preset + +| Name | Value | Description | +| - | - | - | +| `COMMITMENT_REGISTRATION_DELAY` | `Epoch(3)` | Epochs before a new commitment becomes active; MUST be at least `MIN_SEED_LOOKAHEAD + 2` (see Rationale) | +| `MAX_RANDAO_COMMITMENT_REGISTRATIONS` | `uint64(128)` | Maximum commitment registration operations per block | + +### Hash Chain Construction (Off-Chain) + +A validator generates a uniformly random 32-byte chain secret `c_0` and computes a chain of length `n`; every chain value `c_i` is a `Bytes32`, matching the BLAKE3 output width: + +```text +c_i = blake3(HASH_CHAIN_RANDAO_DST + c_{i-1}) for i in 1..n +``` + +where `blake3` is defined in the Cryptographic Functions section above. `HASH_CHAIN_RANDAO_DST` is the fixed byte-string domain-separation prefix defined in the Constants section above, so hash-chain links cannot collide with hashes used elsewhere in the protocol. It carries no per-link index, so a validator still needs to store only chain values, not their positions. + +Nothing in the construction is specific to BLAKE3; any collision- and preimage-resistant hash with a 32-byte output serves (see Rationale for why BLAKE3 over the consensus specifications' `hash` helper). Note that a commitment binds the hash function it was generated with, so a later fork that migrates to a different hash either keeps verifying previously registered chains under BLAKE3 or has validators re-register under the new hash. + +Chain values MUST NOT be the zero word: `process_randao` rejects a zero reveal (the zero word marks an unregistered validator), and a zero commitment cannot be registered. The seed `c_0` is chosen non-zero, and every other `c_i` is a BLAKE3 output, so a zero link occurs only with negligible probability (about `n / 2**256`). Even so, a validator MUST check that no `c_i` is the zero word and regenerate the chain if any link is zero. + +The validator publishes `c_n` as their commitment and stores the chain (or the chain secret plus periodic checkpoints if the chain is large). Reveals are consumed in reverse order: the first reveal is `c_{n-1}`, the next is `c_{n-2}`, and so on. Each revealed value becomes the new on-chain commitment, so the protocol requires no knowledge of `n` or of the validator's position in the chain. + +Validators SHOULD choose `n` large enough that the chain outlasts the validator, since it cannot be extended in place. `n >= 2**16` (~65,000 links) is RECOMMENDED. The whole chain can be generated in milliseconds, and storing every link takes ~2 MB. This exact value is of course arbitrary: the protocol never learns or enforces `n`, and since generation and storage stay cheap, most operators lose nothing by choosing a larger `n`. + +This bounded lifetime is the one structural difference from BLS, where a public key is a commitment that never expires, a hash chain instead lasts for its length. It is not a practical constraint though since we can generate a large enough chain that will last for centuries. + +#### Chain Exhaustion + +A commitment cannot be updated in place, so a chain cannot be extended once registered. The chain is exhausted only when its stored commitment reaches the secret seed `c_0`, whose preimage the validator does not hold. A validator whose chain runs out can no longer propose on the hash-chain path and must exit and re-enter as a new validator to obtain a fresh chain. + +With the recommended `n` this never happens in practice. A validator proposes on the order of 100 times per year even in aggressive futures, so a chain of `2**16` links lasts for centuries; sizing `n` generously (it is cheap) makes exhaustion a non-issue. The same applies to a lost or mis-generated chain secret: there is no in-place recovery, so the remedy is exit and re-entry, and the chain secret SHOULD be guarded like the signing key (see Security Considerations). + +### Containers + +#### New Containers + +```python +class RandaoCommitmentRegistration(Container): + validator_index: ValidatorIndex + commitment: Bytes32 # the hash-chain commitment to register +``` + +```python +class SignedRandaoCommitmentRegistration(Container): + message: RandaoCommitmentRegistration + signature: BLSSignature +``` + +```python +class PendingRandaoCommitment(Container): + validator_index: ValidatorIndex + commitment: Bytes32 + activation_epoch: Epoch +``` + +#### Modified Containers + +`BeaconBlockBody` gains two fields and retains `randao_reveal` transitionally: + +```python +class BeaconBlockBody(Container): + randao_reveal: BLSSignature # transitional; MUST be the G2 point at infinity once the proposer has an active commitment + # ... existing fields ... + hash_chain_reveal: Bytes32 # [New in this EIP] zero unless the proposer has an active commitment + randao_commitment_registrations: List[SignedRandaoCommitmentRegistration, MAX_RANDAO_COMMITMENT_REGISTRATIONS] # [New in this EIP] +``` + +The new fields are appended after all existing fields, following the convention since Capella of adding fields at the end. + +`BeaconState` gains a commitment registry and a pending queue. `randao_commitments` is indexed by validator index, holding one entry per registry member; a zero entry means no commitment is registered and the legacy BLS reveal path applies. The `Validator` container is unchanged. + +```python +class BeaconState(Container): + # ... existing fields ... + randao_commitments: List[Bytes32, VALIDATOR_REGISTRY_LIMIT] # [New in this EIP] + pending_randao_commitments: ProgressiveList[PendingRandaoCommitment] # [New in this EIP] +``` + +The pending queue is a `ProgressiveList` ([EIP-7916](./eip-7916.md)): it is almost always near-empty (worst-case steady state is ~12,300 entries, see Security Considerations), so a progressive shape avoids both an arbitrary capacity constant and the hashing overhead of a large fixed-limit list. `randao_commitments` stays a fixed-limit `List` because it holds one entry per registry member and must track the length of the other per-validator lists, which share the `VALIDATOR_REGISTRY_LIMIT` bound. + +### Block Processing + +#### Modified `process_randao` + +```python +def process_randao(state: BeaconState, body: BeaconBlockBody) -> None: + epoch = get_current_epoch(state) + proposer_index = get_beacon_proposer_index(state) + proposer = state.validators[proposer_index] + if state.randao_commitments[proposer_index] != Bytes32(): + # Hash chain reveal [New in this EIP] + assert body.hash_chain_reveal != Bytes32() + assert blake3(HASH_CHAIN_RANDAO_DST + body.hash_chain_reveal) == state.randao_commitments[proposer_index] # check proposer knows preimage + assert body.randao_reveal == G2_POINT_AT_INFINITY # bls randao reveal should be empty + mix = blake3(get_randao_mix(state, epoch) + body.hash_chain_reveal) + state.randao_commitments[proposer_index] = body.hash_chain_reveal + else: + # Legacy BLS reveal + assert body.hash_chain_reveal == Bytes32() # hash-chain reveal should be empty + signing_root = compute_signing_root(epoch, get_domain(state, DOMAIN_RANDAO)) + assert bls.Verify(proposer.pubkey, signing_root, body.randao_reveal) + mix = xor(get_randao_mix(state, epoch), hash(body.randao_reveal)) + state.randao_mixes[epoch % EPOCHS_PER_HISTORICAL_VECTOR] = mix +``` + +Note the structure of the hash-chain path: + +- Verification checks the proposer's chain step (`blake3(HASH_CHAIN_RANDAO_DST + reveal) == commitment`). +- The accumulator folds in the raw reveal with `mix = blake3(mix + reveal)`. +- `G2_POINT_AT_INFINITY` is the existing BLS point-at-infinity signature constant already defined in the consensus specs. + +The hash accumulator has no efficiently computable inverse, so a validator that copies another's commitment cannot cancel the victim's contribution: re-injecting the same revealed value produces a fresh, unrelated mix rather than undoing it. This is why the hash-chain path can fold in the raw reveal directly, with no assumption on the mixed-in value being unique per validator. The legacy BLS path keeps its existing `xor` accumulator; the two coexist only until the BLS path is sunset. + +The non-zero assert protects the sentinel: a zero entry in `randao_commitments` means "unregistered", and the reveal is stored as the next commitment. Without the guard, a validator that committed to the zero word as a chain value would, upon revealing it, silently store the sentinel and flip onto the legacy branch, while its client, still believing itself registered, produced invalid blocks indefinitely. With the guard, the zero-revealing block is itself invalid, so a validator that committed the zero word simply cannot propose (hence the rule above that no chain value may be the zero word). So "zero means unregistered" is an enforced invariant. + +#### Modified `process_operations` + +`process_operations` gains a loop over the new operation, appended after the existing operations so that existing processing is unchanged: + +```python +def process_operations(state: BeaconState, body: BeaconBlockBody) -> None: + # ... existing operation processing (proposer slashings, attester slashings, + # attestations, deposits, voluntary exits, bls_to_execution_changes) ... + for_ops(body.randao_commitment_registrations, process_randao_commitment_registration) # [New in this EIP] +``` + +#### New `process_randao_commitment_registration` + +```python +def process_randao_commitment_registration(state: BeaconState, signed_registration: SignedRandaoCommitmentRegistration) -> None: + registration = signed_registration.message + assert registration.validator_index < len(state.validators) + assert registration.commitment != Bytes32() + # Register-only: valid only while the validator is unregistered (stored commitment is zero) + assert state.randao_commitments[registration.validator_index] == Bytes32() + validator = state.validators[registration.validator_index] + domain = compute_domain( + DOMAIN_RANDAO_COMMITMENT_REGISTRATION, + genesis_validators_root=state.genesis_validators_root, + ) + signing_root = compute_signing_root(registration, domain) + assert bls.Verify(validator.pubkey, signing_root, signed_registration.signature) + queue_randao_commitment(state, registration.validator_index, registration.commitment) +``` + +Called from `process_operations` for each element of `body.randao_commitment_registrations`. This is the one-time registration path: it moves a validator from the legacy BLS reveal onto its hash chain, and works regardless of proposal schedule. It is valid only while the validator is unregistered (its stored commitment is zero). The signing domain is computed against the genesis fork version (similar to `BLSToExecutionChange`) so that messages remain valid across forks. + +Registration is single-use by construction, covering two windows with two mechanisms. Once a registration has **activated**, `randao_commitments[validator_index]` is non-zero, so any replay fails the unregistered check in this handler. While it is still **pending** (queued but not yet activated), the stored entry is still zero, but the one-pending rule in `queue_randao_commitment` rejects a second submission. Together these leave no gap from inclusion onward. This mirrors the replay-safety of `BLSToExecutionChange`, whose handler likewise asserts the credential has not already been changed. + +#### New `queue_randao_commitment` + +At most one registration per validator may be pending at any time; a block containing a registration for a validator with an in-flight one is invalid. + +```python +def queue_randao_commitment(state: BeaconState, index: ValidatorIndex, commitment: Bytes32) -> None: + # Rejects a second registration for the same validator, both across blocks and + # within a single block: the first op appends this entry, so a later op for the + # same index fails this assert (mirrors how voluntary exits self-reject in-block). + assert all(pending.validator_index != index for pending in state.pending_randao_commitments) + state.pending_randao_commitments.append(PendingRandaoCommitment( + validator_index=index, + commitment=commitment, + activation_epoch=get_current_epoch(state) + COMMITMENT_REGISTRATION_DELAY, + )) +``` + +The resulting lifecycle, worked through to be more explicit: + +- A registration included in epoch `N` is queued with activation epoch `N + COMMITMENT_REGISTRATION_DELAY` (`N + 3` with the preset value). +- The validator stays on the legacy BLS path throughout epochs `N` to `N + 2`; its proposals in that window are BLS reveals as before. +- At the epoch transition into `N + 3`, the pending entry is consumed and the hash chain becomes active; from then on the validator reveals from its chain. + +Activation is a property of the canonical state, not of the validator's broadcast history. A validator MUST continue producing legacy BLS reveals until `randao_commitments[validator_index]` is non-zero in the state it proposes against, even if it has observed its registration included in some block: if that block is orphaned, the pending entry never enters the canonical queue. The message stays valid in that case (the stored commitment is still zero) and remains includable: it persists in operation pools, as with `bls_to_execution_change`. + +### Epoch Processing + +#### New `process_pending_randao_commitments` + +Called from `process_epoch`, immediately after `process_registry_updates`. Pending commitments are applied in queue order once their activation epoch is reached. The one-pending-registration-per-validator rule enforced in `queue_randao_commitment` guarantees entries never conflict. + +```python +def process_pending_randao_commitments(state: BeaconState) -> None: + next_epoch = Epoch(get_current_epoch(state) + 1) + remaining = [] + for pending in state.pending_randao_commitments: + if pending.activation_epoch <= next_epoch: + state.randao_commitments[pending.validator_index] = pending.commitment + else: + remaining.append(pending) + state.pending_randao_commitments = ProgressiveList[PendingRandaoCommitment](remaining) +``` + +### Gossip + +A new global gossip topic `randao_commitment_registration` carries `SignedRandaoCommitmentRegistration` messages. Because a validator registers at most once, first-seen-per-validator deduplication suffices, exactly as for `bls_to_execution_change`. The rules are evaluated in order, so the cheap checks and the seen check precede signature verification. + +- **[REJECT]** `commitment` is zero, or `validator_index` is unknown. +- **[IGNORE]** a `SignedRandaoCommitmentRegistration` for `validator_index` has already been seen, or a pending registration for it already exists in the node's view of the state. +- **[REJECT]** the validator is already registered (a non-zero `randao_commitments` entry in the node's view of the head state). +- **[REJECT]** the signature is invalid. + +### Fork Transition + +At the fork epoch, the `upgrade_to_*` function initializes `randao_commitments` with one zero entry per registry member and `pending_randao_commitments` as empty: + +```python +def upgrade_to_(pre: ) -> BeaconState: + post = BeaconState( + # ... existing fields carried over from `pre` ... + randao_commitments=[Bytes32() for _ in range(len(pre.validators))], # [New in this EIP] + pending_randao_commitments=[], # [New in this EIP] + ) + return post +``` + +This establishes the invariant that `len(state.randao_commitments) == len(state.validators)`, which `process_randao` and `process_randao_commitment_registration` rely on when indexing by validator index. To preserve the invariant for validators onboarded after the fork, `add_validator_to_registry` is modified to append a zero entry alongside the other per-validator lists: + +```python +def add_validator_to_registry(state: BeaconState, + pubkey: BLSPubkey, + withdrawal_credentials: Bytes32, + amount: uint64) -> None: + index = get_index_for_new_validator(state) + validator = get_validator_from_deposit(pubkey, withdrawal_credentials, amount) + set_or_append_list(state.validators, index, validator) + set_or_append_list(state.balances, index, amount) + set_or_append_list(state.previous_epoch_participation, index, ParticipationFlags(0b0000_0000)) + set_or_append_list(state.current_epoch_participation, index, ParticipationFlags(0b0000_0000)) + set_or_append_list(state.inactivity_scores, index, uint64(0)) + set_or_append_list(state.randao_commitments, index, Bytes32()) # [New in this EIP] +``` + +A validator added this way starts unregistered and uses the legacy path until it registers. All validators start on the legacy BLS reveal path and migrate by broadcasting a `SignedRandaoCommitmentRegistration`. At `MAX_RANDAO_COMMITMENT_REGISTRATIONS = 128` per block, the full current validator set (~1M) can register in under two days of full blocks; there is no deadline, and unregistered validators simply continue on the legacy path. + +### Sunset of the Legacy Path + +The BLS reveal branch in `process_randao`, and the `randao_reveal` field itself, are transitional and SHOULD be removed in a later fork after registration has saturated, naturally the fork that reworks deposits for post-quantum signatures, at which point initial hash chain commitments move into the validator onboarding flow and the `RandaoCommitmentRegistration` signature migrates to the post-quantum scheme. + +Note: the `RandaoCommitmentRegistration` operation registers a validator's initial hash chain only. A validator that loses its chain secret must exit and re-enter. Once the post-quantum fork sets initial commitments at deposit time, the operation is no longer needed and can be deprecated alongside the BLS reveal path. + +## Rationale + +### Why a Hash Accumulator on the Hash-Chain Path + +The mixed-in contribution must be unpredictable before the reveal: the mix must never absorb a value derivable from pre-reveal public state. This rules out folding in `blake3(HASH_CHAIN_RANDAO_DST + reveal)`, which for a hash chain is by definition the validator's stored commitment, already public on-chain. The raw reveal is the opposite: it is the secret preimage, unknown to everyone until the proposer publishes it, so folding it in is safe. + +The complication is that hash-chain commitments carry no identity and are trivially copyable. A validator can register a value it did not generate by including a copy of another validator's current commitment, via a perfectly valid, freshly signed registration. Under a linear XOR accumulator this enables a cancellation attack. +A hash accumulator, `mix = blake3(mix + reveal)`, closes this at the root: the hash has no efficiently computable inverse, so re-injecting a copied reveal produces a fresh, unrelated mix rather than undoing anything. + +Because the accumulator itself neutralizes copyability, the hash-chain path folds in the raw reveal with no per-validator personalization. + +### Why BLAKE3 Instead of the Consensus `hash` + +The scheme needs nothing beyond standard collision and preimage resistance; the choice of BLAKE3 is forward-looking rather than security-driven. BLAKE3 is fast in software, and recent results on proving it efficiently in binary-field proof systems make it the leading candidate hash for the post-quantum consensus layer. + +### Why One Link per Block Proposed + +The legacy reveal is a signature over the epoch, so two proposals by the same validator in one epoch reveal the same value. Consuming one link per block is simpler (no per-epoch bookkeeping), makes every block contribute fresh entropy, and costs nothing, since chains are cheap to generate at any reasonable length. + +We also note that with consolidations, the probability of a validator proposing multiple times in the same epoch increases, effectively cancelling out their RANDAO contribution. + +### Why the Protocol Does Not Track Chain Length + +Storing only the current commitment and requiring a preimage per proposal makes the chain length a purely private, off-chain parameter. The protocol state cost is one 32-byte entry per validator regardless of chain length, and validators can size chains so that the chain outlasts the validator. The only reason to store the chain length would be to mandate a specific length for every validator, which seems unnecessarily restrictive. + +### Why an Activation Delay on Registration, and Why `MIN_SEED_LOOKAHEAD + 2` + +The scheme's grinding resistance comes entirely from the commitment predating the validator's knowledge of what it would want to bias. If a validator could register a chain just before proposing and it became active instantly, it could grind: generate many candidate chains, compute the mix each would produce at the upcoming slot, and register the most favourable one. We add a delay, sized so that the registrant cannot even know *whether* it will propose in the activation epoch at the time the registration is included. + +Proposer duties for epoch `E` are drawn from the RANDAO mix as of the end of epoch `E - MIN_SEED_LOOKAHEAD - 1`, i.e. `E - 2` with the current consensus-specs preset `MIN_SEED_LOOKAHEAD = 1`. For a registration included in epoch `N`: + +- Duties through epoch `N + 1` fixed at the end of `N - 1` or earlier: already known at inclusion. +- Duties for `N + 2` fix at the end of `N`, which is nearly over at inclusion: still grindable. +- Duties for `N + 3` fix at the end of `N + 1`: entirely after inclusion. + +The earliest safe activation epoch is therefore `N + MIN_SEED_LOOKAHEAD + 2 = N + 3`, met exactly by `COMMITMENT_REGISTRATION_DELAY = 3`; the new commitment is first usable from the validator's first proposal in that epoch. The constants table states the bound symbolically so it survives a future change to the preset. + +### Why at Most One Pending Registration per Validator + +Without the one-pending rule, a validator's stored entry stays zero until activation, so multiple registrations for it would all pass the unregistered check and could be queued at once. That enables queue stuffing: the entire `MAX_RANDAO_COMMITMENT_REGISTRATIONS` budget could be filled with registrations for a single validator, costing the network 128 signature verifications and 128 queue entries for a single state effect, and crowding legitimate registrations out of the shared per-block budget. + +The one-pending rule also keeps gossip sound: the "IGNORE if a pending registration exists" rule is only meaningful if a further registration for that validator is guaranteed redundant. With the rule, every included registration corresponds to a distinct validator making its one-time transition, and duplicates are invalid rather than merely wasteful. + +### Why Registration Is One-Time, and Replay-Safe + +A commitment can be registered but never updated in place. This is a deliberate simplification: because a chain can be sized to outlast the validator (see Chain Exhaustion), in-place rotation is never needed for exhaustion, and a lost chain secret is treated like a lost signing key, recovered by exiting and re-entering rather than by a protocol operation. + +Replay-safety falls out of the unregistered check. A registration is valid only while `randao_commitments[validator_index]` is zero. Once it activates, the entry is non-zero, so any later replay of the message is invalid; and while the registration is merely pending, the one-pending rule blocks a second submission. This is the analogue of the idempotence predicate that makes `BLSToExecutionChange` replay-safe, whose handler asserts the credentials are still BLS-prefixed, which the first application falsifies. + +### Why Keep a Legacy Fallback Instead of a Registration Deadline + +There is no urgent need for validators to be fully post-quantum yet, so a hard deadline buys little; unregistered validators keep functioning on the BLS path in the meantime. There will also likely be a long tail of validators who never submit the message because they run custom software. We therefore defer the forced cutover to a fork that must touch these code paths anyway, the post-quantum switchover, which also lets us exercise the new code paths first. + +## Backwards Compatibility + +This EIP requires a scheduled consensus-layer hard fork. Within the fork, the change is backwards compatible from the validator's perspective: unregistered validators continue proposing exactly as today. Downstream consumers of `randao_mixes` (including the execution layer's [EIP-4399's](./eip-4399.md) `PREVRANDAO`) are unaffected; the mix remains a 32-byte accumulator updated once per block, only the provenance of contributions changes. + +## Test Cases + +State-transition test vectors to be provided in the consensus-specs test suite. + +## Security Considerations + +### Biasability Is Unchanged + +The proposer's only degree of freedom is still withholding: reveal and propose, or withhold and forfeit the block plus its rewards. This is the same one-bit-per-proposer bias RANDAO has today, with the same economic cost. The validator freely choosing its own chain values (whereas a BLS reveal is uniquely determined by the key and epoch, leaving no freedom) does not add grinding power, because the entire chain is fixed before the validator knows its proposal slots or the co-contributions to any future mix. + +### Registration Cannot Be Used Reactively + +A validator that sees a proposal duty approaching cannot register a favourable chain to influence the outcome. Activation is derived from the inclusion epoch, the only event the protocol observes: registration activates `COMMITMENT_REGISTRATION_DELAY` epochs after inclusion, strictly after every epoch whose proposer-duty seed was fixed (or partially accumulated) at inclusion, so the chain is always fixed before the seed that draws any duty it could serve. Signing earlier than inclusion only means committing with less information. + +Note: This argument assumes the registrant cannot predict the contributions that land between inclusion and the fixing of the target seed. That holds today (a BLS reveal is computable only by its key holder) and after migration (a hash-chain reveal is protected by preimage resistance), but an attacker that already holds a CRQC during the transition could predict every remaining legacy contribution and grind its one-time registration against them. + +Timing games around inclusion add nothing: an unregistered validator stays on the BLS path until activation, and once registered it cannot re-register, so there is no second chain to time. + +Nor can registrations be replayed by third parties: once a validator is registered its stored entry is non-zero, so any replay fails the unregistered check. + +### Degenerate Chains + +A validator may choose pathological values for its *own* chain, for example deriving the chain secret from a publicly known constant like the genesis hash, so that all its reveals are predictable in advance and contribute no effective entropy. + +This cannot be exhaustively restricted, and we do not try to, because RANDAO's security does not assume that *every* contribution is honest. If this were the case, then a validator choosing to add nothing by withholding would also break the security; one honest contributor per mixing period suffices. + +We do forbid the pathological case of using the zero word, since this would collide with the unregistered sentinel when revealed, so `process_randao` rejects zero reveals. + +### Chain Loss and Theft + +Losing the chain secret makes a validator unable to produce valid blocks on the hash-chain path, and there is no in-place recovery: a commitment cannot be re-registered. The remedy is to exit and re-enter as a new validator. Funds are unaffected (withdrawal does not depend on the chain), and the failure mode is missed proposals, not slashing risk. + +Theft of the chain secret alone lets an attacker predict (not choose) the validator's future contributions. In effect this is equivalent to leaking the validator's entire list of future BLS-RANDAO reveals: the attacker learns every future contribution in advance, but can neither choose them nor sign anything else on the validator's behalf. It is therefore strictly less power than theft of the signing key today, which gives the attacker that same prediction plus the ability to sign the validator's blocks and attestations. Validator clients should treat the chain secret with the same custody standards as signing keys. As a safety measure, it should not be derived in a way that links it to the signing key: a hash chain, taken to its limit, eventually reveals its seed, whereas a secret key must never be revealed. + +### Reveal Exposure in Orphaned Blocks + +The legacy BLS reveal signs the epoch number, so a reveal exposed in a block that never makes the canonical chain is worthless outside that epoch. A hash-chain reveal has no such time binding: a reveal published in an *orphaned* block remains the validator's mandatory next contribution at whatever future slot it next proposes. An observer who collects reveals from orphaned blocks (or losing forks) accumulates known future co-contributions. + +Because a chain cannot be retired in place, there is no cheap remediation, but none is needed: the exposure is harmless. A published-but-not-yet-consumed reveal only makes that one future contribution predictable rather than secret, which is no worse than the validator withholding it, and RANDAO tolerates predictable contributions (one honest contributor per mixing period suffices). A validator sufficiently concerned about a specific exposed chain can exit and re-enter, but this is unwarranted in practice. + +### Erroneous Commitments + +Preimage possession is unverifiable at registration: the protocol cannot distinguish a commitment whose chain the validator holds from a typo or the output of a buggy key-derivation. + +Because registration is one-time, a bad commitment cannot be corrected in place: the validator can never propose on the hash-chain path, and the only remedy is to exit and re-enter. This makes an erroneous registration as costly as losing the chain secret, and it does not exist under BLS reveals, where there is nothing to misconfigure beyond the signing key itself. Validator clients should therefore verify a commitment by walking its full chain before registering, and treat the chain secret with the same care as the signing key. + +### Quantum Adversaries + +The scheme rests on two properties of BLAKE3. Grinding resistance rests on collision resistance: a proposer that could find a collision could give a chain link two preimages and choose between them at reveal time to bias its contribution. + +Unpredictability rests on preimage resistance: a reveal cannot be derived from the public commitment before it is published. The post-quantum security of the scheme is therefore comparable to the collision resistance of BLAKE3, which is believed to remain adequate against a quantum adversary. The transitional BLS reveal path and the BLS signature on `RandaoCommitmentRegistration` remain quantum-vulnerable, as do block signatures generally; this EIP removes RANDAO's structural dependence on signature uniqueness so that the eventual post-quantum fork is a signature-scheme swap rather than a randomness redesign. + +### Denial of Service via Registrations + +Registration confers no randomness advantage, so spamming registrations is a pure-cost DoS vector. It is bounded on every surface: + +- **Blocks.** The operations list is capped at `MAX_RANDAO_COMMITMENT_REGISTRATIONS` per block (at most 128 additional BLS verifications). +- **Gossip.** A validator registers at most once, so first-seen-per-validator deduplication (as for `bls_to_execution_change`) bounds gossip to one signature verification per validator, ever. Non-validators cannot participate: their messages fail signature validation and incur gossipsub peer penalties. Messages for an already-registered validator fail the head-state REJECT check, also penalized. +- **State.** `queue_randao_commitment` enforces at most one pending registration per validator, and the queue is additionally bounded by inclusion rates: at most `MAX_RANDAO_COMMITMENT_REGISTRATIONS` entries per block, each resident for `COMMITMENT_REGISTRATION_DELAY` epochs, giving a worst-case steady state of ~12,300 entries. + +## Copyright + +Copyright and related rights waived via [CC0](../LICENSE.md). diff --git a/EIPS/eip-8371.md b/EIPS/eip-8371.md new file mode 100644 index 00000000000000..9cdf48fe3b522f --- /dev/null +++ b/EIPS/eip-8371.md @@ -0,0 +1,177 @@ +--- +eip: 8371 +title: RowDAS - Distributed Blob Reconstruction +description: Distribute reconstruction load in the network through row-level cell messaging. +author: Csaba Kiraly (@cskiraly), Marco Munizaga (@MarcoPolo) +discussions-to: https://ethereum-magicians.org/t/eip-8371-rowdas-distributed-blobspace-reconstruction/29320 +status: Draft +type: Standards Track +category: Networking +created: 2026-08-05 +requires: 7594, 8136 +--- + +## Abstract + +PeerDAS (Peer Data Availability Sampling, [EIP-7594](./eip-7594.md)) requires supernodes to provide reconstruction, and this puts a high burden on supernodes that scales linearly with blob count. RowDAS enables distributed blobspace reconstruction using partial-message-based row topics, allowing all nodes to contribute to reconstruction, while significantly reducing the load on supernodes, leading to a more efficient and more resilient DAS construct. + +## Motivation + +[EIP-7594](./eip-7594.md) PeerDAS was designed with a simple but powerful-enough erasure coding based reconstruction model where any node receiving at least half of the 128 columns should reconstruct the whole extended blob content belonging to a block. As the number of blobs grows, however, the reconstruction burden on every supernode also grows linearly with blob count. + +Moreover, supernodes execute largely redundant work: each one of them reconstructing all missing blobs, without the means to distribute this work efficiently in the network. + +This EIP introduces distributed blobspace reconstruction, where different nodes prioritize the reconstruction of different parts of the blobspace, leading to a faster, less CPU-intensive, and more resilient construct. + +## Specification + +The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", "NOT RECOMMENDED", "MAY", and "OPTIONAL" in this document are to be interpreted as described in [RFC 2119](https://www.rfc-editor.org/rfc/rfc2119) and [RFC 8174](https://www.rfc-editor.org/rfc/rfc8174). + +The EIP introduces new Gossipsub topics, changes to the rules of reconstruction, and a few minor changes to how current column topics operate. + +In this document, a *supernode* is a node subscribed to all 128 column subnets, following the customary use of the term. A *row reconstructor* is any node subscribed to 64 or more of the 128 column subnets, and thus holding enough cells to reconstruct any row on its own; every supernode is also a row reconstructor. On mainnet, the row reconstructor class notably includes staking nodes whose validator custody requirement reaches 64 or more custody groups. + +### Parameters + +| Constant | Value | +| - | - | +| `ROW_SUBNET_COUNT` | `128` | + +### Column topics + +Regarding column topics [EIP-7594](./eip-7594.md) already mandates the following: + +> Once the node obtains a column through reconstruction, the node MUST expose the new column as if it had received it over the network. If the node is subscribed to the subnet corresponding to the column, it MUST send the reconstructed DataColumnSidecar to its topic mesh neighbors. If instead the node is not subscribed to the corresponding subnet, it SHOULD still expose the availability of the DataColumnSidecar as part of the gossip emission process. + +This is extended to allow cell-level operation towards peers that support it using the following rules: + +- Prior to reconstructing, a node MAY also use advertisements received as part of the Gossipsub "fanout" mechanism to collect relevant cells from other peers across column subnets it is not subscribed to. + +- After reconstruction, a node SHOULD use the Gossipsub "fanout" mechanism to provide cells from the reconstructed blob to peers across column subnets it is not subscribed to. A node MAY choose to only advertise to a random subset of these columns rather than all columns. This allows the node to provide another path for cell dissemination to the network. A node MAY choose to delay these fanout messages in order to conserve bandwidth by not competing with other nodes who are subscribed to the column topic, and can provide the cell instead. + +### Row topics + +Similar to column subnets, we introduce new row subnets: `data_row_{subnet_id}`. These resemble, but are not to be confused with, the deprecated `blob_sidecar_{subnet_id}` topics. Their properties: + +- A row subnet MUST use Cell-Level Deltas ([EIP-8136](./eip-8136.md)) without eager push. Like for Cell-Level Deltas in column subnets, the GroupID for a message in the row subnet is the block root. Since cells might arrive from three different sources (`getBlobs`, columns, rows) a node MAY choose to delay the request of cells from rows. +- The number of row subnets is `ROW_SUBNET_COUNT`, a fixed constant independent of the blob count. Blob rows are mapped to subnets with a per-slot pseudo-random permutation, reusing the consensus-layer swap-or-not shuffle: + + ```python + def get_blob_row_subnet(blob_index: BlobIndex, slot: Slot) -> RowSubnetIndex: + seed = hash(b"ROW_SUBNET" + uint_to_bytes(uint64(slot))) + return RowSubnetIndex( + compute_shuffled_index( + uint64(blob_index) % ROW_SUBNET_COUNT, uint64(ROW_SUBNET_COUNT), seed + ) + ) + ``` + + Being a permutation, the mapping assigns distinct subnets to the blobs of a slot as long as the blob count does not exceed `ROW_SUBNET_COUNT`. Changing every slot, it distributes load evenly across the network, including which subnets are idle when the blob count is below `ROW_SUBNET_COUNT`. The seed depends only on the slot, so the mapping is computable without access to chain state. If the blob count exceeds `ROW_SUBNET_COUNT`, blob indices that are equal modulo `ROW_SUBNET_COUNT` share a subnet, so a subnet carries up to `ceil(blob_count / ROW_SUBNET_COUNT)` rows in a slot; the bitmap of a row subnet covers the cells of all rows mapped to it, ordered by `blob_index`. +- Each node derives a single designated row subnet from its node ID, reusing the hash-based derivation of custody column selection in [EIP-7594](./eip-7594.md), but with a different byte window for domain separation: + + ```python + def get_row_subnet(node_id: NodeID) -> RowSubnetIndex: + return RowSubnetIndex( + bytes_to_uint64(hash(uint_to_bytes(uint256(node_id)))[8:16]) + % ROW_SUBNET_COUNT + ) + ``` + + Since custody group derivation uses bytes `[0:8]` of the same hash, taking bytes `[8:16]` keeps the row assignment decorrelated from custody assignment at no extra hashing cost. A row reconstructor MUST subscribe to its designated row subnet. Other nodes SHOULD subscribe to theirs if they support Cell-Level Deltas. Subscription carries no custody obligation. + +Row subnets carry partial messages only; no full-message equivalent is defined for them. A node MUST NOT subscribe to a row subnet unless it supports the Partial Messages Extension, and the subscription requirements above apply only to nodes with such support. A node MAY prune row subnet mesh peers that do not support the extension. Note that the non-discrimination guidance of [EIP-8136](./eip-8136.md) applies to column topics, where full-message fallback exists; on row subnets there is nothing to fall back to, while column topics and `getBlobs` remain fully available to nodes without the extension. + +Row subnet membership is computable from a peer's node ID alone, so no ENR extension is needed for discovery. A node SHOULD locate peers of its designated row subnet by applying `get_row_subnet` to the node IDs it discovers (e.g., through discv5 lookups), and SHOULD maintain enough connections to same-subnet peers to sustain a healthy Gossipsub mesh, keeping in mind that subnet members are a small fraction (approx. `1 / ROW_SUBNET_COUNT`) of the overall node population and are unlikely to appear in sufficient numbers among randomly selected peers. + +The exact wire format — SSZ containers, bitmap encoding, and GroupID derivation — is to be defined in the ethereum/consensus-specs repository, following the approach of [EIP-8136](./eip-8136.md). The semantics this EIP requires of it are: the bitmap of a row subnet covers the rows mapped to the subnet in `blob_index` order, and within a row follows `cell_index` order; the GroupID is derived from the block root, analogously to column topics. A cell received on a row subnet MUST be verified against the corresponding blob KZG commitment before it is forwarded or used, and peers providing invalid cells MUST be penalized under the same rules as on column subnets. + +A peer MAY limit the number of cells it serves a peer on the row subnet to just half of the cells of each mapped row, as the rest of that row can be reconstructed. The limit applies per row: serving fewer than 64 cells of a row does not allow its reconstruction, regardless of cells served from other rows. + +As a node receives cells from any source (either from row subnets, column subnets, or `getBlobs`), it SHOULD send updated bitmap states to its peers. A node MAY choose to debounce these updates. + +### Reconstruction + +A node, even if not a row reconstructor, SHOULD collect at least 64 cells on each row mapped to its designated row subnet and expose these in updated bitmap states to its peers. Note that these cells need not come from the node's own custody: the row subnet pools the custody cells of all its members, so sufficient cells can be collected through the row subnet itself. + +Similar to PeerDAS, reconstruction duties attach to nodes holding enough columns, but reconstruction becomes a phased process. Reconstruction of a row is REQUIRED only once a node holds at least 64 distinct verified cells of that row; subscription alone does not imply possession, so all reconstruction obligations below are conditional on this. + +- 1st phase: a row reconstructor MUST reconstruct each row mapped to its designated row subnet for which it holds sufficient cells, and it MUST send updated bitmap states to its peers. Note that a row reconstructor can satisfy this phase from its own column subscriptions, without foreign row information. A small random delay (recommended range TBD) before reconstruction is allowed to desynchronise nodes in the network and reduce overall load. +- 2nd phase: after a slightly longer random delay (recommended range TBD), during which cells are collected from `getBlobs`, columns, and rows, a supernode SHOULD — and any other row reconstructor MAY — do a second reconstruction phase, reconstructing all missing rows for which it holds sufficient cells, and sharing the results as defined above. This matches the reconstruction behavior PeerDAS already recommends for nodes holding half the columns, with an added delay. The stronger expectation is placed on supernodes because only a node subscribed to all column subnets can observe rows completing elsewhere and cancel the redundant work; row reconstructors with fewer subscriptions lack this signal. +- 3rd phase: after a delay longer than the 2nd-phase delay (recommended range TBD), any node subscribed to a row subnet SHOULD reconstruct the rows mapped to that subnet that are still incomplete and for which it holds sufficient cells, sharing the results as defined above. This provides a reconstruction path that does not depend on row reconstructors or supernodes at all. Its expected CPU cost is near zero, since this phase activates only when the earlier phases have failed to complete a row. + +All delays are measured from the moment the node first obtains the block root of a valid block for the slot. Reconstruction obligations attach to at most one block root per slot: the block root on the node's current head branch, or the first valid block root seen. For any additional (equivocating or competing) block roots of the same slot, reconstruction is OPTIONAL, so that equivocation cannot amplify reconstruction work. A node MUST cancel a pending reconstruction of a row that completes through other means before the timer fires. The delays MUST be bounded by a maximum (value TBD), so that the recovery path is not postponed indefinitely under network degradation. + +## Rationale + +Row topics were part of the DAS discussion from the early days, well before PeerDAS was designed. FullDAS (described in the ethresear.ch post "FullDAS: towards massive scalability with 32MB blocks and beyond") introduced cell-level messaging over both column and row topics, with cross-seeding and in-network reconstruction. It also introduced bitmap representations of partial IHAVE messages, but without the exact protocol details. + +The Gossipsub Partial Message Extension introduced the mapping of bitmap-based partial message representations into Gossipsub, opening the way to use them on columns in [EIP-8136](./eip-8136.md), which builds on that extension and cites its specification. + +Until now, while we have developed the tools to implement better schemes, we remained with the original simplified PeerDAS construct. At the same time, blob count scaling made the CPU and bandwidth requirement of supernodes more of a point of contention. Reliance on supernodes, while abundant on current mainnet, is also a point of concentration leading to a protocol with less resilience than desirable. + +This EIP corrects some of these shortcomings, making sure supernodes are not doing (as much) useless work, and reconstruction is possible (although not yet mandated) even without supernodes. + +### What it is not + +This EIP is not FullDAS. It does not introduce sub-linear (cell-level) sampling. The bandwidth requirement of sampling nodes is still proportional to the number of blobs. + +It also does not introduce column-wise encoding, so protection and reconstruction is still only along the row axis. + +Finally, it does not directly help L2 nodes retrieve individual blobs (although there are possible extensions in that direction). However, it helps them run supernodes with fewer resources, leading to a net gain. + +### Possible extension to retrieve individual blobs + +With the introduction of PeerDAS, L2 nodes have the problem that retrieving a specific blob from a CL client requires them to either be a supernode, or to download it on request through columns. By introducing row topics with allocation rooted in the nodeID, it is easier for nodes to identify which node they can download the relevant blob from. We reserve this for further consideration. + +### Design decisions + +**Why are nodes without reconstruction duties part of the row topics?** + +This is to enable the possibility of reconstructing without supernodes. Even custody-minimum nodes contribute: with roughly 94 members each custodying at least 4 randomly assigned columns, a row subnet collectively covers well over 64 distinct columns with high probability, forming a virtual reconstructor even when no individual member could reconstruct alone. The additional traffic of one row, most probably already suppressed by `getBlobs`, is worth it in our opinion. + +**Why are ordinary nodes not required to reconstruct?** + +While mandated (MUST) reconstruction would be desirable from the perspective of not relying on supernodes at all, it would introduce unconditional CPU load on ordinary nodes. Instead, the 3rd reconstruction phase is a SHOULD, with a delay long enough that it activates only in the rare case when the earlier phases have not completed a row: the expected load is near zero, while the network retains a reconstruction path that works without row reconstructors. + +**Why a fixed number of row subnets, instead of one per blob?** + +Tying the subnet count to the maximum blob count would change every node's subnet assignment at each Blob Parameter Only fork, tearing down and re-forming all row meshes simultaneously, exactly when network stability matters most. A fixed count keeps node-to-subnet assignments stable across forks, and the blob count only affects the stateless per-slot blob-to-subnet mapping. The cost is somewhat thinner per-subnet coverage (nodes are spread over `ROW_SUBNET_COUNT` subnets even when fewer rows exist) and having to define the multi-row-per-subnet general case. + +**Why `ROW_SUBNET_COUNT = 128`?** + +The constant is chosen to keep the expected number of nodes per subnet in a healthy band across plausible participation scenarios. Since each node subscribes to a single row subnet, the expected team size behind a subnet is the number of participating nodes divided by `ROW_SUBNET_COUNT`. This team size has a floor: it must stay several times the Gossipsub mesh degree (accounting for the binomial spread of hash-based assignment and for churn), and it should give a high probability of at least one row reconstructor — a node with a mandatory 1st-phase duty — per subnet. Pushing the constant higher than needed thins subnets towards this floor, while a lower constant increases traffic duplication, as each active subnet's row flows to its entire team. + +With approx. 12K nodes and 3K supernodes on mainnet, `ROW_SUBNET_COUNT = 128` yields ~94 nodes and ~23 supernodes per subnet at full participation, and remains workable even at partial early adoption of Cell-Level Deltas. The value is also above currently planned maximum blob counts, so in practice each subnet carries at most one row per slot, while the construct remains well defined for higher blob counts: per-node row load is bounded by `ceil(blob_count / ROW_SUBNET_COUNT)` rows per slot. If the network size changes by an order of magnitude, raising or lowering the constant remains possible, at the cost of a one-time reshuffle of subnet assignments. + +**Why a pseudo-random per-slot permutation, and not a simple rotation?** + +With fewer rows than subnets, some subnets are idle in a given slot, so the mapping has to change over time for every node to contribute equally. A simple rotation (`(blob_index + slot) % ROW_SUBNET_COUNT`) achieves this only over a full cycle: each subnet would be active for `blob_count` consecutive slots and then idle for the rest of the cycle, making per-node load bursty. The pseudo-random permutation redraws the active subnet set every slot, evening out load also on short time horizons. A permutation (rather than an independent hash per blob) is needed to avoid mapping two blobs of a slot to the same subnet as long as the blob count does not exceed `ROW_SUBNET_COUNT`; `compute_shuffled_index` provides one that clients already implement. Note that the mapping remains publicly predictable; unpredictability would add little, since the block builder controls blob indices and could steer a blob to any of the subnets active in that slot under any public mapping. Guarantees against targeted suppression continue to come from the column topics. + +**Why only a single row, and why is it not dependent on custody?** + +As of August 2026, mainnet has approx. 12K nodes of which 3K are supernodes. The latter is much more than what we expected initially. With current and planned blob counts, even a single row creates abundant overlap. + +## Backwards Compatibility + +Row topics are limited to peers that have libp2p Gossipsub implementations supporting Cell-Level Deltas. The portion of peers that supports the extension is already reaching considerable numbers on mainnet, even before Glamsterdam. We expect the majority of peers to support it after the Glamsterdam fork and [EIP-8136](./eip-8136.md). For peers that do not support the extension, `getBlobs` and column topics are still fully available. + +## Security Considerations + +The EIP changes DAS networking, but it does not change the custody allocation and the probabilistic guarantees of PeerDAS. + +New Gossipsub topics might introduce new attack vectors. Since row distribution is a new additional recovery path, and the old paths are mainly intact, it is not expected that this adversely affects the system, except for a bounded traffic overhead: bitmap-based signaling, plus, when blob data is partially withheld, up to half a row of futile cell pulls per row subnet member per mapped row. Reconstruction CPU cannot be triggered by unavailable data, as all reconstruction duties are gated on holding at least 64 distinct verified cells of a row. +An exception to this is the phased reconstruction process. Here the 2nd phase, the full reconstruction, is explicitly delayed. This delay is, however, something implementations already practice, and it is a one-time (instead of hop-by-hop) delay. + +A withholding block producer can trigger recovery work on a doomed block: releasing 64 columns' worth of cells for all rows except one — kept below the reconstruction threshold by withholding as little as a single cell — causes the network to reconstruct and cross-seed all recoverable rows while the block still ends up unavailable. This attack is inherited from PeerDAS rather than introduced by this EIP: under PeerDAS, every supernode performs this recovery work redundantly, while here the 1st phase distributes it at roughly one row per row reconstructor and the 2nd phase skips rows observed complete, so the total work strictly decreases. Since the 2nd phase is a SHOULD, supernodes can mitigate it further: observing the bitmaps of all column subnets, they can detect the unrecoverable row and legitimately skip collecting and reconstructing for the doomed block. + +Row dissemination is an optimization and MUST NOT weaken availability guarantees: nodes MUST NOT alter sampling or availability decision rules based on row subnet state, nor delay these decisions waiting for row dissemination; column topics and request-response remain the authoritative paths. + +Bitmap-based signaling introduces load of its own. Sending an update on every received cell can lead to quadratic message complexity, so nodes SHOULD debounce and rate-limit bitmap updates, and SHOULD bound the number of GroupIDs tracked per peer, in line with the guidance of [EIP-8136](./eip-8136.md). A peer that repeatedly advertises cells it then fails to provide SHOULD be treated in local peer scoring like a peer providing untimely messages. + +Since row subnet assignment is a static, public function of the node ID, an attacker can grind node IDs to concentrate on, or eclipse, a chosen row subnet. The impact is bounded: a suppressed row subnet degrades to the status quo, as the 2nd reconstruction phase and the column topics cover the affected rows. + +Since the row subnet count is a fixed constant and node-to-subnet assignments do not depend on the blob count, Blob Parameter Only forks ([EIP-7892](./eip-7892.md)) do not affect row subnet subscriptions; only the blob-to-subnet mapping changes with the blob count, and that mapping is stateless per slot. + +## Copyright + +Copyright and related rights waived via [CC0](../LICENSE.md).