Skip to content

Taproot offers (tr0) specification#3

Open
m0wer wants to merge 11 commits into
mainfrom
taproot
Open

Taproot offers (tr0) specification#3
m0wer wants to merge 11 commits into
mainfrom
taproot

Conversation

@m0wer

@m0wer m0wer commented Jun 1, 2026

Copy link
Copy Markdown
Member

Define tr0reloffer/tr0absoffer offer types for Taproot (P2TR, BIP341) CoinJoins: address-type negotiation via dedicated offer types, a Taproot variant of PoDLE that binds the BIP86 output key to the UTXO program, the Schnorr !sig wire encoding, script-type-aware fee sizing, mixed P2WPKH/P2TR maker wallets, and uniform output-type rules.

Specially motivated by Silent Payments (BIP352).

@m0wer m0wer requested review from 1440000bytes and takinbrrrr June 1, 2026 20:20
@m0wer m0wer self-assigned this Jun 1, 2026
Define tr0reloffer/tr0absoffer offer types for Taproot (P2TR, BIP341)
CoinJoins: address-type negotiation via dedicated offer types, a Taproot
variant of PoDLE that binds the BIP86 output key to the UTXO program,
the Schnorr !sig wire encoding, script-type-aware fee sizing, mixed
P2WPKH/P2TR maker wallets, and uniform output-type rules.
@m0wer

m0wer commented Jun 6, 2026

Copy link
Copy Markdown
Member Author

FYI @AdamISZ

@AdamISZ

AdamISZ commented Jun 6, 2026

Copy link
Copy Markdown

As per off-github convo, the obvious concern is the very broad-scale high-level one: in earlier "pit transitions" (from legacy to wrapped segwit, from wrapped segwit to native segwit), the consensus as far as I could tell was: even though it's annoying to split liquidity temporarily, we prefer to have the participants be reasonably comfortable that they are all sticking with the same coin type, to maximize anon set.

You seem to be taking the other side of the argument here - and to be clear, it is definitely an argument, no 100% right answer imo - that to achieve more flexibility w.r.t. other protocols, it can be advantageous to not uniform-ize all inputs and outputs to segwit v1 taproot.

My best guess is that the majority of users prefer the inflexible version. It is easier to reason about and it does not damage the already fragile (as you have already analyzed in some detail!) anonymity set gains achieved over time in the system.

To be clear, the "enforcement" was never absolute; I would have to check the old code more carefully, but, for example, I know that non-segwit inputs were accepted in segwit input sets, for example. I guess the question of 'what is enforced' and 'what is the code intended and coded to do by default' are two slightly separate questions, another thing to consider.

JMP-0001 Phase 3 uses PoDLE to bind a taker to a real UTXO, committing to P = k*G and binding P to the UTXO scriptPubKey. For a Taproot key-path UTXO the committed key is the BIP86 output key q = p + t (mod n), where t = tagged_hash("TapTweak", x_only(p*G)) (empty Merkle root). The proof is generated as in JMP-0001 with k = q. The verifier MUST bind the proof to the UTXO by checking that x_only(P) equals the 32-byte program of the UTXO scriptPubKey (OP_1 <program>); because Taproot keys are x-only this is parity-independent. The discrete-log portion is unchanged, and a verifier MUST perform both checks. A maker without a Taproot signing engine MUST NOT advertise tr0, so a tr0 taker never sends it a Taproot commitment.

Agreed on k=q. On parity, if it were me, I would approach it like this: once you have q (and so, k), you deal with everything as 33 byte compressed representations. So whatever P is as 32 bytes, you're now dealing with '02..' etc, and you may therefore have to flip q to match. From there, the existing algorithm should work exactly as before (I'm going off my own old codebase), since all calculations are now with points not serializations. Let me know if I missed something, but I believe that's right.

the commitment sent H(P2) should follow those rules, which would mean that the privkey wrt the NUMS base, P2, is the flipped privkey if necessary according to the above.
So the commitment itself doesn't change in structure, obviously, because it's a hash.

A maker without a Taproot signing engine MUST NOT advertise tr0, so a tr0 taker never sends it a Taproot commitment.

