Skip to content

PoC: replace the mempool mirror with Bitcoin Core's TxCollection#599

Draft
Sjors wants to merge 19 commits into
stratum-mining:mainfrom
Sjors:2026/07/tx-collection
Draft

PoC: replace the mempool mirror with Bitcoin Core's TxCollection#599
Sjors wants to merge 19 commits into
stratum-mining:mainfrom
Sjors:2026/07/tx-collection

Conversation

@Sjors

@Sjors Sjors commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

100% vibe coded proof of concept for #598.

Instead of keeping a mempool mirror in sync by polling waitNext/getBlock every second (the successor to the original getrawmempool RPC dump), JDS backend Bitcoin Core's node validates declared jobs directly against its mempool using the new TxCollection IPC interface.

Additionally, he JDS submits solutions against the retained (node-side) template (submitSolution() instead of submitBlock()). The mempool mirror, its monitor loop, and all full-block transfers are gone: a block never crosses the IPC boundary in either direction. Also, the declared coinbase is fully consensus-checked at declaration time instead of when a solution is submitted.

The v30.x/v31.x backends are unchanged and keep the mirror.

Depends on

Also

Commits

  • TEMPORARY: pin bitcoin-capnp-types v32 to TxCollection PR branch - the TxCollection bindings only exist on the Add TxCollection 2140-dev/bitcoin-capnp-types#29 branch
  • TEMPORARY: require BITCOIN_CORE_V32_BINARY for integration tests - no released Bitcoin Core has TxCollection; tests need a locally built mining: add TxCollection to bandwidth-efficiently validate external block templates bitcoin/bitcoin#35671 binary
  • TEMPORARY: CI builds Bitcoin Core from bitcoin#35671 - build that binary from source in CI (with ccache) instead of stubbing it
  • JDS: validate DeclareMiningJob wtxid list and provided transactions - duplicate wtxids and out-of-set provided transactions are protocol violations; Bitcoin Core rejects them with RPC-level errors, which would otherwise let a misbehaving downstream tear down the shared IPC connection
  • JDP: restrict stale-tip classification to prev_hash - nbits/min_ntime drift can misclassify errors (bitcoin_core_sv2: JDP stale-chain-tip detection should use prev_hash only (avoid min_ntime/nbits/bip34 drift) #597), and the TxCollection backend has no template to take those fields from when transactions are missing
  • v32x JDP: validate declared jobs with TxCollection, drop mempool mirror - the main change: collectTxs/addMissingTxs/unknownTxPos/makeTemplate (with the declared coinbase) replace the mirror and checkBlock
  • v32x JDP: submit solutions via retained BlockTemplate - keep the validated template inside Bitcoin Core and submit solutions via submitSolution, removing block reconstruction (and per-job txdata) from jd-server
  • integration-tests: cover missing-transactions success flow and coinbase validation - the ProvideMissingTransactions round was only tested on failure paths, and no test mined a block containing a transaction or rejected a consensus-invalid coinbase

plebhash added 10 commits July 2, 2026 14:20
Bitcoin Core submitBlock requires a fully assembled block, so JDS must retain non-coinbase transaction bodies from DeclareMiningJob validation.

As a prerequisite for upcoming PushSolution handling with v32 IPC support, JdResponse::Success now carries txdata and no longer returns redundant txid_list.

JDS derives txids directly from txdata when validating merkle-root/merkle-path consistency for SetCustomMiningJob, without storing an extra txid cache in DeclaredCustomJob.

This addresses an earlier design blindspot and does not change v30/v31 validation behavior.
Add a full v32x unix-capnp runtime for both Job Declaration Protocol and Template Distribution Protocol, and wire V32X through common enum dispatch and version selection.

Include the v32 bitcoin-capnp-types dependency pin, lockfile updates, and docs/examples updates so version 32 is discoverable and selectable.

Also align shared JDP interfaces across versions by switching JdRequest::PushSolution to carry a fully assembled Block. v30/v31 handlers are updated to the new request shape for consistency, while remaining functionally unchanged (PushSolution stays unimplemented there because those IPC schemas do not expose submitBlock).
Reconstruct a full block at JDS when receiving PushSolution and submit it to Bitcoin Core via submitBlock.

Follow the clarified Job Declaration semantics from sv2-spec.

Reference: stratum-mining/sv2-spec#188

Reference: stratum-mining/sv2-spec#189

Design choice (KISS): JDS keeps only the latest declared custom job per downstream connection and only attempts PushSolution propagation against that entry. It does not attempt propagation for previously declared jobs.
@Sjors Sjors changed the title POC: replace the mempool mirror with Bitcoin Core's TxCollection PoC: replace the mempool mirror with Bitcoin Core's TxCollection Jul 7, 2026
@Sjors

Sjors commented Jul 7, 2026

Copy link
Copy Markdown
Contributor Author

Ironically we still need a limited transaction cache on the JDS side, because the current TxCollection does not add transactions to its mempool, does not share transactions between templates and does not hold on to (non-mempool) transactions after a template is released. These are all good future improvements that should not change the interface, but they'll non-trivial.

Instead for now the new commit holds on to any transaction the JDS requested, which avoids round-trips back to the miner. It should be much simpler than the original mempool.rs though.

Sjors added 9 commits July 8, 2026 08:55
Point bitcoin_capnp_types_v32 at Sjors/bitcoin-capnp-types
2026/07/tx-collection (2140-dev/bitcoin-capnp-types#29), which adds the
capnp bindings for the TxCollection interface introduced by
bitcoin/bitcoin#35671.

Revert to the 2140-dev repository (or a crates.io release) once that PR
is merged.
The v32 integration test harness previously fell back to a hardcoded
local build path. Since the TxCollection work depends on a Bitcoin Core
build of bitcoin/bitcoin#35671, require the BITCOIN_CORE_V32_BINARY
environment variable instead and explain how to produce the binary.

Remove once an official Bitcoin Core release ships TxCollection and
v32 follows the standard release-binary download flow.
Instead of stubbing the v32 binary, build Bitcoin Core from the
bitcoin/bitcoin#35671 (TxCollection) PR branch in the Integration
Tests workflow, with ccache to keep warm-cache runs fast. This follows
the approach used by 2140-dev/bitcoin-capnp-types#29.

Remove once an official Bitcoin Core release ships TxCollection.
Reject declared jobs whose wtxid list contains duplicates (invalid-job)
and silently drop ProvideMissingTransactions.Success transactions that
are not part of the declared job; any wtxid left uncovered is simply
reported as missing again.

Both are protocol violations that the mirror-based validators happened
to tolerate. Enforcing them at the jd-server layer establishes an
invariant the upcoming TxCollection-based v32.x backend depends on:
Bitcoin Core's collectTxs rejects duplicate wtxids and addMissingTxs
rejects out-of-set transactions with RPC-level errors, which would
otherwise let a misbehaving downstream tear down the shared IPC
connection.
DeclareMiningJob stale classification at the jd-server layer previously
compared the full validation context (prev_hash, nbits, min_ntime),
which can misclassify errors: min_ntime can increase without a tip
change, and nbits comparison is unnecessary for stale-tip detection.

JdResponse::Error and JdResponse::MissingTransactions now carry only
the chain tip the validator operated against. Error carries an
Option<BlockHash> so internal failures (where no tip was established)
are never misclassified as stale-chain-tip.

On the jd-server side, DeclaredCustomJob now stores the declared-against
prev_hash directly plus an Option<ValidatedJobData> (nbits + txdata),
replacing the validation_context/txdata/validated field triple whose
consistency was only enforced by convention.

This is a partial implementation of stratum-mining#597 (the version-specific
handlers still use their internal validation-context drift heuristics)
and prepares for the v32.x TxCollection backend, which has no template
to take nbits/min_ntime from when transactions are missing.
Bitcoin Core's TxCollection interface (bitcoin/bitcoin#35671) lets the
JDS validate an external block template directly against the node's
mempool, so the v32.x JDP backend no longer needs to keep an optimistic
local mempool mirror in sync via a waitNext monitor loop. This removes
the mirror, the monitor task, the startup createNewBlock call, and the
force-refresh retry logic, along with their staleness races (stratum-mining#268).

DeclareMiningJob validation now:
1. fetches the current tip via getTip
2. calls collectTxs with the declared wtxids
3. completes the collection via addMissingTxs when the client provided
   missing transactions (ProvideMissingTransactions.Success)
4. asks unknownTxPos which transactions are still unknown and, if any,
   responds with MissingTransactions
5. calls makeTemplate with the declared coinbase (zeroed extranonce,
   which no contextual check depends on), so Bitcoin Core reconstructs
   and fully validates the block including the coinbase (BIP34 height,
   output value, sigops, weight, witness commitment) and returns a
   BlockTemplate

The Success response parameters (prev_hash, nbits, min_ntime) come
from the validated template's header (getBlockHeader) and the full
transaction list from getBlock, preserving the response contract with
jd-server for SetCustomMiningJob validation and PushSolution block
reconstruction. The collection and template are destroyed once the
response is assembled.

Stale-tip classification compares the tip before and after validation,
plus a JDS-side check of the declared BIP34 height against the
expected next height so a stale job is reported as stale-chain-tip
rather than the generic invalid-job that bad-cb-height would map to.

Behavioral notes:
- readiness now waits for IBD to finish by polling
  isInitialBlockDownload instead of relying on createNewBlock blocking
  during IBD.
- a future optimization could retain the BlockTemplate and use its
  submitSolution method for PushSolution instead of reconstructing the
  full block in jd-server, which would also remove txdata from the
  Success response.
Instead of destroying the BlockTemplate returned by
TxCollection::makeTemplate, the v32.x backend retains it keyed by
(downstream_id, request_id). A PushSolution now sends only the header
fields and the solved coinbase to Bitcoin Core, which reconstructs the
block from the retained template via BlockTemplate::submitSolution --
the block itself never crosses the IPC boundary in either direction.

This removes the full-block transfer (getBlock) at declaration time
and the block reconstruction in jd-server, along with the per-job
txdata copy jd-server used to hold. SetCustomMiningJob merkle-path
validation now uses the coinbase merkle branch reported by the
validator: getCoinbaseMerklePath on v32.x (the branch is independent
of the template's dummy coinbase), and a branch computed from the
mirror's txids on v30.x/v31.x.

Since the backend now holds per-job node-side state, jd-server mirrors
its job lifecycle over the request channel so retained templates are
freed when they can no longer receive a solution:
- ReleaseDeclaredJob on declaration errors, expired tokens (janitor),
  failed SetCustomMiningJob validation, and active-job replacement
- CleanupDownstream on downstream disconnect
- a re-declared request id replaces (and destroys) the old template
- PushSolution consumes the template

v30.x/v31.x ignore the new lifecycle requests; their PushSolution
handling remains unimplemented (their IPC interface has no way to
submit blocks).
…se validation

Two gaps in the tp -> jdc -> jds end-to-end coverage:

- The ProvideMissingTransactions round was only tested on failure paths:
  jds_ask_for_missing_transactions runs the JDC and JDS against nodes
  with incompatible chains, so the declaration is expected to fail
  after the missing transactions are provided, and no existing test
  mined a block containing a non-coinbase transaction.

  propagated_from_jds_to_tp_with_missing_transactions runs the JDC and
  JDS against separate nodes sharing the same chain (via a new
  sync_chain_from test helper that copies blocks over RPC), with a
  transaction only in the JDC node's mempool. The declaration must
  complete a ProvideMissingTransactions round, and the solved block --
  which reaches the JDS node exclusively through the JDS, as the
  JDC -> TP SubmitSolution path is blocked -- must contain that
  transaction. This exercises collectTxs/addMissingTxs/makeTemplate
  and the retained-template submitSolution reconstruction end to end.

- Nothing checked that a consensus-invalid declared coinbase is
  rejected at declaration time. The new overpaying-coinbase scenario
  in bitcoin_core_ipc_jdp_io asserts that a coinbase paying more than
  the block subsidy is rejected as invalid-job on all supported
  Bitcoin Core versions (via checkBlock on v30.x/v31.x and via
  makeTemplate's coinbase validation on v32.x).
Transactions supplied through ProvideMissingTransactions.Success are
typically ones the JDS node will never learn over P2P (prioritized or
out-of-band transactions that don't meet its mempool policy). Without
a cache, every downstream declaring a template with such a transaction
-- and every retry -- pays its own ProvideMissingTransactions round.

Remember them in a bounded (32 MB), least-recently-used cache keyed by
wtxid, consulted only after unknownTxPos reports a transaction missing.
Unlike the retired mempool mirror this involves no background
synchronization and is never the source of truth: Bitcoin Core decides
what is missing, and the cache merely supplies it without asking the
client again. The wtxid key commits to the full serialized transaction,
so one downstream cannot misrepresent a transaction requested by
another; the worst a malicious downstream can do is churn the cache,
which degrades to today's behavior. Confirmed transactions stop being
looked up and age out on their own.

Bitcoin Core itself remembering TxCollection-provided transactions
(which would additionally improve relay and compact-block propagation
of the solved block) is a possible future replacement; see the
discussion in bitcoin/bitcoin#35671.

The new bitcoin_core_ipc_jdp_io scenario asserts that a transaction
provided once is remembered: a second downstream declaring it succeeds
without a missing-transactions round. It runs against all backends,
since the v30.x/v31.x mirror already behaves this way (previously
untested). The test-harness wallet helpers build a witness-free
transaction from an explicitly selected legacy coinbase output so the
minimal test coinbase needs no witness commitment, and mine funding
blocks in batches to stay under the RPC client timeout when several
test nodes mine concurrently.
@Sjors Sjors force-pushed the 2026/07/tx-collection branch from 354f23a to 31d3c51 Compare July 8, 2026 06:59
@Sjors

Sjors commented Jul 8, 2026

Copy link
Copy Markdown
Contributor Author

Updated to the latest version of 2140-dev/bitcoin-capnp-types#29 which bumped collectTxs() to @10. It also includes getTransactionsByWitnessID(), but we don't use that here.

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