Skip to content

Commit

Permalink
rpc: add checkblock
Browse files Browse the repository at this point in the history
  • Loading branch information
Sjors committed Dec 24, 2024
1 parent aeb532b commit 4eed4dd
Show file tree
Hide file tree
Showing 4 changed files with 215 additions and 0 deletions.
3 changes: 3 additions & 0 deletions src/rpc/client.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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"},
Expand Down
63 changes: 63 additions & 0 deletions src/rpc/mining.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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<CBlock> blockptr = std::make_shared<CBlock>();
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<uint32_t>();
}
if (options.exists("check_pow")) {
check_pow = options["check_pow"].get_bool();
}
}

auto sc = std::make_shared<submitblock_StateCatcher>(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[]{
Expand All @@ -1099,6 +1161,7 @@ void RegisterMiningRPCCommands(CRPCTable& t)
{"mining", &getblocktemplate},
{"mining", &submitblock},
{"mining", &submitheader},
{"mining", &checkblock},

{"hidden", &generatetoaddress},
{"hidden", &generatetodescriptor},
Expand Down
148 changes: 148 additions & 0 deletions test/functional/rpc_checkblock.py
Original file line number Diff line number Diff line change
@@ -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()
1 change: 1 addition & 0 deletions test/functional/test_runner.py
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down

0 comments on commit 4eed4dd

Please sign in to comment.