Skip to content

Commit 98bf557

Browse files
committed
dgb: M3 §7b DigiShield Scrypt-only ancestor walk + work-credit predicate
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.
1 parent 064a717 commit 98bf557

5 files changed

Lines changed: 164 additions & 4 deletions

File tree

.github/workflows/build.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ jobs:
6868
test_mweb_builder \
6969
test_address_resolution test_compute_share_target \
7070
test_utxo test_dgb_subsidy dgb_share_test \
71-
rpc_request_test softfork_check_test genesis_check_test algo_select_test \
71+
rpc_request_test softfork_check_test genesis_check_test algo_select_test digishield_walk_test \
7272
v37_test \
7373
-j$(nproc)
7474
@@ -196,7 +196,7 @@ jobs:
196196
test_mweb_builder \
197197
test_address_resolution test_compute_share_target \
198198
test_utxo test_dgb_subsidy dgb_share_test \
199-
rpc_request_test softfork_check_test genesis_check_test algo_select_test \
199+
rpc_request_test softfork_check_test genesis_check_test algo_select_test digishield_walk_test \
200200
test_coin_broadcaster test_multiaddress_pplns test_pplns_stress \
201201
v37_test \
202202
-j$(nproc)
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
#pragma once
2+
// ---------------------------------------------------------------------------
3+
// DGB DigiShield Scrypt-only ancestor walk (M3 §7b — SCRYPT-ONLY VALIDATION).
4+
//
5+
// DigiByte retargets per-block per-algo with DigiShield/MultiShield, NOT
6+
// Bitcoin's 2016-block window. For the V36 Scrypt-only lane the difficulty
7+
// window must walk SCRYPT-ALGO ancestors ONLY. On a mixed-algo chain the
8+
// header sequence interleaves Scrypt headers (full PoW validate) with
9+
// non-Scrypt headers accepted by continuity (work-neutral). Folding a
10+
// continuity header into the retarget window corrupts the Scrypt target AND
11+
// re-introduces the work-neutrality break documented as the THIRD INVARIANT
12+
// in coin/header_chain.hpp. This header isolates that walk so the trap lives
13+
// in one consensus-pinned, separately-guarded place — exactly the failure the
14+
// header_chain.hpp DIGISHIELD INSERTION POINT note warns is "easy to get wrong
15+
// on a mixed-algo chain".
16+
//
17+
// This is the ancestor-selection step only; the per-algo DigiShield target
18+
// math (LWMA/MultiShield) layers on top in the following slice and consumes
19+
// the indices this returns.
20+
//
21+
// Header-only: depends ONLY on coin/dgb_block_algo.hpp + std, so it links into
22+
// the standalone CI guard (GTest-only, no dgb OBJECT lib) like algo_select.
23+
// ---------------------------------------------------------------------------
24+
25+
#include <cstddef>
26+
#include <cstdint>
27+
#include <functional>
28+
#include <vector>
29+
30+
#include <impl/dgb/coin/dgb_block_algo.hpp>
31+
32+
namespace dgb::coin {
33+
34+
// THIRD-INVARIANT work-accounting predicate. Only a Scrypt header credits
35+
// cumulative work / best-chain weight; a continuity (known non-Scrypt) header
36+
// extends the header chain but is work-neutral. This is the single predicate
37+
// HeaderChain::validate() consults before adding a header's work, so the
38+
// invariant cannot drift between the validate() path and the retarget walk.
39+
inline bool header_credits_work(int32_t n_version) noexcept
40+
{
41+
return is_scrypt_header(n_version);
42+
}
43+
44+
// Collect the Scrypt-algo ancestors that form a DigiShield retarget window.
45+
//
46+
// version_at(k) -> nVersion of the header k positions back from the search
47+
// start (k = 0 is the nearest candidate ancestor).
48+
// depth -> how many positions are walkable (chain length behind).
49+
// window -> number of Scrypt ancestors the retarget needs.
50+
//
51+
// Returns the k-indices of the first `window` SCRYPT headers, nearest-first,
52+
// skipping every non-Scrypt (continuity) header. Returns fewer than `window`
53+
// only when the available chain is exhausted (early-chain / genesis region).
54+
// Unknown-algo headers never enter a validated chain (REJECT at ingest), so
55+
// the walk treats anything not Scrypt as a skip.
56+
inline std::vector<std::size_t> scrypt_window_ancestors(
57+
const std::function<int32_t(std::size_t)>& version_at,
58+
std::size_t depth,
59+
std::size_t window)
60+
{
61+
std::vector<std::size_t> out;
62+
if (window == 0) return out;
63+
out.reserve(window);
64+
for (std::size_t k = 0; k < depth && out.size() < window; ++k)
65+
{
66+
if (is_scrypt_header(version_at(k)))
67+
out.push_back(k);
68+
// else: continuity header -> skipped, contributes no window sample
69+
}
70+
return out;
71+
}
72+
73+
} // namespace dgb::coin

