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