Skip to content

Bring main up to the shipping code: node sync, pool money fixes, and the rebuilt panel - #5

Merged
Moskyera merged 6 commits into
mainfrom
feat/pool-directory-cuda-ptx-panel
Jul 29, 2026
Merged

Bring main up to the shipping code: node sync, pool money fixes, and the rebuilt panel#5
Moskyera merged 6 commits into
mainfrom
feat/pool-directory-cuda-ptx-panel

Conversation

@Moskyera

Copy link
Copy Markdown
Owner

Brings main up to the code that is actually shipping. main was six commits
behind; every release since v0.5.0 has been cut from this branch, so anyone
landing on the repository saw stale code.

Released as v0.5.5,
built and verified on a real mainnet rig before this PR was opened.

What is in it

The node no longer gives up on a sync. A failed batch used to end the sync
for good, which is why a node could sit thousands of blocks short of the tip and
only a restart would move it.

Eleven money defects in the pool, found by running it rather than reading it:
chunked idempotent settlement, a persisted settle guard, retries on a winning
block submit, and PPLNS credit changed from headcount to residence, which closes
a share withholding attack.

The panel rebuilt to the commissioned mockups. The layout was the smaller
half of that work. Drawing a control forces the question of what stands behind
it, and eight controls in those mockups had nothing, so the missing sources were
built instead of the controls being drawn dead:

  • the workers write a rolling log beside their config and the panel tails it,
    so the Logs screen has real lines on Windows for the first time
  • a bounded history file, so the chart survives a restart, with sampling moved
    into the poller (it ran only while the Dashboard was on screen, so an hour in
    Setup left an hour-long hole in a chart claiming to cover the day)
  • GPU temperature is published; efficiency.rs already read one and threw it away

Emergency Stop and LAN Cluster were removed rather than built: stop_mining
already kills immediately, and ConnectMode has two variants.

What the live run found that no test did

The build was installed on a mining RX 9070 XT at height 768,573 before this was
opened, and that run found four defects:

  • oom_work_groups is the current work-group count, not a loss, so > 0
    painted the hardware ring red on every healthy rig that ever ran
  • with no HAC price configured, the Net/day card printed a confident -0.71 EUR
    on a rig earning 3.28 HAC/day, unable to distinguish a loss from an unset price
  • the temperature feature was inert on Windows AMD, because amd-smi and
    rocm-smi are a Linux stack. atiadlxx.dll ships with every Adrenalin driver,
    loads at runtime with no new dependency, and answers in about 1 ms
  • the panel spawned the worker inside the node's own 30 second refusal window,
    putting a red Error in the log of every solo start

Thermal safety

This work touched the thermal guard, so it was checked harder than the rest.
Removing the max_temp_c == 0 early return put every shipped miner onto a path
that used to be free. Detection moved off the startup path (105.7 microseconds
for the caller, measured, instead of seconds on a multi-GPU rig), reporting-only
polls at 30s behind an idle floor that keeps a guarded rig's duty cycle identical
to before, and the freshness window now follows what a pass actually costs.

The guarantee that mining cannot start uncooled had no test at all. A
fire-and-forget mutation let a rig with an 80C limit and no sensor mine, and all
174 tests stayed green. Four mutations now fail, including a monitor thread that
starts and exits, which is the same defect moved one step later in time.

cargo test --locked --workspace exits 0. Nine languages intact, no em dashes.

Moskyera and others added 6 commits July 28, 2026 00:30
One line was the difference between a node that recovers and a node that is dead
without saying so:

    if let Err(e) = res { println!("{}", e); return; }

A failed batch printed and returned, and nothing ever asked for another. The node
then sat at that height indefinitely, answering its RPC and looking healthy, while
the chain moved on. That is how a single

    [Block Sync Warning] insert N failed: diamond status HTAKES not found

became hours of silent nothing. It cost an operator an evening of mining against
blocks the network had settled years earlier.

It now resumes, from where the chain ACTUALLY is rather than from this batch's
end, because a partial batch may have inserted some of its blocks and a wrong
start height is refused by do_synchronize and would fail again at once. The ten
second pause is not cosmetic: without it a permanently bad block becomes a tight
loop that floods the peer and the log. With it a transient failure recovers on its
own, and a permanent one keeps saying so out loud, which beats silence.

