Alert Source Discuss
⚠️ Draft Standards Track: Core

EIP-8243: Batching Attestations at Source

Allow validators scheduled for duty on the same committee to publish a single pre-aggregated attestation in place of N individual ones

Authors Raúl Kripalani (@raulk), Toni Wahrstätter (@nerolation), Mikhail Kalinin (@mkalinin)
Created 2026-05-01
Discussion Link https://ethereum-magicians.org/t/eip-8243-batching-attestations-at-source/28606

Abstract

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.

Motivation

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.

Specification

These changes are applied to the consensus-specs.

New constants

Name Value Description
DOMAIN_BATCH_ATTESTER DomainType('0x0B000000') Domain for batch seal signatures
DOMAIN_BATCHER DomainType('0x0B0000FF') Domain for batcher signatures

New containers

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]

SSZ union serialization

WireAttestation is serialized with a one-byte selector prefix:

Selector Type
0x00 SingleAttestation
0x01 BatchAttestation

Helper functions

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)

State transition changes

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.

P2P changes

Modify beacon_attestation_{subnet_id} topic:

  • Message type changes from SingleAttestation to WireAttestation.
  • Validation dispatches on union selector.
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

Gossip validation rules

The following checks produce a REJECT outcome upon failure:

  • Malformed SSZ or unknown selector.
  • Invalid aggregate attestation signature.
  • Invalid aggregate batch seal.
  • Invalid batcher signature.
  • Validators in aggregation_bits not in the committee for (slot, committee_index).
  • batcher not in the attester set, or its bit not set.
  • Attestation outside the permitted slot window.
  • Wrong subnet for the committee.

Attestation deduplication

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:

  • Accept a message if and only if it contains at least one previously unseen vote. This results in forwarding, even though the message may later fail processing if the contained attestation conflicts with our accumulated aggregate.
  • Accept only the first received batch signed by a given batcher for the slot and committee.

Gossip rules:

  • [IGNORE] This is a SingleAttestation, and att.attester_index is in seen_attesters.
  • [IGNORE] This is a BatchAttestation, and att.batcher is in seen_batchers.
  • [IGNORE] This is a 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:

  1. The spam potential is bounded by the size of the committee, since every committee attester may participate at most once as a single attester and at most once as a batcher.
  2. It is irrational for legitimate batchers to produce multiple batch subsets, because they do not control which one will ultimately be included.
  3. Malicious behaviour is directly attributable to a validator. We do not currently use this to slash or penalize on gossip, but we may choose to do so in the future.

On acceptance:

  • A SingleAttestation adds attester_index to seen_attesters.
  • A 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.

Rationale

Why a per-validator seal?

Consent should be cryptographic, not implicit. Every validator in a batch signs an authorization binding them to the specific batcher for a specific slot.

  • Non-consensual inclusion. A malicious operator cannot construct a batch claiming validators outside their control. BLS forgery is infeasible.
  • Signature theft. An attacker observing V’s 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.

Why bind the seal to (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.

Why the subset dedup rule?

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:

  • A leaked single from a batched validator does not suppress its parent batch, because the batch carries the votes of N-1 other validators that are still unseen.
  • Two operators with overlapping batched validators both propagate as long as each carries unique members, and therefore each has the potential of being helpful downstream; the redundant overlap is deconflicted at aggregation time. Amplification is still bounded by 2 x committee size.
  • Subsequent partial-overlap or strict-subset republications are dropped silently with no peer scoring impact.
  • Worst-case attacker amplification scales with the attacker’s own signing cost: every accepted message must carry valid attestation, seal, and batcher signatures over its members, so wire bandwidth tracks computational expenditure rather than exceeding it.

Why no slashing condition?

Every problematic case is resolved by lighter means:

  • Non-consensual inclusion is structurally impossible: a batcher cannot include a validator without that validator’s seal.
  • Signature theft (replaying a single’s signature into a batch) is structurally impossible: the seal is over a different domain and message.
  • Same-batcher composition equivocation is dropped at gossip via 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.
  • Cross-batcher overlap is benign under the subset rule: both batches propagate if each adds unseen votes, and on-chain aggregation collapses duplicates.
  • Validator-side seal equivocation (signing seals for two batchers in the same duty) is internal operator failure, gossip-bounded to O(k), and produces no harm worth chain-level penalty.

A mandatory slashing primitive would add operator-side risk surface and false-positive exposure with no compensating safety benefit.

Backwards Compatibility

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.

Security Considerations

Spam bound

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.

Signature theft inviable

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.

Leaked singles do not suppress batches

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.

Operational discipline

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:

  • A validator V issuing seals to two batchers wastes seal signatures and bandwidth, but produces no penalty.
  • A validator V publishing a single while also being batched wastes bandwidth, but does not suppress its batch.
  • Two batches from the same operator (different batchers) with overlapping members propagate redundant information.
  • Same-batcher composition equivocation (batcher publishes two distinct compositions for the same duty) wastes the batcher’s signing effort and produces durable evidence that could be slashed if a future EIP enables it.

Recommended practices:

  • Single attestation code path per validator per slot.
  • Deterministic batcher selection within an operator’s validator set.
  • Disable individual attestation for validators designated as batch members.
  • Pre-sign seals at epoch start for the chosen batcher (and optionally for a designated fallback).
  • The batcher’s signing key must be available at slot time wherever the batch is composed.
  • Never share validator signing keys across operational boundaries.

DVT and pooled staking compatibility

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.

Seal pre-signing and batcher failover

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.

Griefing resistance

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.

Validator colocation leakage

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.

Citation

Please cite this document as:

Raúl Kripalani (@raulk), Toni Wahrstätter (@nerolation), Mikhail Kalinin (@mkalinin), "EIP-8243: Batching Attestations at Source [DRAFT]," Ethereum Improvement Proposals, no. 8243, May 2026. Available: https://eips.ethereum.org/EIPS/eip-8243.