Skip to content

Commit 91cc453

Browse files
committed
dgb(#82 gate): grind->reconstruct->submit JOINT (won_block_finalize.hpp)
The missing composition between leg-2's faithful won-block reconstruction and a node-B ProcessNewBlock ACCEPT. finalize_won_block_pow takes a reconstructed block (header[0..79] with merkle_root already set -- merkle FIRST, integrator-pinned ordering) and grinds the nonce at [76..79] THROUGH grind_won_nonce -> scrypt_pow_hash (the #286 digest CALL SSOT) until the DGB-Scrypt PoW satisfies the parent target, splices the winning nonce back, and regenerates the RPC-fallback hex in lockstep with the P2P-arm bytes. This closes the "high-hash, proof of work failed" rejection: the forced-won seam reached node B but carried no real PoW. SSOT call-through, never a bypass hash -- a finalized block is by construction one node B's own Scrypt validation accepts. Fail-closed (nullopt) on sub-80-byte input or no-nonce-in-budget, mirroring make_reconstruct_closure's never-a-partial-block posture. Standalone-guard discipline: depends ONLY on nonce_grinder.hpp (real btclibs scrypt + header-only u256); no core, no dgb OBJECT lib, no util/HexStr (local lowercase encoder, byte-identical to HexStr). Test links the SAME _dgb_scrypt_tus set as dgb_nonce_grinder_test. +5 KATs (dgb_won_block_finalize_test, 5/5 green): finalized header pow<=target via the same SSOT; winning nonce spliced LE into [76..79]; hex == bytes; ONLY the 4 nonce bytes mutate (merkle + tx tail untouched); fail-closed on a 79-byte input. Registered in BOTH build.yml --target allowlists (#143 trap). Fenced: src/impl/dgb/** + build.yml allowlist only. p2pool-merged-v36 surface: NONE. DGB-Scrypt standalone parent in the V36 default build.
1 parent 1f9f827 commit 91cc453

4 files changed

Lines changed: 263 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 dgb_nonce_grinder_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_won_block_finalize_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 dgb_nonce_grinder_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_won_block_finalize_test \
221221
test_coin_broadcaster test_multiaddress_pplns test_pplns_stress \
222222
v37_test \
223223
-j$(nproc)
Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
#pragma once
2+
// ---------------------------------------------------------------------------
3+
// dgb::coin::finalize_won_block_pow -- the grind->reconstruct->submit JOINT
4+
// (#82 gate). It is the missing composition between the faithful won-block
5+
// RECONSTRUCTION (reconstruct_won_block*.hpp, leg-2) and a node-B
6+
// ProcessNewBlock ACCEPT: the A/B delivery proof reached node B but was
7+
// consensus-rejected "high-hash, proof of work failed" because the forced-won
8+
// seam carried no real PoW. This seam grinds the reconstructed block's header
9+
// nonce until the DGB-Scrypt digest satisfies the parent target, then hands the
10+
// finalized {bytes, hex} to the existing dual-path broadcaster (P2P relay arm +
11+
// submitblock RPC fallback) unchanged.
12+
//
13+
// ORDERING (integrator-pinned, 2026-06-21): merkle_root is set FIRST (it is
14+
// already baked into bytes[36..67] by the reconstructor / assemble_won_block),
15+
// THEN the nonce at bytes[76..79] is ground -- scrypt hashes the full 80 bytes,
16+
// so grinding before the merkle was fixed would invalidate the found nonce.
17+
// This seam runs strictly AFTER reconstruction, so that invariant holds by
18+
// construction.
19+
//
20+
// Byte layout: the reconstructor frames the canonical block as
21+
// header(80) | tx_count(CompactSize) | [gentx] ++ other_txs
22+
// where header = version(4)|prev(32)|merkle(32)|time(4)|bits(4)|nonce(4).
23+
// bytes[0..79] is therefore the exact 80-byte header node B re-hashes, and the
24+
// nonce is bytes[76..79] little-endian (== nonce_grinder.hpp kHeaderNonceOffset).
25+
//
26+
// SSOT call-through: the PoW is computed ONLY via grind_won_nonce ->
27+
// scrypt_pow_hash (the #286 digest CALL SSOT), the EXACT pow<=target gate
28+
// header_chain.hpp runs -- so a finalized block this seam emits is, by
29+
// construction, one node B's own Scrypt validation accepts.
30+
//
31+
// FAIL-CLOSED (mirrors make_reconstruct_closure): returns std::nullopt -- never
32+
// a partial/wrong block -- when the input is too short to frame a header or no
33+
// satisfying nonce is found inside max_iters. A header that does not satisfy its
34+
// target is daemon-rejected anyway, so emitting nothing is strictly safer.
35+
//
36+
// Standalone-guard discipline: depends ONLY on nonce_grinder.hpp (real btclibs
37+
// scrypt + header-only u256). NO core, NO dgb OBJECT lib, NO util/HexStr -- the
38+
// RPC-fallback hex is regenerated with a local lowercase encoder byte-identical
39+
// to HexStr, so the seam (and its KAT) link exactly like the grinder. Operates
40+
// on raw {bytes} so it pulls none of the block_assembly/reconstruct include set.
41+
//
42+
// Per-coin isolation: src/impl/dgb/ only. p2pool-merged-v36 surface: NONE.
43+
// DGB-Scrypt is a STANDALONE parent in the V36 default build.
44+
// ---------------------------------------------------------------------------
45+
46+
#include <algorithm>
47+
#include <array>
48+
#include <cstdint>
49+
#include <optional>
50+
#include <string>
51+
#include <utility>
52+
#include <vector>
53+
54+
#include <impl/dgb/coin/nonce_grinder.hpp> // grind_won_nonce, GrindOutcome, u256, kHeaderNonceOffset
55+
56+
namespace dgb::coin {
57+
58+
// A reconstructed parent block whose header nonce now satisfies the parent
59+
// target -- ready for the dual-path broadcaster:
60+
// bytes : the blob the embedded P2P relay sends
61+
// hex : the same block for the external submitblock (RPC) fallback
62+
// grind : the winning nonce + its pow_hash + iteration count (audit trail)
63+
struct FinalizedWonBlock {
64+
std::vector<unsigned char> bytes;
65+
std::string hex;
66+
GrindOutcome grind;
67+
};
68+
69+
// Lowercase hex, byte-identical to bitcoin util HexStr -- kept local so this
70+
// seam stays in the grinder's no-core / no-util standalone lineage.
71+
inline std::string finalize_block_to_hex(const std::vector<unsigned char>& v) {
72+
static constexpr char kHex[] = "0123456789abcdef";
73+
std::string s;
74+
s.reserve(v.size() * 2);
75+
for (unsigned char b : v) {
76+
s.push_back(kHex[(b >> 4) & 0xf]);
77+
s.push_back(kHex[b & 0xf]);
78+
}
79+
return s;
80+
}
81+
82+
// Grind the reconstructed block to a node-acceptable PoW.
83+
//
84+
// block_bytes : a fully reconstructed block (header at [0..79], merkle_root
85+
// ALREADY set); the seam owns header[76..79] only.
86+
// target : compact_to_target(nBits) for the parent -- the SAME u256
87+
// header_chain.hpp / the grinder KAT compare against.
88+
// start_nonce / max_iters : forwarded to grind_won_nonce.
89+
//
90+
// Returns {bytes, hex, grind} with hex == finalize_block_to_hex(bytes) and the
91+
// winning nonce spliced into bytes[76..79] LE. Returns std::nullopt (fail-
92+
// closed) if block_bytes is too short to frame an 80-byte header or no nonce
93+
// satisfies the target within max_iters.
94+
inline std::optional<FinalizedWonBlock>
95+
finalize_won_block_pow(std::vector<unsigned char> block_bytes, const u256& target,
96+
uint32_t start_nonce = 0,
97+
uint64_t max_iters = (uint64_t{1} << 32))
98+
{
99+
if (block_bytes.size() < 80)
100+
return std::nullopt; // too short to frame a header -> fail closed
101+
102+
std::array<unsigned char, 80> header{};
103+
std::copy(block_bytes.begin(), block_bytes.begin() + 80, header.begin());
104+
105+
auto out = grind_won_nonce(header, target, start_nonce, max_iters);
106+
if (!out.has_value())
107+
return std::nullopt; // no satisfying nonce in budget -> fail closed
108+
109+
// Splice the winning nonce (grinder wrote it LE into header[76..79]) back
110+
// into the block; only the 4 nonce bytes change, every tx byte is untouched.
111+
std::copy(header.begin() + kHeaderNonceOffset, header.begin() + 80,
112+
block_bytes.begin() + kHeaderNonceOffset);
113+
114+
std::string hex = finalize_block_to_hex(block_bytes);
115+
return FinalizedWonBlock{std::move(block_bytes), std::move(hex), *out};
116+
}
117+
118+
} // namespace dgb::coin

src/impl/dgb/test/CMakeLists.txt

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -371,4 +371,23 @@ if (BUILD_TESTING AND GTest_FOUND)
371371
target_link_libraries(dgb_nonce_grinder_test PRIVATE
372372
GTest::gtest_main GTest::gtest)
373373
gtest_add_tests(dgb_nonce_grinder_test "" AUTO)
374+
375+
# --- #82 gate: grind->reconstruct->submit JOINT (coin/won_block_finalize.hpp)
376+
# Pins finalize_won_block_pow: takes a faithfully reconstructed block (header
377+
# at [0..79], merkle_root already set -- merkle FIRST) and grinds the nonce
378+
# at [76..79] THROUGH grind_won_nonce -> scrypt_pow_hash (#286 SSOT) until the
379+
# DGB-Scrypt PoW satisfies the parent target, splicing the winning nonce back
380+
# and regenerating the RPC-fallback hex in lockstep with the P2P-arm bytes.
381+
# Asserts: finalized header pow<=target via the same SSOT; nonce spliced LE
382+
# into [76..79]; hex == bytes; ONLY the 4 nonce bytes mutate (merkle + tx tail
383+
# untouched); fail-closed (nullopt) on a sub-80-byte input. Reuses the SAME
384+
# _dgb_scrypt_tus real-scrypt set as dgb_nonce_grinder_test -- header-only
385+
# u256, no core / no dgb OBJECT lib. Same standalone-guard discipline; MUST
386+
# stay in BOTH build.yml --target allowlists (#143 trap).
387+
add_executable(dgb_won_block_finalize_test won_block_finalize_test.cpp ${_dgb_scrypt_tus})
388+
target_include_directories(dgb_won_block_finalize_test PRIVATE ${CMAKE_SOURCE_DIR}/src/btclibs)
389+
target_link_libraries(dgb_won_block_finalize_test PRIVATE
390+
GTest::gtest_main GTest::gtest)
391+
gtest_add_tests(dgb_won_block_finalize_test "" AUTO)
392+
374393
endif()
Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
// DGB won-block finalize joint (coin/won_block_finalize.hpp) -- #82 gate.
2+
//
3+
// Pins finalize_won_block_pow: the grind->reconstruct->submit composition that
4+
// turns a faithfully RECONSTRUCTED parent block (header[0..79] with merkle_root
5+
// already set; merkle FIRST, integrator-pinned ordering) into one whose header
6+
// nonce satisfies the parent target -- the missing link between leg-2's
7+
// reconstruction and a node-B ProcessNewBlock ACCEPT. The PoW is computed ONLY
8+
// through grind_won_nonce -> scrypt_pow_hash (the #286 digest CALL SSOT), so a
9+
// finalized block is, by construction, one node B's own Scrypt validation
10+
// accepts. This is the unit floor directly under the live tip-extension gate.
11+
//
12+
// Real scrypt (the _dgb_scrypt_tus set), header-only u256 -- the same no-core /
13+
// no-OBJECT-lib standalone-guard discipline as nonce_grinder_test / scrypt_pow_test.
14+
15+
#include <algorithm>
16+
#include <array>
17+
#include <cstdint>
18+
#include <optional>
19+
#include <string>
20+
#include <vector>
21+
22+
#include <gtest/gtest.h>
23+
24+
#include <impl/dgb/coin/won_block_finalize.hpp>
25+
#include <impl/dgb/coin/nonce_grinder.hpp>
26+
#include <impl/dgb/coin/scrypt_pow.hpp>
27+
28+
using dgb::coin::u256;
29+
using dgb::coin::scrypt_pow_hash;
30+
using dgb::coin::kHeaderNonceOffset;
31+
using dgb::coin::finalize_won_block_pow;
32+
using dgb::coin::finalize_block_to_hex;
33+
34+
namespace {
35+
36+
// A synthetic reconstructed block: a deterministic 80-byte header (merkle_root
37+
// already baked in -- finalize never touches it) followed by a short tx tail
38+
// standing in for tx_count|[gentx]++other_txs. finalize owns header[76..79] only.
39+
std::vector<unsigned char> synthetic_block(std::size_t tail_len = 37) {
40+
std::vector<unsigned char> b;
41+
b.reserve(80 + tail_len);
42+
for (int i = 0; i < 80; ++i)
43+
b.push_back(static_cast<unsigned char>((i * 13 + 5) % 251));
44+
for (std::size_t i = 0; i < tail_len; ++i)
45+
b.push_back(static_cast<unsigned char>((i * 7 + 3) % 256));
46+
return b;
47+
}
48+
49+
// Target accepting ~1/4 of uniform digests (same shape as nonce_grinder_test):
50+
// terminates in ~4 iters, so the budget below is never a factor.
51+
u256 easy_quarter_target() {
52+
u256 t;
53+
t.limb[0] = 0xffffffffffffffffULL;
54+
t.limb[1] = 0xffffffffffffffffULL;
55+
t.limb[2] = 0xffffffffffffffffULL;
56+
t.limb[3] = 0x3fffffffffffffffULL;
57+
return t;
58+
}
59+
60+
// The 80-byte header sitting at the front of a (finalized) block.
61+
std::array<unsigned char, 80> header_of(const std::vector<unsigned char>& block) {
62+
std::array<unsigned char, 80> h{};
63+
std::copy(block.begin(), block.begin() + 80, h.begin());
64+
return h;
65+
}
66+
67+
uint32_t read_nonce_le(const std::vector<unsigned char>& block) {
68+
return (uint32_t)block[kHeaderNonceOffset + 0]
69+
| ((uint32_t)block[kHeaderNonceOffset + 1] << 8)
70+
| ((uint32_t)block[kHeaderNonceOffset + 2] << 16)
71+
| ((uint32_t)block[kHeaderNonceOffset + 3] << 24);
72+
}
73+
74+
constexpr uint64_t kTestBudget = uint64_t{1} << 20; // vs ~4 expected
75+
76+
// --- finalize yields a block whose header satisfies the parent target -------
77+
TEST(DgbWonBlockFinalizeKAT, FinalizesToSatisfyingPoW) {
78+
u256 target = easy_quarter_target();
79+
auto out = finalize_won_block_pow(synthetic_block(), target, 0, kTestBudget);
80+
ASSERT_TRUE(out.has_value());
81+
// Re-hash the finalized header through the SAME #286 SSOT.
82+
u256 pow = scrypt_pow_hash(header_of(out->bytes));
83+
EXPECT_FALSE(pow > target); // pow <= target -> valid PoW
84+
EXPECT_TRUE(pow == out->grind.pow_hash); // reported digest == SSOT (no bypass)
85+
EXPECT_GE(out->grind.iters, uint64_t{1});
86+
}
87+
88+
// --- the winning nonce is spliced into block bytes[76..79] little-endian ----
89+
TEST(DgbWonBlockFinalizeKAT, SplicesWinningNonceLittleEndian) {
90+
auto out = finalize_won_block_pow(synthetic_block(), easy_quarter_target(), 0, kTestBudget);
91+
ASSERT_TRUE(out.has_value());
92+
EXPECT_EQ(read_nonce_le(out->bytes), out->grind.nonce);
93+
}
94+
95+
// --- RPC-fallback hex stays in lockstep with the P2P-arm bytes --------------
96+
TEST(DgbWonBlockFinalizeKAT, HexMatchesFinalizedBytes) {
97+
auto out = finalize_won_block_pow(synthetic_block(), easy_quarter_target(), 0, kTestBudget);
98+
ASSERT_TRUE(out.has_value());
99+
EXPECT_EQ(out->hex.size(), out->bytes.size() * 2);
100+
EXPECT_EQ(out->hex, finalize_block_to_hex(out->bytes));
101+
}
102+
103+
// --- ONLY the 4 nonce bytes change: merkle_root + every tx byte untouched ---
104+
TEST(DgbWonBlockFinalizeKAT, OnlyNonceBytesMutated) {
105+
auto in = synthetic_block();
106+
auto out = finalize_won_block_pow(in, easy_quarter_target(), 0, kTestBudget);
107+
ASSERT_TRUE(out.has_value());
108+
ASSERT_EQ(out->bytes.size(), in.size());
109+
// header[0..75] (version|prev|merkle|time|bits) identical.
110+
for (std::size_t i = 0; i < kHeaderNonceOffset; ++i)
111+
EXPECT_EQ(out->bytes[i], in[i]) << "prefix byte " << i << " changed";
112+
// tx tail [80..] identical.
113+
for (std::size_t i = 80; i < in.size(); ++i)
114+
EXPECT_EQ(out->bytes[i], in[i]) << "tail byte " << i << " changed";
115+
}
116+
117+
// --- fail-closed: too short to frame an 80-byte header -> nullopt -----------
118+
TEST(DgbWonBlockFinalizeKAT, FailsClosedOnShortInput) {
119+
std::vector<unsigned char> shortb(79, 0x11);
120+
auto out = finalize_won_block_pow(shortb, easy_quarter_target(), 0, kTestBudget);
121+
EXPECT_FALSE(out.has_value());
122+
}
123+
124+
} // namespace

0 commit comments

Comments
 (0)