diff --git a/src/validation.cpp b/src/validation.cpp --- a/src/validation.cpp +++ b/src/validation.cpp @@ -1303,6 +1303,22 @@ scriptError = check2.GetScriptError(); } + // Before banning, we need to check whether the transaction would + // be valid on the other side of the upgrade, so as to avoid + // splitting the network between upgraded and non-upgraded nodes. + // Note that this will create strange error messages like + // "upgrade-conditional-script-failure (Opcode missing or not + // understood)". + CScriptCheck check3(scriptPubKey, amount, tx, i, + mandatoryFlags ^ SCRIPT_ENABLE_OP_REVERSEBYTES, + sigCacheStore, txdata); + if (check3()) { + return state.Invalid( + false, REJECT_INVALID, + strprintf("upgrade-conditional-script-failure (%s)", + ScriptErrorString(check.GetScriptError()))); + } + // Failures of other flags indicate a transaction that is invalid in // new blocks, e.g. a invalid P2SH. We DoS ban such nodes as they // are not following the protocol. That said during an upgrade @@ -1657,6 +1673,10 @@ flags |= SCRIPT_VERIFY_MINIMALDATA; } + if (IsPhononEnabled(params, pindex)) { + flags |= SCRIPT_ENABLE_OP_REVERSEBYTES; + } + // We make sure this node will have replay protection during the next hard // fork. if (IsReplayProtectionEnabled(params, pindex)) { diff --git a/test/functional/abc-op-reversebytes-activation.py b/test/functional/abc-op-reversebytes-activation.py new file mode 100755 --- /dev/null +++ b/test/functional/abc-op-reversebytes-activation.py @@ -0,0 +1,289 @@ +#!/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 EQUAL + tx_fund = create_tx_with_script(spend_from, 0, b'', value, script) + tx_fund.rehash() + + # Spend transaction: + 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()