experiment: combine batch proof verification (#1872), tx revalidation cache (#744) and live intra-block ledger states (#2050) - #2041
Draft
chrispalaskas wants to merge 54 commits into
Draft
chrispalaskas wants to merge 54 commits into
chrispalaskas wants to merge 54 commits into
Conversation
Remove the soft transaction cache and introduce tx revalidation: cached VerifiedTransaction entries are reused when state changes by revalidating against a RevalidationReference instead of re-running full ZK proof verification. Adds cache metrics (miss, strict hit, revalidation hit) and tests covering the full validation lifecycle.
Signed-off-by: Michał Skowron <michal.skowron@shielded.io>
Signed-off-by: Michał Skowron <michal.skowron@shielded.io>
Signed-off-by: Michał Skowron <michal.skowron@shielded.io>
Signed-off-by: Oscar Bailey <79094698+ozgb@users.noreply.github.com>
Signed-off-by: Oscar Bailey <79094698+ozgb@users.noreply.github.com>
Signed-off-by: Oscar Bailey <79094698+ozgb@users.noreply.github.com>
…gress Verify transaction ZK proofs once, in an aggregate batch, instead of one tx at a time. This lands the ledger-crate foundation, the native-direct invocation machinery, and the block-import ingress point; all gated by node config (MidnightCfg) and defaulting OFF. Ledger crate: - New process-global proof-verification cache (tx_hash -> bool), keyed by the state-independent tx_validation_cache_key, with insert/get accessors. - get_verified_transaction consults the cache: defer crypto on a hit, reject on a cached-false, error-log + full inline verify on a miss. - Bridge::batch_verify_transactions: real aggregate verification using the ledger-9 collect_proof_evidence/batch_proof_verify API. The v9-only crypto is isolated in a new per-version batch_verify module so the shared code still compiles against ledger 7/8. Only the mempool isolation fallback remains a todo!(). - Native (non-WASM) entry point host_api::ledger_9::batch_verify_transactions (no new runtime API / host function). Node crate: - MidnightCfg: batch_verify_block_import / batch_verify_mempool (default false) plus queue-tuning params, via serde defaults. - batch_verify.rs: BatchVerifier builds the native inputs (state_key, BlockContext, runtime_version) from the client backend and calls the ledger natively; BatchVerifyMetrics. No backend.state_at trie view is needed - the ledger arena is process-global, so a BasicExternalities + LedgerStorageExt suffices. - batch_block_import.rs: BatchVerifyBlockImport wraps only the import-queue block import (received blocks). Rejects a block only on a genuine invalid proof; on any setup/availability failure it delegates so downstream inline verification still runs (never wrongly halts sync). Remaining (follow-up): mempool custom ChainApi + worker pool, and the per-tx isolation fallback. Assisted-by: Claude:claude-opus-4-8 Signed-off-by: Oscar Bailey <79094698+ozgb@users.noreply.github.com>
Follow-up to the batch ZK-proof verification core (b731346), delivering the two deferred pieces so the mempool ingress path is real and safe to enable. Component A (ledger): implement the isolation fallback in Bridge::batch_verify_transactions (was todo!()). On aggregate-batch failure it verifies each ready transaction individually to isolate the offender(s), caching false for bad proofs and warming caches for good ones. Extracts a shared warm_verified_tx helper used by both the success loop and the fallback. Component B (node): add MidnightChainApi plus a bounded queue and blocking worker pool that batch-verify external Midnight submissions natively, warming the proof/soft/strict caches and building the same validity tags the runtime would (delegating invalid/unavailable cases to the runtime). Reworks the transaction pool to BasicPool<MidnightChainApi> (SingleState), reconstructs pool limits from the CLI (--pool-limit/--pool-kbytes/--tx-ban-seconds), and reuses one BatchVerifier for the mempool and block-import paths. Both flags (batch_verify_block_import, batch_verify_mempool) default off. Assisted-by: Claude:claude-opus-4-8 Signed-off-by: Oscar Bailey <79094698+ozgb@users.noreply.github.com>
Signed-off-by: Oscar Bailey <79094698+ozgb@users.noreply.github.com>
Signed-off-by: Oscar Bailey <79094698+ozgb@users.noreply.github.com>
Signed-off-by: Oscar Bailey <79094698+ozgb@users.noreply.github.com>
Move the aggregate-verification duration timer inside BatchVerifier::batch_verify (around the batch_verify_transactions crypto call) so midnight_batch_verify_duration_seconds is recorded for the block-import path as well as the mempool path, which previously recorded nothing. Remove the mempool's now-redundant external timer to avoid double-counting; both paths funnel through this method. Assisted-by: Claude:claude-4.8-opus Signed-off-by: Oscar Bailey <79094698+ozgb@users.noreply.github.com>
Two-phase Docker A/B benchmark for block-import batch proof verification. prime.sh builds a proof-heavy dev chain (fan-out of shielded coins, then chunked batch-single-tx working around the single funder's DUST-output limit) and archives the node's base_path; benchmark.sh restores it into a non-authoring producer and full-syncs a fresh node with BATCH_VERIFY_BLOCK_IMPORT off vs on, comparing sync time (min of N repeats) and scraping the syncer's batch-verify metrics with a coverage/engagement verdict. Adds 'just batch-verify-perf-{prime,bench}'. See scripts/tests/batch-verify-perf/{README,FINDINGS}.md.
Assisted-by: Claude:claude-4.8-opus
Signed-off-by: Oscar Bailey <79094698+ozgb@users.noreply.github.com>
Add ledger_proof_verify_duration_seconds / _txs_total (labelled by mode) to LedgerMetrics so ZK proof-verification crypto is recorded at transaction granularity on BOTH the inline (OFF) and batch (ON) block-import paths, which already share one LedgerMetricsExt registry: - mode="inline": per-tx well_formed WITH proofs in get_verified_transaction (the OFF / cold-proof-cache path). - mode="batch": the aggregate batch_verify_proofs call (the ON crypto). - mode="batch_prep": per-tx well_formed WITHOUT proofs on the batch path (the non-crypto work both paths pay). get_verified_transaction now returns the inline crypto duration (Some only when it actually verified proofs inline); apply_transaction records it. The OFF verification path is otherwise unchanged -- well_formed is only wrapped in a timer. The existing midnight_batch_verify_duration_seconds records only on the ON path (per-batch) and had nothing to diff against; these per-tx metrics give a clean OFF-vs-ON crypto speedup that sidesteps the block/DB/sync wall-clock noise. benchmark.sh scrapes them from both runs and reports full-verify and crypto-only per-tx speedups (per-tx = _sum / _txs_total); README/FINDINGS updated. Assisted-by: Claude:claude-4.8-opus Signed-off-by: Oscar Bailey <79094698+ozgb@users.noreply.github.com>
The OFF (inline) block-import path never emitted the ledger_proof_verify_* mode="inline" metric, so the batch-verify perf harness reported "insufficient samples" and could not compute the OFF-vs-ON per-tx crypto speedup. Root cause: send_mn_transaction is unsigned, so during execute_block FRAME runs ValidateUnsigned::pre_dispatch (validate_guaranteed_execution) BEFORE dispatching the call. That pre_dispatch runs the inline ZK crypto and warms the STRICT cache but discarded its timing; by the time apply_transaction (the only place recording mode="inline") called get_verified_transaction, it hit the warm cache and got None, so nothing was recorded. Record the inline duration where the crypto actually runs on the OFF block-import path: do_validate_guaranteed_execution now returns the inline duration and validate_guaranteed_execution observes it. apply_transaction's recording stays as a guarded fallback for paths with no preceding pre_dispatch (e.g. tests). On the ON path the batch verifier pre-warms the STRICT/proof caches, so this records no false inline samples. Assisted-by: Claude:claude-4.8-opus Signed-off-by: Oscar Bailey <79094698+ozgb@users.noreply.github.com>
benchmark.sh now picks its run mode like toolkit-tokens-minter-e2e.sh: pass a node image and it runs containers (unchanged); pass nothing and it runs a locally-built binary (NODE_BIN, default target/release/midnight-node) as host processes against the existing archive. This is the fast inner loop for node-side changes -- build once and benchmark without waiting on a CI image. Local mode uses host processes rather than a layered image because the image base (amazonlinux 2023, glibc 2.34) is older than a typical dev host, so a freshly built host binary can't run inside it. The local producer re-supplies the dev preset's authoring args (the CLI replaces the preset's args array) and runs with the repo root as CWD so the preset's relative res/ paths resolve; BASE_PATH points it at the restored archive dir. Docker and local modes share the polling, metrics-scrape and reporting logic via mode-aware node-runner helpers (start_producer/start_syncer/syncer_alive/...); README documents the new mode and its genesis-match caveat. Assisted-by: Claude:claude-4.8-opus Signed-off-by: Oscar Bailey <79094698+ozgb@users.noreply.github.com>
DCO Remediation Commit for Michał Skowron <michal.skowron@iohk.io> I, Michał Skowron <michal.skowron@iohk.io>, hereby add my Signed-off-by to this commit: 0009021 Signed-off-by: Michał Skowron <michal.skowron@iohk.io>
DCO Remediation Commit for Michał Skowron <michal.skowron@shielded.io> I, Michał Skowron <michal.skowron@shielded.io>, hereby add my Signed-off-by to this commit: f684d77 I, Michał Skowron <michal.skowron@shielded.io>, hereby add my Signed-off-by to this commit: 86ba80a Signed-off-by: Michał Skowron <michal.skowron@shielded.io>
…tion tip Re-isolate the ledger `js/batch-verification` branch at `2509be25` and point `[patch.crates-io]` at the new isolate commit `cd54fd2f` on the ledger branch `js/batch-verification-isolated` (appended, no force-push). Two wiring changes beyond the rev bump: - `midnight-ledger-static` joins the isolated set (7 crates now). The branch moved `static/version` 9 -> 10 for the new ZKIR-v3 Dust artifacts without bumping the package version, so the crates.io 9.0.0 copy would bake a stale `version!()` into the data-provider key paths. `zswap/10/*` is byte-identical to `zswap/9/*` upstream, so L7/L8 are unaffected. - The midnight-zk stack (`midnight-proofs`/`-curves`/`-circuits`/`-zk-stdlib`) is patched here too, pinned to `ae7b9aeb` on `midnight-zk` branch `irakoton/batch-verify`. The ledger branch does this in its own `[patch.crates-io]`, which cargo ignores for dependencies — only the root workspace's patch table counts. `base-crypto`, `onchain-runtime`, `storage` and `storage-core` also changed on the branch but only cosmetically (clippy fixes, a semver-compatible `reqwest` bump), so they stay on their rc tags / crates.io copies. `cargo check --workspace --all-targets` is clean. Two follow-ups, both tracked in Batch-Verification-Notes.md: 1. Static fixture transactions need regenerating. `cargo test -p midnight-node-ledger -p midnight-node-e2e --lib` goes 99/99 -> 96/99; `should_apply_transaction`, `should_get_contract_state` and `should_validate_transaction` all fail `InvalidProof` on `res/test-contract/contract_tx_*_undeployed.mn`. The midnight-zk branch changes the proof-system architecture (`nb_arith_cols` on `ZkStdLibArch`, `sha3-circuit`/`blake2b_halo2` moving to git revs), so proofs produced under the old architecture no longer verify — the ledger repo recomputed its own precompile verifier hashes for the same reason (21234ede, 2509be25). Fix is `earthly -P +rebuild-genesis-state-undeployed`, not a pin change. 2. Dust proving keys must come from the bundled static artifacts. The branch's `ledger/static/dust/spend.*.sha256` disagree with what `srs.midnight.network/dust/10/spend.*` serves, and there is no plan to publish the new ZKIR-v3 Dust artifacts, so anything fetching Dust keys via `MidnightDataProvider`/`DUST_EXPECTED_FILES` (`ledger/helpers`, hence proving-side tests and the toolkit) has to source them from the crate's `ledger/static/dust/*` instead. Node-side verification is unaffected — `SPEND_VK` is an `include_bytes!` of that same bundled key. Assisted-by: Claude:claude-opus-5 Signed-off-by: Oscar Bailey <79094698+ozgb@users.noreply.github.com>
… them The batch-verification ledger pin bump moved `static/version` 9 -> 10, which changed the key paths the toolkit's local prover resolves through `MidnightDataProvider`. Zswap keys are unaffected in content (byte-identical to 9) but still require a fresh network fetch under the new path; Dust's new ZKIR-v3 keys aren't byte-identical, and no valid `dust/10/spend.prover` is published anywhere for this branch (srs.midnight.network serves stale content), so that fetch fails outright. Both were tracked as follow-ups in Batch-Verification-Notes.md. - scripts/seed-zswap-keys.sh: seeds zswap/10 by copying the already-fetched zswap/9 bytes (verified byte-identical via sha256), avoiding a redundant fetch under the new version path. - scripts/seed-dust-keys.sh: builds the pinned ledger rev's `zkir` compiler and runs `compile-many` against the bundled Dust circuit source (zkir-precompiles/dust/spend.zkir) to regenerate the prover/verifier/IR locally - the same mechanism midnight-ledger's own nix flake uses for its `local-params` package. Verified the compiled output hash-matches DUST_EXPECTED_FILES exactly. - justfile: `just seed-zswap-keys` / `just seed-dust-keys` for local dev. - Earthfile: seed both into the toolkit image (new `+dust-keys` build target, kept separate from `+build` so only toolkit-image pays the extra compile time) so containers never reach srs.midnight.network for either at runtime. - ledger/helpers/Cargo.toml: adds `midnight-zkir-v3` (binary feature only, to unlock the `zkir` CLI for the Dust seed script). Assisted-by: Claude:claude-sonnet-5 Signed-off-by: Oscar Bailey <79094698+ozgb@users.noreply.github.com>
The batch-verification ledger pin migrated the Dust spend circuit to ZKIR-v3
(`midnight-zkir-v3`) while zswap and contract circuits stayed on ZKIR-v2
(`midnight-zkir`). A single `LocalProvingProvider` can no longer prove a whole
ledger-9 transaction: it parses every circuit through `zkir`'s own `IrSource`,
whose `IrMinorVersion` enum has no v3 case at all, so this isn't a
wrong-dispatch bug - the type genuinely can't represent v3. Any transaction
paying its fee with a Dust spend panicked with
Tx should be provable: Proving(expected one of 'ir-source[v2-generic]'
or 'ir-source[v2]', got 'ir-source[v3-generic]'
reproduced by `generate-txs --dust-warp ... contract-simple deploy`. This
surfaced once 94b2895 made the keys resolvable offline; genesis proving is
unaffected because it never needs a live Dust proof.
Replace the single shared provider with a per-generation
`make_proving_provider`, wired the same way as `block_context` - necessary
because `zkir_v3::IrSource: Zkir` only satisfies the ledger-9
transient-crypto bound and would be a hard compile error under L7/L8, which
compile the same `common/proving.rs` against different concrete crates.
- proving_provider/v2_only.rs (L7/L8): plain v2 pipeline, a direct lift of
the previous call site - no behaviour change.
- proving_provider/v2_or_v3.rs (L9): `VersionedProvingProvider` peeks each
circuit's IR tag with `peek_tag` (a <=512-byte prefix read, not a full
parse) and routes that one proof to `zkir` or `zkir_v3`.
- ledger/helpers/Cargo.toml: rename the dependency key `midnight-zkir-v3` ->
`zkir-v3` so it's referenceable as `zkir_v3` in code, matching how
upstream `ledger`/`proof-server` alias it. The `binary` feature stays;
scripts/seed-dust-keys.sh selects by package name and is unaffected.
Modelled on midnight-ledger's own `CombinedProofProvider`
(ledger/src/test_utilities.rs), which isn't `pub` so can't be imported.
Upstream's whole-tx `/prove-tx` proof-server endpoint has the identical bug
and is explicitly deprecated, so it's no reference either.
Two guard tests: `ir_tags_match_upstream` pins the match-arm tag literals to
`<zkir::IrSource as Tagged>::tag()` / `<zkir_v3::IrSource as Tagged>::tag()`,
since a silent upstream rename would route every proof to the error arm; and
`committed_contract_ir_is_still_v2` fails the moment contract IR turns v3 -
which is exactly when the (currently unreachable) v3 `check()` branch becomes
load-bearing, as `check()` is only ever called for contract calls.
Assisted-by: Claude:claude-sonnet-5
Signed-off-by: Oscar Bailey <79094698+ozgb@users.noreply.github.com>
Signed-off-by: Oscar Bailey <79094698+ozgb@users.noreply.github.com>
…tion tip Refresh the js/batch-verification-isolated commit to the branch's new tip (71fc0084, "Add control over the 'identify_failures' param"), which adds a linear_revalidation bool to ProofKind::batch_proof_verify(). Our call site passes false since batch_verify_transactions already isolates offenders itself on aggregate failure. Also catches ledger/helpers' direct zkir-v3 pin, which isn't covered by [patch.crates-io] and was previously missed. Assisted-by: Claude:claude-sonnet-5 Signed-off-by: Oscar Bailey <79094698+ozgb@users.noreply.github.com>
…te bad proofs
`batch_verify_proofs` now takes `linear_revalidation` from its caller instead of
hardcoding `false`, and returns `BatchVerifyFailure` rather than `Err(())`:
- `Localized(tx_indices)` maps the ledger's `InvalidProofBatch { failed_indices }`
(evidence-space) back to transaction indices via a per-tx evidence prefix-sum
table. Ascending and deduplicated; proofless transactions (`ClaimRewards`)
contribute an empty range and can never be blamed.
- `Unlocalized` covers `linear_revalidation` off, failed evidence collection, and
the ledger paths that don't localize (legacy v2 batch, verifier-key init). It's
also the defensive answer when reported indices don't map into our table — an
empty `Localized` would read as "no offender" and let the caller accept a batch
the ledger just rejected.
`batch_verify_transactions` passes `linear_revalidation: isolate_on_failure`, so
block import takes the cheaper unlocalized rejection (it never needs per-tx
attribution) and the mempool asks for localization. They're the same decision, so
one is derived from the other rather than adding a second flag that would permit
isolate-without-localize.
That removes our custom isolation entirely: `FallbackItem`,
`isolate_fallback_results` and their closure seam are gone, along with the O(n)
batch-of-one re-verification they performed on every bad batch. A localized
failure now walks the existing results loop once — blamed transactions get
`PROOF_VERIFICATION_CACHE = false` plus an `Invalid` result, the rest are warmed
as before, and nothing is re-verified. `Unlocalized` still fails the whole batch,
which is safe for the mempool since `process_batch` then delegates every parked
submission to the runtime.
`BatchVerifyFailure` lives in the new `ledger/src/common/batch.rs` (it carries no
version-dependent types); the ledger-7/8 stubs take the flag and return
`Unlocalized`. The evidence→transaction index mapping is unit-tested.
`cargo fmt` also fixed a pre-existing formatting violation in `common/mod.rs`.
Assisted-by: Claude:claude-opus-5
Signed-off-by: Oscar Bailey <79094698+ozgb@users.noreply.github.com>
Conflicts resolved: - `ledger/src/versions/common/mod.rs`: main threaded a `tblock_correction: Option<&TBlockCorrection>` parameter through `get_verified_transaction` / `do_validate_transaction` / `do_validate_guaranteed_execution` (#1932) and added `block_context_tblock` to `StrictTxValidationKey`; this branch changed the same call sites to return the inline proof-verify duration and to gate `WellFormedStrictness` on the proof-verification cache. Kept both: the correction parameter alongside the batch-verify strictness and timing. `batch_verify_transactions` now applies the same tblock correction before its deferred-proof `well_formed` pass, and passes `block_context.tblock` into `warm_verified_tx` so the STRICT-cache entries it warms use the same key as the per-transaction path. - `node/src/cfg/midnight_cfg/mod.rs`: kept both field groups (`batch_verify_*` and `tblock_correction_*`). - `node/src/service.rs`: main replaced the forked aura import queue with `sc_partner_chains_consensus::PartnerChainsVerifier` + `BasicQueue` (#1700). Adopted that construction and passed the `BatchVerifyBlockImport` wrapper as the queue's block import, so batch verification still covers only the received-block path. `ExtensionsFactory` now also gets `TBlockCorrection::from(&midnight_cfg)` alongside this branch's shared ledger metrics/storage handles. - `node/Cargo.toml`: dropped the duplicate `sp-consensus` entry both sides added. Assisted-by: Claude:claude-opus-5 Signed-off-by: Oscar Bailey <79094698+ozgb@users.noreply.github.com>
`cargo clippy --workspace --all-targets --features runtime-benchmarks,try-runtime -- -D warnings` (the CI invocation) failed with 11 errors, all from the shared `versions/common` module and each reported three times — once per ledger-version instantiation: - `large_enum_variant` on the local `Prep` enum. Allowed rather than boxed: it is a short-lived per-batch buffer, so the wasted bytes are bounded by the batch size, whereas boxing would add two heap allocations per transaction on the very hot path this function exists to speed up. - `useless_conversion` on `Prep::Failed(e.into())` — `tagged_deserialize` already yields a `LedgerApiError`. - `doc_lazy_continuation` ×5 on `get_verified_transaction`'s doc comment, where the "Returns …" paragraph followed the bullet list without a blank line and so parsed as a continuation of the last bullet. Assisted-by: Claude:claude-opus-5 Signed-off-by: Oscar Bailey <79094698+ozgb@users.noreply.github.com>
With batch verification on, `warm_verified_tx` pre-warms the SOFT cache at mempool ingress, so the later `do_validate_transaction` returns early from its cache hit and never reaches its `📋 Validated transaction … for mempool` line. A batch-ON node therefore emitted no per-transaction line at all, making log-derived transaction counts incomparable against a batch-OFF node. Emit the same line from `warm_verified_tx`'s success arm, next to the SOFT-cache insert it corresponds to, using the ledger transaction hash so the value matches what the non-batched path prints for the same transaction (the cache `key` is the Twox128 validation key, not the tx hash). Restricted to the mempool ingress: on the block-import path the non-batched path validates through `pre_dispatch` (`do_validate_guaranteed_execution`), which emits no such line either, so logging there would add lines the OFF side lacks and put per-tx INFO logging inside the hot path the block-import A/B measures. Assisted-by: Claude:claude-opus-5 Signed-off-by: Oscar Bailey <79094698+ozgb@users.noreply.github.com>
Reconcile the revalidation-based validation cache with main's `tblock` correction (#1924). The cache value now carries the effective `well_formed` timestamp alongside the ledger state, and an entry is only served as a strict hit when both match. A differing timestamp revalidates instead, which re-runs exactly the two time-dependent checks — intent TTL and the dust validity window, both under the ledger's `param_check(always = true)` — without redoing the ZK work. Without this, a mempool entry verified at `parent + slot_duration * (1 + MaxSkippedSlots)` would strict-hit `pre_dispatch` and `apply_transaction` at block start, letting a transaction enter a block having only ever been checked against a future timestamp, and making block import depend on local mempool cache contents. The correction itself is threaded through to both `well_formed` call sites so historical blocks below `tblock_correction_disable_after` still import; the mempool path stays uncorrected, as its block context is already skewed. Revalidation hits now also dry-run the guaranteed segment. `well_formed` never checks applicability and `RevalidationReference` skips the stateless checks, so an already-applied transaction revalidated clean and survived in the pool until the producing node's `pre_dispatch` rejected it. Also drops a duplicate `midnight-primitives-ledger` dev-dependency that both sides added to pallets/midnight in different syntaxes, which left the workspace manifest unloadable. Assisted-by: Claude:claude-opus-5 Signed-off-by: Oscar Bailey <79094698+ozgb@users.noreply.github.com>
Every new block import now re-validates cached entries, including a dry-run of `apply`, so relay nodes evict stale transactions from the mempool without needing an unconditional time-based eviction. TTI still caps memory growth on quiet chains. Assisted-by: Claude:claude-opus-5 Signed-off-by: Oscar Bailey <79094698+ozgb@users.noreply.github.com>
Signed-off-by: Oscar Bailey <79094698+ozgb@users.noreply.github.com>
A cache entry records what `well_formed` proved about a transaction at a given state and timestamp, not a validity verdict, so a rejection does not falsify it. Dropping the entry only forced the next attempt to redo the ZK verification to reach the same conclusion. Remove the three invalidation sites — the revalidation failure in `get_verified_transaction` and the dry-run failures in `do_validate_transaction` and `do_validate_guaranteed_execution` — leaving the cache with no invalidation path at all: entries are evicted by capacity or TTI only. Every read still either strict-hits an identical state and timestamp or re-runs the checks that can change, so no stale verdict can survive. Keeping entries for rejected and already-applied transactions is the point rather than a leak: a reorg that returns one to the pool revalidates it instead of re-verifying it from scratch, which matters more as forks become routine. Assisted-by: Claude:claude-opus-5 Signed-off-by: Oscar Bailey <79094698+ozgb@users.noreply.github.com>
Re-isolate the ledger `js/batch-verification` branch at `ef5f7a04` (the ZKIR-v3 zswap recompile) as `fc87c27c` on `js/batch-verification-isolated`, and point the 7 `[patch.crates-io]` entries plus the direct `zkir-v3` pin in `ledger/helpers` at it. Upstream moved zswap's circuits to ZKIR v3, so its proof evidence is now emitted as `ContractProofEvidence::V3` and joins the localisable batch, and `static/version` became `10-dust-zswap-v3`. `batch_proof_verify`'s signature is unchanged, so no node code changes are needed; `cargo test -p midnight-node-ledger` stays green (98/98) with the committed fixture transactions. Key seeding did have to change: neither `dust/10-dust-zswap-v3/*` nor `zswap/10-dust-zswap-v3/*` is published (both 403), and the old fetch-`zswap/9`-and-copy trick no longer applies now that the zswap artifacts differ. Merge `seed-dust-keys.sh` and `seed-zswap-keys.sh` into `scripts/seed-zk-keys.sh`, which compiles both sets with `zkir compile-many` and reads the version and the expected hashes out of the pinned checkout, so a future `static/version` bump needs no edit here. Assisted-by: Claude:claude-opus-5 Signed-off-by: Oscar Bailey <79094698+ozgb@users.noreply.github.com>
Re-isolate the ledger `js/batch-verification` branch at `99f6dbfb` ("Return
meaningful errors for proof verification failure") as `31529634` on
`js/batch-verification-isolated`, and point the 7 `[patch.crates-io]` entries plus
the direct `zkir-v3` pin in `ledger/helpers` at it.
Diagnostics only upstream: `MalformedTransaction::InvalidProof` and
`BatchVerifyError::Unlocalized` now carry the failing stage, proof version, contract
address/entry point, VK `k`, public-input count and proof size instead of a bare
"Invalid proof", and a failing Dust spend proof logs a warning. No API movement, so
no node code changes; `static/version` is untouched, so the seeded ZK keys stay
valid. `cargo test -p midnight-node-ledger` stays green (98/98).
The midnight-zk pin in the ledger branch's lockfile is unchanged (`ae7b9aeb`), and
`cargo check --offline` re-locked exactly the 7 crates' `source` lines.
Assisted-by: Claude:claude-opus-5
Signed-off-by: Oscar Bailey <79094698+ozgb@users.noreply.github.com>
…fication Signed-off-by: Oscar Bailey <79094698+ozgb@users.noreply.github.com> # Conflicts: # Cargo.lock # ledger/helpers/src/lib.rs # ledger/src/host_api/ledger_9.rs # ledger/src/lib.rs # ledger/src/versions/common/mod.rs # node/src/cfg/midnight_cfg/mod.rs # node/src/command.rs # node/src/service.rs Assisted-by: Claude:claude-opus-5 Signed-off-by: Oscar Bailey <79094698+ozgb@users.noreply.github.com>
… toolkit test targets `annotates_git_dependency_with_tag_and_rev` required a `tag:` annotation, but this branch pins the L9 ledger crates by bare `rev` to the batch-verification isolate commit, which carries no tag. Assert the locked commit SHA instead (the immutable build identity) and rename accordingly. `committed_contract_ir_is_still_v2` read the zkir through `CARGO_MANIFEST_DIR/../../static/`, but `static/` isn't in the CI build context - only the subset copied to `MIDNIGHT_LEDGER_TEST_STATIC_DIR` is, so it failed with NotFound in `+test`. Read it from that var, the same one `test_resolver` uses. `+toolkit-image` bakes in the locally compiled Dust/Zswap keys, but `+build-test-toolkit` and `+local-env-ci` prove without them, and this branch's `static/version` (`10-dust-zswap-v3`) isn't published on srs.midnight.network - both died fetching `zswap/10-dust-zswap-v3/spend.prover`. Copy the same `+zk-keys` artifacts in and point `MIDNIGHT_PP` at them, matching what images/toolkit/Dockerfile already does. Assisted-by: Claude:claude-opus-5 Signed-off-by: Oscar Bailey <79094698+ozgb@users.noreply.github.com>
…zswap-v3 circuits The batch-verification ledger pin moved `static/version` from `10` to `10-dust-zswap-v3`, which regenerated the zswap circuits (spend.verifier 544554ef… -> 2098f931…). The committed genesis txs were proven against the old circuits, so the toolkit's replay-from-genesis failed `well_formed` -> `batch_proof_verify` with "Transcript error: Invalid BLS12-381 scalar encoding in proof (batch size: 28)", taking out all five Toolkit E2E jobs. Rebuilt via `+rebuild-genesis-state` for both genesis-spawned networks, using the CI-built toolkit image (which bakes in the locally compiled keys) rather than a local toolkit build. `local` came out byte-identical - it funds no faucet wallets at genesis, so it has no zswap proofs to invalidate; its CI failure was only the missing key seeding. No chainspec rebuild needed: `dev` reads the undeployed .mn files by path and `midnight-node-res` include_bytes! them, and `check_all_chainspec_integrity` passes. Assisted-by: Claude:claude-opus-5 Signed-off-by: Oscar Bailey <79094698+ozgb@users.noreply.github.com>
Ledger 8 and ledger 9 share one zk-params cache namespace here. `midnight-zswap 8.1.1` (crates.io) embeds the pre-v3 expected key hashes, but `midnight-ledger-static` is patched globally to the batch-verification rev, so `version!()` is `10-dust-zswap-v3` for both ledgers - L8 then reads L9's regenerated key and dies with "Hash mismatch in data stored at .../zswap/10-dust-zswap-v3/spend.prover" (found 85c677bc…, expected 19d234b5…). It only passed before the pin bump because the two ledgers' zswap keys were byte-identical; the v3 recompile ended that. Not a missing-key problem, so key seeding can't fix it. The comment records the two real fixes (a separate `MidnightDataProvider::dir` for L8, or re-cutting the isolate on a base whose `static/version` is `10`) for whoever needs L8 proving back. Assisted-by: Claude:claude-opus-5 Signed-off-by: Oscar Bailey <79094698+ozgb@users.noreply.github.com>
Semantic merge of PR #744's revalidation-based validation cache with PR #1872's batch proof verification: - Single TX_VALIDATION_CACHE (revalidation-based) replaces STRICT/SOFT; the batch-verification ingress now warms it (state + effective tblock) instead of the removed STRICT/SOFT caches. - PROOF_VERIFICATION_CACHE re-keyed by (runtime_version, ledger tx hash) to match the validation-cache key, read on the cache-miss path in verify_transaction to defer already-batch-verified ZK crypto. - get_verified_transaction returns (tx, cache outcome, inline verify duration) so both PRs' metrics are preserved. - validate_transaction still returns the state-independent Twox128 provides-tag so the pool tag stays byte-for-byte identical to the native tag built by the node's batch_chain_api. Assisted-by: Claude:claude-fable-5 Signed-off-by: chrispalaskas <chris.palaskas@gmail.com>
… test branch Applied as a squashed three-way patch of 3edc676..6d63b45 instead of a git merge: the branch's criss-cross history (second merge base from 2026-04-20) made 'git merge' generate ~60 spurious conflicts. Adaptations to the combined branch: - Dropped the ledger_7.rs host-api hunk (ledger-7 support no longer exists on this branch). - Resolved the types import conflict (keep LedgerStateKey, WrappedHash stays removed by the PR #744 merge). - Updated PR #744's change_state_hash test helper in pallet-midnight tests to the typed LedgerStateKey state-key API introduced by #1443. Source head: 6d63b45 Assisted-by: Claude:claude-fable-5 Signed-off-by: chrispalaskas <chris.palaskas@gmail.com>
… additions PR #744 added metrics/spec-version mocks to mock.rs using dev-only dependencies; PR #1443 compiles mock.rs into the lib under the test-utils feature (for the persist_refcount integration test), where dev-dependencies are unavailable. Promote sp-version, prometheus-endpoint and midnight-primitives-ledger to optional dependencies enabled by test-utils. Assisted-by: Claude:claude-fable-5 Signed-off-by: chrispalaskas <chris.palaskas@gmail.com>
Reverts c6590ef in preparation for replacing it with PR #2050 (ozgb-ledger-intermediate-states-less-persists), which supersedes #1443 with a keep-alive-cache approach that needs none of #1443's runtime storage / state-key-type changes. pallets/midnight/Cargo.toml is deliberately NOT reverted: #2050 carries the identical test-utils feature block and [[test]] stanza for the same persist_refcount integration test, and ca3e1ab's optional-dependency promotion is still required. Assisted-by: Claude:claude-opus-5 Signed-off-by: chrispalaskas <chris.palaskas@gmail.com>
Replaces PR #1443 on the combined test branch. Applied as a squashed three-way patch of 8d3e0cf..1da3723 (PR #2050 head) rather than a git merge, for the same criss-cross-history reason as the original #1443 application. Unlike #1443, #2050 needs no runtime storage or state-key-type changes: intra-block intermediates are held live in a refcounted keep-alive cache in the ledger crate instead of being persisted, so the pallet's LedgerStateKey typing, migration v3, metadata rebuild and runtime changes are all gone. Adaptations to the combined branch: - Cargo.lock regenerated rather than taken from #2050: this branch pins midnight-ledger by rev (#1872's 2026-08-17 update), not by the ledger-9.1.0.0-rc.4 tag main uses. The only real change is moka 0.11.3 -> 0.12.15, which #2050 needs for `Cache::and_compute_with`. - pallets/midnight/Cargo.toml kept from ca3e1ab: #2050 carries the identical test-utils feature block, and the optional-dependency promotion for #744's mock additions is still required. - ledger/src/versions/common/mod.rs: kept both #1872's proof-verification cache helpers and #2050's keep-alive cache block (adjacency conflict only — they touch different caches). - primitives/ledger/src/lib.rs: kept both #1872's proof_verify_duration / proof_verify_txs metrics and #2050's ledger_state_cache_size gauge. Assisted-by: Claude:claude-opus-5 Signed-off-by: chrispalaskas <chris.palaskas@gmail.com>
…eight Two independent limits closed a block well before the node ran out of execution capacity: - `frame_system::BlockLength` was `max_with_normal_ratio(1 MiB, 75%)`, i.e. ~786 KiB usable for normal dispatches. Midnight transactions are large relative to a typical Substrate extrinsic, so blocks hit the length limit rather than the weight limit. Now 5 MiB. - `ConfigurableTransactionSizeWeight` shared `DefaultWeight` with `ConfigurableOnInitializeWeight` and `ConfigurableOnRuntimeUpgradeWeight`, so its default was `EXTRA_WEIGHT_TX_SIZE` — 20 ms ref-time, ~1% of a 2 s block — added flat to every transaction on top of its real gas-metered cost, and it was the dominant term. It now gets its own `#[pallet::type_value]` returning `Weight::zero()`. `DefaultWeight` and the `get_tx_weight` fallback for an unmeterable transaction are deliberately unchanged and still use `EXTRA_WEIGHT_TX_SIZE`. This changes exactly the flat per-transaction add-on and nothing else; the value stays settable at runtime via the root-only `set_tx_size_weight` extrinsic. `spec_version` 002_001_000 -> 002_001_001. Both values are metadata-visible (`BlockLength` is a `frame_system` constant, the weight default is a storage-entry default), so runtime metadata needs rebuilding via `/bot rebuild-metadata`. Assisted-by: Claude:claude-opus-5 Signed-off-by: chrispalaskas <chris.palaskas@gmail.com>
Open
18 tasks
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.
Overview
Combined test branch integrating three open performance PRs so they can be load-tested
together (perfnet A/B runs), plus two throughput-limit changes needed to actually reach the
regime the three PRs are meant to improve:
ozgb-batch-proof-verification, headc9d9e55, incl. the 2026-08-17 ledger update)skowron/tx-revalidation, head3dd295e, incl. Oscar's tblock + stale-check fixes:202ba1d4,d1d6cc9f,624b3f55)ozgb-ledger-intermediate-states-less-persists, head1da3723e) — replaces fix: stop persisting intermediate ledger states #1443, which was on this branch untilf9c315c8Not intended to merge into
mainas-is — it exists to measure the changes in combination.How the branch was assembled
Base is the #1872 head (freshest ledger pin). The others were integrated on top.
Merge of #744 (commit
2c5df2b) — semantic merge, not mechanicalBoth PRs rewrote the same caching layer in
ledger/src/versions/common/mod.rs: #744 replaces the STRICT/SOFT caches with a single revalidation-basedTX_VALIDATION_CACHE; #1872's batch verification warmed STRICT/SOFT and recorded inline-crypto timings. Resolution decisions:batch_verify_transactions→warm_verified_tx) now warms the revalidation cache with the batch's reference state and effectivewell_formedtblock. A later lookup at the same state strict-hits; a later state revalidates the cheap delta — which is exactly feat: replace soft tx cache with revalidation-based validation cache #744's stale-check semantics (a non-strict hit always re-runs the guaranteed dry-run).PROOF_VERIFICATION_CACHEre-keyed fromTwox128(runtime_version ++ tx_bytes)to(runtime_version, ledger tx.hash())— the same identity as the validation-cache key — so the cache-miss path (verify_transaction) can consult it without threading serialized bytes through feat: replace soft tx cache with revalidation-based validation cache #744's call chain. It is read there to defer already-batch-verified ZK crypto (Some(true)→ defer,Some(false)→ reject,None→ verify inline + log error).get_verified_transactionnow returns(VerifiedTransaction, TxValidationCacheOutcome, Option<Duration>)— feat: replace soft tx cache with revalidation-based validation cache #744's hit/revalidation/miss counters and experiment: batch verification #1872'smode="inline"proof-verify baseline are both recorded on the apply and pre-dispatch paths.validate_transaction's returned hash (theand_providestag) to the ledgertx.hash(); experiment: batch verification #1872's node-sidebatch_chain_apibuilds the Twox128 tag natively and must stay byte-for-byte identical to the runtime's.validate_transactiontherefore still returns the restoredtx_validation_cache_key(Twox128, zero-extended), while the validation cache itself uses feat: replace soft tx cache with revalidation-based validation cache #744's key.SOFT_TX_VALIDATION_CACHEinvalidation on pre-dispatch is gone along with the cache itself, matching feat: replace soft tx cache with revalidation-based validation cache #744's "keep validation cache entries across rejections" fix.Replacing #1443 with #2050 (commits
f9c315c8,6bd32a63)#2050 supersedes #1443 with a materially smaller change, so the swap was done as a clean
revert-then-apply rather than by patching #1443 into #2050 in place:
f9c315c8reverts the fix: stop persisting intermediate ledger states #1443 application (c6590ef) wholesale — 24 files, back to pure experiment: batch verification #1872+feat: replace soft tx cache with revalidation-based validation cache #744.pallets/midnight/Cargo.tomlis deliberately not reverted: feat(node): keep intra-block ledger states live instead of persisting them #2050 carries the identicaltest-utilsfeature block and[[test]]stanza for the samepersist_refcountintegration test, andca3e1ab's optional-dependency promotion for feat: replace soft tx cache with revalidation-based validation cache #744's mock additions is still required.6bd32a63applies feat(node): keep intra-block ledger states live instead of persisting them #2050 as a squashed three-way patch of8d3e0cf4..1da3723e(same criss-cross-history reason as the original fix: stop persisting intermediate ledger states #1443 application —git mergeis not usable here).The swap is a large simplification. #1443 changed runtime storage and the state-key type
(
LedgerStateKey), shipped migration v3, and required a metadata rebuild. #2050 needs noneof that: intra-block intermediates are held live in a refcounted keep-alive cache inside the
ledger crate instead of being persisted.
migrations/v3.rs,ledger-state-key-type.md, theprimitives/midnightandruntime/src/lib.rschanges and the metadata delta are all gone.Adaptations to the combined branch:
Cargo.lockregenerated rather than taken from feat(node): keep intra-block ledger states live instead of persisting them #2050: this branch pinsmidnight-ledgerby rev (31529634, experiment: batch verification #1872's 2026-08-17 update), not by theledger-9.1.0.0-rc.4tagmainuses. The only real change is moka 0.11.3 → 0.12.15, which feat(node): keep intra-block ledger states live instead of persisting them #2050 needs forCache::and_compute_with.ledger/src/versions/common/mod.rs— one adjacency conflict: experiment: batch verification #1872's proof-verification cache helpers (insert_proof_result/get_proof_result/proof_verification_cache_size) sit exactly where feat(node): keep intra-block ledger states live instead of persisting them #2050 inserts its keep-alive block. Both kept; they touch different caches and do not interact.primitives/ledger/src/lib.rs— same shape: experiment: batch verification #1872'sproof_verify_duration/proof_verify_txsand feat(node): keep intra-block ledger states live instead of persisting them #2050'sledger_state_cache_sizegauge both kept.host_api/ledger_9.rs,common/types.rs,pallets/midnight-system, the migrations or the metadata at all, so the revert simply restores those to experiment: batch verification #1872+feat: replace soft tx cache with revalidation-based validation cache #744 state.Block length and the flat per-transaction weight (commit
0871f458)Two independent limits closed a block well before the node ran out of execution capacity,
which would have masked what the three PRs above are measuring:
frame_system::BlockLength1 MiB → 5 MiB. It wasmax_with_normal_ratio(1 MiB, 75%), i.e. ~786 KiB usable for normal dispatches. Midnight transactions are large relative to a typical Substrate extrinsic, so blocks were hitting the length limit, not the weight limit.ConfigurableTransactionSizeWeightnow defaults toWeight::zero(). It sharedDefaultWeightwithConfigurableOnInitializeWeightandConfigurableOnRuntimeUpgradeWeight, so its default wasEXTRA_WEIGHT_TX_SIZE— 20 ms ref-time, ~1% of a 2 s block — added flat to every transaction on top of its real gas-metered cost, and it was the dominant term. It now gets its own#[pallet::type_value].DefaultWeightand theget_tx_weightfallback for a transaction whose cost cannot bemetered are deliberately unchanged and still use
EXTRA_WEIGHT_TX_SIZE. This changes exactlythe flat per-transaction add-on and nothing else; the value stays settable at runtime via the
root-only
set_tx_size_weightextrinsic, so a chain that already zeroed it by extrinsic seesno change from the new default.
spec_versionbumped002_001_000→002_001_001.🗹 TODO before merging
/bot rebuild-metadata—BlockLengthis aframe_systemconstant and the weight default is a storage-entry default, so both are metadata-visible and the checked-in snapshots are stale. (Note this is newly needed for the throughput commit; feat(node): keep intra-block ledger states live instead of persisting them #2050 itself needs no metadata change, unlike fix: stop persisting intermediate ledger states #1443.)mainor stays a perf-test vehicleearthly +node-image) — local WASM build not verified (environment lacked clang)📌 Submission Checklist
git commit -s) for the DCOchanges/runtime/changed/block-length-5mib-zero-tx-size-weight.mdcovers the two throughput changes🧪 Testing Evidence
All native, with
SKIP_WASM_BUILD=1(WASM runtime build not run locally — clang unavailable in the assembly environment):cargo check --workspace --all-targets— cleancargo clippy --workspace --all-targets— cleancargo fmt --all -- --check— clean (22f2ea32fixes two pre-existing diffs in experiment: batch verification #1872's code)cargo test -p midnight-node-ledger— 81 passedcargo test -p pallet-midnight— 29 passed / 2 ignored (incl. feat: replace soft tx cache with revalidation-based validation cache #744's revalidation cache-metrics tests, andsets_extra_transaction_size_weightupdated for the zero default)cargo test -p pallet-midnight --features test-utils --test persist_refcount— 1 passed (feat(node): keep intra-block ledger states live instead of persisting them #2050'skeep_alive_invariants, the real functional proof that the keep-alive caches behave on the combined branch)cargo test -p midnight-node batch— 7 passed (incl. the provides-tag mirror assertion inbatch_chain_api)The ledger/pallet test counts are lower than the previous #1443-based revision (82 / 31) because #2050 drops #1443's
LedgerStateKeyand migration-v3 tests along with the code they covered.🔱 Fork Strategy
#1872, #744 and #2050 are all client-side (#2050 explicitly has no storage-layout, host-ABI
or runtime change — that is the main way it differs from #1443). The block-length and
per-transaction-weight changes are a runtime update and need a
setCode. As with priorperfnet work: host-function changes cannot be A/B'd on a single chain.
Links
🤖 Generated with Claude Code