diff --git a/src/pow.cpp b/src/pow.cpp index bbcf39b5932ed..05cb3c2589919 100644 --- a/src/pow.cpp +++ b/src/pow.cpp @@ -161,3 +161,19 @@ bool CheckProofOfWorkImpl(uint256 hash, unsigned int nBits, const Consensus::Par return true; } + +bool CheckWeakProofOfWork(uint256 hash, unsigned int nBits, unsigned int multiplier, const Consensus::Params& params) +{ + bool fNegative; + bool fOverflow; + arith_uint256 bnTarget; + bnTarget.SetCompact(nBits, &fNegative, &fOverflow); + bnTarget = bnTarget * multiplier; + // Check range + if (fNegative || bnTarget == 0 || fOverflow || bnTarget > UintToArith256(params.powLimit) * multiplier) + return false; + // Check proof of work matches claimed amount + if (UintToArith256(hash) > bnTarget) + return false; + return true; +} \ No newline at end of file diff --git a/src/pow.h b/src/pow.h index 2b28ade273c5c..43b8f37fd6135 100644 --- a/src/pow.h +++ b/src/pow.h @@ -20,6 +20,8 @@ unsigned int CalculateNextWorkRequired(const CBlockIndex* pindexLast, int64_t nF /** Check whether a block hash satisfies the proof-of-work requirement specified by nBits */ bool CheckProofOfWork(uint256 hash, unsigned int nBits, const Consensus::Params&); bool CheckProofOfWorkImpl(uint256 hash, unsigned int nBits, const Consensus::Params&); +/** Check whether a block hash satisfies the (weaker) proof-of-work requirement specified by nBits and multiplier */ +bool CheckWeakProofOfWork(uint256 hash, unsigned int nBits, unsigned int multiplier, const Consensus::Params& params); /** * Return false if the proof-of-work requirement specified by new_nbits at a diff --git a/src/rpc/client.cpp b/src/rpc/client.cpp index 1b711e3c5b150..04010be2c0a49 100644 --- a/src/rpc/client.cpp +++ b/src/rpc/client.cpp @@ -133,6 +133,9 @@ static const CRPCConvertParam vRPCConvertParams[] = { "submitpackage", 0, "package" }, { "submitpackage", 1, "maxfeerate" }, { "submitpackage", 2, "maxburnamount" }, + { "checkblock", 1, "options" }, + { "checkblock", 1, "check_pow"}, + { "checkblock", 1, "multiplier"}, { "combinerawtransaction", 0, "txs" }, { "fundrawtransaction", 1, "options" }, { "fundrawtransaction", 1, "add_inputs"}, diff --git a/src/rpc/mining.cpp b/src/rpc/mining.cpp index 3b05f84eee4f6..8f20f2fabc703 100644 --- a/src/rpc/mining.cpp +++ b/src/rpc/mining.cpp @@ -1089,6 +1089,72 @@ static RPCHelpMan submitheader() }; } +static RPCHelpMan checkblock() +{ + return RPCHelpMan{"checkblock", + "\nChecks a new block without submitting to the network.\n", + { + {"hexdata", RPCArg::Type::STR_HEX, RPCArg::Optional::NO, "the hex-encoded block data to submit"}, + {"options", RPCArg::Type::OBJ_NAMED_PARAMS, RPCArg::Optional::OMITTED, "", + { + {"check_pow", RPCArg::Type::BOOL, RPCArg::Default{true}, "verify the proof-of-work. The nBits value is always checked."}, + {"multiplier", RPCArg::Type::NUM, RPCArg::Default{1}, "Check against a higher target. The nBits value is not used, but still needs to match the consensus value."}, + }, + } + }, + { + RPCResult{"If the block passed all checks", RPCResult::Type::NONE, "", ""}, + RPCResult{"Otherwise", RPCResult::Type::STR, "", "According to BIP22"}, + }, + RPCExamples{ + HelpExampleCli("checkblock", "\"mydata\"") + + HelpExampleRpc("checkblock", "\"mydata\"") + }, + [&](const RPCHelpMan& self, const JSONRPCRequest& request) -> UniValue +{ + std::shared_ptr blockptr = std::make_shared(); + CBlock& block = *blockptr; + if (!DecodeHexBlk(block, request.params[0].get_str())) { + throw JSONRPCError(RPC_DESERIALIZATION_ERROR, "Block decode failed"); + } + + ChainstateManager& chainman = EnsureAnyChainman(request.context); + + unsigned int multiplier{1}; + bool check_pow{true}; + + if (!request.params[1].isNull()) { + UniValue options = request.params[1]; + // RPCTypeCheckObj(options, + // { + // {"multiplier", UniValueType(UniValue::VNUM)}, + // } + // ); + + if (options.exists("multiplier")) { + multiplier = options["multiplier"].getInt(); + } + if (options.exists("check_pow")) { + check_pow = options["check_pow"].get_bool(); + } + } + + // TODO: + // - check that our tip is below BIP34_IMPLIES_BIP30_LIMIT - 1 + // - check we're above BIP34 / BIP68 height + + auto sc = std::make_shared(block.GetHash()); + CHECK_NONFATAL(chainman.m_options.signals)->RegisterSharedValidationInterface(sc); + // TODO: add checkNewBlock() method to chainman interface + chainman.CheckNewBlock(blockptr, check_pow, /*multiplier=*/multiplier); + CHECK_NONFATAL(chainman.m_options.signals)->UnregisterSharedValidationInterface(sc); + // TODO uncomment when that works + // CHECK_NONFATAL(!sc->found); + return BIP22ValidationResult(sc->state); +}, + }; +} + void RegisterMiningRPCCommands(CRPCTable& t) { static const CRPCCommand commands[]{ @@ -1099,6 +1165,7 @@ void RegisterMiningRPCCommands(CRPCTable& t) {"mining", &getblocktemplate}, {"mining", &submitblock}, {"mining", &submitheader}, + {"mining", &checkblock}, {"hidden", &generatetoaddress}, {"hidden", &generatetodescriptor}, diff --git a/src/validation.cpp b/src/validation.cpp index 3f774fd0a1e63..05c276d316ecc 100644 --- a/src/validation.cpp +++ b/src/validation.cpp @@ -4582,6 +4582,205 @@ bool ChainstateManager::AcceptBlock(const std::shared_ptr& pblock, return true; } +void ChainstateManager::CheckNewBlock(const std::shared_ptr& block, const bool check_pow, const unsigned int multiplier) +{ + AssertLockNotHeld(cs_main); + + { + // TODO: cleanup the ret mess + bool ret{true}; + BlockValidationState state; + + // CheckBlock() does not support multi-threaded block validation because CBlock::fChecked can cause data race. + // Therefore, the following critical section must include the CheckBlock() call as well. + LOCK(cs_main); + + CBlockIndex* tip = ActiveTip(); + + if (block->hashPrevBlock != *tip->phashBlock) { + state.Invalid(BlockValidationResult::BLOCK_INVALID_HEADER, "inconclusive-not-best-prevblk", "block not on tip"); + ret = false; + } + + // Check the actual proof-of-work. The nBits value is checked by + // ContextualCheckBlock below later as part of AcceptBlock. + const CChainParams& params{GetParams()}; + if (ret && check_pow) { + ret = CheckWeakProofOfWork(block->GetHash(), block->nBits, multiplier, params.GetConsensus()); + if (!ret) { + if (multiplier == 1) { + state.Invalid(BlockValidationResult::BLOCK_INVALID_HEADER, "high-hash", "proof of work failed"); + } else { + state.Invalid(BlockValidationResult::BLOCK_INVALID_HEADER, "high-weak-hash", "weak proof of work failed"); + } + } + } + if (ret) { + ret = CheckBlock(*block, state, GetConsensus(), /*fCheckPow=*/false); + } + if (ret) { + /** + * At this point ProcessNewBlock would call AcceptBlock(), but we + * don't want to store the block or its header. Run individual checks + * instead. + * + * - don't run AcceptBlockHeader(): it doesn't check anything we care about + * - we already ran CheckBlock() + * - do run ContextualCheckBlockHeader() + * - do run ContextualCheckBlock() + */ + + if (!ContextualCheckBlockHeader(*block, state, ActiveChainstate().m_blockman, ActiveChainstate().m_chainman, tip)) { + ret = false; + } + + if (ret && !ContextualCheckBlock(*block, state, *this, tip)) { + ret = false; + } + + if (ret) { + // Run a subset of ConnectBlock() to check the transactions, without updating + // the UTXO set. Does update validation caches. + + // Skip BIP34-implies-BIP30 check + // TODO: only on mainnet (disallow testnet3 altogether??) + Assert(tip->nHeight < 1983702 - 1); // BIP34_IMPLIES_BIP30_LIMIT + // TODO: assert height is above BIP34 and BIP68 activation + + CBlockIndex index{*block}; + uint256 block_hash(block->GetHash()); + index.pprev = tip; + index.nHeight = tip->nHeight + 1; + index.phashBlock = &block_hash; + + const bool parallel_script_checks{ActiveChainstate().m_chainman.GetCheckQueue().HasThreads()}; + + int nLockTimeFlags = LOCKTIME_VERIFY_SEQUENCE; + + // Get the script flags for this block + unsigned int flags{GetBlockScriptFlags(index, ActiveChainstate().m_chainman)}; + + // We don't want to update the actual chainstate, so create + // a cache on top of it. + CCoinsViewCache tipView(&ActiveChainstate().CoinsTip()); + CCoinsView blockCoins; + CCoinsViewCache view(&blockCoins); + view.SetBackend(tipView); + + // verify that the view's current state corresponds to the previous block + Assume(index.pprev->GetBlockHash() == view.GetBestBlock()); + + // Precomputed transaction data pointers must not be invalidated + // until after `control` has run the script checks (potentially + // in multiple threads). Preallocate the vector size so a new allocation + // doesn't invalidate pointers into the vector, and keep txsdata in scope + // for as long as `control`. + CCheckQueueControl control(parallel_script_checks ? &ActiveChainstate().m_chainman.GetCheckQueue() : nullptr); + std::vector txsdata(block->vtx.size()); + + std::vector prevheights; + CAmount nFees = 0; + int64_t nSigOpsCost = 0; + for (unsigned int i = 0; i < block->vtx.size(); i++) + { + if (!state.IsValid()) break; + const CTransaction &tx = *(block->vtx[i]); + + if (!tx.IsCoinBase()) + { + CAmount txfee = 0; + TxValidationState tx_state; + if (!Consensus::CheckTxInputs(tx, tx_state, view, index.nHeight, txfee)) { + // Any transaction validation failure is a block consensus failure + state.Invalid(BlockValidationResult::BLOCK_CONSENSUS, + tx_state.GetRejectReason(), + tx_state.GetDebugMessage() + " in transaction " + tx.GetHash().ToString()); + break; + } + nFees += txfee; + if (!MoneyRange(nFees)) { + state.Invalid(BlockValidationResult::BLOCK_CONSENSUS, "bad-txns-accumulated-fee-outofrange", + "accumulated fee in the block out of range"); + break; + } + + // Check that transaction is BIP68 final + // BIP68 lock checks (as opposed to nLockTime checks) must + // be in ConnectBlock because they require the UTXO set + prevheights.resize(tx.vin.size()); + for (size_t j = 0; j < tx.vin.size(); j++) { + prevheights[j] = view.AccessCoin(tx.vin[j].prevout).nHeight; + } + + if (!SequenceLocks(tx, nLockTimeFlags, prevheights, index)) { + state.Invalid(BlockValidationResult::BLOCK_CONSENSUS, "bad-txns-nonfinal", + "contains a non-BIP68-final transaction " + tx.GetHash().ToString()); + break; + } + } + + // GetTransactionSigOpCost counts 3 types of sigops: + // * legacy (always) + // * p2sh (when P2SH enabled in flags and excludes coinbase) + // * witness (when witness enabled in flags and excludes coinbase) + nSigOpsCost += GetTransactionSigOpCost(tx, view, flags); + if (nSigOpsCost > MAX_BLOCK_SIGOPS_COST) { + state.Invalid(BlockValidationResult::BLOCK_CONSENSUS, "bad-blk-sigops", "too many sigops"); + break; + } + + if (!tx.IsCoinBase()) + { + std::vector vChecks; + TxValidationState tx_state; + // cacheSigStore and cacheFullScriptStore are set to true + // because the cache needs to be retained for when this + // block is eventually submitted. + if (!CheckInputScripts(tx, tx_state, view, flags, /*cacheSigStore=*/true, /*cacheFullScriptStore=*/true, txsdata[i], ActiveChainstate().m_chainman.m_validation_cache, parallel_script_checks ? &vChecks : nullptr)) { + // Any transaction validation failure in ConnectBlock is a block consensus failure + state.Invalid(BlockValidationResult::BLOCK_CONSENSUS, + tx_state.GetRejectReason(), tx_state.GetDebugMessage()); + break; + } + control.Add(std::move(vChecks)); + } + + CTxUndo undoDummy; + UpdateCoins(tx, view, undoDummy, index.nHeight); + + } + + CAmount blockReward = nFees + GetBlockSubsidy(index.nHeight, params.GetConsensus()); + if (block->vtx[0]->GetValueOut() > blockReward && state.IsValid()) { + state.Invalid(BlockValidationResult::BLOCK_CONSENSUS, "bad-cb-amount", + strprintf("coinbase pays too much (actual=%d vs limit=%d)", block->vtx[0]->GetValueOut(), blockReward)); + } + + auto parallel_result = control.Complete(); + if (parallel_result.has_value() && state.IsValid()) { + state.Invalid(BlockValidationResult::BLOCK_CONSENSUS, strprintf("mandatory-script-verify-flag-failed (%s)", ScriptErrorString(parallel_result->first)), parallel_result->second); + } + + if (!state.IsValid()) { + ret = false; + } + + // TODO: (optionally) loop through the block again and add useful transactions to the mempool? + + } + + } + if (!ret) { + if (m_options.signals) { + m_options.signals->BlockChecked(*block, state); + } + return; + } + } + + return; +} + bool ChainstateManager::ProcessNewBlock(const std::shared_ptr& block, bool force_processing, bool min_pow_checked, bool* new_block) { AssertLockNotHeld(cs_main); diff --git a/src/validation.h b/src/validation.h index e2ff5925c5860..3f8afea9d75c0 100644 --- a/src/validation.h +++ b/src/validation.h @@ -1182,6 +1182,35 @@ class ChainstateManager FlatFilePos* dbp = nullptr, std::multimap* blocks_with_unknown_parent = nullptr); + /** + * Verify a block. Optionally skips proof-of-work check or use nBits multiplier. + * + * TODO: avoid this convoluted mess? + * + * If you want to *possibly* get feedback on whether block is valid, you must + * install a CValidationInterface (see validationinterface.h) - this will have + * its BlockChecked method called whenever *any* block completes validation. + * + * Note that we guarantee that either the proof-of-work is valid on block, or + * (and possibly also) BlockChecked will have been called. + * + * May not be called in a validationinterface callback. + * + * TODO: + * - option to add good transactions to the mempool + * - option to jail non-standard transactions: + * https://delvingbitcoin.org/t/second-look-at-weak-blocks/805 + * - optionally skip PoW check + * - optionally provide higher target + * + * @param[in] block The block we want to process. + * @param[in] check_pow Perform proof-of-work check, nbits in the header + * is always checked. + * @param[in] multiplier nBits multiplier, does not apply to the nbits + * header value check. + */ + void CheckNewBlock(const std::shared_ptr& block, const bool check_pow = true, const unsigned int multiplier = 1) LOCKS_EXCLUDED(cs_main); + /** * Process an incoming block. This only returns after the best known valid * block is made active. Note that it does not, however, guarantee that the diff --git a/test/functional/rpc_checkblock.py b/test/functional/rpc_checkblock.py new file mode 100755 index 0000000000000..7799c0ebc4f42 --- /dev/null +++ b/test/functional/rpc_checkblock.py @@ -0,0 +1,148 @@ +#!/usr/bin/env python3 +# Copyright (c) 2024 The Bitcoin Core developers +# Distributed under the MIT software license, see the accompanying +# file COPYING or http://www.opensource.org/licenses/mit-license.php. +"""Test checkblock RPC + +Generate several (weak) blocks and test them against the checkblock RPC. +Mainly focused on skipping the proof-of-work check. See rpc_blockchain.py +for tests of other conditions that make a block invalid. +""" + +import copy + +from test_framework.blocktools import ( + create_block, + create_coinbase, + add_witness_commitment +) + +from test_framework.test_framework import BitcoinTestFramework +from test_framework.util import ( + assert_equal, +) + +from test_framework.messages import ( + COutPoint, + CTxIn, +) + +from test_framework.wallet import ( + MiniWallet, +) + +class CheckBlockTest(BitcoinTestFramework): + + def set_test_params(self): + # self.setup_clean_chain = True + self.num_nodes = 1 + + def run_test(self): + block_0_hash = self.nodes[0].getbestblockhash() + block_0_height = self.nodes[0].getblockcount() + self.generate(self.nodes[0], sync_fun=self.no_op, nblocks=1) + block_1 = self.nodes[0].getblock(self.nodes[0].getbestblockhash()) + block_2 = create_block(int(block_1['hash'], 16), create_coinbase(block_0_height + 2), block_1['mediantime'] + 1) + + # Block must build on the current tip + prev_hash_before = block_2.hashPrevBlock + block_2.hashPrevBlock = int(block_0_hash, 16) + block_2.solve() + + assert_equal(self.nodes[0].checkblock(block_2.serialize().hex()), "inconclusive-not-best-prevblk") + + # Restore prevhash + block_2.hashPrevBlock = prev_hash_before + + self.log.info("Lowering nBits should make the block invalid") + nbits_before = block_2.nBits + block_2.nBits = block_2.nBits - 1 + block_2.solve() + + assert_equal(self.nodes[0].checkblock(block_2.serialize().hex()), "bad-diffbits") + + # Restore nbits + block_2.nBits = nbits_before + + self.log.info("A weak block won't pass the check by default") + block_2.solve(multiplier=6) + assert_equal(self.nodes[0].checkblock(block_2.serialize().hex()), "high-hash") + + # The above solve found a nonce between 128x and 256x the target. + self.log.info("Checking against a multiplier of 3 should fail.") + assert_equal(self.nodes[0].checkblock(block_2.serialize().hex(), {'multiplier': 3}), "high-weak-hash") + + self.log.info("A multiplier of 6 should work") + assert_equal(self.nodes[0].checkblock(block_2.serialize().hex(), {'multiplier': 6}), None) + + self.log.info("Skip the PoW check altogether") + assert_equal(self.nodes[0].checkblock(block_2.serialize().hex(), {'check_pow': False}), None) + + self.log.info("Add normal proof of work") + block_2.solve() + assert_equal(self.nodes[0].checkblock(block_2.serialize().hex()), None) + + self.log.info("checkblock does not submit the block") + assert_equal(self.nodes[0].getblockcount(), block_0_height + 1) + + self.log.info("Submitting this block should succeed") + assert_equal(self.nodes[0].submitblock(block_2.serialize().hex()), None) + self.nodes[0].waitforblockheight(2) + + self.log.info("Generate a transaction") + tx = MiniWallet(self.nodes[0]).create_self_transfer() + block_3 = create_block(int(block_2.hash, 16), create_coinbase(block_0_height + 3), block_1['mediantime'] + 1, txlist=[tx['hex']]) + assert_equal(len(block_3.vtx), 2) + add_witness_commitment(block_3) + block_3.solve() + assert_equal(self.nodes[0].checkblock(block_3.serialize().hex()), None) + # Call again to ensure the UTXO set wasn't updated + assert_equal(self.nodes[0].checkblock(block_3.serialize().hex()), None) + + self.log.info("Add an invalid transaction") + nvalue_before = tx['tx'].vout[0].nValue + tx['tx'].vout[0].nValue = 10000000000 + bad_tx_hex = tx['tx'].serialize().hex() + assert_equal(self.nodes[0].testmempoolaccept([bad_tx_hex])[0]['reject-reason'], 'bad-txns-in-belowout') + block_3 = create_block(int(block_2.hash, 16), create_coinbase(block_0_height + 3), block_1['mediantime'] + 1, txlist=[bad_tx_hex]) + assert_equal(len(block_3.vtx), 2) + add_witness_commitment(block_3) + block_3.solve() + + self.log.info("This can't be submitted") + assert_equal(self.nodes[0].submitblock(block_3.serialize().hex()), 'bad-txns-in-belowout') + + self.log.info("And should also not pass checkbock") + assert_equal(self.nodes[0].checkblock(block_3.serialize().hex()), 'bad-txns-in-belowout') + + tx['tx'].vout[0].nValue = nvalue_before + + self.log.info("Can't spend coins out of thin air") + tx_vin_0_before = tx['tx'].vin[0] + tx['tx'].vin[0] = CTxIn(outpoint=COutPoint(hash=int('aa' * 32, 16), n=0), scriptSig=b"") + bad_tx_hex = tx['tx'].serialize().hex() + assert_equal(self.nodes[0].testmempoolaccept([bad_tx_hex])[0]['reject-reason'], 'missing-inputs') + block_3 = create_block(int(block_2.hash, 16), create_coinbase(block_0_height + 3), block_1['mediantime'] + 1, txlist=[bad_tx_hex]) + assert_equal(len(block_3.vtx), 2) + add_witness_commitment(block_3) + block_3.solve() + assert_equal(self.nodes[0].checkblock(block_3.serialize().hex()), 'bad-txns-inputs-missingorspent') + tx['tx'].vin[0] = tx_vin_0_before + + self.log.info("Can't spend coins twice") + tx_hex = tx['tx'].serialize().hex() + tx_2 = copy.deepcopy(tx) + tx_2_hex = tx_2['tx'].serialize().hex() + # Nothing wrong with these transactions individually + assert_equal(self.nodes[0].testmempoolaccept([tx_hex])[0]['allowed'], True) + assert_equal(self.nodes[0].testmempoolaccept([tx_2_hex])[0]['allowed'], True) + # But can't be combined + assert_equal(self.nodes[0].testmempoolaccept([tx_hex, tx_2_hex])[0]['package-error'], "package-contains-duplicates") + block_3 = create_block(int(block_2.hash, 16), create_coinbase(block_0_height + 3), block_1['mediantime'] + 1, txlist=[tx_hex, tx_2_hex]) + assert_equal(len(block_3.vtx), 3) + add_witness_commitment(block_3) + block_3.solve() + assert_equal(self.nodes[0].checkblock(block_3.serialize().hex()), 'bad-txns-inputs-missingorspent') + +if __name__ == '__main__': + CheckBlockTest(__file__).main() diff --git a/test/functional/test_framework/messages.py b/test/functional/test_framework/messages.py index b4a5d9d5efa69..6fdc84f615f40 100755 --- a/test/functional/test_framework/messages.py +++ b/test/functional/test_framework/messages.py @@ -814,11 +814,13 @@ def is_valid(self): if self.calc_merkle_root() != self.hashMerkleRoot: return False return True - - def solve(self): + + def solve(self, multiplier = 1): + # If a multiplier is provided, ensure work is between + # multiplier * target and multiplier / 2 * target self.rehash() target = uint256_from_compact(self.nBits) - while self.sha256 > target: + while self.sha256 > target * multiplier or (multiplier != 1 and self.sha256 <= target * multiplier / 4): self.nNonce += 1 self.rehash() diff --git a/test/functional/test_runner.py b/test/functional/test_runner.py index 85170b1045025..bff6306004ea1 100755 --- a/test/functional/test_runner.py +++ b/test/functional/test_runner.py @@ -243,6 +243,7 @@ 'rpc_decodescript.py', 'rpc_blockchain.py --v1transport', 'rpc_blockchain.py --v2transport', + 'rpc_checkblock.py', 'rpc_deprecated.py', 'wallet_disable.py', 'wallet_change_address.py --legacy-wallet',