Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down Expand Up @@ -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)
Expand Down
94 changes: 94 additions & 0 deletions src/impl/dgb/coin/dgb_block_algo.hpp
Original file line number Diff line number Diff line change
@@ -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 <cstdint>

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
2 changes: 1 addition & 1 deletion src/impl/dgb/test/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
83 changes: 83 additions & 0 deletions src/impl/dgb/test/algo_select_test.cpp
Original file line number Diff line number Diff line change
@@ -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 <cstdint>

#include <gtest/gtest.h>

#include <impl/dgb/coin/dgb_block_algo.hpp>

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<int>(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);
}
Loading