Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
b5b6d8a
networking: update committee docs
kamilsa Jan 13, 2026
4867d7d
networking: add committee size configuration
kamilsa Jan 13, 2026
7bcedca
store committee attestations
kamilsa Jan 13, 2026
980b5e8
Add aggregation in 2nd interval
kamilsa Jan 13, 2026
60468af
Committee aggregation
kamilsa Jan 13, 2026
213504a
Rename aggregation committee size to count for clarity
kamilsa Jan 13, 2026
4fac983
Remove committee signatures
kamilsa Jan 14, 2026
f2651d8
Refactor build_block:
kamilsa Jan 14, 2026
cc7548c
Clarify attestation broadcasting and update Devnet reference
kamilsa Jan 14, 2026
cb1a21b
remove adding proposer signatures to gossip_signatures
kamilsa Jan 14, 2026
e398823
Refactor subnet ID computation and rename committee signatures variable
kamilsa Jan 14, 2026
90fc114
Store proposer signature if same subnet
kamilsa Jan 14, 2026
cdae6a4
Update build block with selecting aggregations
kamilsa Jan 14, 2026
b24d3ed
Uncomment on_attestation during on_gossip_aggregation
kamilsa Jan 14, 2026
5c952ff
Update gossipsub topic names to reflect devnet3
kamilsa Jan 14, 2026
8a0c121
Rename aggregation committee to attestation committee and update rela…
kamilsa Jan 15, 2026
cb952f8
Merge remote-tracking branch 'origin/main' into committee-aggregation
kamilsa Jan 15, 2026
9d721bd
refactor: rename committee aggregation topic to aggregated attestation
kamilsa Jan 15, 2026
baddbeb
update validator.md to clarify subnet usage in attestation committees
kamilsa Jan 15, 2026
6556e81
feat: add threshold ratio for committee signature aggregation
kamilsa Jan 16, 2026
3477d6e
feat: replace attestation_subnet_count with attestation_committee_cou…
kamilsa Jan 16, 2026
9174f5b
feat: add committee signature threshold ratio chain config
kamilsa Jan 16, 2026
3115ef5
feat: aggregate on gossip
kamilsa Jan 16, 2026
3fffe71
docs: clarify aggregator role in validator participation
kamilsa Jan 22, 2026
d0462aa
Revert "feat: aggregate on gossip"
kamilsa Jan 23, 2026
e2fd644
Revert "feat: add committee signature threshold ratio chain config"
kamilsa Jan 23, 2026
d40199c
Revert "feat: replace attestation_subnet_count with attestation_commi…
kamilsa Jan 23, 2026
d46dd08
Revert "feat: add threshold ratio for committee signature aggregation"
kamilsa Jan 23, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 26 additions & 5 deletions docs/client/networking.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ Each node entry contains an ENR. This is an Ethereum Node Record. It includes:
- The node's public key
- Network address
- Port numbers
- Committee assignments (for aggregators)
- Other metadata

In production, dynamic discovery would replace static configuration.
Expand Down Expand Up @@ -62,15 +63,35 @@ Messages are organized by topic. Topic names follow a pattern that includes:

This structure lets clients subscribe to relevant messages and ignore others.

The payload carried in the gossipsub message is the SSZ-encoded,
Snappy-compressed message, which type is identified by the topic:

| Topic Name | Message Type | Encoding |
|------------------------------------------------------------|-----------------------------|--------------|
| /lean/consensus/devnet3/blocks/ssz_snappy | SignedBlockWithAttestation | SSZ + Snappy |
| /lean/consensus/devnet3/attestations/ssz_snappy | SignedAttestation | SSZ + Snappy |
| /lean/consensus/devnet3/attestation_{subnet_id}/ssz_snappy | SignedAttestation | SSZ + Snappy |
| /lean/consensus/devnet3/aggregation/ssz_snappy | SignedAggregatedAttestation | SSZ + Snappy |

### Message Types

Two main message types exist:
Three main message types exist:

* _Blocks_, defined by the `SignedBlockWithAttestation` type, are proposed by
validators and propagated on the block topic. Every node needs to see blocks
quickly.

