Skip to content

Commit 7f12a47

Browse files
authored
Merge pull request #286 from frstrtr/dgb/82-4bc-scrypt-pow-ssot
dgb(#82 4b/4c): scrypt_pow.hpp digest CALL SSOT + satisfaction-gate KAT
2 parents 68f1028 + 8e1f1ba commit 7f12a47

8 files changed

Lines changed: 620 additions & 2 deletions

File tree

.github/workflows/build.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,7 @@ jobs:
8484
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 \
8585
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 \
8686
rpc_request_test softfork_check_test genesis_check_test algo_select_test digishield_walk_test header_chain_test \
87-
dgb_coin_node_seam_test dgb_block_broadcast_test dgb_won_block_dispatch_test dgb_forced_won_share_dualpath_test \
87+
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 \
8888
v37_test \
8989
-j$(nproc)
9090
@@ -217,7 +217,7 @@ jobs:
217217
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 \
218218
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 \
219219
rpc_request_test softfork_check_test genesis_check_test algo_select_test digishield_walk_test header_chain_test \
220-
dgb_coin_node_seam_test dgb_block_broadcast_test dgb_won_block_dispatch_test dgb_forced_won_share_dualpath_test \
220+
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 \
221221
test_coin_broadcaster test_multiaddress_pplns test_pplns_stress \
222222
v37_test \
223223
-j$(nproc)
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
#pragma once
2+
// ---------------------------------------------------------------------------
3+
// dgb::coin::grind_won_nonce -- the embedded real work-gen nonce grinder
4+
// (#82 Stage 4b/4c). Given a reconstructed 80-byte DigiByte block header and
5+
// the parent target, it increments the header nonce until the DGB-Scrypt PoW
6+
// digest satisfies the target, returning the winning nonce. This is the
7+
// missing primitive between the faithful won-block RECONSTRUCTION (#82 leg-2,
8+
// reconstruct_won_block.hpp) and a node-B ProcessNewBlock ACCEPT: the A/B
9+
// delivery proof reached node B but was consensus-rejected "high-hash, proof
10+
// of work failed" precisely because the forced-won seam carried no real PoW.
11+
// The grinder closes that gap.
12+
//
13+
// SSOT call-through: the hash is computed ONLY via scrypt_pow_hash (the #286
14+
// digest CALL SSOT), never a private/bypass routine -- so a nonce this grinder
15+
// accepts is, by construction, a nonce node B's own Scrypt validation accepts.
16+
// The comparison is the EXACT satisfaction gate coin/header_chain.hpp runs:
17+
// pow_hash <= target (inclusive), MSB-first via u256::operator>.
18+
//
19+
// Nonce placement: the canonical bitcoin/DigiByte 80-byte header is
20+
// version(4)|prev(32)|merkle(32)|time(4)|bits(4)|nonce(4) -- the nonce is the
21+
// LAST field, bytes [76..79], little-endian. This is the identical layout
22+
// pack(BlockHeaderType) emits (header_sample_build.hpp serializes the header
23+
// once and feeds the SAME bytes to sha256d AND scrypt), so a header serialized
24+
// at the ingest/reconstruct boundary and ground here agree byte-for-byte.
25+
//
26+
// Standalone-guard discipline: header-only, depends ONLY on scrypt_pow.hpp
27+
// (which pulls real btclibs scrypt + the header-only u256). No core, no dgb
28+
// OBJECT lib -- the same no-link constraint dgb_arith256.hpp / scrypt_pow.hpp
29+
// keep, so its guard TU links the _dgb_scrypt_tus real-scrypt set, not a
30+
// synthetic hash. Wiring grind -> reconstruct -> submit onto the live
31+
// forced-won path (serialize BlockHeaderType, grind, write the winning nonce
32+
// back) is the explicitly-next integration slice; this is the pure primitive.
33+
//
34+
// Per-coin isolation: src/impl/dgb/ only. p2pool-merged-v36 surface: NONE --
35+
// DGB-Scrypt is a STANDALONE parent; no share format, coinbase commitment, or
36+
// PPLNS math is touched.
37+
// ---------------------------------------------------------------------------
38+
39+
#include <array>
40+
#include <cstddef>
41+
#include <cstdint>
42+
#include <optional>
43+
44+
#include <impl/dgb/coin/scrypt_pow.hpp> // scrypt_pow_hash (PoW digest SSOT, #286)
45+
#include <impl/dgb/coin/dgb_arith256.hpp> // dgb::coin::u256 (also via scrypt_pow.hpp)
46+
47+
namespace dgb::coin {
48+
49+
// Byte offset of the 4-byte little-endian nonce in the canonical 80-byte header.
50+
inline constexpr std::size_t kHeaderNonceOffset = 76;
51+
52+
// Write a 32-bit nonce little-endian into header[76..79] (consensus byte order).
53+
inline void put_header_nonce_le(std::array<unsigned char, 80>& header, uint32_t nonce) {
54+
header[kHeaderNonceOffset + 0] = static_cast<unsigned char>(nonce & 0xffu);
55+
header[kHeaderNonceOffset + 1] = static_cast<unsigned char>((nonce >> 8) & 0xffu);
56+
header[kHeaderNonceOffset + 2] = static_cast<unsigned char>((nonce >> 16) & 0xffu);
57+
header[kHeaderNonceOffset + 3] = static_cast<unsigned char>((nonce >> 24) & 0xffu);
58+
}
59+
60+
struct GrindOutcome {
61+
uint32_t nonce; // winning nonce -- also left written into header[76..79]
62+
u256 pow_hash; // scrypt_pow_hash of the winning header (<= target)
63+
uint64_t iters; // nonces tried, 1-based -- the search budget consumed
64+
};
65+
66+
// Grind header[76..79] from start_nonce until scrypt_pow_hash(header) <= target.
67+
//
68+
// header : the reconstructed 80-byte header; on success its nonce field
69+
// is left set to the winning value (caller broadcasts THIS blob)
70+
// target : compact_to_target(nBits) for the parent -- the SAME u256 the
71+
// satisfaction gate compares pow_hash against
72+
// start_nonce : where to begin the search (default 0)
73+
// max_iters : search budget. Regtest powLimit is trivially easy, so the live
74+
// forced-won path satisfies in very few iters; this is the guard
75+
// that the grinder TERMINATES (no infinite loop), not an expected
76+
// ceiling. Defaults to the full 2^32 nonce space.
77+
//
78+
// Returns the winning {nonce, pow_hash, iters} on success, or std::nullopt if
79+
// the budget (or the full 2^32 space) is exhausted with no satisfying nonce.
80+
// "No nonce found in budget" is a hard failure the caller MUST NOT broadcast a
81+
// header for -- failing closed mirrors the reconstructor's loud-throw posture:
82+
// a header that does not satisfy its target is rejected by the daemon anyway.
83+
inline std::optional<GrindOutcome>
84+
grind_won_nonce(std::array<unsigned char, 80>& header, const u256& target,
85+
uint32_t start_nonce = 0,
86+
uint64_t max_iters = (uint64_t{1} << 32)) {
87+
uint32_t nonce = start_nonce;
88+
for (uint64_t tried = 1; tried <= max_iters; ++tried) {
89+
put_header_nonce_le(header, nonce);
90+
u256 pow = scrypt_pow_hash(header);
91+
if (!(pow > target)) // pow <= target -> valid PoW (inclusive)
92+
return GrindOutcome{nonce, pow, tried};
93+
++nonce;
94+
if (nonce == start_nonce) break; // wrapped full 2^32 space, none satisfied
95+
}
96+
return std::nullopt;
97+
}
98+
99+
} // namespace dgb::coin
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
#pragma once
2+
// ---------------------------------------------------------------------------
3+
// dgb::coin::regrind_block_nonce -- the grind -> reconstruct -> submit
4+
// INTEGRATION seam (#82 Stage 4b/4c). The missing wiring between the faithful
5+
// won-block RECONSTRUCTION (reconstruct_closure.hpp -> {bytes, hex}) and a
6+
// node-B ProcessNewBlock ACCEPT.
7+
//
8+
// reconstruct_won_block_from_template frames a byte-faithful parent block, but
9+
// the header it carries is the SHARE's small_header verbatim -- whose nonce, on
10+
// the forced-won soak path, was never ground to satisfy the PARENT target. So
11+
// the A/B delivery proof reached node B and was consensus-rejected "high-hash,
12+
// proof of work failed". This seam closes that: given the already-framed block
13+
// blob it grinds the header nonce through the #286 scrypt_pow_hash SSOT until
14+
// the DGB-Scrypt PoW digest satisfies the parent target, writing the winning
15+
// nonce back into the serialized header IN PLACE.
16+
//
17+
// Ordering invariant (integrator 2026-06-21): the merkle_root is fixed FIRST
18+
// (reconstruct recomputes it from gentx_hash up the share merkle_link), THEN
19+
// the nonce is ground -- scrypt hashes the full 80 bytes, so grinding before
20+
// the merkle is fixed would invalidate the found nonce. This seam ENFORCES that
21+
// order structurally: it only ever runs on an already-framed block, and it
22+
// mutates ONLY header bytes [76..79] (kHeaderNonceOffset), never the merkle
23+
// region [36..67] or the tx tail -- so a nonce it finds is valid for the block
24+
// exactly as reconstructed.
25+
//
26+
// SSOT call-through: the hash is computed ONLY via grind_won_nonce ->
27+
// scrypt_pow_hash (the #286 digest CALL SSOT), and the satisfaction comparison
28+
// is the EXACT gate coin/header_chain.hpp runs (pow_hash <= target, inclusive).
29+
// A nonce this seam accepts is, by construction, one node B accepts.
30+
//
31+
// FAIL-CLOSED: returns std::nullopt (block_bytes left UNCHANGED) on a runt blob
32+
// (< 80 bytes, not even a header) or budget exhaustion -- mirroring the
33+
// reconstructor's posture. A header that does not satisfy its target is
34+
// daemon-rejected anyway, so the caller MUST NOT broadcast on a nullopt.
35+
//
36+
// Standalone-guard discipline: header-only, depends ONLY on nonce_grinder.hpp
37+
// (-> scrypt_pow.hpp -> real btclibs scrypt + header-only u256). No core, no
38+
// dgb OBJECT lib -- the same no-link constraint scrypt_pow/nonce_grinder keep,
39+
// so its guard TU links the _dgb_scrypt_tus real-scrypt set.
40+
//
41+
// Per-coin isolation: src/impl/dgb/ only. DGB-Scrypt is a STANDALONE parent in
42+
// the V36 default build. p2pool-merged-v36 surface: NONE -- no share format,
43+
// coinbase commitment, or PPLNS math is touched; only the parent block header
44+
// nonce, exactly as a real miner would have ground it.
45+
// ---------------------------------------------------------------------------
46+
47+
#include <array>
48+
#include <cstddef>
49+
#include <cstdint>
50+
#include <optional>
51+
#include <vector>
52+
53+
#include <impl/dgb/coin/nonce_grinder.hpp> // grind_won_nonce, GrindOutcome, kHeaderNonceOffset, u256
54+
55+
namespace dgb::coin {
56+
57+
// Grind the nonce of an already-framed reconstructed block (block_bytes) until
58+
// its DGB-Scrypt PoW digest satisfies the parent target, writing the winning
59+
// nonce back into the serialized 80-byte header [76..79] IN PLACE.
60+
//
61+
// block_bytes : the reconstruct_won_block_from_template {bytes} blob --
62+
// header(80) | varint(txcount) | txs. On success bytes [76..79]
63+
// are overwritten with the winning nonce; ALL other bytes
64+
// (merkle root, tx tail) are untouched. On failure UNCHANGED.
65+
// target : compact_to_target(nBits) for the parent -- the SAME u256 the
66+
// satisfaction gate compares pow_hash against.
67+
// start_nonce : where to begin the search (default 0).
68+
// max_iters : search budget (default the full 2^32 nonce space). Regtest
69+
// powLimit is trivially easy; this is the TERMINATION guard.
70+
//
71+
// Returns the winning {nonce, pow_hash, iters} on success (block_bytes mutated
72+
// in place), or std::nullopt on a runt blob or exhausted budget (UNCHANGED).
73+
inline std::optional<GrindOutcome>
74+
regrind_block_nonce(std::vector<unsigned char>& block_bytes, const u256& target,
75+
uint32_t start_nonce = 0,
76+
uint64_t max_iters = (uint64_t{1} << 32))
77+
{
78+
if (block_bytes.size() < 80) return std::nullopt; // not even a header -- fail closed
79+
80+
std::array<unsigned char, 80> header{};
81+
for (std::size_t i = 0; i < 80; ++i) header[i] = block_bytes[i];
82+
83+
auto out = grind_won_nonce(header, target, start_nonce, max_iters);
84+
if (!out) return std::nullopt; // budget exhausted -- bytes UNCHANGED
85+
86+
// Winning nonce -> back into the block blob, header [76..79] ONLY. The
87+
// merkle root [36..67] and the tx tail are never touched, so the block is
88+
// valid exactly as reconstructed.
89+
for (std::size_t i = kHeaderNonceOffset; i < 80; ++i) block_bytes[i] = header[i];
90+
return out;
91+
}
92+
93+
} // namespace dgb::coin

src/impl/dgb/coin/scrypt_pow.hpp

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
#pragma once
2+
// ---------------------------------------------------------------------------
3+
// DGB-Scrypt proof-of-work digest CALL (M3 §7b / Stage 4b-4c work-gen).
4+
//
5+
// The satisfaction gate in coin/header_chain.hpp compares HeaderSample::pow_hash
6+
// (a coin/dgb_arith256.hpp u256) against the SetCompact target -- hash <= target.
7+
// dgb_arith256.hpp::u256::from_le_bytes already documents the decode convention
8+
// (the scrypt output is read little-endian, mirroring bitcoin UintToArith256),
9+
// and ends: "the scrypt CALL itself lands at the ingest boundary in a following
10+
// slice." THIS header is that call: the single place the DGB-Scrypt algo hash is
11+
// computed, so the embedded work-gen (nonce grinder) and the ingest boundary
12+
// share ONE digest SSOT and can never disagree on byte order.
13+
//
14+
// V36 is Scrypt-ONLY (project_v36_dgb_scrypt_only): this is the ONLY PoW digest
15+
// DGB validates. The other four DGB algos (SHA256d/Skein/Qubit/Odocrypt) are
16+
// accept-by-continuity and never reach this function.
17+
//
18+
// scrypt_1024_1_1_256 is the canonical pooler/ArtForz Scrypt(N=1024,r=1,p=1)
19+
// from btclibs (src/btclibs/crypto/scrypt.*), the SAME routine DigiByte Core /
20+
// Litecoin Core use for the Scrypt algo. It links transitively here via
21+
// core -> btclibs (target_link_libraries(core PUBLIC ... btclibs)).
22+
// ---------------------------------------------------------------------------
23+
24+
#include <array>
25+
#include <cstdint>
26+
27+
#include <btclibs/crypto/scrypt.h> // scrypt_1024_1_1_256
28+
#include <impl/dgb/coin/dgb_arith256.hpp> // dgb::coin::u256
29+
30+
namespace dgb::coin {
31+
32+
// scrypt_1024_1_1_256 over the 80-byte serialized block header -> pow_hash u256.
33+
// The 32-byte digest is decoded little-endian (from_le_bytes) so the result
34+
// drops straight into the header_chain satisfaction gate with no reshape.
35+
inline u256 scrypt_pow_hash(const unsigned char header80[80]) {
36+
char digest[32];
37+
scrypt_1024_1_1_256(reinterpret_cast<const char*>(header80), digest);
38+
return u256::from_le_bytes(reinterpret_cast<const unsigned char*>(digest));
39+
}
40+
41+
// std::array convenience overload (the form the reconstructed-header builder and
42+
// the nonce grinder carry the 80 header bytes in).
43+
inline u256 scrypt_pow_hash(const std::array<unsigned char, 80>& header80) {
44+
return scrypt_pow_hash(header80.data());
45+
}
46+
47+
} // namespace dgb::coin

src/impl/dgb/test/CMakeLists.txt

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -342,4 +342,50 @@ if (BUILD_TESTING AND GTest_FOUND)
342342
target_link_libraries(header_chain_test PRIVATE
343343
GTest::gtest_main GTest::gtest nlohmann_json::nlohmann_json)
344344
gtest_add_tests(header_chain_test "" AUTO)
345+
346+
# --- Stage 4b/4c: DGB-Scrypt PoW digest CALL (coin/scrypt_pow.hpp) --------
347+
# Pins scrypt_pow_hash = scrypt_1024_1_1_256(80-byte header) decoded via
348+
# u256::from_le_bytes, plus the satisfaction-gate byte order (pow_hash <=
349+
# target) header_chain.hpp compares. Reuses the SAME _dgb_scrypt_tus set as
350+
# header_chain_test (real scrypt, not synthetic) -- header-only u256, no core
351+
# / no dgb OBJECT lib, the standalone-guard discipline dgb_arith256.hpp keeps.
352+
# Same target name, so it stays in BOTH build.yml --target allowlists.
353+
add_executable(dgb_scrypt_pow_test scrypt_pow_test.cpp ${_dgb_scrypt_tus})
354+
target_include_directories(dgb_scrypt_pow_test PRIVATE ${CMAKE_SOURCE_DIR}/src/btclibs)
355+
target_link_libraries(dgb_scrypt_pow_test PRIVATE
356+
GTest::gtest_main GTest::gtest)
357+
gtest_add_tests(dgb_scrypt_pow_test "" AUTO)
358+
359+
# --- Stage 4b/4c: DGB-Scrypt nonce grinder (coin/nonce_grinder.hpp) -------
360+
# Pins grind_won_nonce: increments the 80-byte header nonce until
361+
# scrypt_pow_hash(header) <= target (the EXACT header_chain.hpp satisfaction
362+
# gate), calling THROUGH the #286 scrypt_pow.hpp digest SSOT -- never a
363+
# bypass hash -- so the nonce it finds is one node B accepts. Asserts it
364+
# terminates well inside the test budget, writes the winning nonce LE into
365+
# [76..79], and fails closed (nullopt) when no nonce satisfies in budget.
366+
# Reuses the SAME _dgb_scrypt_tus real-scrypt set as dgb_scrypt_pow_test --
367+
# header-only u256, no core / no dgb OBJECT lib. Same standalone-guard
368+
# discipline; MUST stay in BOTH build.yml --target allowlists (#143 trap).
369+
add_executable(dgb_nonce_grinder_test nonce_grinder_test.cpp ${_dgb_scrypt_tus})
370+
target_include_directories(dgb_nonce_grinder_test PRIVATE ${CMAKE_SOURCE_DIR}/src/btclibs)
371+
target_link_libraries(dgb_nonce_grinder_test PRIVATE
372+
GTest::gtest_main GTest::gtest)
373+
gtest_add_tests(dgb_nonce_grinder_test "" AUTO)
374+
375+
# --- Stage 4b/4c: grind -> reconstruct -> submit seam (coin/regrind_block.hpp)
376+
# Pins regrind_block_nonce: grinds the header nonce of an ALREADY-FRAMED
377+
# reconstructed-block blob until its DGB-Scrypt PoW satisfies the parent
378+
# target, writing the winning nonce back into header [76..79] IN PLACE --
379+
# the wiring between reconstruct (merkle root fixed) and a node-B
380+
# ProcessNewBlock ACCEPT. Asserts the re-ground header satisfies the EXACT
381+
# pow <= target gate via the #286 SSOT, that ONLY [76..79] mutate (merkle
382+
# region [36..67] + tx tail byte-preserved), and fail-closed (nullopt,
383+
# bytes UNCHANGED) on a runt blob or exhausted budget. Reuses the SAME
384+
# _dgb_scrypt_tus real-scrypt set; header-only u256, no core / no OBJECT
385+
# lib. MUST stay in BOTH build.yml --target allowlists (#143 trap).
386+
add_executable(dgb_regrind_block_test regrind_block_test.cpp ${_dgb_scrypt_tus})
387+
target_include_directories(dgb_regrind_block_test PRIVATE ${CMAKE_SOURCE_DIR}/src/btclibs)
388+
target_link_libraries(dgb_regrind_block_test PRIVATE
389+
GTest::gtest_main GTest::gtest)
390+
gtest_add_tests(dgb_regrind_block_test "" AUTO)
345391
endif()

0 commit comments

Comments
 (0)