Documentation
Finality — the design
Status: implemented. The core is in
internal/chain/finality.goandmess.go, the transport ininternal/p2p/wire_finality.go, the node’s side (its key, the seats, signing, the state on disk) incmd/powmachinad/finality.go, the RPCgetfinality, the switch-off--no-finality. The parameters were chosen withcmd/powm-finsim.
The goal: an exchange must not have to deal with reorgs. After ~40 minutes a block is definitive — not probabilistically, but in the sense that a node refuses to rewrite it regardless of the attacker’s work.
1. Two layers on top of each other
PoW (lanes C / G) ← who builds the blocks
↓
MESS ← how much work a deep reorg costs
↓ a depth of 80 blocks (~40 min)
Finality: the miners of the last day ← the point of no return
MESS (mess.go) makes deep reorgs economically impossible: an hour-deep one requires 2.7× the
network’s work, a two-hour one 7×, over 7 hours 31×. Shallow reorgs (1–3 blocks) pass without a
penalty, because that is ordinary operation.
Finality (finality.go) is a hard boundary: what the quorum confirms, CheckReorg refuses to
rewrite.
2. Who is the quorum
Nobody appoints it, and there is no list of keys in the binary.
- Every node has a finality key (
finality.keyin its datadir, made the first time it starts). - The key goes into the share commitment of every block built from the node’s templates — the
node’s own stratum port, a pool on its RPC (
gettemplatecarries it asfinality_key), regtest-mine. The commitment is under the block’s PoW (spec, §3.2, Sharepool, §2.1), so a copy of the block cannot carry another key. - The quorum for the block at height h is every key in the 2,880 blocks below it (~a day).
A key weighs as many blocks as it mined there, and each lane holds half of the weight: in a
window of
nCblocks of lane C andnGof lane G a block of lane C weighsnGand one of lane GnC. Whoever mines only one lane holds at most half, however much hashrate they bring. - A block is final once keys holding more than two thirds of its quorum’s weight have signed it.
- A block without a key weighs like any other and belongs to nobody — it counts as a signature that never comes. Leaving it out would let a handful of keyed blocks decide for a window mined mostly without one.
- No quorum until the window is full. In a chain’s first 2,880 blocks the first few miners would be the whole quorum, and finality held by whoever was there first is worse than none. Until then the chain runs on PoW and MESS.
A solo miner’s seat is the node it mines against. A miner on somebody else’s stratum keeps its own
seat: powmachina-miner logs in with its own finality key (--finality-key, on by default), the
blocks it finds carry that key, and the server asks the miner to sign for it through the stratum
(the attest and attestation messages, see stratum). A pool cannot collect the
votes of the hashrate it serves; only a miner that turns the key off, or runs software without it,
leaves its seat to the server. A new pool is in the quorum a day after it starts finding blocks, and
one that leaves is out a day after it stops — without a release, a vote or anybody’s permission.
3. What the quorum can and cannot do
| It can | confirm a block that the PoW built and that our node verified itself |
| It cannot | produce a block |
| It cannot | rewrite history |
| It cannot | stop the chain |
That is not a promise, it is a property of the code. FinalityState.Update() takes a ChainLookup
— a function that returns a block from our own active chain. Attestations for a block we do not
have are ignored. The test TestQuorumCannotFinalizeForeignBlock verifies that not even a
unanimous quorum can finalize a foreign block.
When the quorum stays silent, finality does not move and the chain keeps running under MESS. The worst case is therefore a return to pure PoW, not the network stopping.
4. A double signature
When a key signs two different blocks at one height, the node:
- produces a proof (
Equivocation) — two signatures by one key, verifiable by anyone, - stops counting that key’s signatures, at every height; its weight stays in the quorum, so a caught key can only make finality harder, never easier,
- forwards the proof and keeps it on disk.
It does not stop and does not wait for a human. The key’s seats run out with the window like any other’s.
The node’s own signer never does it: it records the height and the block on disk before the
signature exists, and refuses any other block at that height or anything below it — a crash leaves
an attestation missing, never a second one (TestTheSignerNeverSignsTwice). The miner keeps the same
record for the signatures it makes through a stratum, whatever the server asks.
5. The parameters
| Parameter | Value | Why |
|---|---|---|
FinalityDepth |
80 blocks (~40 min) | deep enough for shallow reorgs to happen naturally |
FinalityWindow |
2,880 blocks (~a day) | see below |
| the threshold | more than 2/3 of the weight | two conflicting blocks can only both be final if more than a third of the weight signed both — and every such key is caught |
| the signature | Ed25519 | in Go’s standard library, no extra dependency |
| the domain | PowMachina/finality/v1 |
a signature cannot be transferred elsewhere or to another height |
cmd/powm-finsim simulates a young network (one pool with 60 %, one with 25 %, fifteen solo miners)
and measures how long finality stands still. The window trades two things against each other (30 s
block, threshold 2/3, 30 runs each):
| W = 1440 | W = 2880 | W = 5760 | |
|---|---|---|---|
| the 60 % pool leaves for good | 5.9 h | 11.3 h | 21.9 h |
| an attacker rents 2× lane G for 12 h and never signs | stalls in 11 of 30 runs | never | never |
| an attacker rents 2× both lanes for 12 h and never signs | 11.9 h | 11.9 h in 15 of 30 runs | never |
With a threshold of a half instead of two thirds finality stalls less, but a single key signing two branches during a network split would be enough to finalize both — a split that needs a person to resolve. A stall only falls back to PoW, so the threshold stays at two thirds.
What a stall costs and what it does not: while finality stands, exchanges count confirmations (security, §6) and the chain runs on MESS. Renting enough hashrate to stall it for 12 h buys an attacker a window in which a deep reorg is merely expensive — they still need the majority of both lanes for the reorg itself.
The weak spot: a pool holding more than a third of the window whose node mines but does not
sign stalls finality for as long as that lasts. That is why signing is built into the node and needs
nothing from the operator, and why getfinality on a pool’s node shows its own share and the share
that has signed.
6. How an exchange uses it
Instead of counting confirmations, a deposit is credited on proof of finality:
getfinalityreturns the height and the ID of the last confirmed block.- The deposit is credited when its block is at or below that height.
- For large amounts, additionally: cross-check the tip against two independent nodes.
- An alert on a reorg deeper than 80 blocks, on every proof of a double signature (
caught_keys) and on finality lagging more than 240 blocks behind the tip.
Until then (and for anyone not using finality) the recommendation from security, §6 applies: the number of confirmations by amount and blocks from at least two different lanes between confirmations.
getfinality also shows the block on its way: its quorum (members), the share of the weight that
has signed it (signed_permille) and this node’s own share (own_permille).
7. What we pay for it
- Trust in the miners of the last day. Without finality a reorg would merely be expensive; with it, it is ruled out as long as more than two thirds of the last day’s blocks were mined by keys that sign honestly. Hence the asymmetry in §3 — the worst case is a return to the original state.
- The risk of splitting the network. A node that does not have the attestations (disconnected, censored) may accept a reorg the others reject. That is why finality only kicks in at a depth of 80, where reorgs are rare anyway.
- It is not a proof for a third party. The attestations live outside the blocks; whoever did not see them does not know about the finality. If that is ever needed (bridges, light clients), they will be written into a commitment in the block.
- A stolen node is a stolen seat. The key sits next to the node, because a key elsewhere would be one more process every pool has to run — and a pool that does not run it is a silent seat. Its weight is what the pool mined in the last day, and it runs out with the window.
- A key is the miner’s own business. A key nobody holds — a point of small order — is one anybody can sign with. A miner who puts such a key into its blocks gives away its own seat, the same as publishing its private key; nobody else’s weight is touched, so the node does not police it.
powmachinad --no-finality turns all of it off: the node neither counts the quorum nor holds a seat,
runs on pure PoW and is still on the same network.
8. How it runs
An attestation is 136 B on the wire (the height, the block, the key, the signature), a proof of a double signature 272 B — a fixed length, so it cannot be inflated. The node takes an attestation only from a key with a seat in the quorum of that height on its own chain; anything else is dropped and not relayed, so nobody can flood the network with made-up keys. What is accepted is filed, finality is recomputed and the attestation goes to everyone except whoever it came from.
The node signs by itself: whenever a new tip is applied, it signs the block FinalityDepth (80)
below it if its key has a seat in that block’s quorum. A node that is catching up does not sign — it
has nothing to say about blocks the network finalized long ago.
The state on disk
finality.json in the datadir holds the finalized height and block and the proofs of double
signing; it is written whenever finality moves. After a restart finality is back at once, and a
restart of the whole network does not lose it. A finalized block that is not on the chain the
node has after the restart is not taken back — finality comes again with the next signatures.
Attestations after a restart
Duplicate attestations are deliberately not gossiped (otherwise the gossip would never end), so a node that was away has no way of learning about the attestations it missed — the peers have already forwarded them and will not do it again. So it requests them:
fgetattst{from height, count} ──▶ fattests{the attestations the peer has}
- it only asks about the heights between the confirmed one and the one already
FinalityDepthbelow the tip, the newest first, - at most
MaxAttestHeights(64) heights at a time, and the answer is capped atMaxAttestPerMessage(640), so a request cannot force a big message, - the driver asks once every 20 s, and only when finality really is behind.
The finality messages have names of their own (fattest, fequivoc, fgetattst, fattests): a
node that does not know them ignores them instead of reading them wrong.
9. What remains
- A bond (finality bond) was designed for a quorum of appointed keys and does not fit a quorum that changes every day. A key that signs twice already loses its say; whether it should also lose money is open.
- Finality as a proof for a third party (§7) — when somebody needs it.