While here, a correction to what the earlier commit assumed. fast_sync does not
skip writing state. It reaches execution through ChainInfo and relaxes two CHECKS:
protocol/src/context/context.rs:185 trusts a Type3 transaction's declared signers
instead of verifying signatures, and protocol/src/action/macro.rs:125 runs
precheck_runtime_action_fast_sync, which validates tx type and AST depth but skips
the exec_from gating the full path applies. So the likely mechanism is that
fast_sync ADMITS a block full validation would have rejected, leaving a chain a
later block cannot build on. Narrowed, not proven, and stated that way.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
I wrote, in three shipped config comments and a release tag, that fast_sync = true
builds a chain that cannot be extended. That is not true, and a controlled test
run today says so plainly: a clean sync from zero with fast_sync = true reached
the tip at 768,147, logged "all blocks sync finished", and produced zero warnings
of any kind.

What actually repaired the damaged chain yesterday was the clean RESYNC. I had one
sample with the flag off, no sample with it on, and concluded causation anyway.
That is the exact error this project spent the day catching elsewhere, and leaving
it in a config comment would have taught it to whoever read the file next.

The setting stays false, for the reason that survives testing rather than the one
that did not. fast_sync reaches execution through ChainInfo and relaxes checks:
protocol/src/context/context.rs trusts a Type3 transaction's declared signers
instead of verifying signatures, and protocol/src/action/macro.rs runs a lighter
action precheck. For a node serving a pool that pays other people, verifying every
signature is worth more than a faster first sync.

So the real cause of yesterday's stall remains unknown. What IS fixed and verified
is the node giving up on it: a failed batch used to end the sync permanently, and
now retries.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…n reading it

An adversarial audit raised 35 findings; 11 survived an independent refutation
pass and merged into eight. A separate reviewer then found three of those eight
fixes incomplete, two of which had quietly reintroduced the defect they claimed to
close. A full settlement cycle on a purpose-built rig then found three more. All
of it is fixed here, and the whole money path has now run end to end.

THE THREE THAT STOPPED AN OPERATOR

A payout the node had broadcast and then forgotten, which a routine restart
causes, was treated as never paid and re-signed, so both could confirm and the
operator funded the second. node_holds was already recorded and read by no
decision anywhere; it now decides, and a payout the node still holds is
re-broadcast byte-identically instead of re-signed. A submit that timed out is no
longer read as a refusal.

Shares carried no time, so a miner could withhold a block's worth and dump them at
the settlement tick, evicting every honest miner from the window and taking the
whole payout. The pool published the schedule itself in /terms. Credit is now
residence, not headcount: a burst has earned nothing at the moment it lands, and
the shares it evicts keep what they had already earned, so submitting late is
strictly worse than submitting promptly.

A node that failed to answer was valued as a wallet holding zero, because
transport failure, a bad body and the node's own error object all collapsed to the
same empty string and balance_units("") returned a confident Some(0). Every miner
polling /earnings was told it was owed nothing, every thirty seconds, with no log
line at all. Answering is now distinguishable from answering zero.

THE REST

A rejected settlement chunk was forgotten while the log promised the next cycle
would re-issue it; there is now an owed ledger, paid first, persisted, and the
message says what actually happens. Only the coinbase subsidy was held for
maturity, so a block's transaction fees were distributable at zero confirmations
and an orphan could take back money already paid. The share-cost bound was checked
at startup and never again. The settlement thread called a function that can mint
a wallet or process::exit mid-flight. flush_state reported success for a snapshot
it never wrote, and the fsync it rested on was discarded.

AND THREE LIES THE POOL TOLD ITS OPERATOR

A stall alarm counted settlement cycles, which have no relation to block time, so
on mainnet defaults it fired on cycles 3 to 6 of every payout forever, and blamed
a cause it never checked: that the pool's blocks carry only a coinbase. Measured
false on the rig, where the pool's own block carried four transactions and a later
one carried the payout itself. It now counts blocks, and states what it measured.

