diff --git a/src/consensus/activation.h b/src/consensus/activation.h --- a/src/consensus/activation.h +++ b/src/consensus/activation.h @@ -25,4 +25,6 @@ */ bool IsMagneticAnomalyEnabled(const Config &config, int64_t nMedianTimePast); +bool IsNullDummyEnforced(const Config &config, const CBlockIndex *pindexPrev); + #endif // BITCOIN_CONSENSUS_ACTIVATION_H diff --git a/src/consensus/activation.cpp b/src/consensus/activation.cpp --- a/src/consensus/activation.cpp +++ b/src/consensus/activation.cpp @@ -48,3 +48,14 @@ return IsMagneticAnomalyEnabled(config, pindexPrev->GetMedianTimePast()); } + +bool IsNullDummyEnforced(const Config &config, const CBlockIndex *pindexPrev) { + if (pindexPrev == nullptr) { + return false; + } + int64_t activationTime = gArgs.GetArg("-nulldummyenforcetime", 0); + if (activationTime == 0) { + return false; + } + return pindexPrev->GetMedianTimePast() >= activationTime; +} diff --git a/src/validation.cpp b/src/validation.cpp --- a/src/validation.cpp +++ b/src/validation.cpp @@ -1575,6 +1575,10 @@ flags |= SCRIPT_VERIFY_CLEANSTACK; } + if (IsNullDummyEnforced(config, pChainTip)) { + flags |= SCRIPT_VERIFY_NULLDUMMY; + } + // We make sure this node will have replay protection during the next hard // fork. if (IsReplayProtectionEnabled(config, pChainTip)) { @@ -1916,6 +1920,14 @@ g_mempool.clear(); } + // If non-standard NULLDUMMY transactions were accepted before enforcement, + // then we risk having invalid tranasctions in the mempool. Clear it to be + // safe. + if (!fRequireStandard && (IsNullDummyEnforced(config, pindex)) && + !IsNullDummyEnforced(config, pindex->pprev)) { + g_mempool.clear(); + } + return true; } diff --git a/test/functional/abc-somecodename-activation.py b/test/functional/abc-somecodename-activation.py new file mode 100755 --- /dev/null +++ b/test/functional/abc-somecodename-activation.py @@ -0,0 +1,79 @@ +#!/usr/bin/env python3 +# Copyright (c) 2019 The Bitcoin developers +# Distributed under the MIT software license, see the accompanying +# file COPYING or http://www.opensource.org/licenses/mit-license.php. + +from test_framework.test_framework import BitcoinTestFramework +from test_framework.util import sync_blocks, bytes_to_hex_str, assert_equal +from test_framework.blocktools import create_coinbase, create_block +from test_framework.txtools import pad_raw_tx + +ACTIVATION_TIME = 1557921600 + + +class SomeCodenameActivation(BitcoinTestFramework): + + def set_test_params(self): + self.num_nodes = 2 + self.setup_clean_chain = True + common_args = [ + '-whitelist=127.0.0.1', + '-nulldummyenforcetime=%d' % ACTIVATION_TIME, + '-datacarriersize=1000', + "-replayprotectionactivationtime=%d" % (ACTIVATION_TIME * 2)] + + self.extra_args = [ + common_args + ["-acceptnonstdtxn=0"], + common_args + ["-acceptnonstdtxn=1"]] + + # Some codename is NOT activated at the beginning of the test. + self.mocktime = ACTIVATION_TIME - 1000 + + def run_test(self): + node = self.nodes[0] + # Create a spendable utxo + coinbase_block = node.generate(1) + coinbase_id = node.getblock(coinbase_block[0])['tx'][0] + node.generate(101) + sync_blocks(self.nodes) + + # Test that mempool is cleared after activation + to_addr = node.getnewaddress() + rawtx = node.createrawtransaction([{ + 'txid': coinbase_id, + 'vout': 0}], {to_addr: 42}) + rawtx = pad_raw_tx(rawtx) + rawtx = node.signrawtransaction(rawtx)['hex'] + + for n in self.nodes: + n.sendrawtransaction(rawtx, True) + + while True: + lastblock = node.getblockheader(node.getbestblockhash()) + + mtp = lastblock['mediantime'] + if mtp < ACTIVATION_TIME: + for n in self.nodes: + assert_equal(1, n.getmempoolinfo()['size']) + + else: + # New rules enforced. Nodes accepting non-standard transactions should clear its mempool, as it may have had a non-compliant NULLDUMMY transaction in it + + # Node 0 did not accept non-standard + assert_equal(1, self.nodes[0].getmempoolinfo()['size']) + # Node 1 did accept non-standard + assert_equal(0, self.nodes[1].getmempoolinfo()['size']) + break + + next_time = lastblock['time'] + 600 + next_height = lastblock['height'] + 1 + coinbase = create_coinbase(next_height) + block = create_block( + int("0x" + lastblock['hash'], 0), coinbase, next_time) + for n in self.nodes: + n.setmocktime(next_time) + n.submitblock(bytes_to_hex_str(block.serialize())) + + +if __name__ == '__main__': + SomeCodenameActivation().main() diff --git a/test/functional/nulldummy.py b/test/functional/nulldummy.py --- a/test/functional/nulldummy.py +++ b/test/functional/nulldummy.py @@ -1,11 +1,12 @@ #!/usr/bin/env python3 # Copyright (c) 2016 The Bitcoin Core developers +# Copyright (c) 2019 The Bitcoin developers # Distributed under the MIT software license, see the accompanying # file COPYING or http://www.opensource.org/licenses/mit-license.php. from test_framework.test_framework import BitcoinTestFramework -from test_framework.util import * -from test_framework.mininode import CTransaction, NetworkThread +from test_framework.util import hex_str_to_bytes, bytes_to_hex_str, assert_equal, assert_raises_rpc_error +from test_framework.mininode import CTransaction from test_framework.blocktools import create_coinbase, create_block from test_framework.script import CScript from io import BytesIO @@ -13,6 +14,8 @@ NULLDUMMY_ERROR = "64: non-mandatory-script-verify-flag (Dummy CHECKMULTISIG argument must be zero)" +ACTIVATION_TIME = 1557921600 + def trueDummy(tx): scriptSig = CScript(tx.vin[0].scriptSig) @@ -28,14 +31,7 @@ ''' -This test is meant to exercise NULLDUMMY softfork. -Connect to a single node. -Generate 2 blocks (save the coinbases for later). -Generate 427 more blocks. -[Policy/Consensus] Check that NULLDUMMY compliant transactions are accepted in the 430th block. -[Policy] Check that non-NULLDUMMY transactions are rejected before activation. -[Consensus] Check that the new NULLDUMMY rules are not enforced on the 431st block. -[Policy/Consensus] Check that the new NULLDUMMY rules are enforced on the 432nd block. +This test is meant to exercise NULLDUMMY enforcement protocol upgrade. ''' @@ -44,46 +40,86 @@ def set_test_params(self): self.num_nodes = 1 self.setup_clean_chain = True - self.extra_args = [['-whitelist=127.0.0.1']] + self.extra_args = [[ + '-whitelist=127.0.0.1', + '-nulldummyenforcetime=%d' % ACTIVATION_TIME, + "-replayprotectionactivationtime=%d" % (ACTIVATION_TIME * 2)]] + + # NULLDUMMY is NOT activated at the beginning of the test. + self.mocktime = ACTIVATION_TIME - 1000 + + def set_last_block(self, header): + self.lastblockhash = header['hash'] + self.tip = int("0x" + self.lastblockhash, 0) + self.lastblockheight = header['height'] + self.lastblocktime = header['time'] + + def check_activation(self, header, expect_activation): + if expect_activation: + assert(header['mediantime'] >= ACTIVATION_TIME) + else: + assert(header['mediantime'] < ACTIVATION_TIME) def run_test(self): - self.address = self.nodes[0].getnewaddress() - self.ms_address = self.nodes[0].addmultisigaddress(1, [self.address]) + node = self.nodes[0] + self.address = node.getnewaddress() + self.ms_address = node.addmultisigaddress(1, [self.address]) - NetworkThread().start() # Start up network handling in another thread - self.coinbase_blocks = self.nodes[0].generate(2) # Block 2 + self.coinbase_blocks = node.generate(2) coinbase_txid = [] for i in self.coinbase_blocks: - coinbase_txid.append(self.nodes[0].getblock(i)['tx'][0]) - self.nodes[0].generate(427) # Block 429 - self.lastblockhash = self.nodes[0].getbestblockhash() - self.tip = int("0x" + self.lastblockhash, 0) - self.lastblockheight = 429 - self.lastblocktime = int(time.time()) + 429 + coinbase_txid.append(node.getblock(i)['tx'][0]) + + # Mature coinbase for spending + node.generate(101) + lastheader = node.getblockheader(node.getbestblockhash()) + self.set_last_block(lastheader) + self.check_activation(lastheader, expect_activation=False) self.log.info( - "Test 1: NULLDUMMY compliant base transactions should be accepted to mempool and mined before activation [430]") - test1txs = [self.create_transaction( - self.nodes[0], coinbase_txid[0], self.ms_address, 49)] - txid1 = self.nodes[0].sendrawtransaction( + "Test 1 [Policy/Consensus]: NULLDUMMY compliant base transactions should be accepted to mempool and mined before activation") + test1txs = [] + test1txs.append(self.create_transaction( + node, coinbase_txid[0], self.ms_address, 49)) + txid1 = node.sendrawtransaction( bytes_to_hex_str(test1txs[0].serialize()), True) + test1txs.append(self.create_transaction( - self.nodes[0], txid1, self.ms_address, 48)) - txid2 = self.nodes[0].sendrawtransaction( + node, txid1, self.ms_address, 48)) + txid2 = node.sendrawtransaction( bytes_to_hex_str(test1txs[1].serialize()), True) - self.block_submit(self.nodes[0], test1txs, True) + + self.block_submit(node, test1txs, accept=True) self.log.info( - "Test 2: Non-NULLDUMMY base multisig transaction should not be accepted to mempool before activation") + "Test 2 [Policy]: Non-NULLDUMMY base multisig transaction should not be accepted to mempool before activation") test2tx = self.create_transaction( - self.nodes[0], txid2, self.ms_address, 48) + node, txid2, self.ms_address, 48) trueDummy(test2tx) + txid4 = test2tx.hash + assert_raises_rpc_error(-26, NULLDUMMY_ERROR, - self.nodes[0].sendrawtransaction, bytes_to_hex_str(test2tx.serialize()), True) + node.sendrawtransaction, bytes_to_hex_str(test2tx.serialize()), True) + + self.log.info( + "Test 3 [Consensus]: Non-NULLDUMMY base transactions should be accepted in a block before activation") + self.block_submit(node, [test2tx], accept=True) + + self.activate_nulldummy_enforcement(node) + + self.log.info( + "Test 4 [Consensus]: Non-NULLDUMMY base multisig transaction is invalid after activation") + test4tx = self.create_transaction(node, txid4, self.address, amount=47) + trueDummy(test4tx) + assert_raises_rpc_error(-26, NULLDUMMY_ERROR, node.sendrawtransaction, + bytes_to_hex_str(test4tx.serialize()), True) + self.block_submit(node, [test4tx], accept=False) self.log.info( - "Test 3: Non-NULLDUMMY base transactions should be accepted in a block before activation [431]") - self.block_submit(self.nodes[0], [test2tx], True) + "Test 5 [Consensus]: NULLDUMMY compliant base transactions should be accepted to mempool and in block after activation") + test5tx = self.create_transaction(node, txid4, self.address, amount=47) + node.sendrawtransaction(bytes_to_hex_str(test5tx.serialize()), True) + self.block_submit(node, [test5tx], accept=True) def create_transaction(self, node, txid, to_address, amount): inputs = [{"txid": txid, "vout": 0}] @@ -117,6 +153,22 @@ else: assert_equal(node.getbestblockhash(), self.lastblockhash) + def activate_nulldummy_enforcement(self, node): + while True: + lastblock = node.getblockheader(node.getbestblockhash()) + self.set_last_block(lastblock) + + mtp = lastblock['mediantime'] + if mtp >= ACTIVATION_TIME: + return + + next_time = self.lastblocktime + 600 + coinbase = create_coinbase(self.lastblockheight + 1) + block = create_block(self.tip, coinbase, next_time) + + node.setmocktime(next_time) + node.submitblock(bytes_to_hex_str(block.serialize())) + if __name__ == '__main__': NULLDUMMYTest().main() diff --git a/test/functional/test_framework/txtools.py b/test/functional/test_framework/txtools.py --- a/test/functional/test_framework/txtools.py +++ b/test/functional/test_framework/txtools.py @@ -8,7 +8,7 @@ def pad_tx(tx, pad_to_size=MIN_TX_SIZE): """ - Pad a transaction with op_return junk data until it is at least pad_to_size, or + Pad a transaction with op_return junk data until it is at least pad_to_size, or leave it alone if it's already bigger than that. """ curr_size = len(tx.serialize()) @@ -53,7 +53,7 @@ padding = random.randrange( 1 << 8 * padding_len - 2, 1 << 8 * padding_len - 1) tx.vout.append( - CTxOut(0, CScript([padding, OP_RETURN]))) + CTxOut(0, CScript([OP_RETURN, padding]))) curr_size = len(tx.serialize()) required_padding = pad_to_size - curr_size