Blocks are proposed by validators. They propagate on the block topic. Every
node needs to see blocks quickly.
* _Attestations_, defined by the `SignedAttestation` type, come from all
validators. They propagate on the global attestation topic. Additionally,
each committee has its own attestation topic. Validators publish to their
committee's attestation topic and global attestation topic. Non-aggregating
validators subscribe only to the global attestation topic, while aggregators
subscribe to both the global and their committee's attestation topic.

Attestations come from all validators. They propagate on the attestation topic. High volume
but small messages.
* _Committee aggregations_, defined by the `SignedAggregatedAttestation` type,
created by committee aggregators. These combine attestations from committee
members. Aggregations propagate on the aggregation topic to which every
validator subscribes.

### Encoding

Expand Down
43 changes: 33 additions & 10 deletions docs/client/validator.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,9 @@

## Overview

Validators participate in consensus by proposing blocks and producing attestations. This
document describes what honest validators do.
Validators participate in consensus by proposing blocks and producing attestations.
Optionally validators can opt-in to behave as aggregators in their committee .
This document describes what honest validators do.

## Validator Assignment

Expand All @@ -16,6 +17,32 @@ diversity helps test interoperability.
In production, validator assignment will work differently. The current approach
is temporary for devnet testing.

## Attestation Committees and Subnets

Attestation committee is a group of validators contributing to the common
aggregated attestations. Subnets are network channels dedicated to specific committees.

In the devnet-3 design, however, there is one global subnet for signed
attestations propagation, in addition to publishing into per committee subnets.
This is due to 3SF-mini consensus design, that requires 2/3+ of all
Copy link
Contributor

@g11tech g11tech Jan 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this global bit is not required, once the aggregtors publish signed attestations in the 2nd interval, they can be imported by all validators in the 3rd interval

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unfortunately, without global topic for attestations, we might not be able to receive proofs in time to update safe target during interval 2:

  • Interval 0: block propagation
  • Interval 1: votes propagation
  • Interval 2: signatures aggregation (up to one second for 1000 validators in subnet with 1000sigs/second expected sigs aggregation rate) + proof distribution => No time for updating safe target => in next slot validator votes for old target

attestations to be observed by any validator to compute safe target correctly.

Note that non-aggregating validators do not need to subscribe to committee
attestation subnets. They only need to subscribe to the global attestation
subnet.

Every validator is assigned to a single committee. Number of committees is
defined in config.yaml. Each committee maps to a subnet ID. Validator's
subnet ID is derived using their validator index modulo number of committees.
This is to simplify debugging and testing. In the future, validator's subnet ID
will be assigned randomly per epoch.

## Aggregator assignment

Some validators are self-assigned as aggregators. Aggregators collect and combine
attestations from other validators in their committee. To become an aggregator,
a validator sets `is_aggregator` flag to true as ENR record field.

## Proposing Blocks

Each slot has exactly one designated proposer. The proposer is determined by
Expand Down Expand Up @@ -52,7 +79,7 @@ receive and validate it.

## Attesting

Every validator attestations in every slot. Attesting happens in the second interval,
Every validator attests in every slot. Attesting happens in the second interval,
after proposals are made.

### What to Attest For
Expand All @@ -78,8 +105,8 @@ compute the head.

### Broadcasting Attestations

Validators sign their attestations and broadcast them. The network uses a single topic
for all attestations. No subnets or committees in the current design.
Validators sign their attestations and broadcast them into the global
attestation topic and its corresponding subnet topic.

## Timing

Expand All @@ -98,11 +125,7 @@ blocks and attestations.
Attestation aggregation combines multiple attestations into one. This saves bandwidth and
block space.

Devnet 0 has no aggregation. Each attestation is separate. Future devnets will add
aggregation.

When aggregation is added, aggregators will collect attestations and combine them.
Aggregated attestations will be broadcast separately.
Devnet-3 introduces signatures aggregation. Aggregators will collect attestations and combine them. Aggregated attestations will be broadcast separately.

## Signature Handling

Expand Down
3 changes: 3 additions & 0 deletions src/lean_spec/subspecs/chain/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,9 @@
VALIDATOR_REGISTRY_LIMIT: Final = Uint64(2**12)
"""The maximum number of validators that can be in the registry."""

ATTESTATION_COMMITTEE_COUNT: Final = Uint64(1)
"""The number of attestation committees per slot."""


