---
eip: 8243
title: Batching Attestations at Source
description: Allow validators scheduled for duty on the same committee to publish a single pre-aggregated attestation in place of N individual ones
author: Raúl Kripalani (@raulk), Toni Wahrstätter (@nerolation), Mikhail Kalinin (@mkalinin)
discussions-to: https://ethereum-magicians.org/t/eip-8243-batching-attestations-at-source/28606
status: Draft
type: Standards Track
category: Core
created: 2026-05-01
---

## 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][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:

```python
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:

```python
class BatchSealPreimage(Container):
    slot: Slot
    committee_index: CommitteeIndex
    batcher: ValidatorIndex
```

The batcher signature preimage. Commits to the specific composition for this duty:

```python
class BatcherPreimage(Container):
    slot: Slot
    committee_index: CommitteeIndex
    aggregation_bits: Bitlist[MAX_VALIDATORS_PER_COMMITTEE]
```

New `BatchAttestation`:

```python
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:

```python
WireAttestation = Union[SingleAttestation, BatchAttestation]
```

### SSZ union serialization

`WireAttestation` is serialized with a one-byte selector prefix:

| Selector | Type                |
| -------- | ------------------- |
| `0x00`   | `SingleAttestation` |
| `0x01`   | `BatchAttestation`  |

### Helper functions

```python
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)
```

```python
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
```

```python
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.

```python
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.

<!-- TODO: Test cases and reference implementation -->

## 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_seal`s 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

Copyright and related rights waived via [CC0][cc0].

[eip-7251]: ./eip-7251.md
[cc0]: ../LICENSE.md
