Silent payments in the coinbase
How a mining pool could pay its miners directly in the block reward without publishing who got paid — and the two changes to BIP 352 that make it possible.
Status: a construction worked out on paper and pinned down in executable test vectors — not a specification, not reviewed, not implemented anywhere.
G — a fixed point on the curve that everyone agrees on.a lowercase — a private key (a secret number). A uppercase — its public key, where A = a·G.A from a. You cannot go backwards.01 The problem
A pool that pays each miner in the coinbase writes every payout address into the block, in public, forever. Anyone can see how many miners the pool has, what each one earns, and — by watching over time — how each one's hashrate changes.
The fix would be for each payout to go to a fresh address that only that miner can recognise. Not a list of addresses handed over in advance — a rule both sides can compute, so that the pool can pay a fresh address every block and the miner can still find it.
02 How silent payments normally do this
A miner publishes two public keys instead of an address — this is BIP 352's silent payment address — and keeps both secrets: a scan key for spotting payments and a spend key for spending them.
Both sides can now compute the same secret without ever sending it — this is a Diffie-Hellman exchange (ECDH), and it works because scalar multiplication doesn't care about the order you do it in:
The pool turns that shared secret into a number, and uses it to nudge the miner's spend key sideways into a brand-new address:
The miner computes the same t, so it recognises P in the block. And
it can spend from it, because the matching private key is just as simple:
Why two keys, not one. Finding the money needs b_scan.
Spending it needs b_spend. So b_scan can sit on an always-on
machine doing the searching, while b_spend stays offline. Someone who steals
the scanning key learns what you were paid but cannot touch it.
One thing is still missing, and it is the miner's privacy at stake rather than the payer's.
If the same payer pays the same miner twice from the same a, then s is the
same, t is the same, and P is the same address twice —
the address reuse the whole scheme exists to avoid. So the payer mixes in something that never
repeats. In ordinary silent payments that's the outpoint: a pointer to the specific coin
being spent, which by consensus can only ever be spent once.
That hash does a second job, easy to miss. It also commits to A, which stops a payer
choosing a key that reproduces an earlier s — forcing a miner to reuse an
address on purpose. Both jobs have to survive into anything built on this.
03 Why this breaks in a coinbase
The coinbase is the transaction that creates the block reward. It's special: it doesn't spend anything. And that breaks the scheme in two separate places.
There's no A. Look again at what the miner needs:
s = h·b_scan·A. In an ordinary transaction the miner gets A for
free, because it's the public key of the coins being spent and it's right there in the
transaction. A coinbase spends no coins, so there is no public key anywhere in it. The miner
has nothing to do the maths with.
There's no unique outpoint. Every coinbase in history points at the same
fixed placeholder — a null prevout: a zero transaction hash and an index of
0xffffffff. Using it as the nonce would give every block the same
h, so a pool paying a miner each block would pay the same address every time.
04 The variation: two changes
The second problem turns out not to be a problem at all. Since 2013 every block has been required to write its own height into the coinbase (BIP 34). It's unique, it's already there, and both sides know it.
It is fair to ask whether the height is even needed here, because a pool replacing
a_send each block would get fresh addresses from that alone. The two overlap, and one
of them would do. The height is worth keeping anyway because it is free and not optional:
it is already in every coinbase, and consensus — not the pool's own diligence — is what makes it
unique. Without it, a pool that reuses one a_send across two blocks reuses every payout
address for every miner, silently, with nothing on-chain to show it. Restart from stale key state,
fail over to a spare holding an old batch, mis-index the batch by one, and that is what you get.
Leaving the height in makes the whole class of mistake a non-event.
The first problem is the real one, and the fix is to stop reusing a spending key for this
job. Give the pool a key whose only purpose is this calculation — call it
a_send — and publish A_send so miners can use it.
That's the whole variation. Everything downstream is unchanged.
One detail in that formula is load-bearing: the nonce commits to the key actually used —
h = hash(H, A_send), not hash(H) alone. Leave the key out and the
construction has a replay attack: anyone holding one block's a_send can compute
a key for any other block that yields the same shared secret — same t,
same P, forced address reuse. The test suite keeps the broken version as a
negative control; the binding is what kills it.
05 Why the extra key is the point
It looks like a workaround for a missing value. It's actually the more interesting half.
In ordinary silent payments, a is doing two unrelated jobs at once: it does this
shared-secret calculation, and it's the key that spends the sender's money. Those
jobs pull in opposite directions. A money key must exist, hot, at signing time — and in any
real wallet it goes on guarding other funds, so it is kept. A privacy key should be
destroyed the moment it's been used.
a_send spends nothing. Nobody's coins depend on it. So the pool can throw it
away once the block is paid.
To be clear about what that is and isn't: deletion can't be proven. Nobody outside the pool can check whether a key was really destroyed or just quietly filed away, and no protocol can make that verifiable. So this is not an auditable guarantee, and shouldn't be sold as one.
What changes is that throwing the key away becomes possible at all. The fused
alternative is a key that also spends money — in a coinbase, the only key the transaction
reveals for free is the fee-output key, and that one must be kept: there is no version of the
pool that doesn't hold it. With a dedicated a_send, a pool that wants to keep
nothing is able to, and a key that no
longer exists can't leak, can't be stolen, and can't be handed over later. Unverifiable but
available beats unavailable, and it's the pool's own risk it's reducing, so the incentive points
the right way without anyone having to take it on trust.
And if it leaks anyway, no money moves. An attacker holding
a_send can compute s, and therefore t, and therefore
work out which outputs went to which miner. But spending needs
and b_spend never left the miner. Losing this key costs privacy, never
funds — exactly the trade the miner already gets from splitting scan from spend.
The variation just hands the sender the same deal.
06 Where A_send comes from
Whatever A_send the pool is using has to reach the miners somehow — 32 bytes. There
are a few places it can go, and they're not equally good.
Note that A_send does not have to change every block for the payouts to stay
unlinkable — the block height is already mixed into the hash, so every block produces different
addresses even from the same A_send. Replacing it is about limiting how much history a
single leaked key would expose. That makes the replacement rate a dial, not a requirement.
Down the mining connection. A pool is already talking to every one of its
miners, so it can simply send them the list of A_send values, one per upcoming
block. This costs nothing on-chain and is the best option. The catch is that a pool could serve
different lists to different miners to tell them apart, so the list needs to be published
somewhere public rather than whispered to each miner privately.
In the coinbase's spare bytes. Every block already has a small scratch area at
the start of the coinbase, and a second one attached to the segwit commitment. Either has room.
One constraint: while searching for a block, a miner rapidly rewrites part of that scratch area
(the extranonce) millions of times a second. A_send has to sit outside those
bytes. The payout outputs are fixed when the template is built; if the key overlapped the rolled
region, mining itself would overwrite the key the outputs were derived from — the block would pay
addresses no miner can detect.
That first scratch area holds 100 bytes by consensus — a coinbase scriptSig must be
2 to 100 bytes, which is a Bitcoin Core validity rule (bad-cb-length) rather than
anything BIP 34 says; BIP 34's job is only to put the height at the front of it. The room
is comfortably there. This is the layout the demo in section 7 executes:
Worst cases still fit: reserve up to 8 bytes for the height (a software reservation, not a protocol cap — today's heights minimally encode to 4) and 20 for the extranonce, and the 34-byte key still leaves more than 30 bytes free. Note that the key goes in as raw bytes, a 33-byte push: nothing in a scriptSig is text unless the pool wants it to be.
Better: under Stratum V2's
template
distribution protocol, a template provider must reserve the worst-case 400 weight units
— 100 bytes — for the scriptSig whether or not anything uses them, and unused ones are
not handed back to fee-paying transactions. So the space is pre-paid: putting A_send
there displaces nothing.
Stratum V2 also makes the extranonce rule structural rather than conventional.
§5.3.16
spells the assembly out as
coinbase_tx_prefix ‖ extranonce_prefix ‖ extranonce ‖ coinbase_tx_suffix, so the rolled
region is defined as the gap between the two pool-fixed halves and a key placed in the
suffix cannot be rolled by construction. Two constraints follow from
§5.1.2.1:
all channels in a group channel must share one extranonce size, so the 34 bytes shrink that space
uniformly for the group (against a 64-byte ceiling; practical allocations are 8–16). And one
deployment mode is out of scope — under
Job
Declaration the miner's own side builds the coinbase, so the pool cannot place the key at all;
this scheme assumes a pool-controlled template. Because the key's length is constant, none of it
needs per-block renegotiation. One open question: whether merge-mining commitments leave room
alongside those 34 bytes in practice — that hasn't been verified.
Not the pool's own fee output — and the reason is worth understanding. A
taproot output already is a public key sitting in the block, so it looks like a free
carrier. But for taproot, knowing the secret behind that visible key is exactly what lets you
spend the output. So if the fee output were A_send, then a_send would
be the key that spends the pool's fee — a key holding money, which can never be thrown away.
We'd be back where we started. Visibility and spendability are the same property there.
Trying to be clever with the output's spending conditions doesn't rescue it either, and the reason
generalises past taproot. Anything that lets a scanner read A_send off an output that
holds money either grants a_send the power to move that money, or requires it to sign
and so forbids ever deleting it. One or the other, every time. The rule that falls out:
A_send must never appear in a script that controls money. Put it in data, not
in a spending condition.
The neat part: it can replace the pool's name tag. Pools already write a
short bit of text into that scratch area — "/Foundry USA/" and so on — to sign
their blocks. Put A_send there instead, and it costs nothing extra, because
it's taking a slot that was already spoken for.
The pool doesn't even lose its public identity. If its list of A_send values is
published — which it has to be anyway — then "this block used entry number H from that
pool's list" identifies it at least as well as a text tag. Anyone can type someone
else's name into a block, and anyone can copy its A_send too — but a copied
A_send is inert, since you can't derive a single payment from it without
holding a_send. The cost is that block explorers lose a human-readable label.
One practical note. The key doesn't have to sit exactly where the tag sat — anywhere in the pool-fixed bytes works. The layout above (and the demo below) appends it right after the extranonce, in the suffix, where the disjointness is structural rather than remembered. And it goes in as raw bytes, not text — which is why the budget above has room to spare.
The tempting shortcut doesn't work. Publishing one key and deriving the rest looks free, and the maths does line up — the miner really can compute this block's key from the first one:
But read the first line again. Anyone who ever learns any one of these secrets can compute all the others — the differences between them are public — and unmask every payout the pool ever makes, past and future. The one property worth having is gone.
That's not a flaw in this particular formula — it's the general case. If the miner can derive the key from something already published, then so can anyone holding the original secret. Independent keys have to be independently published. There's no shortcut.
07 Run it
The whole mechanism, executing in your browser: one pool, one miner, one block. The pool derives a
payout address and writes A_send into the coinbase; the miner reads it back out and
finds the same address. No libraries — the curve arithmetic is about forty lines of
BigInt, and the hashing is the browser's own SHA-256.
The two sides never exchange a secret. The pool computed that payout key from
a_send and the miner's public keys; the miner computed the same key from
b_scan and the A_send it read out of the block. And the last two lines are
the constraint from section 6: the key sits clear of the bytes miners rewrite while hashing.
Two conventions hidden in that code are load-bearing. The key travels as an x coordinate, so
both sides need a parity rule: the pool canonicalises a_send to the even-Y
representative — negating it first if needed — and the miner does the same on receipt. Omit
the negation and both sides still derive, but from different points, so the scan silently
finds nothing (the suite keeps that failure as a negative control too). And the height is
minimally encoded little-endian on the wire but big-endian inside the hash; the full suite
parses it back out of the scriptSig rather than trusting the input.
The code that produced it:
08 The same thing in Python
The section above is a rewrite of the mechanism in JavaScript. This runs the Python implementation itself — a real CPython compiled to WebAssembly, fetching the files served next to this page — over all the cases, including the ones that show what happens when a piece of the construction is removed.
The catch is that a Python interpreter has to be downloaded first: about 6 MB from a CDN.
Nothing is fetched until you press the button, and if you would rather not,
python3 run_tests.py runs it at a terminal instead — all eleven cases, plus the
28-vector upstream baseline, if you also have a clone of
bitcoin/bips, and the same nine as
here if you don't.
Nine of the eleven cases run here. The rest print SKIP: the upstream baseline and
the two cases that compare against unmodified BIP 352 code — including the
one showing that plain silent payments reject a coinbase outright — need a clone of
bitcoin/bips, which a browser has no way to fetch. The vendor-drift and page-sync
guards skip too: they need files this page doesn't hand to the interpreter.
One warning if this tempts you to reuse the Python. The curve arithmetic comes from
secp256k1lab,
a teaching implementation whose own README says it is insecure and
trivially vulnerable to side-channel attacks. It is the right tool for test vectors and the
wrong one for anything holding money.
09 What this does and doesn't buy
It hides who was paid — and which outputs were payouts at all — from everyone reading the blockchain. The outputs themselves sit in plain sight, amounts included, but no observer can tell which output pays which miner, or even how many miners there are. Each miner sees its own payment and learns nothing about the others. The cost of that invisibility is one curve multiplication per block per silent key — nothing, for a party already running a full node.
It does not hide anything from the pool. The pool computed the payouts, so it knows exactly who is owed what — that's unavoidable in a scheme where one party decides the amounts. What changes is that the pool no longer has to publish that knowledge, and can choose not to keep the key that would let anyone reconstruct it later — with the caveat from section 5 that nobody outside the pool can check whether it really did.