diff --git a/qa/pull-tester/rpc-tests.py b/qa/pull-tester/rpc-tests.py index 8f4784836..373477a10 100755 --- a/qa/pull-tester/rpc-tests.py +++ b/qa/pull-tester/rpc-tests.py @@ -151,6 +151,7 @@ testScripts = [ # 'p2p-versionbits-warning.py', 'preciousblock.py', 'importprunedfunds.py', + 'createauxblock.py', 'signmessages.py', # 'nulldummy.py', 'import-rescan.py', diff --git a/qa/rpc-tests/createauxblock.py b/qa/rpc-tests/createauxblock.py new file mode 100644 index 000000000..2c82c85ef --- /dev/null +++ b/qa/rpc-tests/createauxblock.py @@ -0,0 +1,196 @@ +#!/usr/bin/env python +# Copyright (c) 2021 The Dogecoin Core Developers +# Distributed under the MIT software license, see the accompanying +# file COPYING or http://www.opensource.org/licenses/mit-license.php. +"""CreateAuxBlock QA test. + +# Tests createauxblock and submitauxblock RPC endpoints +""" + +from test_framework.test_framework import BitcoinTestFramework +from test_framework.util import * + +from test_framework import scrypt_auxpow as auxpow + +class CreateAuxBlockTest(BitcoinTestFramework): + + def __init__(self): + super().__init__() + self.setup_clean_chain = True + self.num_nodes = 2 + self.is_network_split = False + + def setup_network(self): + self.nodes = [] + self.nodes.append(start_node(0, self.options.tmpdir, ["-debug", "-txindex"])) + self.nodes.append(start_node(1, self.options.tmpdir, ["-debug"])) + connect_nodes_bi(self.nodes, 0, 1) + self.sync_all() + + def run_test(self): + # Generate an initial chain + self.nodes[0].generate(100) + self.sync_all() + # Generate a block so that we are not "downloading blocks". + self.nodes[1].generate(1) + self.sync_all() + + dummy_p2pkh_addr = "mmMP9oKFdADezYzduwJFcLNmmi8JHUKdx9" + dummy_p2sh_addr = "2Mwvgpd2H7wDPXx8jWe3Vqiciix6JqSbsyz" + + # Compare basic data of createauxblock to getblocktemplate. + auxblock = self.nodes[0].createauxblock(dummy_p2pkh_addr) + blocktemplate = self.nodes[0].getblocktemplate() + assert_equal(auxblock["coinbasevalue"], blocktemplate["coinbasevalue"]) + assert_equal(auxblock["bits"], blocktemplate["bits"]) + assert_equal(auxblock["height"], blocktemplate["height"]) + assert_equal(auxblock["previousblockhash"], blocktemplate["previousblockhash"]) + + # Compare target and take byte order into account. + target = auxblock["target"] + reversedTarget = auxpow.reverseHex(target) + assert_equal(reversedTarget, blocktemplate["target"]) + + # Verify data that can be found in another way. + assert_equal(auxblock["chainid"], 98) + assert_equal(auxblock["height"], self.nodes[0].getblockcount() + 1) + assert_equal(auxblock["previousblockhash"], self.nodes[0].getblockhash(auxblock["height"] - 1)) + + # Calling again should give the same block. + auxblock2 = self.nodes[0].createauxblock(dummy_p2pkh_addr) + assert_equal(auxblock2, auxblock) + + # Calling with an invalid address must fail + try: + auxblock2 = self.nodes[0].createauxblock("x") + raise AssertionError("invalid address accepted") + except JSONRPCException as exc: + assert_equal(exc.error["code"], -8) + + # Calling with a different address ... + dummy_addr2 = self.nodes[0].getnewaddress() + auxblock3 = self.nodes[0].createauxblock(dummy_addr2) + + # ... must give another block because the coinbase recipient differs ... + assert auxblock3["hash"] != auxblock["hash"] + + # ... but must have retained the same parameterization otherwise + assert_equal(auxblock["coinbasevalue"], auxblock3["coinbasevalue"]) + assert_equal(auxblock["bits"], auxblock3["bits"]) + assert_equal(auxblock["height"], auxblock3["height"]) + assert_equal(auxblock["previousblockhash"], auxblock3["previousblockhash"]) + assert_equal(auxblock["chainid"], auxblock3["chainid"]) + assert_equal(auxblock["target"], auxblock3["target"]) + + # If we receive a new block, the template cache must be emptied. + self.sync_all() + self.nodes[1].generate(1) + self.sync_all() + + auxblock4 = self.nodes[0].createauxblock(dummy_p2pkh_addr) + assert auxblock["hash"] != auxblock4["hash"] + try: + self.nodes[0].submitauxblock(auxblock["hash"], "x") + raise AssertionError("invalid block hash accepted") + except JSONRPCException as exc: + assert_equal(exc.error["code"], -8) + + # Invalid format for auxpow. + try: + self.nodes[0].submitauxblock(auxblock4["hash"], "x") + raise AssertionError("malformed auxpow accepted") + except JSONRPCException as exc: + assert_equal(exc.error["code"], -1) + + # Invalidate the block again, send a transaction and query for the + # auxblock to solve that contains the transaction. + self.nodes[0].generate(1) + addr = self.nodes[1].getnewaddress() + txid = self.nodes[0].sendtoaddress(addr, 1) + self.sync_all() + assert_equal(self.nodes[1].getrawmempool(), [txid]) + auxblock = self.nodes[0].createauxblock(dummy_p2pkh_addr) + reversedTarget = auxpow.reverseHex(auxblock["target"]) + + # Compute invalid auxpow. + apow = auxpow.computeAuxpowWithChainId(auxblock["hash"], reversedTarget, "98", False) + res = self.nodes[0].submitauxblock(auxblock["hash"], apow) + assert not res + + # Compute and submit valid auxpow. + apow = auxpow.computeAuxpowWithChainId(auxblock["hash"], reversedTarget, "98", True) + res = self.nodes[0].submitauxblock(auxblock["hash"], apow) + assert res + + # Make sure that the block is accepted. + self.sync_all() + assert_equal(self.nodes[1].getrawmempool(), []) + height = self.nodes[1].getblockcount() + assert_equal(height, auxblock["height"]) + assert_equal(self.nodes[1].getblockhash(height), auxblock["hash"]) + + # check the mined block and transaction + self.check_mined_block(auxblock, apow, dummy_p2pkh_addr, Decimal("500000"), txid) + + # Mine to a p2sh address while having multiple cached aux block templates + auxblock1 = self.nodes[0].createauxblock(dummy_p2pkh_addr) + auxblock2 = self.nodes[0].createauxblock(dummy_p2sh_addr) + auxblock3 = self.nodes[0].createauxblock(dummy_addr2) + reversedTarget = auxpow.reverseHex(auxblock2["target"]) + apow = auxpow.computeAuxpowWithChainId(auxblock2["hash"], reversedTarget, "98", True) + res = self.nodes[0].submitauxblock(auxblock2["hash"], apow) + assert res + + self.sync_all() + + # check the mined block + self.check_mined_block(auxblock2, apow, dummy_p2sh_addr, Decimal("500000")) + + # Solve the first p2pkh template before requesting a new auxblock + # this succeeds but creates a chaintip fork + reversedTarget = auxpow.reverseHex(auxblock1["target"]) + apow = auxpow.computeAuxpowWithChainId(auxblock1["hash"], reversedTarget, "98", True) + res = self.nodes[0].submitauxblock(auxblock1["hash"], apow) + assert res + + chaintips = self.nodes[0].getchaintips() + tipsFound = 0; + for ct in chaintips: + if ct["hash"] in [ auxblock1["hash"], auxblock2["hash"] ]: + tipsFound += 1 + assert_equal(tipsFound, 2) + + # Solve the last p2pkh template after requesting a new auxblock - this fails + self.nodes[0].createauxblock(dummy_p2pkh_addr) + reversedTarget = auxpow.reverseHex(auxblock3["target"]) + apow = auxpow.computeAuxpowWithChainId(auxblock3["hash"], reversedTarget, "98", True) + try: + self.nodes[0].submitauxblock(auxblock3["hash"], apow) + raise AssertionError("Outdated blockhash accepted") + except JSONRPCException as exc: + assert_equal(exc.error["code"], -8) + + def check_mined_block(self, auxblock, apow, addr, min_value, txid=None): + # Call getblock and verify the auxpow field. + data = self.nodes[1].getblock(auxblock["hash"]) + assert "auxpow" in data + auxJson = data["auxpow"] + assert_equal(auxJson["index"], 0) + assert_equal(auxJson["parentblock"], apow[-160:]) + + # Call getrawtransaction and verify the coinbase tx + coinbasetx = self.nodes[0].getrawtransaction(data["tx"][0], True) + + assert coinbasetx["vout"][0]["value"] >= min_value + assert_equal(coinbasetx["vout"][0]["scriptPubKey"]["addresses"][0], addr) + + # Make sure the coinbase contains the block height + coinbase = coinbasetx["vin"][0]["coinbase"] + assert_equal("01%02x01" % auxblock["height"], coinbase[0:6]) + + # Make sure our transaction got mined, if any + if not txid is None: + assert txid in data["tx"] + +if __name__ == "__main__": + CreateAuxBlockTest().main() diff --git a/src/rpc/mining.cpp b/src/rpc/mining.cpp index 8e9f7820d..4e52734a9 100644 --- a/src/rpc/mining.cpp +++ b/src/rpc/mining.cpp @@ -49,7 +49,6 @@ UniValue GetNetworkHashPS(int lookup, int height) { // If lookup is -1, then use blocks since last difficulty change. if (lookup <= 0) lookup = pb->nHeight % Params().GetConsensus(pb->nHeight).DifficultyAdjustmentInterval() + 1; - // // If lookup is larger than chain, then set it to chain length. if (lookup > pb->nHeight) @@ -244,7 +243,7 @@ UniValue generatetoaddress(const JSONRPCRequest& request) CBitcoinAddress address(request.params[1].get_str()); if (!address.IsValid()) throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Error: Invalid address"); - + boost::shared_ptr coinbaseScript(new CReserveScript()); coinbaseScript->reserveScript = GetScriptForDestination(address.Get()); @@ -969,6 +968,158 @@ UniValue estimatesmartpriority(const JSONRPCRequest& request) /* ************************************************************************** */ /* Merge mining. */ +/** + * The variables below are used to keep track of created and not yet + * submitted auxpow blocks. Lock them to be sure even for multiple + * RPC threads running in parallel. + */ + +static CCriticalSection cs_auxblockCache; +static std::map mapNewBlock; +static std::vector> vNewBlockTemplate; + +void AuxMiningCheck() +{ + if(!g_connman) + throw JSONRPCError(RPC_CLIENT_P2P_DISABLED, "Error: Peer-to-peer functionality missing or disabled"); + + if (g_connman->GetNodeCount(CConnman::CONNECTIONS_ALL) == 0 && !Params().MineBlocksOnDemand()) + throw JSONRPCError(RPC_CLIENT_NOT_CONNECTED, "Dogecoin is not connected!"); + + if (IsInitialBlockDownload() && !Params().MineBlocksOnDemand()) + throw JSONRPCError(RPC_CLIENT_IN_INITIAL_DOWNLOAD, + "Dogecoin is downloading blocks..."); + + /* This should never fail, since the chain is already + past the point of merge-mining start. Check nevertheless. */ + { + LOCK(cs_main); + if (Params().GetConsensus(chainActive.Height() + 1).fAllowLegacyBlocks) + throw std::runtime_error("getauxblock method is not yet available"); + } +} + +static UniValue AuxMiningCreateBlock(const CScript& scriptPubKey) +{ + + AuxMiningCheck(); + LOCK(cs_auxblockCache); + + static unsigned nTransactionsUpdatedLast; + static const CBlockIndex* pindexPrev = nullptr; + static uint64_t nStart; + static std::map curBlocks; + static unsigned nExtraNonce = 0; + + // Dogecoin: Never mine witness tx + const bool fMineWitnessTx = false; + + /* Search for cached blocks with given scriptPubKey and assign it to pBlock + * if we find a match. This allows for creating multiple aux templates with + * a single dogecoind instance, for example when a pool runs multiple sub- + * pools with different payout strategies. + */ + CBlock* pblock = nullptr; + CScriptID scriptID (scriptPubKey); + auto iter = curBlocks.find(scriptID); + if (iter != curBlocks.end()) pblock = iter->second; + + { + LOCK(cs_main); + + // Update block + if (pblock == nullptr || pindexPrev != chainActive.Tip() + || (mempool.GetTransactionsUpdated() != nTransactionsUpdatedLast + && GetTime() - nStart > 60)) + { + if (pindexPrev != chainActive.Tip()) + { + // Clear old blocks since they're obsolete now. + mapNewBlock.clear(); + vNewBlockTemplate.clear(); + curBlocks.clear(); + pblock = nullptr; + } + + // Create new block with nonce = 0 and extraNonce = 1 + std::unique_ptr newBlock + = BlockAssembler(Params()).CreateNewBlock(scriptPubKey, fMineWitnessTx); + if (!newBlock) + throw JSONRPCError(RPC_OUT_OF_MEMORY, "out of memory"); + + // Update state only when CreateNewBlock succeeded + nTransactionsUpdatedLast = mempool.GetTransactionsUpdated(); + pindexPrev = chainActive.Tip(); + nStart = GetTime(); + + // Finalise it by setting the version and building the merkle root + IncrementExtraNonce(&newBlock->block, pindexPrev, nExtraNonce); + newBlock->block.SetAuxpowFlag(true); + + // Save + pblock = &newBlock->block; + curBlocks[scriptID] = pblock; + mapNewBlock[pblock->GetHash()] = pblock; + vNewBlockTemplate.push_back(std::move(newBlock)); + } + } + + // At this point, pblock is always initialised: If we make it here + // without creating a new block above, it means that, in particular, + // pindexPrev == chainActive.Tip(). But for that to happen, we must + // already have created a pblock in a previous call, as pindexPrev is + // initialised only when pblock is. + assert(pblock); + + arith_uint256 target; + bool fNegative, fOverflow; + target.SetCompact(pblock->nBits, &fNegative, &fOverflow); + if (fNegative || fOverflow || target == 0) + throw std::runtime_error("invalid difficulty bits in block"); + + UniValue result(UniValue::VOBJ); + result.pushKV("hash", pblock->GetHash().GetHex()); + result.pushKV("chainid", pblock->GetChainId()); + result.pushKV("previousblockhash", pblock->hashPrevBlock.GetHex()); + result.pushKV("coinbasevalue", (int64_t)pblock->vtx[0]->vout[0].nValue); + result.pushKV("bits", strprintf("%08x", pblock->nBits)); + result.pushKV("height", static_cast (pindexPrev->nHeight + 1)); + result.pushKV("target", HexStr(BEGIN(target), END(target))); + + return result; +} + +static bool AuxMiningSubmitBlock(const std::string& hashHex, const std::string& auxpowHex) +{ + + AuxMiningCheck(); + LOCK(cs_auxblockCache); + + uint256 hash; + hash.SetHex(hashHex); + + const std::map::iterator mit = mapNewBlock.find(hash); + if (mit == mapNewBlock.end()) + throw JSONRPCError(RPC_INVALID_PARAMETER, "block hash unknown"); + CBlock& block = *mit->second; + + const std::vector vchAuxPow = ParseHex(auxpowHex); + CDataStream ss(vchAuxPow, SER_GETHASH, PROTOCOL_VERSION); + CAuxPow pow; + ss >> pow; + block.SetAuxpow(new CAuxPow(pow)); + assert(block.GetHash() == hash); + + submitblock_StateCatcher sc(block.GetHash()); + RegisterValidationInterface(&sc); + std::shared_ptr shared_block + = std::make_shared(block); + bool fAccepted = ProcessNewBlock(Params(), shared_block, true, nullptr); + UnregisterValidationInterface(&sc); + + return fAccepted; +} + UniValue getauxblockbip22(const JSONRPCRequest& request) { if (request.fHelp @@ -1011,31 +1162,8 @@ UniValue getauxblockbip22(const JSONRPCRequest& request) if (!coinbaseScript->reserveScript.size()) throw JSONRPCError(RPC_INTERNAL_ERROR, "No coinbase script available (mining requires a wallet)"); - if(!g_connman) - throw JSONRPCError(RPC_CLIENT_P2P_DISABLED, "Error: Peer-to-peer functionality missing or disabled"); - - if (g_connman->GetNodeCount(CConnman::CONNECTIONS_ALL) == 0 && !Params().MineBlocksOnDemand()) - throw JSONRPCError(RPC_CLIENT_NOT_CONNECTED, "Dogecoin is not connected!"); - - if (IsInitialBlockDownload() && !Params().MineBlocksOnDemand()) - throw JSONRPCError(RPC_CLIENT_IN_INITIAL_DOWNLOAD, - "Dogecoin is downloading blocks..."); - - /* This should never fail, since the chain is already - past the point of merge-mining start. Check nevertheless. */ - { - LOCK(cs_main); - if (Params().GetConsensus(chainActive.Height() + 1).fAllowLegacyBlocks) - throw std::runtime_error("getauxblock method is not yet available"); - } - - /* The variables below are used to keep track of created and not yet - submitted auxpow blocks. Lock them to be sure even for multiple - RPC threads running in parallel. */ - static CCriticalSection cs_auxblockCache; + AuxMiningCheck(); LOCK(cs_auxblockCache); - static std::map mapNewBlock; - static std::vector> vNewBlockTemplate; /* Create a new block? */ if (request.params.size() == 0) @@ -1050,38 +1178,38 @@ UniValue getauxblockbip22(const JSONRPCRequest& request) // Dogecoin: Never mine witness tx const bool fMineWitnessTx = false; { - LOCK(cs_main); - if (pindexPrev != chainActive.Tip() - || (mempool.GetTransactionsUpdated() != nTransactionsUpdatedLast - && GetTime() - nStart > 60)) - { - if (pindexPrev != chainActive.Tip()) + LOCK(cs_main); + if (pindexPrev != chainActive.Tip() + || (mempool.GetTransactionsUpdated() != nTransactionsUpdatedLast + && GetTime() - nStart > 60)) { - // Clear old blocks since they're obsolete now. - mapNewBlock.clear(); - vNewBlockTemplate.clear(); - pblock = nullptr; + if (pindexPrev != chainActive.Tip()) + { + // Clear old blocks since they're obsolete now. + mapNewBlock.clear(); + vNewBlockTemplate.clear(); + pblock = nullptr; + } + + // Create new block with nonce = 0 and extraNonce = 1 + std::unique_ptr newBlock(BlockAssembler(Params()).CreateNewBlock(coinbaseScript->reserveScript, fMineWitnessTx)); + if (!newBlock) + throw JSONRPCError(RPC_OUT_OF_MEMORY, "out of memory"); + + // Update state only when CreateNewBlock succeeded + nTransactionsUpdatedLast = mempool.GetTransactionsUpdated(); + pindexPrev = chainActive.Tip(); + nStart = GetTime(); + + // Finalise it by setting the version and building the merkle root + IncrementExtraNonce(&newBlock->block, pindexPrev, nExtraNonce); + newBlock->block.SetAuxpowFlag(true); + + // Save + pblock = &newBlock->block; + mapNewBlock[pblock->GetHash()] = pblock; + vNewBlockTemplate.push_back(std::move(newBlock)); } - - // Create new block with nonce = 0 and extraNonce = 1 - std::unique_ptr newBlock(BlockAssembler(Params()).CreateNewBlock(coinbaseScript->reserveScript, fMineWitnessTx)); - if (!newBlock) - throw JSONRPCError(RPC_OUT_OF_MEMORY, "out of memory"); - - // Update state only when CreateNewBlock succeeded - nTransactionsUpdatedLast = mempool.GetTransactionsUpdated(); - pindexPrev = chainActive.Tip(); - nStart = GetTime(); - - // Finalise it by setting the version and building the merkle root - IncrementExtraNonce(&newBlock->block, pindexPrev, nExtraNonce); - newBlock->block.SetAuxpowFlag(true); - - // Save - pblock = &newBlock->block; - mapNewBlock[pblock->GetHash()] = pblock; - vNewBlockTemplate.push_back(std::move(newBlock)); - } } arith_uint256 target; @@ -1135,6 +1263,59 @@ UniValue getauxblockbip22(const JSONRPCRequest& request) return BIP22ValidationResult(sc.state); } +UniValue createauxblock(const JSONRPCRequest& request) +{ + if (request.fHelp || request.params.size() != 1) + throw std::runtime_error( + "createauxblock
\n" + "\ncreate a new block and return information required to merge-mine it.\n" + "\nArguments:\n" + "1. address (string, required) specify coinbase transaction payout address\n" + "\nResult:\n" + "{\n" + " \"hash\" (string) hash of the created block\n" + " \"chainid\" (numeric) chain ID for this block\n" + " \"previousblockhash\" (string) hash of the previous block\n" + " \"coinbasevalue\" (numeric) value of the block's coinbase\n" + " \"bits\" (string) compressed target of the block\n" + " \"height\" (numeric) height of the block\n" + " \"target\" (string) target in reversed byte order\n" + "}\n" + "\nExamples:\n" + + HelpExampleCli("createauxblock", "\"address\"") + + HelpExampleRpc("createauxblock", "\"address\"") + ); + + // Check coinbase payout address + CBitcoinAddress coinbaseAddress(request.params[0].get_str()); + + if (!coinbaseAddress.IsValid()) + throw JSONRPCError(RPC_INVALID_PARAMETER,"Invalid coinbase payout address"); + + const CScript scriptPubKey = GetScriptForDestination(coinbaseAddress.Get()); + return AuxMiningCreateBlock(scriptPubKey); +} + +UniValue submitauxblock(const JSONRPCRequest& request) +{ + if (request.fHelp || request.params.size() != 2) + throw std::runtime_error( + "submitauxblock \n" + "\nsubmit a solved auxpow for a previously block created by 'createauxblock'.\n" + "\nArguments:\n" + "1. hash (string, required) hash of the block to submit\n" + "2. auxpow (string, required) serialised auxpow found\n" + "\nResult:\n" + "xxxxx (boolean) whether the submitted block was correct\n" + "\nExamples:\n" + + HelpExampleCli("submitauxblock", "\"hash\" \"serialised auxpow\"") + + HelpExampleRpc("submitauxblock", "\"hash\" \"serialised auxpow\"") + ); + + return AuxMiningSubmitBlock(request.params[0].get_str(), + request.params[1].get_str()); +} + UniValue getauxblock(const JSONRPCRequest& request) { const UniValue response = getauxblockbip22(request); @@ -1157,7 +1338,10 @@ static const CRPCCommand commands[] = { "mining", "prioritisetransaction", &prioritisetransaction, true, {"txid","priority_delta","fee_delta"} }, { "mining", "getblocktemplate", &getblocktemplate, true, {"template_request"} }, { "mining", "submitblock", &submitblock, true, {"hexdata","parameters"} }, + { "mining", "getauxblock", &getauxblock, true, {"hash", "auxpow"} }, + { "mining", "createauxblock", &createauxblock, true, {"address"} }, + { "mining", "submitauxblock", &submitauxblock, true, {"hash", "auxpow"} }, { "generating", "generate", &generate, true, {"nblocks","maxtries","auxpow"} }, { "generating", "generatetoaddress", &generatetoaddress, true, {"nblocks","address","maxtries","auxpow"} },