Status: Active development. End-to-end verify() POC + full gas harness. Foundry suite green.
Latest gas (snapshot): test_verify_gas_poc = 68,901,612 gas (logged verify POC ≈ 68,158,524).
PreA milestone: packed A_ntt calldata microbench for compute_w ≈ 1,499,354 gas (rho0/rho1).
Phase12: ERC-7913 adapters + verifyWithPackedA(...) path (calldata packedA_ntt) + tests & gas microbenches.
This repo is research / standardization work. Not audited. Do not use in production.
- Overview
- What's implemented
- Gas benchmarks
- Benchmark datasets (gas-per-secure-bit)
- Standardization track
- Milestones (Phases)
- Build & test
- Repo layout
- Design notes
- Roadmap
- Competitors / related PQ/EVM work
- Security
- License
This repository implements an ML-DSA-65 (FIPS-204 shape) verification pipeline in Solidity, targeting:
- A clean, auditable on-chain verifier baseline (research + standardization)
- ERC-7913-style signature verifier adapters (wallets, AA, sequencers, rollups)
- Reproducible KAT-style tests and deterministic gas snapshots
- A realistic path to reduce the dominant cost: w = A · z − c · t1
Standards / context
- FIPS-204 (ML-DSA): https://csrc.nist.gov/pubs/fips/204/final
- ERC-7913 (Signature Verifiers): https://eips.ethereum.org/EIPS/eip-7913
- EIP-8051 (ML-DSA verification discussion): https://ethereum-magicians.org/t/eip-8051-ml-dsa-verification/25857
- EIP-8052 (Falcon precompile): https://eips.ethereum.org/EIPS/eip-8052
- Keccak/SHAKE backend (vendored) + thin ML-DSA XOF wrapper
- NTT/INTT for ML-DSA modulus/degree:
- q = 8,380,417
- n = 256
- Poly / PolyVec / Hint scaffolding for ML-DSA-65 shape:
- k = 6 (t1)
- ℓ = 5 (z, h)
- FIPS-shaped decode for pubkey/signature (t1 / z / c) + KAT coverage
- Matrix-vector core: w = A · z − c · t1
MLDSA65_Verifier_v2end-to-end verify() POC (decode + checks + compute_w)
MLDSA65_ERC7913Verifieradapter implementing ERC-7913-style verificationMLDSA65_ERC7913BoundCommitA(CommitA binding flow) to prevent matrix substitution when using precomputed A- Phase12 fast-path:
verifyWithPackedA(...)accepts calldata packedA_ntt- Adapter tests + bound-commit tests
- Gas microbench harness
PreA isolates the hot loop and shows what's achievable when A_ntt is supplied efficiently:
- calldata-friendly packed
A_nttformat (loader) - microbench: compute
wfrom packedA_nttin isolation - Commitment binding helper (CommitA) to prevent swapping
A
This repo defines the PreA ABI convention for supplying a precomputed packedA_ntt matrix (NTT-domain)
to the verifier fast-path, and provides an on-chain runner to reproduce the ~1.50M gas compute_w cost.
- PreA (packedA_ntt) convention:
docs/preA_packedA_ntt.md - On-chain proof runner:
script/RunPreAOnChain.s.sol
# terminal 1
anvil
# terminal 2
export RPC_URL=http://127.0.0.1:8545
export PK=<YOUR_ANVIL_PRIVATE_KEY>
forge script script/RunPreAOnChain.s.sol:RunPreAOnChain \
--rpc-url $RPC_URL \
--private-key $PK \
--broadcast -vvgas_compute_w_fromPacked_A_ntt(rho0) 1499354
gas_compute_w_fromPacked_A_ntt(rho1) 1499354
All numbers are from Foundry tests + .gas-snapshot.
| Component | Gas | Where |
|---|---|---|
| verify() POC (snapshot) | 68,901,612 | MLDSA_VerifyGas_Test:test_verify_gas_poc() |
| verify() POC (log) | ~68,158,524 | same test logs |
| compute_w breakdown | see logs | MLDSA_VerifyGasBreakdown_Test |
PreA compute_w_fromPacked_A_ntt |
1,499,354 | PreA_ComputeW_GasMicro_Test |
ERC-7913 verifyWithPackedA (micro) |
~71,796 | MLDSA_ERC7913_PackedA_GasMicro.t.sol |
Key point: end-to-end verifier is dominated by compute_w (matrix-vector core).
PreA demonstrates what the hot loop can look like when A_ntt is supplied efficiently.
This repo is primarily about a clean, FIPS-204-shaped ML-DSA-65 verifier in Solidity (correctness/KATs/structure + gas engineering).
For cross-scheme comparisons and normalized reporting (e.g. gas per effective security bit under explicit assumptions), see:
It contains reproducible datasets (data/results.csv / .jsonl), runners, and provenance tracking (repo, commit, bench_name, chain_profile).
Current default normalization uses a proxy security metric:
gas_per_secure_bit = gas_verify / lambda_eff
(where lambda_eff is recorded explicitly and may evolve into other metric types, including VRF/min-entropy under stated threat models).
Note: vendor implementations remain under upstream licenses; this repo focuses on benchmark artifacts + provenance.
For normalized cross-scheme comparisons and interoperability vectors (AA/UserOp JSON schema), see:
- https://github.com/pipavlo82/gas-per-secure-bit (datasets + runners +
spec/pqsig_userop_schema_v0.1.*)
This repository is structured as a standardization-friendly reference implementation for PQ signature verification on EVM:
We treat ERC-7913 as the canonical wallet / AA / rollup-facing interface for PQ verifiers.
- Spec: https://eips.ethereum.org/EIPS/eip-7913
- Adapter(s):
contracts/verifier/MLDSA65_ERC7913Verifier.sol,contracts/verifier/MLDSA65_ERC7913BoundCommitA.sol
Goal: a verifier that can be plugged into tooling in the same way OpenZeppelin patterns are reused for classical primitives.
We are converging on stable calldata layouts for:
pubkey/signature(FIPS-204 shape)- optional precomputed inputs (e.g.,
packedA_ntt) for performance paths
This enables apples-to-apples gas comparisons across ML-DSA / Dilithium / Falcon, and makes it feasible to build shared tooling.
Vectors are tracked in a KAT-like JSON format to support:
- deterministic decoding tests
- cross-implementation conformance checks
- reproducible CI for correctness
Gas is measured via:
- dedicated gas harness tests
.gas-snapshotsnapshots- logged per-component breakdown (decode vs
compute_w)
This ensures changes are measurable and comparable over time.
The long-term intent is a shared "PQ verifier kit" mindset:
- same high-level interface (ERC-7913)
- compatible vector formats and harnesses
- comparable performance datasets (gas / calldata size / constraints)
Related discussion (ecosystem context):
- EIP-8051 (ML-DSA verification): https://ethereum-magicians.org/t/eip-8051-ml-dsa-verification/25857
- EIP-8052 (Falcon precompile): https://eips.ethereum.org/EIPS/eip-8052
- EthResearch thread: https://ethresear.ch/t/the-road-to-post-quantum-ethereum-transaction-is-paved-with-account-abstraction-aa/21783
- OpenZeppelin interfaces docs (ERC-1271 / interface patterns): https://docs.openzeppelin.com/contracts/5.x/api/interfaces
Surfaces / interfaces: ERC-7913 adapters are the app-facing surface for ML-DSA-65 verification on EVM. EIP-7932 is a candidate protocol-facing surface (precompile-style); the goal is compatible ABI shapes and a shared JSON KAT schema across both surfaces.
This repo has been developed as an iterative set of "phases" with reproducible tests + gas snapshots.
| Phase | Branch / PR | What landed | Tests | Key gas numbers |
|---|---|---|---|---|
| Phase 5 | feature/mldsa-ntt-opt (baseline snapshot commit be08d42) |
Baseline verify() POC with NTT assembly path; established .gas-snapshot discipline |
green | test_verify_gas_poc ≈ 81,630,615 |
| Phase 7 | feature/mldsa-ntt-opt-phase7 → wired back into feature/mldsa-ntt-opt |
_compute_w hot-loop optimization via inline assembly pointer-walk; fixed memory layout bug (row pointers) |
72/72 green (then higher as suite grew) | test_verify_gas_poc ≈ 80,000,775; matrixvec cores logged |
| Phase 10 | feature/mldsa-ntt-opt-phase11-preA (history includes Phase10 commits) |
Switched critical NTT mul to native EVM mulmod (dropped Barrett attempt); large gas win |
green | verify() POC dropped materially vs ~80M-era baseline |
| Phase 11 (PreA) | feature/mldsa-ntt-opt-phase11-preA |
Introduced packed A_ntt calldata format + PreA compute_w microbench; ExpandA gas micro harness; removed dump-only test |
85/85 (later 86/86) green | test_verify_gas_poc ≈ 68,901,612 (log ≈ 68,158,524) ; PreA compute_w_fromPacked_A_ntt ≈ 1,499,354 |
| Phase 11 (CommitA) | feature/mldsa-ntt-opt-phase11-preA |
Added CommitA binding flow to prevent matrix substitution when accepting precomputed A | green | binding overhead measured via micro benches |
| Phase 12 (PackedA + ERC-7913) | feature/mldsa-ntt-opt-phase12-erc7913-packedA / PR #27 |
Added verifyWithPackedA(...) fast-path (calldata packedA_ntt) in ERC-7913 adapters, tests + gas microbenches |
89/89 green | adapter microbench ~71,796 gas (ok), ~71,691 (mismatch); PreA microbench unchanged |
Notes:
- All gas numbers are sourced from Foundry logs and/or
.gas-snapshotin the corresponding phase. - "verify() POC" is a research baseline that is still dominated by
compute_w = A·z − c·t1; PreA isolates the hot loop to track achievable improvements whenA_nttis supplied efficiently.
forge buildforge test -vv# verify() POC gas
forge test --match-test test_verify_gas_poc -vv
# breakdown (decode + compute_w)
forge test --match-contract MLDSA_VerifyGasBreakdown_Test -vv
# PreA packed A_ntt microbench
forge test --match-contract PreA_ComputeW_GasMicro_Test -vv
# MatrixVec correctness + gas
forge test --match-contract MLDSA_MatrixVec_Test -vv
forge test --match-contract MLDSA_MatrixVecGas_Test -vv
# ERC-7913 packedA adapter tests
forge test --match-contract MLDSA_ERC7913_PackedA_Test -vv
# Update .gas-snapshot
forge snapshotTypical structure (names may evolve, but intent stays stable):
contracts/verifier/—MLDSA65_Verifier_v2.sol, ERC-7913 adapters, bound-commit flowntt/— NTT/INTT + zetas tables (ML-DSA q = 8,380,417)- vendored SHAKE/Keccak backend
poly/— Poly / PolyVec / Hint helpers (FIPS shape)
test/- decode tests + JSON KAT tests
- gas harnesses (verify POC, breakdown, matrixvec gas, PreA micro)
- ERC-7913 adapter tests
test_vectors/- JSON KAT-style vectors (pubkey / signature / message-hash)
.gas-snapshotfoundry.tomlREADME.md,PQ-NOTES.MD
ERC-7913 generalizes signature verification beyond "address-only" signatures (EOA / ERC-1271), which is important for PQ keys.
OpenZeppelin interface docs mention the expected magic values:
-
valid:
IERC7913SignatureVerifier.verify.selector -
invalid:
0xffffffffor revert -
OZ interfaces docs: https://docs.openzeppelin.com/contracts/5.x/api/interfaces
If we accept a precomputed A_ntt from calldata, we must prevent an attacker from swapping the matrix.
CommitA binding is a lightweight "bind A to rho / context" mechanism for the fast-path.
Short-term (next practical steps):
-
Wire PreA fast-path into
MLDSA65_Verifier_v2- guarded by CommitA binding
- keep legacy path for reference correctness
-
Tighten end-to-end FIPS-204 conformance
- challenge derivation, sampling details, KAT equality checks
- keep the repo FIPS-shaped and test-vector-driven
-
Push down
compute_wgas- inner-loop reductions (fewer loads/stores)
- unrolling / batching
- minimize memory roundtrips in NTT domain
-
Standardization packaging
- ERC-7913 "drop-in verifier" shape
- canonical JSON KAT pipeline
- reproducible benches + "gas per secure bit" methodology (separate workstream)
- ZKNoxHQ — ETHDILITHIUM: https://github.com/ZKNoxHQ/ETHDILITHIUM
- ZKNoxHQ — ETHFALCON: https://github.com/ZKNoxHQ/ETHFALCON
- Falcon + AA discussion (FalconSimpleWallet demo mentioned):
https://ethresear.ch/t/the-road-to-post-quantum-ethereum-transaction-is-paved-with-account-abstraction-aa/21783
- ERC-7913: https://eips.ethereum.org/EIPS/eip-7913
- EIP-8051 discussion: https://ethereum-magicians.org/t/eip-8051-ml-dsa-verification/25857
- EIP-8052: https://eips.ethereum.org/EIPS/eip-8052
- Not audited. Research-quality code.
- Main risk surfaces:
- calldata-supplied
A_nttpath (must be bound/committed) - decode correctness (FIPS packing/unpacking)
- domain separation (hash/XOF wiring)
- calldata-supplied
- This repo is designed to be test-vector-first and gas-bench reproducible.
MIT (see LICENSE).
Vendored Keccak/SHAKE files retain their original headers and license terms.