This is where mixed pits get a bit tricky. If you have a taproot wallet you just want to work in a taproot pit, right. I think a cleanly separate pit makes more sense.

As noted, allowing silent payments involvement is not incompatible with a 'rigid' bip86 pit. But I think it's better to stick with that on the output side. That was a very old argument in Joinmarket - whether we really should just allow people to send funds to any address, because for some use cases that's "good", and the gut reaction "different address type means the coinjoin didn't anonymize" isn't necessarily correct (in fact Belcher used to argue that that's just wrong). In this case, for taproot outputs it should be quite a bit better. But if we start mixing all types of inputs and change outputs, it feels like it's not a good idea. Sorry to be a bit vague.

Incorporate AdamISZ review feedback on PR #3:

- Make the tr0 pit rigid and separate: all inputs, equal outputs, and
  change are BIP86 key-path P2TR. Drop the taker-chosen output type and
  the per-transaction P2WPKH/P2TR input mixing in favor of a clean,
  uniform Taproot pit, mirroring earlier legacy/segwit pit transitions.
- Reframe mixed wallets as two independent pits (Separate Pits): a maker
  MAY serve sw0 and tr0 but never combines them in one CoinJoin.
- Note silent payments stay compatible because their outputs are P2TR.
- Rewrite the Taproot PoDLE section to normalize parity on 33-byte
  compressed points: negate the tweaked key when q*G is odd so the
  existing discrete-log algorithm and H(P2) commitment run unchanged.
- Add a Rationale section documenting the rigid-pit decision and simplify
  the Signing and Fees sections to Taproot-only.
@m0wer

m0wer commented Jun 8, 2026

Copy link
Copy Markdown
Member Author

Thanks a lot for the feedback, very useful. Incorporated all suggestions: 1beb9da

Does it have you ACK now? What about @takinbrrrr and @1440000bytes ?

FYI @theborakompanioni

@1440000bytes 1440000bytes left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The document does not mention anything about annex.

Comment thread jmp-0005.md
Comment thread jmp-0005.md Outdated
Comment thread jmp-0005.md Outdated
Comment thread jmp-0005.md Outdated
@AdamISZ

AdamISZ commented Jun 8, 2026

Copy link
Copy Markdown

The document does not mention anything about annex.

Does it need to? According to BIP341 we should not be using it.

Comment thread jmp-0005.md
Comment thread jmp-0005.md Outdated
m0wer added 3 commits June 8, 2026 14:15
…sighash)

Incorporate the second round of review feedback on PR #3:

- Remove Silent Payments (BIP352): a receiver derives the output key from
  the sum of all inputs, but in a CoinJoin no party knows the other
  participants' input keys, so SP outputs cannot be detected or spent.
  Reframe the Motivation around future taproot protocols (MuSig2, PTLC
  Lightning, swaps) and explain the BIP352 incompatibility in Rationale.
- Fix the Taproot PoDLE derivation: normalize the BIP341 internal key to
  even Y before applying the tweak (q = p + t is wrong for odd-Y internal
  keys). Clarify P/Q notation and drop the nonce-confusing 'k'.
- Require SIGHASH_DEFAULT for all tr0 inputs; verifiers reject non-64-byte
  signatures, trailing 0x00, and any annex.
- Condense the rigid-versus-flexible rationale (was repeated).
The Taproot !sig payload carries the 32-byte x-only output key after the 64-byte
Schnorr signature (mirroring the P2WPKH layout) so the receiver can identify the
input and verify the signature. The previous table omitted it, which would make an
independent implementation produce an incompatible !sig. The key is not placed in
the witness (a key-path witness is just the 64-byte signature).
…orcement optional

Align the spec with the intended (and implemented) behavior: each participant MUST
produce its own equal output and change as P2TR and MUST NOT mix types in a tx it
builds, but enforcing uniformity on counterparties is RECOMMENDED, not mandatory
(an implementation may tolerate a slightly non-uniform counterparty for liquidity,
like accepting more fee than required). The security-critical rules (PoDLE binding,
SIGHASH_DEFAULT, full-prevout signing, no annex) remain strict MUSTs. Add an
'enforcement is a target, not a consensus rule' note to the Rationale.
Comment thread jmp-0005.md Outdated
Comment thread jmp-0005.md
@1440000bytes

