chore(do not merge): aggregate API - #267
Draft
kevaundray wants to merge 13 commits into
Draft
Conversation
`base_eval_eq_packed_with_packed_output` asserted `log_packing_width <= eval_points.len()`, but `eval_points` is the *middle* slice produced by `par_eval_eq`, not the full point: the `log_packing_width` suffix is already folded into `eq_evals` and the `log_chunks` prefix into the packed scalar. Its length is therefore `n - log_packing_width - log_chunks` and has nothing to do with the packing width. The callers only guarantee it is at least 2, while the assert demanded at least `log_packing_width`, so every `n` in `[lpw + log_chunks + 2, 2*lpw + log_chunks)` panicked in debug builds. On a 32-thread AVX512 host that is log_chunks=7, lpw=4, so 13- and 14-variable polynomials aborted `test_packed_eval_eq` and `lean_prover`'s `test_small_memory`. The band is machine-dependent and non-empty on any target with a packing width above 4. The invariant is real, but it belongs to the callers, which already check it against the full point (`compute_eval_eq_base_packed` and `compute_eval_eq_base_packed_batched`). This restores 5cf504a, which removed the same assert for the same reason and was reverted by e45a0ed. It regressed because no test covered the band, so add one that computes the bounds from the runtime thread count and SIMD width rather than hardcoding them. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Adds `crates/lean_multisig_api`, a facade over `xmss` and
`rec_aggregation` exposing signing and single-message aggregation over
plain byte slices. Every tuning parameter -- recursion topology and each
node's `log_inv_rate` -- is chosen internally, so callers have no knobs.
Callers needing the full parameter space keep using `rec_aggregation`.
aggregate(proof_or_sig, public_keys, message, slot) -> Vec<u8>
verify(aggregate, message, slot) -> Vec<Vec<u8>>
verify_with_signers(aggregate, expected, message, slot)
SecretKey::{generate, from_seed, sign, public_key, prepare, ...}
`proof_or_sig` mixes raw XMSS signatures with prior aggregates,
discriminated by length (a signature is exactly SIGNATURE_SSZ_LEN).
Aggregates carry their own signer sets, so `public_keys` covers raw
entries only and the two vectors are deliberately not index-aligned --
the sharpest edge in the API, documented accordingly.
`verify` returns the proved signer set rather than a bool: an aggregate
over the wrong validator set is still a valid proof, so ignoring who
signed should require discarding a value rather than simply not asking.
Five modules: `plan.rs` is a pure tree planner (millisecond tests, no
prover), `codec.rs` does length dispatch and pubkey pairing, `key.rs`
holds `SecretKey` as an opaque handle so its bottom-subtree cache
survives across signatures, `lib.rs` the entry points, `error.rs` one
flattened enum. 62 tests: 44 unit, 18 across five integration binaries,
plus two `#[ignore]`d boundary tests wired into CI.
Three upstream traps are absorbed so callers never meet them: the
bytecode `OnceLock` (which panics via `get_aggregation_bytecode` and
silently returns `None` from `from_bytes`), the `MAX_XMSS_AGGREGATED`
ceiling applying at every node so recursion cannot raise it, and the
requirement to serialize proving jobs.
`LEAF_TARGET = 1500` is measured rather than inherited: a full leaf
proves in ~8s at the slowest rate the planner assigns, and 1501 splits
and proves in ~6.7s. The largest leaf that proves is still unknown, so
the value is known-good rather than known-optimal.
Design and rationale in docs/plans/2026-08-14-lean-sig-facade-design.md;
the task breakdown and deferred tuning questions in
docs/plans/2026-08-14-lean-sig-implementation.md. The crate was named
`lean_sig` while it was built, which is why those filenames and the
pre-squash commit scopes say so.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
kevaundray
force-pushed
the
feat/lean-sig
branch
from
August 14, 2026 17:47
a6e6c6b to
245fcab
Compare
kevaundray
commented
Aug 14, 2026
| } | ||
| } | ||
|
|
||
| /// `base_eval_eq_packed_with_packed_output` receives the *middle* slice of the eval |
Contributor
Author
There was a problem hiding this comment.
This has been pulled into a separate PR
Quality-only changes from a four-angle review (reuse, simplification,
efficiency, altitude). No behavior changes except the two noted below,
both of which only remove work.
- Drop `Error::Verify`. Nothing in the crate could construct it: the
facade never verifies a raw XMSS signature outside the prover, so that
fault surfaces as `Error::Aggregation`. A public enum variant for an
unreachable case costs consumers a match arm and weakened the
deliberately-exhaustive `source()` match by seeding it with a variant
nobody decided about.
- Share `codec::Raw` instead of restating the same tuple alias in
`lib.rs`. The two only ever typechecked against each other because
they happened to be written identically.
- Extract `proves()` for the "does this aggregate prove (message, slot)?"
test, which `aggregate` and `verify` spelled two different ways. Also
the only place reaching two levels into `rec_aggregation`'s struct.
- `verify_with_signers` no longer collects the proved set into a second
`BTreeSet`. `verify` returns a set `check_single_message_pubkeys`
already enforced to be strictly sorted, so a length match plus
containment is exact. Saves building a tree of up to 32768 nodes on
the path a node runs per gossiped aggregate.
- Guard the three `log_inv_rate` literals against
`lean_vm::{MIN,MAX}_WHIR_LOG_INV_RATE`. `default_whir_config` asserts
rather than errors, so a narrowed band upstream would abort every
aggregation at proving time with no compile- or test-time signal.
Same treatment `MAX_FAN_IN` and `LOG_LIFETIME` already get, for the
same reason: this crate does not own the value. `lean_vm` moves from
dev- to normal dependency for it, which also retires a dev-dep entry
that was redundant (integration tests see `[dependencies]` too).
- `warm_up`'s doc said what it warms but not what it does not. The
worker pool and DFT twiddle table are also lazy and it touches
neither, so a service following the doc still paid both inside its
first request. Points at `setup_prover_without_arena`.
- `base_set()` in round_trip.rs, replacing four hand-rolled collects.
TODO.md records that `#[ignore]` carries two meanings, which is why CI
selects slow tests by naming a binary rather than `--include-ignored`.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`docs/` did not exist before this work and held only the two planning artifacts. Removing them, but four code comments referenced them and the substance was worth keeping, so it moves into the code that depends on it rather than disappearing: - `LEAF_TARGET`'s doc now records the measurement itself: the two `#[ignore]`d boundary tests prove a full 1500-signature leaf and the 1501 split, so it is a size the prover demonstrably accepts. Also records what is still unmeasured — the *largest* leaf that proves — so the value reads as known-good rather than known-optimal. - `Plan::Node.raw` states what blocks the mixed raw+child node: roughly 2x on the incremental fold path, blocked only on whether that trace size fits, which nobody has measured. - The greedy-split comment gains the data point that argues against balancing: 1501 as [1500, 1] plus a root measures faster than 1500 as one node, because only the root pays `RATE_ROOT`. - Two "Task N" references dropped; the task numbering they pointed at no longer exists anywhere. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
kevaundray
commented
Aug 14, 2026
Comment on lines
+70
to
+76
| static PROVE_LOCK: Mutex<()> = Mutex::new(()); | ||
|
|
||
| /// The shared 2-signer aggregate over `(MSG, SLOT)`, proved at most once per process. | ||
| /// | ||
| /// Tests are independent in what they *assert* — this is a fixture, not a channel between them — | ||
| /// but sharing it means one proving job instead of six. | ||
| static BASE: OnceLock<Vec<u8>> = OnceLock::new(); |
# Conflicts: # crates/backend/poly/src/eq_mle.rs
Contributor
Author
|
Created this initial PR to incorporate things: ethereum/lean-multisig-bindings#10 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Quick prototype of the single aggregation API