diff --git a/src/core_io.h b/src/core_io.h index 30f7f5976c..6a84ea5af1 100644 --- a/src/core_io.h +++ b/src/core_io.h @@ -12,6 +12,7 @@ #include class CBlock; +class CBlockHeader; class CScript; class CTransaction; struct CMutableTransaction; @@ -26,6 +27,7 @@ CScript ParseScript(const std::string& s); std::string ScriptToAsmStr(const CScript& script, const bool fAttemptSighashDecode = false); [[nodiscard]] bool DecodeHexTx(CMutableTransaction& tx, const std::string& strHexTx); [[nodiscard]] bool DecodeHexBlk(CBlock&, const std::string& strHexBlk); +bool DecodeHexBlockHeader(CBlockHeader&, const std::string& hex_header); /** * Parse a hex string into 256 bits * @param[in] strHex a hex-formatted, 64-character string diff --git a/src/core_read.cpp b/src/core_read.cpp index 867e026d2d..9debaa40dd 100644 --- a/src/core_read.cpp +++ b/src/core_read.cpp @@ -109,6 +109,20 @@ bool DecodeHexTx(CMutableTransaction& tx, const std::string& strHexTx) return true; } +bool DecodeHexBlockHeader(CBlockHeader& header, const std::string& hex_header) +{ + if (!IsHex(hex_header)) return false; + + const std::vector header_data{ParseHex(hex_header)}; + CDataStream ser_header(header_data, SER_NETWORK, PROTOCOL_VERSION); + try { + ser_header >> header; + } catch (const std::exception&) { + return false; + } + return true; +} + bool DecodeHexBlk(CBlock& block, const std::string& strHexBlk) { if (!IsHex(strHexBlk)) diff --git a/src/rpc/mining.cpp b/src/rpc/mining.cpp index 759c370601..4c31135c54 100644 --- a/src/rpc/mining.cpp +++ b/src/rpc/mining.cpp @@ -789,6 +789,42 @@ static UniValue submitblock(const JSONRPCRequest& request) return BIP22ValidationResult(sc.state); } +static UniValue submitheader(const JSONRPCRequest& request) +{ + if (request.fHelp || request.params.size() != 1) { + throw std::runtime_error( + "submitheader \"hexdata\"\n" + "\nDecode the given hexdata as a header and submit it as a candidate chain tip if valid." + "\nThrows when the header is invalid.\n" + "\nArguments\n" + "1. \"hexdata\" (string, required) the hex-encoded block header data\n" + "\nResult:\n" + "None" + "\nExamples:\n" + + HelpExampleCli("submitheader", "\"aabbcc\"") + + HelpExampleRpc("submitheader", "\"aabbcc\"")); + } + + CBlockHeader h; + if (!DecodeHexBlockHeader(h, request.params[0].get_str())) { + throw JSONRPCError(RPC_DESERIALIZATION_ERROR, "Block header decode failed"); + } + { + LOCK(cs_main); + if (!LookupBlockIndex(h.hashPrevBlock)) { + throw JSONRPCError(RPC_VERIFY_ERROR, "Must submit previous header (" + h.hashPrevBlock.GetHex() + ") first"); + } + } + + CValidationState state; + ProcessNewBlockHeaders({h}, state, Params(), /* ppindex */ nullptr, /* first_invalid */ nullptr); + if (state.IsValid()) return NullUniValue; + if (state.IsError()) { + throw JSONRPCError(RPC_VERIFY_ERROR, FormatStateMessage(state)); + } + throw JSONRPCError(RPC_VERIFY_ERROR, state.GetRejectReason()); +} + static UniValue estimatefee(const JSONRPCRequest& request) { throw JSONRPCError(RPC_METHOD_DEPRECATED, "estimatefee was removed in v0.17.\n" @@ -964,6 +1000,7 @@ static const CRPCCommand commands[] = { "mining", "prioritisetransaction", &prioritisetransaction, {"txid","fee_delta"} }, { "mining", "getblocktemplate", &getblocktemplate, {"template_request"} }, { "mining", "submitblock", &submitblock, {"hexdata","dummy"} }, + { "mining", "submitheader", &submitheader, {"hexdata"} }, #if ENABLE_MINER { "generating", "generatetoaddress", &generatetoaddress, {"nblocks","address","maxtries"} }, diff --git a/test/functional/mining_basic.py b/test/functional/mining_basic.py index bd783bf9ff..45bc061dc2 100755 --- a/test/functional/mining_basic.py +++ b/test/functional/mining_basic.py @@ -9,16 +9,23 @@ - submitblock""" import copy -from binascii import b2a_hex from decimal import Decimal from test_framework.blocktools import create_coinbase -from test_framework.messages import CBlock +from test_framework.messages import ( + CBlock, + CBlockHeader, +) +from test_framework.mininode import ( + P2PDataStore, +) from test_framework.test_framework import BitcoinTestFramework -from test_framework.util import assert_equal, assert_raises_rpc_error +from test_framework.util import ( + assert_equal, + assert_raises_rpc_error, + bytes_to_hex_str as b2x, +) -def b2x(b): - return b2a_hex(b).decode('ascii') def assert_template(node, block, expect, rehash=True): if rehash: @@ -131,5 +138,77 @@ class MiningTest(BitcoinTestFramework): bad_block.hashPrevBlock = 123 assert_template(node, bad_block, 'inconclusive-not-best-prevblk') + self.log.info('submitheader tests') + assert_raises_rpc_error(-22, 'Block header decode failed', lambda: node.submitheader(hexdata='xx' * 80)) + assert_raises_rpc_error(-22, 'Block header decode failed', lambda: node.submitheader(hexdata='ff' * 78)) + assert_raises_rpc_error(-25, 'Must submit previous header', lambda: node.submitheader(hexdata='ff' * 80)) + + block.solve() + + def filter_tip_keys(chaintips): + """ + Dash chaintips rpc returns extra info in each tip (difficulty, chainwork, and + forkpoint). Filter down to relevant ones checked in this test. + """ + check_keys = ["hash", "height", "branchlen", "status"] + filtered_tips = [] + for tip in chaintips: + filtered_tips.append({k: tip[k] for k in check_keys}) + return filtered_tips + + def chain_tip(b_hash, *, status='headers-only', branchlen=1): + return {'hash': b_hash, 'height': 202, 'branchlen': branchlen, 'status': status} + assert chain_tip(block.hash) not in filter_tip_keys(node.getchaintips()) + node.submitheader(hexdata=b2x(block.serialize())) + assert chain_tip(block.hash) in filter_tip_keys(node.getchaintips()) + node.submitheader(hexdata=b2x(CBlockHeader(block).serialize())) # Noop + assert chain_tip(block.hash) in filter_tip_keys(node.getchaintips()) + + bad_block_root = copy.deepcopy(block) + bad_block_root.hashMerkleRoot += 2 + bad_block_root.solve() + assert chain_tip(bad_block_root.hash) not in filter_tip_keys(node.getchaintips()) + node.submitheader(hexdata=b2x(CBlockHeader(bad_block_root).serialize())) + assert chain_tip(bad_block_root.hash) in filter_tip_keys(node.getchaintips()) + # Should still reject invalid blocks, even if we have the header: + assert_equal(node.submitblock(hexdata=b2x(bad_block_root.serialize())), 'invalid') + assert chain_tip(bad_block_root.hash) in filter_tip_keys(node.getchaintips()) + # We know the header for this invalid block, so should just return early without error: + node.submitheader(hexdata=b2x(CBlockHeader(bad_block_root).serialize())) + assert chain_tip(bad_block_root.hash) in filter_tip_keys(node.getchaintips()) + + bad_block_lock = copy.deepcopy(block) + bad_block_lock.vtx[0].nLockTime = 2**32 - 1 + bad_block_lock.vtx[0].rehash() + bad_block_lock.hashMerkleRoot = bad_block_lock.calc_merkle_root() + bad_block_lock.solve() + assert_equal(node.submitblock(hexdata=b2x(bad_block_lock.serialize())), 'invalid') + # Build a "good" block on top of the submitted bad block + bad_block2 = copy.deepcopy(block) + bad_block2.hashPrevBlock = bad_block_lock.sha256 + bad_block2.solve() + assert_raises_rpc_error(-25, 'bad-prevblk', lambda: node.submitheader(hexdata=b2x(CBlockHeader(bad_block2).serialize()))) + + # Should reject invalid header right away + bad_block_time = copy.deepcopy(block) + bad_block_time.nTime = 1 + bad_block_time.solve() + assert_raises_rpc_error(-25, 'time-too-old', lambda: node.submitheader(hexdata=b2x(CBlockHeader(bad_block_time).serialize()))) + + # Should ask for the block from a p2p node, if they announce the header as well: + node.add_p2p_connection(P2PDataStore()) + node.p2p.wait_for_getheaders(timeout=5) # Drop the first getheaders + node.p2p.send_blocks_and_test(blocks=[block], node=node) + # Must be active now: + assert chain_tip(block.hash, status='active', branchlen=0) in filter_tip_keys(node.getchaintips()) + + # Building a few blocks should give the same results + node.generate(10) + assert_raises_rpc_error(-25, 'time-too-old', lambda: node.submitheader(hexdata=b2x(CBlockHeader(bad_block_time).serialize()))) + assert_raises_rpc_error(-25, 'bad-prevblk', lambda: node.submitheader(hexdata=b2x(CBlockHeader(bad_block2).serialize()))) + node.submitheader(hexdata=b2x(CBlockHeader(block).serialize())) + node.submitheader(hexdata=b2x(CBlockHeader(bad_block_root).serialize())) + + if __name__ == '__main__': MiningTest().main()