Skip to content

channels_sv2: fix coinbase scriptSig size/serialization defects that yield consensus-invalid coinbases - #2243

Merged
plebhash merged 4 commits into
stratum-mining:mainfrom
plebhash:2026-08-01-scriptsig-serialization-defects
Aug 10, 2026
Merged

channels_sv2: fix coinbase scriptSig size/serialization defects that yield consensus-invalid coinbases#2243
plebhash merged 4 commits into
stratum-mining:mainfrom
plebhash:2026-08-01-scriptsig-serialization-defects

Conversation

@plebhash

@plebhash plebhash commented Aug 2, 2026

Copy link
Copy Markdown
Member

@plebhash plebhash changed the title channels_sv2: fix coinbase scriptSig size/serialization defects yield consensus-invalid coinbases channels_sv2: fix coinbase scriptSig size/serialization defects that yield consensus-invalid coinbases Aug 2, 2026
@plebhash
plebhash force-pushed the 2026-08-01-scriptsig-serialization-defects branch from cec5c34 to a26f3a5 Compare August 2, 2026 03:07

@bit-aloo bit-aloo left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One suggestion, and rest looks ok

Comment thread sv2/channels-sv2/src/server/standard.rs Outdated
@plebhash
plebhash force-pushed the 2026-08-01-scriptsig-serialization-defects branch 3 times, most recently from fa12779 to 18029b1 Compare August 5, 2026 20:22

@bit-aloo bit-aloo left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ACK

@GitGab19

GitGab19 commented Aug 7, 2026

Copy link
Copy Markdown
Member

Clanker review:

