Skip to content

LocalTransactionProver proves precompile claims locally (prove_sync) instead of deferring them (prove_partial_sync) #3835

Description

@WiktorStarczewski

LocalTransactionProver::prove calls miden_prover::prove_sync, the final-proof API, so every local prove also generates the precompile-VM STARK for its deferred claims. Precompile proving is meant to happen at batch level; the local prover should call prove_partial_sync and ship the claims as a wire.

Symptom. A locally proven transaction whose auth verifies ECDSA signatures carries DeferredProof::Stark (~277 KB) next to the ~100 KB VM STARK, and that precompile STARK is 55-72% of local prove time.

Repro. Prove any ECDSA-authenticated transaction with LocalTransactionProver::default() and match on proven.proof().deferred_proof(): it is Stark { .. }. Cause: crates/miden-tx/src/prover/mod.rs:17 imports prove_sync and :148 calls it (same on next and 0.16.0-rc.9). prove_partial_sync is the documented API for claims "proved later by a delegated or batching prover" (miden-prover 0.29.4, src/lib.rs:164).

Expected. DeferredProof::Wire; the claims are checked natively by the verifier and proven at batch level. On next the verifier side already exists: TransactionVerifier handles PrecompileStatus::Deferred via validate_deferred_witness (crates/miden-protocol/src/transaction/verifier.rs:76-95), so there the fix is the prover switch plus tests.

Impact. Browser local proving (MT wasm, 6 threads) is 3.5x slower than necessary for guarded accounts and 2.8x for single-sig: with the one-line switch, guarded 7.99 s to 2.27 s and single-sig 4.58 s to 1.66 s; native guarded 1.68 s to 0.63 s. The proven transaction shrinks from 391 KB to 104 KB.

On 0.16.0-rc.9 (VM 0.29.4) the switch alone is not deployable: miden_verifier::verify returns UnsupportedDeferredProof for Wire, and both the node RPC (crates/rpc/src/server/api/submit_proven_tx.rs:108) and the validator (bin/validator/src/tx_validation/mod.rs:52) call TransactionVerifier::verify, so a client cannot opt in. Fixing 0.16 needs TransactionVerifier to accept partial proofs (Verifier::verify_partial exists in 0.29.4; rehydrating the wire re-evaluates every node under the registry, so native settlement is a root check) plus the node picking it up.

Evidence: runtime probes (0.16.0-rc.9 protocol crates, VM 0.29.4, web-sdk 0.16.0-rc.7)

Native, LocalTransactionProver::default().prove(executed), printing proven.proof().deferred_proof():

# 2 ECDSA claims (guarded multisig: 1 approver at threshold 1 + guardian)
tx: total_cycles=29095  auth_procedure=19847
DEFERRED_PROOF  Stark { proof: 276768 bytes, public_root: Word([10652471043400787702, 12148757343284257120, 17083288024192604590, 7234156251587950621]) }
VM_PROOF        101663 bytes

# 1 ECDSA claim (BasicAuth)
tx: total_cycles=17720  auth_procedure=9865
DEFERRED_PROOF  Stark { proof: 283680 bytes, public_root: Word([13415207134405930995, 16485817212495615892, 12092707907113953811, 2871210035726898790]) }
VM_PROOF        99711 bytes

# same two shapes after switching miden-tx to prove_partial_sync
DEFERRED_PROOF  Wire (partial, claims NOT proven)
VM_PROOF        100447 bytes   # 2 claims
VM_PROOF        100895 bytes   # 1 claim

Browser, client.proveTransaction(txResult, TransactionProver.newLocalProver()), proven.serialize().length:

single-sig  provenTxBytes 378589
guarded     provenTxBytes 391045
guarded, prove_partial_sync build   provenTxBytes 104118
Evidence: code pointers
  • crates/miden-tx/src/prover/mod.rs:17 use miden_prover::{ExecutionProof, Word, prove_sync}; and :148 prove_sync( (next at b5cbb34; identical in 0.16.0-rc.9).
  • miden-prover 0.29.4 src/lib.rs:123 prove_sync (final), :164 prove_partial_sync, :235-241 prove_deferred_state inside the precompile_vm span, :248-258 the wire variant.
  • miden-core 0.29.4 src/proof.rs:261-275 DeferredProof::{Empty, Wire, Stark}.
  • miden-verifier 0.29.4 src/lib.rs:177-188 resolve_final_deferred_root: Wire yields UnsupportedDeferredProof; src/lib.rs:123-135 verify_partial returns Unsettled(DeferredState).
  • miden-protocol 0.16.0-rc.9 src/transaction/verifier.rs:33 calls miden_verifier::verify (full).
  • node next: crates/rpc/src/server/api/submit_proven_tx.rs:108 and bin/validator/src/tx_validation/mod.rs:52 call TransactionVerifier::new(MIN_PROOF_SECURITY_LEVEL).verify.
  • web-sdk crates/web-client/src/new_transactions.rs:713 LocalTransactionProver::default() for newLocalProver().
Measurements (M5, 4P+6E; native concurrent; browser MT wasm at 6 threads, 9 timed proves per run after a discarded warm-up, medians)
shape prover native prove browser prove proven tx
single-sig ECDSA prove_sync (today) 1376 ms 4584 ms 379 KB
single-sig ECDSA prove_partial_sync 602 ms 1663 ms ~104 KB
guarded (2 ECDSA claims) prove_sync (today) 1678 ms 7987 ms 391 KB
guarded (2 ECDSA claims) prove_partial_sync 630 ms 2272 ms 104 KB

The native precompile_vm span accounts for the whole difference (744 ms and 998 ms respectively; 0 ms with the partial API). The guarded premium over single-sig drops from 302 ms to 28 ms, since only the main trace remains and both shapes pad to 2^15 rows.

The one-line switch used for the "partial" rows, applied to 0.16.0-rc.9 and reverted afterwards:

-use miden_prover::{ExecutionProof, Word, prove_sync};
+use miden_prover::{ExecutionProof, Word, prove_partial_sync};
...
-        let (stack_outputs, proof) = prove_sync(
+        let (stack_outputs, proof) = prove_partial_sync(

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Labels

No labels
No labels

Type

Fields

Priority

None yet

Projects

No projects

    Milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions