diff --git a/bip-0110.mediawiki b/bip-0110.mediawiki
index 8b80c651..61473619 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 00000000..80124af7
--- /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 00000000..1644bdf2
--- /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()