A restart re-stamped the template mid-height, so every connected worker kept
hashing a dead header: 6,320 consecutive rejects, and 1,281 copies of a message
asserting a permanent fault in the worker's software. The stamp now survives a
restart, and the diagnostic reports what was observed instead of diagnosing a
fault the pool cannot see.

One stdout line per accepted share, 7.6 MB in four minutes, sharing a println with
the block-found notice, so the one line that must never be missed was buried in
the one that does not matter.

WHAT WAS PROVEN, ON HARDWARE

each_block_target_time sets a private chain's resting difficulty, because ASERT is
an equilibrium controller. At 455 seconds this box reached 34 bits in 44 minutes,
which is the first configuration the pool will serve AND a GPU can win blocks on.
No consensus code was touched, so mainnet is byte-identical by construction.

On it: a block carrying four fee-paying transactions had its fees held back, not
released; two workers at different hashrates were paid 29 and 5 units, matching
largest-remainder on their residence credit to the unit; and two hard kills across
five process lifetimes produced exactly three payout transactions totalling
exactly what the two miners received. Nothing paid twice, nothing lost.

NOT PROVEN, AND SAID SO RATHER THAN LEFT IMPLIED

The owed ledger was never non-empty on the rig, because the node could not be made
to definitively reject a payout, so that path rests on unit tests alone. No mainnet
block fixture exists; the fee fixtures are real bytes from a real node, but a
testnet one. And fee_got equalled fee throughout, so the gas-refund case is
untested against a node.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The panel now matches the three commissioned screens, but the work that
mattered was not the layout. Drawing a control forces the question of what
stands behind it, and eight controls in those mockups had nothing.

Built the missing sources rather than drawing dead controls:

- The workers write a rolling log beside their config (app/src/worker_log.rs,
  4 MiB, one rotation) and the panel tails it. Giving the node and workers
  their own console window had cost the panel its pipe; now it is both, so
  the Logs screen and Recent Worker Events have real lines on Windows for
  the first time.
- A bounded history file (miner-history.csv, 30s grid, 2880 records) so the
  chart survives a restart, and sampling moved into the poller: it ran only
  inside ui_dashboard() before, so an hour spent in Setup left an hour-long
  hole in a chart claiming to cover the day.
- GPU temperature is published. efficiency.rs already read one and threw it
  away.

Dropped Emergency Stop (stop_mining already kills immediately, so a second
button would have been one lie) and LAN Cluster (ConnectMode has two
variants; the case is served by the pool directory).

Then ran it against mainnet, and the run found four things no test had:

- oom_work_groups is the current work-group COUNT, not a loss, so
  `> 0` painted the ring red on every healthy rig that ever ran. Renamed to
  oom_allowed_work_groups and the test is now the shortfall.
- With hac_price unset the Net/day card printed a confident -0.71 EUR on a
  rig earning 3.28 HAC/day. It could not tell "you are losing money" from
  "nobody told me the price". Mining logic untouched: should_pause_for_profit
  already refused to pause at hac_price <= 0.
- The temperature feature was inert on Windows AMD: amd-smi and rocm-smi are
  a Linux stack. atiadlxx.dll ships with every Adrenalin driver, loads at
  runtime with no new dependency, and answers in about 1 ms. Measured on a
  mining RX 9070 XT: edge 60, memory 68, hotspot 84.
- The panel spawned the worker inside the node's own 30 second refusal
  window, putting a red Error in the log of every solo start. It now waits
  on an observable condition, with a 45s ceiling so it cannot hang.

Thermal safety, checked hard because this change touched it. Removing the
max_temp_c == 0 early return put every shipped miner on a path that used to
be free, so: detection moved off the startup path (measured 105.7us for the
caller instead of seconds), reporting-only polls at 30s with an idle floor
that keeps a guarded rig's duty cycle identical to before, and the freshness
window now follows what a pass actually costs.

The guarantee that mining cannot start uncooled had no test at all. A
fire-and-forget mutation let a rig with an 80C limit and no sensor mine, and
all 174 tests stayed green. Four mutations now fail, including a monitor
thread that starts and exits, which is the same defect moved one step later
in time.

cargo test --locked --workspace exits 0. Nine languages, no em dashes.
@Moskyera
Moskyera merged commit 4fc388b into main Jul 29, 2026
6 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant