diff --git a/src/minerfund.h b/src/minerfund.h --- a/src/minerfund.h +++ b/src/minerfund.h @@ -18,8 +18,18 @@ Amount GetMinerFundAmount(const Amount &coinbaseValue); +/** + * The miner fund whitelist to enforce during consensus checks. + */ std::vector<CTxDestination> GetMinerFundWhitelist(const Consensus::Params ¶ms, const CBlockIndex *pindexPrev); +/** + * The miner fund whitelist to enforce by avalanche policy. + */ +std::vector<CTxDestination> +GetMinerFundPolicyWhitelist(const Consensus::Params ¶ms, + const CBlockIndex *pindexPrev); + #endif // BITCOIN_MINERFUND_H diff --git a/src/minerfund.cpp b/src/minerfund.cpp --- a/src/minerfund.cpp +++ b/src/minerfund.cpp @@ -56,5 +56,27 @@ return {}; } + if (IsWellingtonEnabled(params, pindexPrev)) { + // Do not enforce the whitelist in consensus checks once + // Wellington has activated. + return {}; + } + + return {GetMinerFundDestination(!IsGluonEnabled(params, pindexPrev))}; +} + +std::vector<CTxDestination> +GetMinerFundPolicyWhitelist(const Consensus::Params ¶ms, + const CBlockIndex *pindexPrev) { + if (!gArgs.GetBoolArg("-enableminerfund", params.enableMinerFund)) { + return {}; + } + + if (!IsWellingtonEnabled(params, pindexPrev)) { + // Do not enforce the miner fund as an avalanche policy until + // Wellington has activated. + return {}; + } + return {GetMinerFundDestination(!IsGluonEnabled(params, pindexPrev))}; } diff --git a/src/node/miner.cpp b/src/node/miner.cpp --- a/src/node/miner.cpp +++ b/src/node/miner.cpp @@ -193,8 +193,12 @@ nFees + GetBlockSubsidy(nHeight, consensusParams); coinbaseTx.vin[0].scriptSig = CScript() << nHeight << OP_0; - const std::vector<CTxDestination> whitelisted = + std::vector<CTxDestination> whitelisted = GetMinerFundWhitelist(consensusParams, pindexPrev); + const std::vector<CTxDestination> policyWhitelisted = + GetMinerFundPolicyWhitelist(consensusParams, pindexPrev); + whitelisted.insert(whitelisted.end(), policyWhitelisted.begin(), + policyWhitelisted.end()); if (!whitelisted.empty()) { const Amount fund = GetMinerFundAmount(coinbaseTx.vout[0].nValue); coinbaseTx.vout[0].nValue -= fund; diff --git a/src/primitives/block.h b/src/primitives/block.h --- a/src/primitives/block.h +++ b/src/primitives/block.h @@ -59,6 +59,7 @@ // memory only mutable bool fChecked; + mutable bool fMinerFundPolicyChecked; CBlock() { SetNull(); } diff --git a/src/rpc/mining.cpp b/src/rpc/mining.cpp --- a/src/rpc/mining.cpp +++ b/src/rpc/mining.cpp @@ -1041,6 +1041,12 @@ EncodeDestination(fundDestination, config)); } + for (auto fundDestination : + GetMinerFundPolicyWhitelist(consensusParams, pindexPrev)) { + minerFundList.push_back( + EncodeDestination(fundDestination, config)); + } + int64_t minerFundMinValue = 0; if (IsAxionEnabled(consensusParams, pindexPrev)) { minerFundMinValue = diff --git a/src/validation.h b/src/validation.h --- a/src/validation.h +++ b/src/validation.h @@ -998,6 +998,8 @@ std::string ToString() EXCLUSIVE_LOCKS_REQUIRED(::cs_main); private: + bool ShouldParkFromBlockPolicies(const CBlock &block, CBlockIndex *pindex) + EXCLUSIVE_LOCKS_REQUIRED(cs_main); bool ActivateBestChainStep(const Config &config, BlockValidationState &state, CBlockIndex *pindexMostWork, diff --git a/src/validation.cpp b/src/validation.cpp --- a/src/validation.cpp +++ b/src/validation.cpp @@ -3027,6 +3027,90 @@ assert(!setBlockIndexCandidates.empty()); } +/** + * Apply block policies that will be voted on by Avalanche. This function + * should only bail early and not alter state if consensus failures are found. + * This ensures consensus checks have only one critical path in ConnectBlock(). + * + * Returns true if the block should be parked. + */ +bool CChainState::ShouldParkFromBlockPolicies(const CBlock &block, + CBlockIndex *pindex) { + AssertLockHeld(cs_main); + + // Only check each block policy once. Future attempts to connect this block + // may succeed despite these checks due to Avalanche voting results. + if (block.fMinerFundPolicyChecked) { + return false; + } + block.fMinerFundPolicyChecked = true; + + const Consensus::Params &consensusParams = m_params.GetConsensus(); + const std::vector<CTxDestination> policyWhitelist = + GetMinerFundPolicyWhitelist(consensusParams, pindex->pprev); + if (policyWhitelist.empty()) { + return false; + } + + // Add all outputs + CCoinsViewCache view(&CoinsTip()); + try { + for (const auto &ptx : block.vtx) { + AddCoins(view, *ptx, pindex->nHeight); + } + } catch (const std::logic_error &e) { + // Adding coins to the UTXO set failed, possibly due to a duplicate + // transaction which should not occur at this stage. Bail. + return false; + } + + // Sum fees, bailing early if any consensus failures are found. + Amount nFees = Amount::zero(); + for (const auto &ptx : block.vtx) { + const CTransaction &tx = *ptx; + const bool isCoinBase = tx.IsCoinBase(); + + Amount txfee = Amount::zero(); + TxValidationState tx_state; + if (!isCoinBase && !Consensus::CheckTxInputs(tx, tx_state, view, + pindex->nHeight, txfee)) { + // Transaction validation failed + return false; + } + nFees += txfee; + + if (!MoneyRange(nFees)) { + // Fees were out of range + return false; + } + } + + const Amount blockReward = + nFees + GetBlockSubsidy(pindex->nHeight, consensusParams); + const Amount required = GetMinerFundAmount(blockReward); + for (auto &o : block.vtx[0]->vout) { + if (o.nValue < required) { + // This output doesn't qualify because its amount is too low. + continue; + } + + CTxDestination address; + if (!ExtractDestination(o.scriptPubKey, address)) { + // Cannot decode address. + continue; + } + + if (std::find(policyWhitelist.begin(), policyWhitelist.end(), + address) != policyWhitelist.end()) { + // We found an output that matches the miner fund. + return false; + } + } + + // We did not find an output that match the miner fund requirements. + return true; +} + /** * Try to make some progress towards making pindexMostWork the active block. * pblock is either nullptr or a pointer to a CBlock corresponding to @@ -3089,10 +3173,41 @@ // Connect new blocks. for (CBlockIndex *pindexConnect : reverse_iterate(vpindexToConnect)) { - if (!ConnectTip(config, state, pindexConnect, - pindexConnect == pindexMostWork - ? pblock - : std::shared_ptr<const CBlock>(), + std::shared_ptr<const CBlock> pthisBlock = + pindexConnect == pindexMostWork + ? pblock + : std::shared_ptr<const CBlock>(); + // Read block from disk. + if (!pthisBlock) { + std::shared_ptr<CBlock> pblockNew = std::make_shared<CBlock>(); + if (!ReadBlockFromDisk(*pblockNew, pindexConnect, + m_params.GetConsensus())) { + // 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. + if (m_mempool) { + disconnectpool.updateMempoolForReorg(config, *this, + false, *m_mempool); + } + return AbortNode(state, "Failed to read block"); + } + pthisBlock = pblockNew; + } + + // Apply block policies before attempting to connect new chaintip. + if (ShouldParkFromBlockPolicies(*pthisBlock, pindexConnect)) { + LogPrintf("Park block %s because it violated a block policy\n", + pindexConnect->GetBlockHash().ToString()); + pindexConnect->nStatus = pindexConnect->nStatus.withParked(); + m_blockman.m_dirty_blockindex.insert(pindexConnect); + // TODO don't use fInvalidFound for parked blocks! But this + // works as a hack for now. + fInvalidFound = true; + fContinue = false; + break; + } + + if (!ConnectTip(config, state, pindexConnect, pthisBlock, connectTrace, disconnectpool)) { if (state.IsInvalid()) { // The block violates a consensus rule. 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 does not mine a block with 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()