diff --git a/src/Makefile.am b/src/Makefile.am index e1fee74e0..2e12b6457 100644 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -130,6 +130,7 @@ BITCOIN_CORE_H = \ protocol.h \ random.h \ reverselock.h \ + rpc/auxcache.h \ rpc/blockchain.h \ rpc/client.h \ rpc/mining.h \ @@ -209,6 +210,7 @@ libdogecoin_server_a_SOURCES = \ policy/policy.cpp \ pow.cpp \ rest.cpp \ + rpc/auxcache.cpp \ rpc/auxpow.cpp \ rpc/blockchain.cpp \ rpc/mining.cpp \ diff --git a/src/Makefile.test.include b/src/Makefile.test.include index d45a595e8..e9fca4393 100644 --- a/src/Makefile.test.include +++ b/src/Makefile.test.include @@ -82,6 +82,7 @@ BITCOIN_TESTS =\ test/addrman_tests.cpp \ test/amount_tests.cpp \ test/allocator_tests.cpp \ + test/auxcache_tests.cpp \ test/auxpow_tests.cpp \ test/base32_tests.cpp \ test/base58_tests.cpp \ diff --git a/src/rpc/auxcache.cpp b/src/rpc/auxcache.cpp new file mode 100644 index 000000000..b410395d8 --- /dev/null +++ b/src/rpc/auxcache.cpp @@ -0,0 +1,135 @@ +// Copyright (c) 2025 The Dogecoin Core developers +// Distributed under the MIT software license, see the accompanying +// file COPYING or http://www.opensource.org/licenses/mit-license.php. + +#include "rpc/auxcache.h" + +#include "script/standard.h" // for CScriptID +#include "primitives/block.h" // for CBlock +#include "uint256.h" // for uint256 +#include "utilmemory.h" // for MakeUnique +#include "utiltime.h" // for GetTimeMicros + +#include +#include + +#include // for uint64_t +#include // for std::unique_ptr, std::shared_ptr +#include // for std::mutex +#include // for std::tuple + +namespace { + +//! Type that is stored in the cache +struct AuxCacheItem { + int64_t cache_time; + CScriptID scriptId; + uint256 hash; + std::shared_ptr pblock; +}; + +// The ByScriptId index sorts by CScriptID and item age +struct ByScriptId {}; +using ByScriptIdView = std::tuple; +struct ByScriptIdExtractor +{ + using result_type = ByScriptIdView; + result_type operator()(const AuxCacheItem& item) const + { + // calculate age + const int64_t age = GetMockableTimeMicros() - item.cache_time; + return ByScriptIdView{item.scriptId, age}; + } +}; + +// The ByBlockHash index sorts by blockhash (uint256) +struct ByBlockHash {}; +struct ByBlockHashExtractor +{ + using result_type = uint256; + result_type operator()(const AuxCacheItem& item) const + { + return item.hash; + } +}; + +/** Data type for the main data structure (AuxCacheItem objects with ByScriptID/ByBlockHash indexes). */ +using AuxCacheIndex = boost::multi_index_container< + AuxCacheItem, + boost::multi_index::indexed_by< + boost::multi_index::ordered_non_unique, ByScriptIdExtractor>, + boost::multi_index::ordered_unique, ByBlockHashExtractor> + > +>; + +} //anon namespace + +class CAuxBlockCache::Impl { + AuxCacheIndex m_index; + +private: + std::mutex mut; + +public: + Impl() : m_index(boost::make_tuple( + boost::make_tuple(ByScriptIdExtractor(), std::less()), + boost::make_tuple(ByBlockHashExtractor(), std::less()) + )) {}; + + Impl(const Impl&) = delete; + Impl& operator=(const Impl&) = delete; + + bool Add(const CScriptID scriptId, std::shared_ptr pblock) { + + uint256 hash = pblock->GetHash(); + int64_t micros = GetMockableTimeMicros(); + + std::lock_guard guard(mut); + auto ret = m_index.get().emplace(AuxCacheItem{micros, scriptId, hash, pblock}); + + return ret.second; + } + + void Reset() { + m_index.clear(); + } + + bool Get(const CScriptID scriptId, std::shared_ptr& pblock) const { + auto it = m_index.get().lower_bound(ByScriptIdView{scriptId, 0LL}); + if (it != m_index.get().end() && it->scriptId == scriptId) { + pblock = it->pblock; + return true; + } + pblock.reset(); + return false; + } + + bool Get(const uint256 blockhash, std::shared_ptr& pblock) const { + auto it = m_index.get().lower_bound(blockhash); + if (it != m_index.get().end() && it->hash == blockhash) { + pblock = it->pblock; + return true; + } + pblock.reset(); + return false; + } +}; + +CAuxBlockCache::CAuxBlockCache() : m_impl(MakeUnique()) {}; +CAuxBlockCache::~CAuxBlockCache() = default; + +bool CAuxBlockCache::Add(const CScriptID scriptId, std::shared_ptr pblock) { + return m_impl->Add(scriptId, pblock); +} + +void CAuxBlockCache::Reset() { + m_impl->Reset(); +} + +bool CAuxBlockCache::Get(const CScriptID scriptId, std::shared_ptr& pblock) { + return m_impl->Get(scriptId, pblock); +} + +bool CAuxBlockCache::Get(const uint256 blockhash, std::shared_ptr& pblock) { + return m_impl->Get(blockhash, pblock); +} diff --git a/src/rpc/auxcache.h b/src/rpc/auxcache.h new file mode 100644 index 000000000..c5eec30d1 --- /dev/null +++ b/src/rpc/auxcache.h @@ -0,0 +1,44 @@ +// Copyright (c) 2025 The Dogecoin Core developers +// Distributed under the MIT software license, see the accompanying +// file COPYING or http://www.opensource.org/licenses/mit-license.php. + +#ifndef DOGECOIN_AUXCACHE_H +#define DOGECOIN_AUXCACHE_H + +#include "script/standard.h" // for CScriptID +#include "primitives/block.h" // for CBlock +#include "uint256.h" // for uint256 + +#include // for std::unique_ptr, std::shared_ptr + +/** Cache to keep track of blocks templated for AuxPoW mining, by CScriptID + * (coinbase output script.) + * + * Searchable by coinbase scriptpubkey (CScriptID) and blockhash (uint256) + * + */ +class CAuxBlockCache { + // Do not put impementation details in the header because they are + // heavy on includes. Instead use an implementation class. + class Impl; + const std::unique_ptr m_impl; + +public: + explicit CAuxBlockCache(); + ~CAuxBlockCache(); + + /** Adds a block to the cache */ + bool Add(const CScriptID scriptId, std::shared_ptr pblock); + + /** Resets the entire cache */ + void Reset(); + + /** Get the cached CBlock (optional) for a CScriptID */ + bool Get(const CScriptID scriptId, std::shared_ptr& pblock); + + /** Get the cached CBlock (optional) by block hash */ + bool Get(const uint256 blockhash, std::shared_ptr& pblock); + +}; + +#endif //DOGECOIN_AUXCACHE_H diff --git a/src/rpc/auxpow.cpp b/src/rpc/auxpow.cpp index 19624e54e..0c4b5a25e 100644 --- a/src/rpc/auxpow.cpp +++ b/src/rpc/auxpow.cpp @@ -15,6 +15,7 @@ #include "init.h" #include "miner.h" #include "net.h" +#include "rpc/auxcache.h" #include "rpc/server.h" #include "util.h" #include "utilstrencodings.h" @@ -22,21 +23,15 @@ #include "validationinterface.h" #include -#include +#include #include #include bool fUseNamecoinApi; -/** - * 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 CCriticalSection cs_auxpowrpc; +static CAuxBlockCache auxBlockCache; static std::vector> vNewBlockTemplate; void AuxMiningCheck() @@ -62,14 +57,12 @@ void AuxMiningCheck() static UniValue AuxMiningCreateBlock(const CScript& scriptPubKey) { - AuxMiningCheck(); - LOCK(cs_auxblockCache); + LOCK(cs_auxpowrpc); - static unsigned nTransactionsUpdatedLast; + static unsigned int nTransactionsUpdatedLast; static const CBlockIndex* pindexPrev = nullptr; static uint64_t nStart; - static std::map curBlocks; static unsigned nExtraNonce = 0; // Dogecoin: Never mine witness tx @@ -80,26 +73,23 @@ static UniValue AuxMiningCreateBlock(const CScript& scriptPubKey) * a single dogecoind instance, for example when a pool runs multiple sub- * pools with different payout strategies. */ - CBlock* pblock = nullptr; + std::shared_ptr pblock; CScriptID scriptID (scriptPubKey); - auto iter = curBlocks.find(scriptID); - if (iter != curBlocks.end()) pblock = iter->second; - + auxBlockCache.Get(scriptID, pblock); { LOCK(cs_main); // Update block - if (pblock == nullptr || pindexPrev != chainActive.Tip() + if (!pblock || pindexPrev != chainActive.Tip() || (mempool.GetTransactionsUpdated() != nTransactionsUpdatedLast && GetTime() - nStart > 60)) { if (pindexPrev != chainActive.Tip()) { - // Clear old blocks since they're obsolete now. - mapNewBlock.clear(); + // Clear caches since they're obsolete now. + auxBlockCache.Reset(); vNewBlockTemplate.clear(); - curBlocks.clear(); - pblock = nullptr; + pblock.reset(); } // Create new block with nonce = 0 and extraNonce = 1 @@ -118,9 +108,8 @@ static UniValue AuxMiningCreateBlock(const CScript& scriptPubKey) newBlock->block.SetAuxpowFlag(true); // Save - pblock = &newBlock->block; - curBlocks[scriptID] = pblock; - mapNewBlock[pblock->GetHash()] = pblock; + pblock = std::make_shared(newBlock->block); + auxBlockCache.Add(scriptID, pblock); vNewBlockTemplate.push_back(std::move(newBlock)); } } @@ -150,19 +139,19 @@ static UniValue AuxMiningCreateBlock(const CScript& scriptPubKey) return result; } -static bool AuxMiningSubmitBlock(const std::string& hashHex, const std::string& auxpowHex) +static UniValue AuxMiningSubmitBlock(const std::string& hashHex, const std::string& auxpowHex) { - AuxMiningCheck(); - LOCK(cs_auxblockCache); + LOCK(cs_auxpowrpc); uint256 hash; hash.SetHex(hashHex); - const std::map::iterator mit = mapNewBlock.find(hash); - if (mit == mapNewBlock.end()) + std::shared_ptr pblock; + if (!auxBlockCache.Get(hash, pblock)) { throw JSONRPCError(RPC_INVALID_PARAMETER, "block hash unknown"); - CBlock& block = *mit->second; + } + CBlock& block = *pblock; const std::vector vchAuxPow = ParseHex(auxpowHex); CDataStream ss(vchAuxPow, SER_GETHASH, PROTOCOL_VERSION); @@ -173,12 +162,11 @@ static bool AuxMiningSubmitBlock(const std::string& hashHex, const std::string& submitblock_StateCatcher sc(block.GetHash()); RegisterValidationInterface(&sc); - std::shared_ptr shared_block - = std::make_shared(block); - bool fAccepted = ProcessNewBlock(Params(), shared_block, true, nullptr); + std::shared_ptr shared_block = std::make_shared(block); + ProcessNewBlock(Params(), shared_block, true, nullptr); UnregisterValidationInterface(&sc); - return fAccepted; + return BIP22ValidationResult(sc.state); } UniValue getauxblockbip22(const JSONRPCRequest& request) @@ -216,6 +204,8 @@ UniValue getauxblockbip22(const JSONRPCRequest& request) + HelpExampleRpc("getauxblock", "") ); + AuxMiningCheck(); + std::shared_ptr coinbaseScript; GetMainSignals().ScriptForMining(coinbaseScript); @@ -227,105 +217,25 @@ UniValue getauxblockbip22(const JSONRPCRequest& request) if (!coinbaseScript->reserveScript.size()) throw JSONRPCError(RPC_INTERNAL_ERROR, "No coinbase script available (mining requires a wallet)"); - AuxMiningCheck(); - LOCK(cs_auxblockCache); - /* Create a new block? */ if (request.params.size() == 0) { - static unsigned nTransactionsUpdatedLast; - static const CBlockIndex* pindexPrev = nullptr; - static uint64_t nStart; - static CBlock* pblock = nullptr; - static unsigned nExtraNonce = 0; - - // Update block - // 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()) - { - // 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)); - } - } - - 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(fUseNamecoinApi ? "_target" : "target", HexStr(BEGIN(target), END(target))); - - return result; + return AuxMiningCreateBlock(coinbaseScript->reserveScript); } - /* Submit a block instead. Note that this need not lock cs_main, - since ProcessNewBlock below locks it instead. */ - + /* Submit a block instead. */ assert(request.params.size() == 2); - uint256 hash; - hash.SetHex(request.params[0].get_str()); - const std::map::iterator mit = mapNewBlock.find(hash); - if (mit == mapNewBlock.end()) - throw JSONRPCError(RPC_INVALID_PARAMETER, "block hash unknown"); - CBlock& block = *mit->second; + std::string blockHashHex = request.params[0].get_str(); + std::string auxPowHex = request.params[1].get_str(); - const std::vector vchAuxPow - = ParseHex(request.params[1].get_str()); - CDataStream ss(vchAuxPow, SER_GETHASH, PROTOCOL_VERSION); - CAuxPow pow; - ss >> pow; - block.SetAuxpow(new CAuxPow(pow)); - assert(block.GetHash() == hash); + UniValue response = AuxMiningSubmitBlock(blockHashHex, auxPowHex); - 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); - - if (fAccepted) + if (response.isNull()) { coinbaseScript->KeepScript(); + } - return BIP22ValidationResult(sc.state); + return response; } UniValue createauxblock(const JSONRPCRequest& request) @@ -381,8 +291,12 @@ UniValue submitauxblock(const JSONRPCRequest& request) + HelpExampleRpc("submitauxblock", "\"hash\" \"serialised auxpow\"") ); - return AuxMiningSubmitBlock(request.params[0].get_str(), - request.params[1].get_str()); + std::string blockHashHex = request.params[0].get_str(); + std::string auxPowHex = request.params[1].get_str(); + + UniValue response = AuxMiningSubmitBlock(blockHashHex, auxPowHex); + + return response.isNull(); } UniValue getauxblock(const JSONRPCRequest& request) diff --git a/src/test/auxcache_tests.cpp b/src/test/auxcache_tests.cpp new file mode 100644 index 000000000..ff9906a67 --- /dev/null +++ b/src/test/auxcache_tests.cpp @@ -0,0 +1,150 @@ +// Copyright (c) 2025 The Dogecoin Core developers +// Distributed under the MIT/X11 software license, see the accompanying +// file COPYING or http://www.opensource.org/licenses/mit-license.php. + +#include "amount.h" +#include "consensus/merkle.cpp" +#include "primitives/block.h" +#include "primitives/transaction.h" +#include "rpc/auxcache.h" +#include "script/script.h" +#include "utiltime.h" + +#include "test/test_bitcoin.h" + +#include +#include + +BOOST_FIXTURE_TEST_SUITE(auxcache_tests, TestingSetup) + +static const std::string cb_pk_1 = "03c758272b121a3e50a1a6a25aad800a45af51486227bb8f06257df80a15120135"; +static const std::string cb_pk_2 = "020c163123e1e3b8bcf9114e2df152b5aa0bc5f69458991236746be48adce96bed"; + +// creates a bare dummy block with auxpow on +std::shared_ptr CreateDummyBlock(CScript scriptPubKey, CAmount amount) { + CMutableTransaction coinbase{}; + coinbase.nVersion = 1; + coinbase.vin.push_back(CTxIn(uint256::ZERO, 0)); + coinbase.vout.push_back(CTxOut(amount, scriptPubKey)); + + CBlock block{}; + block.nVersion = 4; + block.hashPrevBlock = uint256::ZERO; + block.nTime = GetTime(); + block.nBits = 0x1e0ffff0; + block.nNonce = 0; + block.SetChainId(98); + block.SetAuxpowFlag(true); + + block.vtx.resize(1); + block.vtx[0] = MakeTransactionRef(std::move(coinbase)); + + block.hashMerkleRoot = BlockMerkleRoot(block); + + return std::make_shared(block); +} + + +BOOST_AUTO_TEST_CASE(check_auxpow) { + CAuxBlockCache cache; + std::shared_ptr cached_block; + bool res; + + CScript cb_script_1 = CScript() << ParseHex(cb_pk_1) << OP_CHECKSIG; + CScriptID scriptId_1(cb_script_1); + std::shared_ptr created_block_1 = CreateDummyBlock(cb_script_1, CAmount(69)); + uint256 blockhash_1 = created_block_1->GetHash(); + + // add to cache + res = cache.Add(scriptId_1, created_block_1); + BOOST_CHECK(res); + + // get by scriptId + res = cache.Get(scriptId_1, cached_block); + BOOST_CHECK(res); + BOOST_CHECK_EQUAL(blockhash_1.GetHex(), cached_block->GetHash().GetHex()); + + // get by hash + res = cache.Get(blockhash_1, cached_block); + BOOST_CHECK(res); + BOOST_CHECK_EQUAL(blockhash_1.GetHex(), cached_block->GetHash().GetHex()); + + // Adding the same block again fails + res = cache.Add(scriptId_1, created_block_1); + BOOST_CHECK(!res); + + CScript cb_script_2 = CScript() << ParseHex(cb_pk_2) << OP_CHECKSIG; + CScriptID scriptId_2(cb_script_2); + std::shared_ptr created_block_2 = CreateDummyBlock(cb_script_2, CAmount(169)); + uint256 blockhash_2 = created_block_2->GetHash(); + + // Make sure the block hashes are different + BOOST_CHECK_NE(blockhash_1.GetHex(), blockhash_2.GetHex()); + + // add second block to cache + res = cache.Add(scriptId_2, created_block_2); + BOOST_CHECK(res); + + // get second block by scriptId + res = cache.Get(scriptId_2, cached_block); + BOOST_CHECK(res); + BOOST_CHECK_EQUAL(blockhash_2.GetHex(), cached_block->GetHash().GetHex()); + + // get second block by hash + res = cache.Get(blockhash_2, cached_block); + BOOST_CHECK(res); + BOOST_CHECK_EQUAL(blockhash_2.GetHex(), cached_block->GetHash().GetHex()); + + // get first block by scriptId + res = cache.Get(scriptId_1, cached_block); + BOOST_CHECK(res); + BOOST_CHECK_EQUAL(blockhash_1.GetHex(), cached_block->GetHash().GetHex()); + + // NOTE: since not all OS' have high precision time, we mock +1 second here + SetMockTime(GetTime() + 1LL); + + // create another block with the first scriptId + std::shared_ptr created_block_3 = CreateDummyBlock(cb_script_1, CAmount(269)); + uint256 blockhash_3 = created_block_3->GetHash(); + + // Make sure the block hashes are different + BOOST_CHECK_NE(blockhash_1.GetHex(), blockhash_3.GetHex()); + + // add third block to cache + res = cache.Add(scriptId_1, created_block_3); + BOOST_CHECK(res); + + // get third block by scriptId + res = cache.Get(scriptId_1, cached_block); + BOOST_CHECK(res); + BOOST_CHECK_EQUAL(blockhash_3.GetHex(), cached_block->GetHash().GetHex()); + + // the first block is still available by hash + res = cache.Get(blockhash_1, cached_block); + BOOST_CHECK(res); + BOOST_CHECK_EQUAL(blockhash_1.GetHex(), cached_block->GetHash().GetHex()); + + // clearing the cache removes all data + cache.Reset(); + res = cache.Get(scriptId_1, cached_block); + BOOST_CHECK(!res); + BOOST_CHECK(!cached_block); + res = cache.Get(scriptId_2, cached_block); + BOOST_CHECK(!res); + BOOST_CHECK(!cached_block); + res = cache.Get(blockhash_1, cached_block); + BOOST_CHECK(!res); + BOOST_CHECK(!cached_block); + res = cache.Get(blockhash_2, cached_block); + BOOST_CHECK(!res); + BOOST_CHECK(!cached_block); + res = cache.Get(blockhash_3, cached_block); + BOOST_CHECK(!res); + BOOST_CHECK(!cached_block); + + // Unmock time + SetMockTime(0LL); + +} + +BOOST_AUTO_TEST_SUITE_END()