44
55from collections .abc import Iterable
66from collections .abc import Set as AbstractSet
7- from typing import TYPE_CHECKING
87
98from lean_spec .subspecs .ssz .hash import hash_tree_root
109from lean_spec .subspecs .xmss .aggregation import AggregatedSignatureProof
1615 Uint64 ,
1716)
1817
19- from ..attestation import AggregatedAttestation , AggregationBits , AttestationData
18+ from ..attestation import AggregatedAttestation , AttestationData
2019from ..block import Block , BlockBody , BlockHeader
2120from ..block .types import AggregatedAttestations
2221from ..checkpoint import Checkpoint
2322from ..config import Config
2423from ..slot import Slot
25- from ..validator import ValidatorIndex , ValidatorIndices
24+ from ..validator import ValidatorIndex
2625from .types import (
2726 HistoricalBlockHashes ,
2827 JustificationRoots ,
3130 Validators ,
3231)
3332
34- if TYPE_CHECKING :
35- from lean_spec .subspecs .forkchoice import AttestationSignatureEntry
36-
3733
3834class State (Container ):
3935 """The main consensus state object."""
@@ -692,7 +688,7 @@ def build_block(
692688
693689 found_entries = True
694690
695- selected , _ = self . _select_proofs_greedily (proofs )
691+ selected , _ = AggregatedSignatureProof . select_greedily (proofs )
696692 aggregated_signatures .extend (selected )
697693 for proof in selected :
698694 aggregated_attestations .append (
@@ -725,16 +721,23 @@ def build_block(
725721
726722 # Compact: merge all proofs sharing the same AttestationData into one
727723 # using recursive children aggregation.
724+ #
725+ # During the fixed-point loop above, multiple proofs may have been
726+ # selected for the same AttestationData across iterations. Group them
727+ # and merge each group into a single recursive proof.
728728 proof_groups : dict [AttestationData , list [AggregatedSignatureProof ]] = {}
729729 for att , sig in zip (aggregated_attestations , aggregated_signatures , strict = True ):
730730 proof_groups .setdefault (att .data , []).append (sig )
731731
732- compacted_attestations : list [ AggregatedAttestation ] = []
733- compacted_signatures : list [ AggregatedSignatureProof ] = []
732+ aggregated_attestations = []
733+ aggregated_signatures = []
734734 for att_data , proofs in proof_groups .items ():
735735 if len (proofs ) == 1 :
736- compacted_signatures . append ( proofs [0 ])
736+ sig = proofs [0 ]
737737 else :
738+ # Multiple proofs for the same data were aggregated separately.
739+ # Merge them into one recursive proof using children-only
740+ # aggregation (no new raw signatures).
738741 children = [
739742 (
740743 proof ,
@@ -745,24 +748,18 @@ def build_block(
745748 )
746749 for proof in proofs
747750 ]
748- merged = AggregatedSignatureProof .aggregate (
751+ sig = AggregatedSignatureProof .aggregate (
749752 xmss_participants = None ,
750753 children = children ,
751754 raw_xmss = [],
752755 message = att_data .data_root_bytes (),
753756 slot = att_data .slot ,
754757 )
755- compacted_signatures .append (merged )
756- compacted_attestations .append (
757- AggregatedAttestation (
758- aggregation_bits = compacted_signatures [- 1 ].participants ,
759- data = att_data ,
760- )
758+ aggregated_signatures .append (sig )
759+ aggregated_attestations .append (
760+ AggregatedAttestation (aggregation_bits = sig .participants , data = att_data )
761761 )
762762
763- aggregated_attestations = compacted_attestations
764- aggregated_signatures = compacted_signatures
765-
766763 # Create the final block with selected attestations.
767764 final_block = Block (
768765 slot = slot ,
@@ -779,116 +776,3 @@ def build_block(
779776 final_block = final_block .model_copy (update = {"state_root" : hash_tree_root (post_state )})
780777
781778 return final_block , post_state , aggregated_attestations , aggregated_signatures
782-
783- @staticmethod
784- def _select_proofs_greedily (
785- * proof_sets : set [AggregatedSignatureProof ] | None ,
786- ) -> tuple [list [AggregatedSignatureProof ], set [ValidatorIndex ]]:
787- """
788- Greedy set-cover selection of signature proofs to maximize validator coverage.
789-
790- Repeatedly selects the proof covering the most uncovered validators until
791- no proof adds new coverage. Earlier proof sets are prioritized.
792-
793- Args:
794- proof_sets: Candidate proof sets in priority order.
795-
796- Returns:
797- Selected proofs and the set of covered validator indices.
798- """
799- selected : list [AggregatedSignatureProof ] = []
800- covered : set [ValidatorIndex ] = set ()
801- for proofs in proof_sets :
802- if not proofs :
803- continue
804- remaining = list (proofs )
805- while remaining :
806- # Pick the proof that covers the most new validators.
807- best = max (
808- remaining ,
809- key = lambda p : len (set (p .participants .to_validator_indices ()) - covered ),
810- )
811- new_coverage = set (best .participants .to_validator_indices ()) - covered
812- # Stop when no proof in this set adds new coverage.
813- if not new_coverage :
814- break
815- selected .append (best )
816- covered .update (new_coverage )
817- remaining .remove (best )
818- return selected , covered
819-
820- def aggregate (
821- self ,
822- attestation_signatures : dict [AttestationData , set [AttestationSignatureEntry ]] | None = None ,
823- new_payloads : dict [AttestationData , set [AggregatedSignatureProof ]] | None = None ,
824- known_payloads : dict [AttestationData , set [AggregatedSignatureProof ]] | None = None ,
825- ) -> list [tuple [AggregatedAttestation , AggregatedSignatureProof ]]:
826- """
827- Aggregate gossip signatures using new payloads, with known payloads as helpers.
828-
829- Args:
830- attestation_signatures: Raw XMSS signatures from gossip, keyed by attestation data.
831- new_payloads: Aggregated proofs pending processing (child proofs).
832- known_payloads: Known aggregated proofs already accepted.
833-
834- Returns:
835- List of (attestation, proof) pairs from aggregation.
836- """
837- gossip_sigs = attestation_signatures or {}
838- new = new_payloads or {}
839- known = known_payloads or {}
840-
841- attestation_keys = new .keys () | gossip_sigs .keys ()
842- if not attestation_keys :
843- return []
844-
845- results : list [tuple [AggregatedAttestation , AggregatedSignatureProof ]] = []
846-
847- for data in attestation_keys :
848- # Phase 1: Greedily select child proofs for maximum validator coverage.
849- # New payloads are prioritized over known payloads.
850- child_proofs , covered = self ._select_proofs_greedily (new .get (data ), known .get (data ))
851-
852- # Phase 2: Collect raw XMSS signatures for validators not yet covered.
853- # Sorted by validator index for deterministic output.
854- raw_entries = [
855- (
856- e .validator_id ,
857- self .validators [e .validator_id ].get_attestation_pubkey (),
858- e .signature ,
859- )
860- for e in sorted (gossip_sigs .get (data , set ()), key = lambda e : e .validator_id )
861- if e .validator_id not in covered
862- ]
863-
864- # Need at least one raw signature, or two child proofs to aggregate.
865- if not raw_entries and len (child_proofs ) < 2 :
866- continue
867-
868- xmss_participants = AggregationBits .from_validator_indices (
869- ValidatorIndices (data = [vid for vid , _ , _ in raw_entries ])
870- )
871- raw_xmss = [(pk , sig ) for _ , pk , sig in raw_entries ]
872-
873- # Phase 3: Build recursive children with their public keys from the registry.
874- children = [
875- (
876- child ,
877- [
878- self .validators [vid ].get_attestation_pubkey ()
879- for vid in child .participants .to_validator_indices ()
880- ],
881- )
882- for child in child_proofs
883- ]
884- proof = AggregatedSignatureProof .aggregate (
885- xmss_participants = xmss_participants ,
886- children = children ,
887- raw_xmss = raw_xmss ,
888- message = data .data_root_bytes (),
889- slot = data .slot ,
890- )
891- attestation = AggregatedAttestation (aggregation_bits = proof .participants , data = data )
892- results .append ((attestation , proof ))
893-
894- return results
0 commit comments