diff --git a/doc/release-notes.md b/doc/release-notes.md --- a/doc/release-notes.md +++ b/doc/release-notes.md @@ -13,3 +13,14 @@ `listunspent` RPC have been deprecated and will be removed in a future version. To keep using these fields, use the `-deprecatedrpc=mempool_ancestors_descendants` option. + +Network upgrade +--------------- + +At the MTP time of `1684152000` (May 15, 2023 12:00:00 UTC), the following +changes will become activated: + - New consensus rule: The `nVersion` field in `CTransaction` now must be either + 1 or 2. This has been a policy rule (i.e. wallets already cannot use anything + other than 1 or 2), but miners were still able to mine transactions with + versions other than 1 and 2. Disallowing them by consensus allows us to use + the version field for e.g. a new & scalable transaction format. diff --git a/src/consensus/activation.h b/src/consensus/activation.h --- a/src/consensus/activation.h +++ b/src/consensus/activation.h @@ -44,6 +44,8 @@ const CBlockIndex *pindexPrev); /** Check if May 15th, 2023 protocol upgrade has activated. */ +bool IsWellingtonEnabled(const Consensus::Params ¶ms, + int64_t nMedianTimePast); bool IsWellingtonEnabled(const Consensus::Params ¶ms, const CBlockIndex *pindexPrev); diff --git a/src/consensus/activation.cpp b/src/consensus/activation.cpp --- a/src/consensus/activation.cpp +++ b/src/consensus/activation.cpp @@ -101,13 +101,17 @@ return IsGluonEnabled(params, pindexPrev->nHeight); } +bool IsWellingtonEnabled(const Consensus::Params ¶ms, + int64_t nMedianTimePast) { + return nMedianTimePast >= gArgs.GetIntArg("-wellingtonactivationtime", + params.wellingtonActivationTime); +} + bool IsWellingtonEnabled(const Consensus::Params ¶ms, const CBlockIndex *pindexPrev) { if (pindexPrev == nullptr) { return false; } - return pindexPrev->GetMedianTimePast() >= - gArgs.GetIntArg("-wellingtonactivationtime", - params.wellingtonActivationTime); + return IsWellingtonEnabled(params, pindexPrev->GetMedianTimePast()); } diff --git a/src/consensus/tx_verify.cpp b/src/consensus/tx_verify.cpp --- a/src/consensus/tx_verify.cpp +++ b/src/consensus/tx_verify.cpp @@ -56,6 +56,15 @@ } } + if (IsWellingtonEnabled(params, nMedianTimePast)) { + // Restrict version to 1 and 2 + if (tx.nVersion > CTransaction::MAX_CONSENSUS_VERSION || + tx.nVersion < CTransaction::MIN_CONSENSUS_VERSION) { + return state.Invalid(TxValidationResult::TX_CONSENSUS, + "bad-txns-version"); + } + } + return true; } diff --git a/src/policy/policy.cpp b/src/policy/policy.cpp --- a/src/policy/policy.cpp +++ b/src/policy/policy.cpp @@ -68,7 +68,9 @@ bool IsStandardTx(const CTransaction &tx, bool permit_bare_multisig, const CFeeRate &dust_relay_fee, std::string &reason) { - if (tx.nVersion > CTransaction::MAX_STANDARD_VERSION || tx.nVersion < 1) { + // Only allow these tx versions, will become consensus rule in Wellington + if (tx.nVersion > CTransaction::MAX_STANDARD_VERSION || + tx.nVersion < CTransaction::MIN_STANDARD_VERSION) { reason = "version"; return false; } diff --git a/src/primitives/transaction.h b/src/primitives/transaction.h --- a/src/primitives/transaction.h +++ b/src/primitives/transaction.h @@ -194,13 +194,15 @@ class CTransaction { public: // Default transaction version. - static const int32_t CURRENT_VERSION = 2; + static constexpr int32_t CURRENT_VERSION = 2; - // Changing the default transaction version requires a two step process: - // first adapting relay policy by bumping MAX_STANDARD_VERSION, and then - // later date bumping the default CURRENT_VERSION at which point both - // CURRENT_VERSION and MAX_STANDARD_VERSION will be equal. - static const int32_t MAX_STANDARD_VERSION = 2; + // Policy: Valid min/max for nVersion. + // Remove after wellington activation. + static constexpr int32_t MIN_STANDARD_VERSION = 1, MAX_STANDARD_VERSION = 2; + + // Consensus: Valid min/max for nVersion, enforced after Wellington. + static constexpr int32_t MIN_CONSENSUS_VERSION = 1, + MAX_CONSENSUS_VERSION = 2; // The local variables are made const to prevent unintended modification // without updating the cached hash value. However, CTransaction is not diff --git a/src/test/activation_tests.cpp b/src/test/activation_tests.cpp --- a/src/test/activation_tests.cpp +++ b/src/test/activation_tests.cpp @@ -55,15 +55,20 @@ blocks[i].pprev = &blocks[i - 1]; } BOOST_CHECK(!IsWellingtonEnabled(params, &blocks.back())); + BOOST_CHECK( + !IsWellingtonEnabled(params, blocks.back().GetMedianTimePast())); SetMTP(blocks, activation - 1); BOOST_CHECK(!IsWellingtonEnabled(params, &blocks.back())); + BOOST_CHECK(!IsWellingtonEnabled(params, activation - 1)); SetMTP(blocks, activation); BOOST_CHECK(IsWellingtonEnabled(params, &blocks.back())); + BOOST_CHECK(IsWellingtonEnabled(params, activation)); SetMTP(blocks, activation + 1); BOOST_CHECK(IsWellingtonEnabled(params, &blocks.back())); + BOOST_CHECK(IsWellingtonEnabled(params, activation + 1)); } BOOST_AUTO_TEST_SUITE_END() diff --git a/test/functional/feature_tx_version.py b/test/functional/feature_tx_version.py new file mode 100644 --- /dev/null +++ b/test/functional/feature_tx_version.py @@ -0,0 +1,177 @@ +#!/usr/bin/env python3 +# Copyright (c) 2023 The Bitcoin developers +# Distributed under the MIT software license, see the accompanying +# file COPYING or http://www.opensource.org/licenses/mit-license.php. + +""" +Test enforcement of strict tx version. Before Wellington, it is a relay-only +rule and after, tx versions must be either 1 or 2 by consensus. +""" + +from typing import Optional + +from test_framework.address import P2SH_OP_TRUE, SCRIPTSIG_OP_TRUE +from test_framework.blocktools import ( + create_block, + create_coinbase, + create_tx_with_script, + make_conform_to_ctor, +) +from test_framework.messages import CBlock, FromHex +from test_framework.p2p import P2PDataStore +from test_framework.test_framework import BitcoinTestFramework +from test_framework.txtools import pad_tx +from test_framework.util import assert_equal, assert_greater_than_or_equal + +OK_VERSIONS = [1, 2] +BAD_VERSIONS = [-0x80000000, -0x7fffffff, -2, -1, 0, 3, 7, 0x100, 0x7fffffff] + +START_TIME = 1_900_000_000 +ACTIVATION_TIME = 2_000_000_000 + + +class TxVersionTest(BitcoinTestFramework): + def set_test_params(self): + self.num_nodes = 1 + self.setup_clean_chain = True + self.extra_args = [[f'-wellingtonactivationtime={ACTIVATION_TIME}', + '-acceptnonstdtxn=0', + '-whitelist=127.0.0.1']] + + def run_test(self): + self.block_heights = dict() + + node = self.nodes[0] + node.setmocktime(START_TIME) + peer = node.add_p2p_connection(P2PDataStore()) + + genesis = FromHex(CBlock(), node.getblock(node.getbestblockhash(), 0)) + + # Mine us some coins + blocks = [genesis] + num_spendable_txs = 30 + spendable_txs = [] + for i in range(100 + num_spendable_txs): + block = self.make_block(blocks[-1]) + if i < num_spendable_txs: + spendable_txs.append(block.vtx[0]) + blocks.append(block) + peer.send_blocks_and_test(blocks[1:], node, success=True) + + def test_mempool_accepts_ok_versions(): + for ok_version in OK_VERSIONS: + spendable_tx = spendable_txs.pop(0) + tx = self.make_tx(spendable_tx, nVersion=ok_version) + peer.send_txs_and_test([tx], node, success=True) + + def test_mempool_rejects_bad_versions(): + bad_version_txs = [] + for bad_version in BAD_VERSIONS: + spendable_tx = spendable_txs.pop(0) + tx = self.make_tx(spendable_tx, nVersion=bad_version) + bad_version_txs.append(tx) + peer.send_txs_and_test( + [tx], + node, + success=False, + reject_reason="was not accepted: version") + return bad_version_txs + + self.log.info("These are always OK for the mempool") + test_mempool_accepts_ok_versions() + + self.log.info("Bad versions always rejected from mempool") + bad_version_txs = test_mempool_rejects_bad_versions() + + self.log.info( + "Before Wellington, we CAN mine blocks with txs with bad versions") + block = self.make_block(blocks[-1], txs=bad_version_txs) + peer.send_blocks_and_test([block], node, success=True) + blocks.append(block) + + self.log.info( + "Before Wellington, we CAN mine blocks with a coinbase with a bad " + "version") + for bad_version in BAD_VERSIONS: + block = self.make_block(blocks[-1], coinbase_version=bad_version) + peer.send_blocks_and_test([block], node, success=True) + blocks.append(block) + + self.log.info( + "Activate Wellington, mine 6 blocks starting at ACTIVATION_TIME") + node.setmocktime(ACTIVATION_TIME) + for offset in range(0, 6): + block = self.make_block( + blocks[-1], nTime=ACTIVATION_TIME + offset) + peer.send_blocks_and_test([block], node, success=True) + blocks.append(block) + + assert_equal(node.getblockchaininfo()["mediantime"], ACTIVATION_TIME) + + self.log.info("Wellington activated!") + + self.log.info("Mempool still accepts OK versions") + test_mempool_accepts_ok_versions() + + self.log.info("Bad version still rejected from mempool") + bad_version_txs = test_mempool_rejects_bad_versions() + + self.log.info( + "After activation, we CANNOT mine blocks with txs with bad " + "versions anymore") + for bad_tx in bad_version_txs: + block = self.make_block(blocks[-1], txs=[bad_tx]) + peer.send_blocks_and_test( + [block], + node, + success=False, + reject_reason="bad-txns-version") + + self.log.info( + "After activation, we CANNOT mine blocks with a coinbase with a " + "bad version anymore") + for bad_version in BAD_VERSIONS: + block = self.make_block(blocks[-1], coinbase_version=bad_version) + peer.send_blocks_and_test( + [block], + node, + success=False, + reject_reason="bad-txns-version") + + def make_tx(self, spend_tx, nVersion): + value = spend_tx.vout[0].nValue - 1000 + assert_greater_than_or_equal(value, 546) + tx = create_tx_with_script( + spend_tx, 0, amount=value, script_pub_key=P2SH_OP_TRUE) + tx.nVersion = nVersion + tx.vin[0].scriptSig = SCRIPTSIG_OP_TRUE + pad_tx(tx) + tx.rehash() + return tx + + def make_block(self, prev_block: CBlock, *, nTime: Optional[int] = None, + coinbase_version=None, txs=None) -> CBlock: + if prev_block.sha256 is None: + prev_block.rehash() + assert prev_block.sha256 is not None + + block_time = prev_block.nTime + 1 if nTime is None else nTime + height = self.block_heights.get(prev_block.sha256, 0) + 1 + coinbase = create_coinbase(height) + coinbase.vout[0].scriptPubKey = P2SH_OP_TRUE + if coinbase_version is not None: + coinbase.nVersion = coinbase_version + coinbase.rehash() + + block = create_block(prev_block.sha256, coinbase, block_time) + if txs: + block.vtx += txs + make_conform_to_ctor(block) + block.hashMerkleRoot = block.calc_merkle_root() + block.solve() + self.block_heights[block.sha256] = height + return block + + +if __name__ == '__main__': + TxVersionTest().main() diff --git a/test/functional/test_framework/address.py b/test/functional/test_framework/address.py --- a/test/functional/test_framework/address.py +++ b/test/functional/test_framework/address.py @@ -13,6 +13,8 @@ ADDRESS_ECREG_UNSPENDABLE_DESCRIPTOR = 'addr(ecregtest:qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqcrl5mqkt)#u6xx93xc' # Coins sent to this address can be spent with a scriptSig of just OP_TRUE ADDRESS_ECREG_P2SH_OP_TRUE = 'ecregtest:prdpw30fk4ym6zl6rftfjuw806arpn26fvkgfu97xt' +P2SH_OP_TRUE = CScript.fromhex( + 'a914da1745e9b549bd0bfa1a569971c77eba30cd5a4b87') SCRIPTSIG_OP_TRUE = CScriptOp.encode_op_pushdata(CScript([OP_TRUE])) chars = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz'