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.

hostile fork attack
Figure 1. A main chain block pays shares mined on a hostile fork

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.

bitcoin chain hostile fork
Figure 2. A block paying from Block 102 pays shares on its chain or one block off it

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 sequence events from the local bitcoind. A block connect adds the block; a disconnect leaves it in place. sequence numbers every message, so a gap shows a missed one.

  • The getchaintips RPC, at startup and after a sequence gap or a ZMQ reconnect. The node walks each listed branch back with getblockheader until 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.

carried bitcoin headers
Figure 3. Each share carries only the bitcoin headers its parent lacks

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 bits is 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.

  1. 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.

  2. Check the carried headers, oldest first, and stop at the first failure. Headers that pass go into the store.

  3. If the bitcoin prev is now known, store the share header, then fetch and validate the body, payout rule included.

  4. 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.

bitcoin view split
Figure 4. Validity tied to the local bitcoind splits the sharechain

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.

bitcoin reorg strands
Figure 5. Rejecting shares off the active chain discards honest work after a reorg

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.

payout rule
Figure 6. Share S’s coinbase pays window shares on its paying chain or one block off it
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.


Back to top

Copyright © 2024-2026 P2Poolv2 Developers. Distributed under the MIT or Apache-2.0 license.

Describes P2Poolv2 at commit dc5bb652 (2026-10-08).

This site uses Just the Docs, a documentation theme for Jekyll.