Documentation

Finality — the design

Status: implemented. The core is in internal/chain/finality.go and mess.go, the transport in internal/p2p/wire_finality.go, the node’s side (its key, the seats, signing, the state on disk) in cmd/powmachinad/finality.go, the RPC getfinality, the switch-off --no-finality. The parameters were chosen with cmd/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.

  1. Every node has a finality key (finality.key in its datadir, made the first time it starts).
  2. 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 (gettemplate carries it as finality_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.
  3. 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 nC blocks of lane C and nG of lane G a block of lane C weighs nG and one of lane G nC. Whoever mines only one lane holds at most half, however much hashrate they bring.
  4. A block is final once keys holding more than two thirds of its quorum’s weight have signed it.
  5. 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.
  6. 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:

  1. produces a proof (Equivocation) — two signatures by one key, verifiable by anyone,
  2. 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,
  3. 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:

  1. getfinality returns the height and the ID of the last confirmed block.
  2. The deposit is credited when its block is at or below that height.
  3. For large amounts, additionally: cross-check the tip against two independent nodes.
  4. 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 FinalityDepth below the tip, the newest first,
  • at most MaxAttestHeights (64) heights at a time, and the answer is capped at MaxAttestPerMessage (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.