Introduce the ability to batch attestations at source, allowing operators running multiple validators scheduled in the same slot committee to publish a single pre-aggregated attestation in place of N individual ones. We extend the existing attestation gossip topic via SSZ union type WireAttestation to carry either a SingleAttestation or BatchAttestation. Each batched validator pre-signs a batch_seal cryptographically authorizing a designated batcher to aggregate them; the batcher signs the resulting composition (batcher_signature) to close over it. The standard attestation signature is unchanged and on-chain compatible: aggregators discard the seal and the batcher signature after gossip validation and feed the batch into the existing aggregation pipeline. Gossip dedup operates on a single principle: a message is accepted only if it conveys at least one previously unseen vote for the duty. No new slashing conditions are necessary.
Ethereum has ~1M active validators today. With 100% participation, every slot triggers N x 1/32 attestations (around 31k), distributed over 64 subnets handling ~485 attestations each. Large operators run many validators, all typically sharing the same consensus view, and therefore typically voting in unison for head.
Today, the protocol supports only individual attestation messages per validator. This rule protects the network from being spammed by malicious actors sending overlapping aggregates: without it, an attacker could harvest attestations and rebundle them into O(2^k) valid subset permutations.
But as we push towards shorter slots and faster finality, we need to drastically reduce the volume of attestations while maintaining protocol and consensus integrity. EIP-7251 enabled reduction via validator balance consolidation, but its opt-in nature has kept uptake slow.
This proposal achieves consolidation-like effects at the network level: operators pre-aggregate attestations for concurrently-scheduled validators at source. No operator action is required; a beacon node managing multiple validator clients can pre-aggregate transparently.
Consensus throughput. Batching also expands the design space for consensus throughput. Because each network byte now carries more attestation information, if we hold traffic constant, we can consolidate subnets (and committees) by the same efficiency factor (e.g. 64 → 16 at 4x efficiency), and in turn shorten the epoch by the same factor (e.g. 32 → 8 slots). Faster L1 finality would follow as a side effect. How aggressively this efficiency can drive consensus parameter changes remains to be seen.
Relationship to attester cap. This mechanism is not a substitute for an eventual active validator cap. The strawmap proposes a 128K cap (J*, at the moment of writing), setting a hard physical invariant that may prove indispensable to unlock downstream designs like dynamic availability or PQ consensus. Batching, by contrast, captures existing headroom without physical validator set reduction.
Privacy. Standardizing this mechanism also creates a primitive for k-anonymity-style attester privacy. Any attester can pick a co-committee member as a batcher and channel its attestation through them instead of publishing openly to the gossip network. This dispatch itself can ride on oblivious routing protocols for added unlinkability. Operators could advertise batching endpoints and, with sufficient validator density across committees, offer this service with high probabilistic continuity. If the origin attester notices that its attestation was not included in the expected batch, it can fall back to publishing individually just-in-time with no penalty. Some additional protocol changes would harden this pattern further (for instance, slashing attesters who emit equivocating consents), but this EIP is a suitable baseline.
These changes are applied to the consensus-specs.
| Name | Value | Description |
|---|---|---|
DOMAIN_BATCH_ATTESTER |
DomainType('0x0B000000') |
Domain for batch seal signatures |
DOMAIN_BATCHER |
DomainType('0x0B0000FF') |
Domain for batcher signatures |
SingleAttestation remains unchanged:
class SingleAttestation(Container):
committee_index: CommitteeIndex
attester_index: ValidatorIndex
data: AttestationData
signature: BLSSignature
The seal preimage. Bound to (slot, committee_index, batcher) only, not to aggregation_bits, so seals can be pre-signed at epoch start and the process can tolerate arbitrary validator client failures:
class BatchSealPreimage(Container):
slot: Slot
committee_index: CommitteeIndex
batcher: ValidatorIndex
The batcher signature preimage. Commits to the specific composition for this duty:
class BatcherPreimage(Container):
slot: Slot
committee_index: CommitteeIndex
aggregation_bits: Bitlist[MAX_VALIDATORS_PER_COMMITTEE]
New BatchAttestation:
class BatchAttestation(Container):
committee_index: CommitteeIndex
aggregation_bits: Bitlist[MAX_VALIDATORS_PER_COMMITTEE]
data: AttestationData
# Standard BLS aggregate of signatures over `data` under DOMAIN_BEACON_ATTESTER.
# Identical to the aggregate signature of included validators.
signature: BLSSignature
# Identifies the validator authorized to compose this batch.
batcher: ValidatorIndex
# Aggregate of seals from every validator indicated by `aggregation_bits`,
# over `BatchSealPreimage(data.slot, committee_index, batcher)`,
# under DOMAIN_BATCH_ATTESTER.
# Gossip-only; discarded after validation.
batch_seal: BLSSignature
# Batcher's signature over `BatcherPreimage(data.slot, committee_index, aggregation_bits)`,
# under DOMAIN_BATCHER.
# Gossip-only; discarded after validation.
batcher_signature: BLSSignature
SSZ union for wire transport:
WireAttestation = Union[SingleAttestation, BatchAttestation]
WireAttestation is serialized with a one-byte selector prefix:
| Selector | Type |
|---|---|
0x00 |
SingleAttestation |
0x01 |
BatchAttestation |
def is_valid_single_attestation(state: BeaconState, att: SingleAttestation) -> bool:
"""Validates a SingleAttestation (existing logic)."""
committee = get_beacon_committee(state, att.data.slot, att.committee_index)
if att.attester_index not in committee:
return False
pubkey = state.validators[att.attester_index].pubkey
return bls.Verify(pubkey, compute_signing_root(att.data), att.signature)
def is_valid_batch_attestation(state: BeaconState, att: BatchAttestation) -> bool:
"""Validates a BatchAttestation."""
# At least two bits set (otherwise use SingleAttestation)
if att.aggregation_bits.count() < 2:
return False
# Resolve attester indices
committee = get_beacon_committee(state, att.data.slot, att.committee_index)
attesters = [committee[i] for i, bit in enumerate(att.aggregation_bits) if bit]
# Batcher must be in the attester set
if att.batcher not in attesters:
return False
pubkeys = [state.validators[i].pubkey for i in attesters]
epoch = compute_epoch_at_slot(att.data.slot)
# Verify aggregate attestation signature (DOMAIN_BEACON_ATTESTER, over data)
if not bls.FastAggregateVerify(pubkeys, compute_signing_root(att.data), att.signature):
return False
# Verify aggregate batch seal (DOMAIN_BATCH_ATTESTER, over BatchSealPreimage)
seal_preimage = BatchSealPreimage(
slot=att.data.slot,
committee_index=att.committee_index,
batcher=att.batcher,
)
seal_domain = get_domain(state, DOMAIN_BATCH_ATTESTER, epoch)
if not bls.FastAggregateVerify(
pubkeys, compute_signing_root(seal_preimage, seal_domain), att.batch_seal
):
return False
# Verify batcher's composition signature (DOMAIN_BATCHER, over BatcherPreimage)
batcher_preimage = BatcherPreimage(
slot=att.data.slot,
committee_index=att.committee_index,
aggregation_bits=att.aggregation_bits,
)
batcher_pubkey = state.validators[att.batcher].pubkey
batcher_domain = get_domain(state, DOMAIN_BATCHER, epoch)
if not bls.Verify(
batcher_pubkey, compute_signing_root(batcher_preimage, batcher_domain), att.batcher_signature
):
return False
return True
def is_valid_wire_attestation(state: BeaconState, att: WireAttestation) -> bool:
if att.selector == 0x00:
return is_valid_single_attestation(state, att.value)
else:
return is_valid_batch_attestation(state, att.value)
None. BatchAttestation.signature is identical in domain and message to a standard aggregated attestation signature, so aggregators feed it directly into compute_on_chain_aggregate and the on-chain Attestation container is unchanged. process_attestation is unchanged. The batch_seal and batcher_signature fields are gossip-only and never reach the chain.
Modify beacon_attestation_{subnet_id} topic:
SingleAttestation to WireAttestation.def validate_beacon_attestation(att: WireAttestation, subnet_id: uint64) -> Result:
match att.selector:
case 0x00:
return validate_single_attestation(att.value, subnet_id)
case 0x01:
return validate_batch_attestation(att.value, subnet_id)
case _:
return REJECT
The following checks produce a REJECT outcome upon failure:
aggregation_bits not in the committee for (slot, committee_index).batcher not in the attester set, or its bit not set.Given data_root = hash_tree_root(att.data), a node maintains a per-(slot, committee_index, data_root) cache, containing fields:
seen_attesters: Set[ValidatorIndex]. Every validator whose vote has been observed via any accepted WireAttestation.seen_batchers: Set[ValidatorIndex]. Every batcher whose batch has been accepted for this slot.The following principles apply:
batcher for the slot and committee.Gossip rules:
SingleAttestation, and att.attester_index is in seen_attesters.BatchAttestation, and att.batcher is in seen_batchers.BatchAttestation, and every validator indicated by att.aggregation_bits is in seen_attesters.These rules result in the following behaviour, awarding strong practical security without extending slashing conditions:
On acceptance:
SingleAttestation adds attester_index to seen_attesters.BatchAttestation adds batcher to seen_batchers and all members indicated by aggregation_bits to seen_attesters.Forwarders are never penalized for relaying a message that turns out to be locally redundant under another node's cache state.
Consent should be cryptographic, not implicit. Every validator in a batch signs an authorization binding them to the specific batcher for a specific slot.
SingleAttestation cannot replay V's signature into a batch. The seal is over a different domain (DOMAIN_BATCH_ATTESTER) and a different message (BatchSealPreimage), neither of which V signs when producing a single attestation.(slot, committee_index, batcher) only, and not to aggregation_bits?Binding the seal to the bitfield would force an extra per-slot synchronous signing round between batcher and effective members. This extra coordination step introduces a stateful interaction, eats into the attestation latency budget, and further complicates the design without visible gain.
This choice also enables seal preparation ahead of time: validators can sign their seals authorizing batcher B for their duty slot in advance (when under finality), offloading this signature from the critical path.
A vote is the underlying object: validator V's signature over data D for (slot, committee). Singles and batches are two encodings of votes; dedup should operate on votes, not encodings. The rule "accept iff the message contains at least one unseen vote" expresses this directly and yields several properties for free:
Every problematic case is resolved by lighter means:
seen_batchers. The batcher signature makes this evidence durable enough to slash, but we leave that as an open question rather than enabling it at this time.A mandatory slashing primitive would add operator-side risk surface and false-positive exposure with no compensating safety benefit.
The beacon_attestation_{subnet_id} topic message type changes from SingleAttestation to WireAttestation. Nodes must upgrade simultaneously at the fork boundary.
SingleAttestation serialization with selector 0x00:
0x00 || ssz(SingleAttestation)
Pre-fork nodes expect raw SingleAttestation bytes. Post-fork nodes expect the selector prefix. Clean separation at the fork boundary.
The on-chain Attestation container and process_attestation are unchanged, so block validity rules and historical block processing are unaffected.
An attacker controlling k co-committee keys can produce at most k accepted messages per duty: every accepted message must add at least one previously unseen attester to seen_attesters, and the attacker's pool of valid attesters has size k. Each accepted message carries valid attestation, seal, and batcher signatures, so the attacker's signing cost grows with their bandwidth output. The naive O(2^k) subset blowup never propagates: given the attacker's k accepted messages, every other subset they could construct has its members fully covered by seen_attesters from prior accepts and is dropped silently.
A passive observer harvesting V's SingleAttestation signature cannot construct a batch including V. The batch seal field requires a separate signature from V over BatchSealPreimage under DOMAIN_BATCH_ATTESTER, which V produces only when authorizing a batcher. Standard attestation flow does not produce a seal.
A validator V issuing both a SingleAttestation and a seal to batcher B (for example, due to misconfigured active-active validator client redundancy) does not cause B's batch to be suppressed. The batch carries V's vote alongside others; if V's single arrives first, the batch is still accepted because its other members' votes are unseen. Conversely, if the batch arrives first, V's redundant single is dropped. In all orderings, no vote is lost, no party is penalized, and the on-chain Attestation reflects V exactly once.
Protocol-level correctness is preserved across operator misconfiguration: leaked singles, redundant seals, and overlapping batches are absorbed by the dedup rule without harm. The remaining concerns are about efficiency, not correctness:
Recommended practices:
Distributed validator technology (DVT) and pooled staking products run inner consensus among co-custodians before broadcasting an attestation. The mechanism slots into both modes: on the synchronous path, the pre-aggregator can be selected after inner consensus completes, piggybacking on the signed attestation that would have been broadcast anyway; on the asynchronous path, batch_seals can be pre-signed during finality, when coordination is cheap. Detailed integration with specific DVT clusters, LST operators, and other shared-custody architectures still needs validation.
Because the seal does not commit to aggregation_bits, validators may sign seals ahead of time. To handle batcher liveness failures within an epoch, operators may have validators pre-sign seals for both a primary and a fallback batcher at epoch start. If the primary fails, the fallback takes over without additional seal signing latency. The fallback batcher must independently produce its own composition signature when it acts. This is operator policy, not protocol; the protocol simply accepts any valid combination of seal aggregate and batcher signature.
If a validator has not pre-signed a seal for any active batcher when its duty arrives, fallback to SingleAttestation is automatic and incurs no penalty.
Attackers cannot include honest validators in malicious batches because they cannot obtain seal signatures without the validators' private keys. Even with seals in hand, an attacker without the batcher's key cannot produce a valid batcher_signature. BLS signatures cannot be forged.
Batches reveal which validators are co-located under the same operator. However, this does not weaken privacy as much as one would think, since the validator identities for medium-large operators are publicly known. That said, this is an inherent privacy/efficiency trade-off. From a staking perspective, the risk can be somewhat equated to that of validator balance consolidation.
The same primitive cuts the other way. A pre-aggregator need not share an operator with the validators it batches: any committee member can collect signed seals from any consenting subset of peers, regardless of operator boundary. Operators with enough density to expect a member in every committee can therefore offer a k-anonymity service to other attesters, obscuring operator origin behind a shared aggregation_bits bundle. Realizing this requires off-protocol coordination not covered here, but the EIP does not preclude it.
Copyright and related rights waived via CC0.