12 min read

We tested SegWit against legacy nodes on regtest. It's a hard fork.

The companion post argued from the specification that SegWit was a hard fork by function under the standard classification. This one runs the experiment. Ten Bitcoin Core regtest nodes at six hashpower ratios, plus targeted tests on real Bitcoin Core 0.12.1 (the truly pre-SegWit version, no witness code at all). Result: both sides validate honestly by their own rules; the rules diverge; the chain splits whenever legacy operates naturally against a segwit-format output. The 2017 outcome picked which side got to keep the ticker, not which side was doing something wrong.

BIP141 and BIP144 in one paragraph

BIP141 is the consensus half of SegWit — new output types (P2WPKH, P2WSH), a separate witness data structure, and a coinbase commitment for any block containing witness transactions. BIP144 is the networking half — the NODE_WITNESS service bit, the extended block/tx wire serialisation, and the rule that a SegWit-aware peer strips witness data before sending to any peer that does not advertise NODE_WITNESS. Bitcoin Core 0.13.0 (August 2016) shipped the code with signalling disabled; 0.13.1 (27 October 2016) turned on BIP9 signalling. Signalling stalled around 30% of miner hashpower through most of the first half of 2017. On 1 August 2017, BIP148's UASF deadline took effect — nodes running BIP148 would orphan any block that didn't signal SegWit — and the same day Bitcoin Cash executed its own coordinated protocol upgrade taking the block-size-scaling path and explicitly excluding the SegWit code change, forking off the BTC chain at that block height. Under BIP148's threat, remaining miners on the BTC chain signalled; SegWit locked in on 8 August 2017 and activated at block 481,824 on 24 August 2017.

Setup and the three node states

Three node states matter for what follows: (i) truly pre-BIP141/144 (Bitcoin Core 0.12.x, no witness code, no NODE_WITNESS advertisement); (ii) 0.13.x pre-activation (has the code, advertises NODE_WITNESS, but SegWit is in DEFINED-not-ACTIVE state, so witness data in a block is not yet allowed by its rules); (iii) post-activation SegWit-aware (0.13.1+ once locked in, or the modern 26.0 we use).

The ratio experiments below use 0.13.2 for the "legacy" team and Bitcoin Core 26.0 with -testactivationheight=segwit@1 for the SegWit team, across ten regtest nodes on three VPSes wired via addnode. The narrower single-host tests swap the legacy side out for actual 0.12.1 to isolate the truly-pre-SegWit behaviour. The two setups diverge from the SegWit chain via different mechanisms, which we come back to below.

Six hashpower ratios, six chain splits

For each ratio: reset chains, mine 10 baseline blocks from one node, then 30 rounds of alternating team mining (each miner produces one block per round), then halt and snapshot every tip. Result at every ratio: two chains, both alive, neither adopted a single block from the other.

ratioSegWit teamlegacy teamdistinct chains
90/10 (9 vs 1, all mine)height 280height 302
70/30 (7 vs 3, all mine)height 220height 722
50/50 (5 vs 5, all mine)height 160height 1042
30/70 (3 vs 7, all mine)height 100height 1562
70/30 with listeners4m+3L, height 1302m+1L, height 452
30/70 with listeners2m+1L, height 705m+2L, height 982

Legacy 0.13.2 rejects every SegWit block on first contact with ContextualCheckBlock: unexpected witness data found. SegWit 26.0 rejects every legacy block that spends a segwit output as missing a witness signature. Two facts then land immediately: even the 90/10 minority (a single legacy node vs nine SegWit miners) holds its own 30-block chain — the "minority just gets orphaned" story fails at the smallest tested ratio. And listener-only nodes track their team's chain identically to miners: n7 was a pure listener and ended on the same tip as the SegWit miners in the 70/30 run. Chain adoption is a validation decision every node makes; the miner/listener distinction is beside the point.

A wake-up sequence confirms the "not frozen" reading of a listener-only chain: 8 SegWit miners plus 2 legacy listeners; after 170 SegWit blocks legacy is still at genesis; then n9 flips to mining and produces 101 blocks; n10 (still pure listener) tracks every one; n9 broadcasts a 1-BTC tx to n10; both mempools hold it across 5 more SegWit-only rounds; n9 mines one more block; tx confirms; n10's wallet balance updates. The legacy chain was never dead — it was waiting for a block under its own rules.

Two versions, two split mechanisms

