Commit 1be9995
authored
feat(aggregation): score recursive aggregation through a per-aggregator subnet window (lambdaclass#613)
## Summary
Every aggregator on the network currently does the same aggregation
work. In a healthy slot all validators vote for the same head, so there
is one hot `AttestationData`; candidates are scored deterministically
from head state, and `select_proofs_greedily` picks the two
highest-coverage children from a pool every aggregator sees, since
aggregates gossip on one global topic. So all aggregators select the
same two children and produce the same merged proof, and every copy of
that leanVM work past the first is wasted.
If every aggregator anchors on the pool's best proof `r0`, then
aggregator `i` publishes `out_i = r0 ∪ x_i`, and `out_i ∪ out_j = out_i
∪ x_j`. Merging two published proofs gains nothing over merging one of
them with a raw pool proof.
This gives each aggregator a **duty subnet** and a **window** of subnets
starting there, used as a scoring lens on child selection, so different
aggregators merge different children.
## Design
- **Window**: the contiguous cyclic run of subnets `{s, s+1, ...}` an
aggregator is responsible for, starting at its duty subnet.
- **Scoring lens, not a filter**: a child is valued by the validators it
newly covers whose subnet is inside the window. A proof straddling the
boundary stays usable for its in-window part; one lying wholly outside
scores zero. A selected child still contributes *all* of its
participants to `covered`, which keeps the marginal-coverage score
honest across greedy rounds.
- **Width from the anchor**: `min(2 * reach(anchor), C)`, where the
*anchor* is the largest-coverage proof in the candidate's pool that
touches the aggregator's own duty subnet, and a proof's *reach* is how
many distinct subnets it touches. Coverage rather than reach picks the
anchor, so a sparse proof holding one validator in each of many subnets
no longer sets the width for everybody. Requiring the anchor to touch
the duty subnet makes "no anchor" mean "no peer has covered my subnet",
which is exactly when this node's raw signatures are irreplaceable: the
window then sits at its narrowest and the aggregator works on those
instead of merging proofs it cannot add to. Deriving the width rather
than choosing it is essential either way, since windows nest and "widest
viable window" would collapse to the full committee set for everyone.
- **Duty subnet**: the first `--aggregate-subnet-ids` value, else the
lowest subscribed subnet, else 0. Logged at startup so a collision is
diagnosable. The node refuses to start on an `--aggregate-subnet-ids`
value at or above the committee count: subscriptions use the raw id
while the window reduces it, so an out-of-range value would subscribe to
a topic no validator publishes on and aggregate as a subnet the node
does not listen to.
- **`--skip-redundant-aggregation`** (opt-in): an aggregator sits out
any candidate whose derived width it does not own this slot
(`duty_subnet % w == slot % w`), and the freed job goes to the next-best
`AttestationData` rather than to a narrower merge of the same one.
Ownership rotates with the slot, so no duty subnet is permanently the
one sitting out, and width 1 is owned by everyone, so a candidate with
no anchor on this node's subnet is never skipped.
- **Deployment precondition**: every subnet below the committee count
needs an aggregator holding it as its duty subnet. Ownership rotates,
but a width can have *no* owner in a slot on a sparser placement even
with every node healthy: at duty subnets `{0, 2}` and committee count 4,
nothing owns width 4 in an odd slot, and the fallback is off, so that
merge level is dropped for the slot. Leave the flag unset on a placement
that does not cover every subnet.
## Safety
- **No consensus impact.** Nothing in attestation processing, block
building, or fork choice is touched. A windowed aggregate binds exactly
`raw_ids ∪ accepted_child_ids` and its bits derive from that same set,
so it is valid with fewer participants: less fork-choice weight, never
wrong weight.
- **No coverage regression.** A window is contiguous but the pool need
not be contiguous in subnet space, so a strided aggregator placement
could leave a window holding one proof and drop a merge the unwindowed
selection would have made. When a windowed selection is not viable it
falls back to the full committee set, so the window can only improve on
the old selection, never regress below it. The fallback is disabled
under `--skip-redundant-aggregation`: every width below the committee
count has several owners, so retrying there would rebuild exactly the
duplication the flag buys away.
- **`attestation_committee_count = 1` is a provable no-op**: the window
contains every validator, so in-window coverage equals total coverage
and the break condition is identical to before, tie-break order
included.
- **Mixed-network safe.** No wire-format, topic, or fork-digest change.
A `main` node's wide proof can raise a branch node's anchor reach where
it touches that node's duty subnet, which opens its window, so a partial
rollout makes the feature weaker rather than inconsistent.
## Known trade-off
Because the anchor must touch the duty subnet, the derived width is no
longer uniform across the network. Two aggregators reading one lopsided
pool can derive different widths, so their windows nest rather than
tile. That costs a round of climbing, not correctness, and it is the
direct price of tying the window to work the aggregator can actually
contribute to.
## Cadence, in practice
There is one aggregation session per slot, and the current slot's pool
is empty at snapshot time since produced aggregates are held until the
interval-2 boundary. So the window bites on the stale candidate, and a
data root gets about one windowed merge rather than a multi-round climb.
The widening matters across slots for a data root that stays live.
That emptiness is a timing expectation, not an invariant. A peer's
aggregate for the current slot landing before this node's snapshot,
under clock skew or a session started early via `EarlyAggregationCheck`,
gives the current-slot candidate an anchor and a width of 2; under
`--skip-redundant-aggregation` the duty subnets that do not own width 2
then sit that slot out and their raw signatures miss the next block. On
the intended topology (distinct duty subnets, distinct subscriptions) a
round-one peer proof never touches this node's subnet, so it holds
anyway. Overlapping subscriptions are where it fails.
## Metrics
- `lean_aggregation_window_width` (Histogram): width derived per
candidate. Climbs from 1 as the anchor climbs; pinned at the committee
count means the window no longer restricts selection. Stuck at 1 while
the network is aggregating means the pool holds nothing on this node's
duty subnet, so it is only aggregating its own raw signatures.
- `lean_aggregation_skipped_redundant_total`: candidates handed to
another duty subnet by the redundancy-skipping rotation. Only increments
with the flag on.
- `lean_aggregation_window_fallback_total`: merges the window would have
dropped, recovered by retrying selection at the full committee set.
Counts recoveries, not attempts: a candidate no window could have made
viable, a lone raw signature or a single-proof group, never reaches the
retry. A persistently rising value therefore means the aggregator
placement is too sparse for the committee count. Stays flat entirely
under `--skip-redundant-aggregation`.
## Test Plan
- [x] `cargo test --workspace --profile release-fast`: 695 tests pass
- [x] `make lint` clean, `make fmt` clean, `make docs` builds
- [x] Four-aggregator reduction pinned end to end: round 1 produces four
distinct proofs, round 2 reaches the full validator set
- [x] Mid-climb widening covered at committee count 8, where the window
grows but stays a proper subset
- [x] The strided-placement regression has a test that fails without the
fallback
- [x] Single-committee no-op pinned
- [x] Anchor selection pinned: a duty subnet the pool does not reach
gets width 1, a sparse wide proof loses to a denser narrow one, a
coverage tie falls to reach so pool order does not matter
- [x] A skipped candidate hands its job budget to the next-best
`AttestationData`
- [x] The full-width retry is skipped where it is provably a no-op: a
full-width window, an empty proof pool, a structureless committee
- [x] An `--aggregate-subnet-ids` value at or above the committee count
is refused at startup, past the first id too
- [ ] Devnet: run all-ethlambda with `attestation_committee_count = 4`
and aggregators on **distinct** duty subnets, confirm
`lean_aggregation_window_width` climbs rather than pinning at 4,
fleet-wide aggregation CPU drops against a control, and finality is
unaffected
- [ ] Devnet: confirm `lean_aggregation_window_fallback_total` stays
flat on the intended placement
## Notes for review
- The default proposer path (`keep_best_proof_per_data`) keeps one proof
per `AttestationData` and drops the rest. Aggregators now emit proofs
with distinct coverage rather than near-identical ones, so that path may
drop more useful coverage than before. Bounded, since the window is
sized to fit two children of the anchor's current reach, so a windowed
proof is the same size as before; `--enable-proposer-aggregation`
removes the exposure entirely. Declared out of scope here but worth
measuring.
- With `--skip-redundant-aggregation` set and no explicit
`--aggregate-subnet-ids`, every aggregator on a subnet-spanning topology
derives duty subnet 0 and sits out in lockstep instead of taking turns.
The node warns at startup when that combination is configured.
- The raw-signature guarantee is structural rather than a hard floor:
"no anchor" means no peer covered our subnet. It is not airtight when
two aggregators share a subnet and hold different slices of it, which no
supported topology does today.1 parent 0a90340 commit 1be9995
7 files changed
Lines changed: 1816 additions & 46 deletions
File tree
- bin/ethlambda/src
- crates/blockchain/src
- docs
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
83 | 83 | | |
84 | 84 | | |
85 | 85 | | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
| 93 | + | |
| 94 | + | |
86 | 95 | | |
87 | 96 | | |
| 97 | + | |
| 98 | + | |
| 99 | + | |
| 100 | + | |
| 101 | + | |
| 102 | + | |
| 103 | + | |
| 104 | + | |
| 105 | + | |
| 106 | + | |
| 107 | + | |
| 108 | + | |
| 109 | + | |
| 110 | + | |
| 111 | + | |
| 112 | + | |
| 113 | + | |
| 114 | + | |
| 115 | + | |
| 116 | + | |
| 117 | + | |
| 118 | + | |
| 119 | + | |
| 120 | + | |
| 121 | + | |
| 122 | + | |
| 123 | + | |
| 124 | + | |
88 | 125 | | |
89 | 126 | | |
90 | 127 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
25 | 25 | | |
26 | 26 | | |
27 | 27 | | |
28 | | - | |
| 28 | + | |
29 | 29 | | |
30 | 30 | | |
31 | 31 | | |
| |||
217 | 217 | | |
218 | 218 | | |
219 | 219 | | |
| 220 | + | |
| 221 | + | |
| 222 | + | |
| 223 | + | |
| 224 | + | |
| 225 | + | |
220 | 226 | | |
221 | 227 | | |
222 | 228 | | |
| |||
279 | 285 | | |
280 | 286 | | |
281 | 287 | | |
| 288 | + | |
| 289 | + | |
| 290 | + | |
| 291 | + | |
| 292 | + | |
| 293 | + | |
| 294 | + | |
| 295 | + | |
| 296 | + | |
| 297 | + | |
| 298 | + | |
| 299 | + | |
| 300 | + | |
| 301 | + | |
| 302 | + | |
| 303 | + | |
| 304 | + | |
| 305 | + | |
| 306 | + | |
| 307 | + | |
282 | 308 | | |
283 | 309 | | |
284 | 310 | | |
285 | 311 | | |
286 | 312 | | |
287 | 313 | | |
| 314 | + | |
| 315 | + | |
288 | 316 | | |
289 | 317 | | |
290 | 318 | | |
| |||
818 | 846 | | |
819 | 847 | | |
820 | 848 | | |
| 849 | + | |
| 850 | + | |
| 851 | + | |
| 852 | + | |
| 853 | + | |
| 854 | + | |
| 855 | + | |
| 856 | + | |
| 857 | + | |
| 858 | + | |
| 859 | + | |
| 860 | + | |
| 861 | + | |
| 862 | + | |
| 863 | + | |
| 864 | + | |
| 865 | + | |
| 866 | + | |
| 867 | + | |
| 868 | + | |
| 869 | + | |
| 870 | + | |
| 871 | + | |
| 872 | + | |
| 873 | + | |
| 874 | + | |
| 875 | + | |
| 876 | + | |
| 877 | + | |
| 878 | + | |
| 879 | + | |
| 880 | + | |
| 881 | + | |
| 882 | + | |
| 883 | + | |
| 884 | + | |
| 885 | + | |
| 886 | + | |
| 887 | + | |
| 888 | + | |
| 889 | + | |
| 890 | + | |
| 891 | + | |
| 892 | + | |
821 | 893 | | |
822 | 894 | | |
823 | 895 | | |
824 | 896 | | |
825 | 897 | | |
826 | 898 | | |
827 | 899 | | |
| 900 | + | |
| 901 | + | |
| 902 | + | |
| 903 | + | |
| 904 | + | |
| 905 | + | |
| 906 | + | |
| 907 | + | |
| 908 | + | |
| 909 | + | |
| 910 | + | |
| 911 | + | |
| 912 | + | |
| 913 | + | |
| 914 | + | |
| 915 | + | |
| 916 | + | |
| 917 | + | |
| 918 | + | |
| 919 | + | |
| 920 | + | |
| 921 | + | |
| 922 | + | |
| 923 | + | |
| 924 | + | |
| 925 | + | |
| 926 | + | |
| 927 | + | |
| 928 | + | |
| 929 | + | |
| 930 | + | |
| 931 | + | |
| 932 | + | |
| 933 | + | |
| 934 | + | |
| 935 | + | |
| 936 | + | |
| 937 | + | |
| 938 | + | |
| 939 | + | |
| 940 | + | |
| 941 | + | |
| 942 | + | |
| 943 | + | |
| 944 | + | |
| 945 | + | |
| 946 | + | |
| 947 | + | |
| 948 | + | |
| 949 | + | |
| 950 | + | |
| 951 | + | |
| 952 | + | |
| 953 | + | |
| 954 | + | |
| 955 | + | |
| 956 | + | |
| 957 | + | |
| 958 | + | |
| 959 | + | |
| 960 | + | |
| 961 | + | |
| 962 | + | |
| 963 | + | |
| 964 | + | |
| 965 | + | |
| 966 | + | |
| 967 | + | |
| 968 | + | |
| 969 | + | |
| 970 | + | |
| 971 | + | |
| 972 | + | |
| 973 | + | |
| 974 | + | |
828 | 975 | | |
829 | 976 | | |
830 | 977 | | |
| |||
0 commit comments