1440000bytes commented Jun 11, 2026

Copy link
Copy Markdown

Does it need to? According to BIP341 we should not be using it.

On a second thought, I think its okay to ignore annex in this doc. Although, some people are using it on-chain.

m0wer added 6 commits June 12, 2026 13:40
Specifies a collaborative BIP352 ECDH protocol that lets a taker pay a
silent payment address from a tr0 CoinJoin: blinded partial shares
(Somsen blind DH) from each maker, proven with DLEQ (the PoDLE
primitive), exchanged via new encrypted !spreq/!spshare commands.
Requires no change to BIP352; documents the remaining limitations
(no formal security proof, static DH oracle exposure).
QA hardening of the collaborative silent-payment derivation:

- Bound the static Diffie-Hellman oracle exposure by the sum of !spreq
  group counts, not just one response per session (a single count=255
  request yields 255 evaluations). Require distinct X_i and a small
  maker-side count cap.
- Mirror BIP352's abort conditions that were implicit: reject infinite
  A_sum, and fail on invalid input_hash or t_k scalars, matching the
  validated single-party create_outputs primitive.
- Clarify that the DLEQ binds X into the challenge (five-point preimage),
  giving per-base binding and separation from the four-point PoDLE proof.

Math verified against a BIP352 reference receiver and adversarial probes
(DLEQ soundness, blinding unlinkability, input-set independence).
- Add scripts/verify_sp_ecdh.py: a dependency-free check that the
  collaborative derivation matches an independent BIP352 reference
  receiver and resists the known attack vectors. Reference it from the
  spec. It verifies correctness, not security; it is not a proof.
- State plainly that no formal proof for the collaborative setting
  exists or is claimed (still an open research question per BIP352 and
  Optech), and mark the feature experimental.
- Note that BIP352's hashing of the ECDH secret (a KDF) weakens the
  Brown-Gallant static-DH concern relative to textbook ElGamal, while
  keeping the per-key evaluation bound.
- Drop the 'planned for joinmarket-ng' reference-implementation line.
- Add a Rationale paragraph distinguishing what existing proofs cover.
  Additive-tweak safety (Groth-Shoup 2021/1330, deterministic wallets)
  and DL aggregate signatures (DahLIAS 2025/692) secure the signature
  side but do not cover the static-DH oracle (returning alpha*X for a
  chosen point), which is the actual open gap; the plausible route is
  transferring the Privacy Pass one-more-decryption analysis.
- Note that restricting silent payment outputs to the taker sidesteps
  the multiparty same-address collision and label-renegotiation problem,
  since one party derives every silent payment output.
@m0wer

m0wer commented Jun 16, 2026

Copy link
Copy Markdown
Member Author

Hi @josibake @jonasnick @real-or-random

We're trying to implement paying to a Silent Payment address from a JoinMarket CoinJoin and maybe you can help us with the protocol flow focusing on the security part.

On jmp-0006.md, additive-tweak and aggregate-signature proofs cover the signing side. The collaborative SP construction additionally exposes a static-DH oracle (alpha·X for chosen X) on each participant's input key, with a DLEQ. Is there (or can there be) a one-more-DH/Privacy-Pass-style reduction that bounds the key-recovery risk for this exact construction, given BIP352 hashes the shared secret?

@m0wer

m0wer commented Jun 16, 2026

Copy link
Copy Markdown
Member Author

The practical question for us (@AdamISZ @1440000bytes @takinbrrrr) now is if this is all worth it. In practical terms, we would be getting into some complex cryptography and with significant wire protocol changes for being able to pay to SP. And developing the Taproot pit just to be able to spend from SP is probably not worth it.

Right now, one can just spend the SP received UTXOs to a P2WPKH address first and then coinjoin. And for spending the other way around. The additional cost for receiving is an additional transaction which has network fees and some privacy implications (timing, easier to track destination maker cluster for md0, ...). More or less the same applies for spending.

My gut feeling is that we should not pursue this yet. But wait until a Taproot pit has more advantages. I'm thinking of a "mini" CoinJoinXT with swaps, PTLC based LN, ...

