Repository navigation
coinbase: per-job salt push so jobs from an unchanged template are distinct work - #98
Open
Wired4ncer wants to merge 2 commits into
Open
Wired4ncer wants to merge 2 commits into
Wired4ncer wants to merge 2 commits into
Conversation
…stinct work The tip watcher rebuilds the job every 30 s even when the template has not changed. The new job then had a byte-identical coinbase and merkle root, so every header a rig could build was the same as in the previous job (only ntime moved). Rigs that restart their search on each job re-find hashes the pool has already credited, and the pool rejects them as duplicate shares in large numbers. Every coinbase builder (coinbase_build, coinbase_build_split, coinbase_build_window, coinbase_build_from_template, coinbase_build_window_from_template) now takes a per-job salt. When it is non-zero, ONE 5-byte push (0x04 + 4 bytes little-endian) is appended after the tag in the scriptSig and counted against the 100-byte scriptSig cap. Salt 0 emits nothing, so callers passing 0 are byte-identical to before. The stratum job derives its salt from its job id (FNV-1a, forced non-zero). The window builders model scriptSig growth to compute the byte budget left for payouts; both probes now count the 5 bytes through a shared constant (COINBASE_JOB_SALT_PUSH_BYTES), so a coinbase at the edge of coinbase_max_bytes is not 5 bytes over budget. Tests: layout and position of the push in all five builders, salt 0 equals the classic layout, the 100-byte cap and the window byte budget (exact edge) both count the push, jobs from one template differ in coinbase and merkle root. The cross-job-id duplicate test pins both jobs to one salt so it keeps exercising the server-wide hash ring, and gains a third job with its own salt that is accepted.
… correct the slot-estimate comment
This branch has not been deployed
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.
Closes #92
What and why
The tip watcher in
src/main.crebuilds the job every 30 s(
now_ms() - s->last_built_ms > 30000), even when the template has notchanged. The rebuilt job had a byte-identical coinbase and merkle root, so
every header a rig could build was the same as in the previous job (only
ntimemoved). Rigs that restart their search on each job re-find hashes thepool has already credited, and the pool answers with a storm of "duplicate
share" rejections.
Fix, as described in #92:
ONE 5-byte push after the tag in the scriptSig, counted against the 100-byte
scriptSig cap.
bits, forced non-zero).
Byte layout
scriptSig, in order:
On the template path the salt push follows the tag, which follows the server's
existing scriptSig. The tag push is untouched, so anything reading the tag text
keeps working. The push sits in
cb1(the extranonce placeholder follows it),cb2is unchanged, and the scriptSig length varint grows by 5.Builders covered
coinbase_build,coinbase_build_split,coinbase_build_window,coinbase_build_from_template,coinbase_build_window_from_template(eachgets
uint32_t job_saltright aftercoinbase_tag). The shared templateimplementation and a single
cb_tag_push()helper emit the bytes, so there isone place that defines the layout. Every builder call in
stratum.c(8) passesthe job's salt, including the after-block recount that stages the payout
rotation.
coinbase_template_reward()'s internal probe passes 0.Budget accounting
coinbase_max_bytes(window builders): both builders estimate the fixedcost of the coinbase before paying anyone. That estimate now includes the 5
bytes (
COINBASE_JOB_SALT_PUSH_BYTES, shared by both), so a coinbase at theedge of the budget is not 5 bytes over it.
coinbase_expected_payout_slots()is unchanged. Its fixed 160-byteenvelope is already optimistic on the classic (non-template) segwit path —
it does not count the reserved operator output or the witness commitment —
so its slot estimate can be one too high with or without this change. It
only shifts the rotation order, never a payment; out of scope here, happy to
open a separate issue.
Operator-visible effect: because the push counts against the budgets, a
coinbase sitting exactly at either limit can now lose its last payout or be
refused where it was not before. There is a CHANGELOG note.
Tests
tests/test_coinbase.c(dispatching over all five builders):cb1equals the unsalted one with the length byte +5 and04 s0 s1 s2 s3appended after the tag;cb2is identical; another saltchanges only those 4 bytes; same with no tag;
serialization rules, not from the builder);
exactly 5 less than without; at the edge it builds, one byte more is refused;
max_coinbase_bytesthat pays all 12miners is exactly 5 higher with a salt; at that budget everyone is paid and
the coinbase fits, one byte less cuts somebody (both window builders);
with no other transactions).
tests/test_stratum.c:coinb1(samecoinb2) and different merkle roots; a job forced to salt 0 sends acoinb15 bytes shorter;
exercises the server-wide hash ring, and adds J3 with its own salt, which is
accepted as a new share.
a salted coinbase cuts a payee and an unsalted one does not, the found block
stages the rotation (a recount without the salt would record "everyone
paid").
Existing builder tests pass 0 and are otherwise unchanged. Full
make testpasses (before and after: all green).
Each new test was checked against a mutant of the code it guards (wrong salt
byte, wrong length byte, cap check ignoring the push in the single and template
builders, each window probe ignoring the push, template implementation dropping
the salt, stratum salt constant/zero, one render path dropping the salt); each
fails its test.
🤖 Generated with Claude Code