class _ChainConfig(StrictBaseModel):
"""
Expand Down
8 changes: 8 additions & 0 deletions src/lean_spec/subspecs/containers/attestation/attestation.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
from lean_spec.subspecs.ssz import hash_tree_root
from lean_spec.types import Bytes32, Container, Uint64

from ...xmss.aggregation import AggregatedSignatureProof
from ...xmss.containers import Signature
from ..checkpoint import Checkpoint
from .aggregation_bits import AggregationBits
Expand Down Expand Up @@ -107,3 +108,10 @@ def aggregate_by_data(
)
for data, validator_ids in data_to_validator_ids.items()
]

class SignedAggregatedAttestation(Container):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@anshalshukla / @GrapeBaBa do we already have this type?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

also better to use message, signature terminlogy

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we also need aggregated bit vector here as well,

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we also need aggregated bit vector here as well,

AggregatedSignatureProof contains AggregationBits

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@anshalshukla / @GrapeBaBa do we already have this type?

no

data: AttestationData
"""Combined attestation data similar to the beacon chain format."""

proof: AggregatedSignatureProof
"""Aggregated signature proof covering all participating validators."""
3 changes: 3 additions & 0 deletions src/lean_spec/subspecs/containers/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,6 @@ class Config(Container):

genesis_time: Uint64
"""The timestamp of the genesis block."""

attestation_subnet_count: Uint64
"""The number of attestation subnets in the network."""
111 changes: 60 additions & 51 deletions src/lean_spec/subspecs/containers/state/state.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
JustifiedSlots,
Validators,
)
from ...chain.config import ATTESTATION_COMMITTEE_COUNT


class State(Container):
Expand Down Expand Up @@ -90,6 +91,7 @@ def generate_genesis(cls, genesis_time: Uint64, validators: Validators) -> "Stat
# Configure the genesis state.
genesis_config = Config(
genesis_time=genesis_time,
attestation_subnet_count=ATTESTATION_COMMITTEE_COUNT,
)

# Build the genesis block header for the state.
Expand Down Expand Up @@ -715,15 +717,13 @@ def build_block(
# Add new attestations and continue iteration
attestations.extend(new_attestations)

# Compute the aggregated signatures for the attestations.
# If the attestations cannot be aggregated, split it in a greedy way.
aggregated_attestations, aggregated_signatures = self.compute_aggregated_signatures(
# Select aggregated attestations and proofs for the final block
aggregated_attestations, aggregated_signatures = self.select_aggregated_proofs(
attestations,
gossip_signatures,
aggregated_payloads,
)

# Update the block with the aggregated attestations
# Update the block with the aggregated attestations and proofs
final_block = candidate_block.model_copy(
update={
"body": BlockBody(
Expand All @@ -738,42 +738,30 @@ def build_block(

return final_block, post_state, aggregated_attestations, aggregated_signatures

def compute_aggregated_signatures(
def aggregate_gossip_signatures(
self,
attestations: list[Attestation],
gossip_signatures: dict[SignatureKey, "Signature"] | None = None,
aggregated_payloads: dict[SignatureKey, list[AggregatedSignatureProof]] | None = None,
) -> tuple[list[AggregatedAttestation], list[AggregatedSignatureProof]]:
) -> list[tuple[AggregatedAttestation, AggregatedSignatureProof]]:
"""
Compute aggregated signatures for a set of attestations.
This method implements a two-phase signature collection strategy:
Collect aggregated signatures from gossip network and aggregate them.
1. **Gossip Phase**: For each attestation group, first attempt to collect
individual XMSS signatures from the gossip network. These are fresh
signatures that validators broadcast when they attest.
2. **Fallback Phase**: For any validators not covered by gossip, fall back
to previously-seen aggregated proofs from blocks. This uses a greedy
set-cover approach to minimize the number of proofs needed.
The result is a list of (attestation, proof) pairs ready for block inclusion.
For each attestation group, attempt to collect individual XMSS signatures
from the gossip network. These are fresh signatures that validators
broadcast when they attest.
Parameters
----------
attestations : list[Attestation]
Individual attestations to aggregate and sign.
gossip_signatures : dict[SignatureKey, Signature] | None
Per-validator XMSS signatures learned from the gossip network.
aggregated_payloads : dict[SignatureKey, list[AggregatedSignatureProof]] | None
Aggregated proofs learned from previously-seen blocks.
Returns:
-------
tuple[list[AggregatedAttestation], list[AggregatedSignatureProof]]
Paired attestations and their corresponding proofs.
list[tuple[AggregatedAttestation, AggregatedSignatureProof]]
- List of (attestation, proof) pairs from gossip collection.
"""
# Accumulator for (attestation, proof) pairs.
results: list[tuple[AggregatedAttestation, AggregatedSignatureProof]] = []

# Group individual attestations by data
Expand All @@ -790,8 +778,6 @@ def compute_aggregated_signatures(
# Get the list of validators who attested to this data.
validator_ids = aggregated.aggregation_bits.to_validator_indices()

# Phase 1: Gossip Collection
#
# When a validator creates an attestation, it broadcasts the
# individual XMSS signature over the gossip network. If we have
# received these signatures, we can aggregate them ourselves.
Expand All @@ -803,16 +789,10 @@ def compute_aggregated_signatures(
gossip_keys: list[PublicKey] = []
gossip_ids: list[Uint64] = []

# Track validators we couldn't find signatures for.
#
# These will need to be covered by Phase 2 (existing proofs).
remaining: set[Uint64] = set()

# Attempt to collect each validator's signature from gossip.
#
# Signatures are keyed by (validator ID, data root).
# - If a signature exists, we add it to our collection.
# - Otherwise, we mark that validator as "remaining" for the fallback phase.
if gossip_signatures:
for vid in validator_ids:
key = SignatureKey(vid, data_root)
Expand All @@ -821,12 +801,6 @@ def compute_aggregated_signatures(
gossip_sigs.append(sig)
gossip_keys.append(self.validators[vid].get_pubkey())
gossip_ids.append(vid)
else:
# No signature available: mark for fallback coverage.
remaining.add(vid)
else:
# No gossip data at all: all validators need fallback coverage.
remaining = set(validator_ids)

# If we collected any gossip signatures, aggregate them into a proof.
#
Expand All @@ -841,14 +815,53 @@ def compute_aggregated_signatures(
message=data_root,
epoch=data.slot,
)
results.append(
(
AggregatedAttestation(aggregation_bits=participants, data=data),
proof,
)
)
attestation = AggregatedAttestation(aggregation_bits=participants, data=data)
results.append((attestation, proof))

return results

def select_aggregated_proofs(
self,
attestations: list[Attestation],
aggregated_payloads: dict[SignatureKey, list[AggregatedSignatureProof]] | None = None,
) -> tuple[list[AggregatedAttestation], list[AggregatedSignatureProof]]:
"""
Select aggregated proofs for a set of attestations.
This method selects aggregated proofs from aggregated_payloads,
prioritizing proofs from the most recent blocks.
# Phase 2: Fallback to existing proofs
Strategy:
1. For each attestation group, aggregate as many signatures as possible
from the most recent block's proofs.
2. If remaining validators exist after step 1, include proofs from
previous blocks that cover them.
Parameters:
----------
attestations : list[Attestation]
Individual attestations to aggregate and sign.
aggregated_payloads : dict[SignatureKey, list[AggregatedSignatureProof]] | None
Aggregated proofs learned from previously-seen blocks.
The list for each key should be ordered with most recent proofs first.
Returns:
-------
tuple[list[AggregatedAttestation], list[AggregatedSignatureProof]]
Paired attestations and their corresponding proofs.
"""
results: list[tuple[AggregatedAttestation, AggregatedSignatureProof]] = []

# Group individual attestations by data
for aggregated in AggregatedAttestation.aggregate_by_data(attestations):
data = aggregated.data
data_root = data.data_root_bytes()
validator_ids = aggregated.aggregation_bits.to_validator_indices() # validators contributed to this attestation

# Validators that are missing in the current aggregation are put into remaining.
remaining: set[Uint64] = set(validator_ids)

# Fallback to existing proofs
#
# Some validators may not have broadcast their signatures over gossip,
# but we might have seen proofs for them in previously-received blocks.
Expand Down Expand Up @@ -924,14 +937,10 @@ def compute_aggregated_signatures(
remaining -= covered

# Final Assembly
#
# - We built a list of (attestation, proof) tuples.
# - Now we unzip them into two parallel lists for the return value.

# Handle the empty case explicitly.
if not results:
return [], []

# Unzip the results into parallel lists.
aggregated_attestations, aggregated_proofs = zip(*results, strict=True)
return list(aggregated_attestations), list(aggregated_proofs)

Loading