diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -604,6 +604,7 @@ node/transaction.cpp node/ui_interface.cpp noui.cpp + policy/blockparking.cpp policy/fees.cpp policy/packages.cpp policy/settings.cpp diff --git a/src/minerfund.cpp b/src/minerfund.cpp --- a/src/minerfund.cpp +++ b/src/minerfund.cpp @@ -52,9 +52,8 @@ GetMinerFundWhitelist(const Consensus::Params ¶ms, const CBlockIndex *pindexPrev) { if (IsWellingtonEnabled(params, pindexPrev)) { - // FIXME When Wellington activates, return the whitelist used by policy - // checks. - return GetMinerFundWhitelist(params, pindexPrev, true); + // Return the whitelist used by policy checks. + return GetMinerFundWhitelist(params, pindexPrev, false); } // Wellington has not activated, so use the whitelist that consensus checks @@ -73,9 +72,7 @@ return {}; } - if (isConsensus) { - // FIXME Add check for Wellington activation once the policy is fully - // implemented. + if (isConsensus && !IsWellingtonEnabled(params, pindexPrev)) { return {GetMinerFundDestination(!IsGluonEnabled(params, pindexPrev))}; } diff --git a/src/policy/blockparking.h b/src/policy/blockparking.h new file mode 100644 --- /dev/null +++ b/src/policy/blockparking.h @@ -0,0 +1,46 @@ +// 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. + +#ifndef BITCOIN_POLICY_BLOCKPARKING_H +#define BITCOIN_POLICY_BLOCKPARKING_H + +#include <consensus/amount.h> +#include <primitives/block.h> + +#include <memory> +#include <vector> + +class CBlockIndex; + +namespace Consensus { +struct Params; +} + +struct ParkingPolicy { + virtual ~ParkingPolicy() {} + + virtual bool operator()() { return false; } +}; + +struct MinerFundPolicy : ParkingPolicy { + const CBlock &m_block; + const Amount &m_blockReward; + const Consensus::Params &m_consensusParams; + const CBlockIndex *m_pindex; + + MinerFundPolicy(const Consensus::Params &consensusParams, + const CBlockIndex *pindex, const CBlock &block, + const Amount &blockReward) + : m_block(block), m_blockReward(blockReward), + m_consensusParams(consensusParams), m_pindex(pindex) {} + + bool operator()() override; +}; + +std::vector<std::shared_ptr<ParkingPolicy>> +GetParkingPoliciesForBlock(const Consensus::Params ¶ms, + const CBlockIndex *pindex, const CBlock &block, + const Amount &blockReward); + +#endif // BITCOIN_POLICY_BLOCKPARKING_H diff --git a/src/policy/blockparking.cpp b/src/policy/blockparking.cpp new file mode 100644 --- /dev/null +++ b/src/policy/blockparking.cpp @@ -0,0 +1,27 @@ +// 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. + +#include <policy/blockparking.h> + +#include <blockindex.h> +#include <minerfund.h> + +std::vector<std::shared_ptr<ParkingPolicy>> +GetParkingPoliciesForBlock(const Consensus::Params ¶ms, + const CBlockIndex *pindex, const CBlock &block, + const Amount &blockReward) { + if (!pindex->pprev) { + return {}; + } + + return { + std::make_shared<MinerFundPolicy>(params, pindex, block, blockReward)}; +} + +bool MinerFundPolicy::operator()() { + assert(m_pindex->pprev); + assert(m_block.vtx.size()); + return !CheckMinerFund(m_consensusParams, m_pindex->pprev, + m_block.vtx[0]->vout, m_blockReward, false); +} diff --git a/src/test/CMakeLists.txt b/src/test/CMakeLists.txt --- a/src/test/CMakeLists.txt +++ b/src/test/CMakeLists.txt @@ -186,6 +186,7 @@ netbase_tests.cpp op_reversebytes_tests.cpp pmt_tests.cpp + policy_blockparking_tests.cpp policy_fee_tests.cpp policyestimator_tests.cpp prevector_tests.cpp diff --git a/src/test/minerfund_tests.cpp b/src/test/minerfund_tests.cpp --- a/src/test/minerfund_tests.cpp +++ b/src/test/minerfund_tests.cpp @@ -85,11 +85,10 @@ BOOST_CHECK(!IsWellingtonEnabled(consensusParams, &blocks.back())); CheckWhitelists(expectedMinerFund, {}); + // Wellington is activated. The whitelist moves from consensus to policy. SetMTP(blocks, activation); BOOST_CHECK(IsWellingtonEnabled(consensusParams, &blocks.back())); - // FIXME Once policy is fully implemented, consensus whitelist should be - // empty (deactivated). - CheckWhitelists(expectedMinerFund, expectedMinerFund); + CheckWhitelists({}, expectedMinerFund); } BOOST_AUTO_TEST_SUITE_END() diff --git a/src/test/policy_blockparking_tests.cpp b/src/test/policy_blockparking_tests.cpp new file mode 100644 --- /dev/null +++ b/src/test/policy_blockparking_tests.cpp @@ -0,0 +1,193 @@ +// 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. + +#include <policy/blockparking.h> + +#include <blockindex.h> +#include <chainparams.h> +#include <consensus/activation.h> +#include <key_io.h> +#include <minerfund.h> +#include <validation.h> + +#include <test/util/blockindex.h> +#include <test/util/setup_common.h> + +#include <boost/test/unit_test.hpp> + +BOOST_FIXTURE_TEST_SUITE(policy_blockparking_tests, BasicTestingSetup) + +static CBlock GetBlockWithMinerFund(const CChainParams &chainparams, int height, + const Amount &minerFundAmount) { + CBlock block; + CMutableTransaction coinbaseTx; + + coinbaseTx.vout.resize(1); + coinbaseTx.vout[0].nValue = minerFundAmount; + const auto minerFund = DecodeDestination( + "ecash:prfhcnyqnl5cgrnmlfmms675w93ld7mvvqd0y8lz07", chainparams); + coinbaseTx.vout[0].scriptPubKey = GetScriptForDestination(minerFund); + block.vtx.push_back(MakeTransactionRef(std::move(coinbaseTx))); + + return block; +} + +static CBlock GetBlock(const CChainParams &chainparams, int height) { + CBlock block; + CMutableTransaction coinbaseTx; + + coinbaseTx.vout.resize(1); + coinbaseTx.vout[0].scriptPubKey = CScript() << OP_TRUE; + block.vtx.push_back(MakeTransactionRef(std::move(coinbaseTx))); + + return block; +} + +BOOST_AUTO_TEST_CASE(get_policies) { + const CChainParams &chainparams = Params(); + const Consensus::Params &consensusParams = chainparams.GetConsensus(); + + std::array<CBlockIndex, 12> blocks; + for (size_t i = 1; i < blocks.size(); ++i) { + blocks[i].pprev = &blocks[i - 1]; + blocks[i].nHeight = consensusParams.gluonHeight + i; + } + CBlockIndex &blockIndex = blocks.back(); + + const Amount blockReward = + GetBlockSubsidy(blockIndex.nHeight, consensusParams); + CBlock block = GetBlockWithMinerFund(chainparams, blockIndex.nHeight, + GetMinerFundAmount(blockReward)); + + const auto activation = gArgs.GetIntArg( + "-wellingtonactivationtime", consensusParams.wellingtonActivationTime); + + // Before Wellington activates + SetMTP(blocks, activation - 1); + BOOST_CHECK(!IsWellingtonEnabled(consensusParams, &blockIndex)); + { + auto policies = GetParkingPoliciesForBlock(consensusParams, &blockIndex, + block, blockReward); + BOOST_CHECK_EQUAL(policies.size(), 1); + } + + // Wellington is activated but new rules won't be enforced until next block + SetMTP(blocks, activation); + BOOST_CHECK(IsWellingtonEnabled(consensusParams, &blockIndex)); + { + auto policies = GetParkingPoliciesForBlock(consensusParams, &blockIndex, + block, blockReward); + BOOST_CHECK_EQUAL(policies.size(), 1); + } + + // Wellington rules are now enforced + SetMTP(blocks, activation + 1); + BOOST_CHECK(IsWellingtonEnabled(consensusParams, blockIndex.pprev)); + { + auto policies = GetParkingPoliciesForBlock(consensusParams, &blockIndex, + block, blockReward); + BOOST_CHECK_EQUAL(policies.size(), 1); + } +} + +BOOST_AUTO_TEST_CASE(policy_minerfund) { + const CChainParams &chainparams = Params(); + const Consensus::Params &consensusParams = chainparams.GetConsensus(); + + std::array<CBlockIndex, 12> blocks; + for (size_t i = 1; i < blocks.size(); ++i) { + blocks[i].pprev = &blocks[i - 1]; + blocks[i].nHeight = consensusParams.gluonHeight + i; + } + CBlockIndex &blockIndex = blocks.back(); + + const Amount blockReward = + GetBlockSubsidy(blockIndex.nHeight, consensusParams); + const Amount minerFund = GetMinerFundAmount(blockReward); + + const auto activation = gArgs.GetIntArg( + "-wellingtonactivationtime", consensusParams.wellingtonActivationTime); + + // Before Wellington activates + SetMTP(blocks, activation - 1); + BOOST_CHECK(!IsWellingtonEnabled(consensusParams, &blockIndex)); + + { + // Block without miner fund + CBlock block = GetBlock(chainparams, blockIndex.nHeight); + MinerFundPolicy shouldPark(consensusParams, &blockIndex, block, + blockReward); + BOOST_CHECK(!shouldPark()); + } + { + // Block with miner fund of various amounts + for (const Amount &amount : + {minerFund - 1 * SATOSHI, minerFund, minerFund + 1 * SATOSHI}) { + CBlock block = + GetBlockWithMinerFund(chainparams, blockIndex.nHeight, amount); + MinerFundPolicy shouldPark(consensusParams, &blockIndex, block, + blockReward); + BOOST_CHECK(!shouldPark()); + } + } + + // Wellington is activated but new rules won't be enforced until next block + SetMTP(blocks, activation); + BOOST_CHECK(IsWellingtonEnabled(consensusParams, &blockIndex)); + + { + // Block without miner fund + CBlock block = GetBlock(chainparams, blockIndex.nHeight); + MinerFundPolicy shouldPark(consensusParams, &blockIndex, block, + blockReward); + BOOST_CHECK(!shouldPark()); + } + { + // Block with miner fund of various amounts + for (const Amount &amount : + {minerFund - 1 * SATOSHI, minerFund, minerFund + 1 * SATOSHI}) { + CBlock block = + GetBlockWithMinerFund(chainparams, blockIndex.nHeight, amount); + MinerFundPolicy shouldPark(consensusParams, &blockIndex, block, + blockReward); + BOOST_CHECK(!shouldPark()); + } + } + + // Wellington rules are now enforced + SetMTP(blocks, activation + 1); + BOOST_CHECK(IsWellingtonEnabled(consensusParams, blockIndex.pprev)); + + { + // Block without miner fund + CBlock block = GetBlock(chainparams, blockIndex.nHeight); + MinerFundPolicy shouldPark(consensusParams, &blockIndex, block, + blockReward); + BOOST_CHECK(shouldPark()); + } + { + // Block with not enough miner fund + for (const Amount &amount : + {1 * SATOSHI, minerFund / 2, minerFund - 1 * SATOSHI}) { + CBlock block = + GetBlockWithMinerFund(chainparams, blockIndex.nHeight, amount); + MinerFundPolicy shouldPark(consensusParams, &blockIndex, block, + blockReward); + BOOST_CHECK(shouldPark()); + } + } + { + // Block with sufficient miner fund + for (const Amount &amount : + {minerFund, minerFund + 1 * SATOSHI, blockReward}) { + CBlock block = + GetBlockWithMinerFund(chainparams, blockIndex.nHeight, amount); + MinerFundPolicy shouldPark(consensusParams, &blockIndex, block, + blockReward); + BOOST_CHECK(!shouldPark()); + } + } +} + +BOOST_AUTO_TEST_SUITE_END() diff --git a/src/validation.h b/src/validation.h --- a/src/validation.h +++ b/src/validation.h @@ -15,6 +15,7 @@ #include <attributes.h> #include <blockfileinfo.h> #include <blockindexcomparators.h> +#include <bloom.h> #include <chain.h> #include <consensus/amount.h> #include <consensus/consensus.h> @@ -683,6 +684,15 @@ const CBlockIndex *m_avalancheFinalizedBlockIndex GUARDED_BY(cs_avalancheFinalizedBlockIndex) = nullptr; + /** + * Filter to prevent parking a block due to block policies more than once. + * After first application of block policies, Avalanche voting will + * determine the final acceptance state. Rare false positives will be + * reconciled by the network and should not have any negative impact. + */ + CRollingBloomFilter m_filterParkingPoliciesApplied = + CRollingBloomFilter{1000, 0.000001}; + CBlockIndex const *m_best_fork_tip = nullptr; CBlockIndex const *m_best_fork_base = nullptr; @@ -849,7 +859,8 @@ EXCLUSIVE_LOCKS_REQUIRED(::cs_main); bool ConnectBlock(const CBlock &block, BlockValidationState &state, CBlockIndex *pindex, CCoinsViewCache &view, - BlockValidationOptions options, bool fJustCheck = false) + BlockValidationOptions options, + Amount *blockFees = nullptr, bool fJustCheck = false) EXCLUSIVE_LOCKS_REQUIRED(cs_main); // Apply the effects of a block disconnection on the UTXO set. @@ -970,7 +981,7 @@ CBlockIndex *pindexNew, const std::shared_ptr<const CBlock> &pblock, ConnectTrace &connectTrace, - DisconnectedBlockTransactions &disconnectpool) + DisconnectedBlockTransactions &disconnectpool, bool &parked) EXCLUSIVE_LOCKS_REQUIRED(cs_main, m_mempool->cs); void InvalidBlockFound(CBlockIndex *pindex, const BlockValidationState &state) diff --git a/src/validation.cpp b/src/validation.cpp --- a/src/validation.cpp +++ b/src/validation.cpp @@ -30,6 +30,7 @@ #include <node/coinstats.h> #include <node/ui_interface.h> #include <node/utxo_snapshot.h> +#include <policy/blockparking.h> #include <policy/mempool.h> #include <policy/policy.h> #include <policy/settings.h> @@ -1767,7 +1768,7 @@ bool CChainState::ConnectBlock(const CBlock &block, BlockValidationState &state, CBlockIndex *pindex, CCoinsViewCache &view, BlockValidationOptions options, - bool fJustCheck) { + Amount *blockFees, bool fJustCheck) { AssertLockHeld(cs_main); assert(pindex); @@ -2145,6 +2146,10 @@ "bad-cb-amount"); } + if (blockFees) { + *blockFees = nFees; + } + if (!CheckMinerFund(consensusParams, pindex->pprev, block.vtx[0]->vout, blockReward, true)) { return state.Invalid(BlockValidationResult::BLOCK_CONSENSUS, @@ -2599,7 +2604,8 @@ CBlockIndex *pindexNew, const std::shared_ptr<const CBlock> &pblock, ConnectTrace &connectTrace, - DisconnectedBlockTransactions &disconnectpool) { + DisconnectedBlockTransactions &disconnectpool, + bool &parked) { AssertLockHeld(cs_main); if (m_mempool) { AssertLockHeld(m_mempool->cs); @@ -2630,9 +2636,10 @@ LogPrint(BCLog::BENCH, " - Load block from disk: %.2fms [%.2fs]\n", (nTime2 - nTime1) * MILLI, nTimeReadFromDisk * MICRO); { + Amount blockFees{Amount::zero()}; CCoinsViewCache view(&CoinsTip()); bool rv = ConnectBlock(blockConnecting, state, pindexNew, view, - BlockValidationOptions(config)); + BlockValidationOptions(config), &blockFees); GetMainSignals().BlockChecked(blockConnecting, state); if (!rv) { if (state.IsInvalid()) { @@ -2644,6 +2651,36 @@ state.ToString()); } + // We check block parking policies before flushing changes to the UTXO + // set. This allows us to avoid rewinding everything immediately after. + // + // Only check block parking presets the first time the block is + // connected. Avalanche voting can override the parking decision made + // by these presets. + const BlockHash blockhash = pindexNew->GetBlockHash(); + if (!IsInitialBlockDownload() && + !m_filterParkingPoliciesApplied.contains(blockhash)) { + m_filterParkingPoliciesApplied.insert(blockhash); + + // The block is valid by consensus rules so now we check if the + // block passes all block preset rules. If not, then park the + // block. + const Amount blockReward = + blockFees + + GetBlockSubsidy(pindexNew->nHeight, consensusParams); + const auto parkingPolicies = GetParkingPoliciesForBlock( + consensusParams, pindexNew, blockConnecting, blockReward); + if (std::any_of(parkingPolicies.begin(), parkingPolicies.end(), + [&](const auto policy) { return (*policy)(); })) { + LogPrintf("Park block %s because it violated a block policy\n", + blockhash.ToString()); + pindexNew->nStatus = pindexNew->nStatus.withParked(); + m_blockman.m_dirty_blockindex.insert(pindexNew); + parked = true; + return false; + } + } + nTime3 = GetTimeMicros(); nTimeConnectTotal += nTime3 - nTime2; assert(nBlocksTotal > 0); @@ -2959,11 +2996,12 @@ // Connect new blocks. for (CBlockIndex *pindexConnect : reverse_iterate(vpindexToConnect)) { + bool parked{false}; if (!ConnectTip(config, state, pindexConnect, pindexConnect == pindexMostWork ? pblock : std::shared_ptr<const CBlock>(), - connectTrace, disconnectpool)) { + connectTrace, disconnectpool, parked)) { if (state.IsInvalid()) { // The block violates a consensus rule. if (state.GetResult() != @@ -2976,6 +3014,13 @@ break; } + if (parked) { + // The block was parked due to a policy violation. + fInvalidFound = true; + fContinue = false; + break; + } + // A system error occurred (disk space, database error, ...). // Make the mempool consistent with the current tip, just in // case any observers try to use it before shutdown. @@ -4407,7 +4452,7 @@ } if (!chainstate.ConnectBlock(block, state, &indexDummy, viewNew, - validationOptions, true)) { + validationOptions, nullptr, true)) { return false; } diff --git a/test/functional/abc_feature_minerfund.py b/test/functional/abc_feature_minerfund.py --- a/test/functional/abc_feature_minerfund.py +++ b/test/functional/abc_feature_minerfund.py @@ -13,6 +13,7 @@ from test_framework.txtools import pad_tx from test_framework.util import assert_equal, assert_greater_than_or_equal +WELLINGTON_ACTIVATION_TIME = 2100000600 MINER_FUND_RATIO = 8 MINER_FUND_ADDR = 'ecregtest:prfhcnyqnl5cgrnmlfmms675w93ld7mvvq9jcw0zsn' MINER_FUND_ADDR_AXION = 'ecregtest:pqnqv9lt7e5vjyp0w88zf2af0l92l8rxdgz0wv9ltl' @@ -21,26 +22,29 @@ class MinerFundTest(BitcoinTestFramework): def set_test_params(self): self.setup_clean_chain = True - self.num_nodes = 1 + self.num_nodes = 2 self.extra_args = [[ '-enableminerfund', + '-wellingtonactivationtime={}'.format(WELLINGTON_ACTIVATION_TIME), + ], [ + '-wellingtonactivationtime={}'.format(WELLINGTON_ACTIVATION_TIME), ]] def run_test(self): node = self.nodes[0] self.log.info('Create some history') - self.generate(node, 50, sync_fun=self.no_op) + self.generate(node, 10) - def get_best_coinbase(): - return node.getblock(node.getbestblockhash(), 2)['tx'][0] + def get_best_coinbase(n): + return n.getblock(n.getbestblockhash(), 2)['tx'][0] - coinbase = get_best_coinbase() + coinbase = get_best_coinbase(node) assert_greater_than_or_equal(len(coinbase['vout']), 2) block_reward = sum([vout['value'] for vout in coinbase['vout']]) def check_miner_fund_output(expected_address): - coinbase = get_best_coinbase() + coinbase = get_best_coinbase(node) assert_equal(len(coinbase['vout']), 2) assert_equal( coinbase['vout'][1]['scriptPubKey']['addresses'][0], @@ -113,6 +117,44 @@ node.submitblock(ToHex(good_block)) assert_equal(node.getbestblockhash(), good_block.hash) + # Move MTP forward to wellington activation. Next block will enforce + # new rules. + address = node.get_deterministic_priv_key().address + for n in self.nodes: + n.setmocktime(WELLINGTON_ACTIVATION_TIME) + self.generatetoaddress(node, nblocks=6, address=address) + assert_equal( + node.getblockchaininfo()['mediantime'], + WELLINGTON_ACTIVATION_TIME) + + # First block that does not have miner fund as a consensus requirement. + # node0 still mines a block with a coinbase output to the miner fund. + first_block_has_miner_fund = self.generatetoaddress( + node, nblocks=1, address=address)[0] + check_miner_fund_output(MINER_FUND_ADDR) + + # Invalidate it + for n in self.nodes: + n.invalidateblock(first_block_has_miner_fund) + + # node1 mines a block without a coinbase output to the miner fund. + first_block_no_miner_fund = self.generatetoaddress( + self.nodes[1], + nblocks=1, + address=address, + sync_fun=self.no_op)[0] + coinbase = get_best_coinbase(self.nodes[1]) + assert_equal(len(coinbase['vout']), 1) + + # node0 parks the block since the miner fund is enforced by policy. + def parked_block(blockhash): + for tip in node.getchaintips(): + if tip["hash"] == blockhash: + assert tip["status"] != "active" + return tip["status"] == "parked" + return False + self.wait_until(lambda: parked_block(first_block_no_miner_fund)) + if __name__ == '__main__': MinerFundTest().main()