|
2 | 2 |
|
3 | 3 | from __future__ import annotations |
4 | 4 |
|
5 | | -from collections.abc import Collection, Iterable |
| 5 | +from collections.abc import Iterable |
6 | 6 | from collections.abc import Set as AbstractSet |
7 | 7 | from typing import TYPE_CHECKING |
8 | 8 |
|
@@ -759,79 +759,89 @@ def build_block( |
759 | 759 |
|
760 | 760 | return final_block, post_state, aggregated_attestations, aggregated_signatures |
761 | 761 |
|
| 762 | + def _extend_proofs_with_unique_participants( |
| 763 | + proofs: set[AggregatedSignatureProof] | None, |
| 764 | + selected: list[AggregatedSignatureProof], |
| 765 | + covered: set[ValidatorIndex], |
| 766 | + ) -> None: |
| 767 | + if not proofs: |
| 768 | + return |
| 769 | + sorted_proofs = sorted( |
| 770 | + proofs, |
| 771 | + key=lambda proof: len(proof.participants.to_validator_indices()), |
| 772 | + reverse=True, |
| 773 | + ) |
| 774 | + for proof in sorted_proofs: |
| 775 | + participants = set(proof.participants.to_validator_indices()) |
| 776 | + if participants - covered: |
| 777 | + selected.append(proof) |
| 778 | + covered.update(participants) |
| 779 | + |
762 | 780 | def aggregate_gossip_signatures( |
763 | 781 | self, |
764 | | - attestations: Collection[Attestation], |
765 | 782 | gossip_signatures: dict[AttestationData, set[GossipSignatureEntry]] | None = None, |
| 783 | + new_payloads: dict[AttestationData, set[AggregatedSignatureProof]] | None = None, |
| 784 | + known_payloads: dict[AttestationData, set[AggregatedSignatureProof]] | None = None, |
766 | 785 | ) -> list[tuple[AggregatedAttestation, AggregatedSignatureProof]]: |
767 | 786 | """ |
768 | | - Collect aggregated signatures from gossip network and aggregate them. |
769 | | -
|
770 | | - For each attestation group, attempt to collect individual XMSS signatures |
771 | | - from the gossip network. These are fresh signatures that validators |
772 | | - broadcast when they attest. |
| 787 | + Aggregate gossip signatures using new payloads, with known payloads as helpers. |
773 | 788 |
|
774 | 789 | Args: |
775 | | - attestations: Individual attestations to aggregate and sign. |
776 | | - gossip_signatures: Per-validator XMSS signatures learned from |
777 | | - the gossip network, keyed by the attestation data they signed. |
| 790 | + gossip_signatures: Raw XMSS signatures learned from gossip keyed by attestation data. |
| 791 | + new_payloads: Aggregated proofs pending processing (child proofs). |
| 792 | + known_payloads: Known aggregated proofs already accepted. |
778 | 793 |
|
779 | 794 | Returns: |
780 | | - List of (attestation, proof) pairs from gossip collection. |
| 795 | + List of (aggregated attestation, proof) pairs to broadcast. |
781 | 796 | """ |
782 | 797 | results: list[tuple[AggregatedAttestation, AggregatedSignatureProof]] = [] |
783 | 798 |
|
784 | | - # Group individual attestations by data |
785 | | - # |
786 | | - # Multiple validators may attest to the same data (slot, head, target, source). |
787 | | - # We aggregate them into groups so each group can share a single proof. |
788 | | - for aggregated in AggregatedAttestation.aggregate_by_data(list(attestations)): |
789 | | - # Extract the common attestation data and its hash. |
790 | | - # |
791 | | - # All validators in this group signed the same message (the data root). |
792 | | - data = aggregated.data |
793 | | - data_root = data.data_root_bytes() |
| 799 | + gossip_signatures = gossip_signatures or {} |
| 800 | + new_payloads = new_payloads or {} |
| 801 | + known_payloads = known_payloads or {} |
794 | 802 |
|
795 | | - # Get the list of validators who attested to this data. |
796 | | - validator_ids = aggregated.aggregation_bits.to_validator_indices() |
| 803 | + # Use only keys from new_payloads and gossip_signatures |
| 804 | + # know_payloads can be used to extend the proof with new_payloads and gossip_signatures |
| 805 | + # but known_payloads are not recursively aggregated into their own proofs |
| 806 | + attestation_keys = set(new_payloads.keys()) | set(gossip_signatures.keys()) |
| 807 | + if not attestation_keys: |
| 808 | + return results |
797 | 809 |
|
798 | | - # When a validator creates an attestation, it broadcasts the |
799 | | - # individual XMSS signature over the gossip network. If we have |
800 | | - # received these signatures, we can aggregate them ourselves. |
801 | | - # |
802 | | - # This is the preferred path: fresh signatures from the network. |
803 | | - |
804 | | - # Parallel lists for signatures, public keys, and validator IDs. |
805 | | - gossip_sigs: list[Signature] = [] |
806 | | - gossip_keys: list[PublicKey] = [] |
807 | | - gossip_ids: list[ValidatorIndex] = [] |
808 | | - |
809 | | - # Look up signatures by attestation data directly. |
810 | | - # Sort by validator ID for deterministic aggregation order. |
811 | | - if gossip_signatures and (entries := gossip_signatures.get(data)): |
812 | | - for entry in sorted(entries, key=lambda e: e.validator_id): |
813 | | - if entry.validator_id in validator_ids: |
814 | | - gossip_sigs.append(entry.signature) |
815 | | - gossip_keys.append(self.validators[entry.validator_id].get_pubkey()) |
816 | | - gossip_ids.append(entry.validator_id) |
817 | | - |
818 | | - # If we collected any gossip signatures, aggregate them into a proof. |
819 | | - # |
820 | | - # The aggregation combines multiple XMSS signatures into a single |
821 | | - # compact proof that can verify all participants signed the message. |
822 | | - if gossip_ids: |
823 | | - participants = AggregationBits.from_validator_indices( |
824 | | - ValidatorIndices(data=gossip_ids) |
825 | | - ) |
826 | | - proof = AggregatedSignatureProof.aggregate( |
827 | | - participants=participants, |
828 | | - public_keys=gossip_keys, |
829 | | - signatures=gossip_sigs, |
830 | | - message=data_root, |
831 | | - slot=data.slot, |
832 | | - ) |
833 | | - attestation = AggregatedAttestation(aggregation_bits=participants, data=data) |
834 | | - results.append((attestation, proof)) |
| 810 | + # Aggregate the proofs for each attestation data |
| 811 | + for data in attestation_keys: |
| 812 | + child_proofs: list[AggregatedSignatureProof] = [] |
| 813 | + covered_validators: set[ValidatorIndex] = set() |
| 814 | + |
| 815 | + self._extend_proofs_with_unique_participants(new_payloads.get(data), child_proofs, covered_validators) |
| 816 | + self._extend_proofs_with_unique_participants(known_payloads.get(data), child_proofs, covered_validators) |
| 817 | + |
| 818 | + raw_entries: list[tuple[ValidatorIndex, PublicKey, Signature]] = [] |
| 819 | + for entry in sorted(gossip_signatures.get(data, set()), key=lambda e: e.validator_id): |
| 820 | + if entry.validator_id in covered_validators: |
| 821 | + continue |
| 822 | + if int(entry.validator_id) >= len(self.validators): |
| 823 | + continue |
| 824 | + public_key = self.validators[entry.validator_id].get_pubkey() |
| 825 | + raw_entries.append((entry.validator_id, public_key, entry.signature)) |
| 826 | + covered_validators.add(entry.validator_id) |
| 827 | + |
| 828 | + if not raw_entries and len(child_proofs) < 2: |
| 829 | + results.append((data, child_proofs)) |
| 830 | + continue |
| 831 | + |
| 832 | + raw_entries = sorted(raw_entries, key=lambda e: e.validator_id) |
| 833 | + raw_xmss = [(pubkey, signature) for _, pubkey, signature in raw_entries] |
| 834 | + xmss_participants = AggregationBits.from_validator_indices(ValidatorIndices(data=[e.validator_id for e in raw_entries])) |
| 835 | + |
| 836 | + proof = AggregatedSignatureProof.aggregate( |
| 837 | + xmss_participants=xmss_participants, |
| 838 | + children=child_proofs, |
| 839 | + raw_xmss=raw_xmss, |
| 840 | + message=data.data_root_bytes(), |
| 841 | + slot=data.slot, |
| 842 | + ) |
| 843 | + attestation = AggregatedAttestation(aggregation_bits=proof.participants, data=data) |
| 844 | + results.append((attestation, proof)) |
835 | 845 |
|
836 | 846 | return results |
837 | 847 |
|
|
0 commit comments