This repository was archived by the owner on Jul 8, 2026. It is now read-only.
Integrate JDP with PPLNS via ext 0x0003 (Dynamic Coinbase Outputs)#115
Open
warioishere wants to merge 7 commits into
Open
Integrate JDP with PPLNS via ext 0x0003 (Dynamic Coinbase Outputs)#115warioishere wants to merge 7 commits into
warioishere wants to merge 7 commits into
Conversation
|
@warioishere can you elaborate on your motivations for multi-output coinbase? the Sv2 JDP spec does allow JDS to establish multiple outputs via but unfortunately, Sv2 JDP spec does not allow non-custodial pooled payouts via multi-output (ala Ocean style); only the first output is standardized as the pool payout output |
|
we want to eventually enable non-custodial pooled payouts, but a Sv2 protocol extension will be required for that edit: oh sorry, just realized there's some extra context here: diving into it now, will report back (either there or here) later today |
|
cross referencing: stratum-mining/sv2-apps#388 (comment) |
32aa440 to
7ae94d3
Compare
c6d49fc to
09786a1
Compare
warioishere
added a commit
that referenced
this pull request
Apr 22, 2026
All of the following features were originally stacked as separate PRs on top of PPLNS: #119 feature/group-solo-mining group-solo engine + API #120 feature/mining-mode-endpoint GET /pplns/mode/:address #121 feature/block-template-mode-aware mode-aware block-template #122 feature/group-chart-endpoint chart + all security hardening + regtests Landing them as separate PRs would have exposed master to intermediate vulnerable states (selfLeave DoS, silent-add token leak, pending-out- of-coinbase math bug, etc.) until #122 closed the stack. All security and regtest work lived exclusively on #122, so merging the lower PRs first would ship a group-solo surface without its hardening. Rolling everything into #118 means the whole feature lands atomically on master with its full test + security story. PR #115 (JDP integration) stays separate, still upstream-blocked. # Conflicts: # src/controllers/pplns/pplns.controller.spec.ts # src/controllers/pplns/pplns.controller.ts
6 tasks
09786a1 to
eebcd58
Compare
Adds optional multi-output JDP coinbase via the spec-track extension 0x0003 — PPLNS and Group-Solo distributions can now be delivered through the Job Declaration Protocol without custody hand-off. - Base JDP behaviour stays exactly spec-pure: when ext 0x0003 isn't negotiated, AllocateMiningJobToken.Success carries one TxOut paying the miner directly (§6.4.3, single-output rule). - New ext 0x0001 (RequestExtensions) handler — the JDC opts into 0x0003 right after SetupConnection.Success, server responds with RequestExtensions.Success / .Error. - When 0x0003 is negotiated, JDS appends the coinbase_tx_output_weights TLV (Type=0x0003/0x01) to AllocateMiningJobToken.Success. Per-output weights are the per-address sats from PplnsService.getPayoutDistribution or GroupSoloService.getPayoutDistribution. - Group-Solo finder-bonus included via JDP: the miner allocating the token is passed as finderAddress, so an additional bonus output is emitted in the coinbase and reflected in the weights. - validateCoinbaseOutputs now checks all pool output scripts are present in coinbaseTxSuffix, not just the first. - TLV header is encoded big-endian per §3.4.3 (value internals stay LE). Spec compliance: §6.4.3 single-output rule preserved by default; ext 0x0003 §1.3 negotiation gate enforced; ext 0x0001 §4.1 error semantics (.Error when none requested are supported) implemented. Tests: 39 new + 875 existing pass; TLV byte sequence verified against the wire example in extensions/0x0003-coinbase-output-weights.md.
PushSolution field order was wrong — spec table §6.4.9 has nonce before ntime, but the SRI Rust reference (push_solution.rs) has ntime before nonce, and that's what real JDCs put on the wire. We follow SRI for interop; the spec table is a documentation bug. Pin the wire layout in a byte-level test so the regression can't sneak back in. Other spec-compliance fixes: - handleRequestExtensions now ignores stray pre-SetupConnection RequestExtensions frames (ext 0x0001 §4.2 — MUST arrive after SetupConnection.Success). - SV2 mining server now returns .Error (not empty .Success) when the JDC requests extensions none of which are supported (ext 0x0001 §4.1 — server MUST respond .Error in that case). - Consolidated the duplicate RequestExtensions definitions in sv2-messages.ts onto sv2-extensions-messages.ts (single source of truth, now incl. serializeRequestExtensionsError). - Documented PushSolution propagation per §6.1 + §6.4.9 (JDS MUST attempt to reconstruct/propagate — independent from JDC's own SubmitSolution path, by design, for orphan-risk reduction). New tests: - byte-level pin for PushSolution field order - spec wire-example roundtrip for the 0x0003 weights TLV - post-base TLV parsing roundtrip (AllocateMiningJobToken.Success + 0x0003 TLV) - JobDeclarationClient end-to-end state-machine tests through real handleFrame dispatch (negotiation success/error/mix, TLV emission gated by ext 0x0003 + multi-output payout, pre-setup rejection) 891 jest tests / 81 suites green; tsc clean.
Closes the per-share worker-attribution gap: a Stratum-V2 proxy that multiplexes multiple workers over a single extended channel can now attribute each share to the right worker, instead of every share landing on the channel-open user_identity. - Wire codec for the 0x0002 TLV (encodeWorkerIdTlv / parseWorkerIdTlv) in sv2-extensions-messages.ts. TLV header big-endian per §3.4.3, value is UTF-8 (max 32 bytes per spec §1.1). - New pure helper resolveShareWorkerNameFromTlv encodes the routing policy (extension-gated, bare-worker OR address.worker form, cross-account attribution dropped silently). - StratumV2Client adds 0x0002 to its SUPPORTED_MINING_EXTENSIONS and tracks negotiatedExtensions per-connection. handleSubmitSharesExtended derives the share's effective worker name from the trailing TLV and threads it into handleValidShare for shareTotalsCacheService and clientDifficultyStatisticsService (the two per-worker aggregations). - Security boundary: address part of the TLV is matched against the channel-locked address (case-insensitive). A TLV pointing at a different address is silently dropped — a multiplexing proxy MUST stay within the address it opened the channel under. 10 new resolver tests covering: extension-gated bypass, missing/ malformed TLV fallback, bare-worker / address.worker forms, cross- account drop, case-insensitive matching, nested dots, empty worker trailing-dot edge case.
939fe05 to
3c47a4c
Compare
Discovered in production 2026-05-11. Same class of doc-bug as PushSolution field order: spec table says channel_msg=0 but every real impl (SRI, bosminer) requires channel_msg=1. Fixed on master in 16434bc. Bitaxe firmware is permissive and works with both bit values.
b857a36 to
4aa4b84
Compare
Replaces the static weights-on-AllocateMiningJobToken.Success TLV with three new messages (RequestCoinbaseOutputs / .Success / .Error) that the JDC sends per declared job. The JDS computes the output set from current pool state and the JDC-reported pool_revenue, applies dust-suppression, caches the response keyed by (token, prev_hash) for validation, and evicts on prev_hash advance. Vanilla JDCs (no 0x0003 negotiation) hit the §6.4.3 single-output fallback path, behaviour unchanged. Matches sv2-spec PR #195 (Matt Corallo's dynamic-outputs design).
…plns-jdp-integration # Conflicts: # src/models/StratumV2Client.ts
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 subscribe to this conversation on GitHub.
Already have an account?
Sign in.
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.
Summary
Implements pool-side support for the sv2-spec ext 0x0003 (Dynamic Coinbase Outputs) extension, so PPLNS / Group-Solo payouts can be delivered through the Job Declaration Protocol without custodial hand-off.
When a JDC negotiates
0x0003, it sends a per-declared-jobRequestCoinbaseOutputs(token, prev_hash, pool_revenue)to the JDS. The pool computes the multi-output distribution from current PPLNS state and the JDC-reported revenue, applies dust-suppression, caches the response for validation, and replies with the consensus-serializedVec<TxOut>. The JDC carries those outputs into itsDeclareMiningJob.coinbase_tx_suffix. JDCs that do not negotiate0x0003see no behavioural change — vanilla §6.4.3 single-output stays intact.What changed
Wire-level (
src/models/sv2/)Sv2RequestCoinbaseOutputscodec triple (/.Success//.Error) insv2-extensions-messages.ts.SV2_EXTENSION_TYPE_DYNAMIC_COINBASE_OUTPUTS = 0x0003+ extension-local msg-type codes (0x00/0x01/0x02).extension_type = 0x0003per spec §3.4.1.Service (
src/services/job-declaration.service.ts)handleRequestCoinbaseOutputs(req, minerAddress):stale-prev-hashwhen JDC's prev_hash diverges from pool's current viewrevenue-too-largewhenpool_revenue > 1.5× template.coinbasevalueinvalid-mining-job-tokenfor unknown / expired tokens (gated by client)internalon transient errorscomputeDynamicOutputsresolves mode (solo/pplns/group-solo), calls the relevant payout service with the JDC-reported revenue, applies dust suppression (sub-294-sat outputs → internal pending).(token, requestId), bound toprev_hash. When the pool observes a new prev_hash viaobservePrevHash, all entries bound to the old one are evicted (template-bound TTL, no wall-clock TTL). This keeps cached responses fresh relative to the current payout window without drifting.findEmittedOutputsForJob(token, prev_hash)lookup for DeclareMiningJob validation.Client (
src/models/JobDeclarationClient.ts)extension_type=0x0003 / msgType=0x00.allocatedTokensnow storesminerAddressat allocation time so RequestCoinbaseOutputs can resolve the distribution for the correct miner.AllocateMiningJobToken.Successcarries the §6.4.3 single-output fallback regardless of negotiation — no more static-weights TLV on the wire.validateCoinbaseOutputsmatches the declared coinbase against (a) any cachedRequestCoinbaseOutputs.Successemission for this token, falling back to (b) the §6.4.3 single-output presence check (matches the spec §3.4 fallback path).extractPoolOutputPairsparses(script, amount)pairs for the strict dynamic-match path.Why this replaces an earlier static-weights draft
An earlier revision of this PR (commit
a18ee40, now superseded byca349d4) implemented the static-weights design that was on the sv2-spec PR before review feedback landed. Per @TheBlueMatt's review on stratum-mining/sv2-spec#195, that model has a structural problem:AllocateMiningJobToken.Successis 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 new dynamic-outputs design moves the output-set decision from token allocation to declared-job time. The JDS computes the output list at request time using its current internal state and the JDC-reported revenue. Dust thresholds, pending-balance rerouting, per-miner output caps, payout consolidation — all of these become JDS-internal policy without further wire-format surface.
Spec side is tracked at stratum-mining/sv2-spec#195.
Cache invalidation policy
The validation cache is template-bound, not block-found-bound (per user direction during design):
prev_hash.observePrevHashdoes this on the nextRequestCoinbaseOutputscall that arrives with the new prev_hash).DeclareMiningJobwith an old prev_hash would fail validation anyway (Bitcoin consensus). The cache invalidation is for memory hygiene, not for safety.Why not block-found-bound: between blocks (~10 min on mainnet) the PPLNS share window changes continuously as new shares enter. Caching responses for the whole inter-block period would re-serve stale distributions that don't include shares submitted by non-JDP miners during the window. Each Request now triggers a fresh computation from current pool state; the cache is just for "what did I emit, so I can validate against it."
Tests
src/models/sv2/sv2-extensions-messages.spec.tssrc/models/JobDeclarationClient.spec.tssrc/services/job-declaration.service.spec.tsdocs/jdp-extensions-spec-conformance.mdca349d4and the new designTest plan
npx tsc --noEmitcleanPG_E2E=1 PGHOST=… npx jest --runInBand→ 1053 tests across 97 suites pass with regtest bitcoind + blitzpool-test-pg upAllocateMiningJobToken.Successcarries §6.4.3 single-output, no trailing TLV. Pinned byJobDeclarationClient.spec.ts.Vec<TxOut>with real amounts. Pinned by service spec.Vec<TxOut>count byte. Pinned.Upstream alignment
When the spec PR lands and a 0x0003-aware JDC implementation exists, multi-output JDP goes live with no further code changes needed pool-side.