What do you think?

Thanks for all the feedback BTW.

@real-or-random

Copy link
Copy Markdown

maybe you can help us with the protocol flow focusing on the security part.

No from my side, sorry. I don't have the bandwidth.

@1440000bytes

Copy link
Copy Markdown

The practical question for us (@AdamISZ @1440000bytes @takinbrrrr) now is if this is all worth it. In practical terms, we would be getting into some complex cryptography and with significant wire protocol changes for being able to pay to SP. And developing the Taproot pit just to be able to spend from SP is probably not worth it.

I think we should have taproot support in joinmarket-ng. Not sure about silent payments.

@josibake

Copy link
Copy Markdown

Hi @m0wer , thanks for the ping and for taking a look at this. Pinging @SomsenRuben, as well. Silent payments was designed with CoinJoin support in mind, so it would be cool to see this materialise. I am fairly bandwidth constrained at the moment and not super familiar with how JoinMarket works, but I can try to be helpful and answer questions on the silent payments specific side.

I also see some (appropriate) push back on implementing it, which I completely understand. Feel free to ping again in the future when this seems more tenable, if you all decide its not worth it right now.

@m0wer

m0wer commented Jun 17, 2026

Copy link
Copy Markdown
Member Author

Hi @josibake, Thanks for chiming in! The main concern is the lack of a formal security proof as discussed at @RubenSomsen 's gist: https://gist.github.com/RubenSomsen/be7a4760dd4596d06963d67baf140406

The specifics of JoinMarket don't matter too much, can be reduced to building a transaction that pays to a Silent Payment address (and other non SP ones) with untrusted peers (each with their own keys).

This is currently documented as not recommended on BIP352, but maybe some of you are working on it or have ideas about it.

@josibake

Copy link
Copy Markdown

Regarding the lack of a formal security proof, I was writing a more formal, rigorous version of the silent payments spec with the goal of moving towards a formal security proof. This isn't the same as a security proof for the blinding step, but does feel like a prerequisite. Even without a security proof, a more formalised version of the spec will at least make it easier to reason about composing the protocol with other protocols such as blinding. I'd be more motivated to pick that work back up of it would help here.

I agree that the specifics don't matter too much for JoinMarket specifically, but I do think silent payments in a collaborative transaction building protocol with untrusted peers is not trivial. There was some discussion around generalising this problem beyond silent payments in the past, but I'm not sure of the status of that work. All that being said, I think we have the right primitives (blinding, DLEQ) to be able to do this, but I don't want to hand wave over the complexity.

@AdamISZ

AdamISZ commented Jun 17, 2026

Copy link
Copy Markdown

The main concern is the lack of a formal security proof as discussed at @RubenSomsen 's gist: https://gist.github.com/RubenSomsen/be7a4760dd4596d06963d67baf140406

But that's the ecash write up (that became cashu), not the silentpayments one? I guess just the wrong link?
@josibake a reminder that I did write this, maybe it can be a starting point for whoever picks it up: https://github.com/AdamISZ/silentpayments-unlinkability-note .But as per this whole blind-DH thing, clearly that's a whole different kettle of fish!

I'll repeat what I said earlier upthread, to @m0wer , I wouldn't be pursuing this, as there's enough other things to do for now. But, meh, just my 2c.

@m0wer

m0wer commented Jun 17, 2026

Copy link
Copy Markdown
Member Author

But that's the ecash write up (that became cashu), not the silentpayments one?

It's linked in the BIP 352, reference 6: https://github.com/bitcoin/bips/blob/master/bip-0352.mediawiki#rationale-and-references There's a bit more of explanation there.

I'll repeat what I said earlier upthread, to @m0wer , I wouldn't be pursuing this, as there's enough other things to do for now. But, meh, just my 2c.

Agreed, thanks.

@AdamISZ

AdamISZ commented Jun 17, 2026

Copy link
Copy Markdown

It's linked in the BIP 352, reference 6: https://github.com/bitcoin/bips/blob/master/bip-0352.mediawiki#rationale-and-references There's a bit more of explanation there.

Oh, I see what you mean now. You'd think I'd have understood, but nope 😆

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.

6 participants