Accounts and keys
This is the page that most changes how you use the chain, so it is worth reading before you write any code.
Frost has no post-quantum externally-owned account. Protocol-level accounts do not carry ML-DSA keys, and no address is derived from an ML-DSA public key. Instead, a post-quantum account is a smart contract: a small piece of code that holds its own public key and verifies its own signatures in a VERIFY frame.
This is not a workaround, it is the only shape that works. A 1,952-byte public key cannot be compressed into a 20-byte address, and lattice signatures offer no public-key recovery, so there is nothing for the protocol to derive an EOA from. Putting the key in the account solves both problems at once — and, as a bonus, means the chain needs no registry mapping addresses to keys.
Ordinary secp256k1 EOAs still work on Frost, for compatibility. They are simply not quantum-safe, and the chain’s own treasury and deployment flow do not use them.
Counterfactual addresses
Section titled “Counterfactual addresses”An account’s address is a pure function of its code and its key material, so it can be computed entirely offline, before anything exists on chain:
initcode = [constructor | runtime | key material]salt = keccak256("frost/account/v1" ‖ ACCOUNT_CODE_ID ‖ uint64_le(index))address = keccak256(0xff ‖ FACTORY ‖ salt ‖ keccak256(initcode))[12:]where FACTORY is the deterministic CREATE2 factory predeployed at
0x4e59b448…B4956C.
The practical consequence is the thing to internalise: you can receive funds at an address that does not exist yet. A wallet can generate a key and display a receiving address immediately, with no transaction, no deployment, and no gas. Funds accumulate at an address with no code, which is perfectly normal.
The account then materialises itself. Its first outgoing transaction carries a DEPLOY frame that deploys the contract and pays for that deployment out of the balance already sitting at the address. There is no separate “deploy your wallet” step, no sponsor required, and no moment where the user must hold a different kind of asset to get started.
The account family
Section titled “The account family”Frost ships a small set of pre-built account contracts. There is no Solidity involved: each is hand-assembled EVM bytecode, generated by the reference tooling, so the code is small, auditable, and identical for every user of that type.
| Code ID | Type | Scheme | Where the key lives |
|---|---|---|---|
{ACCOUNTS.codeIds.fixedMldsa44} |
Fixed-key v1 | ML-DSA-44 (0x15) |
Embedded directly in the contract’s code |
{ACCOUNTS.codeIds.fixedMldsa65} |
Fixed-key v1 | ML-DSA-65 (0x14) |
Embedded directly in the contract’s code |
{ACCOUNTS.codeIds.rotatableV2} |
Rotatable v2 | ML-DSA-65 (0x14) |
Active key behind a storage pointer; backup key committed by hash |
Fixed-key accounts are the simplest thing that works. The public key is part of the contract code, so verification is a code copy and a precompile call. The key can never be changed — if you lose it, the account is gone.
The rotatable v2 account is the one to use by default. It holds two keys:
- an active key, stored behind a pointer contract, used for everyday spends;
- a backup key, of which the account stores only
keccak256(backupPk).
Losing the device holding the active key is survivable. The backup key authorises a rotation that installs a new active key and a new backup commitment, at the same address. The rotation path verifies the backup authorisation specifically, so a compromised active key can never rotate the account — it can spend, but it cannot lock you out. Every rotation consumes the old backup commitment and installs a fresh one.
ML-DSA-65 is not an arbitrary choice for the rotatable account: it is the parameter set Apple’s Secure Enclave supports, which is what makes hardware custody possible. See Hardware custody.
Bytecode is frozen
Section titled “Bytecode is frozen”Account bytecode is fixed by hash and never edited. A change to the code is definitionally a new code ID, never a modification of an existing one.
The reason is a direct consequence of counterfactual addressing. If the
bytecode for code ID 0x03 changed, every not-yet-deployed account of that type
would silently re-derive to a different address — stranding any funds already
sent to the old one, with no way to reach them. So the rule is absolute, and it
is enforced by tests that pin the code hashes.
What a spend looks like
Section titled “What a spend looks like”Putting the pieces together, an ordinary transfer from a rotatable account is:
frames: [0] VERIFY target: (self) flags: 3 gas: 30,000 data: (empty) [1] SENDER target: recipient value: … gas: 50,000
signatures: [0] {scheme: ARBITRARY, msg: (empty), signature: ML-DSA-65 over the sig-hash}The VERIFY frame runs the account’s code, which copies the signature out of the
transaction with SIGPARAM, loads its own active public key, calls the
0x14 precompile, and APPROVEs only
if the precompile returns 1.
Note what the account does not do: it never trusts a public key supplied by the transaction. The key comes from its own state or its own code. That binding is what makes the verification mean anything.
- Create a wallet — do this for real.
- Account contracts — storage layout, dispatch rules, and address derivation in full detail.
- Counterfactual onboarding — why the first send pays for its own deployment.