diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index db1c1eeb5..2dbf796a1 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -84,7 +84,7 @@ jobs: dgb_gentx_coinbase_test nmc_auxpow_merkle_test nmc_template_builder_test nmc_auxpow_wire_test nmc_reconstruct_won_block_test dgb_gentx_share_path_test dgb_other_tx_resolver_test \ dgb_other_tx_assembler_test dgb_reconstruct_won_block_test dgb_reconstruct_closure_test dgb_gentx_unpack_test dgb_work_source_test dgb_template_builder_test dgb_embedded_coin_node_test dgb_embedded_tx_select_test dgb_coinbase_value_parity_test \ rpc_request_test softfork_check_test genesis_check_test algo_select_test digishield_walk_test header_chain_test \ - dgb_coin_node_seam_test dgb_block_broadcast_test dgb_won_block_dispatch_test dgb_forced_won_share_dualpath_test \ + dgb_coin_node_seam_test dgb_block_broadcast_test dgb_won_block_dispatch_test dgb_forced_won_share_dualpath_test dgb_scrypt_pow_test dgb_nonce_grinder_test dgb_regrind_block_test \ v37_test \ -j$(nproc) @@ -217,7 +217,7 @@ jobs: dgb_gentx_coinbase_test nmc_auxpow_merkle_test nmc_template_builder_test nmc_auxpow_wire_test nmc_reconstruct_won_block_test dgb_gentx_share_path_test dgb_other_tx_resolver_test \ dgb_other_tx_assembler_test dgb_reconstruct_won_block_test dgb_reconstruct_closure_test dgb_gentx_unpack_test dgb_work_source_test dgb_template_builder_test dgb_embedded_coin_node_test dgb_embedded_tx_select_test dgb_coinbase_value_parity_test \ rpc_request_test softfork_check_test genesis_check_test algo_select_test digishield_walk_test header_chain_test \ - dgb_coin_node_seam_test dgb_block_broadcast_test dgb_won_block_dispatch_test dgb_forced_won_share_dualpath_test \ + dgb_coin_node_seam_test dgb_block_broadcast_test dgb_won_block_dispatch_test dgb_forced_won_share_dualpath_test dgb_scrypt_pow_test dgb_nonce_grinder_test dgb_regrind_block_test \ test_coin_broadcaster test_multiaddress_pplns test_pplns_stress \ v37_test \ -j$(nproc) diff --git a/src/impl/dgb/coin/nonce_grinder.hpp b/src/impl/dgb/coin/nonce_grinder.hpp new file mode 100644 index 000000000..fa597b4c4 --- /dev/null +++ b/src/impl/dgb/coin/nonce_grinder.hpp @@ -0,0 +1,99 @@ +#pragma once +// --------------------------------------------------------------------------- +// dgb::coin::grind_won_nonce -- the embedded real work-gen nonce grinder +// (#82 Stage 4b/4c). Given a reconstructed 80-byte DigiByte block header and +// the parent target, it increments the header nonce until the DGB-Scrypt PoW +// digest satisfies the target, returning the winning nonce. This is the +// missing primitive between the faithful won-block RECONSTRUCTION (#82 leg-2, +// reconstruct_won_block.hpp) and a node-B ProcessNewBlock ACCEPT: the A/B +// delivery proof reached node B but was consensus-rejected "high-hash, proof +// of work failed" precisely because the forced-won seam carried no real PoW. +// The grinder closes that gap. +// +// SSOT call-through: the hash is computed ONLY via scrypt_pow_hash (the #286 +// digest CALL SSOT), never a private/bypass routine -- so a nonce this grinder +// accepts is, by construction, a nonce node B's own Scrypt validation accepts. +// The comparison is the EXACT satisfaction gate coin/header_chain.hpp runs: +// pow_hash <= target (inclusive), MSB-first via u256::operator>. +// +// Nonce placement: the canonical bitcoin/DigiByte 80-byte header is +// version(4)|prev(32)|merkle(32)|time(4)|bits(4)|nonce(4) -- the nonce is the +// LAST field, bytes [76..79], little-endian. This is the identical layout +// pack(BlockHeaderType) emits (header_sample_build.hpp serializes the header +// once and feeds the SAME bytes to sha256d AND scrypt), so a header serialized +// at the ingest/reconstruct boundary and ground here agree byte-for-byte. +// +// Standalone-guard discipline: header-only, depends ONLY on scrypt_pow.hpp +// (which pulls real btclibs scrypt + the header-only u256). No core, no dgb +// OBJECT lib -- the same no-link constraint dgb_arith256.hpp / scrypt_pow.hpp +// keep, so its guard TU links the _dgb_scrypt_tus real-scrypt set, not a +// synthetic hash. Wiring grind -> reconstruct -> submit onto the live +// forced-won path (serialize BlockHeaderType, grind, write the winning nonce +// back) is the explicitly-next integration slice; this is the pure primitive. +// +// Per-coin isolation: src/impl/dgb/ only. p2pool-merged-v36 surface: NONE -- +// DGB-Scrypt is a STANDALONE parent; no share format, coinbase commitment, or +// PPLNS math is touched. +// --------------------------------------------------------------------------- + +#include +#include +#include +#include + +#include // scrypt_pow_hash (PoW digest SSOT, #286) +#include // dgb::coin::u256 (also via scrypt_pow.hpp) + +namespace dgb::coin { + +// Byte offset of the 4-byte little-endian nonce in the canonical 80-byte header. +inline constexpr std::size_t kHeaderNonceOffset = 76; + +// Write a 32-bit nonce little-endian into header[76..79] (consensus byte order). +inline void put_header_nonce_le(std::array& header, uint32_t nonce) { + header[kHeaderNonceOffset + 0] = static_cast(nonce & 0xffu); + header[kHeaderNonceOffset + 1] = static_cast((nonce >> 8) & 0xffu); + header[kHeaderNonceOffset + 2] = static_cast((nonce >> 16) & 0xffu); + header[kHeaderNonceOffset + 3] = static_cast((nonce >> 24) & 0xffu); +} + +struct GrindOutcome { + uint32_t nonce; // winning nonce -- also left written into header[76..79] + u256 pow_hash; // scrypt_pow_hash of the winning header (<= target) + uint64_t iters; // nonces tried, 1-based -- the search budget consumed +}; + +// Grind header[76..79] from start_nonce until scrypt_pow_hash(header) <= target. +// +// header : the reconstructed 80-byte header; on success its nonce field +// is left set to the winning value (caller broadcasts THIS blob) +// target : compact_to_target(nBits) for the parent -- the SAME u256 the +// satisfaction gate compares pow_hash against +// start_nonce : where to begin the search (default 0) +// max_iters : search budget. Regtest powLimit is trivially easy, so the live +// forced-won path satisfies in very few iters; this is the guard +// that the grinder TERMINATES (no infinite loop), not an expected +// ceiling. Defaults to the full 2^32 nonce space. +// +// Returns the winning {nonce, pow_hash, iters} on success, or std::nullopt if +// the budget (or the full 2^32 space) is exhausted with no satisfying nonce. +// "No nonce found in budget" is a hard failure the caller MUST NOT broadcast a +// header for -- failing closed mirrors the reconstructor's loud-throw posture: +// a header that does not satisfy its target is rejected by the daemon anyway. +inline std::optional +grind_won_nonce(std::array& header, const u256& target, + uint32_t start_nonce = 0, + uint64_t max_iters = (uint64_t{1} << 32)) { + uint32_t nonce = start_nonce; + for (uint64_t tried = 1; tried <= max_iters; ++tried) { + put_header_nonce_le(header, nonce); + u256 pow = scrypt_pow_hash(header); + if (!(pow > target)) // pow <= target -> valid PoW (inclusive) + return GrindOutcome{nonce, pow, tried}; + ++nonce; + if (nonce == start_nonce) break; // wrapped full 2^32 space, none satisfied + } + return std::nullopt; +} + +} // namespace dgb::coin diff --git a/src/impl/dgb/coin/regrind_block.hpp b/src/impl/dgb/coin/regrind_block.hpp new file mode 100644 index 000000000..f2e777e4d --- /dev/null +++ b/src/impl/dgb/coin/regrind_block.hpp @@ -0,0 +1,93 @@ +#pragma once +// --------------------------------------------------------------------------- +// dgb::coin::regrind_block_nonce -- the grind -> reconstruct -> submit +// INTEGRATION seam (#82 Stage 4b/4c). The missing wiring between the faithful +// won-block RECONSTRUCTION (reconstruct_closure.hpp -> {bytes, hex}) and a +// node-B ProcessNewBlock ACCEPT. +// +// reconstruct_won_block_from_template frames a byte-faithful parent block, but +// the header it carries is the SHARE's small_header verbatim -- whose nonce, on +// the forced-won soak path, was never ground to satisfy the PARENT target. So +// the A/B delivery proof reached node B and was consensus-rejected "high-hash, +// proof of work failed". This seam closes that: given the already-framed block +// blob it grinds the header nonce through the #286 scrypt_pow_hash SSOT until +// the DGB-Scrypt PoW digest satisfies the parent target, writing the winning +// nonce back into the serialized header IN PLACE. +// +// Ordering invariant (integrator 2026-06-21): the merkle_root is fixed FIRST +// (reconstruct recomputes it from gentx_hash up the share merkle_link), THEN +// the nonce is ground -- scrypt hashes the full 80 bytes, so grinding before +// the merkle is fixed would invalidate the found nonce. This seam ENFORCES that +// order structurally: it only ever runs on an already-framed block, and it +// mutates ONLY header bytes [76..79] (kHeaderNonceOffset), never the merkle +// region [36..67] or the tx tail -- so a nonce it finds is valid for the block +// exactly as reconstructed. +// +// SSOT call-through: the hash is computed ONLY via grind_won_nonce -> +// scrypt_pow_hash (the #286 digest CALL SSOT), and the satisfaction comparison +// is the EXACT gate coin/header_chain.hpp runs (pow_hash <= target, inclusive). +// A nonce this seam accepts is, by construction, one node B accepts. +// +// FAIL-CLOSED: returns std::nullopt (block_bytes left UNCHANGED) on a runt blob +// (< 80 bytes, not even a header) or budget exhaustion -- mirroring the +// reconstructor's posture. A header that does not satisfy its target is +// daemon-rejected anyway, so the caller MUST NOT broadcast on a nullopt. +// +// Standalone-guard discipline: header-only, depends ONLY on nonce_grinder.hpp +// (-> scrypt_pow.hpp -> real btclibs scrypt + header-only u256). No core, no +// dgb OBJECT lib -- the same no-link constraint scrypt_pow/nonce_grinder keep, +// so its guard TU links the _dgb_scrypt_tus real-scrypt set. +// +// Per-coin isolation: src/impl/dgb/ only. DGB-Scrypt is a STANDALONE parent in +// the V36 default build. p2pool-merged-v36 surface: NONE -- no share format, +// coinbase commitment, or PPLNS math is touched; only the parent block header +// nonce, exactly as a real miner would have ground it. +// --------------------------------------------------------------------------- + +#include +#include +#include +#include +#include + +#include // grind_won_nonce, GrindOutcome, kHeaderNonceOffset, u256 + +namespace dgb::coin { + +// Grind the nonce of an already-framed reconstructed block (block_bytes) until +// its DGB-Scrypt PoW digest satisfies the parent target, writing the winning +// nonce back into the serialized 80-byte header [76..79] IN PLACE. +// +// block_bytes : the reconstruct_won_block_from_template {bytes} blob -- +// header(80) | varint(txcount) | txs. On success bytes [76..79] +// are overwritten with the winning nonce; ALL other bytes +// (merkle root, tx tail) are untouched. On failure UNCHANGED. +// target : compact_to_target(nBits) for the parent -- the SAME u256 the +// satisfaction gate compares pow_hash against. +// start_nonce : where to begin the search (default 0). +// max_iters : search budget (default the full 2^32 nonce space). Regtest +// powLimit is trivially easy; this is the TERMINATION guard. +// +// Returns the winning {nonce, pow_hash, iters} on success (block_bytes mutated +// in place), or std::nullopt on a runt blob or exhausted budget (UNCHANGED). +inline std::optional +regrind_block_nonce(std::vector& block_bytes, const u256& target, + uint32_t start_nonce = 0, + uint64_t max_iters = (uint64_t{1} << 32)) +{ + if (block_bytes.size() < 80) return std::nullopt; // not even a header -- fail closed + + std::array header{}; + for (std::size_t i = 0; i < 80; ++i) header[i] = block_bytes[i]; + + auto out = grind_won_nonce(header, target, start_nonce, max_iters); + if (!out) return std::nullopt; // budget exhausted -- bytes UNCHANGED + + // Winning nonce -> back into the block blob, header [76..79] ONLY. The + // merkle root [36..67] and the tx tail are never touched, so the block is + // valid exactly as reconstructed. + for (std::size_t i = kHeaderNonceOffset; i < 80; ++i) block_bytes[i] = header[i]; + return out; +} + +} // namespace dgb::coin diff --git a/src/impl/dgb/coin/scrypt_pow.hpp b/src/impl/dgb/coin/scrypt_pow.hpp new file mode 100644 index 000000000..6509769c3 --- /dev/null +++ b/src/impl/dgb/coin/scrypt_pow.hpp @@ -0,0 +1,47 @@ +#pragma once +// --------------------------------------------------------------------------- +// DGB-Scrypt proof-of-work digest CALL (M3 ยง7b / Stage 4b-4c work-gen). +// +// The satisfaction gate in coin/header_chain.hpp compares HeaderSample::pow_hash +// (a coin/dgb_arith256.hpp u256) against the SetCompact target -- hash <= target. +// dgb_arith256.hpp::u256::from_le_bytes already documents the decode convention +// (the scrypt output is read little-endian, mirroring bitcoin UintToArith256), +// and ends: "the scrypt CALL itself lands at the ingest boundary in a following +// slice." THIS header is that call: the single place the DGB-Scrypt algo hash is +// computed, so the embedded work-gen (nonce grinder) and the ingest boundary +// share ONE digest SSOT and can never disagree on byte order. +// +// V36 is Scrypt-ONLY (project_v36_dgb_scrypt_only): this is the ONLY PoW digest +// DGB validates. The other four DGB algos (SHA256d/Skein/Qubit/Odocrypt) are +// accept-by-continuity and never reach this function. +// +// scrypt_1024_1_1_256 is the canonical pooler/ArtForz Scrypt(N=1024,r=1,p=1) +// from btclibs (src/btclibs/crypto/scrypt.*), the SAME routine DigiByte Core / +// Litecoin Core use for the Scrypt algo. It links transitively here via +// core -> btclibs (target_link_libraries(core PUBLIC ... btclibs)). +// --------------------------------------------------------------------------- + +#include +#include + +#include // scrypt_1024_1_1_256 +#include // dgb::coin::u256 + +namespace dgb::coin { + +// scrypt_1024_1_1_256 over the 80-byte serialized block header -> pow_hash u256. +// The 32-byte digest is decoded little-endian (from_le_bytes) so the result +// drops straight into the header_chain satisfaction gate with no reshape. +inline u256 scrypt_pow_hash(const unsigned char header80[80]) { + char digest[32]; + scrypt_1024_1_1_256(reinterpret_cast(header80), digest); + return u256::from_le_bytes(reinterpret_cast(digest)); +} + +// std::array convenience overload (the form the reconstructed-header builder and +// the nonce grinder carry the 80 header bytes in). +inline u256 scrypt_pow_hash(const std::array& header80) { + return scrypt_pow_hash(header80.data()); +} + +} // namespace dgb::coin diff --git a/src/impl/dgb/test/CMakeLists.txt b/src/impl/dgb/test/CMakeLists.txt index 55a64efa1..5e9eb0d6e 100644 --- a/src/impl/dgb/test/CMakeLists.txt +++ b/src/impl/dgb/test/CMakeLists.txt @@ -342,4 +342,50 @@ if (BUILD_TESTING AND GTest_FOUND) target_link_libraries(header_chain_test PRIVATE GTest::gtest_main GTest::gtest nlohmann_json::nlohmann_json) gtest_add_tests(header_chain_test "" AUTO) + + # --- Stage 4b/4c: DGB-Scrypt PoW digest CALL (coin/scrypt_pow.hpp) -------- + # Pins scrypt_pow_hash = scrypt_1024_1_1_256(80-byte header) decoded via + # u256::from_le_bytes, plus the satisfaction-gate byte order (pow_hash <= + # target) header_chain.hpp compares. Reuses the SAME _dgb_scrypt_tus set as + # header_chain_test (real scrypt, not synthetic) -- header-only u256, no core + # / no dgb OBJECT lib, the standalone-guard discipline dgb_arith256.hpp keeps. + # Same target name, so it stays in BOTH build.yml --target allowlists. + add_executable(dgb_scrypt_pow_test scrypt_pow_test.cpp ${_dgb_scrypt_tus}) + target_include_directories(dgb_scrypt_pow_test PRIVATE ${CMAKE_SOURCE_DIR}/src/btclibs) + target_link_libraries(dgb_scrypt_pow_test PRIVATE + GTest::gtest_main GTest::gtest) + gtest_add_tests(dgb_scrypt_pow_test "" AUTO) + + # --- Stage 4b/4c: DGB-Scrypt nonce grinder (coin/nonce_grinder.hpp) ------- + # Pins grind_won_nonce: increments the 80-byte header nonce until + # scrypt_pow_hash(header) <= target (the EXACT header_chain.hpp satisfaction + # gate), calling THROUGH the #286 scrypt_pow.hpp digest SSOT -- never a + # bypass hash -- so the nonce it finds is one node B accepts. Asserts it + # terminates well inside the test budget, writes the winning nonce LE into + # [76..79], and fails closed (nullopt) when no nonce satisfies in budget. + # Reuses the SAME _dgb_scrypt_tus real-scrypt set as dgb_scrypt_pow_test -- + # header-only u256, no core / no dgb OBJECT lib. Same standalone-guard + # discipline; MUST stay in BOTH build.yml --target allowlists (#143 trap). + add_executable(dgb_nonce_grinder_test nonce_grinder_test.cpp ${_dgb_scrypt_tus}) + target_include_directories(dgb_nonce_grinder_test PRIVATE ${CMAKE_SOURCE_DIR}/src/btclibs) + target_link_libraries(dgb_nonce_grinder_test PRIVATE + GTest::gtest_main GTest::gtest) + gtest_add_tests(dgb_nonce_grinder_test "" AUTO) + + # --- Stage 4b/4c: grind -> reconstruct -> submit seam (coin/regrind_block.hpp) + # Pins regrind_block_nonce: grinds the header nonce of an ALREADY-FRAMED + # reconstructed-block blob until its DGB-Scrypt PoW satisfies the parent + # target, writing the winning nonce back into header [76..79] IN PLACE -- + # the wiring between reconstruct (merkle root fixed) and a node-B + # ProcessNewBlock ACCEPT. Asserts the re-ground header satisfies the EXACT + # pow <= target gate via the #286 SSOT, that ONLY [76..79] mutate (merkle + # region [36..67] + tx tail byte-preserved), and fail-closed (nullopt, + # bytes UNCHANGED) on a runt blob or exhausted budget. Reuses the SAME + # _dgb_scrypt_tus real-scrypt set; header-only u256, no core / no OBJECT + # lib. MUST stay in BOTH build.yml --target allowlists (#143 trap). + add_executable(dgb_regrind_block_test regrind_block_test.cpp ${_dgb_scrypt_tus}) + target_include_directories(dgb_regrind_block_test PRIVATE ${CMAKE_SOURCE_DIR}/src/btclibs) + target_link_libraries(dgb_regrind_block_test PRIVATE + GTest::gtest_main GTest::gtest) + gtest_add_tests(dgb_regrind_block_test "" AUTO) endif() diff --git a/src/impl/dgb/test/nonce_grinder_test.cpp b/src/impl/dgb/test/nonce_grinder_test.cpp new file mode 100644 index 000000000..a27edec4e --- /dev/null +++ b/src/impl/dgb/test/nonce_grinder_test.cpp @@ -0,0 +1,113 @@ +// DGB nonce grinder guard (coin/nonce_grinder.hpp) -- #82 Stage 4b/4c. +// +// Proves the embedded real work-gen primitive: grind_won_nonce increments the +// header nonce until the DGB-Scrypt PoW digest satisfies the target, computing +// the hash ONLY through scrypt_pow_hash (the #286 digest CALL SSOT) and using +// the EXACT satisfaction comparison coin/header_chain.hpp runs (pow_hash <= +// target). The nonce it returns is therefore the nonce node B's own Scrypt +// validation accepts -- this is the unit floor under the live tip-extension +// (ProcessNewBlock ACCEPT) integration test. +// +// Real scrypt (the _dgb_scrypt_tus set), header-only u256 -- the same no-core / +// no-OBJECT-lib standalone-guard discipline as scrypt_pow_test / dgb_arith256. + +#include +#include +#include + +#include + +#include +#include + +using dgb::coin::u256; +using dgb::coin::grind_won_nonce; +using dgb::coin::scrypt_pow_hash; +using dgb::coin::kHeaderNonceOffset; + +namespace { + +// Arbitrary fixed 76-byte header prefix; the grinder owns bytes [76..79]. +std::array base_header() { + std::array h{}; + for (int i = 0; i < 80; ++i) h[i] = static_cast((i * 13 + 5) % 251); + return h; +} + +uint32_t read_nonce_le(const std::array& h) { + return (uint32_t)h[kHeaderNonceOffset + 0] + | ((uint32_t)h[kHeaderNonceOffset + 1] << 8) + | ((uint32_t)h[kHeaderNonceOffset + 2] << 16) + | ((uint32_t)h[kHeaderNonceOffset + 3] << 24); +} + +// Target accepting ~1/4 of uniform digests: top two bits of the MS limb zero +// (pow.limb[3] <= 0x3fff... with all lower limbs maxed). Expected ~4 iters, so +// termination "well inside the test budget" is overwhelming; max_iters is the +// hard ceiling, not the expected cost. +u256 easy_quarter_target() { + u256 t; + t.limb[0] = 0xffffffffffffffffULL; + t.limb[1] = 0xffffffffffffffffULL; + t.limb[2] = 0xffffffffffffffffULL; + t.limb[3] = 0x3fffffffffffffffULL; + return t; +} + +constexpr uint64_t kTestBudget = uint64_t{1} << 20; // 1,048,576 vs ~4 expected + +// --- grinder finds a satisfying nonce, well inside budget ------------------- +TEST(DgbNonceGrinderKAT, FindsSatisfyingNonceWithinBudget) { + auto h = base_header(); + auto out = grind_won_nonce(h, easy_quarter_target(), 0, kTestBudget); + ASSERT_TRUE(out.has_value()); + EXPECT_GE(out->iters, uint64_t{1}); + // P(iters > 200) = (3/4)^200 ~ 1e-25 -- terminates inside budget by construction. + EXPECT_LE(out->iters, uint64_t{200}); +} + +// --- the winning header actually satisfies the gate (pow <= target) --------- +TEST(DgbNonceGrinderKAT, WinningHeaderSatisfiesTarget) { + auto h = base_header(); + u256 target = easy_quarter_target(); + auto out = grind_won_nonce(h, target, 0, kTestBudget); + ASSERT_TRUE(out.has_value()); + // Recompute through the SAME #286 SSOT over the mutated header. + u256 pow = scrypt_pow_hash(h); + EXPECT_FALSE(pow > target); // pow <= target -> valid PoW + EXPECT_TRUE(pow == out->pow_hash); // reported digest == SSOT digest (no bypass) +} + +// --- the winning nonce is left written into header[76..79], little-endian ---- +TEST(DgbNonceGrinderKAT, WritesWinningNonceLittleEndian) { + auto h = base_header(); + auto out = grind_won_nonce(h, easy_quarter_target(), 0, kTestBudget); + ASSERT_TRUE(out.has_value()); + EXPECT_EQ(read_nonce_le(h), out->nonce); +} + +// --- determinism + start_nonce honored: re-grinding from the winner hits it +// on the first try (the found nonce truly satisfies AND start is respected). +TEST(DgbNonceGrinderKAT, ReGrindFromWinnerSucceedsImmediately) { + auto h = base_header(); + auto first = grind_won_nonce(h, easy_quarter_target(), 0, kTestBudget); + ASSERT_TRUE(first.has_value()); + auto h2 = base_header(); + auto again = grind_won_nonce(h2, easy_quarter_target(), first->nonce, kTestBudget); + ASSERT_TRUE(again.has_value()); + EXPECT_EQ(again->nonce, first->nonce); + EXPECT_EQ(again->iters, uint64_t{1}); // satisfied on the first nonce tried +} + +// --- fail-closed: an unsatisfiable target inside a small budget -> nullopt ---- +// target == 1 (limb[0]=1, rest 0): essentially no scrypt digest is <= 1, so the +// grinder MUST exhaust the budget and return nullopt rather than loop forever. +TEST(DgbNonceGrinderKAT, ReturnsNulloptWhenNoNonceInBudget) { + auto h = base_header(); + u256 impossible; // all-zero ... + impossible.limb[0] = 1; // ... except 1 -> target = 1 + auto out = grind_won_nonce(h, impossible, 0, /*max_iters=*/256); + EXPECT_FALSE(out.has_value()); +} + +} // namespace diff --git a/src/impl/dgb/test/regrind_block_test.cpp b/src/impl/dgb/test/regrind_block_test.cpp new file mode 100644 index 000000000..8fdf65b78 --- /dev/null +++ b/src/impl/dgb/test/regrind_block_test.cpp @@ -0,0 +1,139 @@ +// --------------------------------------------------------------------------- +// dgb_regrind_block_test -- the grind -> reconstruct -> submit INTEGRATION +// seam guard (coin/regrind_block.hpp), #82 Stage 4b/4c. +// +// Pins regrind_block_nonce: given an already-framed reconstructed-block blob +// (header(80) | varint(txcount) | txs), it grinds the header nonce through the +// #286 scrypt_pow_hash SSOT until the DGB-Scrypt PoW digest satisfies the +// parent target, writing the winning nonce back into header [76..79] IN PLACE. +// This is the missing wiring between reconstruct (merkle root fixed) and a +// node-B ProcessNewBlock ACCEPT: the live A/B soak is the corroborating +// evidence; THIS KAT is the binding proof that the re-ground block satisfies +// the EXACT satisfaction gate header_chain.hpp runs (pow <= target) while the +// merkle root and tx tail are byte-preserved. +// +// Real scrypt (the _dgb_scrypt_tus set), header-only u256 -- the same no-core / +// no-OBJECT-lib standalone-guard discipline as nonce_grinder_test / scrypt_pow. +// Per-coin isolation: src/impl/dgb/ only. MUST appear in BOTH test/CMakeLists +// AND the build.yml --target allowlist (#143 NOT_BUILT sentinel trap). +// --------------------------------------------------------------------------- + +#include +#include +#include +#include + +#include + +#include +#include + +using dgb::coin::u256; +using dgb::coin::regrind_block_nonce; +using dgb::coin::scrypt_pow_hash; +using dgb::coin::kHeaderNonceOffset; + +namespace { + +// A synthetic "framed block" blob: 80-byte header (arbitrary, with a non-zero +// merkle region [36..67]) followed by a short tx tail. The seam owns ONLY the +// header nonce [76..79]; everything else must survive byte-for-byte. +std::vector make_framed_block() { + std::vector b; + b.reserve(80 + 5); + for (int i = 0; i < 80; ++i) b.push_back(static_cast((i * 13 + 5) % 251)); + // tx tail: varint count(1) + 4 sentinel bytes (stand in for a tx blob). + b.push_back(0x01); + b.push_back(0xde); b.push_back(0xad); b.push_back(0xbe); b.push_back(0xef); + return b; +} + +uint32_t read_nonce_le(const std::vector& b) { + return (uint32_t)b[kHeaderNonceOffset + 0] + | ((uint32_t)b[kHeaderNonceOffset + 1] << 8) + | ((uint32_t)b[kHeaderNonceOffset + 2] << 16) + | ((uint32_t)b[kHeaderNonceOffset + 3] << 24); +} + +// Top two bits of the MS limb zero -> accepts ~1/4 of uniform digests, ~4 iters +// expected (mirrors nonce_grinder_test::easy_quarter_target). max_iters is the +// hard termination ceiling, not the expected cost. +u256 easy_quarter_target() { + u256 t; + t.limb[0] = 0xffffffffffffffffULL; + t.limb[1] = 0xffffffffffffffffULL; + t.limb[2] = 0xffffffffffffffffULL; + t.limb[3] = 0x3fffffffffffffffULL; + return t; +} + +constexpr uint64_t kTestBudget = uint64_t{1} << 20; // 1,048,576 vs ~4 expected + +// --- the re-ground block's header satisfies the EXACT satisfaction gate ------ +TEST(DgbRegrindBlockKAT, RegroundHeaderSatisfiesTarget) { + auto block = make_framed_block(); + u256 target = easy_quarter_target(); + + auto out = regrind_block_nonce(block, target, 0, kTestBudget); + ASSERT_TRUE(out.has_value()); + EXPECT_LE(out->iters, uint64_t{200}); // (3/4)^200 ~ 1e-25 inside budget + + // Recompute through the SAME #286 SSOT over the mutated header [0..79]. + std::array hdr{}; + for (int i = 0; i < 80; ++i) hdr[i] = block[i]; + u256 pow = scrypt_pow_hash(hdr); + EXPECT_FALSE(pow > target); // pow <= target (inclusive gate) + // The winning nonce the seam reports is the one it wrote back into [76..79]. + EXPECT_EQ(read_nonce_le(block), out->nonce); +} + +// --- ONLY header [76..79] changes: merkle root + tx tail byte-preserved ------ +TEST(DgbRegrindBlockKAT, OnlyNonceBytesMutated) { + auto before = make_framed_block(); + auto after = before; + + auto out = regrind_block_nonce(after, easy_quarter_target(), 0, kTestBudget); + ASSERT_TRUE(out.has_value()); + ASSERT_EQ(before.size(), after.size()); + + for (std::size_t i = 0; i < after.size(); ++i) { + if (i >= kHeaderNonceOffset && i < 80) continue; // the 4 nonce bytes may differ + EXPECT_EQ(before[i], after[i]) << "byte " << i << " (outside nonce) changed"; + } + // In particular the merkle region [36..67] and the tx tail are untouched. + for (std::size_t i = 36; i < 68; ++i) EXPECT_EQ(before[i], after[i]); + for (std::size_t i = 80; i < after.size(); ++i) EXPECT_EQ(before[i], after[i]); +} + +// --- fail-closed on a runt blob (< 80 bytes): nullopt, bytes UNCHANGED ------- +TEST(DgbRegrindBlockKAT, RuntBlobFailsClosed) { + std::vector runt(79, 0x00); // one byte short of a header + auto copy = runt; + auto out = regrind_block_nonce(runt, easy_quarter_target(), 0, kTestBudget); + EXPECT_FALSE(out.has_value()); + EXPECT_EQ(runt, copy); // unchanged +} + +// --- fail-closed on exhausted budget: nullopt, bytes UNCHANGED --------------- +TEST(DgbRegrindBlockKAT, ExhaustedBudgetFailsClosed) { + auto block = make_framed_block(); + auto copy = block; + u256 impossible; // all-zero target -- no digest can be <= 0 in practice + auto out = regrind_block_nonce(block, impossible, 0, /*max_iters=*/256); + EXPECT_FALSE(out.has_value()); + EXPECT_EQ(block, copy); // bytes left exactly as reconstructed +} + +// --- deterministic: same blob + target -> same winning nonce ----------------- +TEST(DgbRegrindBlockKAT, Deterministic) { + auto a = make_framed_block(); + auto b = make_framed_block(); + auto oa = regrind_block_nonce(a, easy_quarter_target(), 0, kTestBudget); + auto ob = regrind_block_nonce(b, easy_quarter_target(), 0, kTestBudget); + ASSERT_TRUE(oa.has_value()); + ASSERT_TRUE(ob.has_value()); + EXPECT_EQ(oa->nonce, ob->nonce); + EXPECT_EQ(a, b); // identical re-ground blobs +} + +} // namespace diff --git a/src/impl/dgb/test/scrypt_pow_test.cpp b/src/impl/dgb/test/scrypt_pow_test.cpp new file mode 100644 index 000000000..f9af6edaa --- /dev/null +++ b/src/impl/dgb/test/scrypt_pow_test.cpp @@ -0,0 +1,81 @@ +// DGB-Scrypt PoW digest CALL guard (coin/scrypt_pow.hpp) -- Stage 4b/4c. +// +// Pins (1) the scrypt_1024_1_1_256 digest bytes over a fixed 80-byte header and +// (2) that the little-endian u256 decode compares correctly against SetCompact- +// shaped targets the way the coin/header_chain.hpp satisfaction gate does +// (pow_hash <= target == valid PoW). The byte order proven here is exactly what +// the embedded nonce grinder must satisfy for node B to ACCEPT a reconstructed +// block, so this is the unit floor under the live tip-extension test. +// +// The digest vector is pinned from btclibs scrypt_1024_1_1_256 itself (the DGB- +// Scrypt algo SSOT, same routine as DigiByte Core); end-to-end agreement with +// DigiByte Core is proven by the live node-B ACCEPT integration test, not here. + +#include +#include + +#include + +using dgb::coin::u256; +using dgb::coin::scrypt_pow_hash; + +namespace { + +// Deterministic 80-byte header: byte i = (i*7+1) mod 251. Arbitrary content but +// fixed; the conversion + comparison proof does not depend on header semantics. +std::array fixed_header() { + std::array h{}; + for (int i = 0; i < 80; ++i) h[i] = static_cast((i * 7 + 1) % 251); + return h; +} + +// scrypt_1024_1_1_256(fixed_header) little-endian u256 limbs (limb[0] = LSB), +// captured from btclibs. digest LE-in-memory: +// 632b7c4db1da77a9683731a4a7a97761a2f600f66a1af420919a3aa0d4869a6c +constexpr uint64_t L0 = 0xa977dab14d7c2b63ULL; +constexpr uint64_t L1 = 0x6177a9a7a4313768ULL; +constexpr uint64_t L2 = 0x20f41a6af600f6a2ULL; +constexpr uint64_t L3 = 0x6c9a86d4a03a9a91ULL; + +u256 expected_pow() { + u256 r; + r.limb[0] = L0; r.limb[1] = L1; r.limb[2] = L2; r.limb[3] = L3; + return r; +} + +// --- KAT: the digest CALL is byte-exact ------------------------------------ +TEST(DgbScryptPowKAT, DigestMatchesPinnedVector) { + EXPECT_TRUE(scrypt_pow_hash(fixed_header()) == expected_pow()); +} + +// The pointer overload and the std::array overload are the same SSOT. +TEST(DgbScryptPowKAT, PointerAndArrayOverloadsAgree) { + auto h = fixed_header(); + EXPECT_TRUE(scrypt_pow_hash(h.data()) == scrypt_pow_hash(h)); +} + +// --- satisfaction-gate byte order (header_chain.hpp: pow_hash <= target) ---- +// A target one ULP above the digest in the MOST-significant limb accepts; +// one ULP below rejects. This is the exact comparison header_chain runs, so it +// proves from_le_bytes feeds the gate in the right (MSB-first compare) order. +TEST(DgbScryptPowKAT, SatisfiesTargetJustAbove) { + u256 pow = scrypt_pow_hash(fixed_header()); + u256 target_above = expected_pow(); + target_above.limb[3] = L3 + 1; // strictly greater + EXPECT_FALSE(pow > target_above); // pow <= target -> valid PoW +} + +TEST(DgbScryptPowKAT, FailsTargetJustBelow) { + u256 pow = scrypt_pow_hash(fixed_header()); + u256 target_below = expected_pow(); + target_below.limb[3] = L3 - 1; // strictly less + EXPECT_TRUE(pow > target_below); // pow > target -> PoW failed (high-hash) +} + +// Equal target is satisfied (hash <= target is inclusive). +TEST(DgbScryptPowKAT, SatisfiesEqualTarget) { + u256 pow = scrypt_pow_hash(fixed_header()); + EXPECT_FALSE(pow > expected_pow()); +} + +} // namespace