Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down Expand Up @@ -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)
Expand Down
99 changes: 99 additions & 0 deletions src/impl/dgb/coin/nonce_grinder.hpp
Original file line number Diff line number Diff line change
@@ -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 <array>
#include <cstddef>
#include <cstdint>
#include <optional>

#include <impl/dgb/coin/scrypt_pow.hpp> // scrypt_pow_hash (PoW digest SSOT, #286)
#include <impl/dgb/coin/dgb_arith256.hpp> // 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<unsigned char, 80>& header, uint32_t nonce) {
header[kHeaderNonceOffset + 0] = static_cast<unsigned char>(nonce & 0xffu);
header[kHeaderNonceOffset + 1] = static_cast<unsigned char>((nonce >> 8) & 0xffu);
header[kHeaderNonceOffset + 2] = static_cast<unsigned char>((nonce >> 16) & 0xffu);
header[kHeaderNonceOffset + 3] = static_cast<unsigned char>((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<GrindOutcome>
grind_won_nonce(std::array<unsigned char, 80>& 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
93 changes: 93 additions & 0 deletions src/impl/dgb/coin/regrind_block.hpp
Original file line number Diff line number Diff line change
@@ -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 <array>
#include <cstddef>
#include <cstdint>
#include <optional>
#include <vector>

#include <impl/dgb/coin/nonce_grinder.hpp> // 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<GrindOutcome>
regrind_block_nonce(std::vector<unsigned char>& 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<unsigned char, 80> 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
47 changes: 47 additions & 0 deletions src/impl/dgb/coin/scrypt_pow.hpp
Original file line number Diff line number Diff line change
@@ -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 <array>
#include <cstdint>

#include <btclibs/crypto/scrypt.h> // scrypt_1024_1_1_256
#include <impl/dgb/coin/dgb_arith256.hpp> // 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<const char*>(header80), digest);
return u256::from_le_bytes(reinterpret_cast<const unsigned char*>(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<unsigned char, 80>& header80) {
return scrypt_pow_hash(header80.data());
}

} // namespace dgb::coin
46 changes: 46 additions & 0 deletions src/impl/dgb/test/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Loading
Loading