1
0
mirror of https://github.com/bitcoin/bips.git synced 2026-06-29 17:39:57 +00:00

Merge pull request #2201 from dathonohm/bip110-complete

BIP-110: advance to Complete status
This commit is contained in:
Jon Atack
2026-06-28 13:34:28 -07:00
committed by GitHub
4 changed files with 767 additions and 5 deletions

View File

@@ -631,13 +631,13 @@ users (see also: [https://en.bitcoin.it/wiki/Economic_majority economic majority
| Gavin Andresen | Gavin Andresen
| Specification | Specification
| Closed | Closed
|- |- style="background-color: #ffffcf"
| [[bip-0110.mediawiki|110]] | [[bip-0110.mediawiki|110]]
| Consensus (soft fork) | Consensus (soft fork)
| Reduced Data Temporary Softfork | Reduced Data Temporary Softfork
| Dathon Ohm | Dathon Ohm
| Specification | Specification
| Draft | Complete
|- style="background-color: #cfffcf" |- style="background-color: #cfffcf"
| [[bip-0111.mediawiki|111]] | [[bip-0111.mediawiki|111]]
| Peer Services | Peer Services

View File

@@ -3,7 +3,7 @@
Layer: Consensus (soft fork) Layer: Consensus (soft fork)
Title: Reduced Data Temporary Softfork Title: Reduced Data Temporary Softfork
Authors: Dathon Ohm <dathonohm+bip@proton.me> Authors: Dathon Ohm <dathonohm+bip@proton.me>
Status: Draft Status: Complete
Type: Specification Type: Specification
Assigned: 2025-12-03 Assigned: 2025-12-03
License: BSD-3-Clause License: BSD-3-Clause
@@ -30,7 +30,11 @@ Blocks during a temporary, one-year deployment are checked with these additional
# Tapscripts including OP_SUCCESS* opcodes anywhere (even unexecuted) are invalid. # Tapscripts including OP_SUCCESS* opcodes anywhere (even unexecuted) are invalid.
# Tapscripts executing the OP_IF or OP_NOTIF instruction (regardless of result) are invalid. # Tapscripts executing the OP_IF or OP_NOTIF instruction (regardless of result) are invalid.
Inputs spending UTXOs that were created before the activation height are exempt from the new rules. ===UTXO grandfathering===
'''Inputs spending UTXOs that were created before the activation height are exempt from all of the new rules.'''
This grandfathering ensures that no existing coins can be frozen or rendered unspendable by this softfork: any UTXO confirmed before activation can always be spent exactly as it could before, throughout the entire deployment.
The new rules apply only to UTXOs created at or after the activation height.
Once the softfork expires, UTXOs of all heights are once again unrestricted. Once the softfork expires, UTXOs of all heights are once again unrestricted.
===GetBlockTemplate=== ===GetBlockTemplate===
@@ -177,7 +181,7 @@ Yes:
# Limiting Taproot control blocks to 257 bytes directly constrains the size of the on-chain, consensus-enforced script tree. This could complicate or possibly even impede advanced smart contracting like BitVM, which relies on a large number of executable scripts. In the worst case scenario, these use cases may just need to wait until this softfork expires. As they are still in early development, testnet and sidechains should be sufficient for the next year while a more scalable rule is implemented. # Limiting Taproot control blocks to 257 bytes directly constrains the size of the on-chain, consensus-enforced script tree. This could complicate or possibly even impede advanced smart contracting like BitVM, which relies on a large number of executable scripts. In the worst case scenario, these use cases may just need to wait until this softfork expires. As they are still in early development, testnet and sidechains should be sufficient for the next year while a more scalable rule is implemented.
# Upgrade hooks are not available for other softforks. As softforks adding new opcodes typically need at least a year to activate, this shouldn't be a practical issue. # Upgrade hooks are not available for other softforks. As softforks adding new opcodes typically need at least a year to activate, this shouldn't be a practical issue.
# Some wallet software such as Miniscript habitually creates Tapleaves containing OP_IF. To mitigate the risk of these funds being frozen for a year, this proposal exempts inputs that spend outputs that were created before activation, and provides a two-week grace period between lock-in and activation, to give users time to prepare. # Despite there being no known use case that requires it, some wallet software such as Miniscript habitually creates Tapleaves containing OP_IF, which in some hypothetical cases can require less data than the equivalent construction using additional Tapscripts. If such a use case is discovered, this simply increases the fee for that construction until the softfork expires.
'''Isn't the limit on Taproot control blocks too restrictive?''' '''Isn't the limit on Taproot control blocks too restrictive?'''
@@ -314,6 +318,15 @@ All other known use cases are not affected.
https://github.com/bitcoinknots/bitcoin/compare/29.x-knots...dathonohm:bitcoin:uasf-modified-bip9 https://github.com/bitcoinknots/bitcoin/compare/29.x-knots...dathonohm:bitcoin:uasf-modified-bip9
==Test vectors==
Test coverage specific to these rules is provided by the reference implementation's functional test suite:
* [https://github.com/dathonohm/bitcoin/blob/uasf-modified-bip9/test/functional/feature_rdts.py feature_rdts.py] exercises all seven consensus rules, including their boundaries and the script-type exemptions to rule 2 (BIP16 redeemScripts, witness scripts, and Tapleaf scripts).
* [https://github.com/dathonohm/bitcoin/blob/uasf-modified-bip9/test/functional/feature_reduced_data_utxo_height.py feature_reduced_data_utxo_height.py] covers the grandfathering of UTXOs created before the activation height.
In addition, a set of transaction-level test vectors generated and verified against the reference implementation is provided in [[bip-0110/test-vectors.json|test-vectors.json]]. Each vector gives the spent output(s), the spending (or output-creating) transaction, and whether a block containing it is valid while these rules are active; the <code>rule</code> and <code>spent_utxo</code> fields identify which numbered rule is exercised and whether the spent UTXO is grandfathered. These vectors were produced by [[bip-0110/test-vectors.py|test-vectors.py]], which submits each case to a regtest node enforcing the rules and emits the verified result; see that file for reproduction instructions.
==Deployment== ==Deployment==
This deployment uses a modified version of BIP9 with the following parameters: This deployment uses a modified version of BIP9 with the following parameters:
@@ -351,3 +364,19 @@ The FAILED state is never reached because timeout is disabled. EXPIRED is the fi
==Credits== ==Credits==
Original draft and advice: Luke-Jr Original draft and advice: Luke-Jr
==Changelog==
* '''1.0.0''' (2026-06-25):
** Advance to Complete
* '''0.0.4''' (2026-02-04):
** Update to BIP-3
** Add EXPIRED state
* '''0.0.3''' (2025-11-25):
** Switch to a modified BIP9 deployment with active_duration, max_activation_height, and a 55% threshold
** Move the activation deadline block height from 934864 (2026-02-01) to 965664 (2026-09-01)
* '''0.0.2''' (2025-11-05):
** Add UTXO grandfathering
** Remove the reactive activation method
* '''0.0.1''' (2025-10-24):
** Initial Draft

325
bip-0110/test-vectors.json Normal file
View File

@@ -0,0 +1,325 @@
{
"comment": "BIP-110 (REDUCED_DATA) consensus test vectors, generated and verified against the reference implementation. Each vector gives the spent output(s), the spending/creating transaction, and whether a block containing it is valid while REDUCED_DATA is active. 'rule' refers to the numbered rules in the BIP Specification. Hex values are byte arrays. 'reject_reason' is informational and normalized to be implementation-agnostic.",
"vectors": [
{
"rule": 1,
"name": "spk_p2wsh_34_valid",
"description": "Create a 34-byte P2WSH output (boundary)",
"reduced_data_active": true,
"spent_utxo": "post-activation",
"spent_outputs": [
{
"prevout": {
"txid": "1100000000000000000000000000000000000000000000000000000000000001",
"vout": 0
},
"scriptPubKey": "00204ae81572f06e1b88fd5ced7a1a000945432e83e1551e6f721ee9c00b8cc33260",
"amount": 100000
}
],
"tx": "02000000000101010000000000000000000000000000000000000000000000000000000000001100000000000000000001b8820100000000002200204ae81572f06e1b88fd5ced7a1a000945432e83e1551e6f721ee9c00b8cc3326001015100000000",
"expected": "valid",
"reject_reason": null
},
{
"rule": 1,
"name": "opreturn_83_valid",
"description": "Create an 83-byte OP_RETURN output (boundary)",
"reduced_data_active": true,
"spent_utxo": "post-activation",
"spent_outputs": [
{
"prevout": {
"txid": "1100000000000000000000000000000000000000000000000000000000000002",
"vout": 0
},
"scriptPubKey": "00204ae81572f06e1b88fd5ced7a1a000945432e83e1551e6f721ee9c00b8cc33260",
"amount": 100000
}
],
"tx": "02000000000101020000000000000000000000000000000000000000000000000000000000001100000000000000000001b882010000000000536a4c50000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001015100000000",
"expected": "valid",
"reject_reason": null
},
{
"rule": 1,
"name": "spk_35_nonopreturn_invalid",
"description": "Create a 35-byte non-OP_RETURN output (>34)",
"reduced_data_active": true,
"spent_utxo": "post-activation",
"spent_outputs": [
{
"prevout": {
"txid": "1100000000000000000000000000000000000000000000000000000000000003",
"vout": 0
},
"scriptPubKey": "00204ae81572f06e1b88fd5ced7a1a000945432e83e1551e6f721ee9c00b8cc33260",
"amount": 100000
}
],
"tx": "02000000000101030000000000000000000000000000000000000000000000000000000000001100000000000000000001b88201000000000023510000000000000000000000000000000000000000000000000000000000000000000001015100000000",
"expected": "invalid",
"reject_reason": "bad-txns-vout-script-toolarge"
},
{
"rule": 1,
"name": "opreturn_84_invalid",
"description": "Create an 84-byte OP_RETURN output (>83)",
"reduced_data_active": true,
"spent_utxo": "post-activation",
"spent_outputs": [
{
"prevout": {
"txid": "1100000000000000000000000000000000000000000000000000000000000004",
"vout": 0
},
"scriptPubKey": "00204ae81572f06e1b88fd5ced7a1a000945432e83e1551e6f721ee9c00b8cc33260",
"amount": 100000
}
],
"tx": "02000000000101040000000000000000000000000000000000000000000000000000000000001100000000000000000001b882010000000000546a4c5100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001015100000000",
"expected": "invalid",
"reject_reason": "bad-txns-vout-script-toolarge"
},
{
"rule": 2,
"name": "witness_item_256_valid",
"description": "Spend with a 256-byte witness item (boundary)",
"reduced_data_active": true,
"spent_utxo": "post-activation",
"spent_outputs": [
{
"prevout": {
"txid": "1100000000000000000000000000000000000000000000000000000000000005",
"vout": 0
},
"scriptPubKey": "002033198a9bfef674ebddb9ffaa52928017b8472791e54c609cb95f278ac6b1e349",
"amount": 100000
}
],
"tx": "02000000000101050000000000000000000000000000000000000000000000000000000000001100000000000000000001b882010000000000015102fd00014242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424202755100000000",
"expected": "valid",
"reject_reason": null
},
{
"rule": 2,
"name": "witness_item_257_invalid",
"description": "Spend with a 257-byte witness item (>256)",
"reduced_data_active": true,
"spent_utxo": "post-activation",
"spent_outputs": [
{
"prevout": {
"txid": "1100000000000000000000000000000000000000000000000000000000000006",
"vout": 0
},
"scriptPubKey": "002033198a9bfef674ebddb9ffaa52928017b8472791e54c609cb95f278ac6b1e349",
"amount": 100000
}
],
"tx": "02000000000101060000000000000000000000000000000000000000000000000000000000001100000000000000000001b882010000000000015102fd0101424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424202755100000000",
"expected": "invalid",
"reject_reason": "script-verify-flag-failed (Push value size limit exceeded)"
},
{
"rule": 3,
"name": "spend_witness_v2_invalid",
"description": "Spend an undefined witness v2 output",
"reduced_data_active": true,
"spent_utxo": "post-activation",
"spent_outputs": [
{
"prevout": {
"txid": "1100000000000000000000000000000000000000000000000000000000000007",
"vout": 0
},
"scriptPubKey": "52204242424242424242424242424242424242424242424242424242424242424242",
"amount": 100000
}
],
"tx": "02000000000101070000000000000000000000000000000000000000000000000000000000001100000000000000000001b8820100000000000151010000000000",
"expected": "invalid",
"reject_reason": "script-verify-flag-failed (Witness version reserved for soft-fork upgrades)"
},
{
"rule": 3,
"name": "spend_tapleaf_v0xc2_invalid",
"description": "Spend an undefined (0xc2) Tapleaf version",
"reduced_data_active": true,
"spent_utxo": "post-activation",
"spent_outputs": [
{
"prevout": {
"txid": "1100000000000000000000000000000000000000000000000000000000000008",
"vout": 0
},
"scriptPubKey": "5120c0a7172b356abde7777c6eb0c55064226088bfc3468e9f2870aae0a1ff6432ee",
"amount": 100000
}
],
"tx": "02000000000101080000000000000000000000000000000000000000000000000000000000001100000000000000000001b882010000000000015102015121c2f9308a019258c31049344f85f89d5229b531c845836f99b08601f113bce036f900000000",
"expected": "invalid",
"reject_reason": "script-verify-flag-failed (Taproot version reserved for soft-fork upgrades)"
},
{
"rule": 4,
"name": "taproot_annex_invalid",
"description": "Spend a taproot script path with an annex",
"reduced_data_active": true,
"spent_utxo": "post-activation",
"spent_outputs": [
{
"prevout": {
"txid": "1100000000000000000000000000000000000000000000000000000000000009",
"vout": 0
},
"scriptPubKey": "5120c45cdd4e172fd7224c8b9cccceea1500216f67139c6e28775a32522f3d8c12d6",
"amount": 100000
}
],
"tx": "02000000000101090000000000000000000000000000000000000000000000000000000000001100000000000000000001b882010000000000015103015121c1f9308a019258c31049344f85f89d5229b531c845836f99b08601f113bce036f90b500000000000000000000000000000",
"expected": "invalid",
"reject_reason": "script-verify-flag-failed (Push value size limit exceeded)"
},
{
"rule": 5,
"name": "control_block_257_valid",
"description": "Spend a leaf at depth 7 (257-byte control block, boundary)",
"reduced_data_active": true,
"spent_utxo": "post-activation",
"spent_outputs": [
{
"prevout": {
"txid": "110000000000000000000000000000000000000000000000000000000000000a",
"vout": 0
},
"scriptPubKey": "5120a1170fa84d302d475a11b32bbfa38ea9ae7085d301539c1b7a90be9934551d3b",
"amount": 100000
}
],
"tx": "020000000001010a0000000000000000000000000000000000000000000000000000000000001100000000000000000001b8820100000000000151020151fd0101c1f9308a019258c31049344f85f89d5229b531c845836f99b08601f113bce036f9808080808080808080808080808080808080808080808080808080808080808081818181818181818181818181818181818181818181818181818181818181818282828282828282828282828282828282828282828282828282828282828282838383838383838383838383838383838383838383838383838383838383838384848484848484848484848484848484848484848484848484848484848484848585858585858585858585858585858585858585858585858585858585858585868686868686868686868686868686868686868686868686868686868686868600000000",
"expected": "valid",
"reject_reason": null
},
{
"rule": 5,
"name": "control_block_289_invalid",
"description": "Spend a leaf at depth 8 (289-byte control block, >257)",
"reduced_data_active": true,
"spent_utxo": "post-activation",
"spent_outputs": [
{
"prevout": {
"txid": "110000000000000000000000000000000000000000000000000000000000000b",
"vout": 0
},
"scriptPubKey": "51205620d1afc09839bda01ad3cda772c25db3917ca7364b9a8d0554786af71d7a79",
"amount": 100000
}
],
"tx": "020000000001010b0000000000000000000000000000000000000000000000000000000000001100000000000000000001b8820100000000000151020151fd2101c1f9308a019258c31049344f85f89d5229b531c845836f99b08601f113bce036f98080808080808080808080808080808080808080808080808080808080808080818181818181818181818181818181818181818181818181818181818181818182828282828282828282828282828282828282828282828282828282828282828383838383838383838383838383838383838383838383838383838383838383848484848484848484848484848484848484848484848484848484848484848485858585858585858585858585858585858585858585858585858585858585858686868686868686868686868686868686868686868686868686868686868686878787878787878787878787878787878787878787878787878787878787878700000000",
"expected": "invalid",
"reject_reason": "script-verify-flag-failed (Invalid Taproot control block size)"
},
{
"rule": 6,
"name": "op_success_invalid",
"description": "Spend a tapscript containing OP_SUCCESS254 (0xfe)",
"reduced_data_active": true,
"spent_utxo": "post-activation",
"spent_outputs": [
{
"prevout": {
"txid": "110000000000000000000000000000000000000000000000000000000000000c",
"vout": 0
},
"scriptPubKey": "512007b2f589f96cc9316ba65294a1f18aa7b057945894767f3bed0c44cf0ab8686f",
"amount": 100000
}
],
"tx": "020000000001010c0000000000000000000000000000000000000000000000000000000000001100000000000000000001b88201000000000001510201fe21c0f9308a019258c31049344f85f89d5229b531c845836f99b08601f113bce036f900000000",
"expected": "invalid",
"reject_reason": "script-verify-flag-failed (OP_SUCCESSx reserved for soft-fork upgrades)"
},
{
"rule": 7,
"name": "tapscript_op_if_invalid",
"description": "Spend a tapscript executing OP_IF",
"reduced_data_active": true,
"spent_utxo": "post-activation",
"spent_outputs": [
{
"prevout": {
"txid": "110000000000000000000000000000000000000000000000000000000000000d",
"vout": 0
},
"scriptPubKey": "5120e73b13739e3c9cc5a3f3147ed8baeda794fe388372fb2c1e9115990e8381b1c6",
"amount": 100000
}
],
"tx": "020000000001010d0000000000000000000000000000000000000000000000000000000000001100000000000000000001b882010000000000015102045163516821c0f9308a019258c31049344f85f89d5229b531c845836f99b08601f113bce036f900000000",
"expected": "invalid",
"reject_reason": "script-verify-flag-failed (OP_IF/NOTIF argument must be minimal in tapscript)"
},
{
"rule": 7,
"name": "witness_v0_op_if_valid",
"description": "OP_IF in a witness v0 script is valid (rule is tapscript-only)",
"reduced_data_active": true,
"spent_utxo": "post-activation",
"spent_outputs": [
{
"prevout": {
"txid": "110000000000000000000000000000000000000000000000000000000000000e",
"vout": 0
},
"scriptPubKey": "0020d7b825146d3d522e489fa9eea62fe3158d776e7fa72f49cf7fd95710cb9fc82f",
"amount": 100000
}
],
"tx": "020000000001010e0000000000000000000000000000000000000000000000000000000000001100000000000000000001b882010000000000015101045163516800000000",
"expected": "valid",
"reject_reason": null
},
{
"rule": 2,
"name": "grandfathered_witness_item_257_valid",
"description": "Spend a PRE-activation UTXO with a 257-byte witness item (grandfathered, exempt)",
"reduced_data_active": true,
"spent_utxo": "pre-activation",
"spent_outputs": [
{
"prevout": {
"txid": "110000000000000000000000000000000000000000000000000000000000000f",
"vout": 0
},
"scriptPubKey": "002033198a9bfef674ebddb9ffaa52928017b8472791e54c609cb95f278ac6b1e349",
"amount": 100000
}
],
"tx": "020000000001010f0000000000000000000000000000000000000000000000000000000000001100000000000000000001b882010000000000015102fd0101424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424202755100000000",
"expected": "valid",
"reject_reason": null
},
{
"rule": 1,
"name": "grandfathered_input_oversized_output_invalid",
"description": "Spend a PRE-activation UTXO but create a 35-byte output (rule 1 not grandfathered)",
"reduced_data_active": true,
"spent_utxo": "pre-activation",
"spent_outputs": [
{
"prevout": {
"txid": "1100000000000000000000000000000000000000000000000000000000000010",
"vout": 0
},
"scriptPubKey": "00204ae81572f06e1b88fd5ced7a1a000945432e83e1551e6f721ee9c00b8cc33260",
"amount": 100000
}
],
"tx": "02000000000101100000000000000000000000000000000000000000000000000000000000001100000000000000000001b88201000000000023510000000000000000000000000000000000000000000000000000000000000000000001015100000000",
"expected": "invalid",
"reject_reason": "bad-txns-vout-script-toolarge"
}
]
}

408
bip-0110/test-vectors.py Executable file
View File

@@ -0,0 +1,408 @@
#!/usr/bin/env python3
# Copyright (c) 2026 The Bitcoin Knots developers
# Distributed under the MIT software license, see the accompanying
# file COPYING or http://www.opensource.org/licenses/mit-license.php.
"""Generate and verify BIP-110 (REDUCED_DATA) consensus test vectors.
For each BIP-110 rule this test constructs a transaction (or output) that
exercises the rule's boundary, submits it inside a block to a regtest node
that is enforcing REDUCED_DATA, and asserts the node accepts/rejects it as
the BIP specifies. Verified cases are written to a portable JSON file so the
exact same vectors can be replayed against any other implementation.
Coverage:
Rule 1: output scriptPubKey size (<=34, or OP_RETURN <=83) [per-output]
Rule 2: push / script-argument witness item size (<=256)
Rule 3: spending undefined witness / Tapleaf versions
Rule 4: Taproot annex
Rule 5: Taproot control block size (<=257)
Rule 6: OP_SUCCESS* in tapscript
Rule 7: OP_IF / OP_NOTIF in tapscript
Grandfathering: inputs spending pre-activation UTXOs are exempt from the
per-input script rules (2-7); the per-output rule (1) still
applies to outputs created while active.
This is a Bitcoin Core / Knots functional test; it needs the test framework and
a built bitcoind, so it is not run from the bips repository. To regenerate and
verify test-vectors.json against the reference implementation:
1. Check out the reference implementation
(https://github.com/dathonohm/bitcoin, branch uasf-modified-bip9) and copy
this file into its test/functional/ directory.
2. Configure and build (this also symlinks the test into the build tree):
cmake -B build && cmake --build build -j"$(nproc)"
3. Run it from the build tree (which auto-discovers the test config):
TEST_VECTOR_OUT=/path/to/test-vectors.json python3 build/test/functional/test-vectors.py
The generator is deterministic, so a successful run reproduces test-vectors.json
byte-for-byte while re-verifying every case against the node.
"""
import json
import os
from io import BytesIO
from test_framework.blocktools import (
COINBASE_MATURITY,
create_block,
create_coinbase,
add_witness_commitment,
)
from test_framework.messages import (
COutPoint,
CTransaction,
CTxIn,
CTxInWitness,
CTxOut,
)
from test_framework.script import (
CScript,
OP_1,
OP_2,
OP_DROP,
OP_ENDIF,
OP_IF,
OP_RETURN,
OP_TRUE,
taproot_construct,
)
from test_framework.script_util import script_to_p2wsh_script
from test_framework.key import generate_privkey, compute_xonly_pubkey
from test_framework.test_framework import BitcoinTestFramework
from test_framework.util import assert_equal
from test_framework.wallet import MiniWallet, MiniWalletMode
REDUCED_DATA_BIT = 4
VERSIONBITS_TOP_BITS = 0x20000000
ACTIVATION_HEIGHT = 432 # with min_activation_height=288 (period 3 start)
FEE = 1000
FUND_AMOUNT = 100000
def block_hash(block):
"""Block hash hex, portable across test-framework versions."""
h = getattr(block, 'hash_hex', None)
return h if h is not None else block.hash
def normalize_reject(res):
"""Make the rejection reason implementation-agnostic.
A REDUCED_DATA script-verification failure is reported as
"mandatory-script-verify-flag-failed (...)" by Bitcoin Knots and as
"block-script-verify-flag-failed (...)" by the Bitcoin Core port; the
parenthetical detail is identical. Canonicalize the differing prefix so the
vectors reproduce identically against either implementation.
"""
if res is None:
return None
for prefix in ("mandatory-script-verify-flag-failed",
"block-script-verify-flag-failed"):
if res.startswith(prefix):
return "script-verify-flag-failed" + res[len(prefix):]
return res
def cb(version, info, leaf):
"""Control block for a taproot leaf."""
return bytes([leaf.version + info.negflag]) + info.internal_pubkey + leaf.merklebranch
def deepen(item, depth):
"""Wrap a taproot tree item under `depth` fictitious partner branches so the
target leaf ends up at the given Merkle depth (control block = 33 + 32*depth)."""
node = item
for i in range(depth):
partner = bytes([0x80 + i]) * 32
node = [node, (lambda h, p=partner: p)]
return node
class ReducedDataTestVectors(BitcoinTestFramework):
def set_test_params(self):
self.num_nodes = 1
self.setup_clean_chain = True
# start=0, timeout=never, min_activation_height=288, max disabled, active_duration permanent
self.extra_args = [[
'-vbparams=reduced_data:0:999999999999:288:2147483647:2147483647',
'-acceptnonstdtxn=1',
]]
# ---- block helpers -------------------------------------------------------
def tip_block(self, txs, signal=False):
node = self.nodes[0]
tip = node.getbestblockhash()
height = node.getblockcount() + 1
block_time = node.getblockheader(tip)['time'] + 1
block = create_block(int(tip, 16), create_coinbase(height), ntime=block_time, txlist=txs)
if signal:
block.nVersion = VERSIONBITS_TOP_BITS | (1 << REDUCED_DATA_BIT)
add_witness_commitment(block)
block.solve()
return block
def mine(self, count, signal=False):
node = self.nodes[0]
for _ in range(count):
block = self.tip_block([], signal=signal)
res = node.submitblock(block.serialize().hex())
assert res is None, f"submitblock failed: {res}"
assert_equal(node.getbestblockhash(), block_hash(block))
def rd_status(self):
return self.nodes[0].getdeploymentinfo()['deployments']['reduced_data']['bip9']
def activate(self):
# Mine until ACTIVE, signaling on every block while STARTED so the
# threshold is met. Activation height is captured dynamically.
node = self.nodes[0]
while True:
info = self.rd_status()
if info['status'] == 'active':
break
self.mine(1, signal=(info['status'] == 'started'))
self.activation_height = self.rd_status()['since']
assert node.getblockcount() >= self.activation_height
self.log.info(f"REDUCED_DATA active since height {self.activation_height}")
# ---- funding -------------------------------------------------------------
def fund(self, spk):
"""Create and confirm an output paying `spk`; return (COutPoint, amount, height)."""
node = self.nodes[0]
sent = self.wallet.send_to(from_node=node, scriptPubKey=spk, amount=FUND_AMOUNT)
self.generate(self.wallet, 1)
txid_int = int(sent['txid'], 16)
return COutPoint(txid_int, sent['sent_vout']), FUND_AMOUNT, node.getblockcount()
def spend(self, outpoint, amount, witness_stack, outputs=None, scriptsig=b''):
tx = CTransaction()
tx.vin = [CTxIn(outpoint, scriptsig)]
if outputs is None:
outputs = [CTxOut(amount - FEE, CScript([OP_TRUE]))]
tx.vout = outputs
tx.wit.vtxinwit.append(CTxInWitness())
tx.wit.vtxinwit[0].scriptWitness.stack = witness_stack
return tx
# ---- vector recording ----------------------------------------------------
def check(self, rule, name, description, spent, tx, expect_valid, spent_age):
"""Submit `tx` in a block, assert accept/reject, record the vector.
The transaction is validated through the node using its real funding
outpoint, then the recorded copy has its input(s) rewritten to a
deterministic synthetic outpoint so the vector is self-contained and
reproducible. None of these spends carry signatures committing to the
prevout, so the rewrite does not change script validity.
"""
node = self.nodes[0]
block = self.tip_block([tx], signal=False)
res = node.submitblock(block.serialize().hex())
accepted = res is None
if expect_valid:
assert accepted, f"[{name}] expected VALID, node rejected: {res}"
assert_equal(node.getbestblockhash(), block_hash(block))
else:
assert not accepted, f"[{name}] expected INVALID, node accepted"
self.log.info(f" rule {rule} [{name}] -> {'valid' if accepted else 'invalid'} "
f"({'reject: ' + str(res) if res else 'accepted'})")
# Rewrite to synthetic, deterministic outpoints for the recorded vector.
spent_outputs = []
for j, (spk, amount) in enumerate(spent):
self._outpoint_counter += 1
synth = self._outpoint_counter
tx.vin[j].prevout = COutPoint(synth, 0)
spent_outputs.append({
"prevout": {"txid": "%064x" % synth, "vout": 0},
"scriptPubKey": spk.hex(),
"amount": amount,
})
self.vectors.append({
"rule": rule,
"name": name,
"description": description,
"reduced_data_active": True,
"spent_utxo": spent_age,
"spent_outputs": spent_outputs,
"tx": tx.serialize().hex(),
"expected": "valid" if expect_valid else "invalid",
"reject_reason": normalize_reject(res),
})
return accepted
# ---- the test ------------------------------------------------------------
def run_test(self):
node = self.nodes[0]
self.vectors = []
self._outpoint_counter = 0x1100000000000000000000000000000000000000000000000000000000000000
self.wallet = MiniWallet(node, mode=MiniWalletMode.RAW_OP_TRUE)
self.generate(self.wallet, COINBASE_MATURITY)
# Fixed internal key for taproot constructions (deterministic vectors)
sec = bytes.fromhex("0000000000000000000000000000000000000000000000000000000000000003")
pub = compute_xonly_pubkey(sec)[0]
# P2WSH(OP_TRUE) anyone-can-spend, used for output-size (rule 1) cases
wsh_true = CScript([OP_TRUE])
wsh_true_spk = script_to_p2wsh_script(wsh_true)
# P2WSH(OP_DROP OP_TRUE) lets us inject an arbitrary witness data element (rule 2)
wsh_drop = CScript([OP_DROP, OP_TRUE])
wsh_drop_spk = script_to_p2wsh_script(wsh_drop)
# Fund "old" UTXOs BEFORE activation so they are grandfathered (exempt
# from the per-input script rules). Done first, at a low height.
self.log.info("Funding pre-activation (grandfathered) UTXOs...")
old_drop_o, old_drop_a, old_drop_h = self.fund(wsh_drop_spk)
old_true_o, old_true_a, old_true_h = self.fund(wsh_true_spk)
self.log.info("Activating REDUCED_DATA...")
self.activate()
assert old_drop_h < self.activation_height
assert old_true_h < self.activation_height
self.generate(self.wallet, 20) # spendable coins for funding "new" UTXOs
self.log.info("=== Rule 1: output scriptPubKey size (per-output) ===")
# boundary-valid: P2WSH (34 bytes) and 83-byte OP_RETURN
op = self.fund(wsh_true_spk)[0:2]; o, a = op
self.check(1, "spk_p2wsh_34_valid", "Create a 34-byte P2WSH output (boundary)",
[(wsh_true_spk, a)], self.spend(o, a, [bytes(wsh_true)],
outputs=[CTxOut(a - FEE, wsh_true_spk)]), True, "post-activation")
o, a = self.fund(wsh_true_spk)[0:2]
opret83 = bytes([OP_RETURN, 0x4c, 0x50]) + b'\x00' * 80
self.check(1, "opreturn_83_valid", "Create an 83-byte OP_RETURN output (boundary)",
[(wsh_true_spk, a)], self.spend(o, a, [bytes(wsh_true)],
outputs=[CTxOut(a - FEE, CScript(opret83))]), True, "post-activation")
# invalid: 35-byte non-OP_RETURN spk, and 84-byte OP_RETURN
o, a = self.fund(wsh_true_spk)[0:2]
spk35 = CScript(bytes([OP_1]) + b'\x00' * 34)
self.check(1, "spk_35_nonopreturn_invalid", "Create a 35-byte non-OP_RETURN output (>34)",
[(wsh_true_spk, a)], self.spend(o, a, [bytes(wsh_true)],
outputs=[CTxOut(a - FEE, spk35)]), False, "post-activation")
o, a = self.fund(wsh_true_spk)[0:2]
opret84 = bytes([OP_RETURN, 0x4c, 0x51]) + b'\x00' * 81
self.check(1, "opreturn_84_invalid", "Create an 84-byte OP_RETURN output (>83)",
[(wsh_true_spk, a)], self.spend(o, a, [bytes(wsh_true)],
outputs=[CTxOut(a - FEE, CScript(opret84))]), False, "post-activation")
self.log.info("=== Rule 2: script-argument witness item size ===")
o, a = self.fund(wsh_drop_spk)[0:2]
self.check(2, "witness_item_256_valid", "Spend with a 256-byte witness item (boundary)",
[(wsh_drop_spk, a)], self.spend(o, a, [b'\x42' * 256, bytes(wsh_drop)]),
True, "post-activation")
o, a = self.fund(wsh_drop_spk)[0:2]
self.check(2, "witness_item_257_invalid", "Spend with a 257-byte witness item (>256)",
[(wsh_drop_spk, a)], self.spend(o, a, [b'\x42' * 257, bytes(wsh_drop)]),
False, "post-activation")
self.log.info("=== Rule 3: undefined witness / Tapleaf versions ===")
# Undefined witness version v2 (OP_2 <32 bytes>): spending is invalid.
wv2_spk = CScript([OP_2, b'\x42' * 32])
o, a = self.fund(wv2_spk)[0:2]
self.check(3, "spend_witness_v2_invalid", "Spend an undefined witness v2 output",
[(wv2_spk, a)], self.spend(o, a, [b'']), False, "post-activation")
# Undefined Tapleaf version (0xc2): spending that leaf is invalid.
leafv = (b'\x51', 0xc2) # (code=OP_1, version=0xc2)
infov = taproot_construct(pub, [("u", leafv[0], leafv[1])])
o, a = self.fund(infov.scriptPubKey)[0:2]
lv = infov.leaves["u"]
self.check(3, "spend_tapleaf_v0xc2_invalid", "Spend an undefined (0xc2) Tapleaf version",
[(bytes(infov.scriptPubKey), a)],
self.spend(o, a, [lv.script, cb(0xc2, infov, lv)]), False, "post-activation")
self.log.info("=== Rule 4: Taproot annex ===")
info1 = taproot_construct(pub, [("ok", CScript([OP_1]))])
o, a = self.fund(info1.scriptPubKey)[0:2]
leaf = info1.leaves["ok"]
annex = bytes([0x50]) + b'\x00' * 10
self.check(4, "taproot_annex_invalid", "Spend a taproot script path with an annex",
[(bytes(info1.scriptPubKey), a)],
self.spend(o, a, [leaf.script, cb(0xc0, info1, leaf), annex]),
False, "post-activation")
self.log.info("=== Rule 5: Taproot control block size ===")
info7 = taproot_construct(pub, [deepen(("d7", CScript([OP_1])), 7)])
o, a = self.fund(info7.scriptPubKey)[0:2]
l7 = info7.leaves["d7"]
assert_equal(len(cb(0xc0, info7, l7)), 257)
self.check(5, "control_block_257_valid", "Spend a leaf at depth 7 (257-byte control block, boundary)",
[(bytes(info7.scriptPubKey), a)],
self.spend(o, a, [l7.script, cb(0xc0, info7, l7)]), True, "post-activation")
info8 = taproot_construct(pub, [deepen(("d8", CScript([OP_1])), 8)])
o, a = self.fund(info8.scriptPubKey)[0:2]
l8 = info8.leaves["d8"]
assert_equal(len(cb(0xc0, info8, l8)), 289)
self.check(5, "control_block_289_invalid", "Spend a leaf at depth 8 (289-byte control block, >257)",
[(bytes(info8.scriptPubKey), a)],
self.spend(o, a, [l8.script, cb(0xc0, info8, l8)]), False, "post-activation")
self.log.info("=== Rule 6: OP_SUCCESS* in tapscript ===")
info_os = taproot_construct(pub, [("os", bytes([0xfe]))]) # OP_SUCCESS254
o, a = self.fund(info_os.scriptPubKey)[0:2]
los = info_os.leaves["os"]
self.check(6, "op_success_invalid", "Spend a tapscript containing OP_SUCCESS254 (0xfe)",
[(bytes(info_os.scriptPubKey), a)],
self.spend(o, a, [los.script, cb(0xc0, info_os, los)]), False, "post-activation")
self.log.info("=== Rule 7: OP_IF / OP_NOTIF in tapscript ===")
if_leaf = CScript([OP_1, OP_IF, OP_1, OP_ENDIF])
info_if = taproot_construct(pub, [("if", if_leaf)])
o, a = self.fund(info_if.scriptPubKey)[0:2]
lif = info_if.leaves["if"]
self.check(7, "tapscript_op_if_invalid", "Spend a tapscript executing OP_IF",
[(bytes(info_if.scriptPubKey), a)],
self.spend(o, a, [lif.script, cb(0xc0, info_if, lif)]), False, "post-activation")
# contrast: OP_IF in a witness v0 (non-tapscript) script is unaffected
wsh_if = CScript([OP_1, OP_IF, OP_1, OP_ENDIF])
wsh_if_spk = script_to_p2wsh_script(wsh_if)
o, a = self.fund(wsh_if_spk)[0:2]
self.check(7, "witness_v0_op_if_valid", "OP_IF in a witness v0 script is valid (rule is tapscript-only)",
[(wsh_if_spk, a)], self.spend(o, a, [bytes(wsh_if)]), True, "post-activation")
self.log.info("=== Grandfathering: pre-activation UTXOs ===")
# An input spending a pre-activation UTXO is exempt from the per-input
# script rules: a 257-byte witness item is accepted here (cf. rule 2).
self.check(2, "grandfathered_witness_item_257_valid",
"Spend a PRE-activation UTXO with a 257-byte witness item (grandfathered, exempt)",
[(wsh_drop_spk, old_drop_a)],
self.spend(old_drop_o, old_drop_a, [b'\x42' * 257, bytes(wsh_drop)]),
True, "pre-activation")
# But the per-output rule 1 still applies even when spending an old UTXO:
# creating an oversized output is rejected regardless of input age.
self.check(1, "grandfathered_input_oversized_output_invalid",
"Spend a PRE-activation UTXO but create a 35-byte output (rule 1 not grandfathered)",
[(wsh_true_spk, old_true_a)],
self.spend(old_true_o, old_true_a, [bytes(wsh_true)],
outputs=[CTxOut(old_true_a - FEE, CScript(bytes([OP_1]) + b'\x00' * 34))]),
False, "pre-activation")
self.write_vectors()
def write_vectors(self):
out = os.environ.get("TEST_VECTOR_OUT")
if not out:
self.log.info("TEST_VECTOR_OUT not set; not writing vector file")
return
doc = {
"comment": ("BIP-110 (REDUCED_DATA) consensus test vectors, generated and "
"verified against the reference implementation. Each vector gives the "
"spent output(s), the spending/creating transaction, and whether a block "
"containing it is valid while REDUCED_DATA is active. 'rule' refers to the "
"numbered rules in the BIP Specification. Hex values are byte arrays. "
"'reject_reason' is informational and normalized to be implementation-agnostic."),
"vectors": self.vectors,
}
with open(out, "w") as f:
json.dump(doc, f, indent=2)
f.write("\n")
self.log.info(f"Wrote {len(self.vectors)} vectors to {out}")
if __name__ == '__main__':
ReducedDataTestVectors(__file__).main()