0.13.2 has SegWit code, so it advertises NODE_WITNESS. SegWit-aware peers see that bit and send it full witness blocks; 0.13.2 then applies its rule "SegWit not yet active, witness data not allowed" and rejects. Split immediately, at the first witness block. 0.12.1 has no SegWit code, doesn't advertise NODE_WITNESS, so SegWit-aware peers strip the block via BIP144 before sending. 0.12.1 parses the stripped format fine and accepts.

We confirmed the second path directly: 3× 0.12.1 nodes on a single host, hand-implemented BIP144 stripping in Python, fed 102 stripped blocks from a 26.0 chain into a 0.12.1 node. All 102 accepted, zero rejections, same tip on both sides. So under the actual designed protocol — BIP141 plus BIP144 — a single SegWit block does not immediately split the chain. Pre-SegWit nodes stay in sync at the header/UTXO-tracking level for as long as they only receive stripped blocks and never produce a block of their own that touches a segwit-format output.

The anyone-can-spend claim, and its scope

The classical BIP141 story is that a P2WPKH output (scriptPubKey = OP_0 <20B hash>) evaluates as anyone-can-spend to a pre-SegWit interpreter. Empty scriptSig, then push zero, then push 20 bytes; stack top is 20 bytes, non-zero, script returns TRUE. No OP_CHECKSIG in that path, so no signature check runs. Tested on 3× 0.12.1: mempool policy rejects the raw spend (CLEANSTACK is enforced there), but the same spend submitted inside a hand-crafted block was accepted by all three nodes. Consensus-level anyone-can-spend on pre-SegWit — confirmed.

This is where the scope matters. The anyone-can-spend property is scoped to one specific scriptPubKey template. Legacy P2PKH (OP_DUP OP_HASH160 <20B> OP_EQUALVERIFY OP_CHECKSIG) contains OP_CHECKSIG and demands a signature to spend, on every chain, before and after SegWit. So does P2SH via its wrapped script. On the pre-SegWit-activation chain every output actually created was P2PKH or P2SH; every spend ran a signature check; the chain was fully secure by its own rules. Anyone-can-spend outputs only enter the picture when a SegWit user deliberately creates a P2WPKH output and a pre-SegWit node then receives it via BIP144 stripping. The SegWit user is the one opting into that gap, trusting the SegWit-aware majority to keep enforcing the extra witness-signature rule.

Does the pre-SegWit node verify?

Yes. It runs its script interpreter on every input, checks the block hash against the difficulty target, verifies the merkle root over transaction txids (BIP141 defines txids over the stripped serialisation, so the merkle root a 0.12.1 node computes matches what a SegWit node wrote in the header). For a P2PKH input it runs OP_CHECKSIG and verifies the DER signature against the pubkey — invalid signature, tx and block rejected. For a P2WPKH-format input inside a stripped block it runs the script literally, sees no OP_CHECKSIG, gets TRUE, accepts. Not skipping verification — running verification, and getting the correct answer under pre-BIP141 semantics. What it cannot do is see the witness signature that BIP141 made load-bearing on the SegWit side. The signature exists on the network; it is not in a form 0.12.1's script model has code for. The whitepaper Section 2 property — a payee independently verifying signatures — asks something 0.12.1 used to do for every output and now, for SegWit-era outputs, no longer can.

Where the split actually happens

Pull all the tests together and the trigger is precise: the chain splits the moment a pre-SegWit miner produces a block that contains a legacy-format spend of a UTXO whose scriptPubKey matches the P2WPKH template. That block is valid under pre-SegWit rules (anyone-can-spend passes at consensus). It is invalid under SegWit rules (witness signature required, missing). All other blocks a pre-SegWit miner can produce — coinbase-only blocks, blocks with real signed P2PKH-to-P2PKH transactions, even blocks building directly on top of the SegWit tip — are accepted by SegWit. We tested a 102-block legacy chain including block 102 with a real DER-signed P2PKH spend inside; the SegWit-active node accepted every one.

The direct demonstration: same 3× 0.12.1 setup. Node L1 sent its own 50-BTC coinbase into a P2WPKH-format output. L2, a different node with no key related to that output, built a spend from that output to L2's own P2PKH address — empty scriptSig, no signature — then hand-assembled a block including that spend and submitted it via submitblock. All three 0.12.1 nodes accepted the block. L2's wallet ended at 49.998 BTC. On the SegWit chain, the same 50 BTC still belongs to whoever holds the witness key. Two chains, two different owners of the same coin. L2 is not a bad actor — it is running honest software following its own consensus rules that classify P2WPKH outputs as anyone-can-spend. If SegWit were truly backward-compatible, honest legacy operation would produce the same chain state as SegWit operation. It does not.

The symmetry

