channels_sv2: fix coinbase scriptSig size/serialization defects that yield consensus-invalid coinbases - #2243
Conversation
channels_sv2: fix coinbase scriptSig size/serialization defects yield consensus-invalid coinbaseschannels_sv2: fix coinbase scriptSig size/serialization defects that yield consensus-invalid coinbases
cec5c34 to
a26f3a5
Compare
bit-aloo
left a comment
There was a problem hiding this comment.
One suggestion, and rest looks ok
fa12779 to
18029b1
Compare
|
Clanker review:
|
18029b1 to
fa1cd82
Compare
addressed, clanker answer:
|
`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>
fa1cd82 to
d62b88c
Compare
close #2242
close https://github.com/project-loupe/audit-stratum/issues/20
close https://github.com/project-loupe/audit-stratum/issues/30
close https://github.com/project-loupe/audit-stratum/issues/79
close https://github.com/project-loupe/audit-stratum/issues/88
companion stratum-mining/sv2-apps#658