Skip to content

Add Extension 0x0003: Dynamic Coinbase Outputs - #195

Closed
warioishere wants to merge 1 commit into
stratum-mining:mainfrom
warioishere:add-extension-0x0003-coinbase-output-weights
Closed

Add Extension 0x0003: Dynamic Coinbase Outputs#195
warioishere wants to merge 1 commit into
stratum-mining:mainfrom
warioishere:add-extension-0x0003-coinbase-output-weights

Conversation

@warioishere

@warioishere warioishere commented May 10, 2026

Copy link
Copy Markdown

Summary

Adds a draft extension 0x0003 — Dynamic Coinbase Outputs that lets a Job Declaration Client (JDC) request, per declared job, the exact list of pool-defined coinbase outputs from the Job Declaration Server (JDS). Three new messages: RequestCoinbaseOutputs (JDC → JDS) and RequestCoinbaseOutputs.Success / .Error responses. The JDC sends its current template's prev_hash and the amount of revenue it is contributing to the pool (pool_revenue); the JDS replies with the consensus-serialized output list computed at request time from current pool state.

This continues the conversation from:

Why this replaces an earlier static-weights draft

An earlier revision of this PR proposed a static SEQ0_64K[U32] weight vector appended as a TLV to AllocateMiningJobToken.Success. Per @TheBlueMatt's review (https://github.com/stratum-mining/sv2-spec/pull/195#issuecomment-…) the static-weights design has a structural problem that defeats the use case it was meant to serve:

  • Per-miner amounts depend on revenue, not just on share weight. A 1% miner is paid 5 000 sat on a 500 000-sat block but 25 sat on a 2 500-sat block — and the latter is dust under every standard output type, so the pool's actual policy is to suppress the output and accumulate the amount in an internal pending balance. A static weight vector cannot express that switching behaviour.
  • Per-miner weights drift continuously. In a sliding-window PPLNS payout, every accepted share shifts the proportions; a vector frozen at token allocation is stale within minutes. Reissuing a fresh token per refresh reintroduces the same round-trip the static design was meant to avoid, with worse semantics (re-negotiating the token rather than re-fetching outputs).

The current design moves the output-set decision from token allocation to declared-job time. The JDS computes the output list using its current internal state and the JDC-reported revenue. Dust thresholds, pending-balance rerouting, per-miner output caps, payout consolidation — all of these fall out as operator policy on the JDS side without further wire-format surface.

Design choices

  1. Wire shape. Three new messages with extension_type = 0x0003, following the pattern of extension 0x0001 (which also introduces new request/response messages). Not a TLV on an existing message because the exchange is per-job and request/response.
  2. Per-job, not per-template. The JDC SHOULD request before each declared job; on the same prev_hash it MAY re-use the last response. On a prev_hash transition the JDC MUST refresh, because the prior PPLNS-window state is now associated with the block that just resolved. Section 3.1 of the new doc spells the refresh rules out.
  3. prev_hash in the request. Carries the template's chain tip so the JDS can refuse stale requests with stale-prev-hash. Protects against the race where a different block is found between the request and the declaration.
  4. pool_revenue, not template_revenue. The JDC reports the amount it is contributing to the pool's output set, after subtracting any non-zero self-outputs it intends to add per §6.4.3. The pool's emitted distribution sums to ≤ pool_revenue. This preserves §6.4.3's existing freedom for the JDC to allocate part of template_revenue to its own outputs.
  5. Fallback. When the JDC cannot get a fresh response (initial connection, JDS unreachable), it MAY fall back to declaring a job per the §6.4.3 single-output rule using AllocateMiningJobToken.Success.coinbase_tx_outputs. The JDS MUST accept §6.4.3-compliant declarations even when this extension is negotiated. Keeps availability under transient disruption.
  6. Predictive prefetch. Section 3.2 documents the recommended pattern for hiding the request/response round-trip at prev_hash transitions: the JDC pre-emptively requests for the predicted next prev_hash before its own TP observes the new tip.

