Skip to content
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
blitzpool-masterfrom
feature/pplns-jdp-integration
Open

Integrate JDP with PPLNS via ext 0x0003 (Dynamic Coinbase Outputs)#115
warioishere wants to merge 7 commits into
blitzpool-masterfrom
feature/pplns-jdp-integration

Conversation

@warioishere

@warioishere warioishere commented Mar 29, 2026

Copy link
Copy Markdown
Owner

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-job RequestCoinbaseOutputs(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-serialized Vec<TxOut>. The JDC carries those outputs into its DeclareMiningJob.coinbase_tx_suffix. JDCs that do not negotiate 0x0003 see no behavioural change — vanilla §6.4.3 single-output stays intact.

What changed

Wire-level (src/models/sv2/)

  • New Sv2RequestCoinbaseOutputs codec triple (/.Success / /.Error) in sv2-extensions-messages.ts.
  • SV2_EXTENSION_TYPE_DYNAMIC_COINBASE_OUTPUTS = 0x0003 + extension-local msg-type codes (0x00/0x01/0x02).
  • Frames carry extension_type = 0x0003 per spec §3.4.1.

Service (src/services/job-declaration.service.ts)

  • New handleRequestCoinbaseOutputs(req, minerAddress):
    • stale-prev-hash when JDC's prev_hash diverges from pool's current view
    • revenue-too-large when pool_revenue > 1.5× template.coinbasevalue
    • invalid-mining-job-token for unknown / expired tokens (gated by client)
    • internal on transient errors
  • computeDynamicOutputs resolves mode (solo / pplns / group-solo), calls the relevant payout service with the JDC-reported revenue, applies dust suppression (sub-294-sat outputs → internal pending).
  • Response cache keyed by (token, requestId), bound to prev_hash. When the pool observes a new prev_hash via observePrevHash, 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)

  • Dispatch for extension_type=0x0003 / msgType=0x00.
  • allocatedTokens now stores minerAddress at allocation time so RequestCoinbaseOutputs can resolve the distribution for the correct miner.
  • AllocateMiningJobToken.Success carries the §6.4.3 single-output fallback regardless of negotiation — no more static-weights TLV on the wire.
  • validateCoinbaseOutputs matches the declared coinbase against (a) any cached RequestCoinbaseOutputs.Success emission for this token, falling back to (b) the §6.4.3 single-output presence check (matches the spec §3.4 fallback path).
  • extractPoolOutputPairs parses (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 by ca349d4) 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:

  • Per-miner amounts depend on revenue, not just weights. 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 real pool policy is to suppress the output and accumulate the amount in pending. A static weight vector cannot express that switching behaviour.
  • Weights drift continuously. In a sliding-window PPLNS payout every accepted share shifts the proportions; a vector frozen at AllocateMiningJobToken.Success 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 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):

  • Each emitted response is tied to the request's prev_hash.
  • When the pool's internal prev_hash advances (next block on the chain), all cache entries bound to the old prev_hash are evicted (observePrevHash does this on the next RequestCoinbaseOutputs call that arrives with the new prev_hash).
  • A DeclareMiningJob with 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

Suite Status
src/models/sv2/sv2-extensions-messages.spec.ts New: Request/Success/Error wire codecs, 26 tests
src/models/JobDeclarationClient.spec.ts New: dispatch path, frame-build, validation, fallback, 10 tests
src/services/job-declaration.service.spec.ts New: PPLNS / Group-Solo paths, dust suppression, stale-prev-hash, revenue-too-large, cache eviction, 33 tests
docs/jdp-extensions-spec-conformance.md Updated to reference ca349d4 and the new design
Full suite (regtest + PG-E2E enabled) 1053/1053 ✅

Test plan

  • npx tsc --noEmit clean
  • PG_E2E=1 PGHOST=… npx jest --runInBand → 1053 tests across 97 suites pass with regtest bitcoind + blitzpool-test-pg up
  • Vanilla JDP (no 0x0003): AllocateMiningJobToken.Success carries §6.4.3 single-output, no trailing TLV. Pinned by JobDeclarationClient.spec.ts.
  • 0x0003 negotiated: Request → Success carries Bitcoin-consensus Vec<TxOut> with real amounts. Pinned by service spec.
  • Stale-prev-hash: divergent prev_hash → Error frame, nothing cached. Pinned.
  • Dust suppression: sub-dust amounts dropped, count reflected in 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.

@plebhash

plebhash commented Apr 2, 2026

Copy link
Copy Markdown

@warioishere can you elaborate on your motivations for multi-output coinbase?

the Sv2 JDP spec does allow JDS to establish multiple outputs via AllocateMiningJobToken.Success, and SRI JDC is able to accomodate for that

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

@plebhash

plebhash commented Apr 2, 2026

Copy link
Copy Markdown

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:

stratum-mining/sv2-apps#388

diving into it now, will report back (either there or here) later today

@plebhash

plebhash commented Apr 2, 2026

Copy link
Copy Markdown

cross referencing: stratum-mining/sv2-apps#388 (comment)

@warioishere warioishere force-pushed the feature/pplns-pool-support branch from 32aa440 to 7ae94d3 Compare April 19, 2026 11:57
@warioishere warioishere force-pushed the feature/pplns-jdp-integration branch from c6d49fc to 09786a1 Compare April 19, 2026 11:57
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
@warioishere warioishere mentioned this pull request Apr 22, 2026
6 tasks
@warioishere warioishere force-pushed the feature/pplns-jdp-integration branch from 09786a1 to eebcd58 Compare May 11, 2026 11:58
@warioishere warioishere changed the base branch from feature/pplns-pool-support to blitzpool-master May 11, 2026 12:42
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.
@warioishere warioishere force-pushed the feature/pplns-jdp-integration branch from 939fe05 to 3c47a4c Compare May 11, 2026 13:12
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.
@warioishere warioishere force-pushed the blitzpool-master branch 4 times, most recently from b857a36 to 4aa4b84 Compare May 14, 2026 12:25
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
@warioishere warioishere changed the title Integrate JDP with PPLNS multi-output coinbaseOutputs Integrate JDP with PPLNS via ext 0x0003 (Dynamic Coinbase Outputs) May 15, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants