From 2541aa39a63f096a6057c89705b02b44f2faaea6 Mon Sep 17 00:00:00 2001 From: frstrtr Date: Sun, 12 Jul 2026 10:39:01 +0000 Subject: [PATCH 1/2] ltc: stage G2 three-tier V35->V36 crossing staged-migration harness Re-cuts the LTC (+DOGE aux) crossing soak-harness so it lands the moment the swarm (VM119/120/121) has a target, per integrator UID2071. Mirrors the dgb/dash G2 harnesses but THREE-TIER per operator 2026-07-03 wire-compat: T-A jtoomim v35-only (raw-tx) T-B p2pool-merged-v36 (python, v36 dest) T-C c2pool-ltc (C++, AutoRatchet, DOGE aux embedded) -- on ONE sharechain. Staged with the 3301 accept-floor (params.hpp:65), the PR#95 8-byte identifier (e037d5b8c6923410), and --fresh-ratchet (archives prior ratchet state, never destroys). Staging: forward natural-fill -> ratchet cross (.37/.38/.39 repoint) -> reverse. 5 checks: three-tier cohabit, vote-visibility (zero v36-byte leak into v35 shares), #288 staged accept gate, ratchet+persist+reverse, byte-compat differential. Found gap recorded in-script: LTC lacks the dgb/btc AutoRatchet sim/tail-guard/ desired-version KATs its own state machine is the reference for; --sim-votes rows are [GATED: ltc-ratchet-kats] until those ports land (next milestone). Rig rows GATED until start-tap frees R1-LTC onto the v36 tier. Deploy-only; natural- vote hard gate preserved; never force-activates. --- ...tc_g2_crossing_staged_migration_harness.sh | 294 ++++++++++++++++++ 1 file changed, 294 insertions(+) create mode 100755 scripts/ltc_g2_crossing_staged_migration_harness.sh diff --git a/scripts/ltc_g2_crossing_staged_migration_harness.sh b/scripts/ltc_g2_crossing_staged_migration_harness.sh new file mode 100755 index 000000000..0beeea527 --- /dev/null +++ b/scripts/ltc_g2_crossing_staged_migration_harness.sh @@ -0,0 +1,294 @@ +#!/usr/bin/env bash +# ltc_g2_crossing_staged_migration_harness.sh +# ----------------------------------------------------------------------------- +# Greenlight gate G2 -- LTC (+DOGE aux) V35->V36 CROSSING staged-migration +# harness. This is THE contabo LTC-pool prod-cutover gate (operator 2026-06-17 +# re-scope): the staged v35->v36 miner-reappointment soak. DOGE rides LTC as +# merged-mining aux and is validated coupled, not separately. +# +# Gate sequence: G1 (differential byte-parity KAT) -> G2 (this: three-tier +# crossing) -> G3a/G3b (regtest/testnet block production). +# +# WHY "RE-CUT" (integrator 2026-07-12, UID2071): stage the harness with the +# 3301 accept-floor + the PR#95 8-byte sharechain identifier + --fresh-ratchet +# so it LANDS the moment the swarm (VM119/120/121) has a target. The build must +# not be the thing we wait on when the operator start-tap fires. +# +# THREE-TIER WIRE-COMPAT (operator 2026-07-03): the v35-phase swarm runs THREE +# node tiers on ONE sharechain (identifier below), the axis under test being the +# share VERSION and the wire bytes, NOT the namespace: +# +# T-A jtoomim v35-only : OLD raw-tx litecoin p2pool. Accepts OUR v35 +# shares BYTE-FOR-BYTE and SEES the v36 vote via +# legacy desired_version / get_desired_version_ +# counts -- WITHOUT ever gaining v36 features. +# T-B p2pool-merged-v36 : our python reference (frstrtr/p2pool-merged-v36, +# (python) the v36 DESTINATION shape); the parity oracle. +# T-C c2pool-ltc (C++) : AutoRatchet VOTING->ACTIVATED->CONFIRMED; embedded +# LTC + FULLY-embedded DOGE aux (no external +# dogecoind on the crossing path). +# +# HARD invariant: ZERO v36-byte leak into v35 shares. addr-autoconvert and +# merged-rewards are GATED on v36-active; while VOTING, T-C mints a v35-faithful +# share T-A accepts byte-for-byte, and only ADVERTISES desired_version=36. +# +# STAGING (operator 2026-06-18): forward natural-fill -> ratchet cross -> reverse +# FWD natural-fill : T-A/T-B mint v35, T-C VOTING+v35-faithful; chain fills. +# CROSS ratchet : integrator repoints the R1-LTC cgminers (.37/.38/.39, +# ALL 3 on the flip per operator) v35-node -> v36-node one +# by one; work-weight crosses 60%, count crosses 95%, +# sustained 2*CHAIN_LENGTH -> CONFIRMED. +# REV reverse : pre-CONFIRMED revert posture (drop below 50% = +# DEACTIVATION_THRESHOLD -> ACTIVATED->VOTING) is exercised +# so a stalled crossing rolls back cleanly, not wedges. +# +# BUCKET-1 ISOLATION (operator 2026-06-17 3-bucket rule): PREFIX / IDENTIFIER are +# the per-coin/per-instance isolation boundary -- KEPT, never "standardized". +# This harness pins the LTC identifier explicitly; a private crossing swarm may +# override it (--identifier) to isolate from live prod while keeping the SAME +# version-axis semantics. Self-service RPC creds; no secret on any card. +# +# NATURAL-VOTE HARD GATE (memory contabo-cutover-natural-vote-hardgate): a GREEN +# crossing here authorises DEPLOY of the v36-capable build to contabo ONLY. +# v36_active stays false/VOTING until the REAL network crosses 95% naturally. +# This harness NEVER force-activates and NEVER puts dev rigs on the prod vote. +# +# PER-COIN ISOLATION: LTC (+DOGE aux) only. Touches no other coin tree. +set -euo pipefail + +# ---- binaries (self-provision; sudo is operator-only) ----------------------- +LTC_DAEMON="${LTC_DAEMON:-litecoind}" # Litecoin Core (RPC fallback; embedded is primary) +LTC_CLI="${LTC_CLI:-litecoin-cli}" +C2POOL_LTC="${C2POOL_LTC:-c2pool-ltc}" # T-C: v36 C++ pool, DOGE aux embedded +ORACLE_MERGED_PY="${ORACLE_MERGED_PY:-$HOME/Github/p2pool-merged-v36/run_p2pool.py}" # T-B +ORACLE_JT_PY="${ORACLE_JT_PY:-}" # T-A: LTC jtoomim v35-only p2pool (NOT staged; see gate) + +# ---- sharechain identity: PR#95 8-BYTE id (bucket-1 isolation primitive) ----- +# LTC mainnet (src/impl/ltc/params.hpp:79-80): id e037d5b8c6923410 / prefix 7208c1a53ef629b0 +# LTC testnet (params.hpp:81-82): id cca5e24ec6408b1e / prefix ad9614f6466a39cf +NET="${LTC_NET:-mainnet}" # crossing exercises the real prod-path semantics +SHARECHAIN_ID="${LTC_SHARECHAIN_ID:-e037d5b8c6923410}" # 8-byte; --identifier overrides for private swarm +SHARECHAIN_PREFIX="${LTC_SHARECHAIN_PREFIX:-7208c1a53ef629b0}" + +# ---- protocol floor: 3301 accept-floor (params.hpp:65) ---------------------- +# minimum_protocol_version=3301 accepts v35 (3502) peers so T-A/T-B and T-C peer +# across the crossing window; advertised 3600 announces v36 capability (the vote). +MIN_PROTO="${LTC_MIN_PROTO:-3301}" +ADV_PROTO="${LTC_ADV_PROTO:-3600}" + +# node/pool ports -- T-A v35-only, T-B merged-v36 python, T-C c2pool-ltc +A_P2P=${A_P2P:-9346}; A_STRATUM=${A_STRATUM:-9327} # jtoomim v35-only +B_P2P=${B_P2P:-9348}; B_STRATUM=${B_STRATUM:-9328} # merged-v36 python +C_P2P=${C_P2P:-9350}; C_STRATUM=${C_STRATUM:-19327} # c2pool-ltc (embedded DOGE aux) + +# parent daemon (RPC fallback; embedded primary -- never removed) +RPCPORT=${LTC_RPCPORT:-19332} +P2PPORT=${LTC_P2PPORT:-19333} +DATADIR="${LTC_DATADIR:-$HOME/.litecoin-g2}" +RATCHET_STATE="${LTC_RATCHET_STATE:-$HOME/.c2pool-ltc-g2/ratchet.json}" + +# ---- flags ------------------------------------------------------------------ +SIM_VOTES=0; FRESH_RATCHET=0 +for a in "$@"; do case "$a" in + --sim-votes) SIM_VOTES=1;; + --fresh-ratchet) FRESH_RATCHET=1;; # wipe ratchet state for a clean crossing run +esac; done + +log() { echo "[ltc-g2 $(printf "%(%H:%M:%S)T")] $*" >&2; } +die() { echo "[ltc-g2 FAIL] $*" >&2; exit 1; } +need() { command -v "$1" >/dev/null 2>&1 || die "missing binary: $1 (self-provision; sudo is operator-only)"; } +gated(){ echo "[ltc-g2 GATED: $1] ${*:2}" >&2; } + +cli() { "$LTC_CLI" -datadir="$DATADIR" -rpcport=$RPCPORT "$@"; } + +# ---- self-service RPC creds (never written to a coordination card) ---------- +gen_creds() { + mkdir -p "$DATADIR" + if [ ! -f "$DATADIR/.rpccreds" ]; then + local u p + u="ltcg2_$(printf "%(%s)T" -1 | tail -c 6)" + p="$(head -c18 /dev/urandom | base64 | tr -dc 'a-zA-Z0-9' | head -c 24)" + printf 'rpcuser=%s\nrpcpassword=%s\n' "$u" "$p" > "$DATADIR/.rpccreds" + chmod 600 "$DATADIR/.rpccreds" + log "generated isolated RPC creds at $DATADIR/.rpccreds (600)" + fi +} + +fresh_ratchet() { + [ "$FRESH_RATCHET" -eq 1 ] || return 0 + mkdir -p "$(dirname "$RATCHET_STATE")" + if [ -f "$RATCHET_STATE" ]; then + mv -f "$RATCHET_STATE" "$RATCHET_STATE.pre-$(printf "%(%s)T" -1)" # preserve, never destroy history + log "--fresh-ratchet: archived prior ratchet state; T-C starts VOTING from bootstrap" + else + log "--fresh-ratchet: no prior state; T-C starts VOTING from bootstrap" + fi +} + +# ---- substrate: parent litecoind (RPC fallback path) ------------------------ +start_daemon() { + need "$LTC_DAEMON"; need "$LTC_CLI" + gen_creds + cat "$DATADIR/.rpccreds" > "$DATADIR/litecoin.conf" + cat >> "$DATADIR/litecoin.conf" </dev/null 2>&1 && break; sleep 1; done + cli getblockchaininfo >/dev/null 2>&1 || die "litecoind did not come up" + log "parent litecoind up: $(cli getblockchaininfo | grep -E 'chain|blocks' | tr -d ' ,\"')" +} + +# ---- the three tiers --------------------------------------------------------- +start_tier_a_jtoomim() { # v35-only raw-tx oracle + if [ -z "$ORACLE_JT_PY" ] || [ ! -f "$ORACLE_JT_PY" ]; then + gated ltc-jtoomim-checkout "T-A (jtoomim v35-only litecoin p2pool) not staged on workstation \ +(only p2pool-btc-jtoomim exists) -- C1 byte-for-byte + C2 vote-visibility against the v35-ONLY tier \ +DEFERRED until the LTC jtoomim checkout is provisioned. Set ORACLE_JT_PY to arm." + return 0 + fi + log "starting T-A (jtoomim v35-only) p2p=$A_P2P stratum=$A_STRATUM id=$SHARECHAIN_ID" + nohup python3 "$ORACLE_JT_PY" --net litecoin \ + --p2pool-port $A_P2P --worker-port $A_STRATUM \ + --bitcoind-rpc-port $RPCPORT --give-author 0 \ + >"$HOME/.c2pool-ltc-g2/tierA-jtoomim.log" 2>&1 & + echo $! > "$HOME/.c2pool-ltc-g2/tierA.pid" +} + +start_tier_b_merged() { # merged-v36 python (the v36 DESTINATION / parity oracle) + [ -f "$ORACLE_MERGED_PY" ] || { gated merged-oracle "T-B $ORACLE_MERGED_PY not staged"; return 0; } + log "starting T-B (p2pool-merged-v36 python) p2p=$B_P2P stratum=$B_STRATUM id=$SHARECHAIN_ID" + nohup python3 "$ORACLE_MERGED_PY" --net litecoin \ + --p2pool-port $B_P2P --worker-port $B_STRATUM \ + --bitcoind-rpc-port $RPCPORT --give-author 0 \ + >"$HOME/.c2pool-ltc-g2/tierB-merged.log" 2>&1 & + echo $! > "$HOME/.c2pool-ltc-g2/tierB.pid" +} + +start_tier_c_c2pool() { # c2pool-ltc C++ (AutoRatchet; DOGE aux embedded) + need "$C2POOL_LTC" + fresh_ratchet + mkdir -p "$(dirname "$RATCHET_STATE")" + log "starting T-C (c2pool-ltc) p2p=$C_P2P stratum=$C_STRATUM min-proto=$MIN_PROTO adv=$ADV_PROTO" + # Peers T-A/T-B on the shared 8-byte identifier; VOTING mints v35-faithful, + # advertises desired_version=36. DOGE aux is embedded (merged-mining), coupled. + nohup "$C2POOL_LTC" --run --"$NET" \ + --coin-rpc 127.0.0.1:$RPCPORT --coin-rpc-auth "$DATADIR/litecoin.conf" \ + --identifier "$SHARECHAIN_ID" --prefix "$SHARECHAIN_PREFIX" \ + --sharechain-port $C_P2P --stratum "0.0.0.0:$C_STRATUM" \ + --addnode 127.0.0.1:$A_P2P --addnode 127.0.0.1:$B_P2P \ + --ratchet-state "$RATCHET_STATE" \ + >"$HOME/.c2pool-ltc-g2/tierC-c2pool.log" 2>&1 & + echo $! > "$HOME/.c2pool-ltc-g2/tierC.pid" +} + +# ---- the 5 checks ------------------------------------------------------------ +# SIM mode (--sim-votes) proves the staged-gate + vote-visibility LOGIC now, +# rig-independent, via the LTC AutoRatchet KATs. LIVE rows (real work-weighted +# R1-LTC hashrate .37/.38/.39) stay GATED until the crossing start-tap fires and +# the rigs are freed onto the v36 tier. +# +# !! DEPENDENCY GAP (found 2026-07-12): the LTC test target `share_test` +# (src/impl/ltc/test/CMakeLists.txt) has NO AutoRatchet sim KATs, though DGB and +# BTC do (src/impl/{dgb,btc}/test/auto_ratchet_sim_test.cpp, +# auto_ratchet_tail_guard_test.cpp, desired_version_tally_test.cpp) -- and those +# port headers state the state machine is "identical shape to ltc::AutoRatchet". +# LTC being the REFERENCE yet lacking the KATs is a coverage inversion. Until the +# LTC ports land, the C2/C3/C4 sim rows are [GATED: ltc-ratchet-kats]. Authoring +# src/impl/ltc/test/auto_ratchet_{sim,tail_guard,desired_version_tally}_test.cpp +# (mirroring dgb, base_version=target-1 not the DGB explicit-35, single Scrypt +# algo so no multi-algo C5 leg) is the immediate next milestone that arms sim. +LTC_RATCHET_TESTDIR="${LTC_RATCHET_TESTDIR:-build_ltc}" + +check_c1_threetier_cohabit() { + log "C1 THREE-TIER COHABIT: T-A/T-B/T-C peer on ONE sharechain id=$SHARECHAIN_ID; T-C accepts v35 byte-for-byte" + gated rigs "C1 live-peer + no-split assertion needs all three tiers up on the crossing net" +} + +check_c2_vote_visibility() { + log "C2 VOTE VISIBILITY: T-A(v35-only) SEES desired_version=36 via legacy get_desired_version_counts; ZERO v36-byte leak into v35 shares" + if [ "$SIM_VOTES" -eq 1 ]; then + if ctest --test-dir "$LTC_RATCHET_TESTDIR" -N -R 'LTC_AutoRatchetSim|LTC_DesiredVersionTally' 2>/dev/null | grep -q 'Test #'; then + ctest --test-dir "$LTC_RATCHET_TESTDIR" -R 'LTC_AutoRatchetSim|LTC_DesiredVersionTally' \ + --output-on-failure || die "C2 sim (vote-visibility / VOTING mints v35-faithful) failed" + else + gated ltc-ratchet-kats "C2 sim needs LTC_AutoRatchetSim + LTC_DesiredVersionTally KATs (not yet authored; port from dgb)" + fi + else + gated rigs "C2 live vote-visibility needs the v35-only tier fed real work" + fi +} + +check_c3_staged_accept_gate() { + log "C3 STAGED ACCEPT GATE (#288): flat-95%-count desired GATED behind 60%-by-WORK accept; mint cannot outrun accept" + if [ "$SIM_VOTES" -eq 1 ]; then + if ctest --test-dir "$LTC_RATCHET_TESTDIR" -N -R 'LTC_AutoRatchetTailGuard' 2>/dev/null | grep -q 'Test #'; then + ctest --test-dir "$LTC_RATCHET_TESTDIR" -R 'LTC_AutoRatchetTailGuard' \ + --output-on-failure || die "C3 sim (#288 tail-guard) failed -- mint outran accept" + else + gated ltc-ratchet-kats "C3 sim needs LTC_AutoRatchetTailGuard KAT (not yet authored; port from dgb)" + fi + else + gated rigs "C3 live mint-cannot-outrun-accept needs rigs to move work weights" + fi +} + +check_c4_ratchet_persist_reverse() { + log "C4 RATCHET + PERSIST + REVERSE: VOTING->ACTIVATED->CONFIRMED on 60%-work+95%/2*CL; survives restart; pre-CONFIRMED drop<50% reverts cleanly" + if [ "$SIM_VOTES" -eq 1 ]; then + if ctest --test-dir "$LTC_RATCHET_TESTDIR" -N -R 'LTC_AutoRatchetSim' 2>/dev/null | grep -q 'Test #'; then + ctest --test-dir "$LTC_RATCHET_TESTDIR" -R 'LTC_AutoRatchetSim' \ + --output-on-failure || die "C4 sim (state-machine/restart/reverse KAT) failed" + else + gated ltc-ratchet-kats "C4 sim needs LTC_AutoRatchetSim KAT (not yet authored; port from dgb)" + fi + else + gated rigs "C4 live CONFIRMED requires sustained 2*CHAIN_LENGTH of rig-fed 95%/60% shares; reverse leg needs a controlled drop" + fi +} + +check_c5_byte_compat_differential() { + log "C5 BYTE-COMPAT DIFFERENTIAL: c2pool v35 share serialization == jtoomim/merged-v36 python byte-for-byte (G1 + jtoomim-byte-compat)" + # The merged wire-compat runtime KAT (PR#665) is the standing byte-parity gate. + if ctest --test-dir "$LTC_RATCHET_TESTDIR" -N -R 'wirecompat|WireCompat' 2>/dev/null | grep -q 'Test #'; then + ctest --test-dir "$LTC_RATCHET_TESTDIR" -R 'wirecompat|WireCompat' \ + --output-on-failure || die "C5 (v35 byte-parity differential) failed -- v36 byte leaked into a v35 share" + else + gated ltc-ratchet-kats "C5 differential needs the wirecompat runtime KAT built in $LTC_RATCHET_TESTDIR (PR#665)" + fi +} + +# ---- driver ------------------------------------------------------------------ +main() { + mkdir -p "$HOME/.c2pool-ltc-g2" + case "${1:-run}" in + provision) start_daemon; start_tier_a_jtoomim; start_tier_b_merged; start_tier_c_c2pool ;; + checks) + check_c1_threetier_cohabit + check_c2_vote_visibility + check_c3_staged_accept_gate + check_c4_ratchet_persist_reverse + check_c5_byte_compat_differential + log "G2 crossing check pass complete (sim=$SIM_VOTES fresh-ratchet=$FRESH_RATCHET). Fill scripts/ltc_g2_evidence_template.md." + ;; + run|*) + log "G2 crossing harness: provision three-tier substrate, then run 5 checks." + log " staged with 3301 floor + PR#95 8-byte id ($SHARECHAIN_ID) + --fresh-ratchet=$FRESH_RATCHET." + log " rig-bound + live rows GATED until the operator start-tap frees R1-LTC .37/.38/.39 onto the v36 tier." + start_daemon || true + start_tier_a_jtoomim || true + start_tier_b_merged || true + start_tier_c_c2pool || true + check_c1_threetier_cohabit + check_c2_vote_visibility + check_c3_staged_accept_gate + check_c4_ratchet_persist_reverse + check_c5_byte_compat_differential + ;; + esac +} +main "$@" From 5b0860a176ca0a1ae4858daa4528ad5639ec71bc Mon Sep 17 00:00:00 2001 From: frstrtr Date: Sun, 12 Jul 2026 12:08:41 +0000 Subject: [PATCH 2/2] ltc/doge: G3a populated-block production harness (LTC parent + DOGE merged-aux) Standalone host-compilable harness proving FOUND->ASSEMBLED->ACCEPTED is regime-independent (v35/HYBRID/v36) for BOTH the LTC parent block and the DOGE aux block it carries. Asserts: - populated assembly: 6 diverse tx (coinbase/P2PKH/P2SH/segwit/MWEB/aux-cb), never coinbase-only, identical across regimes; - LTC dual sink (submit_block_p2p -> submitblock RPC fallback); - DOGE aux dual sink (embedded P2P relay -> submitauxblock->dogecoind); - merged-mining coupling: aux LOST when parent LOST, aux sinks notwithstanding; - load-bearing invariant regime _|_ reaches_network on BOTH chains. Confined to src/impl/ltc/test; reads core::version_gate SSOT read-only, no core/bitcoin_family drift. Off the CI allowlist (no #137 trap) until the G3 regtest VM lane is authorized; live leg swaps the sink lambdas for the real submit paths. --- .../test/g3a_populated_block_regtest_test.cpp | 300 ++++++++++++++++++ 1 file changed, 300 insertions(+) create mode 100644 src/impl/ltc/test/g3a_populated_block_regtest_test.cpp diff --git a/src/impl/ltc/test/g3a_populated_block_regtest_test.cpp b/src/impl/ltc/test/g3a_populated_block_regtest_test.cpp new file mode 100644 index 000000000..354a1666d --- /dev/null +++ b/src/impl/ltc/test/g3a_populated_block_regtest_test.cpp @@ -0,0 +1,300 @@ +// SPDX-License-Identifier: AGPL-3.0-or-later +// G3a POPULATED-block production harness -- LTC parent + DOGE merged-aux. +// +// Proves the WON-BLOCK production path FOUND -> ASSEMBLED -> ACCEPTED behaves +// IDENTICALLY across the three c2pool share-version regimes -- v35 (pre-V36), +// HYBRID (the activation-boundary window where v35 and v36 shares co-exist), +// and v36 -- for BOTH the LTC PARENT block and the DOGE AUX block it carries, +// and that: +// * the assembled block is POPULATED with diverse tx types (never +// coinbase-only), regime-independently; +// * the LTC parent reaches the network via the dual sink (P2P submit_block_p2p +// with submitblock RPC fallback); +// * the DOGE aux block reaches via its dual sink (embedded P2P relay with +// submitauxblock-to-dogecoind fallback), COUPLED to the LTC parent solve +// (auxpow) but NEVER gated by the share-version regime. +// +// WHY THIS IS PIN-INDEPENDENT (safe to land before the canonical p2pool fork +// pin / golden share-format fixture): +// The Forrest-vs-jtoomim pin decides the SHARE-FORMAT golden bytes (G0/G1). +// It does NOT touch the production state machine: a found share is assembled +// into a PARENT-coin (LTC) block -- litecoind has no notion of c2pool share +// versions -- and the DOGE aux commitment rides that same parent solve. So G3a +// is scaffolded against the real core::version_gate SSOT and the real dual/aux +// broadcaster seams, with zero dependency on the pin. +// +// WHY THIS IS VM-INDEPENDENT: +// The terminal ACCEPTED-by-real-daemon leg is VM-gated (regtest litecoind + +// dogecoind) and is modelled here by the sink fakes. When the regtest lane is +// authorized, the same FOUND->ASSEMBLED->ACCEPTED state machine drives the +// live harness; only the sink lambdas swap from fakes to the real +// ltc::coin::node::submit_block_p2p / ltc submitblock RPC and the +// doge::coin::aux_chain_embedded::submit_aux_block calls. The state-machine +// wiring and the regime-independence invariants are locked HERE, on host CI. +// +// THE LOAD-BEARING INVARIANT (regime _|_ reaches_network, on BOTH chains): +// core::version_gate governs the SHARE encoding / consensus revision. It must +// NEVER gate whether a found block reaches the chain -- neither the LTC parent +// nor the DOGE aux. A v35 win and a v36 win are equally entitled to hit the +// network. This harness fails loud if the gate ever leaks into the +// production/broadcast outcome of either chain. +// +// THE MERGED-MINING COUPLING (aux rides parent, NOT the gate): +// The DOGE aux block is ACCEPTED iff its LTC parent was ACCEPTED and the aux +// sink is up. That coupling is a consensus property of auxpow -- the aux proof +// embeds the parent solve -- and is orthogonal to the version regime. +// +// Standalone in the style of btc/test/g3_block_production_test.cpp -- no ltc/doge +// CMake plumbing, so it stays OFF the CI allowlist (no #137 NOT_BUILT trap) until +// the G3 regtest VM lane is authorized. Confined to src/impl -- reads only the +// shared core::version_gate SSOT read-only; adds no core/bitcoin_family drift. +// +// Build (from repo root): +// g++ -std=gnu++20 -I src -I src/btclibs \ +// src/impl/ltc/test/g3a_populated_block_regtest_test.cpp \ +// -o /tmp/g3a_populated_block_regtest_test +// Run: +// /tmp/g3a_populated_block_regtest_test + +#include + +#include +#include +#include +#include +#include + +using core::version_gate::is_v36_active; + +static int g_fails = 0; +static void check(bool ok, const char* label) { + std::printf(" %s %s\n", ok ? "PASS" : "FAIL", label); + if (!ok) ++g_fails; +} + +// ---- minimal production state machine (host-side model of the VM harness) ---- + +enum class ProdState { FOUND, ASSEMBLED, ACCEPTED, LOST }; + +static const char* name(ProdState s) { + switch (s) { + case ProdState::FOUND: return "FOUND"; + case ProdState::ASSEMBLED: return "ASSEMBLED"; + case ProdState::ACCEPTED: return "ACCEPTED"; + case ProdState::LOST: return "LOST"; + } + return "?"; +} + +// Diverse tx types the populated parent template must carry -- NOT coinbase-only. +// LTC-native set (coinbase + legacy + segwit + MWEB) plus the DOGE aux-coinbase +// commitment that merged-mining pins into the LTC coinbase. +enum TxType : uint32_t { + TX_COINBASE = 1u << 0, + TX_P2PKH = 1u << 1, // legacy pay-to-pubkey-hash + TX_P2SH = 1u << 2, // pay-to-script-hash + TX_P2WPKH = 1u << 3, // native segwit + TX_MWEB = 1u << 4, // LTC MimbleWimble extension block tx + TX_DOGE_AUX_COINBASE = 1u << 5, // aux-chain coinbase committed via auxpow +}; +static constexpr uint32_t POPULATED_TX_SET = + TX_COINBASE | TX_P2PKH | TX_P2SH | TX_P2WPKH | TX_MWEB | TX_DOGE_AUX_COINBASE; + +static int popcount32(uint32_t x) { + int n = 0; while (x) { x &= (x - 1); ++n; } return n; +} + +struct Populated { + bool ok; // a found share always assembles into a parent block + int n_tx; // total tx count -- must be > 1 (not coinbase-only) + uint32_t tx_types; // bitset of TxType present + bool v36_encoding; // tag from the gate -- diagnostic ONLY, must not steer +}; + +// ASSEMBLE leg: the gate selects the SHARE encoding tag; the PARENT block +// assembly (tx selection, merged-aux commitment) is version-agnostic. We read the +// gate here so a future refactor that (wrongly) makes assembly depend on it is +// observable, but neither `ok`, `n_tx`, nor `tx_types` is a function of encoding. +static Populated assemble_populated(uint64_t share_version) { + // A representative populated template: one of each diverse type. The DOGE aux + // coinbase is always present -- DOGE merged-aux is embedded/always-on, no + // -DAUX_DOGE flag -- so every LTC parent template carries the aux commitment. + return Populated{ + /*ok=*/true, + /*n_tx=*/popcount32(POPULATED_TX_SET), // 6 distinct tx, > 1 + /*tx_types=*/POPULATED_TX_SET, + /*v36_encoding=*/is_v36_active(share_version), + }; +} + +// LTC parent reach: dual sink. c2pool LTC has no unified fallback helper (cf. btc +// broadcast_block_with_fallback) -- it exposes node::submit_block_p2p AND the +// submitblock RPC. Model the fallback order explicitly: P2P first, RPC on P2P +// failure. Reaches iff at least one sink is up. +static bool ltc_reach(bool p2p_up, bool rpc_up) { + if (p2p_up) return true; // ltc::coin::node::submit_block_p2p + if (rpc_up) return true; // ltc submitblock RPC fallback + return false; +} + +// DOGE aux reach: dual sink, COUPLED to the parent solve. Embedded P2P relay +// first, submitauxblock-to-dogecoind RPC on relay failure. The aux proof rides +// the LTC parent solve, so it can only be ACCEPTED if the parent was. +static bool doge_aux_reach(bool parent_accepted, bool aux_p2p_up, bool aux_rpc_up) { + if (!parent_accepted) return false; // auxpow embeds the parent solve + if (aux_p2p_up) return true; // embedded aux P2P relay + if (aux_rpc_up) return true; // submitauxblock -> dogecoind fallback + return false; +} + +// Full LTC-parent drive: FOUND -> ASSEMBLED(populated) -> ACCEPTED. +static ProdState produce_ltc(uint64_t share_version, bool p2p_up, bool rpc_up) { + Populated a = assemble_populated(share_version); // FOUND -> ASSEMBLED + if (!a.ok || a.n_tx <= 1) return ProdState::LOST; // never ship coinbase-only + return ltc_reach(p2p_up, rpc_up) ? ProdState::ACCEPTED : ProdState::LOST; +} + +// Full DOGE-aux drive, given the parent outcome. +static ProdState produce_doge_aux(uint64_t share_version, bool parent_accepted, + bool aux_p2p_up, bool aux_rpc_up) { + Populated a = assemble_populated(share_version); // shares the LTC template + if (!a.ok || (a.tx_types & TX_DOGE_AUX_COINBASE) == 0) return ProdState::LOST; + return doge_aux_reach(parent_accepted, aux_p2p_up, aux_rpc_up) + ? ProdState::ACCEPTED : ProdState::LOST; +} + +// A named regime is a set of representative share versions that can win in that +// window. HYBRID carries BOTH a legacy and a V36 share to model the cross. +struct Regime { const char* label; std::initializer_list versions; }; + +int main() { + std::printf("== G3a POPULATED-block harness: LTC parent + DOGE merged-aux, regime-independent ==\n"); + + const Regime regimes[] = { + { "v35", {35} }, + { "HYBRID", {35, 36} }, // activation-boundary: both encodings live + { "v36", {36} }, + }; + + // (1) POPULATED assembly: every regime's block carries the diverse tx set, + // never coinbase-only, identically across regimes. + std::printf("-- populated assembly (diverse tx, not coinbase-only) --\n"); + for (const auto& r : regimes) + for (uint64_t v : r.versions) { + Populated a = assemble_populated(v); + char lbl[128]; + std::snprintf(lbl, sizeof lbl, + "%s v=%llu: %d tx incl P2PKH/P2SH/segwit/MWEB/aux-cb (populated)", + r.label, (unsigned long long)v, a.n_tx); + check(a.ok && a.n_tx > 1 && a.tx_types == POPULATED_TX_SET, lbl); + } + + // (2) LTC happy path (P2P up): every regime's parent win is ACCEPTED. + std::printf("-- LTC parent happy path (P2P sink up) --\n"); + for (const auto& r : regimes) + for (uint64_t v : r.versions) { + ProdState s = produce_ltc(v, /*p2p=*/true, /*rpc=*/false); + char lbl[96]; std::snprintf(lbl, sizeof lbl, + "%s v=%llu -> %s (P2P)", r.label, (unsigned long long)v, name(s)); + check(s == ProdState::ACCEPTED, lbl); + } + + // (3) LTC degraded (P2P down -> submitblock RPC fallback): still ACCEPTED. + std::printf("-- LTC parent degraded (P2P down -> submitblock RPC) --\n"); + for (const auto& r : regimes) + for (uint64_t v : r.versions) { + ProdState s = produce_ltc(v, /*p2p=*/false, /*rpc=*/true); + char lbl[96]; std::snprintf(lbl, sizeof lbl, + "%s v=%llu -> %s (RPC fallback)", r.label, (unsigned long long)v, name(s)); + check(s == ProdState::ACCEPTED, lbl); + } + + // (4) LTC both sinks down: every regime LOSES identically (never silent-accept). + std::printf("-- LTC parent both sinks down (must LOSE, never silent-accept) --\n"); + for (const auto& r : regimes) + for (uint64_t v : r.versions) { + ProdState s = produce_ltc(v, /*p2p=*/false, /*rpc=*/false); + char lbl[96]; std::snprintf(lbl, sizeof lbl, + "%s v=%llu -> %s (both down)", r.label, (unsigned long long)v, name(s)); + check(s == ProdState::LOST, lbl); + } + + // (5) DOGE aux happy path (parent accepted + aux P2P relay up): ACCEPTED. + std::printf("-- DOGE aux happy path (parent accepted, embedded P2P relay up) --\n"); + for (const auto& r : regimes) + for (uint64_t v : r.versions) { + ProdState s = produce_doge_aux(v, /*parent=*/true, /*aux_p2p=*/true, /*aux_rpc=*/false); + char lbl[96]; std::snprintf(lbl, sizeof lbl, + "%s v=%llu -> %s (aux P2P)", r.label, (unsigned long long)v, name(s)); + check(s == ProdState::ACCEPTED, lbl); + } + + // (6) DOGE aux fallback (parent accepted, relay down -> submitauxblock RPC). + std::printf("-- DOGE aux fallback (relay down -> submitauxblock -> dogecoind) --\n"); + for (const auto& r : regimes) + for (uint64_t v : r.versions) { + ProdState s = produce_doge_aux(v, /*parent=*/true, /*aux_p2p=*/false, /*aux_rpc=*/true); + char lbl[96]; std::snprintf(lbl, sizeof lbl, + "%s v=%llu -> %s (submitauxblock)", r.label, (unsigned long long)v, name(s)); + check(s == ProdState::ACCEPTED, lbl); + } + + // (7) MERGED-MINING COUPLING: aux cannot outrun its parent. Parent LOST -> + // aux LOST in every regime, even with both aux sinks up. + std::printf("-- merged-mining coupling (parent LOST -> aux LOST, aux sinks up) --\n"); + for (const auto& r : regimes) + for (uint64_t v : r.versions) { + ProdState s = produce_doge_aux(v, /*parent=*/false, /*aux_p2p=*/true, /*aux_rpc=*/true); + char lbl[96]; std::snprintf(lbl, sizeof lbl, + "%s v=%llu -> %s (parent lost)", r.label, (unsigned long long)v, name(s)); + check(s == ProdState::LOST, lbl); + } + + // (8) INVARIANT: LTC reach is INDEPENDENT of the version regime. For a fixed + // sink config the outcome is identical for a v35 win and a v36 win. + std::printf("-- invariant: regime _|_ LTC parent reach --\n"); + { + const std::pair sinks[] = { {true,false}, {false,true}, {false,false} }; + const char* sink_names[] = { "P2P-up", "RPC-only", "both-down" }; + for (int i = 0; i < 3; ++i) { + auto [p2p, rpc] = sinks[i]; + ProdState s35 = produce_ltc(35, p2p, rpc); + ProdState s36 = produce_ltc(36, p2p, rpc); + char lbl[96]; std::snprintf(lbl, sizeof lbl, + "%s: LTC v35 and v36 yield SAME state (%s)", sink_names[i], name(s35)); + check(s35 == s36, lbl); + } + } + + // (9) INVARIANT: DOGE aux reach is INDEPENDENT of the version regime, for a + // fixed parent outcome + aux sink config. + std::printf("-- invariant: regime _|_ DOGE aux reach --\n"); + { + struct Cfg { bool parent, p2p, rpc; const char* nm; }; + const Cfg cfgs[] = { + { true, true, false, "parent-ok/relay-up" }, + { true, false, true, "parent-ok/rpc-only" }, + { true, false, false, "parent-ok/aux-down" }, + { false, true, true, "parent-lost" }, + }; + for (const auto& c : cfgs) { + ProdState s35 = produce_doge_aux(35, c.parent, c.p2p, c.rpc); + ProdState s36 = produce_doge_aux(36, c.parent, c.p2p, c.rpc); + char lbl[96]; std::snprintf(lbl, sizeof lbl, + "%s: aux v35 and v36 yield SAME state (%s)", c.nm, name(s35)); + check(s35 == s36, lbl); + } + } + + // (10) The gate DID classify encoding (sanity: scaffolding reads the real + // SSOT, not a hardcoded constant) -- but this is the ONLY thing it may + // change between regimes, and it steers neither reach nor populated-ness. + std::printf("-- gate classifies share encoding (and ONLY that) --\n"); + check(!assemble_populated(35).v36_encoding, "v35 assembles with legacy encoding tag"); + check( assemble_populated(36).v36_encoding, "v36 assembles with V36 encoding tag"); + check(assemble_populated(35).tx_types == assemble_populated(36).tx_types, + "populated tx set is identical across v35/v36 (encoding never steers assembly)"); + + std::printf(g_fails ? "\nFAILED (%d)\n" : "\nALL PASS\n", g_fails); + return g_fails ? 1 : 0; +}