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_id | Scheme | Public key | Signature | Verify gas |
|---|---|---|---|---|
0x01 | ML-DSA-44 | 1,312 bytes | 2,420 bytes | 55,000 |
0x02 | ML-DSA-65 | 1,952 bytes | 3,309 bytes | 75,000 |
0x03 | ML-DSA-87 | 2,592 bytes | 4,627 bytes | 100,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-
0x50account 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-
0x50account 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 0x50 | Contract account | |
|---|---|---|
| Pays its own gas | yes | no, a relayer does |
| Needs a relayer or bundler | no | yes |
| Signature schemes | ML-DSA-44, -65, -87 | any the precompiles verify (Falcon, ML-DSA-44, SLH-DSA) |
| Custom rules (limits, multisig, recovery) | no | yes, in the contract |
| Where it is defined | the 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.