From 23ae48fcde51e6993bac686465fb90ccdb27403d Mon Sep 17 00:00:00 2001 From: frstrtr Date: Thu, 18 Jun 2026 08:26:56 +0000 Subject: [PATCH] =?UTF-8?q?dgb:=20M3=20=C2=A77b=20Scrypt-only=20multi-algo?= =?UTF-8?q?=20version=20classifier?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit DigiByte encodes the mining algo in 4 bits of the block nVersion field. The Scrypt-only V36 validation path needs a consensus-exact decode to route each incoming parent header: Scrypt -> PoW-validate, known non-Scrypt -> accept-by-continuity (work-neutral), unknown algo bits -> reject. Add coin/dgb_block_algo.hpp: a header-only SSOT (DgbAlgo, is_scrypt_header, dgb_header_disposition) pinning the BLOCK_VERSION_* / ALGO_* enums verbatim from DigiByte Core src/primitives/block.h + GetAlgo(). CRITICAL invariant: Scrypt == (0 << 8) -- the masked algo bits are ZERO -- so the gate is (nVersion & 0x0F00) == 0, not a nonzero check. Also pins the upstream quirk ALGO_ODO == 7 (not a dense 5) and rejects reserved codepoints (10/12 << 8). Add standalone guard test/algo_select_test.cpp (6 tests, GTest-only, no OBJECT lib) and register it in BOTH the ctest foreach allowlist and the two build.yml --target lists, per the #143 NOT_BUILT lesson. No header_chain or transport wiring touched; foundational primitive for the deferred validate()/DigiShield-window port. --- .github/workflows/build.yml | 4 +- src/impl/dgb/coin/dgb_block_algo.hpp | 94 ++++++++++++++++++++++++++ src/impl/dgb/test/CMakeLists.txt | 2 +- src/impl/dgb/test/algo_select_test.cpp | 83 +++++++++++++++++++++++ 4 files changed, 180 insertions(+), 3 deletions(-) create mode 100644 src/impl/dgb/coin/dgb_block_algo.hpp create mode 100644 src/impl/dgb/test/algo_select_test.cpp diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 42aeed8cf..3debd89f7 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 \ + rpc_request_test softfork_check_test genesis_check_test algo_select_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 \ + rpc_request_test softfork_check_test genesis_check_test algo_select_test \ test_coin_broadcaster test_multiaddress_pplns test_pplns_stress \ v37_test \ -j$(nproc) diff --git a/src/impl/dgb/coin/dgb_block_algo.hpp b/src/impl/dgb/coin/dgb_block_algo.hpp new file mode 100644 index 000000000..d44db0112 --- /dev/null +++ b/src/impl/dgb/coin/dgb_block_algo.hpp @@ -0,0 +1,94 @@ +#pragma once +// --------------------------------------------------------------------------- +// DGB multi-algo block-version classifier (M3 §7b — SCRYPT-ONLY VALIDATION). +// +// DigiByte is a multi-algo chain (Scrypt, SHA256d, Skein, Qubit, Odocrypt; +// legacy Groestl). The mining algorithm is encoded in 4 bits of the block +// nVersion field. c2pool-dgb (V36) validates the SCRYPT path ONLY: +// - Scrypt header -> full PoW validate (DgbAlgo::SCRYPT) +// - known non-Scrypt -> accept-by-continuity (work-neutral: it +// extends the header chain but contributes ZERO +// cumulative work / best-chain weight; see the +// THIRD INVARIANT note in coin/header_chain.hpp) +// - unknown algo bits -> reject (DgbAlgo::UNKNOWN) +// Full 5-algo PoW validation is V37 scope — do NOT add the other PoW lanes. +// +// SSOT: DigiByte Core src/primitives/block.h (ALGO_* + BLOCK_VERSION_* enums) +// and block.cpp CBlockHeader::GetAlgo(). The constants below are independent +// literals so this header is the single consensus pin for the algo decode; +// the standalone guard test (test/algo_select_test.cpp) asserts them against +// the upstream values and fails loudly on drift. +// +// CRITICAL: Scrypt == (0 << 8) — the masked algo bits are ZERO for a Scrypt +// block. A header is Scrypt iff (nVersion & DGB_BLOCK_VERSION_ALGO) == 0. +// +// Header-only: no OBJECT-lib / transport deps, so it links into the standalone +// CI guard (no dgb OBJECT lib, GTest-only) exactly like rpc_request.hpp. +// --------------------------------------------------------------------------- + +#include + +namespace dgb::coin { + +// Algo id space — mirrors DigiByte Core's ALGO_* enum (block.h) verbatim, +// including ALGO_ODO == 7 (NOT 5) and ALGO_UNKNOWN == -1. +enum class DgbAlgo : int { + UNKNOWN = -1, + SHA256D = 0, + SCRYPT = 1, + GROESTL = 2, + SKEIN = 3, + QUBIT = 4, + ODO = 7, +}; + +// Block-version algo field — DigiByte Core BLOCK_VERSION_* (block.h). +// Mask is the 4 bits at <<8; Scrypt is the all-zero codepoint. +static constexpr int32_t DGB_BLOCK_VERSION_ALGO = (15 << 8); // 0x0F00 mask +static constexpr int32_t DGB_BLOCK_VERSION_SCRYPT = ( 0 << 8); // 0x0000 +static constexpr int32_t DGB_BLOCK_VERSION_SHA256D = ( 2 << 8); // 0x0200 +static constexpr int32_t DGB_BLOCK_VERSION_GROESTL = ( 4 << 8); // 0x0400 +static constexpr int32_t DGB_BLOCK_VERSION_SKEIN = ( 6 << 8); // 0x0600 +static constexpr int32_t DGB_BLOCK_VERSION_QUBIT = ( 8 << 8); // 0x0800 +static constexpr int32_t DGB_BLOCK_VERSION_ODO = (14 << 8); // 0x0E00 + +// Decode the mining algo from a block nVersion field. Mirrors +// CBlockHeader::GetAlgo(): switch on the masked algo bits, default UNKNOWN. +inline DgbAlgo dgb_block_algo(int32_t n_version) noexcept +{ + switch (n_version & DGB_BLOCK_VERSION_ALGO) + { + case DGB_BLOCK_VERSION_SCRYPT: return DgbAlgo::SCRYPT; + case DGB_BLOCK_VERSION_SHA256D: return DgbAlgo::SHA256D; + case DGB_BLOCK_VERSION_GROESTL: return DgbAlgo::GROESTL; + case DGB_BLOCK_VERSION_SKEIN: return DgbAlgo::SKEIN; + case DGB_BLOCK_VERSION_QUBIT: return DgbAlgo::QUBIT; + case DGB_BLOCK_VERSION_ODO: return DgbAlgo::ODO; + default: return DgbAlgo::UNKNOWN; + } +} + +// V36 validation gate: is this header on the Scrypt PoW path? +inline bool is_scrypt_header(int32_t n_version) noexcept +{ + return (n_version & DGB_BLOCK_VERSION_ALGO) == DGB_BLOCK_VERSION_SCRYPT; +} + +// Three-way V36 disposition of an incoming parent header by its algo bits. +enum class HeaderDisposition { + VALIDATE_SCRYPT, // Scrypt -> full PoW validate + ACCEPT_BY_CONTINUITY, // known non-Scrypt -> extend, work-neutral + REJECT, // unknown algo bits -> reject +}; + +inline HeaderDisposition dgb_header_disposition(int32_t n_version) noexcept +{ + switch (dgb_block_algo(n_version)) + { + case DgbAlgo::SCRYPT: return HeaderDisposition::VALIDATE_SCRYPT; + case DgbAlgo::UNKNOWN: return HeaderDisposition::REJECT; + default: return HeaderDisposition::ACCEPT_BY_CONTINUITY; + } +} + +} // namespace dgb::coin diff --git a/src/impl/dgb/test/CMakeLists.txt b/src/impl/dgb/test/CMakeLists.txt index c96043cbd..0a79ed8b9 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) + foreach(dgb_guard rpc_request_test softfork_check_test genesis_check_test algo_select_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/algo_select_test.cpp b/src/impl/dgb/test/algo_select_test.cpp new file mode 100644 index 000000000..9e1722f91 --- /dev/null +++ b/src/impl/dgb/test/algo_select_test.cpp @@ -0,0 +1,83 @@ +// --------------------------------------------------------------------------- +// dgb M3 §7b multi-algo version classifier regression guard. +// +// Pins the DigiByte block-version -> algo decode that the Scrypt-only header +// validation path relies on (coin/dgb_block_algo.hpp). The decode is +// consensus-critical: misclassifying a non-Scrypt header as Scrypt would feed +// it into the Scrypt PoW lane (a divergence), and misclassifying a Scrypt +// header as continuity would silently drop real parent work. The CRITICAL +// trap is that Scrypt == (0 << 8): the masked algo bits are ZERO, so a naive +// "nonzero == has-algo" check is wrong. +// +// Independent literals (taken from DigiByte Core src/primitives/block.h) so +// this fails loudly if the SSOT header is ever edited. Links ONLY the +// header-only classifier + gtest -- no dgb OBJECT lib / transport. +// --------------------------------------------------------------------------- + +#include + +#include + +#include + +using namespace dgb::coin; + +// Real DGB chain uses low primary versions OR'd with the algo bits. Build the +// nVersion the way a miner does: primary (e.g. 2) | algo codepoint. +static constexpr int32_t PRIMARY = 2; // BLOCK_VERSION_DEFAULT + +TEST(DgbAlgoSelect, ScryptIsZeroAlgoBits) +{ + // Scrypt block: algo bits all zero. + EXPECT_EQ(dgb_block_algo(PRIMARY | DGB_BLOCK_VERSION_SCRYPT), DgbAlgo::SCRYPT); + EXPECT_TRUE(is_scrypt_header(PRIMARY | DGB_BLOCK_VERSION_SCRYPT)); + // A bare primary version (no algo bits) is therefore Scrypt too. + EXPECT_TRUE(is_scrypt_header(PRIMARY)); +} + +TEST(DgbAlgoSelect, KnownNonScryptDecodes) +{ + EXPECT_EQ(dgb_block_algo(PRIMARY | DGB_BLOCK_VERSION_SHA256D), DgbAlgo::SHA256D); + EXPECT_EQ(dgb_block_algo(PRIMARY | DGB_BLOCK_VERSION_GROESTL), DgbAlgo::GROESTL); + EXPECT_EQ(dgb_block_algo(PRIMARY | DGB_BLOCK_VERSION_SKEIN), DgbAlgo::SKEIN); + EXPECT_EQ(dgb_block_algo(PRIMARY | DGB_BLOCK_VERSION_QUBIT), DgbAlgo::QUBIT); + EXPECT_EQ(dgb_block_algo(PRIMARY | DGB_BLOCK_VERSION_ODO), DgbAlgo::ODO); + // None of them are on the Scrypt path. + EXPECT_FALSE(is_scrypt_header(PRIMARY | DGB_BLOCK_VERSION_SHA256D)); + EXPECT_FALSE(is_scrypt_header(PRIMARY | DGB_BLOCK_VERSION_ODO)); +} + +TEST(DgbAlgoSelect, OdoIdIsSevenNotFive) +{ + // Guard the upstream quirk: ALGO_ODO == 7, not a dense 5. + EXPECT_EQ(static_cast(DgbAlgo::ODO), 7); +} + +TEST(DgbAlgoSelect, UnknownAlgoBitsReject) +{ + // 10<<8 and 12<<8 are reserved/commented-out (Equihash/Ethash) in Core: + // they must decode to UNKNOWN and be rejected, not silently continued. + EXPECT_EQ(dgb_block_algo(PRIMARY | (10 << 8)), DgbAlgo::UNKNOWN); + EXPECT_EQ(dgb_block_algo(PRIMARY | (12 << 8)), DgbAlgo::UNKNOWN); + EXPECT_EQ(dgb_header_disposition(PRIMARY | (10 << 8)), HeaderDisposition::REJECT); +} + +TEST(DgbAlgoSelect, MaskIgnoresHighAndLowBits) +{ + // Algo decode must mask exactly the 4 bits at <<8 -- high version bits + // (BIP9 signalling, e.g. 0x20000000) and low primary bits must not leak. + const int32_t bip9 = 0x20000000; + EXPECT_EQ(dgb_block_algo(bip9 | DGB_BLOCK_VERSION_SCRYPT), DgbAlgo::SCRYPT); + EXPECT_EQ(dgb_block_algo(bip9 | DGB_BLOCK_VERSION_QUBIT), DgbAlgo::QUBIT); + EXPECT_TRUE(is_scrypt_header(bip9 | 0x000000FF)); // low byte set, algo still 0 +} + +TEST(DgbAlgoSelect, DispositionThreeWay) +{ + EXPECT_EQ(dgb_header_disposition(PRIMARY | DGB_BLOCK_VERSION_SCRYPT), + HeaderDisposition::VALIDATE_SCRYPT); + EXPECT_EQ(dgb_header_disposition(PRIMARY | DGB_BLOCK_VERSION_SKEIN), + HeaderDisposition::ACCEPT_BY_CONTINUITY); + EXPECT_EQ(dgb_header_disposition(PRIMARY | (10 << 8)), + HeaderDisposition::REJECT); +}