It follows that legacy is not fundamentally insecure — legacy is enforcing its rules correctly. It is that SegWit is producing blocks whose validity depends on data — witness signatures — that pre-SegWit software has no place for. From the SegWit side, legacy blocks that spend anyone-can-spend outputs are invalid. From the legacy side, SegWit is producing outputs whose spends have no signatures where signatures would previously have been required, plus block-format bytes that pre-SegWit code was not written to interpret. Under the mainnet 2017 hashpower ratio (~95% miner signalling for SegWit), the SegWit chain outraced the legacy chain and exchanges and wallets stayed on the SegWit side. Reverse the ratio and the mirror scenario plays out: the legacy chain accumulates faster, includes legacy-format spends of segwit outputs (thefts by SegWit's rules, honest spends by legacy's rules), grows past the SegWit chain, and the market coordinates on that side instead. Bitcoin Cash is the empirical version of that mirror — BCH software never adopted BIP141/144, rejects SegWit blocks outright, and grows its own chain the market treats as a separate asset. Two chains, mutual rejection, both running software correct by its own consensus rules.

What "backward-compatible" actually did

Backward-compatible was doing five separate jobs at once. Testing them one at a time:

Read plainly: "backward-compatible" turned out to mean "old nodes can still keep running", not "old nodes can still verify Bitcoin". BIP144 stripping keeps them from crashing and lets them track the ledger; it does not preserve signature verification, and it does not prevent them from accidentally producing the one kind of block that forks the chain.

What this changes

Nothing about the counting rule. Both chains — the one that inherited the BTC ticker under the soft-fork classification, and the one that continued the pre-2017 block-size scaling path under BCH — remain independently counted cryptocurrencies with their own base-layer blockchains. What the experiment removes is one piece of ambiguity from how the 2017 event gets described.

One clarification on the "theft" framing that recurs above. The block that splits the chain — empty scriptSig spending a P2WPKH output — is not something legacy dishonestly does. It is what BIP144 stripping produces on the wire: when a SegWit-aware peer sends a witness-carrying transaction to a peer without NODE_WITNESS, the peer strips the signature out and sends what looks, structurally, like a stripped-signature exploit. Legacy's script interpreter reads what arrives, finds a script that does not demand a signature, and treats the spend as authorised — the correct behaviour of any script interpreter for that input. In any other context that byte pattern would be flagged as a bug or an exploit; here it is the protocol's normal output. The "theft" label is polite. The honest description is that SegWit's protocol delivers signatures-stripped transactions to any node without BIP141/144 code, and legacy nodes then do the correct thing with what they receive.

Which explains why the 2017 activation avoided a visible chain split on the SegWit side. Not because the mechanism was compatible — the tests above rule that out. Because coordinated pressure got the ecosystem onto one side. 95% miner signalling was forced under BIP148 UASF's orphan-your-block threat; exchanges and wallets stayed unified on the SegWit interpretation; miners signalled rather than see their non-signalling blocks orphaned. And the operators who wanted the pre-2017 rules with block-size scaling instead of SegWit organised their own coordinated protocol upgrade in parallel: Bitcoin Cash forked on 1 August 2017 — the BIP148 deadline day — implementing larger blocks and explicitly excluding the SegWit code change. Without that coordinated alternative, the operators who preferred the pre-SegWit rules would still have produced a chain split; it would just have been a messier, uncoordinated one. Bitcoin Cash organised what would have been a scattered refusal into a coherent alternative chain with its own protocol, its own ecosystem, its own ticker.

Coordination on both sides prevented an uncoordinated mess. It did not prevent the split. A soft fork does not need coercion to activate, and does not need a parallel protocol upgrade to absorb the operators who take a different path. SegWit required both. That is what a hard fork looks like, dressed in a soft-fork label.

Test artifacts. Main split experiment: 10 Bitcoin Core regtest nodes, full-mesh via addnode, six ratios plus wake-up sequence, 30 mining rounds per ratio. Bitcoin Core 26.0 with -testactivationheight=segwit@1 for the SegWit team; 0.13.2 unmodified for the legacy team. Targeted single-host tests use Bitcoin Core 0.12.1 (truly pre-SegWit, no witness code) with -acceptnonstdtxn=1, plus hand-implemented BIP144 stripping and hand-assembled submitblock paths for the anyone-can-spend, theft, and stripping tests. Scripts and raw CSVs reproducible; contact the site if you want the harness.

Related

SegWit: soft fork by classification, hard fork by function — the specification-side argument this experiment tests.

Bitcoin took two upgrade paths in 2017 — the two 2017 activations side by side.

Hard fork vs chain split — the mechanism behind both terms.