From b1e7bc74abaeee55927b2c14789894b56a495129 Mon Sep 17 00:00:00 2001 From: frstrtr Date: Thu, 18 Jun 2026 13:13:31 +0000 Subject: [PATCH 1/2] =?UTF-8?q?dgb:=20M3=20=C2=A77b=20scrypt=20digest=20->?= =?UTF-8?q?=20pow=5Fhash=20little-endian=20conversion=20(u256::from=5Fle?= =?UTF-8?q?=5Fbytes)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add u256::from_le_bytes: builds a 256-bit value from a 32-byte little-endian digest (the scrypt_1024_1_1_256 PoW output), limb[0] = least-significant 8 bytes, mirroring bitcoin UintToArith256. This is the bytes->field half of dropping the embedded DigiByte Core Scrypt digest into HeaderSample::pow_hash at the ingest boundary; the scrypt CALL itself (btclibs) lands next slice. Header-only, depends only on /builtin unsigned char, so the standalone header_chain guard keeps linking with NO btclibs scrypt dependency. +1 vector folded INTO header_chain_test (no new gtest target; #143 NOT_BUILT trap avoided): pins LE limb mapping, zero/max boundaries, and proves a converted digest feeds the existing full-width PoW-satisfaction gate (not a low64 proxy). 22/22 PASS. --- src/impl/dgb/coin/dgb_arith256.hpp | 20 +++++++++++ src/impl/dgb/test/header_chain_test.cpp | 46 +++++++++++++++++++++++++ 2 files changed, 66 insertions(+) diff --git a/src/impl/dgb/coin/dgb_arith256.hpp b/src/impl/dgb/coin/dgb_arith256.hpp index eff88541b..803a581eb 100644 --- a/src/impl/dgb/coin/dgb_arith256.hpp +++ b/src/impl/dgb/coin/dgb_arith256.hpp @@ -45,6 +45,26 @@ struct u256 { static u256 from_u64(uint64_t v) { u256 r; r.limb[0] = v; return r; } + // Construct from a 32-byte little-endian digest (the scrypt_1024_1_1_256 + // PoW output): limb[0] holds the least-significant 8 bytes. Mirrors bitcoin + // UintToArith256 -- the Scrypt PoW hash is read little-endian for the + // hash <= target comparison, so the digest the embedded DigiByte Core port + // produces drops straight into HeaderSample::pow_hash at the ingest boundary + // in the SAME byte order the 256-bit satisfaction gate already compares. + // Depends only on / builtin unsigned char, so the standalone + // header guard keeps linking with NO btclibs scrypt dependency; the scrypt + // CALL itself lands at the ingest boundary in a following slice. + static u256 from_le_bytes(const unsigned char b[32]) { + u256 r; + for (int i = 0; i < 4; ++i) { + uint64_t v = 0; + for (int j = 0; j < 8; ++j) + v |= (uint64_t)b[i * 8 + j] << (8 * j); + r.limb[i] = 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]; } diff --git a/src/impl/dgb/test/header_chain_test.cpp b/src/impl/dgb/test/header_chain_test.cpp index 0a25085af..6285c65d6 100644 --- a/src/impl/dgb/test/header_chain_test.cpp +++ b/src/impl/dgb/test/header_chain_test.cpp @@ -440,3 +440,49 @@ TEST(HeaderChainValidate, IngestPoWSatisfactionRunsAtFullWidth) EXPECT_EQ(hc.validate_and_append(weak), IngestResult::REJECTED); EXPECT_EQ(hc.size(), 1u); // unchanged } + +// --------------------------------------------------------------------------- +// 256-BIT DIGEST INGEST (M3 7b) -- bytes -> field conversion. The embedded +// DigiByte Core port produces the Scrypt PoW hash as a 32-byte little-endian +// digest (scrypt_1024_1_1_256 output). u256::from_le_bytes is the bytes->field +// half of dropping that digest into HeaderSample::pow_hash; the scrypt CALL +// itself (btclibs) lands at the ingest boundary in a following slice. Pins the +// little-endian limb mapping (limb[0] = least-significant 8 bytes, matching +// bitcoin UintToArith256) and proves a converted digest feeds the existing +// full-width satisfaction gate with the SAME ordering -- no low64 proxy. +// --------------------------------------------------------------------------- +TEST(HeaderChainValidate, ScryptDigestLittleEndianConversion) +{ + // Byte i = i+1, so each limb is a distinct, easily-checked LE word. + unsigned char d[32]; + for (int i = 0; i < 32; ++i) d[i] = (unsigned char)(i + 1); + u256 h = u256::from_le_bytes(d); + EXPECT_EQ(h.limb[0], 0x0807060504030201ULL); + EXPECT_EQ(h.limb[1], 0x100f0e0d0c0b0a09ULL); + EXPECT_EQ(h.limb[2], 0x1817161514131211ULL); + EXPECT_EQ(h.limb[3], 0x201f1e1d1c1b1a19ULL); + + // All-zero digest -> 0 (trivially satisfies any non-zero target). + unsigned char zero[32] = {0}; + EXPECT_TRUE(u256::from_le_bytes(zero).is_zero()); + + // All-0xff digest -> MAX 256-bit value (every limb saturated), exceeding + // any real Scrypt target -> would NOT satisfy PoW. + unsigned char ones[32]; + for (int i = 0; i < 32; ++i) ones[i] = 0xff; + u256 maxv = u256::from_le_bytes(ones); + for (int i = 0; i < 4; ++i) EXPECT_EQ(maxv.limb[i], 0xffffffffffffffffULL); + + // Feed a converted digest through the live satisfaction gate: digest 9 + // (low byte) vs target 2^192 + 5 -> 9 <= 2^192+5 at full width -> VALIDATED. + // Proves the converted value flows the SAME full-width path the field-shape + // swap widened, not a low64 proxy. + HeaderChain hc; + u256 target; target.limb[3] = 1; target.limb[0] = 5; + unsigned char nine[32] = {0}; nine[0] = 9; + HeaderSample s{SCRYPT, 1000}; + s.target = target; + s.pow_hash = u256::from_le_bytes(nine); + EXPECT_EQ(hc.validate_and_append(s), IngestResult::VALIDATED_SCRYPT); + EXPECT_EQ(hc.size(), 1u); +} From f49a89e728f4ff1beb173e19674cfd91f9d93cdc Mon Sep 17 00:00:00 2001 From: frstrtr Date: Thu, 18 Jun 2026 13:58:01 +0000 Subject: [PATCH 2/2] =?UTF-8?q?dgb:=20M3=20=C2=A77b=20prove=20scrypt=20dig?= =?UTF-8?q?est->pow=5Fhash=20conversion=20vs=20REAL=20scrypt=5F1024=5F1=5F?= =?UTF-8?q?1=5F256?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Links the btclibs Scrypt PoW TU set (scrypt.cpp + SHA256 family) into header_chain_test and adds an end-to-end test that runs the real scrypt_1024_1_1_256 over an 80-byte header, converts the 32-byte LE digest via u256::from_le_bytes, pins it as a self-derived KAT, and feeds it through the live full-width satisfaction gate (accept vs pow_limit, reject vs a tiny target -> proves full-width ordering, not a low64 proxy). Compiles only the scrypt + SHA256 crypto TUs directly (not the full btclibs archive, whose siphash TU needs the removed base_uint<256> symbol, and not core/the dgb OBJECT lib) -- linking an existing scrypt.cpp, no shared source modified. Same target name, still in both build.yml --target allowlists. Proves the conversion primitive end-to-end against a genuine Scrypt digest; does NOT wire scrypt into the production node.cpp ingest path (that lands in embedded-daemon Phase A, scoped with an exe-smoke). --- src/impl/dgb/test/CMakeLists.txt | 24 ++++++++- src/impl/dgb/test/header_chain_test.cpp | 66 +++++++++++++++++++++++-- 2 files changed, 86 insertions(+), 4 deletions(-) diff --git a/src/impl/dgb/test/CMakeLists.txt b/src/impl/dgb/test/CMakeLists.txt index 4446c52b3..416a7ed94 100644 --- a/src/impl/dgb/test/CMakeLists.txt +++ b/src/impl/dgb/test/CMakeLists.txt @@ -25,10 +25,32 @@ 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 header_chain_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) gtest_add_tests(${dgb_guard} "" AUTO) endforeach() + + # header_chain_test compiles the btclibs Scrypt PoW TU set DIRECTLY (the + # same crypto/*.cpp sources btclibs builds, with the same default flags) so + # the M3 7b digest conversion is exercised against the REAL + # scrypt_1024_1_1_256 hash, not a synthetic digest. Only the scrypt + SHA256 + # family is pulled in -- NOT the full btclibs archive (its siphash.o needs + # the removed base_uint<256> symbol) and NOT core/the dgb OBJECT lib. This + # is linking against an existing scrypt.cpp, no shared source modified: the + # single DGB smoke gate holds, no shared-base flag owed. Same target name, + # so it stays in BOTH build.yml --target allowlists unchanged. + set(_dgb_scrypt_tus + ${CMAKE_SOURCE_DIR}/src/btclibs/crypto/scrypt.cpp + ${CMAKE_SOURCE_DIR}/src/btclibs/crypto/sha256.cpp + ${CMAKE_SOURCE_DIR}/src/btclibs/crypto/sha256_sse4.cpp + ${CMAKE_SOURCE_DIR}/src/btclibs/crypto/sha256_sse41.cpp + ${CMAKE_SOURCE_DIR}/src/btclibs/crypto/sha256_avx2.cpp + ${CMAKE_SOURCE_DIR}/src/btclibs/crypto/sha256_shani.cpp) + add_executable(header_chain_test header_chain_test.cpp ${_dgb_scrypt_tus}) + target_include_directories(header_chain_test PRIVATE ${CMAKE_SOURCE_DIR}/src/btclibs) + target_link_libraries(header_chain_test PRIVATE + GTest::gtest_main GTest::gtest nlohmann_json::nlohmann_json) + gtest_add_tests(header_chain_test "" AUTO) endif() diff --git a/src/impl/dgb/test/header_chain_test.cpp b/src/impl/dgb/test/header_chain_test.cpp index 6285c65d6..dddaa792a 100644 --- a/src/impl/dgb/test/header_chain_test.cpp +++ b/src/impl/dgb/test/header_chain_test.cpp @@ -12,16 +12,21 @@ // 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. +// Links the header-only chain helpers + gtest, plus the btclibs Scrypt PoW TU +// set (scrypt.cpp + the SHA256 family) so the M3 7b digest conversion runs +// against the REAL scrypt_1024_1_1_256 hash -- NOT the full btclibs archive, +// NOT the dgb OBJECT lib / transport. MUST appear in BOTH the test/CMakeLists +// dedicated target block AND both build.yml --target allowlists, or it becomes +// a #143 NOT_BUILT sentinel. // --------------------------------------------------------------------------- #include +#include #include #include +#include using namespace c2pool::dgb; using dgb::coin::DGB_BLOCK_VERSION_SCRYPT; @@ -486,3 +491,58 @@ TEST(HeaderChainValidate, ScryptDigestLittleEndianConversion) EXPECT_EQ(hc.validate_and_append(s), IngestResult::VALIDATED_SCRYPT); EXPECT_EQ(hc.size(), 1u); } + + +// --------------------------------------------------------------------------- +// REAL SCRYPT DIGEST -> pow_hash, END-TO-END (M3 7b, slice (a)). The prior test +// pins the from_le_bytes limb mapping against SYNTHETIC bytes; this one closes +// the loop by running the ACTUAL btclibs scrypt_1024_1_1_256 PoW hash over an +// 80-byte header, converting the 32-byte little-endian digest via from_le_bytes, +// and feeding it through the live full-width satisfaction gate. Proves the +// conversion primitive is correct against a GENUINE Scrypt digest. It does NOT +// prove scrypt is wired into the production node.cpp ingest path -- that +// (b)-shaped call lands in embedded-daemon Phase A, scoped with an exe-smoke. +// --------------------------------------------------------------------------- +TEST(HeaderChainValidate, RealScryptDigestFeedsSatisfactionGate) +{ + // Deterministic 80-byte header (content arbitrary for the conversion proof; + // fixed so the digest is a stable self-derived KAT). + char header[80]; + for (int i = 0; i < 80; ++i) header[i] = (char)(i + 1); + + unsigned char digest[32]; + scrypt_1024_1_1_256(header, reinterpret_cast(digest)); + + // Deterministic: a second run yields the identical digest. + unsigned char digest2[32]; + scrypt_1024_1_1_256(header, reinterpret_cast(digest2)); + EXPECT_EQ(0, std::memcmp(digest, digest2, 32)); + + // Self-derived KAT: pin the actual scrypt PoW hash for this fixed header so + // any drift in scrypt OR the LE conversion is caught. limb[0] is the least- + // significant 8 bytes (from_le_bytes / UintToArith256 ordering). + u256 h = u256::from_le_bytes(digest); + EXPECT_EQ(h.limb[0], 0x3863b84a2bd8d4c7ULL); + EXPECT_EQ(h.limb[1], 0xdbc48fcba50b456bULL); + EXPECT_EQ(h.limb[2], 0x22185f7f7b983293ULL); + EXPECT_EQ(h.limb[3], 0x64d9ed52d29c3fbdULL); + + // The real digest, converted, satisfies a max (pow_limit) target -> VALID. + HeaderChain hc; + u256 max_target; for (int i = 0; i < 4; ++i) max_target.limb[i] = 0xffffffffffffffffULL; + HeaderSample ok{SCRYPT, 1000}; + ok.target = max_target; + ok.pow_hash = h; + EXPECT_EQ(hc.validate_and_append(ok), IngestResult::VALIDATED_SCRYPT); + EXPECT_EQ(hc.size(), 1u); + + // Same real digest vs a tiny target (value 1): the high limbs are nonzero + // (0x64d9...) so hash > 1 at full width -> REJECT. A low64-only proxy could + // not see the high limbs -- this proves the converted digest flows the + // full-width satisfaction path, not a truncated comparison. No mutation. + HeaderSample weak{SCRYPT, 1075}; + weak.target.limb[0] = 1; // value == 1, all high limbs zero + weak.pow_hash = h; + EXPECT_EQ(hc.validate_and_append(weak), IngestResult::REJECTED); + EXPECT_EQ(hc.size(), 1u); // unchanged +}