Skip to content

VERIFY_MLDSA65

Frost post-quantum Signature verification

Verify an ML-DSA-65 (FIPS 204) post-quantum signature.

Address0x0000000000000000000000000000000000000014
NameVERIFY_MLDSA65
Minimum gas16,000
Input5293 bytes (exact)
Output32 bytes, always
Available sinceOsaka (Frost)

Input

msg32 B
signature3309 B
publicKey1952 B
OffsetLengthFieldDescription
032msgThe message. For a frame transaction this is the 32-byte sig-hash.
323309signatureStandard FIPS 204 ML-DSA-65 signature encoding.
33411952publicKeyStandard FIPS 204 ML-DSA-65 public key encoding.

Output

A 32-byte word: 1 if the signature is valid, 0 otherwise.

Gas

16,000 (flat, regardless of input)

Priced at ecrecover throughput parity on validator-class hardware: gas = t_verify × (EcrecoverGas / t_ecrecover). Measured 177.7 µs per verify against ecrecover at 33.7 µs / 3000 gas (≈89 Mgas/s) on AMD EPYC Milan, giving parity at 15.9k, rounded up to 16,000 for margin. CONSENSUS-CRITICAL: changing this price requires a fresh chain.

Failure behaviour

  • Returns the 32-byte zero word — never empty data, and never an error — when the signature is invalid, when the public key is not canonically encoded, or when the input length is not exactly the expected size.
  • Input is not right-padded. A short or long input is a deterministic reject, not a truncation.
  • Gas is charged in full on every call, valid or not.

Notes

  • Verification is pure ML-DSA with an empty context string, per FIPS 204 — not the pre-hash variant. Any conformant signer interoperates.
  • Apple CryptoKit on iOS/macOS supports ML-DSA-65 and ML-DSA-87, so a key generated in a Secure Enclave can sign a Frost transaction directly. This is the parameter set the rotatable v2 account uses for exactly that reason.
  • Soundness rests on the caller binding the exact public key bytes. Frost account contracts embed the key in their own code or behind a storage pointer, so the caller cannot be tricked into verifying against an attacker-supplied key.
  • Conformance is checked against NIST ACVP signature-verification vectors, differentially against an independent Rust implementation, and against golden vectors produced by a real Apple Secure Enclave key.

Worked example

Generate a signature with any FIPS 204 library and check it against the live chain. This uses dilithium-py, an implementation unrelated to the one the chain runs — if it interoperates, so will yours.

from dilithium_py.ml_dsa import ML_DSA_65
msg = bytes.fromhex("11" * 32) # 32 bytes
pk, sk = ML_DSA_65.keygen() # pk 1952 B
sig = ML_DSA_65.sign(sk, msg) # sig 3309 B
payload = (msg + sig + pk).hex() # 5293 bytes total
print(len(msg) + len(sig) + len(pk)) # 5293

Call the precompile with that payload via eth_call to 0x…0014. A valid signature returns 0x00…01; flip a single bit anywhere in the signature and it returns 0x00…00. Truncate the input and it still returns 0x00…00 — never empty.

Calling it

Precompiles have no ABI and no function selector: the input is raw bytes at the exact offsets above. Call with staticcall.

// VERIFY_MLDSA65 at 0x14
(bool ok, bytes memory out) = address(0x14).staticcall(input);
require(ok, "VERIFY_MLDSA65 reverted");