Requiring a post-quantum signature

Two ways to make moving funds require a NIST post-quantum signature, native type 0x50 accounts and contract accounts, and the limit of what each protects.

Docs / Post-quantum / Requiring a post-quantum signature

How to make moving funds on chain 2800 require a NIST post-quantum signature, and the limit of what each way protects.

The point to get right first

Calling a post-quantum verifier does not, by itself, protect anything. If a transaction is authorised with an ECDSA signature and calls a Falcon or ML-DSA verifier inside, an adversary with a quantum computer forges the outer ECDSA signature and never touches the verifier. An account is protected only when the only way to move its funds is a valid post-quantum signature. Chain 2800 offers two ways to build that.

Way one: a native post-quantum account (type 0x50)

Since block 19,900,000 (2026-09-23) chain 2800 accepts an EIP-2718 transaction of type 0x50 whose authorisation is a FIPS 204 ML-DSA signature instead of a secp256k1 one (AIP-20). No secp256k1 operation appears anywhere in its validation, and its sender is an address derived from the post-quantum public key, so no key that a quantum computer could recover ever controls it. The public testnet 28001 has accepted the type since block 2,212,000.

alg_idSchemePublic keySignatureVerify gas
0x01ML-DSA-441,312 bytes2,420 bytes55,000
0x02ML-DSA-651,952 bytes3,309 bytes75,000
0x03ML-DSA-872,592 bytes4,627 bytes100,000

The three rules a client must get exactly right:

sender       = keccak256(0x50 || alg_id || public_key)[12:32]
signing_hash = keccak256(0x50 || rlp([chain_id, nonce, max_priority_fee_per_gas, max_fee_per_gas,
                                       gas_limit, to, value, data, access_list, alg_id, public_key]))
signature    = ML-DSA.Sign(secret_key, signing_hash, ctx = "")      # FIPS 204 pure interface, empty context
raw          = 0x50 || rlp([...the eleven fields..., signature])

Gas starts at 21,000 + 16 * (len(public_key) + len(signature)) + verify gas, plus the usual calldata and access-list costs. Nonce, balance and fee rules are those of type 0x02, and contract creation works as usual.

Check that it is live, without trusting this page

The first type-0x50 transaction on chain 2800, sent once on 2026-09-23 after the activation height:

curl -s -X POST -H 'content-type: application/json' \
  -d '{"jsonrpc":"2.0","id":1,"method":"eth_getTransactionReceipt","params":["0x7cb5bdaceb475a6fee881c6a68aadf69734e78c1fd5648865c4fdde243d602e5"]}' \
  https://rpc.aere.network

The receipt says "type":"0x50", "status":"0x1", block 19,900,028 (0x12fa67c), 180,176 gas (0x2bfd0), read on 2026-10-15. eth_getTransactionByHash returns the transaction with algId, pqPublicKey and pqSignature, and no v, r, s.

Build one yourself

The reference tooling of AIP-20 is published beside the other tools. It is plain JavaScript on @noble/post-quantum and needs nothing of ours:

curl -O https://aere.network/tools/pqtx.mjs -O https://aere.network/tools/package.json && npm install
node pqtx.mjs keygen --alg 2 > key.json          # ML-DSA-65 key and its derived address
node pqtx.mjs sign --key-file key.json --chain 28001 --nonce 0 --to 0x... --value 0
node pqtx.mjs vectors --out ./vectors             # the conformance vectors, valid and invalid

sign prints the raw transaction, its hash and the derived sender; send with --rpc <url> posts it. The secret key is read from the key file, so it appears on no command line. --corrupt-signature changes one bit of the signature and nothing else: both clients must refuse that transaction for its signature, which is how you check a node rather than believe it. Try it on the testnet first: chain 28001 has a faucet.

What it costs you

  • Funding. A type-0x50 account pays its own gas, so its address must hold AERE. On chain 2800 AERE cannot be

obtained before it is listed, so today mainnet use is limited to existing holders; the testnet is open.

  • Key reuse. A key that authorises a type-0x50 account must not also be used by a signer that signs

caller-supplied 32-byte values with the empty context, or that signer can be made to sign a transaction.

  • Wallets. Mainstream wallets do not build this type yet. Until they do, signing happens in tooling like the

above or in your own backend.

Way two: a contract account that checks the signature itself

A contract can hold funds and release them only on a valid post-quantum signature, verified on chain with the precompiles: Falcon-512 at 0x0AE1, ML-DSA-44 at 0x0AE3 (see The five precompiles and Falcon input framing). The transaction that carries the request is an ordinary ECDSA transaction, usually sent by a relayer or bundler, but its signature only pays the gas: the contract moves nothing unless the post-quantum signature over the request verifies. That is real protection of the funds, at the cost of a relayer and an extra contract call.

Two things to design in:

  • Bind the request. The signed message must include the chain id, the contract address, a nonce and every

parameter of the action, or a valid signature can be replayed or reused for another action.

  • No classical back door. An owner key, an upgrade path or a recovery route that accepts an ECDSA signature

undoes the whole point. Read the deployed bytecode for one before trusting an account built this way.

Which to choose

Native type 0x50Contract account
Pays its own gasyesno, a relayer does
Needs a relayer or bundlernoyes
Signature schemesML-DSA-44, -65, -87any the precompiles verify (Falcon, ML-DSA-44, SLH-DSA)
Custom rules (limits, multisig, recovery)noyes, in the contract
Where it is definedthe protocol (AIP-20)your contract

Neither makes the chain's own block production post-quantum: that is the anchor's job, described in The certificate anchor.