Bitcoin Chain Validation
Miners on a hostile minority fork can use P2Poolv2 to earn bitcoin on the main chain while they attack it. To stop them, P2Poolv2 checks which bitcoin chain each share builds on and pays nothing for work deep in a hostile fork.
The Hostile Fork Attack
A hostile fork begins when a group of miners splits from the main bitcoin chain and mines its own branch. If those miners also run P2Poolv2, they can point each share’s bitcoin header at a block on their fork. Unless a rule withholds payment for shares on that block, their shares join the sharechain like any other and gain PPLNS weight.
The attack pays the attackers twice. When an honest share meets bitcoin difficulty, its coinbase pays every share in its PPLNS window, the fork’s shares included. The fork’s miners earn main chain bitcoin for work that attacks the main chain, and they keep any reward the fork earns. P2Poolv2 then subsidizes the attack.
In the diagram, the hostile fork leaves the bitcoin chain at Block 101. Shares 3, 5, and 7 build on hostile blocks, but they sit in the same sharechain as the honest shares. Share S then meets bitcoin difficulty and becomes Block 105, and its coinbase pays all seven shares in its window.
Overview of the Defense
|
Note
|
P2Poolv2 pays a share only for work on the bitcoin chain that the paying block extends, give or take one block. Work on a made-up bitcoin block earns nothing, and work on a block that lost an orphan race still earns rewards. Miners on a hostile fork earn on the main chain for at most the fork’s first two blocks. |
Every share names the bitcoin block it builds on, the previous block hash in its bitcoin header included in the share header. This page calls that block the share’s bitcoin prev. With no rule on checking the bitcoin_prev, a miner can name a bitcoin prev that does not exist, or one on a hostile fork, and still collect PPLNS weight. During a fork war, the fork’s miners then earn from both chains for the same proof of work.
The design has three parts:
- Payout rule
-
A coinbase pays a share in its PPLNS window only if that share’s bitcoin prev sits on the paying share’s bitcoin chain, or one block off it. Every node rebuilds every coinbase during share validation, so this is a validity rule for shares.
- Carried bitcoin headers
-
Share messages carry the bitcoin headers that each receiver needs to place every share’s bitcoin prev. Each node checks these received headers without calling bitcoind RPC.
- Known bitcoin prev
-
A node accepts a share only after it knows the header of the share’s bitcoin prev. Until then it waits for the headers to arrive.
Fork choice and the job builder stay as they are.
In the diagram, the paying share builds on Block 102, so the paying chain ends there. Sibling 102 and Hostile 102 both build on Block 101, so each sits one block off the paying chain, and shares on them earn. Hostile 103 sits two blocks off, so Share 5 earns nothing. Nodes cannot tell a hostile fork’s first block from a benign sibling. The rule pays both, and stops paying at the fork’s second block.
Terms
- Known header
-
A bitcoin header in the node’s header store, reported by the local bitcoind or carried with a share and checked.
- Paying chain
-
For share S, the bitcoin chain ending at S’s bitcoin prev. S’s coinbase extends this chain if S meets bitcoin difficulty.
- Eligible
-
A bitcoin block is eligible for S when the block, or its parent, is on S’s paying chain. A share is eligible for S when its bitcoin prev is.
Local Tree of Bitcoin Headers
Each P2Poolv2 node keeps a store of full bitcoin headers, with their heights and parents. Acceptance and payout checks read only this store and do not call bitcoind RPC. Three sources maintain this store:
-
ZMQ
sequenceevents from the local bitcoind. A block connect adds the block; a disconnect leaves it in place.sequencenumbers every message, so a gap shows a missed one. -
The
getchaintipsRPC, at startup and after a sequence gap or a ZMQ reconnect. The node walks each listed branch back withgetblockheaderuntil it reaches a header already in the store. -
Carried headers with share chain headers that pass the header check.
Payout checks reach about 14 days back (the PPLNS window), and nodes
validate shares up to PRUNE_DEPTH (about 28 days) deep. The store
therefore keeps about six weeks of headers: roughly 6,048.
Siblings at the Tip
The local bitcoind cannot tell every node about every orphan. Bitcoin Core announces a block only after making it its tip, and it ignores an equal-work sibling that arrives as a compact block. Some bitcoinds therefore never learn of an orphan.
A node that cannot place a share on that orphan rebuilds a different coinbase from other nodes. It marks the paying share invalid, but the others accept it, and the sharechain splits. Every node therefore needs the same headers, siblings included. Therefore P2poolv2 shares carry the bitcoin headers needed to connect its bitcoin prev to the active bitcoin chain.
Shares Carry Bitcoin Headers
A share carries the headers on the path from its bitcoin prev back to, but not including, the point where that path meets its parent’s bitcoin prev:
-
Most shares build on the same bitcoin block as their parent and carry nothing.
-
The first share after a new bitcoin block carries one header.
-
The first share after a bitcoin reorg carries the headers back to the fork point.
We call this list of bitcoin headers carried by a share as
ShareHeaderBatch. The first share in each ShareHeaderBatch also
carries the path back to the sender’s active chain, because the
receiver may never have seen its parent. A syncing node learns
weeks-old orphans this way.
The headers travel beside the share header, outside the share header. The share’s proof of work already binds its bitcoin prev, and each header’s previous block hash fixes the header before it, so the share already commits to every header it needs. A sender can only leave headers out or add wrong ones. A missing header leaves the bitcoin prev unknown, and the node waits for a copy with the headers, while a wrong header fails the check.
Checking a Header
A node checks every carried header itself, with no RPC calls to bitcoind. A header passes when:
-
it links to a known header,
-
its hash meets its
bits, -
its
bitsis exactly the target the difficulty rules require, and -
its timestamp is at most two hours ahead of the local clock.
Within a 2,016-block period, the required bits equals the parent’s.
At a retarget boundary, the node computes it with rust-bitcoin’s
CompactTarget::from_next_work_required. On testnet4, the node also
applies the min-difficulty and BIP94 timewarp rules, which rust-bitcoin
leaves to the caller. The timestamp limit stops a fake branch from
setting timestamps that ease its bits.
A header that passes costs a full block of real bitcoin proof of work. It does not prove that a valid body exists. The payout rule makes that proof unnecessary, because it bounds how far off the paying chain paid work can sit.
Accepting a Share
The same steps apply to a share header in a ShareHeaderBatch and to
an unsolicited share block. None of them waits on bitcoind.
-
If the share’s parent or an uncle is unknown, drop the share and ask the sender for headers, as the node does today for a missing parent.
-
Check the carried headers, oldest first, and stop at the first failure. Headers that pass go into the store.
-
If the bitcoin prev is now known, store the share header, then fetch and validate the body, payout rule included.
-
Otherwise drop the share without a verdict, and remember its hash in a bounded, expiring cache so its children are dropped without a refetch.
Requiring a known bitcoin prev keeps nodes in agreement. Otherwise an attacker can mine a share on a real orphan, send it with headers to some nodes and without them to others, and make those nodes rebuild different coinbases later. A share on a made-up bitcoin prev never gets a header, so no node accepts it. An honest share is almost never dropped: its parent arrived first, so its carried headers link onto a known bitcoin prev.
Rejecting Work on Stale Bitcoin Blocks Fails
Rejecting shares whose bitcoin prev is off the local bitcoind’s active chain fails for four reasons:
- Bitcoin nodes disagree for a while
-
A new bitcoin block reaches some bitcoind nodes before others. A node whose bitcoind has not seen the block rejects shares that build on it, and nodes that have seen it accept them. The sharechains on the two groups of nodes diverge.
- Reorgs strand honest work
-
When a bitcoin block loses a race, every share mined on it becomes invalid, and the sharechain rolls back to the last share on the winning block.
- Late shares look like stale shares
-
A share mined just before a new bitcoin block arrived looks the same as a share mined on an old block on purpose. Telling them apart needs a timing rule, and nodes have a clock synchronization delta of 11 bitcoin blocks from the MTP rule.
- Slack pays hostile forks
-
Accepting shares within a few blocks of the local tip pays a hostile fork’s miners for those first blocks, while they attack the main chain.
The first diagram shows two nodes whose bitcoinds see different tips. Node B’s bitcoind received Block O first, and Node A’s received Block T. Under a rule tied to the local bitcoind, Node A rejects Share S and Node B accepts it, and the sharechain forks between the two nodes.
The second diagram shows a reorg. Share 2 builds on Block O, which then loses to Blocks T and 102. A rule that rejects shares off the active chain invalidates Share 2, and Share 3 must extend Share 1. The sharechain then reorgs for every stale bitcoin block, and discards the honest work of miners whose block lost the race.
Solution: Pay Only Work Near the Paying Chain
Nodes accept every share whose bitcoin prev they know, so all nodes hold the same shares. The decision moves from accepting a share to paying it: each coinbase pays only the window shares eligible for the share that builds it.
The Payout Rule
When a node builds or checks the coinbase of share S, it computes the PPLNS window anchored on S’s parent as before. It then pays each window share only if that share is eligible for S. An ineligible share keeps its place in the window and earns nothing.
In the diagram below, Shares 1 to 4 form Share S’s PPLNS window. A branch leaves the paying chain at Block 101; it stands for either a hostile fork or a two-block orphan race. Share 3, one block into the branch, is paid. Share 4, two blocks in, is not.
| Bitcoin prev, relative to the paying chain | Paid | Examples |
|---|---|---|
On it |
Yes |
Ordinary shares; deliberately old bitcoin prevs |
One block off it |
Yes |
A block that lost an orphan race; a hostile fork’s first block; a header-only block |
Two or more blocks off it |
No |
A hostile fork’s second and later blocks; the second block of a two-block orphan race |
Unknown |
Never in a window |
The node never accepts the share |
Eligibility is measured from S’s bitcoin prev, never from the checking node’s bitcoind tip. It depends on headers alone, so every node that knows the same headers rebuilds the same coinbase. A template that lags the sharechain by one block still pays the newest shares, since their bitcoin prev sits one block off its paying chain.
The rule changes share validity. Every node rebuilds every coinbase
and compares it exactly (validate_bitcoin_payout), and any mismatch
makes the share invalid. A rule that only honest job builders applied
fails, because validators must then accept coinbases built either
way.
Uncles
-
An ineligible uncle earns nothing, and its nephew earns no bonus for it. Honest nephews gain nothing by including fork shares.
-
An eligible uncle under an ineligible nephew earns its uncle weight. The nephew’s bonus goes unpaid.
-
Uncles still use no window slots.
When Nothing in the Window Is Eligible
A job builder whose window holds nothing eligible stops the node. Its miners extend a sharechain that an attacker controls.
A node checking a share with nothing eligible marks the share invalid and keeps running. Anyone can make such a share for one share’s proof of work, by naming a main chain block more than 14 days old. No honest share meets this case: its paying chain ends at its node’s current template, and the recent shares in its window sit on or beside that chain.
What the Design Costs
-
A validity change, which needs a coordinated upgrade at a fixed sharechain height.
-
Hostile forks earn for about two block intervals of their hashrate each time a fork starts.
-
Header-only work one block off the paying chain earns, at the cost of a full block of proof of work.
-
Shares on the second block of a losing two-block orphan race earn nothing. Such races are rare.
-
Payout variance rises during a fork war.
-
Signet gets weak protection, since a signet header’s proof of work is nearly free.
Alternatives Considered
- Accept shares within K blocks of the local tip
-
Pays a hostile fork’s miners for its first K blocks. Nodes also disagree for good after a bitcoin reorg.
- Pay only clean shares from the job builder
-
Every node rebuilds every coinbase, so validators must accept coinbases built with the rule or without it.
- Payout rule with bitcoind’s headers only
-
Nodes disagree on coinbases whenever some bitcoinds never saw an orphan. The only consistent form stops paying orphans.
- Chainwork and fork-depth validity rules
-
A header-only block passes them, they still pay a fork’s later blocks, and they need the same headers as the payout rule.
- Pass carried headers to bitcoind with
submitheader -
Costs an RPC round trip per header, and lets attackers load bitcoind. The local check suffices.
- Check only that a header meets its own
bits -
A fake branch then uses easy
bits, at a quarter of a block’s work per header or less.