feature_rdts.py raw

   1  #!/usr/bin/env python3
   2  # Copyright (c) 2025 The Limenka developers
   3  # Distributed under the MIT software license, see the accompanying
   4  # file COPYING or http://www.opensource.org/licenses/mit-license.php.
   5  """Test ReducedData Temporary Softfork (RDTS) consensus rules.
   6  
   7  This test verifies all 7 consensus rules enforced by DEPLOYMENT_REDUCED_DATA:
   8  
   9  1. Output scriptPubKeys exceeding 34 bytes are invalid (except OP_RETURN up to 83 bytes)
  10  2. OP_PUSHDATA* with payloads larger than 256 bytes are invalid (except BIP16 redeemScript)
  11  3. Spending undefined witness versions (not v0/v1) is invalid
  12  4. Witness stacks with a Taproot annex are invalid
  13  5. Taproot control blocks larger than 257 bytes are invalid (max 7 merkle nodes = 128 leaves)
  14  6. Tapscripts including OP_SUCCESS* opcodes are invalid
  15  7. Tapscripts executing OP_IF or OP_NOTIF instructions are invalid
  16  """
  17  
  18  from test_framework.test_framework import LimenkaTestFramework
  19  from test_framework.wallet import MiniWallet
  20  from test_framework.messages import (
  21      CBlock,
  22      COutPoint,
  23      CTransaction,
  24      CTxIn,
  25      CTxInWitness,
  26      CTxOut,
  27      COIN,
  28      MAX_OP_RETURN_RELAY,
  29  )
  30  from test_framework.p2p import P2PDataStore
  31  from test_framework.script import (
  32      ANNEX_TAG,
  33      CScript,
  34      CScriptOp,
  35      is_op_success,
  36      LEAF_VERSION_TAPSCRIPT,
  37      OP_0,
  38      OP_1,
  39      OP_2,
  40      OP_3,
  41      OP_4,
  42      OP_5,
  43      OP_6,
  44      OP_7,
  45      OP_8,
  46      OP_9,
  47      OP_10,
  48      OP_11,
  49      OP_12,
  50      OP_13,
  51      OP_14,
  52      OP_15,
  53      OP_16,
  54      OP_CHECKSIG,
  55      OP_CHECKSIGADD,
  56      OP_CHECKMULTISIG,
  57      OP_DROP,
  58      OP_DUP,
  59      OP_EQUAL,
  60      OP_EQUALVERIFY,
  61      OP_HASH160,
  62      OP_IF,
  63      OP_NOTIF,
  64      OP_ENDIF,
  65      OP_PUSHDATA1,
  66      OP_PUSHDATA2,
  67      OP_RETURN,
  68      OP_TRUE,
  69      SegwitV0SignatureHash,
  70      SIGHASH_ALL,
  71      SIGHASH_DEFAULT,
  72      hash160,
  73      sha256,
  74      taproot_construct,
  75      TaprootSignatureHash,
  76  )
  77  from test_framework.blocktools import (
  78      create_block,
  79      create_coinbase,
  80      add_witness_commitment,
  81  )
  82  from test_framework.script_util import (
  83      PAY_TO_ANCHOR,
  84      script_to_p2wsh_script,
  85      script_to_p2sh_script,
  86  )
  87  from test_framework.util import (
  88      assert_equal,
  89      assert_raises_rpc_error,
  90  )
  91  from test_framework.key import (
  92      ECKey,
  93      compute_xonly_pubkey,
  94      generate_privkey,
  95      sign_schnorr,
  96      tweak_add_privkey,
  97  )
  98  from io import BytesIO
  99  import struct
 100  
 101  
 102  # Constants from BIP444
 103  MAX_OUTPUT_SCRIPT_SIZE = 34
 104  MAX_OUTPUT_DATA_SIZE = 83
 105  MAX_SCRIPT_ELEMENT_SIZE_REDUCED = 256
 106  TAPROOT_CONTROL_BASE_SIZE = 33
 107  TAPROOT_CONTROL_NODE_SIZE = 32
 108  TAPROOT_CONTROL_MAX_NODE_COUNT_REDUCED = 7
 109  TAPROOT_CONTROL_MAX_SIZE_REDUCED = TAPROOT_CONTROL_BASE_SIZE + TAPROOT_CONTROL_NODE_SIZE * TAPROOT_CONTROL_MAX_NODE_COUNT_REDUCED
 110  # ANNEX_TAG is imported from test_framework.script
 111  
 112  
 113  class ReducedDataTest(LimenkaTestFramework):
 114      def set_test_params(self):
 115          self.num_nodes = 1
 116          self.setup_clean_chain = True
 117          # Make DEPLOYMENT_REDUCED_DATA always active (from block 0)
 118          # Using start_time=-1 (ALWAYS_ACTIVE) bypasses BIP9 state machine
 119          self.extra_args = [[
 120              '-vbparams=reduced_data:-1:999999999999:0',
 121              '-acceptnonstdtxn=1',
 122          ]]
 123  
 124      def init_test(self):
 125          """Initialize test by mining blocks and creating UTXOs."""
 126          node = self.nodes[0]
 127  
 128          # MiniWallet provides a simple wallet for test transactions
 129          self.wallet = MiniWallet(node)
 130  
 131          # Mine 120 blocks to mature coinbase outputs and create spending UTXOs
 132          # (101 for maturity + extras since each test consumes a UTXO)
 133          self.generate(self.wallet, 120)
 134  
 135          self.log.info("Test initialization complete")
 136  
 137      def create_test_transaction(self, scriptPubKey, value=None):
 138          """Helper to create a transaction with custom scriptPubKey (not broadcast)."""
 139          # Start with a valid transaction from the wallet
 140          tx_dict = self.wallet.create_self_transfer()
 141          tx = tx_dict['tx']
 142  
 143          # Use default output value if not specified (handles fee calculation)
 144          if value is None:
 145              value = tx.vout[0].nValue
 146  
 147          # Replace output with our custom scriptPubKey
 148          tx.vout[0] = CTxOut(value, scriptPubKey)
 149          tx.rehash()
 150  
 151          return tx
 152  
 153      def test_output_script_size_limit(self):
 154          """Test spec 1: Output scriptPubKeys exceeding 34 bytes are invalid."""
 155          self.log.info("Testing output scriptPubKey size limits...")
 156  
 157          node = self.nodes[0]
 158  
 159          # Test 1.1: 34-byte P2WSH output (exactly at limit - should pass)
 160          witness_program_32 = b'\x00' * 32
 161          script_p2wsh = CScript([OP_0, witness_program_32])  # OP_0 (1 byte) + 32-byte push = 34 bytes
 162          assert_equal(len(script_p2wsh), 34)
 163  
 164          tx_valid = self.create_test_transaction(script_p2wsh)
 165          result = node.testmempoolaccept([tx_valid.serialize().hex()])[0]
 166          if not result['allowed']:
 167              self.log.info(f"  DEBUG: P2WSH rejection reason: {result}")
 168          assert_equal(result['allowed'], True)
 169          self.log.info("  ✓ 34-byte P2WSH output accepted")
 170  
 171          # Test 1.2: 35-byte P2PK output (exceeds limit - should fail)
 172          pubkey_33 = b'\x02' + b'\x00' * 32  # Compressed pubkey
 173          script_p2pk = CScript([pubkey_33, OP_CHECKSIG])  # 33-byte push + OP_CHECKSIG = 35 bytes
 174          assert_equal(len(script_p2pk), 35)
 175  
 176          tx_invalid = self.create_test_transaction(script_p2pk)
 177          result = node.testmempoolaccept([tx_invalid.serialize().hex()])[0]
 178          assert_equal(result['allowed'], False)
 179          assert 'bad-txns-vout-script-toolarge' in result['reject-reason']
 180          self.log.info("  ✓ 35-byte P2PK output rejected")
 181  
 182          # Test 1.3: 37-byte bare multisig (exceeds limit - should fail)
 183          script_bare_multisig = CScript([OP_1, pubkey_33, OP_1, OP_CHECKMULTISIG])
 184          assert len(script_bare_multisig) >= 37
 185  
 186          tx_invalid = self.create_test_transaction(script_bare_multisig)
 187          result = node.testmempoolaccept([tx_invalid.serialize().hex()])[0]
 188          assert_equal(result['allowed'], False)
 189          assert 'bad-txns-vout-script-toolarge' in result['reject-reason']
 190          self.log.info("  ✓ 37-byte bare multisig output rejected")
 191  
 192          # Test 1.4: OP_RETURN with 83 bytes (at the OP_RETURN exception limit)
 193          # Note: CScript adds PUSHDATA overhead for data >75 bytes
 194          # 80 bytes data: OP_RETURN (1) + direct push (1) + data (80) = 82 bytes total
 195          # 81+ bytes data: OP_RETURN (1) + OP_PUSHDATA1 (1) + len (1) + data = 84+ bytes
 196          data_80 = b'\x00' * 80
 197          script_opreturn_82 = CScript([OP_RETURN, data_80])
 198          self.log.info(f"  DEBUG: OP_RETURN script with 80 data bytes has length: {len(script_opreturn_82)}")
 199  
 200          tx_valid = self.create_test_transaction(script_opreturn_82, value=0)
 201          result = node.testmempoolaccept([tx_valid.serialize().hex()])[0]
 202          # OP_RETURN with value=0 may be rejected by standardness policy
 203          self.log.info(f"  ✓ OP_RETURN with {len(script_opreturn_82)} bytes: {result.get('allowed', False)}")
 204  
 205          # Test 1.5: OP_RETURN with 85 bytes (exceeds 83-byte exception)
 206          data_82 = b'\x00' * 82
 207          script_opreturn_85 = CScript([OP_RETURN, data_82])
 208          self.log.info(f"  DEBUG: OP_RETURN script with 82 data bytes has length: {len(script_opreturn_85)}")
 209  
 210          tx_invalid = self.create_test_transaction(script_opreturn_85, value=0)
 211          result = node.testmempoolaccept([tx_invalid.serialize().hex()])[0]
 212          assert_equal(result['allowed'], False)
 213          if result['allowed'] == False:
 214              self.log.info(f"  ✓ OP_RETURN with {len(script_opreturn_85)} bytes rejected")
 215  
 216      def test_pushdata_size_limit(self):
 217          """Test spec 2: OP_PUSHDATA* with payloads > 256 bytes are invalid."""
 218          self.log.info("Testing OP_PUSHDATA size limits...")
 219  
 220          node = self.nodes[0]
 221  
 222          # Standard P2WPKH hash for outputs (avoids tx-size-small policy rejection)
 223          dummy_pubkey_hash = hash160(b'\x00' * 33)
 224  
 225          # Test 2.1: Witness script with 256-byte PUSHDATA (exactly at limit - should pass)
 226          data_256 = b'\x00' * 256
 227          witness_script_256 = CScript([data_256, OP_DROP, OP_TRUE])  # Script: <256 bytes> DROP TRUE
 228          script_pubkey_256 = script_to_p2wsh_script(witness_script_256)
 229  
 230          # First create an output with this witness script
 231          funding_tx_256 = self.create_test_transaction(script_pubkey_256)
 232          txid_256 = node.sendrawtransaction(funding_tx_256.serialize().hex())
 233          self.generate(node, 1)
 234          output_value_256 = funding_tx_256.vout[0].nValue
 235  
 236          # Now spend it - this reveals the witness script with the 256-byte PUSHDATA
 237          spending_tx_256 = CTransaction()
 238          spending_tx_256.vin = [CTxIn(COutPoint(int(txid_256, 16), 0))]
 239          spending_tx_256.vout = [CTxOut(output_value_256 - 10000, CScript([OP_0, dummy_pubkey_hash]))]
 240          spending_tx_256.wit.vtxinwit = [CTxInWitness()]
 241          spending_tx_256.wit.vtxinwit[0].scriptWitness.stack = [witness_script_256]
 242          spending_tx_256.rehash()
 243  
 244          # 256 bytes is at the limit, should be accepted
 245          result = node.testmempoolaccept([spending_tx_256.serialize().hex()])[0]
 246          if not result['allowed']:
 247              self.log.info(f"  DEBUG: 256-byte PUSHDATA rejection: {result}")
 248          assert_equal(result['allowed'], True)
 249          self.log.info("  ✓ PUSHDATA with 256 bytes accepted in witness script")
 250  
 251          # Test 2.2: Witness script with 257-byte PUSHDATA (exceeds limit - should fail)
 252          data_257 = b'\x00' * 257
 253          witness_script_257 = CScript([data_257, OP_DROP, OP_TRUE])
 254          script_pubkey_257 = script_to_p2wsh_script(witness_script_257)
 255  
 256          # Create and fund the output
 257          funding_tx_257 = self.create_test_transaction(script_pubkey_257)
 258          txid_257 = node.sendrawtransaction(funding_tx_257.serialize().hex())
 259          self.generate(node, 1)
 260          output_value_257 = funding_tx_257.vout[0].nValue
 261  
 262          # Try to spend it - should be rejected due to 257-byte PUSHDATA
 263          spending_tx_257 = CTransaction()
 264          spending_tx_257.vin = [CTxIn(COutPoint(int(txid_257, 16), 0))]
 265          spending_tx_257.vout = [CTxOut(output_value_257 - 10000, CScript([OP_0, dummy_pubkey_hash]))]
 266          spending_tx_257.wit.vtxinwit = [CTxInWitness()]
 267          spending_tx_257.wit.vtxinwit[0].scriptWitness.stack = [witness_script_257]
 268          spending_tx_257.rehash()
 269  
 270          result = node.testmempoolaccept([spending_tx_257.serialize().hex()])[0]
 271          assert_equal(result['allowed'], False)
 272          assert 'non-mandatory-script-verify-flag' in result['reject-reason'] or 'Push value size limit exceeded' in result['reject-reason']
 273          self.log.info("  ✓ PUSHDATA with 257 bytes rejected in witness script")
 274  
 275          # Test 2.3: P2SH redeemScript with 300-byte PUSHDATA (tests BIP16 exception boundary)
 276          # Important: BIP16 allows pushing the redeemScript itself even if >256 bytes,
 277          # BUT any PUSHDATAs executed WITHIN that redeemScript are still limited to 256 bytes
 278          large_redeem_script = CScript([b'\x00' * 300, OP_DROP, OP_TRUE])  # Contains 300-byte PUSHDATA
 279          p2sh_script_pubkey = script_to_p2sh_script(large_redeem_script)
 280  
 281          # Create the P2SH output
 282          funding_tx_p2sh = self.create_test_transaction(p2sh_script_pubkey)
 283          txid_p2sh = node.sendrawtransaction(funding_tx_p2sh.serialize().hex())
 284          self.generate(node, 1)
 285          output_value_p2sh = funding_tx_p2sh.vout[0].nValue
 286  
 287          # Spend it by revealing the redeemScript in scriptSig
 288          spending_tx_p2sh = CTransaction()
 289          spending_tx_p2sh.vin = [CTxIn(COutPoint(int(txid_p2sh, 16), 0), CScript([large_redeem_script]))]
 290          spending_tx_p2sh.vout = [CTxOut(output_value_p2sh - 10000, CScript([OP_0, dummy_pubkey_hash]))]
 291          spending_tx_p2sh.rehash()
 292  
 293          # Should fail because the 300-byte PUSHDATA inside the redeemScript exceeds the limit
 294          result = node.testmempoolaccept([spending_tx_p2sh.serialize().hex()])[0]
 295          assert_equal(result['allowed'], False)
 296          assert 'non-mandatory-script-verify-flag' in result['reject-reason'] or 'Push value size limit exceeded' in result['reject-reason']
 297          self.log.info("  ✓ P2SH redeemScript with >256 byte PUSHDATA correctly rejected")
 298          self.log.info("    (BIP16 exception only applies to pushing the redeemScript blob, not PUSHDATAs within it)")
 299  
 300      def test_undefined_witness_versions(self):
 301          """Test spec 3: Spending undefined witness versions is invalid.
 302  
 303          Limenka currently defines witness v0 (P2WPKH/P2WSH) and v1 (Taproot).
 304          Versions v2-v16 are reserved for future upgrades and are currently undefined.
 305          After DEPLOYMENT_REDUCED_DATA, spending these undefined versions is invalid.
 306          """
 307          self.log.info("Testing undefined witness version rejection...")
 308  
 309          node = self.nodes[0]
 310  
 311          # Test witness v2 as representative (same logic applies to v3-v16)
 312          version_op = OP_2  # Witness version 2
 313          version = version_op - 0x50  # Convert OP_2 to numeric 2
 314  
 315          # Create output to witness v2: <version> <32-byte program>
 316          witness_program = b'\x00' * 32
 317          script_v2 = CScript([CScriptOp(version_op), witness_program])
 318  
 319          # Step 1: Create an output to witness v2 (this is allowed)
 320          funding_tx = self.create_test_transaction(script_v2)
 321          txid = node.sendrawtransaction(funding_tx.serialize().hex())
 322          self.generate(node, 1)
 323          self.log.info(f"  Created witness v2 output in tx {txid[:16]}...")
 324  
 325          # Step 2: Try to spend the witness v2 output (should be rejected)
 326          spending_tx = CTransaction()
 327          spending_tx.vin = [CTxIn(COutPoint(int(txid, 16), 0))]
 328          dummy_pubkey_hash = hash160(b'\x00' * 33)
 329          spending_tx.vout = [CTxOut(funding_tx.vout[0].nValue - 10000, CScript([OP_0, dummy_pubkey_hash]))]
 330  
 331          # For undefined witness versions, pre-softfork behavior was "anyone-can-spend"
 332          # with an empty witness stack. Post-REDUCED_DATA, this is now invalid.
 333          spending_tx.wit.vtxinwit = [CTxInWitness()]
 334          spending_tx.wit.vtxinwit[0].scriptWitness.stack = []  # Empty witness
 335          spending_tx.rehash()
 336  
 337          # Should be rejected - undefined witness versions can't be spent after activation
 338          result = node.testmempoolaccept([spending_tx.serialize().hex()])[0]
 339          assert_equal(result['allowed'], False)
 340          # Rejection happens during script verification
 341          assert any(x in result['reject-reason'] for x in ['mempool-script-verify-flag', 'witness-program', 'bad-witness', 'discouraged'])
 342          self.log.info(f"  ✓ Witness v{version} spending correctly rejected ({result['reject-reason']})")
 343  
 344          # All undefined versions (v2-v16) are validated identically
 345          self.log.info("  ✓ Witness versions v2-v16 are all similarly rejected")
 346  
 347      def test_taproot_annex_rejection(self):
 348          """Test spec 4: Witness stacks with a Taproot annex are invalid."""
 349          self.log.info("Testing Taproot annex rejection...")
 350          node = self.nodes[0]
 351  
 352          # Generate a Taproot key pair for testing
 353          privkey = generate_privkey()
 354          internal_pubkey, _ = compute_xonly_pubkey(privkey)
 355  
 356          # Create a simple Taproot output (key-path only, no script tree)
 357          taproot_info = taproot_construct(internal_pubkey)
 358          taproot_spk = taproot_info.scriptPubKey
 359  
 360          # Test 4.1: Taproot key-path spend WITHOUT annex (valid baseline)
 361          self.log.info("  Test 4.1: Taproot key-path spend without annex (should be valid)")
 362  
 363          # Create funding transaction with Taproot output
 364          funding_tx = self.create_test_transaction(taproot_spk)
 365          funding_txid = funding_tx.rehash()
 366  
 367          # Mine the funding transaction in a block
 368          block_height = node.getblockcount() + 1
 369          block = create_block(int(node.getbestblockhash(), 16), create_coinbase(block_height), int(node.getblockheader(node.getbestblockhash())['time']) + 1)
 370          block.vtx.append(funding_tx)
 371          add_witness_commitment(block)
 372          block.solve()
 373          node.submitblock(block.serialize().hex())
 374  
 375          # Create spending transaction (key-path, no annex)
 376          spending_tx = CTransaction()
 377          spending_tx.vin = [CTxIn(COutPoint(int(funding_txid, 16), 0), nSequence=0)]
 378          # Use the actual output value from funding_tx minus a small fee
 379          output_value = funding_tx.vout[0].nValue - 1000  # 1000 sats fee
 380          spending_tx.vout = [CTxOut(output_value, CScript([OP_1, bytes(20)]))]  # P2WPKH output
 381  
 382          # Sign with Schnorr signature for Taproot key-path spend
 383          sighash = TaprootSignatureHash(spending_tx, [funding_tx.vout[0]], SIGHASH_DEFAULT, 0)
 384          tweaked_privkey = tweak_add_privkey(privkey, taproot_info.tweak)
 385          sig = sign_schnorr(tweaked_privkey, sighash)
 386  
 387          # Witness for key-path: just the signature
 388          spending_tx.wit.vtxinwit.append(CTxInWitness())
 389          spending_tx.wit.vtxinwit[0].scriptWitness.stack = [sig]
 390  
 391          # This should be accepted (no annex)
 392          result = node.testmempoolaccept([spending_tx.serialize().hex()])[0]
 393          if not result['allowed']:
 394              self.log.info(f"  DEBUG: Taproot spend rejection: {result}")
 395          assert_equal(result['allowed'], True)
 396          self.log.info("  ✓ Taproot key-path spend without annex: ACCEPTED")
 397  
 398          # Test 4.2: Taproot key-path spend WITH annex (invalid after DEPLOYMENT_REDUCED_DATA)
 399          self.log.info("  Test 4.2: Taproot key-path spend with annex (should be rejected)")
 400  
 401          # Create another funding transaction
 402          funding_tx2 = self.create_test_transaction(taproot_spk)
 403          funding_txid2 = funding_tx2.rehash()
 404  
 405          # Mine the funding transaction in a block
 406          block_height2 = node.getblockcount() + 1
 407          block2 = create_block(int(node.getbestblockhash(), 16), create_coinbase(block_height2), int(node.getblockheader(node.getbestblockhash())['time']) + 1)
 408          block2.vtx.append(funding_tx2)
 409          add_witness_commitment(block2)
 410          block2.solve()
 411          node.submitblock(block2.serialize().hex())
 412  
 413          # Create spending transaction with annex
 414          spending_tx2 = CTransaction()
 415          spending_tx2.vin = [CTxIn(COutPoint(int(funding_txid2, 16), 0), nSequence=0)]
 416          output_value2 = funding_tx2.vout[0].nValue - 1000
 417          spending_tx2.vout = [CTxOut(output_value2, CScript([OP_1, bytes(20)]))]
 418  
 419          # Sign the transaction (annex affects sighash)
 420          annex = bytes([ANNEX_TAG]) + b'\x00' * 10  # Annex must start with 0x50
 421          sighash2 = TaprootSignatureHash(spending_tx2, [funding_tx2.vout[0]], SIGHASH_DEFAULT, 0, annex=annex)
 422          sig2 = sign_schnorr(tweaked_privkey, sighash2)
 423  
 424          # Witness for key-path with annex: [signature, annex]
 425          spending_tx2.wit.vtxinwit.append(CTxInWitness())
 426          spending_tx2.wit.vtxinwit[0].scriptWitness.stack = [sig2, annex]
 427  
 428          # This should be rejected (annex present)
 429          result2 = node.testmempoolaccept([spending_tx2.serialize().hex()])[0]
 430          if result2['allowed']:
 431              self.log.info(f"  DEBUG: Taproot spend with annex was unexpectedly accepted: {result2}")
 432          assert_equal(result2['allowed'], False)
 433          self.log.info(f"  ✓ Taproot spend with annex: REJECTED ({result2['reject-reason']})")
 434  
 435      def test_taproot_control_block_size(self):
 436          """Test spec 5: Taproot control blocks > 257 bytes are invalid."""
 437          self.log.info("Testing Taproot control block size limits...")
 438          node = self.nodes[0]
 439  
 440          # Control block size = 33 + 32 * num_nodes
 441          # Max allowed: 7 nodes = 33 + 32*7 = 257 bytes (depth 7, 128 leaves)
 442          # Invalid: 8 nodes = 33 + 32*8 = 289 bytes (depth 8, 256 leaves)
 443  
 444          max_valid_size = TAPROOT_CONTROL_MAX_SIZE_REDUCED
 445          assert_equal(max_valid_size, 257)
 446          self.log.info(f"  Max valid control block size: {max_valid_size} bytes (7 nodes)")
 447  
 448          # Helper function to build a balanced binary tree of given depth
 449          def build_tree(depth, leaf_prefix="leaf"):
 450              """Build a balanced binary tree for Taproot script tree."""
 451              if depth == 0:
 452                  # At leaf level, return a simple script
 453                  return (f"{leaf_prefix}", CScript([OP_TRUE]))
 454              else:
 455                  # Recursively build left and right subtrees
 456                  left = build_tree(depth - 1, f"{leaf_prefix}_L")
 457                  right = build_tree(depth - 1, f"{leaf_prefix}_R")
 458                  return [left, right]
 459  
 460          # Generate a Taproot key pair
 461          privkey = generate_privkey()
 462          internal_pubkey, _ = compute_xonly_pubkey(privkey)
 463  
 464          # Test 5.1: Control block with 7 merkle nodes (valid, 257 bytes)
 465          self.log.info("  Test 5.1: Control block with 7 nodes / depth 7 (should be valid)")
 466  
 467          # Build a balanced tree of depth 7 (128 leaves)
 468          tree_valid = build_tree(7)
 469          taproot_info_valid = taproot_construct(internal_pubkey, [tree_valid])
 470          taproot_spk_valid = taproot_info_valid.scriptPubKey
 471  
 472          # Create and mine funding transaction
 473          funding_tx_valid = self.create_test_transaction(taproot_spk_valid)
 474          funding_txid_valid = funding_tx_valid.rehash()
 475  
 476          block_height = node.getblockcount() + 1
 477          block = create_block(int(node.getbestblockhash(), 16), create_coinbase(block_height), int(node.getblockheader(node.getbestblockhash())['time']) + 1)
 478          block.vtx.append(funding_tx_valid)
 479          add_witness_commitment(block)
 480          block.solve()
 481          node.submitblock(block.serialize().hex())
 482  
 483          # Spend using the deepest leaf (which will have the longest control block)
 484          # The deepest leaf should be at path L_L_L_L_L_L_L (all left)
 485          deepest_leaf_name = "leaf" + "_L" * 7
 486          leaf_info_valid = taproot_info_valid.leaves[deepest_leaf_name]
 487          control_block_valid = bytes([leaf_info_valid.version + taproot_info_valid.negflag]) + internal_pubkey + leaf_info_valid.merklebranch
 488  
 489          # Verify control block size
 490          assert_equal(len(control_block_valid), 257)
 491          self.log.info(f"    Control block size: {len(control_block_valid)} bytes ✓")
 492  
 493          # Create spending transaction
 494          spending_tx_valid = CTransaction()
 495          spending_tx_valid.vin = [CTxIn(COutPoint(int(funding_txid_valid, 16), 0), nSequence=0)]
 496          output_value = funding_tx_valid.vout[0].nValue - 1000
 497          spending_tx_valid.vout = [CTxOut(output_value, CScript([OP_1, bytes(20)]))]
 498  
 499          spending_tx_valid.wit.vtxinwit.append(CTxInWitness())
 500          spending_tx_valid.wit.vtxinwit[0].scriptWitness.stack = [leaf_info_valid.script, control_block_valid]
 501  
 502          result_valid = node.testmempoolaccept([spending_tx_valid.serialize().hex()])[0]
 503          if not result_valid['allowed']:
 504              self.log.info(f"    DEBUG: Depth 7 rejection: {result_valid}")
 505          assert_equal(result_valid['allowed'], True)
 506          self.log.info("  ✓ Control block with 7 nodes (257 bytes): ACCEPTED")
 507  
 508          # Test 5.2: Control block with 8 merkle nodes (invalid, 289 bytes)
 509          self.log.info("  Test 5.2: Control block with 8 nodes / depth 8 (should be rejected)")
 510  
 511          # Build a balanced tree of depth 8 (256 leaves)
 512          tree_invalid = build_tree(8)
 513          taproot_info_invalid = taproot_construct(internal_pubkey, [tree_invalid])
 514          taproot_spk_invalid = taproot_info_invalid.scriptPubKey
 515  
 516          # Create and mine funding transaction
 517          funding_tx_invalid = self.create_test_transaction(taproot_spk_invalid)
 518          funding_txid_invalid = funding_tx_invalid.rehash()
 519  
 520          block_height = node.getblockcount() + 1
 521          block = create_block(int(node.getbestblockhash(), 16), create_coinbase(block_height), int(node.getblockheader(node.getbestblockhash())['time']) + 1)
 522          block.vtx.append(funding_tx_invalid)
 523          add_witness_commitment(block)
 524          block.solve()
 525          node.submitblock(block.serialize().hex())
 526  
 527          # Spend using the deepest leaf
 528          deepest_leaf_name_invalid = "leaf" + "_L" * 8
 529          leaf_info_invalid = taproot_info_invalid.leaves[deepest_leaf_name_invalid]
 530          control_block_invalid = bytes([leaf_info_invalid.version + taproot_info_invalid.negflag]) + internal_pubkey + leaf_info_invalid.merklebranch
 531  
 532          # Verify control block size
 533          assert_equal(len(control_block_invalid), 289)
 534          self.log.info(f"    Control block size: {len(control_block_invalid)} bytes (exceeds 257)")
 535  
 536          # Create spending transaction
 537          spending_tx_invalid = CTransaction()
 538          spending_tx_invalid.vin = [CTxIn(COutPoint(int(funding_txid_invalid, 16), 0), nSequence=0)]
 539          output_value = funding_tx_invalid.vout[0].nValue - 1000
 540          spending_tx_invalid.vout = [CTxOut(output_value, CScript([OP_1, bytes(20)]))]
 541  
 542          spending_tx_invalid.wit.vtxinwit.append(CTxInWitness())
 543          spending_tx_invalid.wit.vtxinwit[0].scriptWitness.stack = [leaf_info_invalid.script, control_block_invalid]
 544  
 545          result_invalid = node.testmempoolaccept([spending_tx_invalid.serialize().hex()])[0]
 546          if result_invalid['allowed']:
 547              self.log.info(f"    DEBUG: Depth 8 was unexpectedly accepted: {result_invalid}")
 548          assert_equal(result_invalid['allowed'], False)
 549          self.log.info(f"  ✓ Control block with 8 nodes (289 bytes): REJECTED ({result_invalid['reject-reason']})")
 550  
 551      def test_op_success_rejection(self):
 552          """Test spec 6: Tapscripts including OP_SUCCESS* opcodes are invalid."""
 553          self.log.info("Testing OP_SUCCESS opcode rejection...")
 554          node = self.nodes[0]
 555  
 556          # Generate a Taproot key pair
 557          privkey = generate_privkey()
 558          internal_pubkey, _ = compute_xonly_pubkey(privkey)
 559  
 560          # Test 6.1: Tapscript without OP_SUCCESS (valid baseline)
 561          self.log.info("  Test 6.1: Tapscript without OP_SUCCESS (should be valid)")
 562  
 563          # Create a simple Tapscript: OP_TRUE (always valid)
 564          tapscript_valid = CScript([OP_TRUE])
 565          taproot_info_valid = taproot_construct(internal_pubkey, [("valid", tapscript_valid)])
 566          taproot_spk_valid = taproot_info_valid.scriptPubKey
 567  
 568          # Create and mine funding transaction
 569          funding_tx_valid = self.create_test_transaction(taproot_spk_valid)
 570          funding_txid_valid = funding_tx_valid.rehash()
 571  
 572          block_height = node.getblockcount() + 1
 573          block = create_block(int(node.getbestblockhash(), 16), create_coinbase(block_height), int(node.getblockheader(node.getbestblockhash())['time']) + 1)
 574          block.vtx.append(funding_tx_valid)
 575          add_witness_commitment(block)
 576          block.solve()
 577          node.submitblock(block.serialize().hex())
 578  
 579          # Create spending transaction (script-path)
 580          spending_tx_valid = CTransaction()
 581          spending_tx_valid.vin = [CTxIn(COutPoint(int(funding_txid_valid, 16), 0), nSequence=0)]
 582          output_value = funding_tx_valid.vout[0].nValue - 1000
 583          spending_tx_valid.vout = [CTxOut(output_value, CScript([OP_1, bytes(20)]))]
 584  
 585          # Build witness for script-path spend
 586          leaf_info = taproot_info_valid.leaves["valid"]
 587          control_block = bytes([leaf_info.version + taproot_info_valid.negflag]) + internal_pubkey + leaf_info.merklebranch
 588          spending_tx_valid.wit.vtxinwit.append(CTxInWitness())
 589          spending_tx_valid.wit.vtxinwit[0].scriptWitness.stack = [tapscript_valid, control_block]
 590  
 591          result_valid = node.testmempoolaccept([spending_tx_valid.serialize().hex()])[0]
 592          if not result_valid['allowed']:
 593              self.log.info(f"  DEBUG: Valid Tapscript rejection: {result_valid}")
 594          assert_equal(result_valid['allowed'], True)
 595          self.log.info("  ✓ Tapscript without OP_SUCCESS: ACCEPTED")
 596  
 597          # Test 6.2: Tapscript with OP_SUCCESS (invalid)
 598          self.log.info("  Test 6.2: Tapscript with OP_SUCCESS (should be rejected)")
 599  
 600          # Create a Tapscript with OP_SUCCESS: opcodes 0x50, 0x62, etc.
 601          # IMPORTANT: Use CScriptOp to create the actual opcode, not PUSHDATA
 602          # Testing 0x50 (which is also ANNEX_TAG but different context)
 603          for op_success in [0x50, 0x62, 0x89]:
 604              tapscript_invalid = CScript([CScriptOp(op_success)])
 605              taproot_info_invalid = taproot_construct(internal_pubkey, [("invalid", tapscript_invalid)])
 606              taproot_spk_invalid = taproot_info_invalid.scriptPubKey
 607  
 608              # Create and mine funding transaction
 609              funding_tx_invalid = self.create_test_transaction(taproot_spk_invalid)
 610              funding_txid_invalid = funding_tx_invalid.rehash()
 611  
 612              block_height = node.getblockcount() + 1
 613              block = create_block(int(node.getbestblockhash(), 16), create_coinbase(block_height), int(node.getblockheader(node.getbestblockhash())['time']) + 1)
 614              block.vtx.append(funding_tx_invalid)
 615              add_witness_commitment(block)
 616              block.solve()
 617              node.submitblock(block.serialize().hex())
 618  
 619              # Create spending transaction
 620              spending_tx_invalid = CTransaction()
 621              spending_tx_invalid.vin = [CTxIn(COutPoint(int(funding_txid_invalid, 16), 0), nSequence=0)]
 622              output_value = funding_tx_invalid.vout[0].nValue - 1000
 623              spending_tx_invalid.vout = [CTxOut(output_value, CScript([OP_1, bytes(20)]))]
 624  
 625              # Build witness for script-path spend
 626              leaf_info_invalid = taproot_info_invalid.leaves["invalid"]
 627              control_block_invalid = bytes([leaf_info_invalid.version + taproot_info_invalid.negflag]) + internal_pubkey + leaf_info_invalid.merklebranch
 628              spending_tx_invalid.wit.vtxinwit.append(CTxInWitness())
 629              spending_tx_invalid.wit.vtxinwit[0].scriptWitness.stack = [tapscript_invalid, control_block_invalid]
 630  
 631              result_invalid = node.testmempoolaccept([spending_tx_invalid.serialize().hex()])[0]
 632              if result_invalid['allowed']:
 633                  self.log.info(f"  DEBUG: OP_SUCCESS 0x{op_success:02x} was unexpectedly accepted")
 634              assert_equal(result_invalid['allowed'], False)
 635              self.log.info(f"  ✓ Tapscript with OP_SUCCESS (0x{op_success:02x}): REJECTED ({result_invalid['reject-reason']})")
 636  
 637      def test_op_if_notif_rejection(self):
 638          """Test spec 7: Tapscripts executing OP_IF or OP_NOTIF are invalid."""
 639          self.log.info("Testing OP_IF/OP_NOTIF rejection in Tapscript...")
 640          node = self.nodes[0]
 641  
 642          # Generate a Taproot key pair
 643          privkey = generate_privkey()
 644          internal_pubkey, _ = compute_xonly_pubkey(privkey)
 645  
 646          # Test 7.1: Tapscript with OP_IF (invalid in Tapscript under DEPLOYMENT_REDUCED_DATA)
 647          self.log.info("  Test 7.1: Tapscript with OP_IF (should be rejected)")
 648  
 649          # Create a Tapscript with OP_IF: OP_1 OP_IF OP_1 OP_ENDIF
 650          tapscript_if = CScript([OP_1, OP_IF, OP_1, OP_ENDIF])
 651          taproot_info_if = taproot_construct(internal_pubkey, [("with_if", tapscript_if)])
 652          taproot_spk_if = taproot_info_if.scriptPubKey
 653  
 654          # Create and mine funding transaction
 655          funding_tx_if = self.create_test_transaction(taproot_spk_if)
 656          funding_txid_if = funding_tx_if.rehash()
 657  
 658          block_height = node.getblockcount() + 1
 659          block = create_block(int(node.getbestblockhash(), 16), create_coinbase(block_height), int(node.getblockheader(node.getbestblockhash())['time']) + 1)
 660          block.vtx.append(funding_tx_if)
 661          add_witness_commitment(block)
 662          block.solve()
 663          node.submitblock(block.serialize().hex())
 664  
 665          # Create spending transaction
 666          spending_tx_if = CTransaction()
 667          spending_tx_if.vin = [CTxIn(COutPoint(int(funding_txid_if, 16), 0), nSequence=0)]
 668          output_value = funding_tx_if.vout[0].nValue - 1000
 669          spending_tx_if.vout = [CTxOut(output_value, CScript([OP_1, bytes(20)]))]
 670  
 671          # Build witness for script-path spend
 672          leaf_info_if = taproot_info_if.leaves["with_if"]
 673          control_block_if = bytes([leaf_info_if.version + taproot_info_if.negflag]) + internal_pubkey + leaf_info_if.merklebranch
 674          spending_tx_if.wit.vtxinwit.append(CTxInWitness())
 675          spending_tx_if.wit.vtxinwit[0].scriptWitness.stack = [tapscript_if, control_block_if]
 676  
 677          result_if = node.testmempoolaccept([spending_tx_if.serialize().hex()])[0]
 678          if result_if['allowed']:
 679              self.log.info(f"  DEBUG: OP_IF was unexpectedly accepted: {result_if}")
 680          assert_equal(result_if['allowed'], False)
 681          self.log.info(f"  ✓ Tapscript with OP_IF: REJECTED ({result_if['reject-reason']})")
 682  
 683          # Test 7.2: Tapscript with OP_NOTIF (invalid in Tapscript under DEPLOYMENT_REDUCED_DATA)
 684          self.log.info("  Test 7.2: Tapscript with OP_NOTIF (should be rejected)")
 685  
 686          # Create a Tapscript with OP_NOTIF: OP_0 OP_NOTIF OP_1 OP_ENDIF
 687          tapscript_notif = CScript([OP_0, OP_NOTIF, OP_1, OP_ENDIF])
 688          taproot_info_notif = taproot_construct(internal_pubkey, [("with_notif", tapscript_notif)])
 689          taproot_spk_notif = taproot_info_notif.scriptPubKey
 690  
 691          # Create and mine funding transaction
 692          funding_tx_notif = self.create_test_transaction(taproot_spk_notif)
 693          funding_txid_notif = funding_tx_notif.rehash()
 694  
 695          block_height = node.getblockcount() + 1
 696          block = create_block(int(node.getbestblockhash(), 16), create_coinbase(block_height), int(node.getblockheader(node.getbestblockhash())['time']) + 1)
 697          block.vtx.append(funding_tx_notif)
 698          add_witness_commitment(block)
 699          block.solve()
 700          node.submitblock(block.serialize().hex())
 701  
 702          # Create spending transaction
 703          spending_tx_notif = CTransaction()
 704          spending_tx_notif.vin = [CTxIn(COutPoint(int(funding_txid_notif, 16), 0), nSequence=0)]
 705          output_value = funding_tx_notif.vout[0].nValue - 1000
 706          spending_tx_notif.vout = [CTxOut(output_value, CScript([OP_1, bytes(20)]))]
 707  
 708          # Build witness for script-path spend
 709          leaf_info_notif = taproot_info_notif.leaves["with_notif"]
 710          control_block_notif = bytes([leaf_info_notif.version + taproot_info_notif.negflag]) + internal_pubkey + leaf_info_notif.merklebranch
 711          spending_tx_notif.wit.vtxinwit.append(CTxInWitness())
 712          spending_tx_notif.wit.vtxinwit[0].scriptWitness.stack = [tapscript_notif, control_block_notif]
 713  
 714          result_notif = node.testmempoolaccept([spending_tx_notif.serialize().hex()])[0]
 715          if result_notif['allowed']:
 716              self.log.info(f"  DEBUG: OP_NOTIF was unexpectedly accepted: {result_notif}")
 717          assert_equal(result_notif['allowed'], False)
 718          self.log.info(f"  ✓ Tapscript with OP_NOTIF: REJECTED ({result_notif['reject-reason']})")
 719  
 720      def test_mandatory_flags_cannot_be_bypassed(self):
 721          """Test that REDUCED_DATA consensus-mandatory flags cannot be bypassed via ignore_rejects.
 722  
 723          This test verifies that even though PolicyScriptChecks can be bypassed via ignore_rejects,
 724          the subsequent ConsensusScriptChecks enforces consensus rules and prevents invalid transactions
 725          from entering the mempool.
 726          """
 727          self.log.info("Testing that REDUCED_DATA rules are enforced despite ignore_rejects...")
 728          node = self.nodes[0]
 729  
 730          # Test case: Create a witness script with a 257-byte PUSHDATA (violates REDUCED_DATA)
 731          self.log.info("  Test: 257-byte PUSHDATA in witness script")
 732  
 733          # Create a P2WSH output with a witness script containing 257-byte data push
 734          witness_script_257 = CScript([b'\x00' * 257, OP_DROP, OP_TRUE])
 735          script_pubkey_257 = script_to_p2wsh_script(witness_script_257)
 736  
 737          # Create and fund the output
 738          funding_tx_257 = self.create_test_transaction(script_pubkey_257)
 739          txid_257 = node.sendrawtransaction(funding_tx_257.serialize().hex())
 740          self.generate(node, 1)
 741          output_value_257 = funding_tx_257.vout[0].nValue
 742  
 743          # Create spending transaction that reveals the 257-byte PUSHDATA
 744          spending_tx_257 = CTransaction()
 745          spending_tx_257.vin = [CTxIn(COutPoint(int(txid_257, 16), 0))]
 746          # Add padding to output to ensure tx meets minimum size requirements (82 bytes non-witness)
 747          spending_tx_257.vout = [CTxOut(output_value_257 - 1000, CScript([OP_TRUE, OP_DROP] + [OP_TRUE] * 30))]
 748          spending_tx_257.wit.vtxinwit.append(CTxInWitness())
 749          spending_tx_257.wit.vtxinwit[0].scriptWitness.stack = [witness_script_257]
 750          spending_tx_257.rehash()
 751  
 752          # Test 1: Normal testmempoolaccept should reject
 753          self.log.info("    Test 1a: Normal testmempoolaccept (should reject)")
 754          result_normal = node.testmempoolaccept([spending_tx_257.serialize().hex()])[0]
 755          assert_equal(result_normal['allowed'], False)
 756          assert 'mempool-script-verify-flag' in result_normal['reject-reason']
 757          self.log.info(f"    ✓ Normal testmempoolaccept correctly rejected: {result_normal['reject-reason']}")
 758  
 759          # Test 2: Try to bypass with ignore_rejects=["non-mandatory-script-verify-flag"]
 760          # Expected: Transaction is STILL REJECTED because ConsensusScriptChecks enforces consensus rules
 761          self.log.info("    Test 1b: testmempoolaccept with ignore_rejects")
 762          self.log.info("      This bypasses PolicyScriptChecks but NOT ConsensusScriptChecks")
 763          result_bypass = node.testmempoolaccept(
 764              rawtxs=[spending_tx_257.serialize().hex()],
 765              ignore_rejects=["mempool-script-verify-flag-failed"]
 766          )[0]
 767  
 768          # The transaction should still be rejected because ConsensusScriptChecks
 769          # uses GetBlockScriptFlags() which includes REDUCED_DATA consensus rules
 770          self.log.info(f"    Result: allowed={result_bypass['allowed']}")
 771          assert_equal(result_bypass['allowed'], False)
 772          self.log.info(f"    ✓ Transaction correctly rejected: {result_bypass['reject-reason']}")
 773          self.log.info("    ✓ ConsensusScriptChecks prevents bypass of REDUCED_DATA consensus rules")
 774  
 775      def test_p2wsh_multisig_witness_script_exemption(self):
 776          """Test that a large P2WSH witness script (>256 bytes) is exempted from the element size limit.
 777  
 778          Inspired by mainnet tx a0032427454536006263d237819df5e72fe539a38cb26264ea45a1019fb53bee,
 779          which is a 9-input transaction where each input spends an 11-of-15 P2WSH multisig.
 780  
 781          The witness script for 11-of-15 multisig is ~513 bytes, which exceeds the 256-byte
 782          MAX_SCRIPT_ELEMENT_SIZE_REDUCED limit. However, for P2WSH spends, the witness script
 783          is popped from the stack BEFORE the element size check runs in ExecuteWitnessScript,
 784          so it is implicitly exempted.
 785          """
 786          self.log.info("Testing 11-of-15 P2WSH multisig witness script exemption...")
 787  
 788          node = self.nodes[0]
 789  
 790          # Generate 15 key pairs
 791          privkeys = [generate_privkey() for _ in range(15)]
 792          pubkeys = []
 793          for priv in privkeys:
 794              k = ECKey()
 795              k.set(priv, compressed=True)
 796              pubkeys.append(k.get_pubkey().get_bytes())
 797  
 798          # Build 11-of-15 multisig witness script:
 799          # OP_11 <pub1> <pub2> ... <pub15> OP_15 OP_CHECKMULTISIG
 800          witness_script = CScript([OP_11] + pubkeys + [OP_15, OP_CHECKMULTISIG])
 801          self.log.info(f"  Witness script size: {len(witness_script)} bytes")
 802          assert len(witness_script) > MAX_SCRIPT_ELEMENT_SIZE_REDUCED, \
 803              f"Witness script should exceed 256 bytes, got {len(witness_script)}"
 804  
 805          # Create P2WSH output
 806          script_pubkey = script_to_p2wsh_script(witness_script)
 807  
 808          # Fund the P2WSH output
 809          funding_tx = self.create_test_transaction(script_pubkey)
 810          txid = node.sendrawtransaction(funding_tx.serialize().hex())
 811          self.generate(node, 1)
 812  
 813          # Create spending transaction
 814          spending_tx = CTransaction()
 815          spending_tx.vin = [CTxIn(COutPoint(int(txid, 16), 0))]
 816          output_value = funding_tx.vout[0].nValue - 10000
 817          spending_tx.vout = [CTxOut(output_value, CScript([OP_0, hash160(b'\x01' * 33)]))]
 818  
 819          # Sign with 11 of the 15 keys
 820          spending_tx.wit.vtxinwit = [CTxInWitness()]
 821          sighash = SegwitV0SignatureHash(
 822              witness_script, spending_tx, 0, SIGHASH_ALL, funding_tx.vout[0].nValue
 823          )
 824  
 825          sigs = []
 826          for i in range(11):
 827              k = ECKey()
 828              k.set(privkeys[i], compressed=True)
 829              sig = k.sign_ecdsa(sighash) + b'\x01'  # SIGHASH_ALL
 830              sigs.append(sig)
 831  
 832          # Witness stack: [OP_0_dummy, sig1, ..., sig11, witness_script]
 833          spending_tx.wit.vtxinwit[0].scriptWitness.stack = [b''] + sigs + [witness_script]
 834          spending_tx.rehash()
 835  
 836          # Should be ACCEPTED: witness script is popped before size check
 837          result = node.testmempoolaccept([spending_tx.serialize().hex()])[0]
 838          assert_equal(result['allowed'], True)
 839          self.log.info("  PASS: 11-of-15 P2WSH multisig accepted under reduced_data")
 840  
 841      def test_tapscript_script_exemption(self):
 842          """Test that a large tapleaf script (>256 bytes) is exempted from the element size limit.
 843  
 844          Similar to P2WSH, for tapscript spends the tapleaf script is popped from the
 845          witness stack BEFORE the element size check runs in ExecuteWitnessScript,
 846          so it is implicitly exempted.
 847          """
 848          self.log.info("Testing tapleaf script size exemption...")
 849  
 850          node = self.nodes[0]
 851  
 852          # Build a tapscript >256 bytes using repeated <data> OP_DROP, ending with OP_TRUE
 853          # Each data push is ≤256 bytes (valid), but the total script exceeds 256 bytes.
 854          large_tapscript = CScript([b'\x00' * 200, OP_DROP, b'\x00' * 200, OP_DROP, OP_TRUE])
 855          assert len(large_tapscript) > MAX_SCRIPT_ELEMENT_SIZE_REDUCED, \
 856              f"Tapscript should exceed 256 bytes, got {len(large_tapscript)}"
 857          self.log.info(f"  Tapleaf script size: {len(large_tapscript)} bytes")
 858  
 859          # Construct taproot output with this script as a leaf
 860          privkey = generate_privkey()
 861          internal_pubkey, _ = compute_xonly_pubkey(privkey)
 862          taproot_info = taproot_construct(internal_pubkey, [("large_script", large_tapscript)])
 863          taproot_spk = taproot_info.scriptPubKey
 864  
 865          # Fund the taproot output
 866          funding_tx = self.create_test_transaction(taproot_spk)
 867          funding_txid = funding_tx.rehash()
 868  
 869          block_height = node.getblockcount() + 1
 870          block = create_block(int(node.getbestblockhash(), 16), create_coinbase(block_height), int(node.getblockheader(node.getbestblockhash())['time']) + 1)
 871          block.vtx.append(funding_tx)
 872          add_witness_commitment(block)
 873          block.solve()
 874          node.submitblock(block.serialize().hex())
 875  
 876          # Spend via script path
 877          leaf_info = taproot_info.leaves["large_script"]
 878          control_block = bytes([leaf_info.version + taproot_info.negflag]) + internal_pubkey + leaf_info.merklebranch
 879  
 880          spending_tx = CTransaction()
 881          spending_tx.vin = [CTxIn(COutPoint(int(funding_txid, 16), 0), nSequence=0)]
 882          output_value = funding_tx.vout[0].nValue - 1000
 883          spending_tx.vout = [CTxOut(output_value, CScript([OP_1, bytes(20)]))]
 884  
 885          # Witness stack: [<empty stack for script execution>, script, control_block]
 886          # The script just does <data> DROP <data> DROP TRUE, so no stack inputs needed
 887          spending_tx.wit.vtxinwit.append(CTxInWitness())
 888          spending_tx.wit.vtxinwit[0].scriptWitness.stack = [large_tapscript, control_block]
 889  
 890          spending_tx.rehash()
 891  
 892          # Should be ACCEPTED: tapleaf script is popped before size check
 893          result = node.testmempoolaccept([spending_tx.serialize().hex()])[0]
 894          assert_equal(result['allowed'], True)
 895          self.log.info("  PASS: >256-byte tapleaf script accepted under reduced_data")
 896  
 897      def test_generation_output_size_limit(self):
 898          """Test that generation tx outputs are also subject to output size limits."""
 899          self.log.info("Testing generation tx output scriptPubKey size limits...")
 900  
 901          node = self.nodes[0]
 902  
 903          def create_block_with_generation_output(script_pubkey):
 904              """Helper to create a block with a custom generation tx output script."""
 905              tip = node.getbestblockhash()
 906              height = node.getblockcount() + 1
 907              tip_header = node.getblockheader(tip)
 908              block_time = tip_header['time'] + 1
 909              coinbase = create_coinbase(height, script_pubkey=script_pubkey)
 910              block = create_block(int(tip, 16), coinbase, ntime=block_time)
 911              add_witness_commitment(block)
 912              block.solve()
 913              return block
 914  
 915          # Test 1: 34-byte P2WSH generation tx output (exactly at limit - should pass)
 916          self.log.info("  Test: 34-byte P2WSH generation tx output (at limit)")
 917          witness_program_32 = b'\x00' * 32
 918          script_p2wsh = CScript([OP_0, witness_program_32])
 919          assert_equal(len(script_p2wsh), 34)
 920  
 921          block_valid = create_block_with_generation_output(script_p2wsh)
 922          result = node.submitblock(block_valid.serialize().hex())
 923          assert_equal(result, None)
 924          self.log.info("  ✓ 34-byte P2WSH generation tx output accepted")
 925  
 926          # Test 2: 35-byte P2PK generation tx output (exceeds limit - should fail)
 927          self.log.info("  Test: 35-byte P2PK generation tx output (exceeds limit)")
 928          pubkey_33 = b'\x02' + b'\x00' * 32  # Compressed pubkey format
 929          script_p2pk = CScript([pubkey_33, OP_CHECKSIG])
 930          assert_equal(len(script_p2pk), 35)
 931  
 932          block_invalid = create_block_with_generation_output(script_p2pk)
 933          result = node.submitblock(block_invalid.serialize().hex())
 934          assert_equal(result, 'bad-txns-vout-script-toolarge')
 935          self.log.info("  ✓ 35-byte P2PK generation tx output rejected")
 936  
 937          # Test 3: Generation tx with OP_RETURN at 83 bytes (at OP_RETURN limit - should pass)
 938          self.log.info("  Test: Generation tx with 83-byte OP_RETURN extra output (at limit)")
 939          # 80 bytes data = OP_RETURN (1) + push opcode (1) + data (80) = 82 bytes
 940          # We need 83 bytes, so use 81 bytes of data with PUSHDATA1
 941          # OP_RETURN (1) + OP_PUSHDATA1 (1) + len (1) + data (80) = 83 bytes
 942          data_80 = b'\x00' * 80
 943          script_opreturn_83 = CScript([OP_RETURN, data_80])
 944          # Verify we're at exactly 83 bytes (with CScript's encoding)
 945          self.log.info(f"    OP_RETURN script length: {len(script_opreturn_83)} bytes")
 946  
 947          # Create block with valid main output and OP_RETURN extra output
 948          tip = node.getbestblockhash()
 949          height = node.getblockcount() + 1
 950          tip_header = node.getblockheader(tip)
 951          block_time = tip_header['time'] + 1
 952          coinbase = create_coinbase(height, extra_output_script=script_opreturn_83)
 953          block_opreturn_valid = create_block(int(tip, 16), coinbase, ntime=block_time)
 954          add_witness_commitment(block_opreturn_valid)
 955          block_opreturn_valid.solve()
 956  
 957          result = node.submitblock(block_opreturn_valid.serialize().hex())
 958          if result is None:
 959              self.log.info("  ✓ Generation tx with 83-byte OP_RETURN output accepted")
 960          else:
 961              self.log.info(f"  Note: Generation tx OP_RETURN result: {result}")
 962  
 963          # Test 4: Generation tx with OP_RETURN at 84 bytes (exceeds limit - should fail)
 964          self.log.info("  Test: Generation tx with 84-byte OP_RETURN extra output (exceeds limit)")
 965          # 81 bytes data = OP_RETURN (1) + OP_PUSHDATA1 (1) + len (1) + data (81) = 84 bytes
 966          data_81 = b'\x00' * 81
 967          script_opreturn_84 = CScript([OP_RETURN, data_81])
 968          self.log.info(f"    OP_RETURN script length: {len(script_opreturn_84)} bytes")
 969  
 970          tip = node.getbestblockhash()
 971          height = node.getblockcount() + 1
 972          tip_header = node.getblockheader(tip)
 973          block_time = tip_header['time'] + 1
 974          coinbase = create_coinbase(height, extra_output_script=script_opreturn_84)
 975          block_opreturn_invalid = create_block(int(tip, 16), coinbase, ntime=block_time)
 976          add_witness_commitment(block_opreturn_invalid)
 977          block_opreturn_invalid.solve()
 978  
 979          result = node.submitblock(block_opreturn_invalid.serialize().hex())
 980          assert_equal(result, 'bad-txns-vout-script-toolarge')
 981          self.log.info("  ✓ Generation tx with 84-byte OP_RETURN output rejected")
 982  
 983      def test_p2a_witness_rejected(self):
 984          """Test that P2A (PayToAnchor) spends with non-empty witness are rejected."""
 985          self.log.info("Testing P2A non-empty witness rejection...")
 986          node = self.nodes[0]
 987  
 988          # Create a P2A output (4 bytes, within the 34-byte limit)
 989          p2a_funding = self.create_test_transaction(PAY_TO_ANCHOR)
 990          p2a_funding.rehash()
 991          p2a_value = p2a_funding.vout[0].nValue
 992  
 993          block_height = node.getblockcount() + 1
 994          block = create_block(int(node.getbestblockhash(), 16), create_coinbase(block_height), int(node.getblockheader(node.getbestblockhash())['time']) + 1)
 995          block.vtx.append(p2a_funding)
 996          add_witness_commitment(block)
 997          block.solve()
 998          assert_equal(node.submitblock(block.serialize().hex()), None)
 999          self.log.info("  P2A output created")
