diff --git a/src/validation.cpp b/src/validation.cpp --- a/src/validation.cpp +++ b/src/validation.cpp @@ -392,6 +392,11 @@ return IsGreatWallEnabled(config, chainActive.Tip()); } +static bool IsGravitonEnabledForCurrentBlock(const Config &config) { + AssertLockHeld(cs_main); + return IsGravitonEnabled(config, chainActive.Tip()); +} + // Command-line argument "-replayprotectionactivationtime=" will // cause the node to switch to replay protected SigHash ForkID value when the // median timestamp of the previous 11 blocks is greater than or equal to @@ -724,6 +729,10 @@ extraFlags |= SCRIPT_ENABLE_SCHNORR; } + if (IsGravitonEnabledForCurrentBlock(config)) { + extraFlags |= SCRIPT_VERIFY_NULLDUMMY; + } + // Check inputs based on the set of flags we activate. uint32_t scriptVerifyFlags = STANDARD_SCRIPT_VERIFY_FLAGS; if (!config.GetChainParams().RequireStandard()) { @@ -1687,6 +1696,13 @@ flags |= SCRIPT_ENABLE_SCHNORR; } + // If the Graviton upgrade is enabled, we start enforcing NULLDUMMY + // verification to deal with CHECKMULTISIG dummy stack element + // malleablility. + if (IsGravitonEnabled(config, pChainTip)) { + flags |= SCRIPT_VERIFY_NULLDUMMY; + } + // We make sure this node will have replay protection during the next hard // fork. if (IsReplayProtectionEnabled(config, pChainTip)) { diff --git a/test/functional/abc-nulldummy-activation.py b/test/functional/abc-nulldummy-activation.py new file mode 100755 --- /dev/null +++ b/test/functional/abc-nulldummy-activation.py @@ -0,0 +1,205 @@ +#!/usr/bin/env python3 +# Copyright (c) 2016 The Bitcoin Core developers +# 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. + +from test_framework.blocktools import create_coinbase, create_block, make_conform_to_ctor +from test_framework.test_framework import BitcoinTestFramework +from test_framework.util import assert_equal, assert_raises_rpc_error +from test_framework.mininode import network_thread_start, P2PInterface +from test_framework.messages import ToHex, FromHex, CTransaction, msg_tx, msg_block, COIN +from test_framework.script import CScript + +NULLDUMMY_ERROR = "64: non-mandatory-script-verify-flag (Dummy CHECKMULTISIG argument must be zero)" + +# far into the future +GRAVITON_START_TIME = 2000000000 + +# If we don't do this, autoreplay protection will activate simultaneous with +# graviton and all our sigs will mysteriously fail. +REPLAY_PROTECTION_START_TIME = GRAVITON_START_TIME * 2 + + +def swap_dummy_element(tx): + assert(len(tx.vin[0].scriptSig)) + assert(tx.vin[0].scriptSig[0] == 0) + scriptSig = CScript(tx.vin[0].scriptSig) + newscript = [] + for i in scriptSig: + if (len(newscript) == 0): + assert(len(i) == 0) + newscript.append(b'\x51') + else: + newscript.append(i) + tx.vin[0].scriptSig = CScript(newscript) + tx.rehash() + + +class BlockHeader: + def __init__(self, header): + self.header = header + + def __getattr__(self, name): + return self.header[name] + + def hash_as_int(self): + return int(self.hash, 16) + + +def fetch_best_header(node): + return BlockHeader(node.getblockheader(node.getbestblockhash())) + + +class NullDummyActivation(BitcoinTestFramework): + def set_test_params(self): + self.num_nodes = 1 + self.setup_clean_chain = True + self.extra_args = [[ + # promiscuous allows us to accept non-std transactions in ATM + "-promiscuousmempoolflags=1", + "-whitelist=127.0.0.1", + "-gravitonactivationtime={}".format( + GRAVITON_START_TIME), + "-replayprotectionactivationtime={}".format( + REPLAY_PROTECTION_START_TIME)]] + self.mocktime = GRAVITON_START_TIME - 200 + self.lastblock = None + + def run_test(self): + self.nodes[0].add_p2p_connection(P2PInterface()) + network_thread_start() + + coinbase_utxo = self.create_coinbase_utxo(4) + + self.test_nulldummy_before_activation(coinbase_utxo.pop()) + self.move_mtp(GRAVITON_START_TIME - 1) + self.test_nulldummy_before_activation(coinbase_utxo.pop()) + + # Activation time. + before_activation = self.lastblock.hash + self.move_mtp(GRAVITON_START_TIME) + self.test_nulldummy_after_activation(coinbase_utxo.pop()) + + self.log.info("Rollback to before activation time.") + self.nodes[0].invalidateblock(before_activation) + self.lastblock = fetch_best_header(self.nodes[0]) + assert(not self.is_graviton_activated()) + self.test_nulldummy_before_activation(coinbase_utxo.pop()) + + def test_nulldummy_before_activation(self, coinbase_utxo): + self.log.info( + "Check that nulldummy is accepted to mempool/block before activation") + assert(not self.is_graviton_activated()) + + p2pk_address = self.nodes[0].getnewaddress() + ms_address = self.nodes[0].addmultisigaddress(1, [p2pk_address]) + + [to_multisig_tx, ms_utxo] = self.create_transaction( + coinbase_utxo, ms_address) + self.nodes[0].sendrawtransaction(ToHex(to_multisig_tx)) + + [from_multisig_tx, _] = self.create_transaction(ms_utxo, ms_address) + + # Swap out nulldummy element with non-nulldummy, making the tx non-standard. + swap_dummy_element(from_multisig_tx) + + # promiscuousmempoolflags allows the transaction to be accepted to + # mempool even though it's non-standard. + self.nodes[0].p2p.send_message(msg_tx(from_multisig_tx)) + self.nodes[0].p2p.sync_with_ping() + assert(from_multisig_tx.hash in set(self.nodes[0].getrawmempool())) + + block = self.create_block([to_multisig_tx, from_multisig_tx]) + self.nodes[0].p2p.send_message(msg_block(block)) + self.nodes[0].p2p.sync_with_ping() + self.lastblock = fetch_best_header(self.nodes[0]) + assert_equal(self.lastblock.hash, block.hash) + + def test_nulldummy_after_activation(self, coinbase_utxo): + self.log.info( + "Check that nulldummy is rejected from mempool/block after activation") + assert(self.is_graviton_activated()) + + p2pk_address = self.nodes[0].getnewaddress() + ms_address = self.nodes[0].addmultisigaddress(1, [p2pk_address]) + + [to_multisig_tx, ms_utxo] = self.create_transaction( + coinbase_utxo, ms_address) + self.nodes[0].sendrawtransaction(ToHex(to_multisig_tx)) + + [from_multisig_tx, _] = self.create_transaction(ms_utxo, ms_address) + + # Swap out nulldummy element with non-nulldummy, making the tx non-standard. + swap_dummy_element(from_multisig_tx) + + self.nodes[0].p2p.send_message(msg_tx(from_multisig_tx)) + self.nodes[0].p2p.sync_with_ping() + assert(from_multisig_tx.hash not in set(self.nodes[0].getrawmempool())) + + assert_raises_rpc_error(-26, NULLDUMMY_ERROR, + self.nodes[0].sendrawtransaction, ToHex(from_multisig_tx)) + + block = self.create_block([to_multisig_tx, from_multisig_tx]) + self.nodes[0].p2p.send_message(msg_block(block)) + self.nodes[0].p2p.sync_with_ping() + assert_equal(self.lastblock.hash, self.nodes[0].getbestblockhash()) + + self.nodes[0].submitblock(ToHex(block)) + assert_equal(self.lastblock.hash, self.nodes[0].getbestblockhash()) + + def create_coinbase_utxo(self, n): + coinbase_utxo = [] + for block in self.nodes[0].generate(n): + txid = self.nodes[0].getblock(block)['tx'][0] + coinbase_utxo.append({'txid': txid, 'vout': 0, 'value': 50 * COIN}) + + self.nodes[0].generate(101) + self.lastblock = fetch_best_header(self.nodes[0]) + return coinbase_utxo + + def move_mtp(self, new_mtp): + assert new_mtp >= self.lastblock.mediantime + + sign = "+" if new_mtp >= GRAVITON_START_TIME else "-" + self.log.info("Moving MTP to GRAVITON_START_TIME {} {}".format( + sign, abs(new_mtp - GRAVITON_START_TIME))) + while self.lastblock.mediantime != new_mtp: + self.nodes[0].setmocktime(self.lastblock.time + 1) + self.nodes[0].generate(1) + self.lastblock = fetch_best_header(self.nodes[0]) + + def is_graviton_activated(self): + return self.lastblock.mediantime >= GRAVITON_START_TIME + + def create_block(self, txs): + block = create_block(self.lastblock.hash_as_int(), create_coinbase( + self.lastblock.height + 1), self.lastblock.time + 1) + block.nVersion = 4 + block.vtx.extend(txs) + make_conform_to_ctor(block) + block.hashMerkleRoot = block.calc_merkle_root() + block.rehash() + block.solve() + return block + + # Returns the created transaction + an utxo to spend it + def create_transaction(self, spend, to_address): + node = self.nodes[0] + TX_FEE = 1000 + + inputs = [{ + "txid": spend['txid'], + "vout": spend['vout']}] + amount = spend['value'] - TX_FEE + outputs = {to_address: amount / COIN} + + rawtx = node.createrawtransaction(inputs, outputs) + signresult = node.signrawtransactionwithwallet(rawtx) + tx = FromHex(CTransaction(), signresult['hex']) + tx.calc_sha256() + return [tx, {"txid": tx.hash, "vout": 0, "value": amount}] + + +if __name__ == '__main__': + NullDummyActivation().main()