Bring main up to the shipping code: node sync, pool money fixes, and the rebuilt panel - #5
Merged
Merged
Conversation
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.
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.
Brings
mainup to the code that is actually shipping.mainwas six commitsbehind; 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:
so the Logs screen has real lines on Windows for the first time
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)
efficiency.rsalready read one and threw it awayEmergency Stop and LAN Cluster were removed rather than built:
stop_miningalready kills immediately, and
ConnectModehas 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_groupsis the current work-group count, not a loss, so> 0painted the hardware ring red on every healthy rig that ever ran
-0.71 EURon a rig earning 3.28 HAC/day, unable to distinguish a loss from an unset price
amd-smiandrocm-smiare a Linux stack.atiadlxx.dllships with every Adrenalin driver,loads at runtime with no new dependency, and answers in about 1 ms
putting a red
Errorin the log of every solo startThermal safety
This work touched the thermal guard, so it was checked harder than the rest.
Removing the
max_temp_c == 0early return put every shipped miner onto a paththat 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 --workspaceexits 0. Nine languages intact, no em dashes.