Skip to content

History and storage

A secp256k1 signature is 65 bytes. An ML-DSA-65 signature is 3,309. That single ratio is the reason this page exists.

On a classical chain, signature bytes are a rounding error next to state and calldata. On a post-quantum chain they are the dominant term: a frame transaction is roughly 90% signature bytes by volume. Storage growth stops being an operational detail and becomes a first-order design constraint — one the chain has to answer for explicitly, on both the execution and consensus axes.

Frost’s answer has three parts.

The key observation is that a signature’s value decays. Before a block is final, the witness is what proves the transaction was authorised. After the block is final and the chain has moved on, the network has already agreed that it was authorised — and the witness is several kilobytes of data that nobody will check again.

So Frost separates them. Each block header carries a strippedTransactionsRoot: a commitment over the signature-stripped encoding of its transactions. The signature bytes themselves live in a separately prunable sidecar.

This means a node can discard multi-kilobyte witnesses once they are no longer needed, while still holding a commitment that proves exactly what was in the block. Anyone who needs the full witness can fetch it from a node that keeps it and verify it against the commitment — the pruning is not a loss of verifiability, only of local storage.

Not every signature byte is elidable. An ARBITRARY signature entry with a non-empty msg is permanent — it can never be pruned — so those bytes carry an extra gas surcharge on top of ordinary calldata gas, pricing them above the elidable floor.

On the consensus side, the raw DAG of signed blocks is heavy and grows faster than the execution-layer chain does. Retaining all of it forever is not viable.

Instead, validators quorum-sign compact hash-linked checkpoint summaries. Each summary is a couple of hundred bytes and pins three things: the consensus position, the execution-layer head, and the block-key schedule. Together they form a checkpoint chain that certifies the history without containing it.

Once a certified checkpoint covers a range of consensus blocks, the heavy signed block data behind it can be pruned within a rolling window. The checkpoint chain remains, and it is what a new node verifies against.

This is also how joining works. A new node does not replay from genesis. It verifies the checkpoint chain against the committee digest recorded in the genesis block, installs the block-key schedule with every delegation checked against the checkpoint roots, and restores a verified snapshot. Trust bottoms out in the chain definition, not in whoever served the data.

The result is that no single node has to hold everything. A node’s tier is defined by what its execution layer retains:

  • pruned — a bounded recent window of bodies and receipts, plus the full header chain and current state.
  • tx-history — signature-stripped transaction bodies and receipts back to genesis, all provable, with witness sidecars pruned behind a rolling window.
  • full-history — everything, including every witness.
  • state-archive — all historical state, so eth_call works at any height.

The live network’s nodes run the pruned and tx-history tiers; no live node keeps every witness. Complete history lives instead in a cold archive: certified consensus segments and full-witness block groups uploaded to untrusted S3-compatible object storage by the exporting node, which verifies each upload by hash before it is allowed to prune the corresponding data locally. A state-archive node is built by restoring from that archive, not by syncing from the live network.

“Untrusted” is doing real work in that sentence. The archive is verified against commitments the chain already carries, so a storage provider that corrupts or loses data is detected rather than believed.

You can ask any node what it holds:

Terminal window
curl -s https://rpc.frostfi.net \
-H 'content-type: application/json' \
--data '{"jsonrpc":"2.0","id":1,"method":"frost_historyStatus","params":[]}'
{
"tier": "tx-history",
"headBlock": "0xdb703",
"earliestFullBody": "0xd8ff3",
"historyCutoff": "0x0",
"persistedState": "0xd8b18"
}

If you need deep history, query a node that serves the tier you need rather than assuming the public endpoint has it; witnesses older than a node’s sidecar window come only from the cold archive or a node restored from it. Node types describes each tier.

Very little, day to day, but two things are worth knowing:

Your transactions are large. A post-quantum spend is a few kilobytes of calldata, and calldata gas is most of what you pay. The cheaper ML-DSA-44 account type exists partly for this reason.

Historical queries are tier-dependent. A request for a very old block body may be served by one node and refused by another. That is by design, not a failure — check frost_historyStatus and pick an endpoint that matches what you need.