Stratum V2

NexusPool Ships Native Stratum V2 Job Declaration

NexusPool runs Stratum V2 Job Declaration in production: miners choose their transaction set, tested end to end against a real client.

NexusPool Ships Native Stratum V2 Job Declaration

Most pools decide, on their own, which transactions go into the block a miner is working on. Stratum V2 Job Declaration is the extension that changes that: it lets a miner declare its own transaction set to the pool, and the pool has to either mine exactly that set or refuse the block outright. NexusPool now runs this natively, in production, alongside the rest of its Stratum V2 implementation, tested end to end with real transactions against a real client before it ever reached a live connection.

What Job Declaration actually changes

Under the ordinary path, a pool builds a block template, and the miner just works whatever job it's handed. Job Declaration adds a separate negotiation: a miner's job declarator client tells the pool which transactions it wants in the next block, the pool records that declaration, and when a share from that job turns out to be a full block, the pool has to assemble the block from the declared set, not its own template. If the pool can't produce that exact set, the correct behavior is to refuse the block, not quietly swap in something else.

That refusal path is the part worth being specific about, because it's the part that actually proves the feature does what it claims. NexusPool's implementation checks the declared set at block-assembly time and rejects the block outright if that set was evicted or never matched, rather than falling back to its own template. The block is either built from exactly what the miner declared or it isn't sent at all.

How the pool tracks a declaration without over-spending memory

A naive implementation stores each declared transaction as its 32-byte identifier. NexusPool resolves each declared transaction to its position in the current block template at the moment it's declared, and stores that position as a 2-byte index instead. Resolving early, rather than at mining time, matters because the underlying template rotates: resolving late is exactly the kind of staleness bug this design has to avoid, and that resolution step doubles as the correctness check the format needs anyway. The result holds a fixed, small memory budget with no heap allocation per declaration, and assembly walks the declared list in the order the client actually declared it, since that's the order the client's own merkle commitment depends on.

Token bookkeeping follows the same discipline: a bounded table recycles its oldest live entry once full, rather than depending only on a chain-tip change to clear it, so the feature can't be starved by simply holding declarations open.

Testing Stratum V2 Job Declaration against a real client, not just itself

Internal unit tests passed cleanly with all of this in place, and still missed four real defects that only showed up once the implementation was run against SRI's actual reference Job Declaration client, the same reference implementation the wider Stratum V2 ecosystem builds and tests against, over a live TP-to-JDC-to-pool topology. Among them: a field-count mismatch that silently broke a core allocation message for any real client, a payout field that was declared but never wired to anything, and a connection reaper that was closing every healthy Job Declaration connection at roughly two minutes, because it required a mining channel that a JD-only connection never opens by design.

Finding those against a real client instead of a self-written test is the point worth stating plainly: a protocol implementation that only talks to its own test harness can pass every test it wrote and still fail the first time it meets someone else's client.

The bug that looked like someone else's problem

Once those four were fixed, a real transaction set still wouldn't go through. The client's process kept crashing, so the obvious read was a bug in the client, not the pool. It wasn't. Wrong twice, in fact: the first read was a client bug, the second read was CPU saturation from an unrelated stuck process. Both got ruled out, and the actual cause was sitting in the pool's own log the whole time: SV2 msg too big.

The pool had a size cap on SV2 frames set before Job Declaration existed, well under what a declaration listing real transactions needs. A declared set of just over a hundred real transactions was getting rejected at the transport layer before the Job Declaration code ever saw it, which silently closed the connection, which knocked out the client's upstream, which is what was actually making it crash three steps downstream. Raising the limit globally wasn't the fix, because it would let any connection force the pool to buffer a large frame, at scale, from every peer at once. The fix scopes the larger limit to connections that have already completed the encrypted handshake and explicitly opened a Job Declaration channel. Everything else keeps the small cap it always had.

Proven against real transactions, not empty blocks

With that fixed, the entire path was run against a mempool holding real transactions, not the synthetic empty blocks a lab environment tends to produce by default. Across the test run, the pool accepted 173 declarations listing between 129 and 256 real transactions each, installed 295 jobs from them, and 123 of those jobs were mined and submitted as blocks, all 123 accepted. One of those blocks carried 203 transactions (1 coinbase plus 202 declared) in a single 45,127-byte block, with every step in the chain actually happening: the miner declared 202 real transactions, the pool resolved all 202 to positions and held them, installed the job, the miner solved it, the pool assembled the block from exactly the declared set, submitted it, and it was accepted.

That's proof the mechanism itself works correctly under real transaction volume, run in NexusPool's own test environment against a real Job Declaration client. It is not, and this post won't call it, proof of a mainnet block. That's a distinct, still-open claim, addressed directly below rather than blurred into this one.

What the declared path costs, measured

The declared assembly path isn't just correct, it's also cheaper than the ordinary one: about 1.35 ms per megabyte of block, against about 1.47 ms for the standard template path, in NexusPool's own measurements. Walking a list of positions resolved ahead of time costs less than reconstructing a block from scratch, which is the expected direction for the design, now with a number attached to it instead of an assumption.

The one milestone still ahead, stated plainly

Everything above is proven: the protocol implementation, the fail-closed refusal path, the memory design, the fixes found against a real client, and correct assembly under real transaction volume. What hasn't happened yet, on the actual Bitcoin network, is a real miner declaring a real, non-empty transaction set and NexusPool finding a block built from it. That's not a gap in the implementation. It's a specific event only the network can produce, and it's the one Job Declaration claim this post won't make in advance of it. When it happens, it'll be checkable the same way every other block is: on-chain, not on a dashboard, and this post will be updated to name it.

What this looks like from where a miner sits

None of this is abstract if you're the one running the client. Point a Job Declaration client at NexusPool today and the exchange is the same one that ran 295 times during testing: you declare a set, the pool binds a token to it, and every job you're handed afterward is built from exactly what you named, nothing swapped in behind it. If NexusPool can't build what you declared, what you get back is a refusal, not a template you never agreed to. None of it changes your odds of finding a block, which stay exactly what network difficulty says they are regardless of who picked the transactions. What it changes is who decided what's inside the block, on the day you're the one who finds it.

Trust nothing. Verify that the job you're handed matches exactly what you declared, not what the pool says it matches, and once a block actually pays out from a declared set, run it through NexusPool's payout preflight tool the same way you'd check any other block.