1000  
1001          # Test 1: Spend with 100 KB of arbitrary witness data (must be rejected)
1002          self.log.info("  Test: P2A spend with large arbitrary witness (should be rejected)")
1003          arbitrary_data = b'\xab' * 100_000
1004  
1005          p2a_spend = CTransaction()
1006          p2a_spend.vin = [CTxIn(COutPoint(int(p2a_funding.rehash(), 16), 0))]
1007          p2a_spend.vout = [CTxOut(p2a_value - 1000, CScript([OP_0, hash160(b'\x01' * 33)]))]
1008          p2a_spend.wit.vtxinwit = [CTxInWitness()]
1009          p2a_spend.wit.vtxinwit[0].scriptWitness.stack = [arbitrary_data]
1010          p2a_spend.rehash()
1011  
1012          block_height = node.getblockcount() + 1
1013          block_bad = create_block(int(node.getbestblockhash(), 16), create_coinbase(block_height), int(node.getblockheader(node.getbestblockhash())['time']) + 1)
1014          block_bad.vtx.append(p2a_spend)
1015          add_witness_commitment(block_bad)
1016          block_bad.solve()
1017  
1018          result = node.submitblock(block_bad.serialize().hex())
1019          assert result is not None
1020          assert_equal(node.getblockcount(), block_height - 1)
1021          self.log.info(f"  ✓ P2A spend with 100 KB witness rejected ({result})")
1022  
1023          # Test 2: Spend with empty witness (must still be accepted)
1024          self.log.info("  Test: P2A spend with empty witness (should be accepted)")
1025          p2a_spend_empty = CTransaction()
1026          p2a_spend_empty.vin = [CTxIn(COutPoint(int(p2a_funding.rehash(), 16), 0))]
1027          p2a_spend_empty.vout = [CTxOut(p2a_value - 1000, CScript([OP_0, hash160(b'\x01' * 33)]))]
1028          p2a_spend_empty.wit.vtxinwit = [CTxInWitness()]
1029          p2a_spend_empty.wit.vtxinwit[0].scriptWitness.stack = []
1030          p2a_spend_empty.rehash()
1031  
1032          block_height = node.getblockcount() + 1
1033          block_good = create_block(int(node.getbestblockhash(), 16), create_coinbase(block_height), int(node.getblockheader(node.getbestblockhash())['time']) + 1)
1034          block_good.vtx.append(p2a_spend_empty)
1035          add_witness_commitment(block_good)
1036          block_good.solve()
1037  
1038          assert_equal(node.submitblock(block_good.serialize().hex()), None)
1039          assert_equal(node.getblockcount(), block_height)
1040          self.log.info("  ✓ P2A spend with empty witness accepted")
1041  
1042      def run_test(self):
1043          self.init_test()
1044  
1045          # Run all spec tests
1046          self.test_output_script_size_limit()
1047          self.test_generation_output_size_limit()
1048          self.test_pushdata_size_limit()
1049          self.test_undefined_witness_versions()
1050          self.test_taproot_annex_rejection()
1051          self.test_taproot_control_block_size()
1052          self.test_op_success_rejection()
1053          self.test_op_if_notif_rejection()
1054          self.test_mandatory_flags_cannot_be_bypassed()
1055          self.test_p2a_witness_rejected()
1056          self.test_p2wsh_multisig_witness_script_exemption()
1057          self.test_tapscript_script_exemption()
1058  
1059          self.log.info("All ReducedData tests completed")
1060  
1061  
1062  if __name__ == '__main__':
1063      ReducedDataTest(__file__).main()
1064