There is some debate around whether it’s preferable to make changes via soft or hard fork. Each technique has advantages and disadvantages.
When soft-forking in new bitcoin consensus rules it is important to consider how old nodes will interpret the new rules. For this reason the preferred method historically was to make something (e.g. an unused OPCODE which was to be repurposed) "non-standard" prior to the upgrade. Making the opcode non-standard has the effect that transaction scripts using it will not be relayed by nodes using this policy. Once the soft fork is activated policy is amended to make relaying transactions using this opcode standard policy again, so long as they comply with the ruleset of the soft fork.
Using the analogy above, we could think of OP_NOP opcodes as unsculpted areas of marble.
|Currently OP_NOP1 and OP_NOP4-NOP_NOP10 remain available for this.
Once the opcode has been made non-standard we can then sculpt the new rule from the marble and later re-standardize transactions using the opcode so long as they follow the new rule.
This makes sense from the perspective of an old, un-upgraded node who we are trying to remain in consensus with. From their perspective they see an OP_NOP performing (like the name implies) nothing, but not marking the transaction as invalid. After the soft fork they will still see the (repurposed) OP_NOP apparently doing nothing but also not failing the transaction.
However from the perspective of the upgraded node they now have two possible evaluation paths for the OP_NOP: 1) Do nothing (for the success case) and 2) Fail evaluation (for the failure case). This is summarized in the table below.
|Before soft fork
|After soft fork
1) Nothing (soft forked rule evaluation success)
You may notice here that there is still room for discrepancy; a miner who is not upgraded could possibly include transactions in a block which were valid according to legacy nodes, but invalid according to upgraded nodes. If this miner had any significant hashpower this would be enough to initiate a chainsplit, as upgraded miners would not follow this tip.
Repurposing OP_NOPs does have its limitations. First and foremost they cannot manipulate the stack, as this is something that un-upgraded nodes would not expect or validate identically. Getting rid of the OP_DROP requirement when using repurposed OP_NOPs would require a hard fork.
Examples of soft forks which re-purposed OP_NOPs include CLTV and CSV. Ideally these operations would remove the subsequent object from the stack when they had finished processing it, so you will often see them followed by OP_DROP which removes the object, for example in the script used for the
to_local output in a lightning commitment transaction:
# Penalty transaction
There are other limitations associated with repurposing OP_NOPs, and ideally bitcoin needed a better upgrade system…
SegWit was the first attempt to go beyond simply repurposing OP_NOPs for upgrades. The idea was that the
redeemScript would consist of a 1 byte push opcode (0-16) followed by a data push between 2 and 40 bytes. The value of the first push would represent the version number, and the second push the witness program. If the conditions to interpret this as a SegWit script were matched, then this would be followed by a
witness, whose data varied on whether this was a P2WPKH or P2WSH witness program.
Legacy nodes, who would not have the witness data, would interpret this output as
anyonecanspend and so would be happy to validate it, whereas upgraded nodes could validate it using the additional
witness against the new rules. To revert to the statue analogy this gave us the ability to work with a new area of the marble which was entirely untouched.
The addition of a versioning scheme to SegWit was a relatively late addition which stemmed from noticing that, due to the CLEANSTACK policy rule which required exactly 1 true element to remain on the stack after execution, SegWit outputs would be of the form
OP_N + DATA. With SegWit we wanted a compact way of creating a new output which didn’t have any consensus rules associated with it, yet had lots of freedom, was ideally already non-standard, and was permitted by CLEANSTACK.
The solution was to use two pushes: according to old nodes there are two elements, which was non-standard. The first push must be at least one byte, so we can use one of the
OP_N opcodes, which we then interpret as the SegWit version. The second is the data we have to push.
Whilst this immediately gave us new upgrade paths via SegWit versions Taproot (SegWit version 1) went a step further and declared new opcodes inside of SegWit, also evaluated as
anyonecanspend by nodes that don’t support SegWit, giving us yet more soft fork upgradability. These opcodes could in theory be used for anything, for example if there was ever a need to have a new consensus rule on 64 bit numbers we could use one of these opcodes.