From 98bf557f568fa44af1fa7db0f6fe4da8df577da9 Mon Sep 17 00:00:00 2001 From: frstrtr Date: Thu, 18 Jun 2026 08:58:00 +0000 Subject: [PATCH 1/8] =?UTF-8?q?dgb:=20M3=20=C2=A77b=20DigiShield=20Scrypt-?= =?UTF-8?q?only=20ancestor=20walk=20+=20work-credit=20predicate?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Isolates the consensus quirk flagged at header_chain.hpp DIGISHIELD INSERTION POINT: the Scrypt difficulty window must walk Scrypt-algo ancestors ONLY and skip continuity (non-Scrypt) headers; folding a continuity header into the window corrupts the Scrypt retarget and breaks the THIRD INVARIANT work- neutrality. Adds header_credits_work() as the single SSOT predicate the validate() work-accounting and the retarget walk both consult, so they cannot drift. Header-only (deps: dgb_block_algo.hpp + std), so it links into the standalone GTest guard with no dgb OBJECT lib. - coin/dgb_digishield.hpp: scrypt_window_ancestors(), header_credits_work() - test/digishield_walk_test.cpp: 6 guards (zero-bits Scrypt, interleave skip, leading-continuity run, exhausted chain, zero window, work predicate) - wired into BOTH ctest foreach AND both build.yml --target allowlists (#143) Per-algo DigiShield target math layers on top in the following slice. --- .github/workflows/build.yml | 4 +- src/impl/dgb/coin/dgb_digishield.hpp | 73 +++++++++++++++++++ src/impl/dgb/coin/header_chain.hpp | 5 +- src/impl/dgb/test/CMakeLists.txt | 2 +- src/impl/dgb/test/digishield_walk_test.cpp | 84 ++++++++++++++++++++++ 5 files changed, 164 insertions(+), 4 deletions(-) create mode 100644 src/impl/dgb/coin/dgb_digishield.hpp create mode 100644 src/impl/dgb/test/digishield_walk_test.cpp diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 3debd89f7..ff5d5e765 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -68,7 +68,7 @@ jobs: test_mweb_builder \ test_address_resolution test_compute_share_target \ test_utxo test_dgb_subsidy dgb_share_test \ - rpc_request_test softfork_check_test genesis_check_test algo_select_test \ + rpc_request_test softfork_check_test genesis_check_test algo_select_test digishield_walk_test \ v37_test \ -j$(nproc) @@ -196,7 +196,7 @@ jobs: test_mweb_builder \ test_address_resolution test_compute_share_target \ test_utxo test_dgb_subsidy dgb_share_test \ - rpc_request_test softfork_check_test genesis_check_test algo_select_test \ + rpc_request_test softfork_check_test genesis_check_test algo_select_test digishield_walk_test \ test_coin_broadcaster test_multiaddress_pplns test_pplns_stress \ v37_test \ -j$(nproc) diff --git a/src/impl/dgb/coin/dgb_digishield.hpp b/src/impl/dgb/coin/dgb_digishield.hpp new file mode 100644 index 000000000..1c5a8e9dd --- /dev/null +++ b/src/impl/dgb/coin/dgb_digishield.hpp @@ -0,0 +1,73 @@ +#pragma once +// --------------------------------------------------------------------------- +// DGB DigiShield Scrypt-only ancestor walk (M3 §7b — SCRYPT-ONLY VALIDATION). +// +// DigiByte retargets per-block per-algo with DigiShield/MultiShield, NOT +// Bitcoin's 2016-block window. For the V36 Scrypt-only lane the difficulty +// window must walk SCRYPT-ALGO ancestors ONLY. On a mixed-algo chain the +// header sequence interleaves Scrypt headers (full PoW validate) with +// non-Scrypt headers accepted by continuity (work-neutral). Folding a +// continuity header into the retarget window corrupts the Scrypt target AND +// re-introduces the work-neutrality break documented as the THIRD INVARIANT +// in coin/header_chain.hpp. This header isolates that walk so the trap lives +// in one consensus-pinned, separately-guarded place — exactly the failure the +// header_chain.hpp DIGISHIELD INSERTION POINT note warns is "easy to get wrong +// on a mixed-algo chain". +// +// This is the ancestor-selection step only; the per-algo DigiShield target +// math (LWMA/MultiShield) layers on top in the following slice and consumes +// the indices this returns. +// +// Header-only: depends ONLY on coin/dgb_block_algo.hpp + std, so it links into +// the standalone CI guard (GTest-only, no dgb OBJECT lib) like algo_select. +// --------------------------------------------------------------------------- + +#include +#include +#include +#include + +#include + +namespace dgb::coin { + +// THIRD-INVARIANT work-accounting predicate. Only a Scrypt header credits +// cumulative work / best-chain weight; a continuity (known non-Scrypt) header +// extends the header chain but is work-neutral. This is the single predicate +// HeaderChain::validate() consults before adding a header's work, so the +// invariant cannot drift between the validate() path and the retarget walk. +inline bool header_credits_work(int32_t n_version) noexcept +{ + return is_scrypt_header(n_version); +} + +// Collect the Scrypt-algo ancestors that form a DigiShield retarget window. +// +// version_at(k) -> nVersion of the header k positions back from the search +// start (k = 0 is the nearest candidate ancestor). +// depth -> how many positions are walkable (chain length behind). +// window -> number of Scrypt ancestors the retarget needs. +// +// Returns the k-indices of the first `window` SCRYPT headers, nearest-first, +// skipping every non-Scrypt (continuity) header. Returns fewer than `window` +// only when the available chain is exhausted (early-chain / genesis region). +// Unknown-algo headers never enter a validated chain (REJECT at ingest), so +// the walk treats anything not Scrypt as a skip. +inline std::vector scrypt_window_ancestors( + const std::function& version_at, + std::size_t depth, + std::size_t window) +{ + std::vector out; + if (window == 0) return out; + out.reserve(window); + for (std::size_t k = 0; k < depth && out.size() < window; ++k) + { + if (is_scrypt_header(version_at(k))) + out.push_back(k); + // else: continuity header -> skipped, contributes no window sample + } + return out; +} + +} // namespace dgb::coin diff --git a/src/impl/dgb/coin/header_chain.hpp b/src/impl/dgb/coin/header_chain.hpp index 1694d8fb6..cdf2bb4b6 100644 --- a/src/impl/dgb/coin/header_chain.hpp +++ b/src/impl/dgb/coin/header_chain.hpp @@ -34,5 +34,8 @@ // ONLY — never the interleaved multi-algo header chain. On a mixed-algo chain // the previous-block / window walk must skip non-Scrypt (continuity) headers; // folding them into the window corrupts the Scrypt retarget and re-introduces -// the work-neutrality break above. Easy to get wrong on a mixed-algo chain. +// the work-neutrality break above. Easy to get wrong on a mixed-algo chain. The Scrypt-only +// ancestor selection for that window is implemented + guarded in +// coin/dgb_digishield.hpp (scrypt_window_ancestors + header_credits_work); +// the per-algo target math layers on top of it. namespace c2pool::dgb { /* TODO(M3): HeaderChain w/ Scrypt-only validate() + accept-by-continuity */ } diff --git a/src/impl/dgb/test/CMakeLists.txt b/src/impl/dgb/test/CMakeLists.txt index 0a79ed8b9..230c43c06 100644 --- a/src/impl/dgb/test/CMakeLists.txt +++ b/src/impl/dgb/test/CMakeLists.txt @@ -25,7 +25,7 @@ if (BUILD_TESTING AND GTest_FOUND) # transport rpc.cpp wiring is deferred (Option-B scope). Each MUST appear # in BOTH this ctest registration AND the build.yml --target allowlist, # or it becomes a #143-style NOT_BUILT sentinel that reds master. - foreach(dgb_guard rpc_request_test softfork_check_test genesis_check_test algo_select_test) + foreach(dgb_guard rpc_request_test softfork_check_test genesis_check_test algo_select_test digishield_walk_test) add_executable(${dgb_guard} ${dgb_guard}.cpp) target_link_libraries(${dgb_guard} PRIVATE GTest::gtest_main GTest::gtest nlohmann_json::nlohmann_json) diff --git a/src/impl/dgb/test/digishield_walk_test.cpp b/src/impl/dgb/test/digishield_walk_test.cpp new file mode 100644 index 000000000..1b060e176 --- /dev/null +++ b/src/impl/dgb/test/digishield_walk_test.cpp @@ -0,0 +1,84 @@ +// --------------------------------------------------------------------------- +// dgb M3 §7b DigiShield Scrypt-only ancestor-walk regression guard. +// +// Pins the consensus quirk flagged at coin/header_chain.hpp's DIGISHIELD +// INSERTION POINT: on a mixed-algo DGB chain the Scrypt difficulty window must +// walk SCRYPT ancestors ONLY and SKIP continuity (non-Scrypt) headers. Folding +// a continuity header into the window corrupts the Scrypt retarget and breaks +// work-neutrality (THIRD INVARIANT). Also pins header_credits_work() == the +// Scrypt predicate, so the validate() work-accounting and the retarget walk +// share one SSOT and cannot drift apart. +// +// Links ONLY the header-only helpers + gtest -- no dgb OBJECT lib / transport. +// --------------------------------------------------------------------------- + +#include +#include + +#include + +#include + +using namespace dgb::coin; + +static constexpr int32_t PRIMARY = 2; // BLOCK_VERSION_DEFAULT +static constexpr int32_t SCRYPT = PRIMARY | DGB_BLOCK_VERSION_SCRYPT; +static constexpr int32_t SHA256D = PRIMARY | DGB_BLOCK_VERSION_SHA256D; +static constexpr int32_t SKEIN = PRIMARY | DGB_BLOCK_VERSION_SKEIN; +static constexpr int32_t ODO = PRIMARY | DGB_BLOCK_VERSION_ODO; + +// Build a version_at() over a fixed nearest-first chain. +static std::function chain(const std::vector& c) +{ + return [c](std::size_t k) { return c.at(k); }; +} + +TEST(DigishieldWalk, WorkCreditPredicateIsScryptOnly) +{ + EXPECT_TRUE (header_credits_work(SCRYPT)); + EXPECT_TRUE (header_credits_work(PRIMARY)); // bare primary == Scrypt + EXPECT_FALSE(header_credits_work(SHA256D)); + EXPECT_FALSE(header_credits_work(SKEIN)); + EXPECT_FALSE(header_credits_work(ODO)); +} + +TEST(DigishieldWalk, AllScryptChainTakesContiguousWindow) +{ + std::vector c(10, SCRYPT); + auto w = scrypt_window_ancestors(chain(c), c.size(), 4); + ASSERT_EQ(w.size(), 4u); + EXPECT_EQ(w, (std::vector{0, 1, 2, 3})); +} + +TEST(DigishieldWalk, SkipsInterleavedContinuityHeaders) +{ + // Mixed: S, sha, S, skein, S, odo, S -> Scrypt at k = 0,2,4,6. + std::vector c{SCRYPT, SHA256D, SCRYPT, SKEIN, SCRYPT, ODO, SCRYPT}; + auto w = scrypt_window_ancestors(chain(c), c.size(), 3); + ASSERT_EQ(w.size(), 3u); + EXPECT_EQ(w, (std::vector{0, 2, 4})); // continuity skipped +} + +TEST(DigishieldWalk, LeadingContinuityRunIsSkipped) +{ + // Nearest headers are all non-Scrypt; first Scrypt sample is deep. + std::vector c{SHA256D, SKEIN, ODO, SCRYPT, SCRYPT}; + auto w = scrypt_window_ancestors(chain(c), c.size(), 2); + ASSERT_EQ(w.size(), 2u); + EXPECT_EQ(w, (std::vector{3, 4})); +} + +TEST(DigishieldWalk, ExhaustedChainReturnsFewerThanWindow) +{ + // Only 2 Scrypt headers exist but the window wants 4 (early-chain region). + std::vector c{SCRYPT, SHA256D, SCRYPT, SHA256D}; + auto w = scrypt_window_ancestors(chain(c), c.size(), 4); + ASSERT_EQ(w.size(), 2u); + EXPECT_EQ(w, (std::vector{0, 2})); +} + +TEST(DigishieldWalk, ZeroWindowIsEmpty) +{ + std::vector c(4, SCRYPT); + EXPECT_TRUE(scrypt_window_ancestors(chain(c), c.size(), 0).empty()); +} From 25822a5c9f79b1cd6b81cf9107819cc6503b5c5d Mon Sep 17 00:00:00 2001 From: frstrtr Date: Thu, 18 Jun 2026 09:19:24 +0000 Subject: [PATCH 2/8] =?UTF-8?q?dgb:=20M3=20=C2=A77b=20HeaderChain=20Scrypt?= =?UTF-8?q?-only=20validate()=20+=20retarget-window=20body?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace the header_chain.hpp namespace stub with the validate() body that drives the Scrypt-only chain: per-header disposition (Scrypt validate / accept-by-continuity / reject), work-neutral accounting, and the Scrypt-only DigiShield retarget-window assembly (averaged target + actual timespan). Work-neutrality SSOT: cumulative work advances iff header_credits_work(), the same predicate scrypt_window_ancestors() consults for the retarget window, so the work-accounting and retarget paths cannot drift. Continuity headers extend the chain but credit zero work and are excluded from the retarget window. THIRD INVARIANT under test: a continuity header inside the nominal window is proven skipped from both avg_target and actual_timespan (a naive all-headers window would corrupt both). The damped DigiShield/MultiShield arith_uint256 multiply layers on RetargetWindow in the following slice. Guard header_chain_test wired into the ctest foreach AND both build.yml --target allowlists (the #143 NOT_BUILT lesson). 6/6 PASS, build EXIT=0. --- .github/workflows/build.yml | 4 +- src/impl/dgb/coin/header_chain.hpp | 159 ++++++++++++++++++++++-- src/impl/dgb/test/CMakeLists.txt | 2 +- src/impl/dgb/test/header_chain_test.cpp | 125 +++++++++++++++++++ 4 files changed, 276 insertions(+), 14 deletions(-) create mode 100644 src/impl/dgb/test/header_chain_test.cpp diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index ff5d5e765..21641b3a0 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -68,7 +68,7 @@ jobs: test_mweb_builder \ test_address_resolution test_compute_share_target \ test_utxo test_dgb_subsidy dgb_share_test \ - rpc_request_test softfork_check_test genesis_check_test algo_select_test digishield_walk_test \ + rpc_request_test softfork_check_test genesis_check_test algo_select_test digishield_walk_test header_chain_test \ v37_test \ -j$(nproc) @@ -196,7 +196,7 @@ jobs: test_mweb_builder \ test_address_resolution test_compute_share_target \ test_utxo test_dgb_subsidy dgb_share_test \ - rpc_request_test softfork_check_test genesis_check_test algo_select_test digishield_walk_test \ + rpc_request_test softfork_check_test genesis_check_test algo_select_test digishield_walk_test header_chain_test \ test_coin_broadcaster test_multiaddress_pplns test_pplns_stress \ v37_test \ -j$(nproc) diff --git a/src/impl/dgb/coin/header_chain.hpp b/src/impl/dgb/coin/header_chain.hpp index cdf2bb4b6..d7815c04f 100644 --- a/src/impl/dgb/coin/header_chain.hpp +++ b/src/impl/dgb/coin/header_chain.hpp @@ -28,14 +28,151 @@ // // >>> DIGISHIELD INSERTION POINT (M1 §2) <<< // DGB uses DigiShield/MultiShield per-algo difficulty retarget, NOT BTC's -// 2016-block retarget. TODO(M3): port per-algo DigiShield target calc for the -// Scrypt algo lane only. -// TODO(M3): the DigiShield difficulty window MUST walk Scrypt-algo ancestors -// ONLY — never the interleaved multi-algo header chain. On a mixed-algo chain -// the previous-block / window walk must skip non-Scrypt (continuity) headers; -// folding them into the window corrupts the Scrypt retarget and re-introduces -// the work-neutrality break above. Easy to get wrong on a mixed-algo chain. The Scrypt-only -// ancestor selection for that window is implemented + guarded in -// coin/dgb_digishield.hpp (scrypt_window_ancestors + header_credits_work); -// the per-algo target math layers on top of it. -namespace c2pool::dgb { /* TODO(M3): HeaderChain w/ Scrypt-only validate() + accept-by-continuity */ } +// 2016-block retarget. The Scrypt-only ancestor selection for that window is +// implemented + guarded in coin/dgb_digishield.hpp (scrypt_window_ancestors + +// header_credits_work). THIS header lands the validate() body that drives the +// chain: per-header disposition (Scrypt validate / continuity / reject), +// work-neutral accounting, and the Scrypt-only retarget-window ASSEMBLY +// (averaged target + actual timespan over the Scrypt samples). The damped +// DigiShield/MultiShield multiply (bnNew = avg * f(timespan)/target_timespan, +// arith_uint256, clamp + adjustment) layers on top of RetargetWindow in the +// following slice — its inputs are pinned here so the multiply cannot reach a +// continuity header. + +#include +#include +#include +#include + +#include +#include + +namespace c2pool::dgb { + +using ::dgb::coin::DgbAlgo; +using ::dgb::coin::HeaderDisposition; +using ::dgb::coin::dgb_header_disposition; +using ::dgb::coin::header_credits_work; +using ::dgb::coin::is_scrypt_header; +using ::dgb::coin::scrypt_window_ancestors; + +// Minimal header sample the Scrypt-only retarget body consumes. The embedded +// DigiByte Core port (M3+) carries the full CBlockHeader; the validate() + +// retarget path needs only the algo bits (disposition + work-credit), the +// timestamp (timespan), and the expanded difficulty target (averaging). +// `target` is a fixed-width proxy for the standalone CI guard; the per-algo +// DigiShield slice swaps arith_uint256 in with the SAME field shape, so the +// averaging/timespan assembly below is unchanged when the real width lands. +struct HeaderSample { + int32_t n_version = 0; + int64_t n_time = 0; + uint64_t target = 0; // expanded PoW target (smaller == more work) +}; + +// Outcome of validating + ingesting one header. +enum class IngestResult { + VALIDATED_SCRYPT, // Scrypt header, PoW-validated, work credited + ACCEPTED_CONTINUITY, // known non-Scrypt, appended, ZERO work credited + REJECTED, // unknown algo bits, or malformed Scrypt header +}; + +// Inputs the DigiShield/MultiShield damped multiply consumes. Assembled over +// SCRYPT ancestors ONLY — a continuity header can never reach `avg_target` or +// `actual_timespan`, which is precisely the corruption the THIRD INVARIANT and +// the DIGISHIELD INSERTION POINT warn against. +struct RetargetWindow { + std::size_t scrypt_samples = 0; // Scrypt headers folded in (<= window) + uint64_t avg_target = 0; // mean expanded target over samples + int64_t actual_timespan = 0; // newest - oldest Scrypt sample time + bool sufficient = false; // true iff the full window was found +}; + +// Work-neutral header chain with a Scrypt-only DigiShield retarget body. +// Append order is oldest..newest; the retarget walk reads nearest-first. +class HeaderChain { +public: + // Validate one header and, unless rejected, append it to the chain. + // Work-neutrality SSOT: cumulative work advances iff header_credits_work() + // — the SAME predicate scrypt_window_ancestors() consults for the retarget + // window (header_credits_work forwards to is_scrypt_header). The two paths + // cannot drift: a header either credits work AND enters the window, or + // does neither. + IngestResult validate_and_append(const HeaderSample& h) + { + switch (dgb_header_disposition(h.n_version)) + { + case HeaderDisposition::REJECT: + return IngestResult::REJECTED; + + case HeaderDisposition::VALIDATE_SCRYPT: + // PoW validate path. A zero target is not a validatable Scrypt + // header (the embedded daemon's full scrypt(header) <= target + // check lands with the daemon port; the structural guard here is + // that a malformed target never credits work). + if (h.target == 0) + return IngestResult::REJECTED; + m_chain.push_back(h); + m_cumulative_work += work_from_target(h.target); // credited + return IngestResult::VALIDATED_SCRYPT; + + case HeaderDisposition::ACCEPT_BY_CONTINUITY: + default: + // Extends the header chain; work-neutral (NO m_cumulative_work + // change) and excluded from the retarget window by construction. + m_chain.push_back(h); + return IngestResult::ACCEPTED_CONTINUITY; + } + } + + // Assemble the Scrypt-only retarget window for the next block. Walks the + // SCRYPT ancestors of the tip (skipping continuity headers via the shared + // helper) and returns the averaged target + actual timespan the DigiShield + // multiply needs. `actual_timespan` spans the newest and oldest Scrypt + // samples only — continuity headers between them never widen it. + RetargetWindow next_retarget_window(std::size_t window) const + { + RetargetWindow rw; + if (window == 0 || m_chain.empty()) + return rw; + + const std::size_t depth = m_chain.size(); + // nearest-first view: k == 0 is the tip. + const auto version_at = [&](std::size_t k) { + return m_chain[depth - 1 - k].n_version; + }; + const std::vector idx = + scrypt_window_ancestors(version_at, depth, window); + if (idx.empty()) + return rw; + + unsigned __int128 target_sum = 0; + for (std::size_t k : idx) + target_sum += m_chain[depth - 1 - k].target; + + rw.scrypt_samples = idx.size(); + rw.avg_target = static_cast(target_sum / idx.size()); + // idx is nearest-first: front() == newest Scrypt sample (smallest k), + // back() == oldest. Timespan is over Scrypt samples exclusively. + rw.actual_timespan = m_chain[depth - 1 - idx.front()].n_time + - m_chain[depth - 1 - idx.back()].n_time; + rw.sufficient = (idx.size() == window); + return rw; + } + + uint64_t cumulative_work() const { return m_cumulative_work; } + std::size_t size() const { return m_chain.size(); } + +private: + // Work proxy: inversely proportional to the target (smaller target == more + // work), matching real PoW work accounting in shape. The per-algo slice + // swaps the arith_uint256 work() in; only Scrypt headers ever reach here. + static uint64_t work_from_target(uint64_t target) + { + return target == 0 ? 0 : (UINT64_MAX / target); + } + + std::vector m_chain; // oldest .. newest + uint64_t m_cumulative_work = 0; +}; + +} // namespace c2pool::dgb diff --git a/src/impl/dgb/test/CMakeLists.txt b/src/impl/dgb/test/CMakeLists.txt index 230c43c06..4446c52b3 100644 --- a/src/impl/dgb/test/CMakeLists.txt +++ b/src/impl/dgb/test/CMakeLists.txt @@ -25,7 +25,7 @@ if (BUILD_TESTING AND GTest_FOUND) # transport rpc.cpp wiring is deferred (Option-B scope). Each MUST appear # in BOTH this ctest registration AND the build.yml --target allowlist, # or it becomes a #143-style NOT_BUILT sentinel that reds master. - foreach(dgb_guard rpc_request_test softfork_check_test genesis_check_test algo_select_test digishield_walk_test) + foreach(dgb_guard rpc_request_test softfork_check_test genesis_check_test algo_select_test digishield_walk_test header_chain_test) add_executable(${dgb_guard} ${dgb_guard}.cpp) target_link_libraries(${dgb_guard} PRIVATE GTest::gtest_main GTest::gtest nlohmann_json::nlohmann_json) diff --git a/src/impl/dgb/test/header_chain_test.cpp b/src/impl/dgb/test/header_chain_test.cpp new file mode 100644 index 000000000..a491c0601 --- /dev/null +++ b/src/impl/dgb/test/header_chain_test.cpp @@ -0,0 +1,125 @@ +// --------------------------------------------------------------------------- +// dgb M3 §7b HeaderChain Scrypt-only validate() + retarget-body guard. +// +// Pins the THIRD INVARIANT (coin/header_chain.hpp) at the validate() level: +// 1. Work-neutrality SSOT — only a Scrypt header credits cumulative work; a +// continuity (known non-Scrypt) header appends but is work-neutral; an +// unknown-algo or malformed header is rejected. The work-credit decision +// and the retarget-window inclusion go through ONE predicate +// (header_credits_work == is_scrypt_header) so they cannot drift. +// 2. Retarget continuity-skip — a continuity header sitting INSIDE the +// nominal window is excluded from the Scrypt target computation: it never +// reaches avg_target nor widens actual_timespan. A naive all-headers +// window would corrupt both; this proves it can't. +// +// Header-only guard: links the header-only chain helpers + gtest, no dgb +// OBJECT lib / transport. MUST appear in BOTH the test/CMakeLists foreach AND +// both build.yml --target allowlists, or it becomes a #143 NOT_BUILT sentinel. +// --------------------------------------------------------------------------- + +#include + +#include + +#include + +using namespace c2pool::dgb; +using dgb::coin::DGB_BLOCK_VERSION_SCRYPT; +using dgb::coin::DGB_BLOCK_VERSION_SHA256D; +using dgb::coin::DGB_BLOCK_VERSION_SKEIN; + +static constexpr int32_t PRIMARY = 2; // BLOCK_VERSION_DEFAULT +static constexpr int32_t SCRYPT = PRIMARY | DGB_BLOCK_VERSION_SCRYPT; +static constexpr int32_t SHA256D = PRIMARY | DGB_BLOCK_VERSION_SHA256D; +static constexpr int32_t SKEIN = PRIMARY | DGB_BLOCK_VERSION_SKEIN; +static constexpr int32_t UNKNOWN_ALGO = PRIMARY | (10 << 8); // not a known codepoint + +TEST(HeaderChainValidate, ScryptCreditsWorkContinuityIsNeutral) +{ + HeaderChain hc; + EXPECT_EQ(hc.validate_and_append({SCRYPT, 1000, 100}), + IngestResult::VALIDATED_SCRYPT); + const uint64_t after_one = hc.cumulative_work(); + EXPECT_GT(after_one, 0u); + + // Continuity header: appended, but ZERO work credited. + EXPECT_EQ(hc.validate_and_append({SHA256D, 1075, 999999}), + IngestResult::ACCEPTED_CONTINUITY); + EXPECT_EQ(hc.cumulative_work(), after_one); // work-neutral + EXPECT_EQ(hc.size(), 2u); // but chain extended +} + +TEST(HeaderChainValidate, RejectsUnknownAlgoAndMalformedScrypt) +{ + HeaderChain hc; + EXPECT_EQ(hc.validate_and_append({UNKNOWN_ALGO, 1000, 100}), + IngestResult::REJECTED); + EXPECT_EQ(hc.validate_and_append({SCRYPT, 1000, 0}), // zero target + IngestResult::REJECTED); + EXPECT_EQ(hc.size(), 0u); // neither appended + EXPECT_EQ(hc.cumulative_work(), 0u); // no work credited +} + +TEST(HeaderChainValidate, ContinuityHeaderInsideWindowSkippedFromTarget) +{ + // oldest..newest: S(100) , sha(cheap) , S(100) , S(100) + // The continuity header sits between two Scrypt samples, INSIDE a window=3. + HeaderChain hc; + ASSERT_EQ(hc.validate_and_append({SCRYPT, 1000, 100}), IngestResult::VALIDATED_SCRYPT); + ASSERT_EQ(hc.validate_and_append({SHA256D, 1075, 999999}), IngestResult::ACCEPTED_CONTINUITY); + ASSERT_EQ(hc.validate_and_append({SCRYPT, 1150, 100}), IngestResult::VALIDATED_SCRYPT); + ASSERT_EQ(hc.validate_and_append({SCRYPT, 1225, 100}), IngestResult::VALIDATED_SCRYPT); + + const RetargetWindow rw = hc.next_retarget_window(3); + + EXPECT_TRUE(rw.sufficient); + EXPECT_EQ(rw.scrypt_samples, 3u); + // Scrypt-only average: (100+100+100)/3 == 100. A naive all-headers window + // would fold the cheap 999999 target in and blow this up. + EXPECT_EQ(rw.avg_target, 100u); + // Timespan spans the newest..oldest SCRYPT samples (1225 - 1000 == 225), + // NOT the naive newest..3rd-back-header span (1225 - 1075 == 150). + EXPECT_EQ(rw.actual_timespan, 225); + EXPECT_NE(rw.actual_timespan, 150); + + // The continuity header was work-neutral: only the 3 Scrypt headers count. + EXPECT_EQ(hc.size(), 4u); + EXPECT_EQ(hc.cumulative_work(), 3u * (UINT64_MAX / 100)); +} + +TEST(HeaderChainValidate, AllScryptWindowIsContiguous) +{ + HeaderChain hc; + hc.validate_and_append({SCRYPT, 1000, 200}); + hc.validate_and_append({SCRYPT, 1100, 100}); + hc.validate_and_append({SCRYPT, 1200, 300}); + const RetargetWindow rw = hc.next_retarget_window(3); + EXPECT_TRUE(rw.sufficient); + EXPECT_EQ(rw.scrypt_samples, 3u); + EXPECT_EQ(rw.avg_target, (200u + 100u + 300u) / 3u); + EXPECT_EQ(rw.actual_timespan, 200); // 1200 - 1000 +} + +TEST(HeaderChainValidate, InsufficientWindowFlaggedNotSufficient) +{ + // Only 2 Scrypt samples exist; a window of 4 is under-filled (early chain). + HeaderChain hc; + hc.validate_and_append({SCRYPT, 1000, 100}); + hc.validate_and_append({SKEIN, 1050, 555}); // continuity + hc.validate_and_append({SCRYPT, 1100, 100}); + const RetargetWindow rw = hc.next_retarget_window(4); + EXPECT_FALSE(rw.sufficient); + EXPECT_EQ(rw.scrypt_samples, 2u); + EXPECT_EQ(rw.avg_target, 100u); + EXPECT_EQ(rw.actual_timespan, 100); // 1100 - 1000, Scrypt-only +} + +TEST(HeaderChainValidate, EmptyChainAndZeroWindowAreSafe) +{ + HeaderChain hc; + EXPECT_FALSE(hc.next_retarget_window(3).sufficient); // empty chain + hc.validate_and_append({SCRYPT, 1000, 100}); + const RetargetWindow rw = hc.next_retarget_window(0); // zero window + EXPECT_FALSE(rw.sufficient); + EXPECT_EQ(rw.scrypt_samples, 0u); +} From 587d9f19cc2623ae0a478405032b9cce7381bcd5 Mon Sep 17 00:00:00 2001 From: frstrtr Date: Thu, 18 Jun 2026 09:39:02 +0000 Subject: [PATCH 3/8] =?UTF-8?q?dgb:=20M3=20=C2=A77b=20per-algo=20DigiShiel?= =?UTF-8?q?d=20damped=20retarget=20multiply?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit digishield_next_target() + DigiShieldParams in coin/header_chain.hpp: consumes the Scrypt-only RetargetWindow (avg_target + actual_timespan over Scrypt ancestors only) and produces the next expanded target via DigiByte DigiShield v3: amplitude filter damped = nominal + (actual-nominal)/8, clamp to [3/4, 3/2] nominal rails, bnNew = avg*damped/nominal, capped to pow_limit. __int128 intermediate (proxy-overflow safe); arith_uint256 swaps in later with the same field shape. Returns 0 on empty window / degenerate nominal so the caller keeps the prior target. header_chain_test: +6 DigiShield cases -- nominal (target unchanged), both clamp rails (ceiling 3/2, floor 3/4 via negative timespan), pow_limit floor, empty-window/zero-nominal safety, and end-to-end over the live continuity- skipped window. 12/12 pass, build EXIT=0. Header-only; reuses the already- wired header_chain_test guard (no new ctest/build.yml target). --- src/impl/dgb/coin/header_chain.hpp | 66 +++++++++++++++++++++ src/impl/dgb/test/header_chain_test.cpp | 76 +++++++++++++++++++++++++ 2 files changed, 142 insertions(+) diff --git a/src/impl/dgb/coin/header_chain.hpp b/src/impl/dgb/coin/header_chain.hpp index d7815c04f..ff7a9bbb7 100644 --- a/src/impl/dgb/coin/header_chain.hpp +++ b/src/impl/dgb/coin/header_chain.hpp @@ -87,6 +87,72 @@ struct RetargetWindow { bool sufficient = false; // true iff the full window was found }; +// DigiShield/MultiShield retarget parameters (per-algo). `target_timespan` is +// the nominal per-block Scrypt solve time the damped filter pulls toward; +// `pow_limit` is the easiest (largest) admissible target -- bnNew is capped to +// it so a long stall can never relax difficulty past the network minimum. +struct DigiShieldParams { + int64_t target_timespan = 0; // nominal solve time the filter centers on + uint64_t pow_limit = 0; // max (easiest) target; result never exceeds +}; + +// Per-algo DigiShield/MultiShield damped retarget multiply (M3 7b). +// +// Consumes the Scrypt-only RetargetWindow (averaged target + actual timespan, +// both assembled over SCRYPT ancestors EXCLUSIVELY in next_retarget_window) and +// produces the next block's expanded target. Pins DigiByte Core's DigiShield v3 +// amplitude filter: +// +// damped = target_timespan + (actual_timespan - target_timespan) / 8 +// clamp damped into [target_timespan*3/4, target_timespan*3/2] +// bnNew = avg_target * damped / target_timespan +// bnNew = min(bnNew, pow_limit) +// +// The /8 filter damps a single block's deviation; the clamp rails bound the +// per-step move so a manipulated (or non-monotonic) timestamp run cannot swing +// difficulty arbitrarily in one retarget. The lower rail is reachable only when +// actual_timespan goes sharply negative (out-of-order block times); the upper +// rail trips once actual_timespan exceeds ~5x nominal. +// +// `target` here is the fixed-width uint64 proxy the standalone CI guard uses; +// the embedded-daemon port swaps arith_uint256 in with the SAME field shape, so +// this multiply/clamp shape is unchanged when the real 256-bit width lands. The +// intermediate uses __int128 so avg_target * damped cannot overflow the proxy. +// +// Returns 0 when the window carries no Scrypt samples -- the caller MUST keep +// the prior target rather than retarget off an empty window (early/genesis). +inline uint64_t digishield_next_target(const RetargetWindow& rw, + const DigiShieldParams& params) +{ + if (rw.scrypt_samples == 0 || params.target_timespan <= 0) + return 0; + + const int64_t nominal = params.target_timespan; + + // Amplitude filter: pull the observed timespan 1/8 of the way off nominal. + int64_t damped = nominal + (rw.actual_timespan - nominal) / 8; + + // Per-step clamp rails: [3/4 nominal, 3/2 nominal]. + const int64_t floor_ts = nominal - nominal / 4; + const int64_t ceil_ts = nominal + nominal / 2; + if (damped < floor_ts) damped = floor_ts; + if (damped > ceil_ts) damped = ceil_ts; + + // bnNew = avg_target * damped / nominal, __int128 to avoid proxy overflow. + // damped is strictly positive after the clamp (floor_ts > 0 for nominal>0). + unsigned __int128 bn_new = + (static_cast(rw.avg_target) * + static_cast(static_cast(damped))) + / static_cast(static_cast(nominal)); + + // Difficulty floor: never relax past the network's easiest target. + if (params.pow_limit != 0 && + bn_new > static_cast(params.pow_limit)) + return params.pow_limit; + + return static_cast(bn_new); +} + // Work-neutral header chain with a Scrypt-only DigiShield retarget body. // Append order is oldest..newest; the retarget walk reads nearest-first. class HeaderChain { diff --git a/src/impl/dgb/test/header_chain_test.cpp b/src/impl/dgb/test/header_chain_test.cpp index a491c0601..4376cf8f6 100644 --- a/src/impl/dgb/test/header_chain_test.cpp +++ b/src/impl/dgb/test/header_chain_test.cpp @@ -123,3 +123,79 @@ TEST(HeaderChainValidate, EmptyChainAndZeroWindowAreSafe) EXPECT_FALSE(rw.sufficient); EXPECT_EQ(rw.scrypt_samples, 0u); } + + +// --------------------------------------------------------------------------- +// DigiShield/MultiShield damped retarget multiply (digishield_next_target). +// Exercises the amplitude filter + clamp at BOTH rails plus the nominal case, +// and the pow_limit difficulty floor. Inputs are hand-built RetargetWindows so +// the multiply is pinned independently of the (already-tested) Scrypt-only +// window assembly above. +// +// nominal target_timespan = 60 -> floor = 60 - 60/4 = 45, +// ceil = 60 + 60/2 = 90. +// --------------------------------------------------------------------------- +static RetargetWindow make_window(uint64_t avg_target, int64_t actual_timespan) +{ + RetargetWindow rw; + rw.scrypt_samples = 3; + rw.avg_target = avg_target; + rw.actual_timespan = actual_timespan; + rw.sufficient = true; + return rw; +} + +TEST(DigiShieldRetarget, NominalTimespanLeavesTargetUnchanged) +{ + // actual == nominal -> damped == nominal -> bnNew == avg_target. + const DigiShieldParams p{60, 0}; + EXPECT_EQ(digishield_next_target(make_window(1000, 60), p), 1000u); +} + +TEST(DigiShieldRetarget, CeilingRailCapsTheEasing) +{ + // actual far above nominal: damped = 60 + (1000-60)/8 = 177 -> clamps to 90. + // bnNew = 1000 * 90 / 60 = 1500 (target relaxes by exactly the 3/2 rail). + const DigiShieldParams p{60, 0}; + EXPECT_EQ(digishield_next_target(make_window(1000, 1000), p), 1500u); +} + +TEST(DigiShieldRetarget, FloorRailCapsTheTightening) +{ + // Sharply negative timespan (out-of-order block times): damped goes well + // below floor -> clamps to 45. bnNew = 1000 * 45 / 60 = 750 (3/4 rail). + const DigiShieldParams p{60, 0}; + EXPECT_EQ(digishield_next_target(make_window(1000, -1000), p), 750u); +} + +TEST(DigiShieldRetarget, PowLimitFloorsTheTarget) +{ + // Ceiling rail would yield 1500, but pow_limit (easiest target) caps it. + const DigiShieldParams p{60, 1200}; + EXPECT_EQ(digishield_next_target(make_window(1000, 1000), p), 1200u); +} + +TEST(DigiShieldRetarget, EmptyWindowKeepsPriorTarget) +{ + // No Scrypt samples -> 0 sentinel: caller keeps the prior target. + RetargetWindow rw; // scrypt_samples == 0 + const DigiShieldParams p{60, 0}; + EXPECT_EQ(digishield_next_target(rw, p), 0u); + // Degenerate nominal is also rejected (no divide-by-zero). + EXPECT_EQ(digishield_next_target(make_window(1000, 60), DigiShieldParams{0, 0}), 0u); +} + +TEST(DigiShieldRetarget, ConsumesLiveScryptOnlyWindow) +{ + // End-to-end: the same continuity-skipped window the validate() path builds + // (avg 100, timespan 225 over 3 Scrypt samples) feeds the multiply. With + // nominal 225 the damped value is exactly nominal -> target unchanged at 100. + HeaderChain hc; + hc.validate_and_append({SCRYPT, 1000, 100}); + hc.validate_and_append({SHA256D, 1075, 999999}); // continuity, skipped + hc.validate_and_append({SCRYPT, 1150, 100}); + hc.validate_and_append({SCRYPT, 1225, 100}); + const RetargetWindow rw = hc.next_retarget_window(3); + ASSERT_TRUE(rw.sufficient); + EXPECT_EQ(digishield_next_target(rw, DigiShieldParams{225, 0}), 100u); +} From f524643ae7585d64e00cc4501709e1fdc724843b Mon Sep 17 00:00:00 2001 From: frstrtr Date: Thu, 18 Jun 2026 09:58:57 +0000 Subject: [PATCH 4/8] =?UTF-8?q?dgb:=20M3=20=C2=A77b=20wire=20DigiShield=20?= =?UTF-8?q?next-target=20into=20HeaderChain=20ingest=20gate?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit validate_and_append now enforces the DigiShield-computed next target on the VALIDATE_SCRYPT path: the declared Scrypt target must EQUAL the next-target computed over the Scrypt-only retarget window ending at the current tip (assembled before the header is appended), the nBits-style consensus rule. A configured HeaderChain(DigiShieldParams, window) arms the gate; a default- constructed chain leaves target_timespan 0 so digishield_next_target() returns the 0 sentinel and the gate is a no-op (existing retarget/work-accounting tests run unconstrained, unchanged). A rejected header never mutates the chain (size + cumulative work preserved). +1 ingest-gate test vector (window 1, deterministic 7/8 next-target): wrong target REJECTED with chain unchanged, exact target VALIDATED_SCRYPT crediting work, continuity header bypasses the gate. Added INTO header_chain_test (no new gtest target -> #143 NOT_BUILT trap avoided). 13/13 PASS, build EXIT=0. --- src/impl/dgb/coin/header_chain.hpp | 30 +++++++++++++++++++ src/impl/dgb/test/header_chain_test.cpp | 38 +++++++++++++++++++++++++ 2 files changed, 68 insertions(+) diff --git a/src/impl/dgb/coin/header_chain.hpp b/src/impl/dgb/coin/header_chain.hpp index ff7a9bbb7..ed7d09b9e 100644 --- a/src/impl/dgb/coin/header_chain.hpp +++ b/src/impl/dgb/coin/header_chain.hpp @@ -157,6 +157,17 @@ inline uint64_t digishield_next_target(const RetargetWindow& rw, // Append order is oldest..newest; the retarget walk reads nearest-first. class HeaderChain { public: + HeaderChain() = default; + + // Configure the consensus retarget gate: the DigiShield params + the + // Scrypt-only window depth validate_and_append enforces declared targets + // against. Default-constructed chains leave target_timespan == 0, which + // makes digishield_next_target() return 0 and the gate a no-op -- the + // retarget-window/work-accounting tests run unconstrained, exactly as + // before this slice. + HeaderChain(DigiShieldParams ds, std::size_t retarget_window) + : m_ds_params(ds), m_retarget_window(retarget_window) {} + // Validate one header and, unless rejected, append it to the chain. // Work-neutrality SSOT: cumulative work advances iff header_credits_work() // — the SAME predicate scrypt_window_ancestors() consults for the retarget @@ -171,15 +182,32 @@ class HeaderChain { return IngestResult::REJECTED; case HeaderDisposition::VALIDATE_SCRYPT: + { // PoW validate path. A zero target is not a validatable Scrypt // header (the embedded daemon's full scrypt(header) <= target // check lands with the daemon port; the structural guard here is // that a malformed target never credits work). if (h.target == 0) return IngestResult::REJECTED; + + // Consensus retarget gate: the declared target must equal the + // DigiShield next-target computed over the Scrypt-only window + // ending at the CURRENT tip (assembled BEFORE h is appended). + // expected == 0 means no Scrypt ancestor is in range yet + // (genesis/bootstrap) or the gate is unconfigured -- no retarget + // constraint is enforceable, so the header passes on its + // structural (non-zero) target alone. nBits-style exact match + // mirrors Bitcoin/DigiByte consensus: the header carries the + // required next-work value, not merely a target it satisfies. + const RetargetWindow rw = next_retarget_window(m_retarget_window); + const uint64_t expected = digishield_next_target(rw, m_ds_params); + if (expected != 0 && h.target != expected) + return IngestResult::REJECTED; + m_chain.push_back(h); m_cumulative_work += work_from_target(h.target); // credited return IngestResult::VALIDATED_SCRYPT; + } case HeaderDisposition::ACCEPT_BY_CONTINUITY: default: @@ -237,6 +265,8 @@ class HeaderChain { return target == 0 ? 0 : (UINT64_MAX / target); } + DigiShieldParams m_ds_params{}; // retarget gate params + std::size_t m_retarget_window = 0; // Scrypt window depth std::vector m_chain; // oldest .. newest uint64_t m_cumulative_work = 0; }; diff --git a/src/impl/dgb/test/header_chain_test.cpp b/src/impl/dgb/test/header_chain_test.cpp index 4376cf8f6..b09b05741 100644 --- a/src/impl/dgb/test/header_chain_test.cpp +++ b/src/impl/dgb/test/header_chain_test.cpp @@ -199,3 +199,41 @@ TEST(DigiShieldRetarget, ConsumesLiveScryptOnlyWindow) ASSERT_TRUE(rw.sufficient); EXPECT_EQ(digishield_next_target(rw, DigiShieldParams{225, 0}), 100u); } + +// --------------------------------------------------------------------------- +// Ingest-path retarget gate: validate_and_append must demand the declared +// Scrypt target EQUAL the DigiShield next-target computed over the Scrypt-only +// window ending at the current tip (nBits-style exact consensus match). A +// default-constructed chain leaves the gate unconfigured (target_timespan 0 -> +// expected 0), so every test above runs unconstrained; this one configures it. +// +// window depth 1, nominal 80. Window(1) over a lone Scrypt tip has +// actual_timespan 0 (front == back) -> damped = 80 + (0-80)/8 = 70, above the +// floor rail 60 -> next target = avg * 70/80 = avg * 7/8, deterministically. +// --------------------------------------------------------------------------- +TEST(HeaderChainValidate, IngestGateEnforcesDigiShieldNextTarget) +{ + HeaderChain hc(DigiShieldParams{80, 0}, /*retarget_window=*/1); + + // Seed: empty window -> gate no-op, any non-zero target seeds the chain. + ASSERT_EQ(hc.validate_and_append({SCRYPT, 1000, 4096}), + IngestResult::VALIDATED_SCRYPT); + + // Required next target = 4096 * 7/8 = 3584. A mismatch is consensus-invalid + // and must REJECT without mutating the chain (size + work unchanged). + const std::size_t size_before = hc.size(); + const uint64_t work_before = hc.cumulative_work(); + EXPECT_EQ(hc.validate_and_append({SCRYPT, 1080, 4096}), + IngestResult::REJECTED); + EXPECT_EQ(hc.size(), size_before); + EXPECT_EQ(hc.cumulative_work(), work_before); + + // The exact DigiShield target is accepted and credits work. + EXPECT_EQ(hc.validate_and_append({SCRYPT, 1080, 3584}), + IngestResult::VALIDATED_SCRYPT); + EXPECT_GT(hc.cumulative_work(), work_before); + + // Continuity headers bypass the retarget gate (work-neutral, no nBits). + EXPECT_EQ(hc.validate_and_append({SHA256D, 1090, 1}), + IngestResult::ACCEPTED_CONTINUITY); +} From ca21fdd125152c2106a0ed671a84172fca119881 Mon Sep 17 00:00:00 2001 From: frstrtr Date: Thu, 18 Jun 2026 10:22:45 +0000 Subject: [PATCH 5/8] =?UTF-8?q?dgb:=20M3=20=C2=A77b=20DigiShield=20damped?= =?UTF-8?q?=20multiply=20at=20true=20256-bit=20width?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Swap the uint64/__int128 retarget intermediate for a header-only 256-bit unsigned (coin/dgb_arith256.hpp) reproducing arith_uint256 multiply-then- divide with 256-bit overflow truncation -- the consensus arithmetic of DigiByte Core CalculateNextWorkRequired. For uint64-range avg_target the path is bit-identical to the prior intermediate, so the existing 13 vectors are unchanged; +5 boundary vectors prove the divergence above 2^64 and the 256-bit truncation near pow_limit that an __int128 proxy could not reproduce. Header-only, single-coin (src/impl/dgb only), tests added into header_chain_test (no new gtest target). --- src/impl/dgb/coin/dgb_arith256.hpp | 91 ++++++++++++++++++++++++ src/impl/dgb/coin/header_chain.hpp | 35 ++++++---- src/impl/dgb/test/header_chain_test.cpp | 93 +++++++++++++++++++++++++ 3 files changed, 207 insertions(+), 12 deletions(-) create mode 100644 src/impl/dgb/coin/dgb_arith256.hpp diff --git a/src/impl/dgb/coin/dgb_arith256.hpp b/src/impl/dgb/coin/dgb_arith256.hpp new file mode 100644 index 000000000..8f93934e6 --- /dev/null +++ b/src/impl/dgb/coin/dgb_arith256.hpp @@ -0,0 +1,91 @@ +#pragma once +// --------------------------------------------------------------------------- +// DGB DigiShield retarget arithmetic at TRUE 256-bit width (M3 §7b). +// +// The DigiShield/MultiShield damped multiply in coin/header_chain.hpp pins +// DigiByte Core CalculateNextWorkRequired: +// +// bnNew = avg_target (arith_uint256) +// bnNew *= damped_timespan <-- 256-bit multiply, OVERFLOW TRUNCATES +// bnNew /= nominal_timespan +// +// Earlier §7b slices used an unsigned __int128 proxy: correct ONLY while the +// expanded target fits 64 bits. A real DigiByte Scrypt target is a full +// arith_uint256 (pow_limit ~2^224); near it, avg_target * damped exceeds +// 2^256 and arith_uint256 DROPS the overflow limb. That truncation is +// CONSENSUS behaviour — a wider (overflow-safe) intermediate would compute a +// DIFFERENT next target and diverge from DigiByte Core / p2pool-merged-v36. +// +// This header is the minimal 256-bit unsigned that reproduces that exact +// truncation, isolated + separately guarded like dgb_digishield.hpp. It is +// header-only and depends ONLY on , so it links into the standalone +// GTest guard (no dgb OBJECT lib, no bitcoin arith_uint256 link) — the same +// constraint algo_select / header_chain tests run under. When the embedded +// DigiByte Core port lands, real arith_uint256 drops in with this same +// multiply-then-divide ordering and the boundary vectors carry over. +// --------------------------------------------------------------------------- + +#include + +namespace dgb::coin { + +// 256-bit unsigned, little-endian limbs (limb[0] least significant). Only the +// operations the DigiShield damped multiply needs: scale-by-u64 (truncating at +// 256 bits exactly as arith_uint256::operator*=), divide-by-u64, and compare. +struct u256 { + uint64_t limb[4] = {0, 0, 0, 0}; + + static u256 from_u64(uint64_t v) { u256 r; r.limb[0] = v; return r; } + + bool fits_u64() const { return limb[1] == 0 && limb[2] == 0 && limb[3] == 0; } + bool is_zero() const { return limb[0] == 0 && limb[1] == 0 && limb[2] == 0 && limb[3] == 0; } + uint64_t low64() const { return limb[0]; } + + // Multiply by a 64-bit scalar. Carry past the top limb is DISCARDED, i.e. + // the result is truncated to 256 bits — byte-for-byte what arith_uint256 + // does. This is the divergence point vs a 128/wider proxy. + u256 mul_u64(uint64_t m) const { + u256 r; + unsigned __int128 carry = 0; + for (int i = 0; i < 4; ++i) { + unsigned __int128 prod = (unsigned __int128)limb[i] * m + carry; + r.limb[i] = (uint64_t)prod; + carry = prod >> 64; + } + // carry beyond limb[3] dropped -> 256-bit truncation (consensus). + return r; + } + + // Divide by a 64-bit scalar (d must be non-zero), truncating toward zero. + // Schoolbook long division from the most-significant limb down. + u256 div_u64(uint64_t d) const { + u256 r; + unsigned __int128 rem = 0; + for (int i = 3; i >= 0; --i) { + unsigned __int128 cur = (rem << 64) | limb[i]; + r.limb[i] = (uint64_t)(cur / d); + rem = cur % d; + } + return r; + } + + friend bool operator<(const u256& a, const u256& b) { + for (int i = 3; i >= 0; --i) + if (a.limb[i] != b.limb[i]) return a.limb[i] < b.limb[i]; + return false; + } + friend bool operator>(const u256& a, const u256& b) { return b < a; } + friend bool operator==(const u256& a, const u256& b) { + return a.limb[0] == b.limb[0] && a.limb[1] == b.limb[1] + && a.limb[2] == b.limb[2] && a.limb[3] == b.limb[3]; + } +}; + +// avg * mul / div at full 256-bit width, in DigiByte Core ordering: MULTIPLY +// FIRST (overflow truncates at 256 bits), THEN divide. Reordering to divide +// first would lose low-order precision and is NOT what consensus computes. +inline u256 mul_div_u256(const u256& avg, uint64_t mul, uint64_t div) { + return avg.mul_u64(mul).div_u64(div); +} + +} // namespace dgb::coin diff --git a/src/impl/dgb/coin/header_chain.hpp b/src/impl/dgb/coin/header_chain.hpp index ed7d09b9e..0c33d3849 100644 --- a/src/impl/dgb/coin/header_chain.hpp +++ b/src/impl/dgb/coin/header_chain.hpp @@ -44,6 +44,7 @@ #include #include +#include #include #include @@ -54,7 +55,9 @@ using ::dgb::coin::HeaderDisposition; using ::dgb::coin::dgb_header_disposition; using ::dgb::coin::header_credits_work; using ::dgb::coin::is_scrypt_header; +using ::dgb::coin::mul_div_u256; using ::dgb::coin::scrypt_window_ancestors; +using ::dgb::coin::u256; // Minimal header sample the Scrypt-only retarget body consumes. The embedded // DigiByte Core port (M3+) carries the full CBlockHeader; the validate() + @@ -114,10 +117,15 @@ struct DigiShieldParams { // actual_timespan goes sharply negative (out-of-order block times); the upper // rail trips once actual_timespan exceeds ~5x nominal. // -// `target` here is the fixed-width uint64 proxy the standalone CI guard uses; -// the embedded-daemon port swaps arith_uint256 in with the SAME field shape, so -// this multiply/clamp shape is unchanged when the real 256-bit width lands. The -// intermediate uses __int128 so avg_target * damped cannot overflow the proxy. +// The damped-multiply intermediate runs at TRUE 256-bit width via mul_div_u256 +// (coin/dgb_arith256.hpp): arith_uint256 multiply-then-divide with 256-bit +// overflow truncation -- the consensus behaviour DigiByte Core exhibits near +// pow_limit, which an __int128 proxy could NOT reproduce (it would compute a +// wider, divergent next target). `target` here is still the uint64 field proxy +// the standalone CI guard uses; for any uint64-range avg_target the 256-bit path +// is bit-identical to the prior intermediate, so existing vectors are unchanged. +// The embedded-daemon port swaps real arith_uint256 into the field shape with +// this exact multiply/clamp ordering. // // Returns 0 when the window carries no Scrypt samples -- the caller MUST keep // the prior target rather than retarget off an empty window (early/genesis). @@ -138,19 +146,22 @@ inline uint64_t digishield_next_target(const RetargetWindow& rw, if (damped < floor_ts) damped = floor_ts; if (damped > ceil_ts) damped = ceil_ts; - // bnNew = avg_target * damped / nominal, __int128 to avoid proxy overflow. - // damped is strictly positive after the clamp (floor_ts > 0 for nominal>0). - unsigned __int128 bn_new = - (static_cast(rw.avg_target) * - static_cast(static_cast(damped))) - / static_cast(static_cast(nominal)); + // bnNew = avg_target * damped / nominal at full 256-bit width. damped is + // strictly positive after the clamp (floor_ts > 0 for nominal > 0). The + // multiply truncates at 256 bits exactly as arith_uint256 does, so a target + // near pow_limit retargets to the SAME value DigiByte Core computes. + const u256 bn_new = mul_div_u256(u256::from_u64(rw.avg_target), + static_cast(damped), + static_cast(nominal)); // Difficulty floor: never relax past the network's easiest target. if (params.pow_limit != 0 && - bn_new > static_cast(params.pow_limit)) + bn_new > u256::from_u64(params.pow_limit)) return params.pow_limit; - return static_cast(bn_new); + // uint64 field proxy: a uint64-range avg keeps bn_new within 64 bits, so + // low64() is exact (the embedded port returns the full arith_uint256). + return bn_new.low64(); } // Work-neutral header chain with a Scrypt-only DigiShield retarget body. diff --git a/src/impl/dgb/test/header_chain_test.cpp b/src/impl/dgb/test/header_chain_test.cpp index b09b05741..f14a7c7d4 100644 --- a/src/impl/dgb/test/header_chain_test.cpp +++ b/src/impl/dgb/test/header_chain_test.cpp @@ -237,3 +237,96 @@ TEST(HeaderChainValidate, IngestGateEnforcesDigiShieldNextTarget) EXPECT_EQ(hc.validate_and_append({SHA256D, 1090, 1}), IngestResult::ACCEPTED_CONTINUITY); } + + +// --------------------------------------------------------------------------- +// 256-BIT BOUNDARY (M3 §7b — arith_uint256 swap). Proves, not assumes, that +// the DigiShield damped multiply runs at TRUE 256-bit width: a uint64/__int128 +// proxy and the full-width path DIVERGE once avg_target leaves the 64-bit range +// or the product overflows 256 bits near pow_limit. mul_div_u256 reproduces +// arith_uint256 multiply-then-divide with 256-bit overflow truncation, which is +// CONSENSUS behaviour in DigiByte Core CalculateNextWorkRequired. +// --------------------------------------------------------------------------- +#include +using dgb::coin::u256; +using dgb::coin::mul_div_u256; + +static constexpr uint64_t BIT63 = 0x8000000000000000ull; // 2^63 + +TEST(DigiShield256, Uint64RangeIsBitIdenticalToProxy) +{ + // For every avg in 64-bit range the 256-bit path equals the old __int128 + // proxy (a*m)/d exactly — this is the no-regression guard for the swap. + const uint64_t cases[][3] = { + {1000, 60, 60}, // nominal -> 1000 + {1000, 90, 60}, // ceiling rail-> 1500 + {1000, 45, 60}, // floor rail -> 750 + {4096, 70, 80}, // window-1 7/8-> 3584 + {999999999ull, 90, 60}, + }; + for (auto& c : cases) { + const u256 r = mul_div_u256(u256::from_u64(c[0]), c[1], c[2]); + ASSERT_TRUE(r.fits_u64()); + const unsigned __int128 proxy = + ((unsigned __int128)c[0] * c[1]) / c[2]; + EXPECT_EQ(r.low64(), (uint64_t)proxy); + } +} + +TEST(DigiShield256, FullWidthAvgDivergesFromUint64Proxy) +{ + // avg_target = 2^64 (limb[1]=1, limb[0]=0): a value the uint64 proxy CANNOT + // hold — it sees only low64()==0. Full width: 2^64 * 3/2 = 3*2^63. + u256 avg; avg.limb[1] = 1; avg.limb[0] = 0; // == 2^64 + const u256 full = mul_div_u256(avg, 3, 2); + + // Full-width result carries the high limb: 3*2^63 = 2^64 + 2^63. + EXPECT_EQ(full.limb[1], 1u); + EXPECT_EQ(full.limb[0], BIT63); + EXPECT_FALSE(full.fits_u64()); + + // The proxy, fed the truncated low64()==0, would compute 0 -> divergence. + const u256 proxy = mul_div_u256(u256::from_u64(avg.low64()), 3, 2); + EXPECT_TRUE(proxy.is_zero()); + EXPECT_FALSE(full == proxy); +} + +TEST(DigiShield256, MultiplyTruncatesAt256BitsLikeArithUint256) +{ + // avg = 2^255 (top bit of the top limb). *4 = 2^257 -> wraps to 0 mod 2^256. + // A wider/overflow-safe intermediate would yield 2^257 (non-zero); the + // 256-bit consensus path MUST truncate to 0. + u256 avg; avg.limb[3] = BIT63; // == 2^255 + EXPECT_TRUE(mul_div_u256(avg, 4, 1).is_zero()); + + // *3 = 2^256 + 2^255 -> truncates back to 2^255. + const u256 wrap = mul_div_u256(avg, 3, 1); + EXPECT_EQ(wrap.limb[3], BIT63); + EXPECT_EQ(wrap.limb[0], 0u); + EXPECT_EQ(wrap.limb[1], 0u); + EXPECT_EQ(wrap.limb[2], 0u); +} + +TEST(DigiShield256, NearPowLimitCeilingRailRetargetsAtFullWidth) +{ + // A near-pow_limit avg at the ceiling rail (damped 90, nominal 60 -> *3/2). + // avg = 2^224 (representative DGB Scrypt pow_limit magnitude); *3/2 stays in + // range here (no wrap) but lives ENTIRELY in the high limbs the proxy drops. + u256 avg; avg.limb[3] = 0x0000000100000000ull; // 2^224 + const u256 full = mul_div_u256(avg, 90, 60); // *3/2 + // 2^224 * 3/2 = 3 * 2^223 = 2^224 + 2^223; high limb 0x0000000180000000. + EXPECT_EQ(full.limb[3], 0x0000000180000000ull); + EXPECT_FALSE(full.fits_u64()); + // Proxy on low64()==0 collapses to 0 — the rail would be computed wrong. + EXPECT_TRUE(mul_div_u256(u256::from_u64(avg.low64()), 90, 60).is_zero()); +} + +TEST(DigiShield256, ComparePicksTheLargerFullWidthValue) +{ + // pow_limit cap uses u256 compare; verify ordering across the limb boundary. + u256 big; big.limb[3] = 1; // 2^192 + u256 small = u256::from_u64(UINT64_MAX); // 2^64 - 1 + EXPECT_TRUE(small < big); + EXPECT_TRUE(big > small); + EXPECT_FALSE(big < small); +} From c6742b74401221a4933236ccce376f61239103de Mon Sep 17 00:00:00 2001 From: frstrtr Date: Thu, 18 Jun 2026 11:01:22 +0000 Subject: [PATCH 6/8] =?UTF-8?q?dgb:=20M3=20=C2=A77b=20ingest-gate=20pow=5F?= =?UTF-8?q?limit=20minimum-difficulty=20ceiling?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit VALIDATE_SCRYPT path now rejects a declared Scrypt target EASIER than pow_limit (target > pow_limit), mirroring DigiByte Core CheckProofOfWork. This fires on the bootstrap/empty-window path (expected == 0) where the nBits-style retarget-equality gate is a no-op, closing the hole where a sub-minimum-difficulty header could seed/extend the chain during a stall. pow_limit == 0 keeps the gate unconfigured (legacy default-ctor chains unconstrained). +1 vector folded into header_chain_test (no new gtest target; #143 NOT_BUILT trap avoided). 19/19 PASS, build EXIT=0. --- src/impl/dgb/coin/header_chain.hpp | 12 +++++++++ src/impl/dgb/test/header_chain_test.cpp | 33 +++++++++++++++++++++++++ 2 files changed, 45 insertions(+) diff --git a/src/impl/dgb/coin/header_chain.hpp b/src/impl/dgb/coin/header_chain.hpp index 0c33d3849..2f186c70f 100644 --- a/src/impl/dgb/coin/header_chain.hpp +++ b/src/impl/dgb/coin/header_chain.hpp @@ -201,6 +201,18 @@ class HeaderChain { if (h.target == 0) return IngestResult::REJECTED; + // Minimum-difficulty ceiling (DigiByte Core CheckProofOfWork + // rejects when bnTarget > bnPowLimit). A declared Scrypt target + // EASIER (numerically larger) than the network minimum is + // consensus-invalid REGARDLESS of the retarget window -- this + // guards the bootstrap/empty-window path (expected == 0) where + // the nBits-style equality below cannot fire yet, so a stall + // could not otherwise reject a sub-minimum-difficulty header. + // pow_limit == 0 leaves the gate unconfigured (legacy + // default-ctor chains stay unconstrained, exactly as before). + if (m_ds_params.pow_limit != 0 && h.target > m_ds_params.pow_limit) + return IngestResult::REJECTED; + // Consensus retarget gate: the declared target must equal the // DigiShield next-target computed over the Scrypt-only window // ending at the CURRENT tip (assembled BEFORE h is appended). diff --git a/src/impl/dgb/test/header_chain_test.cpp b/src/impl/dgb/test/header_chain_test.cpp index f14a7c7d4..1f785ca4e 100644 --- a/src/impl/dgb/test/header_chain_test.cpp +++ b/src/impl/dgb/test/header_chain_test.cpp @@ -239,6 +239,39 @@ TEST(HeaderChainValidate, IngestGateEnforcesDigiShieldNextTarget) } +// Ingest-path minimum-difficulty ceiling: a Scrypt header declaring a target +// EASIER than pow_limit (numerically larger) is consensus-invalid REGARDLESS of +// the retarget window -- it must be rejected even on the bootstrap/empty-window +// path where the nBits-style equality gate is a no-op (expected == 0). Mirrors +// DigiByte Core CheckProofOfWork rejecting when bnTarget > bnPowLimit. +TEST(HeaderChainValidate, IngestGateRejectsTargetAbovePowLimit) +{ + // pow_limit configured, retarget_window 1. Seed empty -> equality gate is a + // no-op, so only the ceiling can reject this first header. + HeaderChain hc(DigiShieldParams{80, /*pow_limit=*/4096}, /*window=*/1); + + // target == pow_limit is the easiest ADMISSIBLE target (strict > reject): + // accepted and credits work, seeding the chain. + ASSERT_EQ(hc.validate_and_append({SCRYPT, 1000, 4096}), + IngestResult::VALIDATED_SCRYPT); + + // A target ABOVE pow_limit (easier than the network minimum) must REJECT + // without mutating the chain -- and it must do so on the ceiling alone, + // before the retarget-equality gate (which here would demand 4096*7/8=3584). + const std::size_t size_before = hc.size(); + const uint64_t work_before = hc.cumulative_work(); + EXPECT_EQ(hc.validate_and_append({SCRYPT, 1080, 4097}), + IngestResult::REJECTED); + EXPECT_EQ(hc.size(), size_before); + EXPECT_EQ(hc.cumulative_work(), work_before); + + // pow_limit == 0 leaves the ceiling unconfigured: an enormous target is not + // rejected by THIS gate (legacy default-ctor behaviour preserved). + HeaderChain unconfigured; // target_timespan 0, pow_limit 0 + EXPECT_EQ(unconfigured.validate_and_append({SCRYPT, 1000, UINT64_MAX}), + IngestResult::VALIDATED_SCRYPT); +} + // --------------------------------------------------------------------------- // 256-BIT BOUNDARY (M3 §7b — arith_uint256 swap). Proves, not assumes, that // the DigiShield damped multiply runs at TRUE 256-bit width: a uint64/__int128 From bf5c4d366b5388a9814a236cc1d6805ea1d1aae7 Mon Sep 17 00:00:00 2001 From: frstrtr Date: Thu, 18 Jun 2026 11:32:37 +0000 Subject: [PATCH 7/8] =?UTF-8?q?dgb:=20M3=20=C2=A77b=20ingest-gate=20scrypt?= =?UTF-8?q?(header)=20PoW-satisfaction=20check?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CheckProofOfWork second half: the VALIDATE_SCRYPT path now rejects a header whose scrypt(header) digest exceeds its declared target (hash > target). Half 1 (target <= pow_limit) already landed; this closes the missing PoW-satisfaction guard that was a placeholder. HeaderSample gains a uint64 pow_hash proxy (mirrors ltc coin/header_chain hash field). Context-free check, run after the pow_limit ceiling and before the contextual nBits-equality gate, mirroring DigiByte Core if (UintToArith256(hash) > bnTarget) return false. pow_hash == 0 is the standalone default (trivially satisfies any target) so every existing vector is unchanged; the embedded-daemon port fills the real digest into the same field shape. +1 vector folded INTO header_chain_test (no new gtest target, #143 trap avoided): rejects pow_hash > target, accepts == and < boundary, and confirms a continuity header short-circuits before the PoW check. header_chain_test 20/20 PASS, build EXIT=0. --- src/impl/dgb/coin/header_chain.hpp | 30 ++++++++++++++----- src/impl/dgb/test/header_chain_test.cpp | 40 +++++++++++++++++++++++++ 2 files changed, 63 insertions(+), 7 deletions(-) diff --git a/src/impl/dgb/coin/header_chain.hpp b/src/impl/dgb/coin/header_chain.hpp index 2f186c70f..355345402 100644 --- a/src/impl/dgb/coin/header_chain.hpp +++ b/src/impl/dgb/coin/header_chain.hpp @@ -62,14 +62,18 @@ using ::dgb::coin::u256; // Minimal header sample the Scrypt-only retarget body consumes. The embedded // DigiByte Core port (M3+) carries the full CBlockHeader; the validate() + // retarget path needs only the algo bits (disposition + work-credit), the -// timestamp (timespan), and the expanded difficulty target (averaging). -// `target` is a fixed-width proxy for the standalone CI guard; the per-algo -// DigiShield slice swaps arith_uint256 in with the SAME field shape, so the -// averaging/timespan assembly below is unchanged when the real width lands. +// timestamp (timespan), the expanded difficulty target (averaging), and the +// scrypt(header) PoW digest (`pow_hash`, the CheckProofOfWork hash <= target +// check). `target` and `pow_hash` are fixed-width proxies for the standalone CI +// guard; the per-algo DigiShield slice swaps arith_uint256 in with the SAME +// field shape, so the averaging/timespan assembly below is unchanged when the +// real width lands. pow_hash == 0 is the proxy default for "scrypt(header) not +// evaluated here" (trivially satisfies any target); the daemon port fills it. struct HeaderSample { int32_t n_version = 0; int64_t n_time = 0; uint64_t target = 0; // expanded PoW target (smaller == more work) + uint64_t pow_hash = 0; // scrypt(header) digest; hash <= target == valid PoW }; // Outcome of validating + ingesting one header. @@ -195,9 +199,8 @@ class HeaderChain { case HeaderDisposition::VALIDATE_SCRYPT: { // PoW validate path. A zero target is not a validatable Scrypt - // header (the embedded daemon's full scrypt(header) <= target - // check lands with the daemon port; the structural guard here is - // that a malformed target never credits work). + // header (the structural guard here is that a malformed target + // never credits work). if (h.target == 0) return IngestResult::REJECTED; @@ -213,6 +216,19 @@ class HeaderChain { if (m_ds_params.pow_limit != 0 && h.target > m_ds_params.pow_limit) return IngestResult::REJECTED; + // PoW satisfaction (DigiByte Core CheckProofOfWork second half): + // the header's scrypt(header) digest must SATISFY its declared + // target -- hash <= target. A header claiming work it does not meet + // is consensus-invalid. This is the CONTEXT-FREE PoW check (mirrors + // `if (UintToArith256(hash) > bnTarget) return false`), run before + // the contextual nBits-equality gate below. pow_hash == 0 is the + // standalone proxy for "scrypt(header) not evaluated here" and + // trivially satisfies any target, so the chain-helper tests stay + // unconstrained; the embedded-daemon port fills pow_hash with the + // real Scrypt digest and this gate goes live with the SAME shape. + if (h.pow_hash > h.target) + return IngestResult::REJECTED; + // Consensus retarget gate: the declared target must equal the // DigiShield next-target computed over the Scrypt-only window // ending at the CURRENT tip (assembled BEFORE h is appended). diff --git a/src/impl/dgb/test/header_chain_test.cpp b/src/impl/dgb/test/header_chain_test.cpp index 1f785ca4e..67546934e 100644 --- a/src/impl/dgb/test/header_chain_test.cpp +++ b/src/impl/dgb/test/header_chain_test.cpp @@ -272,6 +272,46 @@ TEST(HeaderChainValidate, IngestGateRejectsTargetAbovePowLimit) IngestResult::VALIDATED_SCRYPT); } +// Ingest-path PoW satisfaction (DigiByte Core CheckProofOfWork second half): +// a Scrypt header whose scrypt(header) digest is NUMERICALLY GREATER than its +// declared target does not meet the work it claims and is consensus-invalid -- +// rejected without mutating the chain, independent of the retarget/ceiling +// gates. Mirrors `if (UintToArith256(hash) > bnTarget) return false`. The +// pow_hash field defaults to 0 (every brace-init vector above), which trivially +// satisfies any target, so all chain-helper tests run unchanged; here we set it +// explicitly to drive the gate. A default-ctor chain leaves the retarget gate +// and pow_limit ceiling unconfigured, so ONLY this PoW check can fire. +TEST(HeaderChainValidate, IngestGateRejectsInsufficientScryptPoW) +{ + HeaderChain hc; // gate + ceiling unconfigured: only the PoW check can fire + + // pow_hash strictly ABOVE the declared target: the header does not satisfy + // its own difficulty. REJECT, no work credited, chain not extended. + HeaderSample weak{SCRYPT, 1000, 100}; + weak.pow_hash = 101; + EXPECT_EQ(hc.validate_and_append(weak), IngestResult::REJECTED); + EXPECT_EQ(hc.size(), 0u); + EXPECT_EQ(hc.cumulative_work(), 0u); + + // pow_hash == target is the boundary: hash <= target satisfies the work. + HeaderSample exact{SCRYPT, 1000, 100}; + exact.pow_hash = 100; + EXPECT_EQ(hc.validate_and_append(exact), IngestResult::VALIDATED_SCRYPT); + + // pow_hash strictly below target also satisfies (more work than required). + HeaderSample strong{SCRYPT, 1075, 100}; + strong.pow_hash = 1; + EXPECT_EQ(hc.validate_and_append(strong), IngestResult::VALIDATED_SCRYPT); + EXPECT_EQ(hc.size(), 2u); + + // A continuity (non-Scrypt) header never reaches the PoW check: its + // disposition short-circuits to ACCEPT_BY_CONTINUITY even with a huge + // pow_hash, confirming the gate is Scrypt-path only. + HeaderSample cont{SHA256D, 1090, 100}; + cont.pow_hash = UINT64_MAX; + EXPECT_EQ(hc.validate_and_append(cont), IngestResult::ACCEPTED_CONTINUITY); +} + // --------------------------------------------------------------------------- // 256-BIT BOUNDARY (M3 §7b — arith_uint256 swap). Proves, not assumes, that // the DigiShield damped multiply runs at TRUE 256-bit width: a uint64/__int128 From 0b23d8853d5c44b1fc3d835993d0ace605391d4e Mon Sep 17 00:00:00 2001 From: frstrtr Date: Thu, 18 Jun 2026 12:22:34 +0000 Subject: [PATCH 8/8] =?UTF-8?q?dgb:=20M3=20=C2=A77b=20HeaderSample/retarge?= =?UTF-8?q?t=20field-shape=20swap=20to=20arith=5Fuint256=20(u256)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Swap HeaderSample::target/pow_hash, RetargetWindow::avg_target, DigiShieldParams::pow_limit and the digishield_next_target() return from uint64 proxies to the full-width u256 (coin/dgb_arith256.hpp). An implicit uint64 widening ctor keeps every uint64-range vector byte-identical, so the ingest PoW-satisfaction + minimum-difficulty ceiling checks and the DigiShield averaging now run at true arith_uint256 width through validate_and_append. Adds u256 operator+= for the window target-sum average. +1 ingest-path boundary vector folded into header_chain_test (no new gtest target): a >2^64 target/digest decides acceptance the same way arith_uint256 would and diverges from a low64-only proxy in both directions. 21/21 PASS. Confined to src/impl/dgb/; no shared base / bitcoin_family / CMake / build.yml touch. --- src/impl/dgb/coin/dgb_arith256.hpp | 21 +++++++ src/impl/dgb/coin/header_chain.hpp | 74 +++++++++++++------------ src/impl/dgb/test/header_chain_test.cpp | 37 +++++++++++++ 3 files changed, 97 insertions(+), 35 deletions(-) diff --git a/src/impl/dgb/coin/dgb_arith256.hpp b/src/impl/dgb/coin/dgb_arith256.hpp index 8f93934e6..eff88541b 100644 --- a/src/impl/dgb/coin/dgb_arith256.hpp +++ b/src/impl/dgb/coin/dgb_arith256.hpp @@ -35,6 +35,14 @@ namespace dgb::coin { struct u256 { uint64_t limb[4] = {0, 0, 0, 0}; + // Default-zero, plus an IMPLICIT widening from uint64_t so the field-shape + // swap (M3 7b) leaves every existing uint64-range brace-init / literal + // comparison byte-identical: `target = 4096`, `h.target == expected`, + // `pow_limit != 0` all keep compiling and computing the same value, while + // the embedded-daemon port drops full-width digests through this SAME field. + u256() = default; + u256(uint64_t v) { limb[0] = v; } + static u256 from_u64(uint64_t v) { u256 r; r.limb[0] = v; return r; } bool fits_u64() const { return limb[1] == 0 && limb[2] == 0 && limb[3] == 0; } @@ -69,6 +77,19 @@ struct u256 { return r; } + // Add another 256-bit value, truncating carry past limb[3] (256-bit wrap, + // same width discipline as mul_u64). Accumulates the retarget window's + // target sum before the divide-by-count average. + u256& operator+=(const u256& o) { + unsigned __int128 carry = 0; + for (int i = 0; i < 4; ++i) { + unsigned __int128 s = (unsigned __int128)limb[i] + o.limb[i] + carry; + limb[i] = (uint64_t)s; + carry = s >> 64; + } + return *this; + } + friend bool operator<(const u256& a, const u256& b) { for (int i = 3; i >= 0; --i) if (a.limb[i] != b.limb[i]) return a.limb[i] < b.limb[i]; diff --git a/src/impl/dgb/coin/header_chain.hpp b/src/impl/dgb/coin/header_chain.hpp index 355345402..983e4f8b0 100644 --- a/src/impl/dgb/coin/header_chain.hpp +++ b/src/impl/dgb/coin/header_chain.hpp @@ -64,16 +64,18 @@ using ::dgb::coin::u256; // retarget path needs only the algo bits (disposition + work-credit), the // timestamp (timespan), the expanded difficulty target (averaging), and the // scrypt(header) PoW digest (`pow_hash`, the CheckProofOfWork hash <= target -// check). `target` and `pow_hash` are fixed-width proxies for the standalone CI -// guard; the per-algo DigiShield slice swaps arith_uint256 in with the SAME -// field shape, so the averaging/timespan assembly below is unchanged when the -// real width lands. pow_hash == 0 is the proxy default for "scrypt(header) not -// evaluated here" (trivially satisfies any target); the daemon port fills it. +// check). `target` and `pow_hash` are full 256-bit (coin/dgb_arith256.hpp u256) +// as of the field-shape swap: the implicit uint64 widening keeps every +// uint64-range vector byte-identical while the ingest PoW/ceiling checks and the +// DigiShield averaging now run at true arith_uint256 width, so the embedded +// daemon port drops real digests into this SAME field with no reshape. pow_hash +// == 0 is the default for "scrypt(header) not evaluated here" (trivially +// satisfies any target); the daemon port fills it. struct HeaderSample { int32_t n_version = 0; int64_t n_time = 0; - uint64_t target = 0; // expanded PoW target (smaller == more work) - uint64_t pow_hash = 0; // scrypt(header) digest; hash <= target == valid PoW + u256 target = 0; // expanded PoW target (smaller == more work) + u256 pow_hash = 0; // scrypt(header) digest; hash <= target == valid PoW }; // Outcome of validating + ingesting one header. @@ -89,7 +91,7 @@ enum class IngestResult { // the DIGISHIELD INSERTION POINT warn against. struct RetargetWindow { std::size_t scrypt_samples = 0; // Scrypt headers folded in (<= window) - uint64_t avg_target = 0; // mean expanded target over samples + u256 avg_target = 0; // mean expanded target over samples int64_t actual_timespan = 0; // newest - oldest Scrypt sample time bool sufficient = false; // true iff the full window was found }; @@ -100,7 +102,7 @@ struct RetargetWindow { // it so a long stall can never relax difficulty past the network minimum. struct DigiShieldParams { int64_t target_timespan = 0; // nominal solve time the filter centers on - uint64_t pow_limit = 0; // max (easiest) target; result never exceeds + u256 pow_limit = 0; // max (easiest) target; result never exceeds }; // Per-algo DigiShield/MultiShield damped retarget multiply (M3 7b). @@ -125,19 +127,19 @@ struct DigiShieldParams { // (coin/dgb_arith256.hpp): arith_uint256 multiply-then-divide with 256-bit // overflow truncation -- the consensus behaviour DigiByte Core exhibits near // pow_limit, which an __int128 proxy could NOT reproduce (it would compute a -// wider, divergent next target). `target` here is still the uint64 field proxy -// the standalone CI guard uses; for any uint64-range avg_target the 256-bit path -// is bit-identical to the prior intermediate, so existing vectors are unchanged. -// The embedded-daemon port swaps real arith_uint256 into the field shape with -// this exact multiply/clamp ordering. +// wider, divergent next target). With the field-shape swap the target/avg_target +// fields ARE arith_uint256 (u256); for any uint64-range avg_target the result is +// bit-identical to the prior __int128 proxy, so existing vectors are unchanged, +// while a >2^64 avg now retargets at full width. The embedded-daemon port reuses +// this exact multiply/clamp ordering with real arith_uint256. // // Returns 0 when the window carries no Scrypt samples -- the caller MUST keep // the prior target rather than retarget off an empty window (early/genesis). -inline uint64_t digishield_next_target(const RetargetWindow& rw, - const DigiShieldParams& params) +inline u256 digishield_next_target(const RetargetWindow& rw, + const DigiShieldParams& params) { if (rw.scrypt_samples == 0 || params.target_timespan <= 0) - return 0; + return u256{}; const int64_t nominal = params.target_timespan; @@ -154,18 +156,19 @@ inline uint64_t digishield_next_target(const RetargetWindow& rw, // strictly positive after the clamp (floor_ts > 0 for nominal > 0). The // multiply truncates at 256 bits exactly as arith_uint256 does, so a target // near pow_limit retargets to the SAME value DigiByte Core computes. - const u256 bn_new = mul_div_u256(u256::from_u64(rw.avg_target), + const u256 bn_new = mul_div_u256(rw.avg_target, static_cast(damped), static_cast(nominal)); - // Difficulty floor: never relax past the network's easiest target. - if (params.pow_limit != 0 && - bn_new > u256::from_u64(params.pow_limit)) + // Difficulty floor: never relax past the network's easiest target. The cap + // compare runs at full 256-bit width, so a pow_limit in the high limbs is + // honoured exactly (a uint64 proxy would mis-decide near 2^64). + if (!params.pow_limit.is_zero() && bn_new > params.pow_limit) return params.pow_limit; - // uint64 field proxy: a uint64-range avg keeps bn_new within 64 bits, so - // low64() is exact (the embedded port returns the full arith_uint256). - return bn_new.low64(); + // Full-width next target (uint64-range inputs leave the high limbs zero, so + // this is bit-identical to the prior low64() return for existing vectors). + return bn_new; } // Work-neutral header chain with a Scrypt-only DigiShield retarget body. @@ -201,7 +204,7 @@ class HeaderChain { // PoW validate path. A zero target is not a validatable Scrypt // header (the structural guard here is that a malformed target // never credits work). - if (h.target == 0) + if (h.target.is_zero()) return IngestResult::REJECTED; // Minimum-difficulty ceiling (DigiByte Core CheckProofOfWork @@ -213,7 +216,7 @@ class HeaderChain { // could not otherwise reject a sub-minimum-difficulty header. // pow_limit == 0 leaves the gate unconfigured (legacy // default-ctor chains stay unconstrained, exactly as before). - if (m_ds_params.pow_limit != 0 && h.target > m_ds_params.pow_limit) + if (!m_ds_params.pow_limit.is_zero() && h.target > m_ds_params.pow_limit) return IngestResult::REJECTED; // PoW satisfaction (DigiByte Core CheckProofOfWork second half): @@ -239,8 +242,8 @@ class HeaderChain { // mirrors Bitcoin/DigiByte consensus: the header carries the // required next-work value, not merely a target it satisfies. const RetargetWindow rw = next_retarget_window(m_retarget_window); - const uint64_t expected = digishield_next_target(rw, m_ds_params); - if (expected != 0 && h.target != expected) + const u256 expected = digishield_next_target(rw, m_ds_params); + if (!expected.is_zero() && !(h.target == expected)) return IngestResult::REJECTED; m_chain.push_back(h); @@ -278,12 +281,12 @@ class HeaderChain { if (idx.empty()) return rw; - unsigned __int128 target_sum = 0; + u256 target_sum; for (std::size_t k : idx) target_sum += m_chain[depth - 1 - k].target; rw.scrypt_samples = idx.size(); - rw.avg_target = static_cast(target_sum / idx.size()); + rw.avg_target = target_sum.div_u64(static_cast(idx.size())); // idx is nearest-first: front() == newest Scrypt sample (smallest k), // back() == oldest. Timespan is over Scrypt samples exclusively. rw.actual_timespan = m_chain[depth - 1 - idx.front()].n_time @@ -296,12 +299,13 @@ class HeaderChain { std::size_t size() const { return m_chain.size(); } private: - // Work proxy: inversely proportional to the target (smaller target == more - // work), matching real PoW work accounting in shape. The per-algo slice - // swaps the arith_uint256 work() in; only Scrypt headers ever reach here. - static uint64_t work_from_target(uint64_t target) + // Work proxy: inversely proportional to the target. The full-width field + // narrows to low64() for the proxy, keeping cumulative_work byte-identical + // for every uint64-range vector; the embedded port swaps in arith_uint256 + // work() (2^256 / (target+1)) over the same Scrypt-only credit path. + static uint64_t work_from_target(const u256& target) { - return target == 0 ? 0 : (UINT64_MAX / target); + return target.is_zero() ? 0 : (UINT64_MAX / target.low64()); } DigiShieldParams m_ds_params{}; // retarget gate params diff --git a/src/impl/dgb/test/header_chain_test.cpp b/src/impl/dgb/test/header_chain_test.cpp index 67546934e..0a25085af 100644 --- a/src/impl/dgb/test/header_chain_test.cpp +++ b/src/impl/dgb/test/header_chain_test.cpp @@ -403,3 +403,40 @@ TEST(DigiShield256, ComparePicksTheLargerFullWidthValue) EXPECT_TRUE(big > small); EXPECT_FALSE(big < small); } + +// --------------------------------------------------------------------------- +// 256-BIT INGEST PATH (M3 7b -- field-shape swap). The swap widened +// HeaderSample::target/pow_hash (and the retarget fields) to u256, so the +// ingest PoW-satisfaction check (h.pow_hash <= h.target) now runs at TRUE +// 256-bit width through validate_and_append -- not only inside +// digishield_next_target. This PROVES it: targets/digests living in the HIGH +// limbs decide acceptance the SAME way arith_uint256 would and DIVERGE from a +// uint64 (low64-only) proxy in BOTH directions. Gate + ceiling stay +// unconfigured (default ctor) so only the PoW satisfaction check can fire. +// --------------------------------------------------------------------------- +TEST(HeaderChainValidate, IngestPoWSatisfactionRunsAtFullWidth) +{ + HeaderChain hc; // unconfigured: only the PoW satisfaction gate can fire + + // target = 2^192 + 5 (lives in the top limb; low64 == 5). + u256 target; target.limb[3] = 1; target.limb[0] = 5; + + // pow_hash = 10 (fits u64). Full width: 10 <= 2^192+5 -> PoW SATISFIED, the + // header is valid. A low64-only proxy would compare 10 > 5 and WRONGLY + // reject -- so acceptance here proves the check is full-width. + HeaderSample ok{SCRYPT, 1000}; + ok.target = target; + ok.pow_hash = u256::from_u64(10); + EXPECT_EQ(hc.validate_and_append(ok), IngestResult::VALIDATED_SCRYPT); + EXPECT_EQ(hc.size(), 1u); + + // pow_hash = 2^193 (top limb 2, low64 == 0) vs the same 2^192+5 target. + // Full width: 2^193 > 2^192+5 -> does NOT satisfy -> REJECT. A low64-only + // proxy would compare 0 > 5 (false) and WRONGLY accept -- divergence the + // other direction. No mutation on reject. + HeaderSample weak{SCRYPT, 1075}; + weak.target = target; + weak.pow_hash.limb[3] = 2; // 2^193, low64 == 0 + EXPECT_EQ(hc.validate_and_append(weak), IngestResult::REJECTED); + EXPECT_EQ(hc.size(), 1u); // unchanged +}