Verdict: request changes on [PR #2243](#2243).

Finding

@plebhash
plebhash force-pushed the 2026-08-01-scriptsig-serialization-defects branch from 18029b1 to fa1cd82 Compare August 7, 2026 16:46
@plebhash

plebhash commented Aug 7, 2026

Copy link
Copy Markdown
Member Author

Clanker review:

Verdict: request changes on [PR #2243](#2243).

Finding

addressed, clanker answer:

Good catch on the custom job path — confirmed, and fixed in fa1cd824.

I reproduced all three cases before touching anything:

JDC new_custom_job (52-byte tag, 9-byte prefix): Ok -> 101-byte scriptSig
pool new_extended_job_from_custom_job:           Ok
hostile 250-byte prefix from downstream:         Ok -> 282-byte scriptSig

The delegation argument doesn't hold for the receiving path: new_extended_job_from_custom_job takes coinbase_prefix from a downstream JDC over the wire, so the "upstream caller" is a remote peer and there is nothing in-tree left to delegate to. That one is worth fixing regardless of scope, so I did — the 250-byte case above is a hostile downstream, not a misconfigured local one.

The two paths can't share one validator. Factoring the exact-size check out of coinbase() and calling it from both doesn't work, because the arithmetic differs:

  • new_custom_job (JDC builds the prefix): the factory adds the pool/miner tag itself, so script_sig_size(template.coinbase_prefix.len(), full_extranonce_size) applies.
  • new_extended_job_from_custom_job (pool receives it): the tag is already embedded in the incoming coinbase_prefix, so it's coinbase_prefix.len() + full_extranonce_size — exactly your second snippet. Reusing script_sig_size here would double-count the tag.

Both are bounded now. The receiving check sits in custom_coinbase so the prefix and suffix builders are covered by a single choke point, mirroring how coinbase() owns the template path. Bounding the custom path to 100 bytes also closes the CompactSize concern you raised, since the scriptSig can no longer reach 253.

Covered by test_new_custom_job_rejects_oversized_script_sig and test_custom_job_rejects_oversized_script_sig: exact-100 → Ok, 101 → Err, plus the 250-byte hostile case.

plebhash and others added 4 commits August 7, 2026 21:39
`StandardChannel::validate_share` rebuilds the coinbase from scratch when a
share meets the network target. It wrote the `OP_PUSHBYTES` opcode from
`self.extranonce_prefix.len()` (the channel's *current* prefix) but then pushed
`job.get_extranonce_prefix()` (the prefix the job was created with).

`set_extranonce_prefix` may rotate the channel to a prefix of a different length
at any point after a job is created. When that happens the opcode disagrees with
the number of bytes that follow, so the reconstructed coinbase deserializes into
a different `scriptSig` and its txid diverges from the `merkle_root` the job (and
the winning share's header) is committed to. The block is then rejected by
Bitcoin nodes and the reward is silently lost.

The job's prefix is the only correct source here: the job's `merkle_root` is
committed to it, and jobs created before a rotation intentionally keep serving
the old prefix for share validation.

Refs stratum-mining#2242
The three server channel constructors enforced the 100-byte `scriptSig` cap
using a hardcoded `5 // BIP34` allowance for the template's `coinbase_prefix`,
and nothing re-checked the budget once the actual `NewTemplate` arrived. A
config sitting at the edge of the budget therefore assembled an oversized,
consensus-invalid `scriptSig` as soon as a template carried a longer prefix.
For a group channel that means the pool distributes unmineable work to every
channel in the group.

The template's `coinbase_prefix` is only known inside `JobFactory::coinbase`,
which is where the `scriptSig` is actually assembled, so that is where the
authoritative check belongs. Placing it there covers every `NewTemplate`-derived
path at once (`new_standard_job`, `new_extended_job` and
`new_coinbase_tx_prefix_and_suffix`), hence all three channels'
`on_new_template`. The `SetCustomMiningJob` path is left alone: length
validation there is delegated to upstream callers.

The constructors keep a conservative pre-flight check, so an unusable config
still fails at startup rather than at every job creation, but the allowance is
raised from 5 to `MAX_COINBASE_PREFIX_SIZE` (8) — the cap the Template
Distribution Protocol spec puts on `NewTemplate::coinbase_prefix`. The spec's
"not including the length byte" refers to the `B0255` wire length prefix, not
the BIP34 script push opcode, which is already part of those 8 bytes. This costs
3 bytes of tag/extranonce budget relative to the old estimate, in exchange for
no in-spec template ever overflowing a channel that was accepted at
construction.

Both checks now share `JobFactory::script_sig_size`, which also removes the tag
length arithmetic that was duplicated across `coinbase_tx_prefix` and
`coinbase_tx_suffix`.

Finally, `op_pushbytes_pool_miner_tag` capped the tag blob at 61 bytes, a number
derived from yet another budget model (it assumed a 5-byte BIP34 prefix and a
32-byte extranonce), so it disagreed with the constructors in both directions.
Now that `MAX_SCRIPT_SIG_SIZE` is enforced on the real assembled size, that match
only needs to enforce what it is actually about: `OP_PUSHBYTES_N` is a valid
single-byte push opcode only for N in 1..=75.

Refs stratum-mining#2242
The channel constructors enforce that the tags, the extranonce and a worst-case
coinbase prefix fit within the 100-byte `scriptSig` cap, but the setters that can
change the extranonce configuration afterwards dropped that invariant:

- `ExtendedChannel::set_extranonce_prefix` and
  `StandardChannel::set_extranonce_prefix` only re-checked `MAX_EXTRANONCE_LEN`,
  so swapping in a prefix that is individually valid but longer than the one the
  channel was built with silently pushed the total past the cap;
- `GroupChannel::set_full_extranonce_size` checked nothing at all.

Every job created after such a call carries a consensus-invalid coinbase, so a
block found on the channel is rejected and the reward is lost.

All three now re-run the same invariant through `JobFactory::script_sig_size`,
before touching any state, so a rejected update leaves the channel exactly as it
was. `set_full_extranonce_size` therefore becomes fallible; its only caller is
in-crate.

Refs stratum-mining#2242
The scriptSig budget check added in the previous commit lives in
`JobFactory::coinbase`, which covers every `NewTemplate`-derived path but none
of the `SetCustomMiningJob` ones: `new_custom_job` and `custom_coinbase` build
their own scriptSig and never reach it.

Issue stratum-mining#2242 scoped that out, on the grounds that length validation for custom
jobs is delegated to upstream callers. That holds for `new_custom_job`, which a
Job Declarator Client drives from its own config, but not for
`new_extended_job_from_custom_job`: there the `coinbase_prefix` arrives from a
downstream Job Declarator Client over the wire, so the "upstream caller" is a
remote peer and there is nothing in-tree left to delegate to. A 250-byte prefix
from a hostile or buggy downstream had the pool assemble a 282-byte scriptSig,
which is consensus-invalid and, being past 252 bytes, also breaks the one-byte
CompactSize assumption the prefix/suffix split indexes rely on, silently
mis-slicing the coinbase.

The two paths need different arithmetic and do not share a validator. When this
factory builds the job it also adds the pool/miner tag, so `script_sig_size`
applies. When it receives one, the tag is already embedded in the incoming
`coinbase_prefix`, so the assembled scriptSig is just that prefix plus the
extranonce; reusing `script_sig_size` there would double-count the tag.

The receiving check goes in `custom_coinbase` rather than in
`new_extended_job_from_custom_job`, so that the prefix and suffix builders are
both covered by a single choke point, mirroring how `coinbase` owns the
template path.

The Job Declarator Client side is bounded too, so that an oversized local
config fails where it is configured instead of being rejected by the pool after
the fact.

Refs stratum-mining#2242

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@plebhash
plebhash force-pushed the 2026-08-01-scriptsig-serialization-defects branch from fa1cd82 to d62b88c Compare August 8, 2026 00:40
@plebhash
plebhash merged commit 9b0454e into stratum-mining:main Aug 10, 2026
16 checks passed
@plebhash
plebhash deleted the 2026-08-01-scriptsig-serialization-defects branch August 10, 2026 12:47
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.

channels_sv2: need to fix coinbase tx scriptSig size/serialization defects that yield consensus-invalid coinbase txs

3 participants