CUDA verified end to end, pool safety fixes, and a node config that does not stall - #4
Merged
Merged
Conversation
Port the OpenCL x16rs kernels to CUDA (x16rs-cuda, --features cuda) so NVIDIA GPUs can mine via nvcc-compiled kernels. Validated on a Colab T4 (sm_75): the GPU produces byte-identical hashes to the CPU reference across all 16 algorithms and at mainnet repeat=16, for both single hashing and the batch mining kernel. Seven bugs fixed to make it compile, launch, and be correct: 1. ocl_compat.cuh `#define __attribute__(x)` stripped CUDA's own __global__ (nvcc expands __global__ through __attribute__) -> kernels became plain __host__. 2. sph_u64 typedef conflict (jh.cl `ulong` vs x16rs.cl `unsigned long long`); x16rs.cl CUDA branch now uses `typedef ulong sph_u64` to match. 3. ALIGN (__attribute__((aligned))) is illegal on function parameters in nvcc; added ALIGN_PARAM (no-op under CUDA, =ALIGN on OpenCL) at the 5 param sites. 4. The rotate shim was hard-coded 64-bit, so 32-bit rotates were UB (PTX 0), silently corrupting cubehash/luffa/simd/hamsi + the AES tables. Replaced with width-correct overloaded __device__ rotate(uint,uint)/(ulong,uint). 5. cudaLaunchKernel FFI used the driver cuLaunchKernel layout -> scrambled ABI -> gridDim.y/z garbage -> cudaErrorInvalidConfiguration on every launch. Fixed with a #[repr(C)] Dim3 passed by value + correct parameter order. 6. cuda_mine_batch aggregated the MAX hash across workgroups; the kernel returns each workgroup's MIN and mining wants the min. Fixed the comparison direction. 7. x16rs_cuda_main per-thread reduction init `best_hash = 0` should be `= index` (matches x16rs_main.cl); with 0 the batch reduction missed the true minimum. All x16rs/opencl/*.cl edits are OpenCL-safe (byte-identical for OpenCL builds; the CUDA-specific changes are gated behind __CUDA__ / ALIGN_PARAM). Validated by x16rs-cuda/tests/genesis_vector.rs: cuda_genesis_block_hash_when_available, cuda_matches_cpu_across_many_inputs (all 16 algos + repeat=16), and cuda_batch_matches_cpu (self-consistent + true argmin, single/multi-workgroup, repeat=16). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ility Wire the validated CUDA backend into the app and miner panel, and make the CUDA kernels compile on Windows nvcc (the release build platform), not just Linux. Windows-nvcc portability (kernels compiled clean on Linux nvcc but failed on Windows nvcc v13.3 / MSVC host): - The ALIGN macros used __attribute__((aligned(N))), which MSVC-hosted nvcc rejects in union/local positions. Alignment here is a perf hint only (the kernels use element-wise access, never vector/uint4 loads), so it is now a no-op under CUDA (ocl_compat.cuh), with the __attribute__ versions kept for OpenCL builds only (util.cl, gated on __CUDA__). Re-validated on the Colab T4: all four correctness tests still pass byte-for-byte, confirming the change is value-neutral. app crate (the cuda feature path had never been compiled and had accumulated bugs): - mining_batch.rs referenced crate::CudaMiningResources / crate::do_group_block_mining_cuda, but those live in crate::poworker (cuda_pow.rs is include!d there) -> fixed the paths. - The MiningRuntimeState import was gated #[cfg(feature = "ocl")] but CudaBlockBackend needs it too -> widened to any(ocl, cuda). Verified with `cargo check -p app --features cuda` (full Windows nvcc + MSVC build). miner-panel CUDA toggle: - PanelSettings.use_cuda + LoadedPanelIni.use_cuda (persisted, round-trips through the ini loader). write_poworker_config now emits use_cuda/cuda_device and turns use_opencl off, GATED on the selected GPU being NVIDIA (a stale flag never enables CUDA for AMD/Intel). - An NVIDIA-only "Use CUDA (NVIDIA)" checkbox (presets::profile_is_nvidia) in the hardware section, with label_use_cuda added to all 9 locales. - Two unit tests: NVIDIA+use_cuda writes the CUDA backend and round-trips; a stale use_cuda on an AMD GPU is ignored. `cargo test -p miner-panel`: 52 passed. Also bundles an earlier panel change (HAC bid/amount fields shown as plain decimals instead of mei:fin, e.g. "1" not "1:0") in hacash_config.rs / help_options.rs / i18n.rs / ui_settings_tab.rs. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Replace the NVIDIA "Use CUDA" checkbox with an explicit "Backend: OpenCL / CUDA"
dropdown so it is clear the default is OpenCL (label_use_cuda -> label_backend across
all 9 locales).
- Remove the RX 9070 XT / RDNA4 "how it works" note from the settings and dashboard
panels, plus the now-dead gpu_rdna4_badge/gpu_rdna4_hint strings, the unused
is_rdna4_experimental helper, and the unused DashboardDetails.gpu_slug field.
- Replace em-dash separators (" — ") with ": " throughout the panel's user-facing text
(274 occurrences across 9 files).
cargo test -p miner-panel: 52 + 2 passed, no warnings.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…g.ini The panel wrote hacash.config.ini with [miner]/[server]/[diamondminer] but no [node] section. A config created via the panel before START-MAINNET.bat had run therefore had no boot nodes and not_find_nodes defaulted off-network, so hacash started an ISOLATED LOCAL chain (height stayed near 0 -> x16rs repeat=1 -> an inflated ~280 MH/s that is not real mining). write_hac_miner_only and write_diamond_miner now ensure the mainnet [node] block (boots + not_find_nodes=false + fast_sync) when it is absent; an existing [node] is kept. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add a community-requested Stratum + free-IP mining pool and wire it into the panel as all-in-one. - New `miner-pool` crate (bin `hac-pool`): an HTTP miner-RPC proxy (poworker-compatible, default :3333) plus a minimal Hacash-oriented Stratum TCP server (:3334) in front of a fullnode's miner API. Anyone can run it on 0.0.0.0 as a public pool; an optional --pool-token gates access. - Panel integration (miner-panel/src/public_pool.rs + ui_settings_tab.rs): a "PUBLIC FREE-IP POOL (ALL-IN-ONE)" settings section to host/start/stop hac-pool, set the upstream/ports/token, and optionally mine through the local pool. Settings persist to public-pool.json. - Docs: COMMUNITY-REQUIREMENTS.md (status matrix for the 6 community asks), PUBLIC-POOL.md (usage), JOJOIN-REBUILD.md (reproducible-rebuild recipe for requirement 6). Deployment modes, all supported: solo (local fullnode via START-MAINNET.bat); connect to a remote node/pool (panel Connect mode = Pool -> their IP:port, no fullnode needed, use the miner-only package); or host a public pool (this feature). v1 limits: the pool is a work proxy (no share accounting / PPS / payouts yet); Stratum is Hacash-native (job carries block_intro + height); poworker uses the HTTP pool port for zero worker-side change. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…l UX Panel: - Updatable pool directory (built-in list + optional pools.json), pool dropdown with per-pool notes/links + Refresh; apply/override by name, no rebuild to add - "Test connection" / "Test upstream" TCP reachability probes (probe_reachable) - Advanced worker settings in the GUI (nonce_max/notice_wait) so no file editing - Honest relabel: "PUBLIC FREE-IP POOL" -> "SHARED NODE / OPEN WORK RELAY" with a work-relay/no-payout disclosure and an honest NAT/CGNAT reachability note - Master Panel tab (fleet worker table) [carried, same files] CUDA (x16rs-cuda): - Add PTX gencode (arch=compute_89,code=compute_89) so newer NVIDIA archs (Hopper sm_90, Blackwell/RTX 50xx sm_120) JIT at runtime instead of failing with cudaErrorNoKernelImageForDevice; validated with nvcc 13.3 - Add cuda-13/12.8/12.6 Linux toolkit discovery paths i18n: - Fix the "GPU mining uses OpenCL only, no CUDA" contradiction across 9 locales Docs: - COMMUNITY-POOL-DESIGN.md: trust-minimized PPLNS batched-settlement pool plan - MINING-NVIDIA-CUDA.md: PTX forward-compat note Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…d by node
Proves the foundation of the no-custody / batched community-pool design: a
process OUTSIDE the fullnode assembles a valid coinbase-only BlockV1 whose
coinbase pays a CHOSEN address, CPU-mines it, and submits it via POST
/submit/block — with ZERO node changes.
Validated end-to-end on a local isolated testnet (chain_id=2): submitted block 1
paying an address the node has no [miner] config for; node accepted it
({ok:true}), the chain advanced to height 1, and the chosen address now holds the
1 HAC block reward. Reuses the node's own pub APIs (create_coinbase_tx, BlockV1,
calculate_mrklroot, x16rs::block_hash, DifficultyTarget, genesis_block_hash) via
workspace path deps; mirrors impl_packing_next_block for the coinbase-only case.
Spike only; targets fresh-testnet bootstrap difficulty (LOWEST_DIFFICULTY) and
does not yet reproduce mainnet ASERT difficulty (next milestone).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ctional amounts Adds settle-spike + a shared lib (mine_and_submit_block with extra txs). Proves the pool's on-chain "payout" half end-to-end on the local testnet: a Type2 transaction from a controlled secp256k1 account pays 3 recipients fractional amounts (0.2/0.3/0.1 HAC) via HacToTrs actions, signed once with fill_sign, submitted to the mempool (ret:0), then confirmed by mining a block that includes it. All 3 recipient balances landed exactly. Both on-chain pool interactions are now validated with ZERO node changes: coinbase-in (chosen coinbase accepted) and settlement-out (batched fractional transfer). Remaining pool work (share protocol, PPLNS accounting) is off-node software that touches no consensus. Reuses sys::Account (secp256k1), protocol TransactionType2 + HacToTrs, Transaction::fill_sign; endpoints /submit/transaction + /submit/block. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ayout split Off-node accounting core (no consensus, no node changes): - share_target_hash: the network target eased by 2^factor (saturating shift) - meets_target: share/block check via x16rs::block_hash vs a target - Pplns: rolling-window share accounting per worker - split_payout: largest-remainder exact split with pool fee + dust floor 7 unit tests. This is the brain that turns validated shares into the per-miner amounts fed to the already-proven batched settlement transfer, tying the two proven on-chain halves together. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…oportional payout Adds the pool engine on top of the proven primitives, all off-node, no node changes: - lib: Template + per-worker extranonce (coinbase miner_nonce) giving each worker a private search space; fetch_template / intro_bytes / assemble_block / submit_block_bytes - pool-server: blocking HTTP pool. GET /work (template + share target + extranonce), GET /share (validate via pool_core, record PPLNS, and on a network-target hit assemble + submit the real block), GET /stats - test-miner: the worker side — pulls work, mines the 32-bit nonce at header bytes 79..83, submits shares - pool-payout: reads live PPLNS counts, splits with pool_core::split_payout, and pays every miner in ONE signed transaction, confirmed on-chain Verified end-to-end on the local testnet: alice 2 shares + bob 3 shares -> 5 blocks mined through the pool (chain 4->9), pool wallet earned 5 HAC, then 4.5 HAC split exactly 2:3 -> alice +1.8 HAC, bob +2.7 HAC in a single signed tx. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…est key load_or_create_wallet(path) reads a 64-hex secp256k1 private key from a file, or generates a fresh random one (system RNG) and persists it on first run. The key never leaves the file — only the address is printed. pool-server now takes a wallet file and derives its coinbase address from it; pool-payout loads the same file to sign settlements. Removes the publicly-derivable demo key ([1u8;32]) from the payout path, which must never hold real funds. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…es on the pool The pool now exposes /query/miner/pending, /query/miner/notice and /submit/miner/success exactly as a fullnode does, with ONE difference: target_hash carries the POOL's share target instead of the network target. An unmodified worker therefore submits shares, and the pool promotes a submission to a real block whenever it also beats the network target. - pending emits block_intro (89B hex), height (JSON number), target_hash (32B hex), coinbase_body (hex, with the extend present so the worker's own set_mining_nonce is not a no-op) and an empty mkrl_modify_list (coinbase-only) - notice is a real long-poll that never holds the state lock while sleeping - submit rebuilds exactly what the worker hashed (its own coinbase_nonce -> coinbase hash -> merkle root -> intro + block nonce) and validates it - shares are attributed by source IP, since the base protocol carries no worker id - accept_share is now shared by both the standard API and our own /share Verified with the SHIPPED poworker v1.0.9 binary, unmodified: it pulled work at the pool's target, mined ~1.8 MH/s, had 3 submissions accepted and correctly got "stale" for late ones; the pool assembled and submitted the real blocks (chain 10->12) and its wallet earned 3 HAC. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ayouts The base miner protocol carries no worker identity, so the pool could only attribute shares by source IP. A worker may now announce where it wants to be paid, and the pool uses that address AS the share-accounting key — which removes the name->address mapping entirely. - poworker: optional `pool_worker = <address>` config key, appended as `&worker=<address>` to the pending and submit URLs. Empty (the default) keeps the URLs byte-identical to solo mining against a plain fullnode, so nothing changes for existing solo users. - pool server: credits the announced address when it is a valid PRIVAKEY address, else falls back to the source IP. - pool-payout: pays the PPLNS keys directly; keys that are not addresses (the IP fallback) are skipped with a note, replacing the hardcoded demo name map. Verified end-to-end: poworker announced 1NVYv5jm..., mined 7 shares / 7 blocks (chain 13->20, pool wallet 3 -> 10 HAC), then payout split 7 HAC with a 10% pool fee and sent 6.3 HAC to that exact address — recipient 2.0 -> 8.3 HAC, with no manual mapping anywhere. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…t templates Reimplements the node's ASERT rule outside the node (mint/src/check/difficulty_asert.rs) so the pool computes the next block's difficulty itself. Every detail is load-bearing and mirrored exactly: i128 truncating division for the exponent, arithmetic-shift split into num_shifts + a 16-bit fraction, the cubic 2^x approximation with its round-half-up term, TWO separate BigUint shifts (fusing them changes the truncation), and the clamp order (zero floor -> 2x ease cap -> LOWEST ceiling). Template now carries BOTH representations, which are not interchangeable: the header's u32 `difficulty` and the exact 32-byte PoW target (more precise than u32_to_hash(num) on the from_big path). The pool mines and validates against the exact target. mine_and_submit_block no longer rolls the timestamp on nonce exhaustion, because under ASERT the difficulty is a function of that timestamp. CONSENSUS-VALIDATED on the local testnet, not merely unit-tested: mined across the testnet ASERT activation height. Node-stored difficulties: h288/h289 = 4294967294 (bootstrap LOWEST), h290 = 3922722815 (= ASERT_START_TARGET_NUM 0xe9cfffff), then h291..h307 tracked the rule down to 3922700247 as blocks arrived faster than the 10s target. Every one of those blocks was assembled off-node and ACCEPTED by the node — a single off-by-one would have stalled the chain at 290. Shares also became properly rarer than blocks (367 vs 287). 5 new unit tests (bootstrap range, activation constant, on-schedule reproduces the anchor, faster/slower direction, 2x ease cap); 12 in the crate. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…, auto-settle
Four protections that matter once other people's hashrate is at stake:
1. DUPLICATE SHARES REJECTED. accept_share now keys every solution by
(height, coinbase_nonce, block_nonce) and refuses replays before crediting
anything. Without this a miner could resubmit one solution N times and steal
N times its fair share of the payout. Verified: replaying an accepted share
twice returns {"kind":"duplicate"} and accepted_shares stays put.
2. ACCOUNTING PERSISTED. Pplns gained snapshot()/restore(); the server writes the
window plus counters to <wallet>.state.json on every accepted share and reloads
it at startup, so a restart never erases credited work. Verified across a real
restart: 1 share / worker "dup-test" survived.
3. REORG-AWARE BLOCK COUNTING. A submitted block is parked in `submitted` and only
counted once the chain still holds OUR hash at that height; a mismatch is
reported as orphaned instead of being paid for. /stats now separates
blocks_confirmed / blocks_pending / blocks_orphaned.
4. AUTOMATIC SETTLEMENT. A timer thread pays every miner their PPLNS share of the
spendable balance (keeping a fee reserve) in ONE signed transaction, using the
pool wallet from the key file. Interval is a CLI arg.
Also: /stats reports the live difficulty, and pool_core gained hash_of/beats so a
solution is hashed once and compared against both targets.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…hain history A go/no-go tool before pointing a pool at any chain: for the last N blocks it recomputes each block's difficulty from that block's own timestamp, its parent's difficulty and the anchor timestamp, then compares against what the chain actually stored. MAINNET RESULT (real synced fullnode at height 766,891): 20 matched, 0 mismatched — h=766872..766891 reproduced EXACTLY. PASS: the off-node ASERT reproduces real chain difficulty exactly. Live mainnet pool run on the back of that: pool-server against the real node served work at height 766892 with difficulty 3585604039 (computed by our own ASERT) and its own share target; an unmodified poworker mined at ~290 KH/s and had 26 shares accepted. No block was found, as expected — a mainnet block needs ~2^42.8 hashes, which is hashrate/luck, not correctness. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…announced In Pool mode the panel now writes the address the user already typed as `pool_worker` in poworker.config.ini, so the pool credits and pays that miner automatically. Solo mode leaves it empty, keeping the worker's requests byte-identical to a plain fullnode's — no behaviour change for solo users. This closes the last manual step in the pool path: pick a pool from the directory, type your address (already required), press Start. Test: pool mode writes the address, solo stays empty. 58 panel tests pass. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The settings page now opens in a Simple view: three numbered steps (what to mine, where to connect, where your coins go) and one Start button. Everything else, GPU tuning, power limits, worker knobs, hosting a shared node, fleet, sits behind an Advanced switch at the top right. The choice is remembered in panel-ui.json, and a first-time user starts simple. New theme components rather than ad-hoc widgets, so it looks like the rest of the app: step_card (gold numbered badge, title, quiet hint, then its controls), segmented (the Simple/Advanced control), btn_primary_large and note. The shared controls were extracted into connect_mode_row, connect_target_block, wallet_field and action_row, and BOTH views call them, so the simple and advanced pages can never drift apart. Step 1 also shows the detected graphics card with a Detect button when none is found, so a beginner can see the GPU will actually be used. 58 panel tests pass. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ency, security
Pool server / lib (pool-spike):
- balance_units: FLOOR sub-0.1-HAC balances to whole 0.1-HAC units instead of
returning 0. Node amounts are normalized ("49:246" = 4.9 HAC), so the old
code discarded the whole wallet after any fee-paying settle and froze all
payouts. (high)
- Block submit no longer holds the pool Mutex across blocking node HTTP + sleeps.
Split into credit_share (locked, no I/O, assembles the block bytes) and
handle_submission (submits OFF the lock). A background thread now keeps the
template current with the chain tip and confirms our blocks off-lock, so one
submission can no longer stall every miner, and work advances when the NETWORK
finds a block, not only when we do. (high)
- fetch_template returns Option instead of panicking on transient node errors;
refresh keeps the old template on failure, so a node blip can no longer poison
the pool Mutex and crash the whole pool. (high)
- Wallet key file is created owner-only (0600 on Unix) with create_new, and a
non-NotFound read error aborts instead of overwriting a possibly-present key.
(high/medium)
- handle(): socket read/write timeouts + a 16 KiB read cap, closing an
unauthenticated slow-loris / unbounded-read DoS. (high)
- Template is replaced only when the height advances, so refreshing no longer
invalidates in-flight shares by bumping the same-height timestamp. (regression
fix found while re-testing)
- Settlement is now idempotent (skips while a prior payout has not drained the
confirmed balance), the timer loop catches panics, and state is written at most
every 16 shares instead of on every share under the lock. (low)
Panel:
- Simple view now shows the HACD bid-password step, so choosing diamonds no
longer dead-ends at Start with a hidden required field. (medium)
- Auto Tune writes an empty pool_worker into the benchmark config (it only
measures local hashrate; no payout address should be announced). (low)
CUDA build.rs:
- Windows toolkit picked by parsed (major, minor) version, not a byte-wise string
sort that ranked v9.2 above v12.4. (low)
- compute_86 / compute_89 gencode flags gated by nvcc version (>= 11.1 / >= 11.8)
with a compute_75 PTX fallback, so an older toolkit builds instead of failing
on an unsupported-arch error. (low)
pool_core gained Pplns snapshot/restore + hash_of/beats; 13 crate tests pass.
Re-tested end-to-end on a fresh testnet: shares credited, duplicates rejected,
blocks submitted off-lock and confirmed by the background thread, chain advanced.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Second adversarial review (concurrency, money, regressions, consensus, input, panel) found 8 confirmed issues, all addressed: Settlement (the money-critical ones): - Idempotency is now keyed on the payout TRANSACTION HASH, polled via /query/transaction, not on an absolute wallet-balance snapshot. This fixes two real bugs at once: (1) a lost or timed-out submit ACK used to leave the marker unset and double-pay every miner next cycle; (2) coinbase income to the same wallet could keep the balance above the marker forever and freeze all payouts. The tx hash is recorded BEFORE submit, so a lost ACK still blocks a resubmit; it clears only once the node reports the tx confirmed or gone. Verified: our tx.hash() matches the node's returned hash exactly, and settlement paid once across many cycles with zero double pay. - The phantom 10 percent fee is removed. split_payout subtracted a fee that was never paid to anyone and just got redistributed to miners the next cycle, so the operator earned nothing anyway. It is now an honest zero-fee community pool; the reserve covers the tiny tx fee. Denial of service / robustness: - Live connections are capped (1024), so unauthenticated long-poll or slow clients cannot spawn unbounded threads. - The anonymous /work worker map is capped at 100k; past that it hands out a deterministic name-derived extranonce instead of growing memory. - The wallet key is written via a temp file plus atomic rename, so a concurrent reader can never see an empty or half-written key. - Panel HACD bid step placeholder fixed from "0:5" (parsed to 0) to "0.5". Also: every block the pool mines now carries the coinbase message "HBIT pool", tagging it as ours (verified on-chain). Known scope limit, documented not fixed: the pool mines coinbase-only blocks, so its own settlement tx confirms when any network miner includes it (it pays a fee). A minority-hashrate community pool is unaffected; a dominant-hashrate pool would want its own blocks to include mempool txs, a later enhancement. pool-spike 13 tests, panel 58 tests, all green. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Resolve the full production audit (2 Critical, 5 High, 19 Medium, 20 Low) plus the earlier community bug list, across the miner core (app), the pool (pool-spike) and the stratum/RPC bridge (miner-pool). Builds on default, OpenCL and CUDA features; the full test suite passes. Critical: * panic=unwind + catch_unwind and mutex-poison recovery, so a request or mining-thread panic becomes an error/skip instead of killing the process * pool wallet key restricted owner-only on Windows (icacls), not just unix 0600 High: * winning-block submit retries transient HTTP 5xx / non-JSON 200 instead of dropping the block (rpc_http surfaces retryable status; poworker retries) * payout chunked to <=190 actions (node TX_ACTIONS_MAX=200), acceptance checked * settlement guard persisted (idempotent across restart); payout has a persisted pending-tx ledger and is dry-run by default * stratum: bounded line framing (no OOM), connection-cap semaphore, idle timeout * pool server: per-IP connection cap and a separate long-poll budget Medium / Low (highlights): * share target derived relative to network difficulty so PPLNS credit tracks hashrate, not batch cadence * pool split over payable addresses only; unpayable IP-fallback shares rejected with a clear "set pool_worker=<address>" message * atomic state writes + corrupt-file preservation; connection-slot Drop guard; x16rs share hashing moved off the global lock; cached pending response * CUDA honors the thermal cap and adds OOM/error work-group backoff; GPU nonce holes and same-height reorg handling fixed (template epoch) * stratum accept-error and malformed-line resilience; correct notify fields (target_hash + coinbase_body); constant-time token compare; upstream URL normalization and hex-only nonce validation (query-injection) * required explicit chain arg (no silent testnet default); settle-spike guarded to testnet only; many correctness/robustness fixes See HBIT-AUDIT-REPORT.html for the full itemized list and per-finding fixes. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* efficiency: default supervene_min = 1 (was 2), so an explicit supervene=1 is not silently bumped to two * diaworker: HACD hashrate divides by the number of parallel workers, not the batch count, so sequential batches no longer overcount the displayed rate * poworker: percent-encode pool_worker in the request query string * diaworker: use a saturating nonce range end to avoid a u64 overflow edge in the CPU diamond loop * diaworker: warn clearly when a config still carries GPU keys, since HACD mining is CPU / full-node only and those keys are ignored Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The device-property fix declared cudaDeviceGetName, which is a CUDA driver-API symbol absent from cudart.lib, so linking poworker with the cuda feature failed (LNK2019 unresolved external). Read the name from cudaGetDeviceProperties (name is at offset 0, safe with an oversized struct) and keep cudaDeviceGetAttribute (a real runtime symbol) for the compute capability and MP count, which were the fields the fragile byte-pad misread. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Fat LTO (lto=true) combined with panic=unwind crashed rustc with an access violation while optimizing the diaworker binary on Windows/MSVC. Thin LTO avoids the crash, builds far faster and with much less memory, and has negligible runtime impact for this workload (the hot path is x16rs and the GPU kernels, not the Rust glue). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Self-contained CUDA validation on a free Colab T4 (or any Linux NVIDIA box): detects the GPU + CUDA toolkit, installs rustup if needed, forces a fast non-LTO profile, and runs the x16rs-cuda tests (genesis byte-vector + CPU/GPU parity + batch) with a heartbeat so a free session does not look dead. Writes a PASS/FAIL summary. Lets us prove the CUDA kernels on a real NVIDIA GPU by cloning the fork and running one script, no local card required. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…y operation A second exhaustive production audit (18 focused finders, then adversarial verification of every finding) produced 43 confirmed findings plus 7 the verifiers could not reach. Fixing them surfaced 6 further real defects. All 103 items are now resolved, or consciously declined with the reason recorded. Money correctness - Pool settlement and the manual payout tool now share one on-disk ledger and one exclusive OS lock on the wallet, so they can no longer both pay out the same PPLNS window. Hashes left in the old private ledger are adopted on upgrade so an in-flight payout cannot be lost. - Coinbase income is held back until it is buried 16 confirmations deep, so an orphaned block is no longer paid out of the operator's own funds. The hold-back is persisted, so a restart cannot forget it. - The settlement guard now fails closed: an inconclusive answer from the node keeps the guard instead of clearing it and paying a second time. - Share replay across a same-height reorg no longer double-credits PPLNS. - A missing [miner] reward key used to fall back to a hardcoded example address, which sent every coinbase to a wallet the operator does not control. Reward addresses and bid amounts are now required and the error names the exact section and key. Security - Wallet key permissions are derived from the process token SID instead of USERNAME, the grant is always issued in the same call as /inheritance:r, the resulting DACL is read back, and every failure is now fatal. - Optional encryption at rest for the pool wallet key, using Argon2id and AES-256-GCM with the same parameters as the existing keystore. - The well known bid password 123456 is refused by both the node and the panel. - The ini parser no longer cuts a value at the first ; or # found anywhere in the line, which silently turned a bid password or an api token into a different, shorter secret. - The release workflow pins every action to a commit SHA and publishes build provenance attestation, and dependabot keeps those pins current. Stability - Every block and diamond mining loop iteration now runs inside a shared panic firewall (app/src/mining_guard.rs), so a single panic can no longer end all submissions and payouts while the miner still looks alive. - Result channels are bounded and never drop a result that meets its target. - Connections are accepted with thread::Builder and backpressure instead of a spawn whose failure took down the whole pool, and every request now has an absolute 5s deadline rather than a per-syscall timeout. - The stratum bridge retries found-block submits, expires stale jobs, and signals workers when the upstream node has gone stale so they stop burning power on frozen work. Node - The mining template is invalidated when its parent is no longer the chain tip, so a reorg can no longer serve work built on an orphaned block. - A diamond bid can no longer exceed the configured dmer_bid_max. - An unparseable chain_id is a hard startup failure instead of silently meaning mainnet. CI now builds and tests pool-spike, the crate that handles real money and that was never built in CI before. Verified: 2504 tests pass across the workspace, and cargo check is clean for the default, ocl and cuda feature sets. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…two regressions found verifying them
Brings in the mainnet diamond consensus work, the block miner correctness pass,
the reorg-aware job handling and the GPU batch hardening, then fixes what
verifying that work turned up.
Mainnet diamond and consensus
- Diamond format back to the mainnet DMD_L=10 / DMD_M=16, diamond mint gated on
height % 5 == 0 on both the validation and the block building side, and the
miner reads the diamond name from check_diamond_hash_result.
- Mainnet config templates under mainnet-configs/ with a full [diamondminer]
section. The diamond keys are read only when enable = true, so the shipped
template starts cleanly with reward and bid_password left commented out.
Block miner
- Every distinct (height, coinbase, nonce) winner is submitted, the target
comparison is equal-inclusive to match the node's own rule, the nonce advances
by what was actually mined so a GPU error no longer leaves holes, and results
carry an epoch so a template switch cannot submit work from the previous job.
- A GPU miner that was explicitly requested and failed to initialise is now
fatal instead of falling back to the CPU, which would burn GPU-rig power at
CPU speed. A CPU-only configuration is unaffected.
- A pending template whose JSON height disagrees with the height in its own
block_intro is rejected: the JSON height picks the x16rs repeat count while
consensus reads the header, so a mismatch mines at the wrong repeat.
Reorg handling
- The miner installs a template whenever the height differs, including a reorg
to a lower tip, records the last intro only after a successful install, and
re-fetches pending after every notice long-poll so a same-height tip rewrite
is picked up.
- Stratum job ids carry a block_intro fingerprint so a same-height reorg
produces a new job and clients get a mining.notify.
GPU
- CUDA re-verifies the GPU's best result and falls back to a bounded CPU
recovery, and refuses a device whose batch kernel cannot launch the 256-thread
block its reduction structurally requires, rather than launching a smaller
block and silently corrupting the reduction.
- OpenCL enforces the 89-byte block intro and disables a card for the session
after 20 consecutive failures at the floor.
- The diamond OpenCL kernel pads SHA3 at the true message end (61 or 93 bytes)
instead of always assuming 93. This is the diamond-only entry point; the block
path's sha3_256_hash is untouched, so block hashing is unchanged.
Two regressions found while verifying the above
- The stratum job id gained a reorg tag, but the submit path still parsed the
height with strip_prefix('h').parse(), which no longer matches. It silently
fell back to the CURRENT height, so a solution found just before the tip moved
was billed to the wrong height and rejected, defeating the node's own
multi-template grace window. Height parsing now lives in job::job_height,
accepts both the tagged and the older bare form, and is covered by a
round-trip test.
- The job id tag was built by byte-slicing block_intro, which comes from
upstream JSON. A non-ASCII value would have landed mid-codepoint and panicked
the poll task. The tag is built char-wise now.
Also: the miner-api test fixture built a template with height N in its JSON and
height 0 in its intro. A real node never does that, and the new consistency
check correctly rejected it, so the fixture is now self-consistent.
Verified: 2529 tests pass across the workspace; cargo check is clean for the
default and ocl feature sets.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
… change set An audit of the previous commit's 46 changes verified 26 of them and found 19 that were present but flawed, plus 44 defects no claim mentioned. Five were critical. None of them were caught by the 2529-test suite, and the reason for that is fixed here too. The miner was throwing away most of its own work - The fullnode re-serializes a pending template on EVERY /query/miner/pending: it bumps the coinbase nonce, recomputes the merkle root, and the merkle root lives inside the serialized block_intro. Two polls for the same job therefore never return the same bytes. - The miner decided "the template changed" by comparing those raw bytes, so it reinstalled on every poll and bumped the mining epoch every time. A pre-send gate then returned before the result was built, silently destroying a finished batch and any winning nonce in it. Re-fetching pending after every notice long-poll turned that from once per block into once per notice window: about 7% of all mined work at the default settings, about 98% at notice_wait=3, and everything against an upstream that answers the notice immediately. - A new job is now decided from (height, parent hash), which the node's per-request refresh leaves untouched, and the epoch only moves when that pair really moves. A finished batch is always sent; a job switch stops new work instead of voiding work already done. A winning result is discarded only when the live template sits at the same height with a different parent, which is the one case where the node has genuinely evicted it. Everything else is submitted and the node decides, because it keeps several recent heights. - The 200 ms anti-spin floor on the notice cycle is back. Without it, an upstream answering instantly (which is exactly what a saturated pool bridge does) drew about 80 requests per second from each miner and made the overload worse. Why the tests did not see it: the miner-api simulator returned one constant block_intro, unlike the real node. It now mirrors the node exactly, and its coinbase carries the extend block, without which set_mining_nonce was a silent no-op and half the search space was invisible to every sim test. A new poworker_template_churn_sim test polls a churning template and proves the miner still submits. A GPU could be lost for the whole session - The session disable was a write-once latch with no path back: 20 consecutive failures, which a failing card reaches in 30 to 60 seconds, well inside a TDR storm, a driver update or a brief thermal excursion. On gfx1201 the floor is one step below the cap, so a single error already satisfied the "at floor" precondition. CUDA had no such precondition at all. An operator asleep lost the card until someone restarted the process. - It is now a time-based quarantine with exponential backoff and re-probing, applied the same way to OpenCL and CUDA, and it is announced rather than silent. Work groups can also ramp back after a transient error instead of staying at the floor for the rest of the session. - GPU init failure retries for about 110 seconds before it is fatal, so a driver that is still loading after a boot, a resume or a device reset no longer permanently stops an unattended rig. The shipped config mined to a stranger - The mainnet template shipped [miner] enable = true together with a valid third-party reward address. Anyone who copied it as the instructions say paid every block reward to a wallet they do not control, irreversibly and silently. This is exactly the failure the strict loader was built to prevent, arriving through the config instead of through the code. The address is gone, mining is off by default, and every key that must be the operator's own is commented out so the loader fails loudly and names it. - The release copy of poworker.config.ini carried the developer's absolute paths and username. Removed. - .gitignore's blanket *.ini was swallowing the very templates the docs tell people to copy, so mainnet-configs and hac-pool.example.ini existed only on one machine. They are versioned now, and the packaging script ships them. The stratum reorg tag discriminated nothing - The job id folded in the LAST 16 hex characters of block_intro, which for the 89-byte layout are the low bytes of the template nonce and are identical across a reorg. Two different templates at one height produced the same job id, so the mining.notify that the tag exists for never fired and clients kept hashing an orphaned parent for up to a full block interval. The tag is now a fingerprint of the parent hash, which is the field that actually differs, and the height still round-trips out of the id. Also: the HACD workers no longer keep hashing a stale (number, prev_hash) after a reorg, a queue.finish() driver error no longer discards an already-verified diamond, the diamond kernel's ranking now matches the CPU acceptance rule that is the authority, and a CUDA rig gets CPU-assist threads (they were only spawned on the OpenCL arm, so a CUDA card hitting the disable path left the rig with no work at all). Verified: 2537 tests pass; cargo check is clean for default, ocl and cuda (cuda built under vcvars64, since nvcc needs cl.exe and recompiles the shared .cl kernels). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Found by running the miner against a real node on real hardware, not by reading the code: 90 seconds on an AMD gfx1201 produced 3 accepted blocks and 1729 rejected submits, and the GPU then stopped hashing altogether. A height holds exactly one block, and mint/src/api/miner_success.rs drops the template the moment it accepts one, so every later winner for that template is answered "pending block height N not found". At low difficulty a fast card finds hundreds of those per template, and each one cost a blocking HTTP round trip. The pipeline jammed in order: the submit queue filled, the overflow path submitted inline on the drain thread, the drain stopped, the bounded result channel filled, and the workers blocked because a result meeting its target must never be dropped. The rig spent its last twenty seconds draining dead height-3 solutions while already holding height-4 work. The previous audit's rule was right and is kept: never discard a winner locally, let the node decide. What was missing is that a second winner for a template the node has already settled is not a winner the node might accept - it is provably dead. So submissions are now gated per template, keyed by the (height, prevhash) pair the result already carries. The first winner for a template is always submitted; that is the money guarantee and it does not change. While one is in flight, later winners for the same template are dropped without an HTTP call. Once the node gives a verdict that settles the template - accepted, or an error proving the template is gone - every later winner for it is dropped too. A pure transport failure re-opens the template, so a network hiccup can never silence a height, and a panic inside a submit re-opens it as well. The map is pruned to a window of recent heights so it cannot grow for the life of the process. Redundant winners no longer reach the queue at all, which makes the inline blocking fallback unreachable in the case that used to trigger it. Operator output was itself part of the problem at 1729 lines, so there is now one line per template, printed once the miner has moved on and the count is final: "[Mining] height 3 settled (accepted), suppressed 634 redundant winners." Measured on the same rig after the change, same config, same 90 seconds: submits 1732 -> 4 rejected 1729 -> 0 blocks 3 -> 4 hashrate 77.59 MH/s -> 98-190 MH/s sustained, 212.93 peak miner log 12997 lines -> 1394 The hashrate nearly doubled because the card is no longer waiting behind HTTP. Also verified live end to end: the same miner through the hac-pool relay mined 3 blocks with 3 submits and 0 failures, and pool-server refuses to start a payout while it holds the wallet's settle lock, which is the double-pay guard from the earlier audit doing its job. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The per-template submit gate from 059e082 learned only the fullnode's reply shapes. Running a real GPU into pool-spike's pool-server showed it misreads a pool in two ways, one active and one dormant but far worse. A pool answers with a "kind" field and no "err" at all, so the gate's fullnode error-text rules never matched. 180 seconds of one card produced 526 submits, of which 519 were {"kind":"stale","ret":1}: every stale reply was read as a fault in that one submission, which re-opened the template and let the next winner try again, and the next. 98.7 percent of the traffic was spent re-offering work to a template the pool had already retired. The dormant one is the money defect. A credited SHARE also answers ret 0, which the gate read as "block accepted" and used to settle the template, dropping every later share locally. On a pool a share IS payable work, so that is systematic underpayment of the miner. It stayed invisible on the test chain because at bootstrap difficulty the share target and the block target coincide, so the pool never returned kind "share" once. On mainnet, where share_bits makes a share 2^24 easier than a block, "share" is nearly every reply. A pre-fix simulation submitted 1 of 200 shares and suppressed 199. So the classifier now understands both dialects. ret 0 with kind "block", and the fullnode's "mining":"success", settle the template. ret 0 with kind "share" is a new verdict that never settles, is never counted as a redundant winner, and latches the template open so every later share is submitted; the latch is inherited by new templates, because an upstream that pays per share keeps doing so. ret 1 with kind "stale" settles, which is what kills the 519. "busy" means the pool is rate limiting, so that template pauses for two seconds rather than being hammered or killed. "duplicate" does not settle. "invalid" re-opens, as before. A body with no numeric ret stays a transport non-verdict and re-opens, so a network hiccup can still never silence a height. Fullnode behaviour is unchanged, asserted by a test. Measured on the same rig after the change, one GPU into pool-server across the ASERT transition into real difficulty: submits 333: 281 share, 15 block, 37 stale, 0 duplicate, 0 busy (before: 526 submits, 519 stale, 7 block, 0 share) The full money cycle then ran for real, which is the first time it has been observed rather than reasoned about: shares credited to two workers by PPLNS, blocks found, coinbase held back with "[settle] holding back 160 unit(s) of block income that is not yet buried 16 deep", three of the pool's own blocks correctly detected as orphaned and not paid, and finally "[settle] submitted payout tx 71d3d97ff367c20d paying 2 miner(s) 35 units". Not fixed here, and it needs its own investigation: that payout transaction never reached the node. The node reports "[TxPool] tx count: 0(0), 1(0)" and every block mined afterwards still carries txs 0, yet the pool logged "settlement done" and moved on. A pool that believes it paid when nobody was paid is worse than one that knows it failed. Filed as the next thing to chase. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
… comment The previous commit justified counting leading zero bits by claiming the published bootstrap target is fffffeffff..., and that the earlier fffffd guess was the bug. That is backwards. mint/src/api/common.rs right_00_to_ff decrements the last non-zero byte and fills the tail with ff, so u32_to_hash(0xFFFFFFFE), which is FF FF FE 00.., is published as fffffdffff... The original prefix was right. The code change stands on its own merits and is unchanged: counting leading zero bits tests the same condition the pool tests, min(24, N) >= 18, instead of a proxy for it, and it prints the derived factor at each sample. Only the reason written beside it was wrong, and a wrong fact in a comment about difficulty encoding is the kind that gets believed later. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Phase 1 repointed the worker config at the node, then restored a snapshot taken as the file was found. If the cell died anywhere in the warm-up, that left the config on the node address, and the next run would snapshot THAT and faithfully restore it. Phase 2 would then mine solo while the pool served nobody: the stats would read zero shares for both workers, Cell 6 would call the share list broken, and the actual cause would be a leftover from the previous attempt. The config is now normalised to the pool address before the snapshot is taken, so the restore is the same whatever state the file was left in, and an assertion checks the restore took effect before the pool run begins. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…ight The run reached phase 2 correctly, the warm-up lifted the target to 22 zero bits and the pool started, and then the rival guard fired again. It was not crying wolf this time: the rival really did come up as a CUDA worker. The cause was the isolation I added for it. sys/src/config.rs resolves the default config against the EXECUTABLE's directory, not the working directory, and current_exe() reads /proc/self/exe, which follows symlinks. So the symlinked poworker inside cpurival/ resolved back to the release directory and loaded the GPU config: one card, two CUDA miners, both paid to the same address, and no rival to measure against. The comment that justified those symlinks was also wrong on its own terms. It claimed poworker ignores a config path on the command line. It does not: resolve_config_path_from takes args[1] when exactly one argument is present. That was always the correct mechanism, so the rival now passes an absolute config path and runs the real binary, with its directory kept only to separate the stats file. Added the check that should have been there first. poworker prints the canonical path of the config it loaded, so the guard now asserts on that line instead of inferring from behaviour. This is worth the belt and braces: a config that cannot be read is not fatal, load_config_path prints an error and returns an empty map, and the rival would then run on defaults and look entirely plausible. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…ssage The previous commit wrote the guard's failure message through a shell heredoc and the escape did not survive: the string literal ended up split across two lines, so Cell 4 would not parse at all. Rewritten without the escape, and every python cell in this document now parses under ast.parse. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The run reached phase 2, the warm-up lifted the target to 22 zero bits, and then the pool died on EADDRINUSE. A pool from the PREVIOUS attempt was still holding 18082, because two separate defects combined. First, the cleanup never matched it. The kernel stores comm in TASK_COMM_LEN bytes, 16 including the NUL, so a 16-character binary name is only ever visible as 15. "hbit-pool-server" is exactly 16, so `pkill hbit-pool-server` matched nothing, every time. "fullnode" and "poworker" are 8 characters, which is why those were cleaned up correctly and only the pool ever leaked. Now killed by exact comm, both spellings. Second, nothing tore down on failure. The rival guard raised SystemExit before the teardown line, so the previous run left the node, the pool and two miners running. The whole cell is now wrapped in try/finally, so every process dies on any exit path, and the pkill is repeated at the end. Also added the checks that would have named this immediately: both ports are probed before anything starts, with a message saying what to run to find the survivor, and the node must actually answer on 18080 rather than the loop simply falling through after 60 failures. -x rather than -f is deliberate. -f matches the full command line, which for this cleanup includes the shell running it, so the first pkill would kill the shell and the remaining ones would never run. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…ad ratio check_share_target had a hole wide enough to lose money through, and it is the condition the function was written to refuse. The guard bounded achieved_share_factor, a RATIO: how much easier a share is than a block. It said nothing about what the share itself costs. On a chain whose network target has 22 leading zero bits, share_bits=24 saturates the derivation to the all-0xff ceiling. The achieved factor then reads 22, sails past MIN_SHARE_FACTOR of 18, the pool starts, and every hash in existence beats the share target. Credit measures HTTP round trips, and the fastest submitter takes the 4096-share window from miners doing more work. That is verbatim the failure the original guard describes in its own doc comment. It caught only the LOWEST_DIFFICULTY case, where the ratio collapses to 0 as well. Anything between MIN_SHARE_FACTOR and share_bits passed. That is not a corner: it is exactly the ASERT activation target, 0xe9cfffff, which every non-mainnet chain lands on at height difficulty_adjust_blocks + 2 and, at one block per second against a 10800 second half-life, stays at for hours. Both bounds are now applied. pool_core::share_cost_bits reports the leading zero bits of the target actually served, which is what one share costs as a power of two, and MIN_SHARE_COST_BITS requires 2^16. Below that even a single CPU thread produces more than one share per second and the ordering stops being about work. Real mainnet difficulty clears it at any legal share_bits, so the bound only bites on a chain too easy to account on. The two failures also need opposite advice, so they are reported separately. Since leading_zero_bits(network) == achieved + cost, lowering share_bits moves work from the ratio into the cost: where the chain has room the message names the highest share_bits that would work, and where it does not it says so and stops, instead of sending the operator to retune a value that cannot help. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…nswer this
The local-chain premise was false and every run built on it was void. Off mainnet
the ASERT anchor sits at difficulty_adjust_blocks + 2 and its target is the fixed
constant 0xe9cfffff, which u32_to_hash gives exactly 22 leading zero bits. The
chain does not climb to that value, it is pinned there, and afterwards ASERT's
half-life is 10800 seconds of wall clock while blocks cannot arrive faster than
one per second, so the difficulty gains one bit per twenty minutes. With the
lowest legal share_bits of 18, the most a share could ever cost on that chain is
sixteen hashes. Cell 3's claim that shrinking the adjustment window reaches real
difficulty in minutes was wrong, and the pool now refuses the regime outright.
So the node syncs the real chain, and share_bits is computed from the difficulty
actually in force rather than typed, leaving each share about 2^20 hashes: tens
per second for the card, well under one for a CPU thread, which is the spread
being measured.
The audit that found this also found the harness would have reported PASS without
measuring anything, so:
- rate_hps could not match the miner's own kilohash unit. rates_to_show emits an
uppercase K with no space ("938.14KH/s") and the pattern accepted only
lowercase, so it returned no match at all. A single CPU thread lives in that
band, so the control's hashrate read as zero, the expected share collapsed to
100%, and the proportionality check silently became a copy of the window check.
- Two checks were written as "<input missing> or <real test>", which reports PASS
precisely when the input could not be read. Every check now requires its own
measurement, and a new one requires the control to have run at all: a rival that
died at minute two and a perfect result were previously the same numbers.
- Nothing deleted the logs or final_stats.json, and nothing checked their
freshness, so any abort left Cells 6 and 7 reprinting the previous run's verdict
byte for byte. They are removed up front, stamped with a run id, and asserted.
- The template divisor counted drain ticks that lagged the tip rather than
templates, with no dedupe, so it inflated exactly when the share list works
best. Counts distinct heights now.
- Cell 1 hid build failures behind a pipe to tail, whose exit status is always 0,
and never updated an existing checkout, so the run could measure a binary older
than the fix. Fixed with pipefail and an explicit fetch/reset.
- The pool log was printed as the last 800 bytes, a window that starts after the
startup summary and therefore hides the line stating the share factor really
served. Printed in full, and a capped factor now aborts.
- Corrected three pieces of guidance that contradicted the source: SUBMIT FAILED
is the catch-all arm so it tracks stale on any pooled run and cannot be zero;
an EMPTY template gate section is the healthy pooled result rather than a
missing one; and [miner] enable starts no node-side hasher, so the closing
advice to let "the node's own CPU miner" confirm a payout could never work.
- UNDERSAMPLING is throttled to one line per 30s and carries the session total
beside the per-batch figure. The report took the wrong number and told the
operator to raise share_bits, which makes shares easier and overflows sooner.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…lem worse share_bits is how many powers of two EASIER a share is than a block, so raising it makes shares easier, makes more nonces per batch payable, and overflows the share list sooner. The warning printed when that list overflows said "ask the operator to raise share_bits". An operator following it would watch their lost income grow and have no way to tell why. The remedy is the opposite: lower share_bits so each share is harder. Says so now, and says which direction does what, because the value's name does not make that obvious. Also states that the line is rate limited to one per thirty seconds. Without that, the number of warnings looks like the number of overflowing batches, when it is capped at about twenty for a ten minute run, and the per-batch count looks like the loss when the session total is sitting in the same sentence. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2.7 GB, taken from a completed sync of the same chain rather than estimated, so the operator knows before starting the long cell that Colab's disk is not the constraint. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…r exists Cell 3 said the worker config's connect line is rewritten later. It was, by the local-chain warm-up, which is gone: the difficulty now comes from the real chain, so nothing touches the file after it is written. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Run 20260727T154114 on a Colab T4 against the real chain at a block cost of 2^42: 2,493 submissions to the CPU control's 6, taking 99.76% of the PPLNS window on 99.697% of the hashrate. The window snapshot and the whole-sample submission split agree exactly, and all five checks passed with no stale, no failures and no share list overflow. The CUDA share list pays the card for what it mines. Two notes beside the numbers so they are not misread. 6.92 MH/s is not a regression against the 86 MH/s this card showed before. block_hash_repeat is height / 50000 + 1 capped at 16, so mainnet heights hash at repeat 16 while a chain at genesis uses repeat 1. That factor of sixteen is the whole difference, and this is the mainnet-representative figure. The control produced 6 shares, so Poisson noise on it is about plus or minus 2.4 and the measurable window share spans 99.64% to 99.84%. The expected value falls inside, so the agreement holds, but its precision is set by that count. Also recorded: only one height change was seen, so template rolling and the stale path were barely exercised, and the share list never overflowed, so the undersampling path was reached only by the Cell 2 unit test that targets it directly. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The compose file asked for share_bits 24 and the operator guide called it "the right answer unless you have measured otherwise". It has now been measured: a block on the live chain costs 2^42, so 24 leaves each share at 2^18. Two things follow, and both argue for 20. Margin. The pool now refuses to start when a share would cost less than 2^16, so 24 sat two bits above a hard refusal. A fourfold drop in network difficulty would have taken this pool offline at its next restart, which is a poor property for a service that holds other people's balances. Payout memory, which matters more. PPLNS pays on the last 4096 shares, so easier shares make that window span LESS time. At 2^18 an ordinary card produces about 26 shares a second and a ten-miner pool turns the entire window over in roughly fifteen seconds: a miner that drops off for half a minute loses everything it was owed. At 2^22 the same pool keeps about four minutes of history. That is the difference between a payout scheme and a lottery on connection stability. The operator guide also still described the old single check and repeated its advice that lowering share_bits cannot help. That is now only true for one of the two failure modes. Rewritten to explain both, why a healthy ratio does not imply a share that costs anything, and that the two want opposite remedies. The error table gains the second message and stops recommending 24. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Building this image on a 32 core host killed the Docker engine mid-compile. The whole application went down with it and the only trace left was "rpc error: code = Unavailable ... EOF", which says nothing about the cause. Cargo defaults to one job per core and each rustc on this tree can hold well over a gigabyte, so 32 of them exhausted the engine VM. That is worth fixing rather than working around, because the machines this image is FOR are far smaller than the one that failed. On a 2 to 8 GB VPS or mini PC an unbounded build is not merely slow, it is certain to be killed, and the operator sees a signal number rather than a memory message. CARGO_BUILD_JOBS is now 4 by default, which keeps the peak near 6 to 8 GB, and is exposed as a build arg so a builder with the memory can raise it at roughly 2 GB per job. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The existing patterns match a directory named target, so they missed .codex-target-security-20260719-reaudit completely: 3.5 GB of compiler output from an earlier audit, uploaded to the daemon on every single build. The measured context was 3.94 GB and took 198 seconds to transfer before any work started, which is 55 times what this image actually needs. Widened to anything with "target" in the name, wherever it sits, plus .codex-*. Verified safe: no tracked file in this repository has "target" anywhere in its path, and the only directories the pattern matches are the three build outputs. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…ssing driver Found by running the packaged Linux release rather than by reading it. ./list_opencl and ./diagnose_opencl both exited 101, a Rust panic, with nothing on stdout: thread 'main' panicked at ocl-0.19.7/src/standard/platform.rs:49:38: Platform::list: Error retrieving platform list: ApiWrapper(GetPlatformIdsPlatformListUnavailable(10)) Platform::list() panics when the ICD loader is installed but no vendor driver is registered, which is every fresh Linux install, every container and every WSL session. That is precisely the machine whose owner was told by README-LINUX to run ./list_opencl to check whether their OpenCL driver is present. The one tool whose job is to diagnose a missing driver was the one that died on it, and it died showing a path inside a crates.io dependency. platform_list() now enumerates through ocl::core::get_platform_ids(), which returns a Result, and treats "none" as an answer rather than a failure. The scan adds a warning naming what to install and where the .icd file belongs, and says that HACD is CPU-only so diamond mining is unaffected. The same call sat in the miner's GPU init, where it was worse: a panic there took the miner down instead of letting it fall back to the CPU. It now reports the absence and returns no devices, which the caller already handles. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Seventy commits since v0.4.1, and the release carries a behaviour change rather than only fixes: a pool that started before may now refuse, because the share target guard checks what a share costs and not merely how it compares to a block. That is a minor bump, not a patch. Cargo.lock moves with it so the workflow's --locked builds still resolve. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The v0.5.0 run failed on Windows and on Linux, for unrelated reasons, and published nothing. Two earlier release runs had failed the same Windows way. Windows: the HACD CPU-only check looked for dumpbin under "Microsoft Visual Studio\2022\...". Visual Studio no longer installs there; on this machine it is "Microsoft Visual Studio\18\BuildTools". Worse, GitHub runs pwsh with ErrorActionPreference = Stop, so Get-ChildItem threw on the missing directory before the script's own "dumpbin.exe not found" message could say anything useful. It now asks vswhere where Visual Studio actually is and falls back to globbing every year and edition under both Program Files roots, with the lookup errors suppressed so the intended message is the one that prints. It still refuses to continue if dumpbin cannot be found: HACD must be PROVEN CPU-only before release, not assumed. Linux: a doctest in app/src/opencl_gpu/resources.rs. A four-space indent inside a /// comment is a Rust code block to rustdoc, so it tried to compile the prose "lambda = batch seconds * shares per second" and stopped the build. Fenced as text. Worth recording why the local suite missed it: `cargo test --workspace` was run without --features ocl, and the module is gated behind that feature, so the doctest was never collected. CI runs `-p app --features ocl`. Matching the CI flags, not approximating them, is the check that would have caught this. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The Windows key-file hardening verified its own DACL and rejected ANY principal other than the current account. On a machine where the OS keeps its own ACE it therefore failed closed, and for a pool failing closed on the wallet means it does not start and nobody gets paid. The release build caught it: write and secure the key file: "...wallet.key.tmp is still accessible to `NT AUTHORITY\SYSTEM`" Excluding LocalSystem or the local Administrators group buys nothing. Anything running as either can take ownership of the file, read this process's memory, or load a driver, so a key they cannot reach through the DACL is one they can reach another way moments later. What the DACL actually protects against is other ordinary accounts on a shared machine, and that is untouched by leaving these two in. The trade being made was availability for no security at all. Both are now accepted, and the pool prints who else can read the key rather than passing over it silently. Matched by SID, not by name. icacls prints LOCALISED account names, so comparing against "NT AUTHORITY\SYSTEM" or "BUILTIN\Administrators" would quietly stop working on a German or Greek Windows and start rejecting the exact principals this is meant to allow. Honest limit: the new branch cannot be exercised on a machine whose ACL is already clean, which is the case here. It is the CI runner that reproduces the condition. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Groundwork for gating the mining start. The panel treats "the RPC answered" as readiness, and on a node still downloading the chain that is not readiness: it is a promise the miner cannot keep, and it fails quietly while looking like success. Seen on a real machine today. The node sat at height 70,000 of a chain past 800,000, the miner started anyway, and it reported 215 MH/s. That is HIGHER than a synced node produces, because block_hash_repeat is height / 50000 + 1 capped at 16, so down at 70,000 each hash costs an eighth of what it costs at the tip. The operator saw a large number, concluded it was mining, and every solution the card found was for a block the network had settled years earlier. Progress is measured in chain TIME rather than in blocks, because the height of the real tip is precisely what an unsynced node does not know. The timestamp of the block it does have, against the clock, says how far back it is standing and needs nobody else to tell it. The arithmetic is separated from the fetching so it can be tested without a node. The cases covered are the ones that would otherwise divide by zero or show a bar running backwards at a user: a tip ahead of the clock, a genesis at or after now, and a body that cannot be parsed, which must degrade to "unknown, keep waiting" rather than to zero, since zero would read as genesis and look plausible. Both endpoint bodies in the tests are captured verbatim from the running node. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Groundwork for showing the operator what is actually happening. Every child the panel starts is currently spawned with CREATE_NO_WINDOW, so a full node downloading the chain does its work entirely out of sight. That is how a node stuck part way through a sync came to look, from the panel, exactly like one that was ready to mine on. The trade is stated in the code rather than discovered later: a process with its own console is not piping its output back, so the panel cannot quote its log in an error message. Status is unaffected, because it comes from polling the RPC and from the child's exit code, neither of which needs the pipe. The detail moves to a window the user can see. Windows only, and the comment says why: on Linux and macOS a GUI-launched child has no terminal to attach to, and conjuring one means choosing a terminal emulator that may not be installed. The piped log stays the answer there. Not yet wired to the spawn sites; that lands with the sync progress bar, so the two arrive together rather than leaving a window open above a panel that still claims to be ready. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The panel started mining the moment the node's RPC answered. On a node still downloading the chain that produced the worst kind of failure: the miner hashed against blocks the network settled years ago, every solution was rejected, and because block_hash_repeat is height / 50000 + 1 capped at 16, the reported hashrate was HIGHER than normal. It read as success. A solo start now waits. The 45 second deadline still governs getting the node UP, but stops applying once it answers, because catching up legitimately takes an hour and abandoning a node that is working perfectly well would be its own bug. Progress goes in the FOOTER, visible from every tab, because the failure being replaced is a user concluding the thing is broken. A stalled sync and a ready panel used to look identical; now the bar moves, names the height, and says how many blocks are left. It also asks for a repaint, since egui otherwise redraws only on input and a frozen bar would imply the very hang this disproves. A probe that cannot answer counts as "keep waiting", never as "caught up". A node mid-batch is briefly unresponsive, and treating that silence as readiness is the bug being fixed. The probe runs on its own thread; three blocking HTTP requests must never sit on the UI thread. Strings in all nine languages. The script asserted the block count and refused to write when it found nine rather than the seven assumed, which is why Thai and Russian are not missing here. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…walk A node behind the tip had only one recovery path once it was past height zero: ask a peer for `unstable_block` block hashes. That request exists to find the common ancestor of a SHALLOW fork. Used on a real backlog it advances four blocks per announcement, which on mainnet is four blocks every five minutes, so a node three thousand blocks short of the tip needed days and looked completely idle the whole time. Both places that could reach it now start a batch sync instead when the gap is deeper than `unstable_block`: the status handler, and the arriving-block path, where the peer that just sent a future block demonstrably has the newer chain. The message there was misleading too. "ignore future block ... during history sync" was printed once per arriving block while nothing advanced, and read as a node refusing work when it was really asking for four hashes and getting nowhere. It now reports the gap and the catch-up. HONEST LIMIT, because the run that prompted this found something else. On the stalled node the actual halt is a single `[Block Sync Warning] insert 446201 failed: diamond status HTAKES not found`, after which the sync stops and never retries. That is a STATE error, not a network one, and this commit does not address it: chain/src/sync.rs aborts the batch on an insert error and nothing above it tries again. The prime suspect is `fast_sync = true` skipping state that a later block needs, which would also explain the head rolling back from 765,065 to 446,200 across a restart. Not yet proven, and it is the next thing to test. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
… extended Proven on hardware today, not reasoned about. A node synced with fast_sync = true stopped dead and stayed stopped: [Block Sync Warning] insert 446201 failed: diamond status HTAKES not found One line, then silence. chain/src/sync.rs aborts the batch on an insert error and nothing above it retries, so the node sits at that height indefinitely while answering its RPC and looking healthy. Turning the flag off afterwards does NOT repair it: the state was never written. The head had also rolled back from 765,065 to 446,200 across a restart, which is the same damage showing from the other side. A clean sync of the same chain with fast_sync = false reached the real tip, 768,129, in about seven minutes with zero errors, and then held there. That is the whole basis for this change. It was shipped in every path a user can take: the config the panel writes for them, the deployment node config, the mainnet example, and the Colab document. So anyone who followed our instructions built a chain that would stall, and on a stalled node solo mining hashes against blocks the network settled years ago while reporting a HIGHER than normal hashrate. That is how this was found. What is NOT fixed here: why fast_sync skips that state, and why a failed insert never retries. Either would be a better fix than avoiding the flag. Both are open. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The last piece the operator asked for, and it lands now rather than earlier for a reason: a console opening above a panel that still claimed "ready" on an unsynced node would have been worse than no window at all. With the sync gate in place the window and the panel finally say the same thing. Both HAC and HACD get it, and so does the node. start_mining_after_opencl branches on connect mode, not on mining kind, so every path goes through the same spawn and the same gate. Windows only, and the trade is the one already documented on configure_visible_command: a child with its own console pipes nothing back, so the channel returned here stays empty and the panel quotes no log. Status is unaffected, coming from the RPC and the exit code. On Linux and macOS a GUI-launched child has no terminal to attach to, so the piped path stays, and its imports are now cfg-gated so neither target builds dead code or warns. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Gating Stdio to non-Windows fixed the release build and broke the test build. Caught only because the verification command piped cargo into grep, which returns 0 when it finds the word "error", so a failing test compile read as success and the commit went through anyway. Same masking that hid a Docker failure and a release failure earlier; the exit code now comes from PIPESTATUS. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Fixes to v0.5.0, all found by running the product rather than reading it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Packaged APART from the miner in both jobs, and that separation is the point. The pool is server software that holds a wallet and pays other people; putting it inside an archive aimed at gaming PCs would hand a money-handling binary to everyone who only wanted to mine. It ships with no wallet, no config and no address. The operator supplies all three and the pool refuses to start without them, so nothing in this archive can pay a stranger by default. The Windows ZIP validation now counts `hacash-miner-*.zip` rather than every zip in dist. It asserted exactly two, so a third package would have failed the build for a reason that had nothing to do with the miner. Written with literal file edits rather than a shell script, after the previous attempt sent `\target\release` through a bash heredoc, then Python, then YAML, and arrived as a TAB and a CR. Paths in the new PowerShell use Join-Path and forward slashes so there is nothing left to escape. Verified: the workflow parses, both jobs carry the step, and the file contains no tab or carriage return at all. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The first pool archive shipped hbit-pool-server and hbit-pool-payout and nothing else. That is not a deployable package: the pool takes its templates from a full node and submits blocks to one, so an operator who downloaded it got software that cannot start. A packaging bug, not something to paper over in a README. The archive now carries the node binary, a mainnet config template, the systemd units, the readiness script, the deployment notes, and a first-time setup script. That script checks and then stops. It does NOT invent a reward address, does NOT write a passphrase, and does NOT start anything: it refuses to continue while the reward field is empty, while fast_sync is true, or while no wallet passphrase is configured, and it says why each one matters in the terms of what it costs. Every one of those could have been "helpfully" defaulted, and every default would have been worth less than what it risks, since this is software that holds other people's money. It also refuses on under ten gigabytes free, because the measured mainnet chain is 2.7 GB and growing, and a sync that dies on a full disk fails late and messily. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
No code change since v0.5.1. This exists so the pool package is rebuilt with the full node inside it, which the previous archive was missing. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
107 commits.
maincurrently shipsfast_sync = true, which builds a chain thatcannot be extended, so anyone cloning the default branch today gets the defect
that started this work.
The ones that matter
fast_sync = truecorrupts the chain. A node synced with it stops dead at ablock whose state it never wrote (
diamond status HTAKES not found) and nothingretries, so it sits there answering its RPC and looking healthy. A clean sync with
it off reached the tip in seven minutes. Removed from every config we ship.
The pool accepted shares that cost nothing. The guard bounded a ratio, not the
work behind it, so a chain at 22 leading zero bits with
share_bits24 saturatedto the all-ones ceiling: credit measured HTTP round trips. Both bounds now apply.
The panel mined on unsynced nodes. "The RPC answered" was treated as ready. It
now waits for the tip and shows progress, because a stalled sync and a ready panel
were indistinguishable.
CUDA verified on real hardware. On a Colab T4 against mainnet at a block cost
of 2^42: 99.76% of the PPLNS window on 99.697% of the hashrate, against a CPU
control in the same window.
Also:
list_openclno longer panics when no driver is present, which is exactlywhen people are told to run it; the deploy image builds on the 2 to 8 GB machines
it targets; the release workflow itself is repaired; and the HBIT pool ships as a
deployable package.
Everything here was found by running the product, not by reading it.