feature_nulldummy.py
1 #!/usr/bin/env python3 2 # Copyright (c) 2016-2022 The Bitcoin Core 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 NULLDUMMY softfork. 6 7 Connect to a single node. 8 Generate 2 blocks (save the coinbases for later). 9 Generate COINBASE_MATURITY (CB) more blocks to ensure the coinbases are mature. 10 [Policy/Consensus] Check that NULLDUMMY compliant transactions are accepted in block CB + 3. 11 [Policy] Check that non-NULLDUMMY transactions are rejected before activation. 12 [Consensus] Check that the new NULLDUMMY rules are not enforced on block CB + 4. 13 [Policy/Consensus] Check that the new NULLDUMMY rules are enforced on block CB + 5. 14 """ 15 import time 16 17 from test_framework.address import address_to_scriptpubkey 18 from test_framework.blocktools import ( 19 COINBASE_MATURITY, 20 NORMAL_GBT_REQUEST_PARAMS, 21 add_witness_commitment, 22 create_block, 23 ) 24 from test_framework.messages import ( 25 CTransaction, 26 tx_from_hex, 27 ) 28 from test_framework.script import ( 29 OP_0, 30 OP_TRUE, 31 ) 32 from test_framework.test_framework import BitcoinTestFramework 33 from test_framework.util import ( 34 assert_equal, 35 assert_raises_rpc_error, 36 ) 37 from test_framework.wallet import getnewdestination 38 from test_framework.wallet_util import generate_keypair 39 40 NULLDUMMY_ERROR = "mandatory-script-verify-flag-failed (Dummy CHECKMULTISIG argument must be zero)" 41 42 43 def invalidate_nulldummy_tx(tx): 44 """Transform a NULLDUMMY compliant tx (i.e. scriptSig starts with OP_0) 45 to be non-NULLDUMMY compliant by replacing the dummy with OP_TRUE""" 46 assert_equal(tx.vin[0].scriptSig[0], OP_0) 47 tx.vin[0].scriptSig = bytes([OP_TRUE]) + tx.vin[0].scriptSig[1:] 48 tx.rehash() 49 50 51 class NULLDUMMYTest(BitcoinTestFramework): 52 def set_test_params(self): 53 self.num_nodes = 1 54 self.setup_clean_chain = True 55 # This script tests NULLDUMMY activation, which is part of the 'segwit' deployment, so we go through 56 # normal segwit activation here (and don't use the default always-on behaviour). 57 self.extra_args = [[ 58 f'-testactivationheight=segwit@{COINBASE_MATURITY + 5}', 59 '-addresstype=legacy', 60 '-par=1', # Use only one script thread to get the exact reject reason for testing 61 ]] 62 63 def create_transaction(self, *, txid, input_details=None, addr, amount, privkey): 64 input = {"txid": txid, "vout": 0} 65 output = {addr: amount} 66 rawtx = self.nodes[0].createrawtransaction([input], output) 67 # Details only needed for scripthash or witness spends 68 input = None if not input_details else [{**input, **input_details}] 69 signedtx = self.nodes[0].signrawtransactionwithkey(rawtx, [privkey], input) 70 return tx_from_hex(signedtx["hex"]) 71 72 def run_test(self): 73 self.privkey, self.pubkey = generate_keypair(wif=True) 74 cms = self.nodes[0].createmultisig(1, [self.pubkey.hex()]) 75 wms = self.nodes[0].createmultisig(1, [self.pubkey.hex()], 'p2sh-segwit') 76 self.ms_address = cms["address"] 77 ms_unlock_details = {"scriptPubKey": address_to_scriptpubkey(self.ms_address).hex(), 78 "redeemScript": cms["redeemScript"]} 79 self.wit_ms_address = wms['address'] 80 81 self.coinbase_blocks = self.generate(self.nodes[0], 2) # block height = 2 82 coinbase_txid = [] 83 for i in self.coinbase_blocks: 84 coinbase_txid.append(self.nodes[0].getblock(i)['tx'][0]) 85 self.generate(self.nodes[0], COINBASE_MATURITY) # block height = COINBASE_MATURITY + 2 86 self.lastblockhash = self.nodes[0].getbestblockhash() 87 self.lastblockheight = COINBASE_MATURITY + 2 88 self.lastblocktime = int(time.time()) + self.lastblockheight 89 90 self.log.info(f"Test 1: NULLDUMMY compliant base transactions should be accepted to mempool and mined before activation [{COINBASE_MATURITY + 3}]") 91 test1txs = [self.create_transaction(txid=coinbase_txid[0], addr=self.ms_address, amount=49, 92 privkey=self.nodes[0].get_deterministic_priv_key().key)] 93 txid1 = self.nodes[0].sendrawtransaction(test1txs[0].serialize_with_witness().hex(), 0) 94 test1txs.append(self.create_transaction(txid=txid1, input_details=ms_unlock_details, 95 addr=self.ms_address, amount=48, 96 privkey=self.privkey)) 97 txid2 = self.nodes[0].sendrawtransaction(test1txs[1].serialize_with_witness().hex(), 0) 98 test1txs.append(self.create_transaction(txid=coinbase_txid[1], 99 addr=self.wit_ms_address, amount=49, 100 privkey=self.nodes[0].get_deterministic_priv_key().key)) 101 txid3 = self.nodes[0].sendrawtransaction(test1txs[2].serialize_with_witness().hex(), 0) 102 self.block_submit(self.nodes[0], test1txs, accept=True) 103 104 self.log.info("Test 2: Non-NULLDUMMY base multisig transaction should not be accepted to mempool before activation") 105 test2tx = self.create_transaction(txid=txid2, input_details=ms_unlock_details, 106 addr=self.ms_address, amount=47, 107 privkey=self.privkey) 108 invalidate_nulldummy_tx(test2tx) 109 assert_raises_rpc_error(-26, NULLDUMMY_ERROR, self.nodes[0].sendrawtransaction, test2tx.serialize_with_witness().hex(), 0) 110 111 self.log.info(f"Test 3: Non-NULLDUMMY base transactions should be accepted in a block before activation [{COINBASE_MATURITY + 4}]") 112 self.block_submit(self.nodes[0], [test2tx], accept=True) 113 114 self.log.info("Test 4: Non-NULLDUMMY base multisig transaction is invalid after activation") 115 test4tx = self.create_transaction(txid=test2tx.hash, input_details=ms_unlock_details, 116 addr=getnewdestination()[2], amount=46, 117 privkey=self.privkey) 118 test6txs = [CTransaction(test4tx)] 119 invalidate_nulldummy_tx(test4tx) 120 assert_raises_rpc_error(-26, NULLDUMMY_ERROR, self.nodes[0].sendrawtransaction, test4tx.serialize_with_witness().hex(), 0) 121 self.block_submit(self.nodes[0], [test4tx], accept=False) 122 123 self.log.info("Test 5: Non-NULLDUMMY P2WSH multisig transaction invalid after activation") 124 test5tx = self.create_transaction(txid=txid3, input_details={"scriptPubKey": test1txs[2].vout[0].scriptPubKey.hex(), 125 "amount": 49, "witnessScript": wms["redeemScript"]}, 126 addr=getnewdestination(address_type='p2sh-segwit')[2], amount=48, 127 privkey=self.privkey) 128 test6txs.append(CTransaction(test5tx)) 129 test5tx.wit.vtxinwit[0].scriptWitness.stack[0] = b'\x01' 130 assert_raises_rpc_error(-26, NULLDUMMY_ERROR, self.nodes[0].sendrawtransaction, test5tx.serialize_with_witness().hex(), 0) 131 self.block_submit(self.nodes[0], [test5tx], with_witness=True, accept=False) 132 133 self.log.info(f"Test 6: NULLDUMMY compliant base/witness transactions should be accepted to mempool and in block after activation [{COINBASE_MATURITY + 5}]") 134 for i in test6txs: 135 self.nodes[0].sendrawtransaction(i.serialize_with_witness().hex(), 0) 136 self.block_submit(self.nodes[0], test6txs, with_witness=True, accept=True) 137 138 def block_submit(self, node, txs, *, with_witness=False, accept): 139 tmpl = node.getblocktemplate(NORMAL_GBT_REQUEST_PARAMS) 140 assert_equal(tmpl['previousblockhash'], self.lastblockhash) 141 assert_equal(tmpl['height'], self.lastblockheight + 1) 142 block = create_block(tmpl=tmpl, ntime=self.lastblocktime + 1, txlist=txs) 143 if with_witness: 144 add_witness_commitment(block) 145 block.solve() 146 assert_equal(None if accept else NULLDUMMY_ERROR, node.submitblock(block.serialize().hex())) 147 if accept: 148 assert_equal(node.getbestblockhash(), block.hash) 149 self.lastblockhash = block.hash 150 self.lastblocktime += 1 151 self.lastblockheight += 1 152 else: 153 assert_equal(node.getbestblockhash(), self.lastblockhash) 154 155 156 if __name__ == '__main__': 157 NULLDUMMYTest().main()