src/impl/dgb/coin/header_chain.hpp

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,5 +34,8 @@
3434
// ONLY — never the interleaved multi-algo header chain. On a mixed-algo chain
3535
// the previous-block / window walk must skip non-Scrypt (continuity) headers;
3636
// folding them into the window corrupts the Scrypt retarget and re-introduces
37-
// the work-neutrality break above. Easy to get wrong on a mixed-algo chain.
37+
// the work-neutrality break above. Easy to get wrong on a mixed-algo chain. The Scrypt-only
38+
// ancestor selection for that window is implemented + guarded in
39+
// coin/dgb_digishield.hpp (scrypt_window_ancestors + header_credits_work);
40+
// the per-algo target math layers on top of it.
3841
namespace c2pool::dgb { /* TODO(M3): HeaderChain w/ Scrypt-only validate() + accept-by-continuity */ }

src/impl/dgb/test/CMakeLists.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ if (BUILD_TESTING AND GTest_FOUND)
2525
# transport rpc.cpp wiring is deferred (Option-B scope). Each MUST appear
2626
# in BOTH this ctest registration AND the build.yml --target allowlist,
2727
# or it becomes a #143-style NOT_BUILT sentinel that reds master.
28-
foreach(dgb_guard rpc_request_test softfork_check_test genesis_check_test algo_select_test)
28+
foreach(dgb_guard rpc_request_test softfork_check_test genesis_check_test algo_select_test digishield_walk_test)
2929
add_executable(${dgb_guard} ${dgb_guard}.cpp)
3030
target_link_libraries(${dgb_guard} PRIVATE
3131
GTest::gtest_main GTest::gtest nlohmann_json::nlohmann_json)
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
// ---------------------------------------------------------------------------
2+
// dgb M3 §7b DigiShield Scrypt-only ancestor-walk regression guard.
3+
//
4+
// Pins the consensus quirk flagged at coin/header_chain.hpp's DIGISHIELD
5+
// INSERTION POINT: on a mixed-algo DGB chain the Scrypt difficulty window must
6+
// walk SCRYPT ancestors ONLY and SKIP continuity (non-Scrypt) headers. Folding
7+
// a continuity header into the window corrupts the Scrypt retarget and breaks
8+
// work-neutrality (THIRD INVARIANT). Also pins header_credits_work() == the
9+
// Scrypt predicate, so the validate() work-accounting and the retarget walk
10+
// share one SSOT and cannot drift apart.
11+
//
12+
// Links ONLY the header-only helpers + gtest -- no dgb OBJECT lib / transport.
13+
// ---------------------------------------------------------------------------
14+
15+
#include <cstdint>
16+
#include <vector>
17+
18+
#include <gtest/gtest.h>
19+
20+
#include <impl/dgb/coin/dgb_digishield.hpp>
21+
22+
using namespace dgb::coin;
23+
24+
static constexpr int32_t PRIMARY = 2; // BLOCK_VERSION_DEFAULT
25+
static constexpr int32_t SCRYPT = PRIMARY | DGB_BLOCK_VERSION_SCRYPT;
26+
static constexpr int32_t SHA256D = PRIMARY | DGB_BLOCK_VERSION_SHA256D;
27+
static constexpr int32_t SKEIN = PRIMARY | DGB_BLOCK_VERSION_SKEIN;
28+
static constexpr int32_t ODO = PRIMARY | DGB_BLOCK_VERSION_ODO;
29+
30+
// Build a version_at() over a fixed nearest-first chain.
31+
static std::function<int32_t(std::size_t)> chain(const std::vector<int32_t>& c)
32+
{
33+
return [c](std::size_t k) { return c.at(k); };
34+
}
35+
36+
TEST(DigishieldWalk, WorkCreditPredicateIsScryptOnly)
37+
{
38+
EXPECT_TRUE (header_credits_work(SCRYPT));
39+
EXPECT_TRUE (header_credits_work(PRIMARY)); // bare primary == Scrypt
40+
EXPECT_FALSE(header_credits_work(SHA256D));
41+
EXPECT_FALSE(header_credits_work(SKEIN));
42+
EXPECT_FALSE(header_credits_work(ODO));
43+
}
44+
45+
TEST(DigishieldWalk, AllScryptChainTakesContiguousWindow)
46+
{
47+
std::vector<int32_t> c(10, SCRYPT);
48+
auto w = scrypt_window_ancestors(chain(c), c.size(), 4);
49+
ASSERT_EQ(w.size(), 4u);
50+
EXPECT_EQ(w, (std::vector<std::size_t>{0, 1, 2, 3}));
51+
}
52+
53+
TEST(DigishieldWalk, SkipsInterleavedContinuityHeaders)
54+
{
55+
// Mixed: S, sha, S, skein, S, odo, S -> Scrypt at k = 0,2,4,6.
56+
std::vector<int32_t> c{SCRYPT, SHA256D, SCRYPT, SKEIN, SCRYPT, ODO, SCRYPT};
57+
auto w = scrypt_window_ancestors(chain(c), c.size(), 3);
58+
ASSERT_EQ(w.size(), 3u);
59+
EXPECT_EQ(w, (std::vector<std::size_t>{0, 2, 4})); // continuity skipped
60+
}
61+
62+
TEST(DigishieldWalk, LeadingContinuityRunIsSkipped)
63+
{
64+
// Nearest headers are all non-Scrypt; first Scrypt sample is deep.
65+
std::vector<int32_t> c{SHA256D, SKEIN, ODO, SCRYPT, SCRYPT};
66+
auto w = scrypt_window_ancestors(chain(c), c.size(), 2);
67+
ASSERT_EQ(w.size(), 2u);
68+
EXPECT_EQ(w, (std::vector<std::size_t>{3, 4}));
69+
}
70+
71+
TEST(DigishieldWalk, ExhaustedChainReturnsFewerThanWindow)
72+
{
73+
// Only 2 Scrypt headers exist but the window wants 4 (early-chain region).
74+
std::vector<int32_t> c{SCRYPT, SHA256D, SCRYPT, SHA256D};
75+
auto w = scrypt_window_ancestors(chain(c), c.size(), 4);
76+
ASSERT_EQ(w.size(), 2u);
77+
EXPECT_EQ(w, (std::vector<std::size_t>{0, 2}));
78+
}
79+
80+
TEST(DigishieldWalk, ZeroWindowIsEmpty)
81+
{
82+
std::vector<int32_t> c(4, SCRYPT);
83+
EXPECT_TRUE(scrypt_window_ancestors(chain(c), c.size(), 0).empty());
84+
}

0 commit comments

Comments
 (0)