Add EIP: Batching Attestations at Source#11589
Conversation
File
|
| @@ -0,0 +1,397 @@ | |||
| --- | |||
| eip: 9999 | |||
There was a problem hiding this comment.
| eip: 9999 | |
| eip: 8243 |
Assigning next sequential EIP/ERC/RIP number.
Numbers are assigned by editors & associates.
Please also update the filename.
| 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: |
There was a problem hiding this comment.
Please create a discussions topic on Eth Magicians using the template
https://ethereum-magicians.org/c/eips/5
|
|
||
| ### 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. |
There was a problem hiding this comment.
What if we make this rule more aggressive: like, a batch is ignored if less than a half of its attesting indices are unseen. In this case SingleAttestation and any batch containing that attestation won’t suppress each other, at the same time the attack complexity reduces to O(log(k)).
I believe fallback batchers will have the same subset of attesters and if either of those two batches reaches different part of the network first, there doesn’t seem to be any harm from suppressing another batch with the same information.
There was a problem hiding this comment.
Right, good flag. If V both submits a SingleAttestation and participates in a batch, the current proposal races. If the SingleAttestation arrives first, it can block the batch from being accepted, unless the client deconflicts and evicts it in favour of the stronger batch.
This race could occur if V decides to attest independently in addition to participating in a batch. This could happen as an operational fallback, or as malicious behaviour.
Generally speaking, the current version assumes high trust between the VC and the beacon node. That assumption may not hold in DVT, LST, and other staking designs. It would be good to loop in various teams and pressure-test against their specific assumptions.
| - **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`? |
| | ------------------------------- | -------------------------- | ------------------------------------ | | ||
| | `DOMAIN_BATCH_ATTESTER` | `DomainType('0x0B000000')` | Domain for batch seal signatures | | ||
| | `DOMAIN_BATCHER` | `DomainType('0x0B0000FF')` | Domain for batcher signatures | | ||
| | `BATCH_SUBNET_REDUCTION_FACTOR` | TBD | Power-of-2 reduction in subnet count | |
There was a problem hiding this comment.
I really think this change should be in a separate EIP as it is suggested below. I would like to see the mainnet effect of batching at the source in the first place, and then reduce the subnets as much as that effect allows to.
There was a problem hiding this comment.
Agreed -- I left it in the first draft for illustration, to make the potential downstream impact impact of this EIP easier to follow. Higher information density per committee => fewer messages => fewer bytes on the wire per committee => merge committees while keeping traffic constant => fewer slots per epoch => shorter epochs. What if we keep it as as placeholder for now, so it remains easy to frame the benefits in a single spec, then spin it off later?
There was a problem hiding this comment.
I think we should remove it from the spec level at some point before moving the EIP status from Draft to Review. The information about this EIP enabling other positive changes can sit in the Motivation section of the EIP
| SSZ union for wire transport: | ||
|
|
||
| ```python | ||
| WireAttestation = Union[SingleAttestation, BatchAttestation] |
There was a problem hiding this comment.
Not sure if union is widely supported across all CL clients but even if it is not, it should be easy to support this specific case.
There was a problem hiding this comment.
This isn't used anywhere else, in general, in Ethereum in SSZ. It doesn't seem that warranted here, either.
There was a problem hiding this comment.
More typically, the consensus specs would have new subnets for new SSZ types, so there would be BatchAttestation-carrying subnets/(currently-)libp2p topics; is there a reason this is avoided here? It fits more naturally into the rest of the consensus-specs than this approach.
jochem-brouwer
left a comment
There was a problem hiding this comment.
Some editorial comments, also address @abcoathup comments 😄 👍
| @@ -0,0 +1,397 @@ | |||
| --- | |||
| eip: 9999 | |||
| title: Batching attestations at source | |||
There was a problem hiding this comment.
| title: Batching attestations at source | |
| title: Batching attestations at Source |
EIP-1 style, title should be Title Case
| type: Standards Track | ||
| category: Consensus | ||
| created: 2026-05-01 | ||
| requires: |
There was a problem hiding this comment.
If no required EIPs then this should be removed
| requires: |
| requires: | ||
| --- | ||
|
|
||
| # EIP-XXXX: Batching attestations at source |
There was a problem hiding this comment.
This title will get rendered by the site and should be removed
| # EIP-XXXX: Batching attestations at source |
|
|
||
| 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. | ||
|
|
||
| As we push towards shorter slots and faster finality, we need to drastically reduce the volume of attestations while maintaining protocol and consensus integrity. While EIP-7251 achieves this via validator balance consolidation, uptake has been relatively slow. |
There was a problem hiding this comment.
| As we push towards shorter slots and faster finality, we need to drastically reduce the volume of attestations while maintaining protocol and consensus integrity. While EIP-7251 achieves this via validator balance consolidation, uptake has been relatively slow. | |
| As we push towards shorter slots and faster finality, we need to drastically reduce the volume of attestations while maintaining protocol and consensus integrity. While [EIP-7251](./eip-7251.md) achieves this via validator balance consolidation, uptake has been relatively slow. |
(link EIPs on first ref)
|
|
||
| This change is largely orthogonal to batching and may be split into a separate EIP. | ||
|
|
||
| ## Backwards compatibility |
There was a problem hiding this comment.
| ## Backwards compatibility | |
| ## Backwards Compatibility |
(sections should be Title Case)
|
|
||
| TBD. | ||
|
|
||
| ## Reference implementation |
There was a problem hiding this comment.
| ## Reference implementation | |
| ## Reference Implementation |
(sections should be Title Case)
|
|
||
| Fewer subnets means higher message volume per subnet. Determining the optimal `BATCH_SUBNET_REDUCTION_FACTOR` requires simulation. | ||
|
|
||
| ## Test cases |
There was a problem hiding this comment.
| ## Test cases | |
| ## Test Cases |
(sections should be Title Case)
|
|
||
| TBD. | ||
|
|
||
| ## Open questions |
There was a problem hiding this comment.
(this should be Title Case as well)
This is not a mandatory section, I would remove it unless there is content there (which might also better fit in Rationale depending on the question)
|
|
||
| ## Copyright | ||
|
|
||
| Copyright and related rights waived via [CC0](https://creativecommons.org/publicdomain/zero/1.0/). |
There was a problem hiding this comment.
| Copyright and related rights waived via [CC0](https://creativecommons.org/publicdomain/zero/1.0/). | |
| ## Copyright | |
| Copyright and related rights waived via [CC0](../LICENSE.md). |
(the URL is not allowed)
|
|
||
| ### 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 VC 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. |
There was a problem hiding this comment.
What's the VC here? Is it a typo? If it is an abbreviation then it should be defined before abbreviating it
There was a problem hiding this comment.
Switched to full form. VC = Validator Client. Refs:
https://lighthouse-book.sigmaprime.io/help_vc.html
https://prysm.offchainlabs.com/docs/learn/dev-concepts/prysm-validator-client/
b32d640 to
a0e1e50
Compare
|
The commit a0e1e50 (as a parent of 0179e95) contains errors. |
mkalinin
left a comment
There was a problem hiding this comment.
Great work! 👍 I left a few comments, other than that looks good to me
| # Identifies the validator authorized to compose this batch. | ||
| batcher: ValidatorIndex | ||
| # Aggregate of seals from every validator indicated by `aggregation_bits`, | ||
| # over `BatchSealPreimage(slot, committee_index, batcher)`, |
There was a problem hiding this comment.
| # over `BatchSealPreimage(slot, committee_index, batcher)`, | |
| # over `BatchSealPreimage(data.slot, committee_index, batcher)`, |
I think the slot must match data.slot
There was a problem hiding this comment.
Good catch, fixed in f080c86. The BatchAttestation container carries slot indirectly via data.slot, so the docstring should reflect that.
| # under DOMAIN_BATCH_ATTESTER. | ||
| # Gossip-only; discarded after validation. | ||
| batch_seal: BLSSignature | ||
| # Batcher's signature over `BatcherPreimage(slot, committee_index, aggregation_bits)`, |
There was a problem hiding this comment.
| # Batcher's signature over `BatcherPreimage(slot, committee_index, aggregation_bits)`, | |
| # Batcher's signature over `BatcherPreimage(data.slot, committee_index, aggregation_bits)`, |
There was a problem hiding this comment.
Fixed in f080c86, same rationale as above.
| ### Helper functions | ||
|
|
||
| ```python | ||
| def is_batch(att: WireAttestation) -> bool: |
There was a problem hiding this comment.
Looks like this function is unused
There was a problem hiding this comment.
You are right, is_batch is dead: the validators dispatch on att.selector directly. Removed in f080c86.
Replace the strawmap inline link with plain text and add the ethereum-magicians thread ID (28606) to satisfy markdown-rel-links and preamble-re-discussions-to. Address mkalinin review comments: refer to `data.slot` in the BatchSealPreimage and BatcherPreimage docstring blocks (since BatchAttestation carries the slot via `data`), and drop the unused `is_batch` helper. Convert remaining inline links to reference-style at the bottom.
|
|
||
| | Name | Value | Description | | ||
| | ----------------------- | -------------------------- | -------------------------------- | | ||
| | `DOMAIN_BATCH_ATTESTER` | `DomainType('0x0B000000')` | Domain for batch seal signatures | |
There was a problem hiding this comment.
This clashes with DOMAIN_BEACON_BUILDER
| return False | ||
|
|
||
| pubkey = state.validators[att.attester_index].pubkey | ||
| return bls.Verify(pubkey, compute_signing_root(att.data), att.signature) |
There was a problem hiding this comment.
compute_signing_root is missing domain
|
|
||
| # 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] |
There was a problem hiding this comment.
Should there be a length check?
if len(att.aggregation_bits) != len(committee):
return False
| ``` | ||
|
|
||
| ```python | ||
| def is_valid_wire_attestation(state: BeaconState, att: WireAttestation) -> bool: |
There was a problem hiding this comment.
Maybe safer this way?
| def is_valid_wire_attestation(state: BeaconState, att: WireAttestation) -> bool: | |
| def is_valid_wire_attestation(state: BeaconState, att: WireAttestation) -> bool: | |
| if att.selector == 0x00: | |
| return is_valid_single_attestation(state, att.value) | |
| elif att.selector == 0x01: | |
| return is_valid_batch_attestation(state, att.value) | |
| else: | |
| return False |
No description provided.