From f952fd1fc0a8f4921bddba7b7fe0561ca6c6e036 Mon Sep 17 00:00:00 2001 From: Dathon Ohm Date: Wed, 24 Jun 2026 15:42:44 -0600 Subject: [PATCH 1/3] BIP-110: advance to Complete status; emphasize UTXO grandfathering; restore OP_IF byte-saving note --- README.mediawiki | 4 ++-- bip-0110.mediawiki | 10 +++++++--- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/README.mediawiki b/README.mediawiki index 0d77050583..2b470c9204 100644 --- a/README.mediawiki +++ b/README.mediawiki @@ -624,13 +624,13 @@ users (see also: [https://en.bitcoin.it/wiki/Economic_majority economic majority | Gavin Andresen | Specification | Closed -|- +|- style="background-color: #ffffcf" | [[bip-0110.mediawiki|110]] | Consensus (soft fork) | Reduced Data Temporary Softfork | Dathon Ohm | Specification -| Draft +| Complete |- style="background-color: #cfffcf" | [[bip-0111.mediawiki|111]] | Peer Services diff --git a/bip-0110.mediawiki b/bip-0110.mediawiki index 5bc6dea841..f050bfa809 100644 --- a/bip-0110.mediawiki +++ b/bip-0110.mediawiki @@ -3,7 +3,7 @@ Layer: Consensus (soft fork) Title: Reduced Data Temporary Softfork Authors: Dathon Ohm - Status: Draft + Status: Complete Type: Specification Assigned: 2025-12-03 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 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. ===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. # 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?''' From 422fca742a5e02bc0142f232f833936fc2158cde Mon Sep 17 00:00:00 2001 From: Dathon Ohm Date: Thu, 25 Jun 2026 21:44:45 -0600 Subject: [PATCH 2/3] BIP-110: Add Test vectors and Changelog sections --- bip-0110.mediawiki | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/bip-0110.mediawiki b/bip-0110.mediawiki index f050bfa809..8b80c65141 100644 --- a/bip-0110.mediawiki +++ b/bip-0110.mediawiki @@ -318,6 +318,13 @@ All other known use cases are not affected. 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. + ==Deployment== This deployment uses a modified version of BIP9 with the following parameters: @@ -355,3 +362,19 @@ The FAILED state is never reached because timeout is disabled. EXPIRED is the fi ==Credits== 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 From b0a1a276021cf371a93865315b274a55616e3b6c Mon Sep 17 00:00:00 2001 From: Dathon Ohm Date: Thu, 25 Jun 2026 21:53:42 -0600 Subject: [PATCH 3/3] BIP-110: Add test vectors JSON and generator script --- bip-0110.mediawiki | 2 + bip-0110/test-vectors.json | 325 +++++++++++++++++++++++++++++ bip-0110/test-vectors.py | 408 +++++++++++++++++++++++++++++++++++++ 3 files changed, 735 insertions(+) create mode 100644 bip-0110/test-vectors.json create mode 100755 bip-0110/test-vectors.py diff --git a/bip-0110.mediawiki b/bip-0110.mediawiki index 8b80c65141..6147361987 100644 --- a/bip-0110.mediawiki +++ b/bip-0110.mediawiki @@ -325,6 +325,8 @@ Test coverage specific to these rules is provided by the reference implementatio * [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 rule and spent_utxo 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== This deployment uses a modified version of BIP9 with the following parameters: diff --git a/bip-0110/test-vectors.json b/bip-0110/test-vectors.json new file mode 100644 index 0000000000..80124af79f --- /dev/null +++ b/bip-0110/test-vectors.json @@ -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" + } + ] +} diff --git a/bip-0110/test-vectors.py b/bip-0110/test-vectors.py new file mode 100755 index 0000000000..1644bdf2d2 --- /dev/null +++ b/bip-0110/test-vectors.py @@ -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()