Skip to content

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
mainfrom
christos-batch-reval-ledger-storage
Draft

chrispalaskas wants to merge 54 commits into
mainfrom
christos-batch-reval-ledger-storage

Conversation

@chrispalaskas

@chrispalaskas chrispalaskas commented Aug 18, 2026 •

Copy link
Copy Markdown
Contributor

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:

Not intended to merge into main as-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 mechanical

Both PRs rewrote the same caching layer in ledger/src/versions/common/mod.rs: #744 replaces the STRICT/SOFT caches with a single revalidation-based TX_VALIDATION_CACHE; #1872's batch verification warmed STRICT/SOFT and recorded inline-crypto timings. Resolution decisions:

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:

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 none
of 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, the
primitives/midnight and runtime/src/lib.rs changes and the metadata delta are all gone.

Adaptations to the combined branch:

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::BlockLength 1 MiB → 5 MiB. It 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 were hitting the length limit, not the weight limit.
  • ConfigurableTransactionSizeWeight now defaults to Weight::zero(). It 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].

DefaultWeight and the get_tx_weight fallback for a transaction whose cost cannot be
metered 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, so a chain that already zeroed it by extrinsic sees
no change from the new default.

spec_version bumped 002_001_000 → 002_001_001.

🗹 TODO before merging

📌 Submission Checklist

  • All commits are signed off (git commit -s) for the DCO
  • Changes are backward-compatible (or flagged if breaking)
  • Pull request description explains why the change is needed
  • Self-reviewed the diff
  • I have included a change file, or skipped for this reason: change files from the source PRs are carried in by the merge; changes/runtime/changed/block-length-5mib-zero-tx-size-weight.md covers the two throughput changes
  • If the changes introduce a new feature, I have bumped the node minor version
  • Update documentation (if relevant)
  • Updated AGENTS.md if build commands, architecture, or workflows changed
  • No new todos introduced

🧪 Testing Evidence

All native, with SKIP_WASM_BUILD=1 (WASM runtime build not run locally — clang unavailable in the assembly environment):

The ledger/pallet test counts are lower than the previous #1443-based revision (82 / 31) because #2050 drops #1443's LedgerStateKey and migration-v3 tests along with the code they covered.

  • Additional tests are provided (if possible)

🔱 Fork Strategy

  • Node Runtime Update
  • Node Client Update
  • Other:
  • N/A

#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 prior
perfnet work: host-function changes cannot be A/B'd on a single chain.

Links

🤖 Generated with Claude Code

mpskowron and others added 30 commits February 23, 2026 12:55
   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>
ozgb and others added 20 commits July 29, 2026 13:48
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>
@chrispalaskas chrispalaskas added skip-changes-check-issue bot:ai-assisted Authored or substantially edited by an AI agent labels Aug 18, 2026
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>
Two pre-existing formatting diffs in ledger/src/versions/common/mod.rs
from the #1872 batch-verification code; unrelated to the #2050 swap but
they fail CI's fmt check.

Assisted-by: Claude:claude-opus-5
Signed-off-by: chrispalaskas <chris.palaskas@gmail.com>
@chrispalaskas chrispalaskas changed the title experiment: combine batch proof verification (#1872), tx revalidation cache (#744) and intermediate ledger-state fix (#1443) experiment: combine batch proof verification (#1872), tx revalidation cache (#744) and live intra-block ledger states (#2050) Sep 1, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bot:ai-assisted Authored or substantially edited by an AI agent skip-changes-check-issue

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants