diff --git a/src/crypto/progpow.cpp b/src/crypto/progpow.cpp index ecc6bb41e8..882ab69d29 100644 --- a/src/crypto/progpow.cpp +++ b/src/crypto/progpow.cpp @@ -4,14 +4,12 @@ #include "progpow.h" -#include -#include #include #include #include -#include -#include +#include +#include static inline ethash::hash256 U256ToH256(const uint256& in) { @@ -40,15 +38,23 @@ static inline uint256 H256ToU256(const ethash::hash256& in) { uint256 progpow_hash_full(const CProgPowHeader& header, uint256& mix_hash) { - static ethash::epoch_context_ptr epochContext{nullptr,nullptr}; - if (!epochContext || epochContext->epoch_number != ethash::get_epoch_number(header.nHeight)) - { - epochContext.reset(); - epochContext = ethash::create_epoch_context(ethash::get_epoch_number(header.nHeight)); - } + return progpow_hash_full(progpow_header_hash(header), header.nHeight, header.nNonce64, mix_hash); +} - const auto header_h256{U256ToH256(SerializeHash(header))}; - const auto result = progpow::hash(*epochContext, header.nHeight, header_h256, header.nNonce64); +ethash::hash256 progpow_header_hash(const CProgPowHeader& header) +{ + return U256ToH256(SerializeHash(header)); +} + +uint256 progpow_hash_full(const ethash::hash256& header_hash, uint32_t height, uint64_t nonce, uint256& mix_hash) +{ + // The managed cache keeps this context alive in the calling thread, including across + // epoch changes in other threads. Hashing only reads the light cache. + const auto* epochContext = ethash_get_global_epoch_context(ethash::get_epoch_number(height)); + if (!epochContext) + throw std::runtime_error("Unable to allocate FiroPoW epoch context"); + + const auto result = progpow::hash(*epochContext, height, header_hash, nonce); mix_hash = H256ToU256(result.mix_hash); return H256ToU256(result.final_hash); } @@ -63,4 +69,4 @@ uint256 progpow_hash_light(const CProgPowHeader& header) const auto seed_h256{progpow::hash_seed(header_h256, header.nNonce64)}; const auto final_h256{progpow::hash_final(seed_h256, mix_h256)}; return H256ToU256(final_h256); -} \ No newline at end of file +} diff --git a/src/crypto/progpow.h b/src/crypto/progpow.h index 6b1f7b5e4a..24889e55bc 100644 --- a/src/crypto/progpow.h +++ b/src/crypto/progpow.h @@ -42,7 +42,13 @@ class CProgPowHeader { /* Performs a full progpow hash (DAG loops implied) provided header already hash nHeight valued */ uint256 progpow_hash_full(const CProgPowHeader& header, uint256& mix_hash); +/** Precompute the nonce-independent input. Refresh after changing any serialized header field. */ +ethash::hash256 progpow_header_hash(const CProgPowHeader& header); + +/** Hash one nonce using the managed light cache. Height must match the precomputed header. */ +uint256 progpow_hash_full(const ethash::hash256& header_hash, uint32_t height, uint64_t nonce, uint256& mix_hash); + /* Performs a light progpow hash (DAG loops excluded) provided header has mix_hash */ uint256 progpow_hash_light(const CProgPowHeader& header); -#endif // FIRO_PROGPOW_H \ No newline at end of file +#endif // FIRO_PROGPOW_H diff --git a/src/miner.cpp b/src/miner.cpp index b1c315c801..218633a22c 100644 --- a/src/miner.cpp +++ b/src/miner.cpp @@ -34,10 +34,12 @@ #include "crypto/MerkleTreeProof/mtp.h" #include "crypto/Lyra2Z/Lyra2Z.h" #include "crypto/Lyra2Z/Lyra2.h" +#include "crypto/progpow.h" #include "evo/spork.h" #include #include #include +#include #include #include @@ -1083,6 +1085,7 @@ void static FiroMiner(const CChainParams &chainparams) { } while (true) { + boost::this_thread::interruption_point(); if (chainparams.MiningRequiresPeers()) { // Busy-wait for the network to come online so we don't waste time mining // on an obsolete chain. In regtest mode we expect to fly solo. @@ -1109,10 +1112,6 @@ void static FiroMiner(const CChainParams &chainparams) { // Create new block // unsigned int nTransactionsUpdatedLast = mempool.GetTransactionsUpdated(); - CBlockIndex *pindexPrev = chainActive.Tip(); - if (pindexPrev) { - LogPrintf("loop pindexPrev->nHeight=%d\n", pindexPrev->nHeight); - } LogPrintf("BEFORE: pblocktemplate\n"); std::unique_ptr pblocktemplate = BlockAssembler(Params()).CreateNewBlock(coinbaseScript->reserveScript, {}); LogPrintf("AFTER: pblocktemplate\n"); @@ -1121,7 +1120,14 @@ void static FiroMiner(const CChainParams &chainparams) { return; } CBlock *pblock = &pblocktemplate->block; - IncrementExtraNonce(pblock, pindexPrev, nExtraNonce); + CBlockIndex* pindexPrev; + { + LOCK(cs_main); + pindexPrev = chainActive.Tip(); + if (pblock->hashPrevBlock != pindexPrev->GetBlockHash()) + continue; + IncrementExtraNonce(pblock, pindexPrev, nExtraNonce); + } LogPrintf("Running FiroMiner with %u transactions in block (%u bytes)\n", pblock->vtx.size(), ::GetSerializeSize(*pblock, SER_NETWORK, PROTOCOL_VERSION)); @@ -1145,10 +1151,15 @@ void static FiroMiner(const CChainParams &chainparams) { // Check if something found uint256 thash; uint256 mix_hash; + bool found = false; + const bool fProgPow = pblock->IsProgPow(); + const auto headerHash = fProgPow ? progpow_header_hash(pblock->GetProgPowHeader()) : ethash::hash256{}; + // Bound stale work by elapsed time as well as the nonce count. + const auto batchEnd = std::chrono::steady_clock::now() + std::chrono::seconds(1); while (true) { - if (pblock->IsProgPow()) { - thash = pblock->GetProgPowHashFull(mix_hash); + if (fProgPow) { + thash = progpow_hash_full(headerHash, pblock->nHeight, pblock->nNonce64, mix_hash); } else if (pblock->IsMTP()) { thash = mtp::hash(*pblock, Params().GetConsensus().powLimit); pblock->mtpHashValue = thash; @@ -1194,27 +1205,32 @@ void static FiroMiner(const CChainParams &chainparams) { // In regression test mode, stop mining after a block is found. if (chainparams.MineBlocksOnDemand()) throw boost::thread_interrupted(); + found = true; break; } pblock->nNonce += 1; pblock->nNonce64 += 1; - if ((pblock->nNonce & 0xFF) == 0) + if ((pblock->nNonce & 0xFF) == 0 || std::chrono::steady_clock::now() >= batchEnd) break; } + // Rebuild after any solution, including a stale or rejected block. + if (found) + break; // Regtest mode doesn't require peers - if (g_connman->GetNodeCount(CConnman::CONNECTIONS_ALL) == 0 && chainparams.MiningRequiresPeers()) + if (chainparams.MiningRequiresPeers() && g_connman->GetNodeCount(CConnman::CONNECTIONS_ALL) == 0) break; if (pblock->nNonce >= 0xffff0000) break; if (mempool.GetTransactionsUpdated() != nTransactionsUpdatedLast && GetTime() - nStart > 60) break; - if (pindexPrev != chainActive.Tip()) - break; + { + LOCK(cs_main); + if (pindexPrev != chainActive.Tip()) + break; - // Update nTime every few seconds - if (UpdateTime(pblock, chainparams.GetConsensus(), pindexPrev) < 0) - break; // Recreate the block if the clock has run backwards, - // so that we can use the correct time. + if (UpdateTime(pblock, chainparams.GetConsensus(), pindexPrev) < 0) + break; // Recreate the block if the clock has run backwards. + } if (chainparams.GetConsensus().fPowAllowMinDifficultyBlocks) { // Changing pblock->nTime can change work required on testnet: hashTarget.SetCompact(pblock->nBits); @@ -1234,30 +1250,42 @@ void static FiroMiner(const CChainParams &chainparams) { void GenerateBitcoins(bool fGenerate, int nThreads, const CChainParams& chainparams) { - static boost::thread_group* minerThreads = NULL; + // Workers need cs_main while stopping; callers must not hold it across join_all(). + AssertLockNotHeld(cs_main); + static CCriticalSection cs_miner; + LOCK(cs_miner); + boost::this_thread::disable_interruption noInterrupt; + static std::unique_ptr minerThreads; if (nThreads < 0) nThreads = GetNumCores(); - if (minerThreads != NULL) + if (minerThreads) { minerThreads->interrupt_all(); - delete minerThreads; - minerThreads = NULL; + minerThreads->join_all(); + minerThreads.reset(); } if (nThreads == 0 || !fGenerate) return; - minerThreads = new boost::thread_group(); - for (int i = 0; i < nThreads; i++) - minerThreads->create_thread(boost::bind(&FiroMiner, boost::cref(chainparams))); + minerThreads = std::make_unique(); + try { + for (int i = 0; i < nThreads; ++i) + minerThreads->create_thread(boost::bind(&FiroMiner, boost::cref(chainparams))); + } catch (...) { + minerThreads->interrupt_all(); + minerThreads->join_all(); + minerThreads.reset(); + throw; + } } void IncrementExtraNonce(CBlock* pblock, const CBlockIndex* pindexPrev, unsigned int& nExtraNonce) { // Update nExtraNonce - static uint256 hashPrevBlock; + static thread_local uint256 hashPrevBlock; if (hashPrevBlock != pblock->hashPrevBlock) { nExtraNonce = 0; diff --git a/src/miner.h b/src/miner.h index cd55d640eb..f947f4ba90 100644 --- a/src/miner.h +++ b/src/miner.h @@ -242,7 +242,7 @@ class BlockAssembler /** Modify the extranonce in a block */ void IncrementExtraNonce(CBlock* pblock, const CBlockIndex* pindexPrev, unsigned int& nExtraNonce); int64_t UpdateTime(CBlockHeader* pblock, const Consensus::Params& consensusParams, const CBlockIndex* pindexPrev); -/** Run the miner threads */ +/** Run the miner threads. Must not be called with cs_main held. */ void GenerateBitcoins(bool fGenerate, int nThreads, const CChainParams& chainparams); #endif // BITCOIN_MINER_H diff --git a/src/rpc/mining.cpp b/src/rpc/mining.cpp index 1823c508ea..777482cf98 100644 --- a/src/rpc/mining.cpp +++ b/src/rpc/mining.cpp @@ -137,7 +137,10 @@ UniValue generateBlocks(boost::shared_ptr coinbaseScript, int nG CBlock *pblock = &pblocktemplate->block; { LOCK(cs_main); - IncrementExtraNonce(pblock, chainActive.Tip(), nExtraNonce); + const CBlockIndex* pindexPrev = chainActive.Tip(); + if (pblock->hashPrevBlock != pindexPrev->GetBlockHash()) + continue; + IncrementExtraNonce(pblock, pindexPrev, nExtraNonce); } /** @@ -149,9 +152,10 @@ UniValue generateBlocks(boost::shared_ptr coinbaseScript, int nG */ if (pblock->IsProgPow()) { + const auto header_hash = progpow_header_hash(pblock->GetProgPowHeader()); while (nMaxTries > 0 && pblock->nNonce64 < nInnerLoopCount) { uint256 mix_hash; - auto final_hash{progpow_hash_full(pblock->GetProgPowHeader(), mix_hash)}; + auto final_hash{progpow_hash_full(header_hash, pblock->nHeight, pblock->nNonce64, mix_hash)}; if (CheckProofOfWork(final_hash, pblock->nBits, Params().GetConsensus())) { pblock->mix_hash = mix_hash; diff --git a/src/test/CMakeLists.txt b/src/test/CMakeLists.txt index 6408280c81..a25b79ea7a 100644 --- a/src/test/CMakeLists.txt +++ b/src/test/CMakeLists.txt @@ -54,6 +54,7 @@ add_executable(test_firo ${CMAKE_CURRENT_SOURCE_DIR}/mbstring_tests.cpp ${CMAKE_CURRENT_SOURCE_DIR}/mempool_tests.cpp ${CMAKE_CURRENT_SOURCE_DIR}/merkle_tests.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/miner_tests.cpp ${CMAKE_CURRENT_SOURCE_DIR}/mtp_halving_tests.cpp ${CMAKE_CURRENT_SOURCE_DIR}/mtp_tests.cpp ${CMAKE_CURRENT_SOURCE_DIR}/mtp_trans_tests.cpp diff --git a/src/test/firopow_tests.cpp b/src/test/firopow_tests.cpp index 0e81100044..55708c4670 100644 --- a/src/test/firopow_tests.cpp +++ b/src/test/firopow_tests.cpp @@ -2,10 +2,16 @@ #include "test/fixtures.h" #include +#include #include #include #include #include +#include + +#include +#include +#include BOOST_FIXTURE_TEST_SUITE(firpow_tests, BasicTestingSetup) BOOST_AUTO_TEST_CASE(firopow_hash_and_verify) { @@ -39,4 +45,75 @@ BOOST_AUTO_TEST_CASE(firopow_hash_and_verify) { } } +BOOST_AUTO_TEST_CASE(firopow_prepared_header) +{ + const CProgPowHeader original{1, uint256S("12"), uint256S("34"), 123456, 0x207fffff, 1, 42, uint256()}; + std::array headers; + headers.fill(original); + ++headers[1].nVersion; + ++headers[2].hashPrevBlock.begin()[0]; + ++headers[3].hashMerkleRoot.begin()[0]; + ++headers[4].nTime; + ++headers[5].nBits; + ++headers[6].nHeight; + ++headers[7].nNonce64; + headers[8].mix_hash = uint256S("56"); + + auto context = ethash::create_epoch_context(0); + BOOST_REQUIRE(context); + const auto original_hash = progpow_header_hash(original); + for (size_t i = 0; i < headers.size(); ++i) { + const auto& header = headers[i]; + // Encode the consensus header independently of CProgPowHeader's serializer. + std::array bytes{}; + WriteLE32(bytes.data(), header.nVersion); + std::copy(header.hashPrevBlock.begin(), header.hashPrevBlock.end(), bytes.begin() + 4); + std::copy(header.hashMerkleRoot.begin(), header.hashMerkleRoot.end(), bytes.begin() + 36); + WriteLE32(bytes.data() + 68, header.nTime); + WriteLE32(bytes.data() + 72, header.nBits); + WriteLE32(bytes.data() + 76, header.nHeight); + uint256 serialized_hash; + CHash256().Write(bytes.data(), bytes.size()).Finalize(serialized_hash.begin()); + const auto reference_header = to_hash256(serialized_hash.GetHex()); + const auto prepared_header = progpow_header_hash(header); + BOOST_CHECK(ethash::is_equal(prepared_header, reference_header)); + BOOST_CHECK_EQUAL(ethash::is_equal(prepared_header, original_hash), i == 0 || i >= 7); + + const auto reference = progpow::hash(*context, header.nHeight, reference_header, header.nNonce64); + uint256 prepared_mix, wrapper_mix; + const auto prepared = progpow_hash_full(prepared_header, header.nHeight, header.nNonce64, prepared_mix); + const auto wrapped = progpow_hash_full(header, wrapper_mix); + BOOST_CHECK_EQUAL(prepared.GetHex(), to_hex(reference.final_hash)); + BOOST_CHECK_EQUAL(prepared_mix.GetHex(), to_hex(reference.mix_hash)); + BOOST_CHECK(prepared == wrapped); + BOOST_CHECK(prepared_mix == wrapper_mix); + } +} + +BOOST_AUTO_TEST_CASE(firopow_concurrent_epochs) +{ + const std::array cases{ + &firopow_hash_test_cases[4], &firopow_hash_test_cases[5]}; + BOOST_REQUIRE_EQUAL(cases[0]->block_number, ethash::epoch_length - 1); + BOOST_REQUIRE_EQUAL(cases[1]->block_number, ethash::epoch_length); + std::array, 4> workers; + std::latch start{workers.size()}; + for (size_t worker = 0; worker < workers.size(); ++worker) { + workers[worker] = std::async(std::launch::async, [&, worker] { + start.arrive_and_wait(); + for (size_t offset = 0; offset < cases.size(); ++offset) { + const auto& t = *cases[(worker + offset) % cases.size()]; + uint256 mix; + const auto result = progpow_hash_full(to_hash256(t.header_hash_hex), t.block_number, + std::stoull(t.nonce_hex, nullptr, 16), mix); + if (result.GetHex() != t.final_hash_hex || mix.GetHex() != t.mix_hash_hex) + return false; + } + return true; + }); + } + for (auto& worker : workers) + BOOST_CHECK(worker.get()); +} + BOOST_AUTO_TEST_SUITE_END() diff --git a/src/test/miner_tests.cpp b/src/test/miner_tests.cpp new file mode 100644 index 0000000000..4518d0ff3b --- /dev/null +++ b/src/test/miner_tests.cpp @@ -0,0 +1,142 @@ +// Copyright (c) 2026 The Firo developers +// Distributed under the MIT software license, see the accompanying +// file COPYING or http://www.opensource.org/licenses/mit-license.php. + +#include "chain.h" +#include "chainparams.h" +#include "miner.h" +#include "test/test_bitcoin.h" +#include "utiltime.h" +#include "validationinterface.h" + +#include +#include +#include +#include +#include +#include +#include + +BOOST_FIXTURE_TEST_SUITE(miner_tests, BasicTestingSetup) + +BOOST_AUTO_TEST_CASE(extra_nonce_workers_are_independent) +{ + CBlockIndex previous; + previous.nHeight = 1; + const auto makeBlock = [](const char* previousHash) { + CBlock block; + block.hashPrevBlock = uint256S(previousHash); + CMutableTransaction coinbase; + coinbase.vin.resize(1); + block.vtx.push_back(MakeTransactionRef(std::move(coinbase))); + return block; + }; + + std::promise firstReady, secondDone; + auto first = std::async(std::launch::async, [&] { + CBlock block = makeBlock("01"); + unsigned int extraNonce = 99; + std::array nonces; + IncrementExtraNonce(&block, &previous, extraNonce); + nonces[0] = extraNonce; + firstReady.set_value(); + secondDone.get_future().wait(); + IncrementExtraNonce(&block, &previous, extraNonce); + nonces[1] = extraNonce; + block.hashPrevBlock = uint256S("03"); + IncrementExtraNonce(&block, &previous, extraNonce); + nonces[2] = extraNonce; + return nonces; + }); + auto second = std::async(std::launch::async, [&] { + firstReady.get_future().wait(); + CBlock block = makeBlock("02"); + unsigned int extraNonce = 99; + IncrementExtraNonce(&block, &previous, extraNonce); + secondDone.set_value(); + return extraNonce; + }); + + const auto nonces = first.get(); + BOOST_CHECK_EQUAL(nonces[0], 1U); + BOOST_CHECK_EQUAL(nonces[1], 2U); + BOOST_CHECK_EQUAL(nonces[2], 1U); + BOOST_CHECK_EQUAL(second.get(), 1U); +} + +BOOST_AUTO_TEST_CASE(restart_and_stop_wait_for_workers) +{ + std::mutex mutex; + std::condition_variable changed; + unsigned int active = 0, started = 0, interrupted = 0; + bool release = false; + std::future change; + boost::signals2::scoped_connection connection(GetMainSignals().ScriptForMining.connect( + [&](boost::shared_ptr& script) { + std::unique_lock lock(mutex); + ++active; + ++started; + changed.notify_all(); + lock.unlock(); + try { + while (true) + MilliSleep(1000); + } catch (const boost::thread_interrupted&) { + lock.lock(); + ++interrupted; + changed.notify_all(); + changed.wait(lock, [&] { return release; }); + --active; + changed.notify_all(); + } + // An empty script makes the worker exit without touching chain or wallet state. + script.reset(); + })); + BOOST_SCOPE_EXIT_ALL(&) { + { + std::lock_guard lock(mutex); + release = true; + changed.notify_all(); + } + GenerateBitcoins(false, 0, Params()); + std::unique_lock lock(mutex); + changed.wait(lock, [&] { return active == 0; }); + }; + + GenerateBitcoins(true, 2, Params()); + { + std::unique_lock lock(mutex); + BOOST_REQUIRE(changed.wait_for(lock, std::chrono::seconds(5), [&] { return started == 2; })); + } + change = std::async(std::launch::async, [] { GenerateBitcoins(true, 1, Params()); }); + { + std::unique_lock lock(mutex); + BOOST_REQUIRE(changed.wait_for(lock, std::chrono::seconds(5), [&] { return interrupted == 2; })); + BOOST_CHECK(change.wait_for(std::chrono::milliseconds(50)) == std::future_status::timeout); + BOOST_CHECK_EQUAL(started, 2U); + BOOST_CHECK_EQUAL(active, 2U); + release = true; + changed.notify_all(); + } + change.get(); + { + std::unique_lock lock(mutex); + BOOST_REQUIRE(changed.wait_for(lock, std::chrono::seconds(5), [&] { return started == 3; })); + BOOST_CHECK_EQUAL(active, 1U); + release = false; + } + change = std::async(std::launch::async, [] { GenerateBitcoins(false, 0, Params()); }); + { + std::unique_lock lock(mutex); + BOOST_REQUIRE(changed.wait_for(lock, std::chrono::seconds(5), [&] { return interrupted == 3; })); + BOOST_CHECK(change.wait_for(std::chrono::milliseconds(50)) == std::future_status::timeout); + BOOST_CHECK_EQUAL(active, 1U); + release = true; + changed.notify_all(); + } + change.get(); + std::lock_guard lock(mutex); + BOOST_CHECK_EQUAL(active, 0U); +} + +BOOST_AUTO_TEST_SUITE_END()