From 4eed4dda27a40b08606fdc426795b0c78aef9981 Mon Sep 17 00:00:00 2001 From: Sjors Provoost Date: Tue, 24 Dec 2024 09:41:58 +0100 Subject: [PATCH] rpc: add checkblock --- src/rpc/client.cpp | 3 + src/rpc/mining.cpp | 63 +++++++++++++ test/functional/rpc_checkblock.py | 148 ++++++++++++++++++++++++++++++ test/functional/test_runner.py | 1 + 4 files changed, 215 insertions(+) create mode 100755 test/functional/rpc_checkblock.py diff --git a/src/rpc/client.cpp b/src/rpc/client.cpp index 1b711e3c5b1500..04010be2c0a49b 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 3b05f84eee4f6e..a3a003f8a55d22 100644 --- a/src/rpc/mining.cpp +++ b/src/rpc/mining.cpp @@ -1089,6 +1089,68 @@ 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(); + } + } + + 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 +1161,7 @@ void RegisterMiningRPCCommands(CRPCTable& t) {"mining", &getblocktemplate}, {"mining", &submitblock}, {"mining", &submitheader}, + {"mining", &checkblock}, {"hidden", &generatetoaddress}, {"hidden", &generatetodescriptor}, diff --git a/test/functional/rpc_checkblock.py b/test/functional/rpc_checkblock.py new file mode 100755 index 00000000000000..7799c0ebc4f42d --- /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_runner.py b/test/functional/test_runner.py index 85170b10450256..bff6306004ea11 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',