Changeset View
Changeset View
Standalone View
Standalone View
test/functional/abc-op-reversebytes-activation.py
- This file was added.
| Property | Old Value | New Value |
|---|---|---|
| File Mode | null | 100755 |
| #!/usr/bin/env python3 | |||||
| # Copyright (c) 2020 The Bitcoin developers | |||||
| # Distributed under the MIT software license, see the accompanying | |||||
| # file COPYING or http://www.opensource.org/licenses/mit-license.php. | |||||
| """ | |||||
| This test checks the activation logic of OP_REVERSEBYTES. | |||||
| Derived from both abc-schnorrmultisig-activation.py (see https://reviews.bitcoinabc.org/D3736) and | |||||
| abc-schnorrmultisig.py | |||||
| """ | |||||
| from test_framework.blocktools import ( | |||||
| create_block, | |||||
| create_coinbase, | |||||
| create_tx_with_script, | |||||
| make_conform_to_ctor, | |||||
| ) | |||||
| from test_framework.messages import ( | |||||
| CBlock, | |||||
| COutPoint, | |||||
| CTransaction, | |||||
| CTxIn, | |||||
| CTxOut, | |||||
| FromHex, | |||||
| ToHex, | |||||
| ) | |||||
| from test_framework.mininode import ( | |||||
| P2PDataStore, | |||||
| ) | |||||
| from test_framework.script import ( | |||||
| CScript, | |||||
| OP_EQUAL, | |||||
| OP_REVERSEBYTES, | |||||
| OP_RETURN, | |||||
| ) | |||||
| from test_framework.test_framework import BitcoinTestFramework | |||||
| from test_framework.util import assert_equal, assert_raises_rpc_error | |||||
| # The upgrade activation time, which we artificially set far into the future. | |||||
| PHONON_START_TIME = 2000000000 | |||||
| # Blocks with invalid scripts give this error: | |||||
| BAD_INPUTS_ERROR = 'blk-bad-inputs' | |||||
| # Pre-upgrade, we get a BAD_OPCODE error | |||||
| PRE_UPGRADE_BAD_OPCODE_ERROR = \ | |||||
| 'upgrade-conditional-script-failure (Opcode missing or not understood)' | |||||
| class OpReversebytesActivationTest(BitcoinTestFramework): | |||||
| def set_test_params(self): | |||||
| self.num_nodes = 1 | |||||
| self.block_heights = {} | |||||
| self.extra_args = [ | |||||
| ["-phononactivationtime={}".format(PHONON_START_TIME)]] | |||||
| def bootstrap_p2p(self, *, num_connections=1): | |||||
| """Add a P2P connection to the node. | |||||
| Helper to connect and wait for version handshake.""" | |||||
| for _ in range(num_connections): | |||||
| self.nodes[0].add_p2p_connection(P2PDataStore()) | |||||
| self.nodes[0].p2p.wait_for_verack() | |||||
| def reconnect_p2p(self, **kwargs): | |||||
| """Tear down and bootstrap the P2P connection to the node. | |||||
| The node gets disconnected several times in this test. This helper | |||||
| method reconnects the p2p and restarts the network thread.""" | |||||
| self.nodes[0].disconnect_p2ps() | |||||
| self.bootstrap_p2p(**kwargs) | |||||
| def get_best_block(self, node): | |||||
| """Get the best block. Register its height so we can use build_block.""" | |||||
| block_height = node.getblockcount() | |||||
| blockhash = node.getblockhash(block_height) | |||||
| block = FromHex(CBlock(), node.getblock(blockhash, 0)) | |||||
| block.calc_sha256() | |||||
| self.block_heights[block.sha256] = block_height | |||||
| return block | |||||
| def build_block(self, parent, transactions=(), n_time=None): | |||||
| """Make a new block with an OP_1 coinbase output. | |||||
| Requires parent to have its height registered.""" | |||||
| parent.calc_sha256() | |||||
| block_height = self.block_heights[parent.sha256] + 1 | |||||
| block_time = (parent.nTime + 1) if n_time is None else n_time | |||||
| block = create_block( | |||||
| parent.sha256, create_coinbase(block_height), block_time) | |||||
| block.vtx.extend(transactions) | |||||
| make_conform_to_ctor(block) | |||||
| block.hashMerkleRoot = block.calc_merkle_root() | |||||
| block.solve() | |||||
| self.block_heights[block.sha256] = block_height | |||||
| return block | |||||
| def check_for_no_ban_on_rejected_tx(self, tx, reject_reason): | |||||
| """Check we are not disconnected when sending a txn that the node rejects.""" | |||||
| self.nodes[0].p2p.send_txs_and_test( | |||||
| [tx], self.nodes[0], success=False, reject_reason=reject_reason) | |||||
| def check_for_ban_on_rejected_tx(self, tx, reject_reason=None): | |||||
| """Check we are disconnected when sending a txn that the node rejects. | |||||
| (Can't actually get banned, since bitcoind won't ban local peers.)""" | |||||
| self.nodes[0].p2p.send_txs_and_test( | |||||
| [tx], self.nodes[0], success=False, expect_disconnect=True, reject_reason=reject_reason) | |||||
| self.reconnect_p2p() | |||||
| def check_for_ban_on_rejected_block(self, block, reject_reason=None): | |||||
| """Check we are disconnected when sending a block that the node rejects. | |||||
| (Can't actually get banned, since bitcoind won't ban local peers.)""" | |||||
| self.nodes[0].p2p.send_blocks_and_test([block], self.nodes[0], success=False, | |||||
| reject_reason=reject_reason, expect_disconnect=True) | |||||
| self.reconnect_p2p() | |||||
| def run_test(self): | |||||
| node, = self.nodes | |||||
| self.bootstrap_p2p() | |||||
| tip = self.get_best_block(node) | |||||
| self.log.info("Create some blocks with OP_1 coinbase for spending.") | |||||
| blocks = [] | |||||
| for _ in range(10): | |||||
| tip = self.build_block(tip) | |||||
| blocks.append(tip) | |||||
| node.p2p.send_blocks_and_test(blocks, node, success=True) | |||||
| spendable_outputs = [block.vtx[0] for block in blocks] | |||||
| self.log.info("Mature the blocks and get out of IBD.") | |||||
| node.generate(100) | |||||
| tip = self.get_best_block(node) | |||||
| self.log.info( | |||||
| "Set up spending transactions to test and mine the funding transactions.") | |||||
| def create_fund_and_spend_tx(): | |||||
| spend_from = spendable_outputs.pop() | |||||
| value = spend_from.vout[0].nValue | |||||
| # Reversed data | |||||
| data = bytes.fromhex('0123456789abcdef') | |||||
| rev_data = bytes(reversed(data)) | |||||
| # Lockscript: provide a bytestring that reverses to X | |||||
| script = CScript([OP_REVERSEBYTES, rev_data, OP_EQUAL]) | |||||
| # Fund transaction: REVERSEBYTES <reversed(x)> EQUAL | |||||
| tx_fund = create_tx_with_script(spend_from, 0, b'', value, script) | |||||
| tx_fund.rehash() | |||||
| # Spend transaction: <x> | |||||
| tx_spend = CTransaction() | |||||
| tx_spend.vout.append( | |||||
| CTxOut(value - 1000, CScript([b'x' * 100, OP_RETURN]))) | |||||
| tx_spend.vin.append( | |||||
| CTxIn(COutPoint(tx_fund.sha256, 0), b'')) | |||||
| tx_spend.vin[0].scriptSig = CScript([data]) | |||||
| tx_spend.rehash() | |||||
| return tx_spend, tx_fund | |||||
| # Create funding/spending transaction pair | |||||
| tx_reversebytes_spend, tx_reversebytes_fund = create_fund_and_spend_tx() | |||||
| # Mine funding transaction into block. Pre-upgrade output scripts can have | |||||
| # OP_REVERSEBYTES and still be fully valid, but they cannot spend it. | |||||
| tip = self.build_block(tip, [tx_reversebytes_fund]) | |||||
| node.p2p.send_blocks_and_test([tip], node) | |||||
| self.log.info("Start pre-upgrade tests") | |||||
| self.log.info( | |||||
| "Sending rejected transaction (bad opcode) via RPC (doesn't ban)") | |||||
| assert_raises_rpc_error(-26, PRE_UPGRADE_BAD_OPCODE_ERROR, | |||||
| node.sendrawtransaction, ToHex(tx_reversebytes_spend)) | |||||
| self.log.info( | |||||
| "Sending rejected transaction (bad opcode) via net (no banning)") | |||||
| self.check_for_no_ban_on_rejected_tx( | |||||
| tx_reversebytes_spend, PRE_UPGRADE_BAD_OPCODE_ERROR) | |||||
| self.log.info( | |||||
| "Sending invalid transactions in blocks (bad inputs, and get banned)") | |||||
| self.check_for_ban_on_rejected_block(self.build_block(tip, [tx_reversebytes_spend]), | |||||
| BAD_INPUTS_ERROR) | |||||
| self.log.info("Start activation tests") | |||||
| self.log.info("Approach to just before upgrade activation") | |||||
| # Move our clock to the upgrade time so we will accept such future-timestamped blocks. | |||||
| node.setmocktime(PHONON_START_TIME) | |||||
| # Mine six blocks with timestamp starting at PHONON_START_TIME-1 | |||||
| blocks = [] | |||||
| for i in range(-1, 5): | |||||
| tip = self.build_block(tip, n_time=PHONON_START_TIME + i) | |||||
| blocks.append(tip) | |||||
| node.p2p.send_blocks_and_test(blocks, node) | |||||
| # Ensure our MTP is PHONON_START_TIME-1, just before activation | |||||
| assert_equal(node.getblockchaininfo()['mediantime'], | |||||
| PHONON_START_TIME - 1) | |||||
| self.log.info( | |||||
| "The next block will activate, but the activation block itself must follow old rules") | |||||
| self.check_for_ban_on_rejected_block( | |||||
| self.build_block(tip, [tx_reversebytes_spend]), BAD_INPUTS_ERROR) | |||||
| # Save pre-upgrade block, we will reorg based on this block later | |||||
| pre_upgrade_block = tip | |||||
| self.log.info("Mine the activation block itself") | |||||
| tip = self.build_block(tip, []) | |||||
| node.p2p.send_blocks_and_test([tip], node) | |||||
| self.log.info("We have activated!") | |||||
| # Ensure our MTP is PHONON_START_TIME, exactly at activation | |||||
| assert_equal(node.getblockchaininfo()['mediantime'], PHONON_START_TIME) | |||||
| # Ensure empty mempool | |||||
| assert_equal(node.getrawmempool(), []) | |||||
| # Save upgrade block, we will invalidate and reconsider this block later | |||||
| upgrade_block = tip | |||||
| self.log.info( | |||||
| "Submitting a new OP_REVERSEBYTES tx via net, and mining it in a block") | |||||
| # Send OP_REVERSEBYTES tx | |||||
| node.p2p.send_txs_and_test([tx_reversebytes_spend], node) | |||||
| # Verify OP_REVERSEBYTES tx is in mempool | |||||
| assert_equal(set(node.getrawmempool()), {tx_reversebytes_spend.hash}) | |||||
| # Mine OP_REVERSEBYTES tx into block | |||||
| tip = self.build_block(tip, [tx_reversebytes_spend]) | |||||
| node.p2p.send_blocks_and_test([tip], node) | |||||
| # Save post-upgrade block, we will invalidate and reconsider this block later | |||||
| post_upgrade_block = tip | |||||
| self.log.info("Start deactivation tests") | |||||
| self.log.info( | |||||
| "Invalidating the post-upgrade blocks returns OP_REVERSEBYTES transaction to mempool") | |||||
| node.invalidateblock(post_upgrade_block.hash) | |||||
| assert_equal(set(node.getrawmempool()), { | |||||
| tx_reversebytes_spend.hash}) | |||||
| self.log.info( | |||||
| "Invalidating the upgrade block evicts the OP_REVERSEBYTES transaction") | |||||
| node.invalidateblock(upgrade_block.hash) | |||||
| assert_equal(set(node.getrawmempool()), set()) | |||||
| self.log.info("Return to our tip") | |||||
| node.reconsiderblock(upgrade_block.hash) | |||||
| node.reconsiderblock(post_upgrade_block.hash) | |||||
| assert_equal(node.getbestblockhash(), tip.hash) | |||||
| assert_equal(node.getrawmempool(), []) | |||||
| self.log.info( | |||||
| "Create an empty-block reorg that forks from pre-upgrade") | |||||
| tip = pre_upgrade_block | |||||
| blocks = [] | |||||
| for _ in range(10): | |||||
| tip = self.build_block(tip) | |||||
| blocks.append(tip) | |||||
| node.p2p.send_blocks_and_test(blocks, node) | |||||
| self.log.info( | |||||
| "Transactions from orphaned blocks are sent into mempool ready to be mined again, " | |||||
| "including upgrade-dependent ones even though the fork deactivated and reactivated " | |||||
| "the upgrade.") | |||||
| assert_equal(set(node.getrawmempool()), | |||||
| {tx_reversebytes_spend.hash}) | |||||
| node.generate(1) | |||||
| tip = self.get_best_block(node) | |||||
| assert (set(tx.rehash() for tx in tip.vtx) >= | |||||
| {tx_reversebytes_spend.hash}) | |||||
| if __name__ == '__main__': | |||||
| OpReversebytesActivationTest().main() | |||||