diff --git a/src/script/script.cpp b/src/script/script.cpp --- a/src/script/script.cpp +++ b/src/script/script.cpp @@ -371,6 +371,9 @@ } uint32_t CScript::GetSigOpCount(uint32_t flags, bool fAccurate) const { + if (flags & SCRIPT_ZERO_SIGOPS) { + return 0; + } uint32_t n = 0; const_iterator pc = begin(); opcodetype lastOpcode = OP_INVALIDOPCODE; diff --git a/src/script/script_flags.h b/src/script/script_flags.h --- a/src/script/script_flags.h +++ b/src/script/script_flags.h @@ -93,9 +93,8 @@ SCRIPT_ENABLE_REPLAY_PROTECTION = (1U << 17), // Count sigops for OP_CHECKDATASIG and variant. The interpreter treats - // OP_CHECKDATASIG(VERIFY) as always valid, this flag only affects sigops - // counting. - // + // OP_CHECKDATASIG(VERIFY) as always valid. This flag only affects sigops + // counting, and will be removed during cleanup of the SigChecks upgrade. SCRIPT_VERIFY_CHECKDATASIG_SIGOPS = (1U << 18), // The exception to CLEANSTACK and P2SH for the recovery of coins sent @@ -115,6 +114,11 @@ // Whether the new OP_REVERSEBYTES opcode can be used. SCRIPT_ENABLE_OP_REVERSEBYTES = (1U << 23), + // Setting this flag zeroes sigops counting and thus results in the removal + // of all sigop limits. This flag only affects sigops counting, and will be + // removed during cleanup of the SigChecks upgrade. + SCRIPT_ZERO_SIGOPS = (1U << 30), + // A utility flag to decide whether VerifyScript should output the correct // sigchecks value or to report zero. // This has no effect on script success / failure, and will be removed diff --git a/src/validation.cpp b/src/validation.cpp --- a/src/validation.cpp +++ b/src/validation.cpp @@ -1657,6 +1657,10 @@ flags |= SCRIPT_VERIFY_MINIMALDATA; } + if (IsPhononEnabled(params, pindex)) { + flags |= SCRIPT_ZERO_SIGOPS; + } + // We make sure this node will have replay protection during the next hard // fork. if (IsReplayProtectionEnabled(params, pindex)) { diff --git a/test/functional/abc-magnetic-anomaly-mining.py b/test/functional/abc-magnetic-anomaly-mining.py --- a/test/functional/abc-magnetic-anomaly-mining.py +++ b/test/functional/abc-magnetic-anomaly-mining.py @@ -13,6 +13,9 @@ from test_framework.test_framework import BitcoinTestFramework +# Set test to run with sigops deactivation far in the future. +SIGOPS_DEACTIVATION_TIME = 2000000000 + class CTORMiningTest(BitcoinTestFramework): def set_test_params(self): @@ -25,7 +28,8 @@ self.blocks = {} self.mocktime = int(time.time()) - 600 * 100 - extra_arg = ['-spendzeroconfchange=0', '-whitelist=127.0.0.1'] + extra_arg = ['-spendzeroconfchange=0', '-whitelist=127.0.0.1', + '-phononactivationtime={}'.format(SIGOPS_DEACTIVATION_TIME)] self.extra_args = [extra_arg, extra_arg] def run_test(self): diff --git a/test/functional/abc-mempool-accept-txn.py b/test/functional/abc-mempool-accept-txn.py --- a/test/functional/abc-mempool-accept-txn.py +++ b/test/functional/abc-mempool-accept-txn.py @@ -47,6 +47,9 @@ # Error for too many sigops in one TX RPC_TXNS_TOO_MANY_SIGOPS_ERROR = "bad-txns-too-many-sigops" +# Set test to run with sigops deactivation far in the future. +SIGOPS_DEACTIVATION_TIME = 2000000000 + class PreviousSpendableOutput(): @@ -67,6 +70,8 @@ self.coinbase_pubkey = self.coinbase_key.get_pubkey() self.tip = None self.blocks = {} + self.extra_args = [ + ['-phononactivationtime={}'.format(SIGOPS_DEACTIVATION_TIME)]] def add_options(self, parser): super().add_options(parser) diff --git a/test/functional/abc-p2p-fullblocktest-sigops.py b/test/functional/abc-p2p-fullblocktest-sigops.py --- a/test/functional/abc-p2p-fullblocktest-sigops.py +++ b/test/functional/abc-p2p-fullblocktest-sigops.py @@ -54,6 +54,9 @@ from test_framework.test_framework import BitcoinTestFramework from test_framework.util import assert_equal +# Set test to run with sigops deactivation far in the future. +SIGOPS_DEACTIVATION_TIME = 2000000000 + class PreviousSpendableOutput(): @@ -73,7 +76,7 @@ self.blocks = {} self.excessive_block_size = 100 * ONE_MEGABYTE self.extra_args = [['-whitelist=127.0.0.1', - "-excessiveblocksize={}".format(self.excessive_block_size)]] + "-excessiveblocksize={}".format(self.excessive_block_size), '-phononactivationtime={}'.format(SIGOPS_DEACTIVATION_TIME)]] def add_options(self, parser): super().add_options(parser) diff --git a/test/functional/abc-sigops-deactivation.py b/test/functional/abc-sigops-deactivation.py new file mode 100755 --- /dev/null +++ b/test/functional/abc-sigops-deactivation.py @@ -0,0 +1,350 @@ +#!/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. +""" +Test deactivation of sigops counting + +based on abc-schnorrmultisig-activation.py (D3736). +""" + +from test_framework.blocktools import ( + create_block, + create_coinbase, + make_conform_to_ctor, +) +from test_framework.cdefs import ( + ONE_MEGABYTE, +) +from test_framework.messages import ( + CBlock, + COutPoint, + CTransaction, + CTxIn, + CTxOut, + FromHex, + ToHex, +) +from test_framework.mininode import ( + P2PDataStore, +) +from test_framework.script import ( + CScript, + hash160, + OP_CHECKMULTISIG, + OP_CHECKDATASIG, + OP_ENDIF, + OP_EQUAL, + OP_FALSE, + OP_HASH160, + OP_IF, + OP_RETURN, + OP_TRUE, +) +from test_framework.test_framework import BitcoinTestFramework +from test_framework.txtools import pad_tx +from test_framework.util import ( + assert_equal, + assert_raises_rpc_error, + sync_blocks, +) +from decimal import Decimal +from collections import deque + +SATOSHI = Decimal('0.00000001') + +# Set test to run with sigops deactivation far in the future. +SIGOPS_DEACTIVATION_TIME = 2000000000 + +# If we don't do this, autoreplay protection will activate before graviton and +# all our sigs will mysteriously fail. +REPLAY_PROTECTION_START_TIME = SIGOPS_DEACTIVATION_TIME * 2 + +# Transactions for mempool with too many sigops give this error: +MEMPOOL_TXSIGOPS_ERROR = 'bad-txns-too-many-sigops' +MEMPOOL_P2SH_SIGOPS_ERROR = 'bad-txns-nonstandard-inputs' +# Blocks that have single txes with too many sigops give this error: +BLOCK_TXSIGOPS_ERROR = 'bad-txn-sigops' +# Blocks with too many sigops give this error: +BLOCK_TOTALSIGOPS_ERROR = 'bad-blk-sigops' + + +def create_transaction(spendfrom, custom_script, amount=None): + # Fund and sign a transaction to a given output. + # spendfrom should be a CTransaction with first output to OP_TRUE. + + # custom output will go on position 1, after position 0 which will be + # OP_TRUE (so it can be reused). + customout = CTxOut(0, bytes(custom_script)) + # set output amount to required dust if not given + customout.nValue = amount or (len(customout.serialize()) + 148) * 3 + + ctx = CTransaction() + ctx.vin.append(CTxIn(COutPoint(spendfrom.sha256, 0), b'')) + ctx.vout.append( + CTxOut(0, bytes([OP_TRUE]))) + ctx.vout.append(customout) + pad_tx(ctx) + + fee = len(ctx.serialize()) + ctx.vout[0].nValue = spendfrom.vout[0].nValue - customout.nValue - fee + ctx.rehash() + + return ctx + + +def check_for_ban_on_rejected_tx(node, tx, reject_reason=None): + """Check we are disconnected when sending a txn that the node rejects, + then reconnect after. + + (Can't actually get banned, since bitcoind won't ban local peers.)""" + node.p2p.send_txs_and_test( + [tx], node, success=False, expect_disconnect=True, reject_reason=reject_reason) + node.disconnect_p2ps() + node.add_p2p_connection(P2PDataStore()) + + +def check_for_ban_on_rejected_block(node, block, reject_reason=None): + """Check we are disconnected when sending a block that the node rejects, + then reconnect after. + + (Can't actually get banned, since bitcoind won't ban local peers.)""" + node.p2p.send_blocks_and_test( + [block], node, success=False, reject_reason=reject_reason, expect_disconnect=True) + node.disconnect_p2ps() + node.add_p2p_connection(P2PDataStore()) + + +def check_for_no_ban_on_rejected_tx(node, tx, reject_reason=None): + """Check we are not disconnected when sending a txn that the node rejects.""" + node.p2p.send_txs_and_test( + [tx], node, success=False, reject_reason=reject_reason) + + +class SigopsDeactivationTest(BitcoinTestFramework): + + def set_test_params(self): + self.setup_clean_chain = True + self.num_nodes = 2 + self.block_heights = {} + timeargs = ["-phononactivationtime={}".format( + SIGOPS_DEACTIVATION_TIME), + "-replayprotectionactivationtime={}".format( + REPLAY_PROTECTION_START_TIME)] + # many standardness rules are actually enforced on regtest, except for P2SH sigops. + self.extra_args = [timeargs, timeargs + ['-acceptnonstdtxn=0']] + + def getbestblock(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=(), nTime=None, cbextrascript=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 nTime is None else nTime + + block = create_block( + parent.sha256, create_coinbase(block_height), block_time) + if cbextrascript is not None: + block.vtx[0].vout.append(CTxOut(0, cbextrascript)) + block.vtx[0].rehash() + 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 run_test(self): + (node, std_node) = self.nodes + node.add_p2p_connection(P2PDataStore()) + std_node.add_p2p_connection(P2PDataStore()) + # Get out of IBD + node.generate(1) + + tip = self.getbestblock(node) + + self.log.info("Create some blocks with OP_1 coinbase for spending.") + blocks = [] + for _ in range(20): + tip = self.build_block(tip) + blocks.append(tip) + node.p2p.send_blocks_and_test(blocks, node, success=True) + self.spendable_outputs = deque(block.vtx[0] for block in blocks) + + self.log.info("Mature the blocks.") + node.generate(100) + + tip = self.getbestblock(node) + + self.log.info("Generating some high-sigop transactions.") + + # Tx with 4001 sigops (valid but non standard) + tx_4001 = create_transaction(self.spendable_outputs.popleft(), [ + OP_CHECKMULTISIG] * 200 + [OP_CHECKDATASIG]) + + # Tx with 20001 sigops (consensus-invalid) + tx_20001 = create_transaction(self.spendable_outputs.popleft(), [ + OP_CHECKMULTISIG] * 1000 + [OP_CHECKDATASIG]) + + # P2SH tx with too many sigops (valid but nonstandard for std_node) + redeem_script = bytes( + [OP_IF, OP_CHECKMULTISIG, OP_ENDIF, OP_TRUE]) + p2sh_script = CScript([OP_HASH160, hash160(redeem_script), OP_EQUAL]) + tx_fundp2sh = create_transaction( + self.spendable_outputs.popleft(), p2sh_script) + tx_spendp2sh = CTransaction() + tx_spendp2sh.vin.append( + CTxIn(COutPoint(tx_fundp2sh.sha256, 1), CScript([OP_FALSE, redeem_script]))) + tx_spendp2sh.vout.append( + CTxOut(0, CScript([OP_RETURN, b'pad' * 20]))) + tx_spendp2sh.rehash() + + # Chain of 10 txes with 2000 sigops each. + txes_10x2000_sigops = [] + tx = self.spendable_outputs.popleft() + for _ in range(10): + tx = create_transaction(tx, [OP_CHECKMULTISIG] * 100) + txes_10x2000_sigops.append(tx) + + def make_hightotalsigop_block(): + # 20001 total sigops + return self.build_block(tip, txes_10x2000_sigops, cbextrascript=bytes([OP_CHECKDATASIG])) + + def make_highsigop_coinbase_block(): + # 60000 sigops in the coinbase + return self.build_block(tip, cbextrascript=bytes([OP_CHECKMULTISIG] * 3000)) + + self.log.info( + "Try various high-sigop transactions in blocks / mempool before upgrade") + + # mempool refuses over 4001. + check_for_no_ban_on_rejected_tx(node, tx_4001, MEMPOOL_TXSIGOPS_ERROR) + # it used to be that exceeding 20000 would cause a ban, but it's + # important that this causes no ban: we want that upgraded nodes + # can't get themselves banned by relaying huge-sigops transactions. + check_for_no_ban_on_rejected_tx(node, tx_20001, MEMPOOL_TXSIGOPS_ERROR) + + # the 20001 tx can't be mined + check_for_ban_on_rejected_block(node, self.build_block( + tip, [tx_20001]), BLOCK_TXSIGOPS_ERROR) + + self.log.info( + "The P2SH script has too many sigops (20 > 15) for a standard node.") + # Mine the P2SH funding first because it's nonstandard. + tip = self.build_block(tip, [tx_fundp2sh]) + std_node.p2p.send_blocks_and_test([tip], node) + assert_raises_rpc_error(-26, MEMPOOL_P2SH_SIGOPS_ERROR, + std_node.sendrawtransaction, ToHex(tx_spendp2sh)) + + self.log.info( + "A bunch of 2000-sigops txes can be put in mempool but not mined all at once.") + # Send the 2000-sigop transactions, which are acceptable. + for tx in txes_10x2000_sigops: + node.sendrawtransaction(ToHex(tx)) + + # They can't be mined all at once if the coinbase has a single sigop (total 20001) + check_for_ban_on_rejected_block( + node, make_hightotalsigop_block(), BLOCK_TOTALSIGOPS_ERROR) + + # Activation tests + + self.log.info("Approach to just before upgrade activation") + # Move our clock to the uprade time so we will accept such future-timestamped blocks. + node.setmocktime(SIGOPS_DEACTIVATION_TIME) + std_node.setmocktime(SIGOPS_DEACTIVATION_TIME) + # Mine six blocks with timestamp starting at SIGOPS_DEACTIVATION_TIME-1 + blocks = [] + for i in range(-1, 5): + tip = self.build_block(tip, nTime=SIGOPS_DEACTIVATION_TIME + i) + blocks.append(tip) + node.p2p.send_blocks_and_test(blocks, node) + assert_equal(node.getblockchaininfo()[ + 'mediantime'], SIGOPS_DEACTIVATION_TIME - 1) + + self.log.info( + "The next block will activate, but the activation block itself must follow old rules") + + check_for_ban_on_rejected_block(node, self.build_block( + tip, [tx_20001]), BLOCK_TXSIGOPS_ERROR) + check_for_ban_on_rejected_block( + node, make_hightotalsigop_block(), BLOCK_TOTALSIGOPS_ERROR) + check_for_ban_on_rejected_block( + node, make_highsigop_coinbase_block(), BLOCK_TXSIGOPS_ERROR) + + self.log.info("Mine the activation block itself") + tip = self.build_block(tip) + node.p2p.send_blocks_and_test([tip], node) + sync_blocks(self.nodes) + + self.log.info("We have activated!") + assert_equal(node.getblockchaininfo()[ + 'mediantime'], SIGOPS_DEACTIVATION_TIME) + assert_equal(std_node.getblockchaininfo()[ + 'mediantime'], SIGOPS_DEACTIVATION_TIME) + + # save this tip for later + upgrade_block = tip + + self.log.info( + "The mempool is now a free-for-all, and we can get all the high-sigops transactions in") + std_node.sendrawtransaction(ToHex(tx_spendp2sh)) + node.sendrawtransaction(ToHex(tx_spendp2sh)) + node.sendrawtransaction(ToHex(tx_4001)) + node.sendrawtransaction(ToHex(tx_20001)) + # resend the 2000-sigop transactions, which will have expired due to setmocktime. + for tx in txes_10x2000_sigops: + node.sendrawtransaction(ToHex(tx)) + + alltxes = set(tx.hash for tx in [ + tx_spendp2sh, tx_4001, tx_20001] + txes_10x2000_sigops) + assert_equal(set(node.getrawmempool()), alltxes) + + self.log.info( + "The miner will include all the high-sigops transactions at once, without issue.") + node.generate(1) + tip = self.getbestblock(node) + assert_equal(set(tx.rehash() for tx in tip.vtx[1:]), alltxes) + # even though it is far smaller than one megabyte, we got in something like 44000 sigops + assert len(tip.serialize()) < ONE_MEGABYTE + + # save this tip for later + postupgrade_block = tip + + # Deactivation tests + + self.log.info( + "Invalidating the post-upgrade block returns the transactions to mempool") + node.invalidateblock(postupgrade_block.hash) + assert_equal(set(node.getrawmempool()), alltxes) + + self.log.info("Test some weird alternative blocks") + tip = upgrade_block + self.log.info("A 40000-sigop coinbase is acceptable now") + tip = make_highsigop_coinbase_block() + node.p2p.send_blocks_and_test([tip], node) + self.log.info("We can get in our 20001 sigop total block") + tip = make_hightotalsigop_block() + node.p2p.send_blocks_and_test([tip], node) + + self.log.info( + "Invalidating the upgrade block evicts the bad txes") + goodtxes = alltxes - {tx_4001.hash, tx_20001.hash} + # loose-rules node just evicts the too-many-sigops transactions + node.invalidateblock(upgrade_block.hash) + assert_equal(set(node.getrawmempool()), goodtxes) + # std_node evicts everything as either nonstandard scriptpubkey or p2sh too-many-sigops. + std_node.invalidateblock(upgrade_block.hash) + assert_equal(std_node.getrawmempool(), []) + + +if __name__ == '__main__': + SigopsDeactivationTest().main() diff --git a/test/functional/abc-sigops-mempool-mining.py b/test/functional/abc-sigops-mempool-mining.py --- a/test/functional/abc-sigops-mempool-mining.py +++ b/test/functional/abc-sigops-mempool-mining.py @@ -49,6 +49,9 @@ SATOSHI = Decimal('0.00000001') +# Set test to run with sigops deactivation far in the future. +SIGOPS_DEACTIVATION_TIME = 2000000000 + def create_var_transaction(spendfrom, custom_script, size_bytes, fee_sats): # Fund and sign a transaction to a given output, padding it to exactly @@ -108,7 +111,8 @@ def set_test_params(self): self.setup_clean_chain = True self.num_nodes = 1 - self.extra_args = [["-maxmempool=5"]] + self.extra_args = [ + ["-maxmempool=5", '-phononactivationtime={}'.format(SIGOPS_DEACTIVATION_TIME)]] self.block_heights = {} def getbestblock(self, node): diff --git a/test/functional/feature_block_sigops.py b/test/functional/feature_block_sigops.py --- a/test/functional/feature_block_sigops.py +++ b/test/functional/feature_block_sigops.py @@ -46,12 +46,16 @@ from test_framework.txtools import pad_tx from test_framework.util import assert_equal +# Set test to run with sigops deactivation far in the future. +SIGOPS_DEACTIVATION_TIME = 2000000000 + class FullBlockSigOpsTest(BitcoinTestFramework): def set_test_params(self): self.num_nodes = 1 self.setup_clean_chain = True - self.extra_args = [['-noparkdeepreorg', '-maxreorgdepth=-1']] + self.extra_args = [['-noparkdeepreorg', '-maxreorgdepth=-1', + '-phononactivationtime={}'.format(SIGOPS_DEACTIVATION_TIME)]] def run_test(self): self.bootstrap_p2p() # Add one p2p connection to the node