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 + // recovering coins sent to segwit addresses + if (IsGreatWallEnabled(config, pChainTip)) { + flags |= SCRIPT_ALLOW_SEGWIT_RECOVERY; + } + // We make sure this node will have replay protection during the next hard // fork. if (IsReplayProtectionEnabled(config, pChainTip)) { @@ -2222,6 +2228,31 @@ } } + // If this block deactivates the Great wall fork, we remove and reprocess + // all mempool entries + if (IsGreatWallEnabled(config, pindexDelete) && + !IsGreatWallEnabled(config, pindexDelete->pprev)) { + if (disconnectpool) { + LogPrint( + BCLog::MEMPOOL, + "Reprocessing mempool for a reorg crossing a fork boundary"); + // We fake we disconnected a block that contained all mempool + // transactions. addForBlock takes care of the topological ordering + // in disconnectpool's internal queue. Caller must call + // updateMempoolForReorg + std::vector vtx; + vtx.reserve(g_mempool.mapTx.size()); + for (const CTxMemPoolEntry &e : g_mempool.mapTx) { + vtx.push_back(e.GetSharedTx()); + } + disconnectpool->addForBlock(vtx); + } else { + LogPrint(BCLog::MEMPOOL, + "Clearing mempool for a reorg crossing a fork boundary"); + } + g_mempool.clear(); + } + if (disconnectpool) { disconnectpool->addForBlock(block.vtx); } diff --git a/test/functional/abc-segwit-recovery-activation.py b/test/functional/abc-segwit-recovery-activation.py new file mode 100755 --- /dev/null +++ b/test/functional/abc-segwit-recovery-activation.py @@ -0,0 +1,233 @@ +#!/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. +""" +This test checks activation of the SCRIPT_ALLOW_SEGWIT_RECOVERY flag +""" + +from test_framework.test_framework import ComparisonTestFramework +from test_framework.util import satoshi_round, assert_equal +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 + + +class SegwitRecoveryActivationTest(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, + "-promiscuousmempoolflags=0", + "-acceptnonstdtxn", + "-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) + network_thread_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 create_dummy_tx(self, utxo_n): + node = self.nodes[0] + utxos = node.listunspent() + assert(len(utxos) > utxo_n) + utxo = utxos[utxo_n] + tx = CTransaction() + value = int(satoshi_round(utxo["amount"]) * COIN) + tx.vin = [CTxIn(COutPoint(int(utxo["txid"], 16), utxo["vout"]))] + tx.vout = [CTxOut(value, CScript([OP_TRUE]))] + 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 in 2 separate transactions + tx0, tx0_hex = self.create_tx_spending_to_segwit(redeem_scripts) + tx0_id = node.sendrawtransaction(tx0_hex) + tx1, tx1_hex = self.create_tx_spending_to_segwit(redeem_scripts) + tx1_id = node.sendrawtransaction(tx1_hex) + assert(tx0_id in node.getrawmempool()) + assert(tx1_id in node.getrawmempool()) + + node.generate(1) + assert(tx0_id not in node.getrawmempool()) + assert(tx1_id not in node.getrawmempool()) + + # Create 2 dummy transactions (non segwit related) with pre-fork + # coinbases + _, tx_dummy0 = self.create_dummy_tx(0) + _, tx_dummy1 = self.create_dummy_tx(1) + + 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([OP_RETURN, random.getrandbits(800)]))] + 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]]) + + # returns a test case that asserts that the transaction was accepted + def tx_accepted(tx): + return TestInstance([[tx, True]]) + + # returns a test case that asserts that the transaction was rejected + def tx_rejected(tx, reject): + return TestInstance([[tx, 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 + self.log.info( + "Try to spend a segwit coin before activation via mempool") + tx_spend0 = spend_from_segwit(tx0, redeem_scripts) + yield tx_rejected(tx_spend0, RejectResult(16, b'mandatory-script-verify-flag-failed (Script did not clean its stack)')) + + # 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_spend0) + yield rejected(b, RejectResult(16, b'blk-bad-inputs')) + + self.log.info("Activate SEGWIT_RECOVERY") + 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 acceptable into the mempool since + # standardness checks were disabled + self.log.info( + "Try to spend a segwit coin after activation via mempool") + yield tx_accepted(tx_spend0) + + # 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_spend0) + yield accepted(b) + + # Add a dummy tx in a block + tx_dummy0_id = node.sendrawtransaction(tx_dummy0) + assert(tx_dummy0_id in node.getrawmempool()) + + node.generate(1) + assert(tx_dummy0_id not in node.getrawmempool()) + + # Push the 2nd segwit spending tx into the mempool + tx_spend1 = spend_from_segwit(tx1, redeem_scripts) + tx_spend1_id = node.sendrawtransaction(ToHex(tx_spend1)) + assert(tx_spend1_id in node.getrawmempool()) + + # Push the 2nd dummy tx into the mempool + tx_dummy1_id = node.sendrawtransaction(tx_dummy1) + assert(tx_dummy1_id in node.getrawmempool()) + + # Cause a reorg to check that the activation wasn't latched + self.log.info( + "Cause a reorg that deactivates SEGWIT_RECOVERY 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_spend0) + yield rejected(b, RejectResult(16, b'blk-bad-inputs')) + + # Check that both segwit spending transactions haven't returned into + # the mempool and that both dummy transactions have + assert(set(node.getrawmempool()) == set([tx_dummy0_id, tx_dummy1_id])) + +if __name__ == '__main__': + SegwitRecoveryActivationTest().main()