Explicitly out of scope

  • Trustless share attestation between JDC and JDS.
  • Fee-vs-subsidy split policy (handled internally by the pool's distribution computation).
  • JDS↔Pool internal coordination (same in nature as the existing JDS↔Pool sharing of AllocateMiningJobToken.Success.coinbase_tx_outputs).
  • Per-output revenue caps, dust thresholds, pending-balance ledgers, payout consolidation cadence — operator policy, not protocol semantics.
  • JDS-initiated invalidation/push messages — if operational experience shows these are needed, a future extension can add them.

Open to feedback

  • Whether the round-trip cost is acceptable. Section 5.2 of the doc argues the dominant latency is at prev_hash transitions and is naturally addressed by predictive prefetch. If the maintainers see operational scenarios where this is still too costly, please raise them — moving to a hybrid model (static fallback + dynamic refresh) is possible.
  • Error code list in RequestCoinbaseOutputs.Error. The current set (invalid-mining-job-token / stale-prev-hash / revenue-too-large / coinbase-size-budget-exceeded / internal) is what I think we need; happy to extend or trim.
  • Extension number. 0x0003 is held over from the prior revision; happy to use whatever the registry should allocate.
  • Anything else.

Files changed

  • extensions/0x0003-dynamic-coinbase-outputs.md — new extension doc.
  • 09-Extensions.md — registry entry updated.

@TheBlueMatt

Copy link
Copy Markdown

Is it not simpler to eat the round-trip? Have an extension that allows the client to make arbitrary "what outputs would you want in a coinbase transaction paying the pool X?". You never know what kind of dust pruning policy the pool might want and trying to capture arbitrary payout construction rules sounds like it should get crazy complicated. The roundtrip sucks but if we lean on next block prediction it should be tolerable.

warioishere added a commit to warioishere/sv2-spec that referenced this pull request May 15, 2026
Static weights at AllocateMiningJobToken.Success cannot express
amount-dependent pool policy (dust thresholds, pending-balance
rerouting) and freeze the distribution for the token's lifetime
while a real PPLNS share window changes continuously.

Replace with three new messages: RequestCoinbaseOutputs (JDC->JDS,
carrying prev_hash and pool_revenue) and RequestCoinbaseOutputs.
Success/Error responses. JDS returns the consensus-serialized
output list computed at request time from current pool state.

Per design discussion with TheBlueMatt on PR stratum-mining#195.
@warioishere
warioishere force-pushed the add-extension-0x0003-coinbase-output-weights branch from 336a238 to 69dcc08 Compare May 15, 2026 06:40
@warioishere warioishere changed the title Add Extension 0x0003: Coinbase Output Weights Add Extension 0x0003: Dynamic Coinbase Outputs May 15, 2026
@warioishere

Copy link
Copy Markdown
Author

Thanks @TheBlueMatt — you're right, and on reflection the static-weights model has a structural problem

In a PPLNS pool the share weights drift continuously as new shares enter the window and the oldest fall out, so a vector frozen at AllocateMiningJobToken.Success is stale within minutes. Reissuing a token per refresh just to update the weights reintroduces the same round-trip you proposed, only with worse semantics (re-negotiating the token rather than re-fetching outputs). The whole "static weights" framing was the wrong shape for the problem and I didnt think this to the end.

Just force-pushed a refactor along the lines you described. Three new messages — RequestCoinbaseOutputs / .Success / .Error that the JDC sends per declared job, carrying prev_hash and pool_revenue. The JDS returns the consensus-serialized output list computed at request time from current pool state. Dust thresholds, pending-balance rerouting and any other amount-dependent pool policy fall out as JDS-internal computation rather than wire-format surface. Section 3 covers the lifecycle (per-prev_hash refresh, predictive prefetch to hide the round-trip at block transitions, fallback to §6.4.3 if the JDS path is disrupted).

PR body updated to reflect the new design. Round-trip cost is discussed in §5.2 — I think predictive prefetch handles it for the common case, but if you see operational scenarios where it doesn't, I'd appreciate the push-back.

Static weights at AllocateMiningJobToken.Success cannot express
amount-dependent pool policy (dust thresholds, pending-balance
rerouting) and freeze the distribution for the token's lifetime
while a real PPLNS share window changes continuously.

Replace with three new messages: RequestCoinbaseOutputs (JDC->JDS,
carrying prev_hash and pool_revenue) and RequestCoinbaseOutputs.
Success/Error responses. JDS returns the consensus-serialized
output list computed at request time from current pool state.

Per design discussion with TheBlueMatt on PR stratum-mining#195.
@warioishere
warioishere force-pushed the add-extension-0x0003-coinbase-output-weights branch from 69dcc08 to c723820 Compare May 29, 2026 13:12
@warioishere

warioishere commented May 29, 2026

Copy link
Copy Markdown
Author

Pushed a revision that reworks how the per-job output size is bounded, after implementing an experimental JDS side and finding the original draft leaned on a coinbase-size field that does not actually exist on the wire.

What changed

  • No new wire message / field. The earlier text (and §2.2/§2.3/§4.3) referenced coinbase_output_max_additional_size "committed in AllocateMiningJobToken.Success", but that message carries only coinbase_tx_outputs — there is no separate size field. Per §7.1 the JDC already derives its CoinbaseOutputConstraints reservation from the serialized size of AllocateMiningJobToken.Success.coinbase_tx_outputs. The extension now reuses exactly that channel instead of inventing a new one.
  • Per-job stays fully dynamic. RequestCoinbaseOutputs.Success still returns a fresh per-job set (recipients and amounts); it must only fit the size the JDC reserved from the token. The token is sized at the maximum plausible revenue, so it already covers the per-job revenue range with no dummy padding.
  • Growth handling. If the recipient set outgrows the token's reservation, the JDS returns coinbase-size-budget-exceeded and the JDC obtains a larger token. §4.3 rewritten accordingly: the tight (no-padding) policy is the default; 0-value padding for growth headroom is presented as an optional operator choice.
  • Base-spec cleanup (bundled). §6.4.3's two sentences referenced the non-existent coinbase_output_max_additional_size field on AllocateMiningJobToken.Success; reworded to point at the coinbase_tx_outputs serialized size + §7.1. Happy to split this into a separate PR if you'd prefer the extension PR to stay self-contained.

Net effect: the extension adds no coinbase-size-negotiation surface and is implementable against a JDC that follows the existing §7.1 reservation behavior.

@warioishere

Copy link
Copy Markdown
Author

closing in favor of #203

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.

2 participants