pqc-migration-toolkit/README.md

347 lines
18 KiB
Markdown

# Aere Quantum Migration Toolkit
A starting toolkit to assess and migrate EVM accounts toward post-quantum
readiness on Aere Network (chain 2800), or on any EVM chain reachable by RPC.
Aere Network is the post-quantum EVM L1: it ships five live NIST post-quantum
verification precompiles on mainnet (Falcon-512, Falcon-1024, ML-DSA-44,
SLH-DSA-128s, SHAKE256, at `0x0AE1..0x0AE5`, activated at block 9,189,161), plus
a live Falcon-512-owned smart account and factory. That makes Aere the natural
home for tooling that helps others measure their quantum exposure and move to a
post-quantum account. This toolkit extends Aere's internal account-migration
work outward into something anyone can run.
It is a real product with one unified CLI and zero runtime dependencies (pure
Node built-ins, Node 18+):
1. `scan.js` / `aere-pqc scan`, a real PQC-readiness scanner (EOA vs contract,
and which signature primitives a contract uses).
2. `lib/migrator.js` / `aere-pqc build-migration`, the transaction-CONSTRUCTION
SDK: given a classical ECDSA EOA and a target Falcon-512 public key it derives
the counterfactual post-quantum account, enumerates the assets to move
(read-only), and emits the exact sequence of UNSIGNED transactions a holder
would sign. It never signs, deploys, or moves funds.
3. `lib/readiness.js` / `aere-pqc readiness` + `simulate.js` / `aere-pqc
simulate`, the quantum-readiness cost simulator: one-time migration gas plus
ongoing per-authorization cost, from the measured PQC verify gas.
`aere-pqc` is the single entry point that wraps `scan`, `simulate`,
`build-migration`, and `readiness`. The lower-level `scan.js`, `plan.js`,
`simulate.js` CLIs remain for direct use.
## The scope boundary (this never moves)
Migrating an account's authentication to a post-quantum scheme changes how that
account authorizes. It does NOT make Aere consensus post-quantum. Mainnet 2800
still seals blocks with classical secp256k1 QBFT, and the on-chain ZK verifiers
(BN254 Groth16) are classical. Nothing in this toolkit claims otherwise, and the
scanner and simulator both restate the boundary in their output.
## Honest status
This is a starting toolkit, not a finished product. What is real and verified is
listed under "What actually runs" below. What is heuristic, out of scope, or not
yet deployed is flagged inline with `[VERIFY]` and `[MEASURE]`, in the code and
in the reports the tools print.
## 1. The readiness scanner: `scan.js`
Given an EVM address, the scanner reports quantum exposure as a colour:
- **RED**, classical-only. An EOA (secp256k1 ECDSA authenticated, so
Shor-breakable and unable to hold verification code), or a contract with no
detectable post-quantum precompile usage.
- **YELLOW**, hybrid. A contract that uses both a classical signature primitive
(ecrecover at `0x01`, or P-256 at `0x100`) and Aere's live PQC precompile band.
- **GREEN**, PQC-capable. A contract that uses Aere's live native PQC precompiles
(`0x0AE1..0x0AE5`).
```bash
# live scan over JSON-RPC (this is the only part that needs network access)
node scan.js 0x465d9E3b476BF98Aa1393079e240Db5D2a9bEA6A --rpc https://rpc.aere.network
# offline: scan a bundled real-bytecode fixture
node scan.js --fixture AerePQCAttestation
# offline: scan bytecode you already have
node scan.js 0xYourContract --bytecode 0x60806040...
# machine-readable
node scan.js 0x465d... --rpc https://rpc.aere.network --json
```
### How it detects, and why it is honest
The scanner does not grep the hex. A naive search for `610ae1` (PUSH2 0x0AE1)
false-positives, because those bytes can sit inside another instruction's push
data. Two techniques make the detection real:
1. **Opcode-aware decoding.** The bytecode is walked as an instruction stream,
skipping each PUSH's immediate bytes, so a matched constant is a genuine push.
2. **Local stack simulation to resolve CALL targets.** For every
CALL/STATICCALL/DELEGATECALL/CALLCODE the scanner recovers the address
argument (the second stack item) by simulating the stack per basic block. That
distinguishes an actual call to `0x0AE1` from the bare number `0x0AE5` used as
a memory length near an unrelated staticcall, which is precisely the false
positive a proximity heuristic hits. (An earlier proximity-only version of
this tool misread `AereHybridAuth` as hybrid for exactly that reason; the
stack-simulation version reads it correctly.)
The signature-verify addresses `0x0AE1..0x0AE4` (2785..2788) are not round
numbers, so a genuine opcode-aligned push of one is a medium-confidence signal
even when the call target is staged through memory and cannot be resolved
locally. The common literals `0x01`, `0x100`, and `0x0AE5` are never used as a
fallback signal; they count only when stack simulation resolves them as an actual
call target.
### Scanner limitations (read these)
- Bytecode scanning only sees NATIVE precompile calls. A contract that verifies
Falcon through the SOLIDITY `AereFalcon512Verifier` (`0x4E8e...D8fFC`, the path
the deployed `AerePQCAccount` and `AereHybridAuth` actually take) delegates to
a normal contract address, so it reads as RED here even though it is
PQC-capable. The bundled `AereHybridAuth` fixture demonstrates this on purpose.
Confirm an auth path against source when the scanner says RED.
- `ecrecover` (`0x01`) detection is high confidence only because stack simulation
resolves the call target to the exact address `1`. A contract that never calls
a signature precompile (authorization is by owner or role) also reads RED,
which is correct: it has no quantum-vulnerable signature surface to migrate.
- The scanner classifies signature-verification exposure. It does not audit key
management, upgradeability, or business logic.
## 2. The migration SDK: `lib/migrator.js` (transaction construction)
Given a classical ECDSA EOA and a target NIST Falcon-512 public key, this module
builds the exact sequence of UNSIGNED transactions a holder would sign to move
their assets onto a post-quantum `AerePQCAccount`. It is the concrete,
signable-artifact layer on top of the older `lib/migrate.js` plan builder.
```bash
# demo (uses the live sample account's real Falcon key; demo EOA 0x..dEaD)
node aere-pqc.js build-migration
# real build: derive the target, enumerate a real EOA's assets read-only, and
# attach a live gas estimate to each unsigned tx
node aere-pqc.js build-migration --eoa 0xYourEoa --falcon 0x09... \
--rpc https://rpc.aere.network --native 1000000000000000 --estimate
# atomic sweep via the (audit-gated, not-yet-deployed) AereAccountMigrator
node aere-pqc.js build-migration --eoa 0xYourEoa --path migrator \
--migrator 0xVerifiedMigrator --tokens 0xTokenA,0xTokenB --amounts 1000,2000
# machine-readable
node aere-pqc.js build-migration --eoa 0xYourEoa --rpc https://rpc.aere.network --json
```
Key functions (`import ... from '@aere/pqc-migration-toolkit/migrator'`):
- `predictTargetAddress({ falconPubKey, salt })`: the counterfactual
`AerePQCAccount` address, pure CREATE2 math, no RPC. Mirrors
`AerePQCAccountFactory.predictAddress` and is verified in `selftest.js` against
the live on-chain sample account.
- `enumerateAssets({ rpcUrl, owner, tokens })`: READ-ONLY. `eth_getBalance` for
native AERE and `eth_call balanceOf(owner)` per ERC-20 candidate; returns which
assets have a non-zero balance and therefore need moving. Signs nothing.
- `buildMigrationTransactions({ path, eoa, falconPubKey, tokens, nativeWei, ... })`:
the core. Returns an array of UNSIGNED tx objects, each stamped `signed:false`,
shaped `{ to, from, value, data, chainId }` (nonce/gas left for the signer).
Two paths:
- `direct` (works today, no extra contract): per-asset `ERC-20 transfer` +
native send straight to the target account. Not atomic.
- `migrator` (atomic, all-or-nothing): `approve(migrator, amount)` per token
then one `AereAccountMigrator.migrateToPqcAccount(...)` that binds the
destination to the Falcon key on-chain. REQUIRES an explicit `--migrator`
address, because `AereAccountMigrator` is repo source in the audit-gated
`notDeployedHeld` set and has NO live mainnet address (see below).
- `estimateMigrationGas(plan, rpcUrl)`: attaches a read-only `eth_estimateGas`
result to each unsigned tx (or an error string when a step cannot be simulated
pre-deploy). Broadcasts nothing.
- `encodeMigrateCalldata`, `encodeMigrateToPqcAccountCalldata`,
`encodeApproveCalldata`, `encodeTransferCalldata`, `encodeCreateAccountCalldata`:
the individual calldata builders, each verified byte-for-byte against ethers v6
(see "What actually runs").
The calldata encoder (`lib/abi.js`) is a zero-dependency Solidity ABI encoder for
exactly the types the migrator needs (`uint256, address, bool, bytes, address[],
uint256[]`), validated against ethers 6.16.0.
### The migration paths
**PQC account (Falcon-512 owned).** Derive the counterfactual account
(step 1, off-chain), deploy it through the live factory with
`createAccount(falconPubKey, salt)` (step 2, permissionless and idempotent), then
move assets and roles off the old EOA into it (step 3). After the move, spending
authority is the Falcon-512 key only. There is no ECDSA fallback, so register the
key in `AerePQCKeyRegistry` (proof-of-possession) and set PQC social-recovery
guardians before moving material value.
**Hybrid account (ECDSA + Falcon-512).** Register a hybrid identity in
`AereHybridAuth` binding your existing classical address to a Falcon-512 key,
then authorize with both legs during the transition. An attacker must break both
secp256k1 AND Falcon-512.
**EIP-7702 delegation.** Aere supports EIP-7702, so an EOA can temporarily
delegate execution to account code to batch a migration move in a single EOA
transaction.
### Founder-gated caveat, and what works today
The `AerePQCAccountFactory` (`0xd5315Ea7...CE58`) is live, and `createAccount` is
permissionless, so deploying a PQC account and moving assets into it with the
`direct` path is possible TODAY with no special authority: the whole `direct`
sequence is plain factory + ERC-20 + native transactions the holder signs.
What is NOT deployed:
- `AereAccountMigrator` (the atomic `migrate` / `migrateToPqcAccount` conduit)
is repo source (`contracts/pqc/AereAccountMigrator.sol`) in the audit-gated
`notDeployedHeld.externalAuditGated_fundFlow` set. It has NO mainnet address.
The `migrator` path therefore refuses to build unless you pass an explicit
`--migrator` address, and it prints a `[VERIFY]` warning telling you to confirm
that address holds the exact audited bytecode before granting it any approval.
- `AereHybridAuth` is repo source with no mainnet address, so the hybrid path's
deploy step is a founder/deployer step, flagged `[VERIFY]`.
Deploying NEW contracts or a corrected V2 redeploy is signed by the
Foundation/deployer key, never by this toolkit.
## 3. The cost simulator: `aere-pqc readiness` and `simulate.js`
Two views on cost, both over the MEASURED PQC verify gas from
`aerenew/docs/AERE-BENCHMARK-REPORT.md`:
- `aere-pqc readiness <address> --rpc <url>` is the per-account report: it scans
the account, enumerates its movable assets read-only, and prints the one-time
migration gas (account deploy + asset moves) plus the ongoing
per-authorization cost. Asset-move gas is a clearly-labeled `[VERIFY]` planning
estimate anchored to the one asset-move figure measured live this session (a
native AERE send at 21,246 gas via `eth_estimateGas`); PQC auth gas is `[CITED]`.
- `simulate.js` / `aere-pqc simulate` is the fleet projection across many
accounts.
```bash
aere-pqc readiness 0xYourEoa --rpc https://rpc.aere.network --tokens 0xTokenA
node simulate.js --count 100 --scheme hybrid --auths 5
node simulate.js --accounts accounts.json --gwei 1
node simulate.js --schemes # list modeled schemes
```
Modeled per-authorization gas (native precompile verify-and-record receipt,
`[CITED: AERE-BENCHMARK-REPORT.md Part C.1]`):
| Scheme | Marginal verify gas | Verify-and-record tx gas |
|---|--:|--:|
| Falcon-512 | 40,000 | 86,336 |
| Hybrid (ECDSA + Falcon-512) | 43,000 | 89,336 |
| Falcon-1024 | 75,000 | 145,496 |
| ML-DSA-44 | 55,000 | 351,050 |
| SLH-DSA-128s | 350,000 | 558,276 |
The `falcon512_solidity` scheme instead models the CURRENTLY-DEPLOYED
`AerePQCAccount`, which verifies Falcon in the EVM via the Solidity verifier: one
authorization measured 10,278,313 gas on-chain `[CITED: pqc-account.json]`, under
the EIP-7825 2^24 per-tx cap. These two Falcon paths (native precompile ~86k vs
deployed-account Solidity ~10.5M) must not be conflated; the simulator keeps them
distinct. Account CREATE2 deploy gas is 1,493,084 `[CITED: pqc-account.json]`.
## What actually runs (verified, not asserted)
`node selftest.js` runs with no network and checks all of this (21 assertions,
all PASS):
- `keccak256("")` equals the known Ethereum/Keccak vector (the zero-dependency
keccak is correct).
- `predictPqcAccountAddress` re-derives the LIVE sample `AerePQCAccount` address
`0xa42a5e7F72E46BadC11367650Ec34D676194326f` from its real 897-byte Falcon-512
key (proves the CREATE2 derivation is byte-correct against a real deployment).
- The scanner classifies the four bundled REAL bytecode fixtures correctly:
`AerePQCAttestation` GREEN, `AerePQCTxAccount` GREEN, `AereHybridAuth` RED (the
Solidity-verifier scope limit), `AereFalcon512Verifier` RED (its `0x0100`
constants are literals, not P-256). A synthetic hybrid reads YELLOW; empty code
reads RED (EOA).
- The migration-SDK calldata builders (`migrate`, `approve`, `transfer`,
`balanceOf`) are byte-identical to values independently generated by ethers
6.16.0 (the encoder in `lib/abi.js` is a correct Solidity ABI encoder; the
ground truth was generated with `aerenew/contracts/node_modules/ethers` and
hardcoded into `selftest.js` so the check stays zero-dependency).
- `buildMigrationTransactions` returns UNSIGNED objects only: every tx is stamped
`signed:false`, none carries an `r`/`s`/`v`/`signature` field, and the target
matches the CREATE2 derivation. The `migrator` path refuses to build without an
explicit migrator address (because it is not deployed).
- The readiness report composes the CITED deployed-account auth figure
(10,278,313 gas) and counts movable assets correctly.
Additionally verified live against `https://rpc.aere.network` (chain 2800,
~block 10,416,783, 2026-07-19):
- `aere-pqc scan` fetched real bytecode and classified the live
`AerePQCAttestation` (`0x465d...`) as GREEN (4402 bytes) and the live deployer
EOA (`0xbeB3...`) as RED (0 bytes of code).
- `aere-pqc build-migration --eoa 0xbeB3... --native 1000000000000000 --estimate`
produced two unsigned txs and attached REAL `eth_estimateGas` results:
`createAccount` 59,064 gas (low because the salt-0 sample account is ALREADY
deployed, so the factory's idempotent `createAccount` short-circuits; a FIRST
deploy is the cited 1,493,084), and the native send 21,246 gas.
- Honest hazard surfaced by the live estimate: auto-draining the FULL native
balance of an ACTIVE account fails, because the sender must reserve gas AND the
balance can move between the read and the estimate. The tool now reserves a gas
buffer by default and supports `--native <wei>` for a fixed, race-free amount.
- `aere-pqc readiness 0xbeB3... --rpc ...` reported a one-time migration cost of
1,514,330 gas (1,493,084 deploy + 21,246 native) = 0.001514330 AERE at 1 Gwei,
and an ongoing 10,278,313 gas/auth (deployed reality) vs an 86,336 gas/auth
native-precompile projection.
Nothing above broadcasts a transaction. Every network call is a read
(`eth_getCode`, `eth_getBalance`, `eth_call`, `eth_estimateGas`, `eth_gasPrice`).
## Addresses used (chain 2800)
| Name | Address | Status |
|---|---|---|
| PQC precompile band | `0x0AE1..0x0AE5` | live (block 9,189,161) |
| P-256 (RIP-7212/7951) | `0x100` | live (Osaka) |
| ecrecover | `0x01` | protocol constant |
| AerePQCAccountFactory | `0xd5315Ea7caa60d320c4f34b1bEd70dd9cc02CE58` | live |
| AerePQCAccount (sample) | `0xa42a5e7F72E46BadC11367650Ec34D676194326f` | live |
| AereFalcon512Verifier (Solidity) | `0x4E8e9682329e646784fB3bd01430aA4bA54D8fFC` | live |
| AerePQCKeyRegistry | `0x1eCa3c5ADcBD0b22636D8672b00faC6D89363691` | live |
| AereEntryPointV2 | `0x8D6f40598d552fF0Cb358b6012cF4227B86aF770` | live |
| AereHybridAuth | (repo source) | not deployed `[VERIFY]` |
| AereAccountMigrator | (repo source) | not deployed, audit-gated `[VERIFY]` |
Testnet-only PQC precompiles `0x0AE6` (ML-KEM-768) and `0x0AE7` (Falcon HashToPoint) are NOT on mainnet 2800; a mainnet
call to them returns empty. The scanner names them and flags `[VERIFY]` if seen.
## Files
```
aere-pqc.js unified CLI: scan | simulate | build-migration | readiness
scan.js readiness scanner CLI (also aere-pqc scan)
plan.js migration-plan CLI (older step-by-step plan builder)
simulate.js migration cost simulator CLI (also aere-pqc simulate)
selftest.js no-network proof: keccak + CREATE2 + fixtures + calldata + txs
lib/keccak.js zero-dependency keccak256 + hex/checksum helpers
lib/abi.js zero-dependency Solidity ABI encoder (validated vs ethers v6)
lib/bytecode.js opcode-aware decoder + stack-simulation CALL-target resolver
lib/scanner.js account classifier (EOA vs contract, RED/YELLOW/GREEN)
lib/migrate.js plan builder + CREATE2 derivation (the original SDK)
lib/migrator.js transaction-construction SDK (unsigned txs, asset enumeration)
lib/readiness.js per-account quantum-readiness + migration-cost report
lib/simulate.js fleet cost model over the measured PQC verify gas
lib/precompiles.js address bands + measured gas, single source of truth
lib/rpc.js minimal read-only JSON-RPC client (global fetch)
fixtures/ real deployed bytecode extracted from this repo's artifacts
```
## The one hard invariant
This toolkit CONSTRUCTS and SIMULATES. It never signs, deploys, or moves funds.
There is no private key anywhere in it. `build-migration` hands you inert,
unsigned transaction objects; you review and sign them in your own wallet. Every
network call is read-only. Executing a migration is user-gated (and, for the
migrator/hybrid contracts, audit-and-founder-gated).
## License
MIT.