diff --git a/test/functional/feature_block.py b/test/functional/feature_block.py index de0d62abcb..91cd02281e 100755 --- a/test/functional/feature_block.py +++ b/test/functional/feature_block.py @@ -73,6 +73,10 @@ class CBrokenBlock(CBlock): def normal_serialize(self): return super().serialize() + +DUPLICATE_COINBASE_SCRIPT_SIG = b'\x01\x78' # Valid for block at height 120 + + class FullBlockTest(BitcoinTestFramework): def set_test_params(self): self.num_nodes = 1 @@ -95,6 +99,13 @@ class FullBlockTest(BitcoinTestFramework): self.spendable_outputs = [] # Create a new block + b_dup_cb = self.next_block('dup_cb') + b_dup_cb.vtx[0].vin[0].scriptSig = DUPLICATE_COINBASE_SCRIPT_SIG + b_dup_cb.vtx[0].rehash() + duplicate_tx = b_dup_cb.vtx[0] + b_dup_cb = self.update_block('dup_cb', []) + self.send_blocks([b_dup_cb]) + b0 = self.next_block(0) self.save_spendable_output() self.send_blocks([b0]) @@ -767,7 +778,7 @@ class FullBlockTest(BitcoinTestFramework): # Test a few invalid tx types # - # -> b35 (10) -> b39 (11) -> b42 (12) -> b43 (13) -> b53 (14) -> b55 (15) -> b57 (16) -> b60 (17) + # -> b35 (10) -> b39 (11) -> b42 (12) -> b43 (13) -> b53 (14) -> b55 (15) -> b57 (16) -> b60 () # \-> ??? (17) # @@ -793,14 +804,14 @@ class FullBlockTest(BitcoinTestFramework): # reset to good chain self.move_tip(57) - b60 = self.next_block(60, spend=out[17]) + b60 = self.next_block(60) self.send_blocks([b60], True) self.save_spendable_output() - # Test BIP30 + # Test BIP30 (reject duplicate) # - # -> b39 (11) -> b42 (12) -> b43 (13) -> b53 (14) -> b55 (15) -> b57 (16) -> b60 (17) - # \-> b61 (18) + # -> b39 (11) -> b42 (12) -> b43 (13) -> b53 (14) -> b55 (15) -> b57 (16) -> b60 () + # \-> b61 () # # Blocks are not allowed to contain a transaction whose id matches that of an earlier, # not-fully-spent transaction in the same chain. To test, make identical coinbases; @@ -808,20 +819,44 @@ class FullBlockTest(BitcoinTestFramework): # self.log.info("Reject a block with a transaction with a duplicate hash of a previous transaction (BIP30)") self.move_tip(60) - b61 = self.next_block(61, spend=out[18]) - b61.vtx[0].vin[0].scriptSig = b60.vtx[0].vin[0].scriptSig # Equalize the coinbases + b61 = self.next_block(61) + b61.vtx[0].vin[0].scriptSig = DUPLICATE_COINBASE_SCRIPT_SIG b61.vtx[0].rehash() b61 = self.update_block(61, []) - assert_equal(b60.vtx[0].serialize(), b61.vtx[0].serialize()) + assert_equal(duplicate_tx.serialize(), b61.vtx[0].serialize()) self.send_blocks([b61], success=False, reject_reason='bad-txns-BIP30', reconnect=True) + # Test BIP30 (allow duplicate if spent) + # + # -> b57 (16) -> b60 () + # \-> b_spend_dup_cb (b_dup_cb) -> b_dup_2 () + # + self.move_tip(57) + b_spend_dup_cb = self.next_block('spend_dup_cb') + tx = CTransaction() + tx.vin.append(CTxIn(COutPoint(duplicate_tx.sha256, 0))) + tx.vout.append(CTxOut(duplicate_tx.vout[0].nValue, CScript([OP_TRUE]))) + self.sign_tx(tx, duplicate_tx) + tx.rehash() + b_spend_dup_cb = self.update_block('spend_dup_cb', [tx]) + + b_dup_2 = self.next_block('dup_2') + b_dup_2.vtx[0].vin[0].scriptSig = DUPLICATE_COINBASE_SCRIPT_SIG + b_dup_2.vtx[0].rehash() + b_dup_2 = self.update_block('dup_2', []) + assert_equal(duplicate_tx.serialize(), b_dup_2.vtx[0].serialize()) + assert_equal(self.nodes[0].gettxout(txid=duplicate_tx.hash, n=0)['confirmations'], 119) + self.send_blocks([b_spend_dup_cb, b_dup_2], success=True) + # The duplicate has less confirmations + assert_equal(self.nodes[0].gettxout(txid=duplicate_tx.hash, n=0)['confirmations'], 1) + # Test tx.isFinal is properly rejected (not an exhaustive tx.isFinal test, that should be in data-driven transaction tests) # - # -> b39 (11) -> b42 (12) -> b43 (13) -> b53 (14) -> b55 (15) -> b57 (16) -> b60 (17) - # \-> b62 (18) + # -> b_spend_dup_cb (b_dup_cb) -> b_dup_2 () + # \-> b62 (18) # self.log.info("Reject a block with a transaction with a nonfinal locktime") - self.move_tip(60) + self.move_tip('dup_2') b62 = self.next_block(62) tx = CTransaction() tx.nLockTime = 0xffffffff # this locktime is non-final @@ -834,11 +869,11 @@ class FullBlockTest(BitcoinTestFramework): # Test a non-final coinbase is also rejected # - # -> b39 (11) -> b42 (12) -> b43 (13) -> b53 (14) -> b55 (15) -> b57 (16) -> b60 (17) - # \-> b63 (-) + # -> b_spend_dup_cb (b_dup_cb) -> b_dup_2 () + # \-> b63 (-) # self.log.info("Reject a block with a coinbase transaction with a nonfinal locktime") - self.move_tip(60) + self.move_tip('dup_2') b63 = self.next_block(63) b63.vtx[0].nLockTime = 0xffffffff b63.vtx[0].vin[0].nSequence = 0xDEADBEEF @@ -854,14 +889,14 @@ class FullBlockTest(BitcoinTestFramework): # What matters is that the receiving node should not reject the bloated block, and then reject the canonical # block on the basis that it's the same as an already-rejected block (which would be a consensus failure.) # - # -> b39 (11) -> b42 (12) -> b43 (13) -> b53 (14) -> b55 (15) -> b57 (16) -> b60 (17) -> b64 (18) - # \ - # b64a (18) + # -> b_spend_dup_cb (b_dup_cb) -> b_dup_2 () -> b64 (18) + # \ + # b64a (18) # b64a is a bloated block (non-canonical varint) # b64 is a good block (same as b64 but w/ canonical varint) # self.log.info("Accept a valid block even if a bloated version of the block has previously been sent") - self.move_tip(60) + self.move_tip('dup_2') regular_block = self.next_block("64a", spend=out[18]) # make it a "broken_block," with non-canonical serialization @@ -889,7 +924,7 @@ class FullBlockTest(BitcoinTestFramework): node.disconnect_p2ps() self.reconnect_p2p() - self.move_tip(60) + self.move_tip('dup_2') b64 = CBlock(b64a) b64.vtx = copy.deepcopy(b64a.vtx) assert_equal(b64.hash, b64a.hash) @@ -901,7 +936,7 @@ class FullBlockTest(BitcoinTestFramework): # Spend an output created in the block itself # - # -> b42 (12) -> b43 (13) -> b53 (14) -> b55 (15) -> b57 (16) -> b60 (17) -> b64 (18) -> b65 (19) + # -> b_dup_2 () -> b64 (18) -> b65 (19) # self.log.info("Accept a block with a transaction spending an output created in the same block") self.move_tip(64) @@ -915,8 +950,8 @@ class FullBlockTest(BitcoinTestFramework): # Attempt to spend an output created later in the same block # - # -> b43 (13) -> b53 (14) -> b55 (15) -> b57 (16) -> b60 (17) -> b64 (18) -> b65 (19) - # \-> b66 (20) + # -> b64 (18) -> b65 (19) + # \-> b66 (20) self.log.info("Reject a block with a transaction spending an output created later in the same block") self.move_tip(65) b66 = self.next_block(66) @@ -927,8 +962,8 @@ class FullBlockTest(BitcoinTestFramework): # Attempt to double-spend a transaction created in a block # - # -> b43 (13) -> b53 (14) -> b55 (15) -> b57 (16) -> b60 (17) -> b64 (18) -> b65 (19) - # \-> b67 (20) + # -> b64 (18) -> b65 (19) + # \-> b67 (20) # # self.log.info("Reject a block with a transaction double spending a transaction created in the same block") @@ -942,8 +977,8 @@ class FullBlockTest(BitcoinTestFramework): # More tests of block subsidy # - # -> b43 (13) -> b53 (14) -> b55 (15) -> b57 (16) -> b60 (17) -> b64 (18) -> b65 (19) -> b69 (20) - # \-> b68 (20) + # -> b64 (18) -> b65 (19) -> b69 (20) + # \-> b68 (20) # # b68 - coinbase with an extra 10 satoshis, # creates a tx that has 9 satoshis from out[20] go to fees @@ -969,8 +1004,8 @@ class FullBlockTest(BitcoinTestFramework): # Test spending the outpoint of a non-existent transaction # - # -> b53 (14) -> b55 (15) -> b57 (16) -> b60 (17) -> b64 (18) -> b65 (19) -> b69 (20) - # \-> b70 (21) + # -> b65 (19) -> b69 (20) + # \-> b70 (21) # self.log.info("Reject a block containing a transaction spending from a non-existent input") self.move_tip(69) @@ -985,8 +1020,8 @@ class FullBlockTest(BitcoinTestFramework): # Test accepting an invalid block which has the same hash as a valid one (via merkle tree tricks) # - # -> b53 (14) -> b55 (15) -> b57 (16) -> b60 (17) -> b64 (18) -> b65 (19) -> b69 (20) -> b72 (21) - # \-> b71 (21) + # -> b65 (19) -> b69 (20) -> b72 (21) + # \-> b71 (21) # # b72 is a good block. # b71 is a copy of 72, but re-adds one of its transactions. However, it has the same hash as b72. @@ -1014,8 +1049,8 @@ class FullBlockTest(BitcoinTestFramework): # Test some invalid scripts and MAX_BLOCK_SIGOPS # - # -> b55 (15) -> b57 (16) -> b60 (17) -> b64 (18) -> b65 (19) -> b69 (20) -> b72 (21) - # \-> b** (22) + # -> b69 (20) -> b72 (21) + # \-> b** (22) # # b73 - tx with excessive sigops that are placed after an excessively large script element. diff --git a/test/functional/test_framework/blocktools.py b/test/functional/test_framework/blocktools.py index 39f2560379..2b26f390c8 100644 --- a/test/functional/test_framework/blocktools.py +++ b/test/functional/test_framework/blocktools.py @@ -23,7 +23,6 @@ from .messages import ( ToHex, hash256, hex_str_to_bytes, - ser_string, ser_uint256, sha256, uint256_from_str, @@ -32,6 +31,7 @@ from .messages import ( from .script import ( CScript, CScriptNum, + CScriptOp, OP_0, OP_1, OP_CHECKMULTISIG, @@ -53,7 +53,10 @@ WITNESS_COMMITMENT_HEADER = b"\xaa\x21\xa9\xed" # Assumes a BIP34 valid commitment exists def get_coinbase_height(coinbase): - return CScriptNum.decode(coinbase.vin[0].scriptSig) + if CScriptOp.is_small_int(coinbase.vin[0].scriptSig[0]): + return CScriptOp.decode_op_n(coinbase.vin[0].scriptSig[0]) + else: + return CScriptNum.decode(coinbase.vin[0].scriptSig) def create_block(hashprev, coinbase, ntime=None, *, version=1): """Create a block (with regtest difficulty).""" @@ -104,20 +107,14 @@ def add_witness_commitment(block, nonce=0): block.hashMerkleRoot = block.calc_merkle_root() block.rehash() -def serialize_script_num(value): - r = bytearray(0) - if value == 0: - return r - neg = value < 0 - absvalue = -value if neg else value - while (absvalue): - r.append(int(absvalue & 0xff)) - absvalue >>= 8 - if r[-1] & 0x80: - r.append(0x80 if neg else 0) - elif neg: - r[-1] |= 0x80 - return r + +def script_BIP34_coinbase_height(height): + if height <= 16: + res = CScriptOp.encode_op_n(height) + # Append dummy to increase scriptSig size above 2 (see bad-cb-length consensus rule) + return CScript([res, OP_1]) + return CScript([CScriptNum(height)]) + def create_coinbase(height, pubkey=None): """Create a coinbase transaction, assuming no miner fees. @@ -125,8 +122,7 @@ def create_coinbase(height, pubkey=None): If pubkey is passed in, the coinbase output will be a P2PK output; otherwise an anyone-can-spend output.""" coinbase = CTransaction() - coinbase.vin.append(CTxIn(COutPoint(0, 0xffffffff), - ser_string(serialize_script_num(height)), 0xffffffff)) + coinbase.vin.append(CTxIn(COutPoint(0, 0xffffffff), script_BIP34_coinbase_height(height), 0xffffffff)) coinbaseoutput = CTxOut() value = 50 * COIN halvings = int(height / 150) # regtest