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.
| ratio | SegWit team | legacy team | distinct chains |
|---|---|---|---|
| 90/10 (9 vs 1, all mine) | height 280 | height 30 | 2 |
| 70/30 (7 vs 3, all mine) | height 220 | height 72 | 2 |
| 50/50 (5 vs 5, all mine) | height 160 | height 104 | 2 |
| 30/70 (3 vs 7, all mine) | height 100 | height 156 | 2 |
| 70/30 with listeners | 4m+3L, height 130 | 2m+1L, height 45 | 2 |
| 30/70 with listeners | 2m+1L, height 70 | 5m+2L, height 98 | 2 |
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:
- SegWit-aware node originating a legacy P2PKH transaction: works. Byte-identical to what pre-SegWit expects — 26.0's legacy tx hex parsed successfully by 0.12.1 with a matching structure and DER signature.
- Pre-SegWit node accepting SegWit blocks inside stripped form: works at consensus. 102 blocks fed through hand-implemented BIP144 stripping, all accepted.
- Pre-SegWit node accepting SegWit txs on relay: does not work. Mempool policy rejects (CLEANSTACK). SegWit spends reach pre-SegWit nodes only inside blocks.
- Pre-SegWit node verifying signatures on SegWit-era transactions:
does not happen. The block-level acceptance is anyone-can-spend from the pre-SegWit
interpreter; no
OP_CHECKSIGever runs on those inputs. - Legacy miner producing a block the SegWit chain accepts: works — including with real signed P2PKH transactions inside — but breaks the moment the block contains a spend of a segwit-format UTXO.
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.