Skip to content

Commit 72f080c

Browse files
committed
dgb(#82 4b/4c): embedded nonce grinder over the scrypt_pow.hpp SSOT
grind_won_nonce(header80, target) increments the 80-byte header nonce [76..79] LE until scrypt_pow_hash(header) <= target -- the EXACT header_chain.hpp satisfaction gate -- calling THROUGH the #286 digest CALL SSOT (never a bypass hash), so a nonce it accepts is one node B ACCEPTs. This is the missing primitive between the faithful won-block reconstruction and a ProcessNewBlock ACCEPT: the leg-2 A/B delivery proof reached node B but was consensus-rejected high-hash precisely because the forced-won seam carried no real PoW. Fails closed: returns nullopt when no nonce satisfies inside the budget (or the full 2^32 space wraps) -- a header that does not satisfy its target is never broadcast, mirroring the reconstructor loud-throw. dgb_nonce_grinder_test (5/5): finds a satisfying nonce well inside budget, the winning header satisfies the gate and its reported digest == the SSOT digest, the winning nonce is written LE into [76..79], re-grinding from the winner succeeds on iter 1, and an unsatisfiable target exhausts the budget to nullopt. Reuses the _dgb_scrypt_tus real-scrypt set; header-only u256, no core / no dgb OBJECT lib. Added to both build.yml --target allowlists. Fenced src/impl/dgb/ + build.yml. Wiring grind -> reconstruct -> submit on the live forced-won path and the node-B ACCEPT gate proof are the explicitly-next integration slice.
1 parent 314ae5f commit 72f080c

4 files changed

Lines changed: 230 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 dgb_scrypt_pow_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 \
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 dgb_scrypt_pow_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 \
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

src/impl/dgb/test/CMakeLists.txt

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -355,4 +355,20 @@ if (BUILD_TESTING AND GTest_FOUND)
355355
target_link_libraries(dgb_scrypt_pow_test PRIVATE
356356
GTest::gtest_main GTest::gtest)
357357
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)
358374
endif()
Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
// DGB nonce grinder guard (coin/nonce_grinder.hpp) -- #82 Stage 4b/4c.
2+
//
3+
// Proves the embedded real work-gen primitive: grind_won_nonce increments the
4+
// header nonce until the DGB-Scrypt PoW digest satisfies the target, computing
5+
// the hash ONLY through scrypt_pow_hash (the #286 digest CALL SSOT) and using
6+
// the EXACT satisfaction comparison coin/header_chain.hpp runs (pow_hash <=
7+
// target). The nonce it returns is therefore the nonce node B's own Scrypt
8+
// validation accepts -- this is the unit floor under the live tip-extension
9+
// (ProcessNewBlock ACCEPT) integration test.
10+
//
11+
// Real scrypt (the _dgb_scrypt_tus set), header-only u256 -- the same no-core /
12+
// no-OBJECT-lib standalone-guard discipline as scrypt_pow_test / dgb_arith256.
13+
14+
#include <array>
15+
#include <cstdint>
16+
#include <optional>
17+
18+
#include <gtest/gtest.h>
19+
20+
#include <impl/dgb/coin/nonce_grinder.hpp>
21+
#include <impl/dgb/coin/scrypt_pow.hpp>
22+
23+
using dgb::coin::u256;
24+
using dgb::coin::grind_won_nonce;
25+
using dgb::coin::scrypt_pow_hash;
26+
using dgb::coin::kHeaderNonceOffset;
27+
28+
namespace {
29+
30+
// Arbitrary fixed 76-byte header prefix; the grinder owns bytes [76..79].
31+
std::array<unsigned char, 80> base_header() {
32+
std::array<unsigned char, 80> h{};
33+
for (int i = 0; i < 80; ++i) h[i] = static_cast<unsigned char>((i * 13 + 5) % 251);
34+
return h;
35+
}
36+
37+
uint32_t read_nonce_le(const std::array<unsigned char, 80>& h) {
38+
return (uint32_t)h[kHeaderNonceOffset + 0]
39+
| ((uint32_t)h[kHeaderNonceOffset + 1] << 8)
40+
| ((uint32_t)h[kHeaderNonceOffset + 2] << 16)
41+
| ((uint32_t)h[kHeaderNonceOffset + 3] << 24);
42+
}
43+
44+
// Target accepting ~1/4 of uniform digests: top two bits of the MS limb zero
45+
// (pow.limb[3] <= 0x3fff... with all lower limbs maxed). Expected ~4 iters, so
46+
// termination "well inside the test budget" is overwhelming; max_iters is the
47+
// hard ceiling, not the expected cost.
48+
u256 easy_quarter_target() {
49+
u256 t;
50+
t.limb[0] = 0xffffffffffffffffULL;
51+
t.limb[1] = 0xffffffffffffffffULL;
52+
t.limb[2] = 0xffffffffffffffffULL;
53+
t.limb[3] = 0x3fffffffffffffffULL;
54+
return t;
55+
}
56+
57+
constexpr uint64_t kTestBudget = uint64_t{1} << 20; // 1,048,576 vs ~4 expected
58+
59+
// --- grinder finds a satisfying nonce, well inside budget -------------------
60+
TEST(DgbNonceGrinderKAT, FindsSatisfyingNonceWithinBudget) {
61+
auto h = base_header();
62+
auto out = grind_won_nonce(h, easy_quarter_target(), 0, kTestBudget);
63+
ASSERT_TRUE(out.has_value());
64+
EXPECT_GE(out->iters, uint64_t{1});
65+
// P(iters > 200) = (3/4)^200 ~ 1e-25 -- terminates inside budget by construction.
66+
EXPECT_LE(out->iters, uint64_t{200});
67+
}
68+
69+
// --- the winning header actually satisfies the gate (pow <= target) ---------
70+
TEST(DgbNonceGrinderKAT, WinningHeaderSatisfiesTarget) {
71+
auto h = base_header();
72+
u256 target = easy_quarter_target();
73+
auto out = grind_won_nonce(h, target, 0, kTestBudget);
74+
ASSERT_TRUE(out.has_value());
75+
// Recompute through the SAME #286 SSOT over the mutated header.
76+
u256 pow = scrypt_pow_hash(h);
77+
EXPECT_FALSE(pow > target); // pow <= target -> valid PoW
78+
EXPECT_TRUE(pow == out->pow_hash); // reported digest == SSOT digest (no bypass)
79+
}
80+
81+
// --- the winning nonce is left written into header[76..79], little-endian ----
82+
TEST(DgbNonceGrinderKAT, WritesWinningNonceLittleEndian) {
83+
auto h = base_header();
84+
auto out = grind_won_nonce(h, easy_quarter_target(), 0, kTestBudget);
85+
ASSERT_TRUE(out.has_value());
86+
EXPECT_EQ(read_nonce_le(h), out->nonce);
87+
}
88+
89+
// --- determinism + start_nonce honored: re-grinding from the winner hits it
90+
// on the first try (the found nonce truly satisfies AND start is respected).
91+
TEST(DgbNonceGrinderKAT, ReGrindFromWinnerSucceedsImmediately) {
92+
auto h = base_header();
93+
auto first = grind_won_nonce(h, easy_quarter_target(), 0, kTestBudget);
94+
ASSERT_TRUE(first.has_value());
95+
auto h2 = base_header();
96+
auto again = grind_won_nonce(h2, easy_quarter_target(), first->nonce, kTestBudget);
97+
ASSERT_TRUE(again.has_value());
98+
EXPECT_EQ(again->nonce, first->nonce);
99+
EXPECT_EQ(again->iters, uint64_t{1}); // satisfied on the first nonce tried
100+
}
101+
102+
// --- fail-closed: an unsatisfiable target inside a small budget -> nullopt ----
103+
// target == 1 (limb[0]=1, rest 0): essentially no scrypt digest is <= 1, so the
104+
// grinder MUST exhaust the budget and return nullopt rather than loop forever.
105+
TEST(DgbNonceGrinderKAT, ReturnsNulloptWhenNoNonceInBudget) {
106+
auto h = base_header();
107+
u256 impossible; // all-zero ...
108+
impossible.limb[0] = 1; // ... except 1 -> target = 1
109+
auto out = grind_won_nonce(h, impossible, 0, /*max_iters=*/256);
110+
EXPECT_FALSE(out.has_value());
111+
}
112+
113+
} // namespace

0 commit comments

Comments
 (0)