Offline signing lab · post-quantum custody

Sign in the cold.
Broadcast from the heat.

Your keys live on a machine that never touches the network. The hot machine issues a chain ticket with everything the cold side needs — genesis hash, runtime versions, your next nonce — you sign the transfer offline with real ML-DSA keys, and the hot side re-verifies the signature locally before anything reaches the chain.

1
Hot desk issues a ticketChain facts for one transfer, packed into a single QR.
2
Cold desk signs offline24 words in, signed extrinsic out — chunked QRs, keys wiped after.
3
Hot desk verifies & broadcastsSignature re-checked against the ticket, fee quoted, then sent.
The airgap is a procedure, not a promise this page can prove. For real cold storage, the cold side should run on a machine with no Wi-Fi, no Bluetooth, no Ethernet — ideally booted from read-only media. This page can't verify your machine is offline; that's on you.

1 Issue a chain ticket

Connects to a node, reads the chain facts a cold signer needs, and packs them into one QR. The ticket binds the signature to a specific genesis, runtime, nonce and era window.

needs a live node — no node, no ticket (nothing is fabricated)

2 Receive, verify & broadcast

Bring the signed package back from the cold machine — scan the chunk QRs, drop images, paste the text blocks, or load the file. The desk reassembles, decodes, and re-verifies the ML-DSA signature locally before offering broadcast.

🛡 What the hot desk guarantees — and what it can't

  • Byte-exact payload: the signature is verified against a payload re-derived from the ticket, not from values inside the package — a package that claims a different genesis, runtime, nonce or amount fails.
  • Nonce freshness: before broadcast, the desk compares the package nonce with the chain's accountNextIndex. A stale nonce means something else was sent first — re-issue the ticket.
  • Era window: the mortal era (64 blocks) is checked against the current head; an expired package is refused, not retried.
  • What it can't do: prove the cold machine was offline, or that the 24 words were never photographed. Cold hygiene is physical.

⚙ How the protocol works

The chain ticket

A signed-by-nobody JSON blob: sender address, next nonce, genesis hash, spec & transaction versions, head number+hash, and the mortal-era window (birth block + hash). Serialized as QAGT1:<base64url> — small enough for one QR. The cold side refuses tickets with a bad SS58 checksum, a non-189 prefix, or malformed chain fields.

The signed package

A complete signed extrinsic — version byte 0x84, AccountId32 sender, variant-tagged ML-DSA signature + pubkey, mortal era, nonce, tip, and the Balances.transfer_keep_alive call. Transported as numbered chunks QAGX:<session>:<i>/<n>:<base64url>; reassembly tolerates any scan order and duplicates, and refuses mixed sessions or missing chunks.

Hot-side verification

The desk decodes the extrinsic, pulls the ticket's genesis/spec/tx-version/era-birth, re-derives the exact signing payload (QUANTUS_EXTRINSIC context), and verifies the ML-DSA signature locally. Only then is a node fee quoted and broadcast offered. Tamper with one byte and the badge goes red.

Honest limits

  • No page can prove its machine is offline — the airgap is your procedure.
  • Fees are quoted by the node at broadcast time; the cold side only knows byte length.
  • The era window is 64 blocks — dawdle and the package expires; re-issue the ticket.
  • If the sender's nonce moved (another transfer went out), the ticket's nonce is stale — the hot desk checks accountNextIndex and warns.