-
Notifications
You must be signed in to change notification settings - Fork 40
Committee aggregation #282
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 15 commits
b5b6d8a
4867d7d
7bcedca
980b5e8
60468af
213504a
4fac983
f2651d8
cc7548c
cb1a21b
e398823
90fc114
cdae6a4
b24d3ed
5c952ff
8a0c121
cb952f8
9d721bd
baddbeb
6556e81
3477d6e
9174f5b
3115ef5
3fffe71
d0462aa
e2fd644
d40199c
d46dd08
2439177
61b8100
d66dfd3
360bfb0
379ddd6
fe8317c
6af933b
2b68c0c
22bd960
7cf9773
6e85356
da21184
6ad7b19
ac9b2e3
73429c6
5fd7e0b
113e136
0616107
5cabfbd
e39b042
0ec04f1
4bd283c
7d1e0f2
3a06ccd
1943eda
9f3e05b
ed02f93
583201d
8f6c63c
9a2a75f
d48d9c3
db34a77
8271f78
6c94cd6
4ddaccd
72a6f78
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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 | ||
|
|
@@ -107,3 +108,10 @@ def aggregate_by_data( | |
| ) | ||
| for data, validator_ids in data_to_validator_ids.items() | ||
| ] | ||
|
|
||
| class SignedAggregatedAttestation(Container): | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @anshalshukla / @GrapeBaBa do we already have this type?
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. also better to use message, signature terminlogy
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think we also need aggregated bit vector here as well,
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
AggregatedSignatureProof contains
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
no |
||
| data: AttestationData | ||
| """Combined attestation data similar to the beacon chain format.""" | ||
|
|
||
| proof: AggregatedSignatureProof | ||
| """Aggregated signature proof covering all participating validators.""" | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -30,6 +30,7 @@ | |
| JustifiedSlots, | ||
| Validators, | ||
| ) | ||
| from ...chain.config import AGGREGATION_COMMITTEE_COUNT | ||
|
|
||
|
|
||
| class State(Container): | ||
|
|
@@ -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=AGGREGATION_COMMITTEE_COUNT, | ||
|
||
| ) | ||
|
|
||
| # Build the genesis block header for the state. | ||
|
|
@@ -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( | ||
|
|
@@ -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 | ||
|
|
@@ -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. | ||
|
|
@@ -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) | ||
|
|
@@ -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. | ||
| # | ||
|
|
@@ -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. | ||
|
|
@@ -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) | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
original prefix is
/leanconsensus, this is an expected change/lean/consensus?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
no.
Thanks for noticing. Fixed