Skip to main content

Stylus ML-DSA-65 verifier

MLDSAVerifier is a stateless Arbitrum Stylus contract that verifies an ML-DSA-65 (FIPS 204) signature given a public key, a message hash, and a signature, returning true or false.

Key numbers

MetricValue
Verification gas~374,000
WASM size (raw)~20.7 KB
WASM size (gzipped)~7.8 KB
Stylus compressed size limit24 KB
Public key1,952 bytes
Signature3,309 bytes
Message32 bytes (keccak256 hash)

Position in the stack

flowchart LR
A[Smart Account] -->|validateUserOp| B[PQValidatorModule<br/>Solidity]
B -->|verify<br/>pubKey, msgHash, sig| C[MLDSAVerifier<br/>Stylus/WASM]
C -->|bool| B
B -->|VALIDATION_SUCCESS<br/>or VALIDATION_FAILED| A

The contract is stateless — public keys are stored in the Solidity module and passed as calldata on every call.

Verifying against the deployed contract

# Generate ML-DSA test vectors into your shell env
eval $(cargo run --bin gen_test_data --manifest-path ml-dsa-test/Cargo.toml 2>/dev/null)

# Sanity check
echo "${PK_HEX:0:20}..."

# Verify against the deployed contract
cast call $STYLUS_VERIFIER "verify(bytes,bytes32,bytes)(bool)" \
$PK_HEX $MSG_HASH $SIG_HEX --rpc-url http://127.0.0.1:8547

# Estimate verification gas
cast estimate $STYLUS_VERIFIER "verify(bytes,bytes32,bytes)" \
$PK_HEX $MSG_HASH $SIG_HEX --rpc-url http://127.0.0.1:8547

Tests

cargo test --package pq-validator

Four test cases using stylus_test::TestVM:

  • Valid signature → returns true.
  • Flipped signature byte → returns false.
  • Wrong-length public key → reverts with InvalidPublicKey.
  • Cross-implementation: a JavaScript signature from @noble/post-quantum verified by the Rust ml-dsa crate, asserting that the off-chain signer and on-chain verifier agree byte-for-byte.

Production limitations

LimitationDetail
Reactivation requiredAuto-deactivates after 365 days or ArbOS upgrades. No built-in monitoring.
SDK maturitystylus-sdk-rs is pre-1.0. See Stylus maturity assessment.
WASM size headroom~3.3 KB before hitting the 24 KB compressed limit.
No tracingcast run returns OpcodeNotFound. Use cast call for on-chain simulation.
ml-dsa crate is RCv0.1.0-rc.7 is not yet stable. Pin the version and re-test before upgrading.