diff --git a/src/validation.cpp b/src/validation.cpp --- a/src/validation.cpp +++ b/src/validation.cpp @@ -1575,6 +1575,12 @@ flags |= SCRIPT_VERIFY_CLEANSTACK; } + // If the Great Wall fork is enabled, we start accepting transactions + // dependant on the exception of the cleanstack rule + if (IsGreatWallEnabled(config, pChainTip)) { + flags |= SCRIPT_ENABLE_CLEANSTACK_EXCEPTION; + } + // We make sure this node will have replay protection during the next hard // fork. if (IsReplayProtectionEnabled(config, pChainTip)) { diff --git a/test/functional/abc-cleanstack_exception-activation.py b/test/functional/abc-cleanstack_exception-activation.py new file mode 100755 --- /dev/null +++ b/test/functional/abc-cleanstack_exception-activation.py @@ -0,0 +1,199 @@ +#!/usr/bin/env python3 +# Copyright (c) 2018 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 activation of CLEANSTACK_EXCEPTION flag +""" + +from test_framework.test_framework import ComparisonTestFramework +from test_framework.util import satoshi_round, assert_equal, assert_raises_rpc_error +from test_framework.comptool import TestManager, TestInstance, RejectResult +from test_framework.blocktools import * +from test_framework.script import * + +# far into the future +GREAT_WALL_START_TIME = 2000000000 + +# Error due to invalid opcodes +CLEANSTACK_ERROR = b'non-mandatory-script-verify-flag (Script did not clean its stack)' +RPC_CLEANSTACK_ERROR = "64: " + \ + CLEANSTACK_ERROR.decode("utf-8") + + +class PreviousSpendableOutput(): + + def __init__(self, tx=CTransaction(), n=-1): + self.tx = tx + self.n = n # the output we're spending + + +class CleanstackExceptionActivationTest(ComparisonTestFramework): + + def set_test_params(self): + self.num_nodes = 1 + self.setup_clean_chain = True + self.extra_args = [["-whitelist=127.0.0.1", + "-greatwallactivationtime=%d" % GREAT_WALL_START_TIME, + "-replayprotectionactivationtime=%d" % (2 * GREAT_WALL_START_TIME)]] + + def run_test(self): + self.test = TestManager(self, self.options.tmpdir) + self.test.add_all_connections(self.nodes) + # Start up network handling in another thread + NetworkThread().start() + self.test.run() + + def create_tx_spending_to_segwit(self, redeem_scripts): + node = self.nodes[0] + utxos = node.listunspent() + assert(len(utxos) > 0) + utxo = utxos[0] + tx = CTransaction() + n = len(redeem_scripts) + value = int(satoshi_round(utxo["amount"]) * COIN) // n + tx.vin = [CTxIn(COutPoint(int(utxo["txid"], 16), utxo["vout"]))] + tx.vout = [] + for redeem_script in redeem_scripts: + tx.vout.append( + CTxOut(value, CScript([OP_HASH160, hash160(redeem_script), OP_EQUAL]))) + tx.vout[0].nValue -= node.calculate_fee(tx) + tx_signed = node.signrawtransaction(ToHex(tx))["hex"] + tx = FromHex(CTransaction(), tx_signed) + tx.rehash() + return tx, tx_signed + + def get_tests(self): + node = self.nodes[0] + + # First, we generate some coins to spend. + node.generate(125) + + # To make sure we'll be able to recover coins sent to segwit addresses, + # we test using historical recoveries from btc.com: + # Spending from a P2SH-P2WPKH coin, + # txhash:a45698363249312f8d3d93676aa714be59b0bd758e62fa054fb1ea6218480691 + redeem_script0 = bytearray.fromhex( + '0014fcf9969ce1c98a135ed293719721fb69f0b686cb') + # Spending from a P2SH-P2WSH coin, + # txhash:6b536caf727ccd02c395a1d00b752098ec96e8ec46c96bee8582be6b5060fa2f + redeem_script1 = bytearray.fromhex( + '0020fc8b08ed636cb23afcb425ff260b3abd03380a2333b54cfa5d51ac52d803baf4') + redeem_scripts = [redeem_script0, redeem_script1] + + # Create outputs in segwit addresses + tx, tx_hex = self.create_tx_spending_to_segwit(redeem_scripts) + tx_id = node.sendrawtransaction(tx_hex) + assert(tx_id in set(node.getrawmempool())) + + node.generate(1) + assert(tx_id not in set(node.getrawmempool())) + + def spend_from_segwit(txin, redeem_scripts): + tx = CTransaction() + tx.vin = [] + amount = 0 + for i in range(len(redeem_scripts)): + tx.vin.append(CTxIn(COutPoint(txin.sha256, i))) + tx.vin[i].scriptSig = CScript([redeem_scripts[i]]) + amount += txin.vout[i].nValue + tx.vout = [CTxOut(amount, CScript([])), + CTxOut(0, CScript([random.getrandbits(800), OP_RETURN]))] + tx.vout[0].nValue -= node.calculate_fee(tx) + tx.rehash() + return tx + + def next_block(block_time, spend_tx=None): + # get block height + blockchaininfo = node.getblockchaininfo() + height = int(blockchaininfo['blocks']) + + # create the block + coinbase = create_coinbase(height) + coinbase.rehash() + block = create_block( + int(node.getbestblockhash(), 16), coinbase, block_time) + if spend_tx: + # Add this tx in this block and recompute the merkle root + # We ignore the transaction fees + block.vtx.append(spend_tx) + block.hashMerkleRoot = block.calc_merkle_root() + + # Do PoW, which is cheap on regnet + block.solve() + return block + + # returns a test case that asserts that the current tip was accepted + def accepted(tip): + return TestInstance([[tip, True]]) + + # returns a test case that asserts that the current tip was rejected + def rejected(tip, reject=None): + if reject is None: + return TestInstance([[tip, False]]) + else: + return TestInstance([[tip, reject]]) + + # Push MTP forward just before activation. + node.setmocktime(GREAT_WALL_START_TIME) + + for i in range(6): + b = next_block(GREAT_WALL_START_TIME + i - 1) + yield accepted(b) + + assert_equal( + node.getblockheader(node.getbestblockhash())['mediantime'], + GREAT_WALL_START_TIME - 1) + + # Check that segwit spending tx is not acceptable into the + # mempool using default policy config + self.log.info( + "Try to spend a segwit coin before activation via mempool") + + tx_spend = spend_from_segwit(tx, redeem_scripts) + tx_spend_hex = ToHex(tx_spend) + assert_raises_rpc_error(-26, RPC_CLEANSTACK_ERROR, + node.sendrawtransaction, tx_spend_hex) + + # Check that segwit spending tx is not acceptable in a block + self.log.info( + "Try to spend a segwit coin before activation in a new block") + b = next_block(GREAT_WALL_START_TIME + 6, tx_spend) + yield rejected(b, RejectResult(16, b'blk-bad-inputs')) + + self.log.info("Activate cleanstack_exception") + fork_block = next_block(GREAT_WALL_START_TIME + 6) + yield accepted(fork_block) + + assert_equal( + node.getblockheader(node.getbestblockhash())['mediantime'], + GREAT_WALL_START_TIME) + + # Check that segwit spending tx is still not acceptable into the + # mempool using default policy config + self.log.info( + "Try to spend a segwit coin after activation via mempool") + + assert_raises_rpc_error(-26, RPC_CLEANSTACK_ERROR, + node.sendrawtransaction, tx_spend_hex) + + # Check that segwit spending tx is acceptable in a block + self.log.info( + "Try to spend a segwit coin after activation in a new block") + b = next_block(GREAT_WALL_START_TIME + 7, tx_spend) + yield accepted(b) + + # Cause a reorg to check that the activation wasn't latched + self.log.info( + "Cause a reorg that deactivates cleanstack_exception and test again") + node.invalidateblock(fork_block.hash) + + # Check that segwit spending tx is again not acceptable in a block + self.log.info( + "Try to spend a segwit coin after a reorg in a new pre-activation block") + b = next_block(GREAT_WALL_START_TIME + 6, tx_spend) + yield rejected(b, RejectResult(16, b'blk-bad-inputs')) + + +if __name__ == '__main__': + CleanstackExceptionActivationTest().main()