Skip to content

node, aggregation: zeam aggregate-build is 5–7x slower than ethlambda on devnet (2–3s vs 0.4s) #899

Description

@ch4r10t33r

Summary

On the multi-client devnet (8 aggregators across zeam / ethlambda / ream), zeam's per-aggregate build time is 5–7× higher than ethlambda's, measured cross-client at the same wall-clock with the same standard lean_pq_sig_aggregated_signatures_building_time_seconds histogram.

Aggregator mean build time sample count head_slot
zeam_8 2.90 s 64 256
zeam_4 2.91 s 48 227
zeam_16 2.00 s 64 261
ethlambda_8 0.43 s 88 271
ethlambda_5 0.41 s 8 108
ethlambda_16 0.40 s 12 263
ream_0 (metric not exposed — see Notes) 223
ream_13 (metric not exposed) 215

(All numbers from each host's local Prometheus, snapshot 2026-05-19T15:13Z, ~30 min after fresh devnet restart.)

Verify side is fine across all clients (aggregated_signatures_verification_time is 35–71 ms; attestation_verification_time is 0.78–1.25 ms). The cost gap is squarely on the build path.

drops and queue saturation are clean on this run (lean_chain_queue_dropped_total = 0 for block and attestation queues on every zeam aggregator), so the slowness is not a queue-backpressure artifact and is not the regression PR #894 fixed. It's intrinsic per-aggregate cost.

Why it matters — direct FFG impact

Slow build → zeam aggregators frequently miss a slot's aggregation window (zeam_aggregate_skip_total{reason="in_flight"} ticks up because aggregate_group.concurrent is gated at concurrent_limit=1, see pkgs/node/src/chain.zig:4357). The downstream effect is visible directly in the next block proposal's coverage. From zeam_8 log at slot 256:

[forkchoice] block proposal slot=256: attestation aggregate coverage:
  payloads = subnet0=3/8(37.50%)  subnet1=8/8(100%)  subnet2=7/8(87.50%)
             subnet3=8/8(100%)   subnet4=7/8(87.50%) subnet5=1/8(12.50%)
             subnet6=6/8(75%)    subnet7=6/8(75%)
  network=46/64(71.88%)

Subnets 0, 3, 5 are the three zeam-aggregator subnets. Subnet 5 contributed 1 of 8 votes. Subnet 0 contributed 3 of 8. Network coverage hovers around 70–72%, marginal vs the 67% justification threshold once any vote is on a divergent target — currently latest_justified_slot=0 and latest_finalized_slot=0 for every node in the devnet.

So this issue is the dominant single contributor to "no FFG progress" on the current cross-client devnet (other contributors — #863 slot-driver backlog, head divergence — are already tracked).

Where the cost lives

  • Entry: chain.zig submitAggregateOnIntervalaggregate_group.concurrent on the dedicated aggregate_io thread (the move done in Off aggregate from libxev thread to avoid slot advance blocking #873).
  • Worker: chain.zig aggregateImpl (pkgs/node/src/chain.zig:4371) →
    forkChoice.aggregateaggregateUnlocked (pkgs/node/src/forkchoice.zig:2035).

aggregateUnlocked is structured in three phases:

  1. Snapshot under signatures_mutexAggregateSnapshot.takeUnderLock. Comment claims O(10 ms) typical.
  2. Compute with no lock held — agg.computeAggregatedSignatures(&state.validators, &snap.signatures, &snap.new_payloads, &snap.known_payloads). This is the heavy XMSS FFI path. The same docstring on aggregate() at line 2240 says "The ~18 s computeAggregatedSignatures runs in between with no locks" — i.e. the worst case acknowledged in code is ~18 s, and the live mean we measure today is 2–3 s.
  3. Commit under signatures_mutex — merge results, prune consumed snapshot vids. ms-scale.

So the work that's slow is phase 2 only — the FFI into multisig-glue for XMSS aggregation/proof-construction. Phases 1 and 3 are not the cost drivers.

ethlambda doing the equivalent work in 0.4 s is the strongest signal that this is not a fundamental cost of the spec — there is something specific to zeam's multisig FFI path that is 5–7× off.

Notes on cross-client comparability

  • lean_pq_sig_aggregated_signatures_building_time_seconds is part of the standard leanMetrics set, so the histogram name and semantics match across zeam / ethlambda by construction.
  • ream does not expose this histogram (or _total, _invalid_total, _in_aggregated_signatures_total, or the individual attestation_verification_time histogram). Their /metrics only carries _valid_total + _verification_time_seconds for aggregated sigs. That very likely means ream does not generate a recursive/SNARK aggregation proof at all (just packs verified XMSS sigs + a participation bitfield), and therefore has nothing to time. Worth confirming with the ream team before drawing conclusions, but it does suggest that even a SNARK-free packing is spec-acceptable.

Suggested investigation

  1. Profile multisig-glue aggregation FFI under a representative load (8 raw atts, validator set 64) on a single host. Identify whether the cost is in:
    • per-signature recursive-proof witness construction (parallelisable per signature)
    • shared setup / SRS load (cacheable, currently re-done?)
    • bigint / hash-chain work (constant factor)
    • any debug-mode build artefact in the rust crate that landed on the devnet image.
  2. Compare the multisig-glue API surface with what ethlambda uses. If they use a different (lighter) aggregation construction, decide whether to align — or document the cost/security trade-off.
  3. Check parallelism. aggregate_io is concurrent_limit=1 (Off aggregate from libxev thread to avoid slot advance blocking #873), so two slots' aggregations cannot run in parallel, but within a single aggregation the per-signature work could be parallelised across a thread pool. Currently computeAggregatedSignatures appears to call FFI sequentially per attestation; verify and parallelise if so.
  4. Verify Rust release-mode / opt-level on the devnet build. A 5–7× hit is consistent with debug-mode FFI, so rule that out first.
  5. Add per-phase timing to aggregateUnlocked — break the histogram into phase1_snapshot, phase2_compute_ffi, phase3_commit so we can tell from Prometheus alone whether the cost is in the FFI or in surrounding Zig work.

Acceptance criteria

  • Mean lean_pq_sig_aggregated_signatures_building_time_seconds on a zeam aggregator ≤ 0.6 s on the multi-client devnet (matching ethlambda within ~1.5×).
  • On a 32-validator-per-subnet stress run, p99 build time ≤ 2 s.
  • zeam_aggregate_skip_total{reason="in_flight"} rate on a zeam aggregator drops by ≥ 4× compared to today's devnet baseline.
  • Per-phase build histogram exposed (phase1_snapshot, phase2_compute_ffi, phase3_commit) so we can attribute future regressions.
  • Block-proposal aggregate coverage from a zeam-managed subnet (0, 3, 5) reaches ≥ 6/8 in steady state on the same devnet.

Related

Devnet repro context

  • Branch under test on zeam aggregators: PR node: harden blocks_by_range catch-up retry and fork recovery #894 (commit b2654679).
  • Date: 2026-05-19, devnet restarted at ~14:43 UTC.
  • Topology: 24 zeam + 24 ethlambda + 16 ream validators across 8 subnets (gean and nlean slots replaced with zeam/ethlambda in lean-quickstart ansible-devnet/genesis/validator-config.yaml).
  • Live at snapshot time: head spread 108–271, jst=0, fin=0 on every aggregator.

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions