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/scripts/dgb_regtest_won_block_soak.sh b/scripts/dgb_regtest_won_block_soak.sh new file mode 100755 index 000000000..bdcef3dc0 --- /dev/null +++ b/scripts/dgb_regtest_won_block_soak.sh @@ -0,0 +1,145 @@ +#!/usr/bin/env bash +# dgb_regtest_won_block_soak.sh -- DGB #82 won-block dual-path soak (regtest substrate). +# +# Closes the DGB broadcaster dual-path gate (#82): a real won block must reach the +# network down BOTH arms before #281 (the faithful reconstruct-closure install at +# main_dgb.cpp:317) can merge. Per integrator (2026-06-21, UID1708) two properties +# must be PROVEN on the wire, not merely "not-errored": +# +# PROP 1 (COINBASE-ONLY IS CORRECT, NOT FAIL-OPEN) -- template_other_txs_fn is +# empty today ("correct-and-empty"). The soak must reconstruct + get a +# COINBASE-ONLY block ACCEPTED by a peer, proving empty is valid rather +# than a silent malformed-block drop (the original #82 root cause). +# PROP 2 (BOTH ARMS REACH THE NETWORK) -- +# ARM A (P2P PRIMARY) : on_block_found -> make_reconstruct_closure_from_template +# (#280) -> submit_block_p2p_raw relays to a real peer +# (main_dgb.cpp:317/333). Assert peer node B ingests it. +# ARM B (RPC FALLBACK): the independent external submitblock RPC fallback (the +# &coin_node arm at main_dgb.cpp:317) lands the SAME block. +# +# WON-SHARE TRIGGER: UpdateTip-driven, no test-only seam. Unlike NMC (no --regtest +# CLI -> forced-won-share KAT seam, PR #276), c2pool-dgb already targets an external +# regtest daemon directly (main_dgb.cpp:393 "dev regtest daemon"; :433 fresh-regtest +# locator), and at regtest difficulty the embedded work loop finds a winning share +# naturally -- a genuine live UpdateTip won-block, not a synthetic inject. +# +# CLI is real (main_dgb.cpp:534-560): --run --coin-daemon H:P --coin-rpc H:P +# --coin-rpc-auth --coin-magic --coin-genesis . +# +# TWO OPEN LIVE-LEG ITEMS (surfaced to integrator, do NOT read as covered): +# (1) digibyted regtest binary self-provisioned -- not yet on the fleet (only +# namecoind exists). Until provisioned this is a substrate validator + harness, +# not a green gate. +# (2) ARM B isolation MECHANISM. There is NO --no-p2p-relay flag; both arms land on +# node A. Cleanly proving the submitblock fallback INDEPENDENTLY (P2P arm +# suppressed) is a design decision for integrator -- mirrors NMC's +# forced-seam-vs-CLI call. Current draft asserts ARM B against node A only as a +# placeholder and is marked GATED below until that mechanism is chosen. +# +# PER-COIN ISOLATION: DGB only. Localhost-only. Self-service creds (no secret on any +# coordination card). Ports sit outside the bitcoin-family 1844x regtest band. +set -euo pipefail + +CLI_BIN="${DGB_CLI:-digibyte-cli}" +DAEMON_BIN="${DGB_DAEMON:-digibyted}" +C2POOL_DGB="${C2POOL_DGB:-c2pool-dgb}" + +DATADIR_A="${DGB_REGTEST_DATADIR:-$HOME/.c2pool-dgb-regtest}" +DATADIR_B="${DATADIR_A}-peer" +RPCPORT_A=${DGB_RPCPORT_A:-18843} +RPCPORT_B=${DGB_RPCPORT_B:-18853} +P2PPORT_A=${DGB_P2PPORT_A:-18844} +P2PPORT_B=${DGB_P2PPORT_B:-18854} +PAYOUT_LABEL="c2pool-soak" + +log() { echo "[dgb-won-soak $(printf "%(%H:%M:%S)T")] $*" >&2; } +die() { echo "[dgb-won-soak FAIL] $*" >&2; exit 1; } +need() { command -v "$1" >/dev/null 2>&1 || die "missing binary: $1 (set DGB_DAEMON/DGB_CLI or self-provision DigiByte Core)"; } + +cli_a() { "$CLI_BIN" -regtest -datadir="$DATADIR_A" -rpcport=$RPCPORT_A "$@"; } +cli_b() { "$CLI_BIN" -regtest -datadir="$DATADIR_B" -rpcport=$RPCPORT_B "$@"; } + +C2POOL_PID="" +cleanup() { + [ -n "$C2POOL_PID" ] && kill "$C2POOL_PID" >/dev/null 2>&1 || true + cli_a stop >/dev/null 2>&1 || true + cli_b stop >/dev/null 2>&1 || true +} +trap cleanup EXIT + +need "$DAEMON_BIN"; need "$CLI_BIN"; need "$C2POOL_DGB"; need openssl + +provision() { + local dd="$1" rpcport="$2" + mkdir -p "$dd"; chmod 700 "$dd" + if [ ! -f "$dd/digibyte.conf" ]; then + local pass; pass="$(openssl rand -hex 24)" + cat > "$dd/digibyte.conf" </dev/null 2>&1; do n=$((n+1)); [ $n -gt 60 ] && die "rpc never came up: $*"; sleep 0.5; done; } +tip_b() { cli_b getblockcount; } + +# --- substrate: two peered regtest nodes -------------------------------------- +log "starting node A (regtest RPC $RPCPORT_A / P2P $P2PPORT_A)" +"$DAEMON_BIN" -regtest -datadir="$DATADIR_A" -rpcport=$RPCPORT_A -port=$P2PPORT_A -daemon >/dev/null +wait_rpc cli_a +log "starting node B (regtest RPC $RPCPORT_B / P2P $P2PPORT_B), peering to A" +"$DAEMON_BIN" -regtest -datadir="$DATADIR_B" -rpcport=$RPCPORT_B -port=$P2PPORT_B \ + -connect=127.0.0.1:$P2PPORT_A -daemon >/dev/null +wait_rpc cli_b + +cli_a createwallet "$PAYOUT_LABEL" >/dev/null 2>&1 || cli_a loadwallet "$PAYOUT_LABEL" >/dev/null 2>&1 || true +ADDR_A="$(cli_a getnewaddress "$PAYOUT_LABEL")" +log "mining 101 maturity blocks to $ADDR_A" +cli_a generatetoaddress 101 "$ADDR_A" >/dev/null +BASE_B="$(tip_b)" + +# regtest magic + genesis are read from the running daemon's chainparams so the +# embedded backend's getheaders bootstrap matches node A exactly (no hand-typed +# consensus literal). DGB regtest magic is fixed in DigiByte Core chainparams. +DGB_REGTEST_GENESIS="$(cli_a getblockhash 0)" +: "${DGB_REGTEST_MAGIC:?set DGB_REGTEST_MAGIC to the DigiByte regtest pchMessageStart hex (from chainparams)}" +log "substrate OK: node A height $(cli_a getblockcount), peer B $BASE_B, genesis $DGB_REGTEST_GENESIS" + +# --- ARM A (P2P PRIMARY): live UpdateTip won block relayed to node B ---------- +log "ARM A: launching embedded c2pool-dgb against node A (regtest diff -> natural won share)" +"$C2POOL_DGB" --run \ + --coin-daemon 127.0.0.1:$P2PPORT_A \ + --coin-rpc 127.0.0.1:$RPCPORT_A \ + --coin-rpc-auth "$DATADIR_A/digibyte.conf" \ + --coin-magic "$DGB_REGTEST_MAGIC" \ + --coin-genesis "$DGB_REGTEST_GENESIS" >/tmp/c2pool-dgb-soak.log 2>&1 & +C2POOL_PID=$! +log "c2pool-dgb PID $C2POOL_PID; waiting for a won share to relay a coinbase-only block to node B" +n=0 +until [ "$(tip_b)" -gt "$BASE_B" ]; do + n=$((n+1)); [ $n -gt 240 ] && die "ARM A: no won block reached node B within 120s (P2P relay arm)" + sleep 0.5 +done +WON_HASH="$(cli_b getblockhash "$(tip_b)")" +NTX="$(cli_b getblock "$WON_HASH" | grep -o '"tx"[^]]*]' | grep -o '"[0-9a-f]\{64\}"' | wc -l)" +[ "$NTX" -eq 1 ] || die "ARM A: won block $WON_HASH has $NTX txs, expected 1 (coinbase-only, PROP 1)" +log "ARM A OK: P2P-relayed coinbase-only won block $WON_HASH accepted by peer node B (PROP 1 + ARM A)" +kill "$C2POOL_PID" >/dev/null 2>&1; C2POOL_PID="" + +# --- ARM B (RPC FALLBACK): GATED on integrator isolation-mechanism decision ---- +# No --no-p2p-relay flag exists; isolating the submitblock fallback from the P2P arm +# is an open design call (see header item 2). Until integrator picks the mechanism, +# ARM B is NOT asserted -- shipping a placeholder assert would be the silent-gap the +# gate exists to prevent. +log "ARM B: GATED -- submitblock-fallback isolation mechanism pending integrator decision (header item 2)" + +log "ARM A proven; ARM B + digibyted provisioning are the two remaining live-leg items for the #82 gate." diff --git a/src/c2pool/main_dgb.cpp b/src/c2pool/main_dgb.cpp index 6d88ee747..7d85d1785 100644 --- a/src/c2pool/main_dgb.cpp +++ b/src/c2pool/main_dgb.cpp @@ -34,6 +34,8 @@ #include // make_mempool_tx_source (EmbeddedTxSource) #include #include // make_reconstruct_closure_from_template (#280) +#include // regrind_block_nonce -- forced-won soak PoW (#82 4b/4c) +#include // c2pool::dgb::compact_to_target (nBits -> u256) #include // won_share_inputs (#279) #include #include @@ -86,7 +88,7 @@ void print_banner(const char* argv0, const core::CoinParams& p) std::cout << "c2pool-dgb " << C2POOL_VERSION << " โ€” DigiByte Scrypt-only (V36)\n\n" << "Usage: " << argv0 - << " [--version] [--help] [--selftest] [--run] [--stratum [H:]P]\n\n" + << " [--version] [--help] [--selftest] [--run] [--no-p2p-relay] [--soak-regrind] [--stratum [H:]P]\n\n" << "Status: pool/sharechain pillars live (Phase B); run-loop up\n" << " (--run: io_context + sharechain peer + Stratum standup).\n" << " --stratum [HOST:]PORT binds a miner-facing TCP listener\n" @@ -142,7 +144,9 @@ int run_node(const core::CoinParams& params, bool testnet, const std::vector& coin_magic, const uint256& coin_genesis, const std::string& rpc_endpoint, - const std::string& rpc_conf_path) + const std::string& rpc_conf_path, + bool no_p2p_relay, + bool soak_regrind) { io::io_context ioc; @@ -379,14 +383,68 @@ int run_node(const core::CoinParams& params, bool testnet, return {}; // coinbase-only today (see note above) }); + // -- #82 forced-won SOAK regrind (--soak-regrind, default OFF) --------- + // A genuine pool-won share already satisfies the parent target, so the + // reconstructed block's PoW is valid as-framed and the PRODUCTION path + // NEVER regrinds. A *forced*-won soak share (injected with no real Scrypt + // work) reconstructs a block node B rejects as high-hash. With + // --soak-regrind, grind the reconstructed block's nonce until its DGB-Scrypt + // digest satisfies the block's OWN declared nBits -- exactly the target node + // B validates against -- so the soak proves a node-B ACCEPT end to end. + // Mutates header[76..79] ONLY (regrind_block_nonce): merkle root + tx tail + // untouched. OFF in any real deployment. + if (soak_regrind) { + auto inner = std::move(faithful_reconstruct); + faithful_reconstruct = + [inner = std::move(inner)](const uint256& share_hash) + -> std::optional, std::string>> { + auto blk = inner(share_hash); + if (!blk) return blk; // unassemblable -- pass through + if (blk->first.size() < 80) return blk; // runt -- leave to fail closed + // nBits is header bytes [72..75], little-endian. + const uint32_t nbits = + static_cast(blk->first[72]) + | (static_cast(blk->first[73]) << 8) + | (static_cast(blk->first[74]) << 16) + | (static_cast(blk->first[75]) << 24); + const auto target = c2pool::dgb::compact_to_target(nbits); + auto out = dgb::coin::regrind_block_nonce(blk->first, target); + if (!out) { + std::cout << "[DGB-BLOCK] --soak-regrind: nonce search EXHAUSTED " + "(nBits=" << std::hex << nbits << std::dec + << ") -- block UNCHANGED, will reject high-hash" + << std::endl; + return blk; + } + blk->second = HexStr(blk->first); // hex tracks the mutated bytes + std::cout << "[DGB-BLOCK] --soak-regrind: ground winning nonce=" + << out->nonce << " after " << out->iters + << " iters (PoW now satisfies nBits=" << std::hex + << nbits << std::dec << ")" << std::endl; + return blk; + }; + } + p2p_node.tracker().m_on_block_found = dgb::coin::make_on_block_found( /*reconstruct=*/std::move(faithful_reconstruct), - /*p2p_relay=*/[&ioc, &coin_p2p](const std::vector& block_bytes) { + /*p2p_relay=*/[&ioc, &coin_p2p, no_p2p_relay](const std::vector& block_bytes) { // #82 PRIMARY arm: relay the won block over the embedded coin-daemon // P2P producer. The sink fires from the compute thread, so post the // peer write onto the io thread (NodeP2P is single-thread-confined). // No-op when --coin-daemon is absent (coin_p2p null) โ€” the RPC // fallback below still fires (dual-path rule). + // + // --no-p2p-relay (#82 ARM B isolation): suppress ONLY this embedded + // P2P-relay arm and leave the external-digibyted submitblock seam + // (fallback) live, so a regtest soak can prove the RPC fallback lands + // the block ON ITS OWN โ€” not masked by a silent or dead P2P relay + // (the historical #82 gap). A real operator toggle, not a test seam. + if (no_p2p_relay) { + std::cout << "[DGB-BLOCK] --no-p2p-relay: embedded P2P-relay arm " + "SUPPRESSED; submitblock-RPC fallback remains live" + << std::endl; + return; + } if (!coin_p2p) return; io::post(ioc, [&coin_p2p, bytes = block_bytes]() { if (coin_p2p) coin_p2p->submit_block_p2p_raw(bytes); @@ -587,6 +645,8 @@ int main(int argc, char** argv) uint256 coin_genesis; // --coin-genesis HASH (initial getheaders locator base) std::string rpc_endpoint; // --coin-rpc HOST:PORT (external digibyted submit arm) std::string rpc_conf_path; // --coin-rpc-auth PATH to digibyte.conf (creds source) + bool no_p2p_relay = false; // --no-p2p-relay: suppress embedded P2P-relay arm (#82 ARM B isolation) + bool soak_regrind = false; // --soak-regrind: grind reconstructed block PoW for forced-won regtest soak (OFF in prod) for (int i = 1; i < argc; ++i) { if (std::strcmp(argv[i], "--version") == 0) { std::cout << "c2pool-dgb " << C2POOL_VERSION << "\n"; @@ -621,6 +681,8 @@ int main(int argc, char** argv) if (std::strcmp(argv[i], "--coin-rpc-auth") == 0 && i + 1 < argc) { rpc_conf_path = argv[++i]; // path to digibyte.conf (rpcpassword stays in-file) } + if (std::strcmp(argv[i], "--no-p2p-relay") == 0) no_p2p_relay = true; + if (std::strcmp(argv[i], "--soak-regrind") == 0) soak_regrind = true; } const core::CoinParams params = dgb::make_coin_params(/*testnet=*/false); @@ -633,7 +695,7 @@ int main(int argc, char** argv) if (want_run) return run_node(params, /*testnet=*/false, stratum_addr, stratum_port, coin_daemon, coin_magic, coin_genesis, - rpc_endpoint, rpc_conf_path); + rpc_endpoint, rpc_conf_path, no_p2p_relay, soak_regrind); // --selftest, or a bare invocation: drive the live score path so the // binary exercises real consensus code, then exit cleanly. 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