Reward detection: refuse dust coinbase outputs so a block builder cannot force an epoch rotation #38
Labels
No labels
bug
duplicate
enhancement
help wanted
invalid
question
wontfix
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
vnprc/hashpool#38
Loading…
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Make the mint's reward detection refuse rewards that the pool did not actually earn, so that nobody can force an epoch rotation by putting a dust output to the mint's receive script in a coinbase they built, which would close the current epoch against a worthless reward and strand the value of every miner's shares in it.
Primary goals:
[hashpool_mint]setting with a safe default for a pool that mines whole blocks, so a proxy deployment that legitimately receives a fraction can lower it deliberately rather than the code guessing.Current state and specifics
On the reward-trigger branch (vnprc/hashpool#37, implementing vnprc/hashpool#18) the watcher in
roles/mint/src/lib/epoch/mod.rsreads only the coinbase of each new block (coinbase_rewardfetchestx[0]), sums every output whose script equals the configuredreceive_addressscript, and treats any positive sum as a reward:process_blockopens a provisional epoch at that height. An ordinary transaction to the receive address therefore cannot trigger a rotation. A coinbase output can, and any block builder controls their own coinbase outputs: a competing pool, a solo miner, or a miner on this pool who also builds blocks elsewhere can add a 1-sat output to the mint's receive script. The mint then closes the current epoch, openshash_<pool>_<height>withreward_sats = 1, and every quote created afterwards stamps into the new unit. Under the settlement model, the closed epoch's ehash is backed by a 1-sat reward, so the work behind it is worth nothing, while whoever holds shares in the next epoch captures the pool's next real reward undiluted. The attacker's cost is the dust plus nothing else. Dissolve does not help, since the dust block is canonical.The economics of the floor: with the floor at the block subsidy, forcing a rotation means paying the mint at least a full subsidy, which is a real reward that the closed epoch's miners are paid from. The attack becomes a donation. Regtest subsidy is
50 BTC >> (height / 150); mainnet is the usual 210,000-block schedule; the mint already knows the height and the network from the node. Alargest outputrule on top closes the case where a builder splits the coinbase to put the floor amount on the mint and keep fees elsewhere, which is harmless economically but keeps the rule simple: the mint's output is the payout, or it is not a reward.What the floor does not do: it cannot tell the pool's own block from another pool's block that happens to pay the mint a full reward, and it does not apply to a proxy pool paid a fraction by an upstream. Two exploratory directions, each its own follow-up once this issue lands:
pool_signatureinto the coinbase scriptSig, but a plain string is forgeable. A tag the mint can verify, for example a signature by the pool identity key (thepool_pubkeythat namespaces epoch units) over the height, or an HMAC under a pool-mint shared secret, in an OP_RETURN output or the scriptSig, would make a forged reward impossible. This overlaps with the deferred "keyset id in the coinbase" item indocs/EPOCH_DESIGN.mdand with the template-validation gate of vnprc/hashpool#20, which already plans to gate payment on "boundary final AND template valid"; one commitment in the coinbase can serve both.Scope
In scope here: the amount floor and largest-output rule in
coinbase_reward/process_block, with a[hashpool_mint]setting (default: the subsidy at that height; a fraction knob for proxy deployments), a unit test for a dust output, a split coinbase and a full reward, and a regtest witness: mine a block whose coinbase pays the mint 1 sat (generateblockwith a custom coinbase, or a second regtest wallet building the block) and confirm no epoch opens and the log says why, thengeneratetoaddress 1 <mint>and confirm the normal rotation. Exploratory, tracked as follow-ups once filed: the authenticated coinbase tag and the upstream attestation, which both need a design decision on key material before implementation.Part of vnprc/hashpool#13. Depends on vnprc/hashpool#37 landing. Related: vnprc/hashpool#20.
Scoping decision from the owner, 2026-10-09: the minimum viable pool runs without job declaration, so miners mine only on templates the pool builds. That is tracked as vnprc/hashpool#39 (the pool refuses
SetCustomMiningJob, and the JD server and client leave the stack).What that changes here: the "is this reward block ours" question becomes a lookup against the pool's own template log, since the pool built every coinbase its miners could have found, so the exploratory authenticated-coinbase-tag direction above is deferred, and the proxy-pool attestation is out of the MVP (no upstream exists). What it does not change: the dust attack from block builders who are not on this pool at all, which is what the amount floor and largest-output rule in the implementation-ready part of this issue close. The floor stays the deliverable here; the pool-side template match is a follow-up once vnprc/hashpool#39 lands.