diff --git a/src/init.cpp b/src/init.cpp --- a/src/init.cpp +++ b/src/init.cpp @@ -229,6 +229,7 @@ // Destroy various global instances g_avalanche.reset(); + g_block_template_manager.reset(); node.connman.reset(); node.banman.reset(); g_txindex.reset(); @@ -2778,6 +2779,9 @@ return false; } + // Step 12.5 (I guess ?): Initialize Block Template Manager + g_block_template_manager = std::make_unique(); + // Step 13: finished SetRPCWarmupFinished(); diff --git a/src/miner.h b/src/miner.h --- a/src/miner.h +++ b/src/miner.h @@ -233,4 +233,30 @@ unsigned int &nExtraNonce); int64_t UpdateTime(CBlockHeader *pblock, const CChainParams &chainParams, const CBlockIndex *pindexPrev); + +using BlockTemplateId = uint64_t; + +/** + * BlockTemplateManager stores block templates during the mining process + * to reduce cross-talk between the node and mining software. + */ +class BlockTemplateManager { + /* + * Currently, we only store the most recently generated block template as a + * proof of concept. This will be replaced with an id-addressable container + * in the future. + */ + std::shared_ptr blockTemplate; + +public: + BlockTemplateId + addBlockTemplate(std::shared_ptr templateIn); + std::shared_ptr getBlockTemplate(BlockTemplateId id); +}; + +/** + * Global block template manager instance. + */ +extern std::unique_ptr g_block_template_manager; + #endif // BITCOIN_MINER_H diff --git a/src/miner.cpp b/src/miner.cpp --- a/src/miner.cpp +++ b/src/miner.cpp @@ -29,6 +29,8 @@ #include #include +std::unique_ptr g_block_template_manager; + int64_t UpdateTime(CBlockHeader *pblock, const CChainParams &chainParams, const CBlockIndex *pindexPrev) { int64_t nOldTime = pblock->nTime; @@ -567,3 +569,15 @@ pblock->vtx[0] = MakeTransactionRef(std::move(txCoinbase)); pblock->hashMerkleRoot = BlockMerkleRoot(*pblock); } + +BlockTemplateId BlockTemplateManager::addBlockTemplate( + std::shared_ptr templateIn) { + blockTemplate = templateIn; + return 0; +} + +std::shared_ptr +BlockTemplateManager::getBlockTemplate(BlockTemplateId id) { + // Just return the latest template for now + return blockTemplate; +} diff --git a/src/rpc/mining.cpp b/src/rpc/mining.cpp --- a/src/rpc/mining.cpp +++ b/src/rpc/mining.cpp @@ -9,6 +9,7 @@ #include #include #include +#include #include #include #include @@ -584,7 +585,7 @@ // Update block static CBlockIndex *pindexPrev; static int64_t nStart; - static std::unique_ptr pblocktemplate; + static std::shared_ptr pblocktemplate; if (pindexPrev != ::ChainActive().Tip() || (g_mempool.GetTransactionsUpdated() != nTransactionsUpdatedLast && GetTime() - nStart > 5)) { @@ -604,6 +605,7 @@ if (!pblocktemplate) { throw JSONRPCError(RPC_OUT_OF_MEMORY, "Out of memory"); } + g_block_template_manager->addBlockTemplate(pblocktemplate); // Need to update only after we know CreateNewBlock succeeded pindexPrev = pindexPrevNew; @@ -772,6 +774,126 @@ return BIP22ValidationResult(config, sc.state); } +static UniValue submitheader(const Config &config, CBlockHeader &h); + +static UniValue submitblocksolution(const Config &config, + const JSONRPCRequest &request) { + RPCHelpMan{ + "submitblocksolution", + "\nSubmit a candidate block to the network based on a previously\n" + "generated template. The submitted solution contains only the fields\n" + "modified during the mining process and leaves the contents of the\n" + "block up to the node software.\n" + "WARNING: THIS RPC IS EXPERIMENTAL AND CURRENTLY IN DEVELOPMENT.\n" + "IT MAY BE INCOMPLETE OR NOT WORK AT ALL. USE AT YOUR OWN RISK!\"", + { + {"block_solution", + RPCArg::Type::OBJ, + RPCArg::Optional::NO, + "A json object in the following spec", + { + {"template", + RPCArg::Type::OBJ, + RPCArg::Optional::NO, + "Same template object received from 'getblocktemplate'", + { + {"template_id", RPCArg::Type::NUM, RPCArg::Optional::NO, + "The template ID"}, + }, + "\"template\""}, + {"header", + RPCArg::Type::OBJ, + RPCArg::Optional::NO, + "The mutable header fields", + { + {"coinbase", RPCArg::Type::STR_HEX, RPCArg::Optional::NO, + "The final coinbase transaction, including any " + "extranonce if used"}, + {"nonce", RPCArg::Type::NUM, RPCArg::Optional::NO, + "The nonce"}, + {"time", RPCArg::Type::NUM, RPCArg::Optional::OMITTED, + "The block time, if modified"}, + {"version", RPCArg::Type::NUM, RPCArg::Optional::OMITTED, + "The version, if modified"}, + }, + "\"template\""}, + }, + "\"block_solution\""}, + }, + RPCResult{"None"}, + RPCExamples{ + HelpExampleCli( + "submitblocksolution", + "'{\"template\":{\"template_id\":123},\"header\":{...}}'") + + HelpExampleRpc( + "submitblocksolution", + "{\"template\":{\"template_id\":123},\"header\":{...}}")}, + } + .Check(request); + + const UniValue &solution = request.params[0].get_obj(); + const UniValue &templateObj = find_value(solution, "template"); + if (!templateObj.isObject()) { + throw JSONRPCError(RPC_INVALID_PARAMETER, + "Block solution has invalid template object"); + } + + const UniValue &templateIdObj = find_value(templateObj, "template_id"); + BlockTemplateId templateId; + if (templateIdObj.isNum()) { + templateId = BlockTemplateId(templateIdObj.get_int64()); + } else { + throw JSONRPCError(RPC_INVALID_PARAMETER, + "Template ID is expected to be a number"); + } + + // Temporary until the block manager supports getting templates by ID + if (templateId) { + throw JSONRPCError(RPC_INVALID_PARAMETER, + "Template IDs other that '0' are not yet supported"); + } + + const UniValue &headerObj = find_value(solution, "header"); + if (!headerObj.isObject()) { + throw JSONRPCError(RPC_INVALID_PARAMETER, + "Block solution has invalid header object"); + } + + const UniValue &nonceObj = find_value(headerObj, "nonce"); + uint32_t nonce; + if (nonceObj.isNum()) { + nonce = nonceObj.get_int(); + } else { + throw JSONRPCError(RPC_INVALID_PARAMETER, + "Nonce is expected to be a number"); + } + + const UniValue &coinbaseObj = find_value(headerObj, "coinbase"); + CMutableTransaction coinbaseTx; + if (coinbaseObj.isStr()) { + std::string coinbaseTxStr = coinbaseObj.get_str(); + if (!DecodeHexTx(coinbaseTx, coinbaseTxStr)) { + throw JSONRPCError(RPC_DESERIALIZATION_ERROR, + "Coinbase decode failed"); + } + } else { + throw JSONRPCError( + RPC_INVALID_PARAMETER, + "Block solution header expects coinbase to be a hex string"); + } + + std::shared_ptr blockTemplate = + g_block_template_manager->getBlockTemplate(templateId); + CBlock block = blockTemplate->block; + block.nNonce = nonce; + block.vtx[0] = MakeTransactionRef(std::move(coinbaseTx)); + block.hashMerkleRoot = BlockMerkleRoot(block); + + // First submit the header + return submitheader(config, block); + // TODO: Submit the entire block +} + static UniValue submitheader(const Config &config, const JSONRPCRequest &request) { RPCHelpMan{ @@ -794,6 +916,10 @@ throw JSONRPCError(RPC_DESERIALIZATION_ERROR, "Block header decode failed"); } + return submitheader(config, h); +} + +static UniValue submitheader(const Config &config, CBlockHeader &h) { { LOCK(cs_main); if (!LookupBlockIndex(h.hashPrevBlock)) { @@ -838,6 +964,7 @@ {"mining", "prioritisetransaction", prioritisetransaction, {"txid", "dummy", "fee_delta"}}, {"mining", "getblocktemplate", getblocktemplate, {"template_request"}}, {"mining", "submitblock", submitblock, {"hexdata", "dummy"}}, + {"mining", "submitblocksolution", submitblocksolution, {"block_solution"}}, {"mining", "submitheader", submitheader, {"hexdata"}}, {"generating", "generatetoaddress", generatetoaddress, {"nblocks", "address", "maxtries"}}, diff --git a/test/functional/abc_mining_basic.py b/test/functional/abc_mining_basic.py new file mode 100755 --- /dev/null +++ b/test/functional/abc_mining_basic.py @@ -0,0 +1,113 @@ +#!/usr/bin/env python3 +# Copyright (c) 2020 The Bitcoin developers +# Distributed under the MIT software license, see the accompanying +# file COPYING or http://www.opensource.org/licenses/mit-license.php. +""" +Tests for Bitcoin ABC mining RPCs +""" + + +from test_framework.blocktools import ( + create_coinbase, + TIME_GENESIS_BLOCK, +) +from test_framework.messages import ( + CBlock, +) +from test_framework.test_framework import BitcoinTestFramework +from test_framework.util import ( + assert_equal, + assert_raises_rpc_error, + connect_nodes, +) + + +class AbcMiningBasicTest(BitcoinTestFramework): + def set_test_params(self): + self.num_nodes = 2 + self.setup_clean_chain = True + + def mine_chain(self): + self.log.info('Create some old blocks') + node = self.nodes[0] + address = node.get_deterministic_priv_key().address + for t in range(TIME_GENESIS_BLOCK, + TIME_GENESIS_BLOCK + 200 * 600, 600): + node.setmocktime(t) + node.generatetoaddress(1, address) + mining_info = node.getmininginfo() + assert_equal(mining_info['blocks'], 200) + assert_equal(mining_info['currentblocktx'], 0) + self.restart_node(0) + connect_nodes(self.nodes[0], self.nodes[1]) + + def run_test(self): + self.mine_chain() + node = self.nodes[0] + + # Mine a block to leave initial block download + node.generatetoaddress(1, node.get_deterministic_priv_key().address) + + # Get a block template and solve a block from it + blockTemplate = node.getblocktemplate() + + next_height = int(blockTemplate["height"]) + coinbase_tx = create_coinbase(height=next_height) + coinbase_tx.rehash() + + block = CBlock() + block.nVersion = blockTemplate["version"] + block.hashPrevBlock = int(blockTemplate["previousblockhash"], 16) + block.nTime = blockTemplate["curtime"] + block.nBits = int(blockTemplate["bits"], 16) + block.nNonce = 0 + block.vtx = [coinbase_tx] + block.hashMerkleRoot = block.calc_merkle_root() + block.solve() + + def submitblocksolution(coinbase, nonce, templateId=0): + return node.submitblocksolution({ + "template": { + "template_id": templateId, + }, + "header": { + "coinbase": coinbase, + "nonce": nonce, + } + }) + + # Submit some invalid solutions + assert_raises_rpc_error(-8, "Block solution has invalid template object", + lambda: node.submitblocksolution({})) + assert_raises_rpc_error(-8, "Block solution has invalid header object", + lambda: node.submitblocksolution({ + "template": { + "template_id": 0, + }, + })) + + assert_raises_rpc_error(-8, "Template IDs other that '0' are not yet supported", + lambda: submitblocksolution(coinbase_tx.serialize().hex(), block.nNonce, templateId=1)) + assert_raises_rpc_error(-8, "Template ID is expected to be a number", + lambda: submitblocksolution(coinbase_tx.serialize().hex(), block.nNonce, templateId="invalid")) + + assert_raises_rpc_error(-8, "Block solution header expects coinbase to be a hex string", + lambda: submitblocksolution(0xdeadbeef, block.nNonce)) + assert_raises_rpc_error(-22, "Coinbase decode failed", + lambda: submitblocksolution("invalid coinbase", block.nNonce)) + + assert_raises_rpc_error(-8, "Nonce is expected to be a number", + lambda: submitblocksolution(coinbase_tx.serialize().hex(), "invalid nonce")) + + # Valid block solution + submitblocksolution(coinbase_tx.serialize().hex(), block.nNonce) + + def chain_tip(blockHash, *, height, status='headers-only', branchlen=1): + return {'hash': blockHash, 'height': height, + 'branchlen': branchlen, 'status': status} + + assert chain_tip(block.hash, height=202) in node.getchaintips() + + +if __name__ == '__main__': + AbcMiningBasicTest().main()