Skip to content
Closed
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
145 changes: 145 additions & 0 deletions scripts/dgb_regtest_won_block_soak.sh
Original file line number Diff line number Diff line change
@@ -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 <digibyte.conf> --coin-magic <hex> --coin-genesis <hash>.
#
# 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" <<CONF
regtest=1
server=1
rpcuser=c2pool
rpcpassword=$pass
fallbackfee=0.0002
[regtest]
rpcport=$rpcport
CONF
chmod 600 "$dd/digibyte.conf"
fi
}
provision "$DATADIR_A" "$RPCPORT_A"
provision "$DATADIR_B" "$RPCPORT_B"

wait_rpc() { local n=0; until "$@" getblockcount >/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."
70 changes: 66 additions & 4 deletions src/c2pool/main_dgb.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@
#include <impl/dgb/coin/embedded_tx_select.hpp> // make_mempool_tx_source (EmbeddedTxSource)
#include <impl/dgb/coin/won_block_dispatch.hpp>
#include <impl/dgb/coin/reconstruct_closure.hpp> // make_reconstruct_closure_from_template (#280)
#include <impl/dgb/coin/regrind_block.hpp> // regrind_block_nonce -- forced-won soak PoW (#82 4b/4c)
#include <impl/dgb/coin/header_sample_build.hpp> // c2pool::dgb::compact_to_target (nBits -> u256)
#include <impl/dgb/coin/won_share_inputs.hpp> // won_share_inputs (#279)
#include <impl/dgb/coin/node_interface.hpp>
#include <impl/dgb/coin/header_ingest.hpp>
Expand Down Expand Up @@ -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"
Expand Down Expand Up @@ -142,7 +144,9 @@ int run_node(const core::CoinParams& params, bool testnet,
const std::vector<std::byte>& 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;

Expand Down Expand Up @@ -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::pair<std::vector<unsigned char>, 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<uint32_t>(blk->first[72])
| (static_cast<uint32_t>(blk->first[73]) << 8)
| (static_cast<uint32_t>(blk->first[74]) << 16)
| (static_cast<uint32_t>(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<unsigned char>& block_bytes) {
/*p2p_relay=*/[&ioc, &coin_p2p, no_p2p_relay](const std::vector<unsigned char>& 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);
Expand Down Expand Up @@ -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";
Expand Down Expand Up @@ -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);
Expand All @@ -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.
Expand Down
Loading
Loading