Skip to content

Taproot swaps#10756

Draft
m0wer wants to merge 12 commits into
spesmilo:masterfrom
m0wer:feat/taproot-swaps
Draft

Taproot swaps#10756
m0wer wants to merge 12 commits into
spesmilo:masterfrom
m0wer:feat/taproot-swaps

Conversation

@m0wer

@m0wer m0wer commented Jul 16, 2026

Copy link
Copy Markdown

This adds Taproot-based submarine swaps, including script-path signing and MuSig2 support.

Tests and release notes are included. Needs spesmilo/electrum-ecc#17. Assumes electrum-ecc>=0.0.8, which does not exist and is not bumped in the electrum-ecc PR as requested.

Here are the minimal changes needed to make the complete feature work, split across atomic commits. These could be split into smaller PRs if needed.

Breakdown:

  • Add tapscript transaction signing

    Adds BIP342 signature hashing and script-path signing.

    Tests cover control blocks, annexes, code separators, leaf versions, and all supported sighash modes.

    Commits:

    • 68e6e76 test(transaction): cover tapscript signing vectors
    • 0d0e456 feat(transaction): support tapscript sighash signing
  • Add Taproot swap primitives

    Adds two-leaf Taproot swap contracts and MuSig2 session primitives.

    This contains contract and signing logic only. It does not change the swap protocol or server behavior.

    Commits:

    • 92bdb90 test(swaps): verify Taproot swap primitives
    • 3f8cea8 feat(swaps): add Taproot swap primitives
    • 8119fb7 build(ecc): enable MuSig2 support
  • Add Taproot submarine swaps

    Adds optional taproot-v1 support to Electrum's existing submarine swap flow.

    It preserves the current hold-invoice flow, reverse prepayment, transports, and legacy P2WSH fallback. Taproot swaps use script-path claims and refunds.

    Commits:

    • 779816c feat(swaps): settle taproot-v1 swaps via script paths
    • f622359 feat(swapserver): create taproot-v1 swaps
    • b121e86 feat(swaps): negotiate taproot-v1 swaps
    • fff6fbb feat(swaps): persist Taproot swap contracts
  • Add cooperative Taproot swap settlement

    Adds optional taproot-cooperative-v1 settlement using MuSig2 key-path spends.

    MuSig2 sessions are bounded, memory-only, and single-use. Cooperative failures fall back to the existing Taproot script paths.

    Commits:

    • 9bd2f90 feat(swaps): persist cooperative Taproot spends
    • a7e834b feat(swapserver): add bounded MuSig2 sessions
    • 7cd4565 feat(swaps): validate cooperative Taproot spends

@SomberNight SomberNight left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Taproot-swaps: pretty cool feature.

This is a lot of code. It is fairly clear you generated it.
It is so much that it is a daunting task to review.
You basically need to get buy-in from a maintainer to review it.

but also, before asking someone else to review it, you need to simplify, de-dupe, and review it yourself. Did you review it yourself?!

Also, what's with all the Tor stuff? It seems completely unrelated. The larger you make the PR, the more likely it will never get merged. Split out unrelated stuff for later.

Even stuff you don't postpone for later, see if you can split into logically separate small commits of up to a few hundred lines each (not counting tests).

(The electrum-ecc PR needs to come first anyway though.)

Keep in mind someone needs to maintain master. Debugging code is much harder than writing it, and that was stated 50 years ago (Kernighan's Law), before clankers could write it for you.

Comment thread electrum/taproot_swaps.py Outdated
Comment thread electrum/network.py Outdated
Comment thread RELEASE-NOTES Outdated
@m0wer
m0wer marked this pull request as draft July 17, 2026 06:45
@m0wer

m0wer commented Jul 17, 2026

Copy link
Copy Markdown
Author

Hi @SomberNight, thanks for taking a look.

This wasn't ready for review yet, my bad for not labeling it as draft (fixed now). I opened it early to discuss the concept and validate the approach as a PoC before polishing the code, but I should have made that explicit. Your three code comments are all valid catches (the _encode_cltv duplication, the indentation churn, and the release-notes edit, which came from me over-cleaning earlier "Boltz" mentions), and I'll fix them. To answer your question directly: yes, I used AI tooling heavily. I reviewed the design and the protocol-level logic myself, but not yet the code. That's on me for presenting it as ready.

On the concept, which is what I'd like to settle first. The swapserver feature is very cool because it easily enables a P2P LN swap market for many Electrum users. That's why I want to build on top of it rather than start something from scratch (I reached out to @ecdsa a few weeks ago and he showed interest in Taproot swaps). Three things I'd like to improve:

  • Taproot swaps instead of the current SegWit ones. Mostly for privacy: a cooperative key-path spend looks like an ordinary single-sig spend on-chain, without revealing any script or even that there was one, and a non-cooperative spend only reveals the necessary leaf. This is how Boltz does it: https://blog.boltz.exchange/p/introducing-taproot-swaps-putting
  • Transport-level privacy (the Tor stuff). Nostr relays should not become surveillance honeypots. If clients and providers do all posting, retrieval, and swap coordination through them, the relays learn too much and can time swaps or selectively drop messages. The idea: the client fetches offers from many relays over Tor, picks a provider, then talks to it directly on its onion address (or an advertised clearnet URL), with Nostr only used as a fallback transport. Similar to JoinMarket's directory nodes, which serve as public orderbooks with direct connections for the actual protocol.
  • A simple web client for non-Electrum users. Something like https://swapmarket.github.io/ so the provider market is usable outside Electrum. Offer discovery over Nostr works from a browser; direct communication works via HTTPS URLs (or onion addresses from Tor Browser), with the Nostr fallback otherwise.

The first two already work on this branch: I have live regtest E2E tests where the client discovers the offer on a real Nostr relay, negotiates taproot-v1, and settles the swap (cooperative MuSig2 claim and script-path claim) against bitcoind + Lightning. The web client is in progress. My plan is to get the whole thing running on signet to learn the limitations, then settle the protocol and do a clean, reviewable implementation. That we can split into small PRs or commits, however you see fit.

What do you think about the concept? Is there anything you would change or do differently? Is this concept something you'd want in Electrum?

@f321x

f321x commented Jul 17, 2026

Copy link
Copy Markdown
Member

@m0wer some thoughts regarding your comment:

Transport-level privacy (the Tor stuff)

another approach might be using onion messages, Electrum already supports them but it might be non-trivial to get onion messages working in a web app (though maybe some LDK crates could be used with wasm).
The upside would be that it doesn't rely on Tor (being both available and functional for clients and servers).

A simple web client for non-Electrum users

Note there is already such a project ongoing, https://www.whales.exchange/.
They encountered some limitations described here #10611 and here #10650.

@m0wer

m0wer commented Jul 17, 2026

Copy link
Copy Markdown
Author

Hi @f321x, thanks for the pointers.

Onion messages: I looked at onion_message.py. Since OnionMessageManager already handles blinded paths, reply paths, and request/reply correlation, an onion message transport would be moderate effort: a payload handler registration mechanism (the TODO in on_onion_message_received_unsolicited), a TLV schema for the swap RPCs, and a client-side transport class. Offers are signed and already advertise endpoints, so providers could additionally advertise a node_id or blinded path. I see it as a complementary transport rather than a replacement, because:

  • Offer discovery still goes through Nostr relays, so it doesn't address the relay metadata problem; Tor covers discovery and coordination.
  • The anonymity set is small today: LND doesn't forward onion messages, Electrum forwards only behind an experimental flag, and the current code falls back to a direct peer connection (revealing the client's IP to the provider) when no route exists.
  • It's fire-and-forget with a 32 KB ceiling, awkward for the multi-round MuSig2 RPCs, and a web client would need an LN stack in wasm plus a WebSocket bridge.

whales.exchange limitations (#10611, #10650): thanks, I wasn't aware of these. Both apply to this branch as-is, since taproot-v1 only changes the on-chain contract and keeps the LN flows unchanged. A recap of the flows for context:

Forward swaps (client wants to send onchain and receive through LN):

  1. Old v1 flow (removed, submarine_swaps: Reintroduce support for the old submarine swap protocol #10650 reintroduces it): the user brings a standard bolt11 invoice and locks funds on-chain; the server pays the invoice and claims on-chain with the revealed preimage. Trustless and works with any wallet, but if the server can't route the payment, the user's funds stay locked until the CLTV timeout plus two mining fees. That failure mode is why it was removed.
  2. Current flow (#8566): the server generates the preimage and the client creates a hold invoice, holding the HTLC before locking anything on-chain. A routing failure costs the user nothing, but it requires client-side hold invoice support, which many third-party wallets don't have. That's the [Electrum Swap,WEX] Third party client forward swaps require different protocol on swap server side #10611 wall.
  3. Boltz taproot flow: same UX as v1 (standard invoice, any wallet), plus a route probe before accepting (TimeoutDeltaProvider.checkRoutability -> queryRoutes), and, if the LN payment fails, the server co-signs an instant cooperative MuSig2 key-path refund (MusigSigner.signRefund, guarded so it never signs while a payment is pending). Worst case drops to two mining fees with an immediate refund that reveals nothing on-chain. If the server stops cooperating, the CLTV script-path leaf remains the trustless fallback, i.e., it degrades to v1's worst case but never below it. As an optional optimization, a client could pre-negotiate with provider A a refund tx that pays directly into the lockup address for provider B (refund and re-lock in one tx), saving one tx.

Reverse swaps (client wants to send through LN and receive onchain): are already structurally the same in Electrum and Boltz, and third-party friendly: the client generates the preimage, the hold invoice lives server-side only, and the client claims on-chain with the preimage (cooperative key path under taproot). The remaining wart is the miner-fee prepay, which protects the server from a mining-fee DoS (user never claims after the server broadcasts the lockup; see ThomasV's bundled payments post). Electrum protects the client via bundled payments, which is an Electrum-only invoice extension, so third-party wallets risk losing the prepay. Boltz's taproot API dropped the prepay entirely (prepayMinerFee: false) and absorbs that DoS risk.

Since this branch already implements the cooperative key-path refund (refundtaprootswap), a Boltz-style forward flow could be added as an optional provider capability, keeping the hold-invoice flow as the safer default for Electrum clients. Likewise the prepay could become optional per provider (providers and clients can do the math on risk and the market offers and fees adapt to it). Both would address #10611 without reintroducing the old flow's worst case.

@SomberNight

Copy link
Copy Markdown
Member

The idea: the client fetches offers from many relays over Tor, picks a provider, then talks to it directly on its onion address (or an advertised clearnet URL), with Nostr only used as a fallback transport

Where does the client fetch the offers from? Is that still nostr?
Anyway, this just feels too complicated. It means servers need to set up Tor, they need to set up Let's Encrypt, and they still use nostr anyway. For clients too you kind of assume Tor, which we do not bundle in the client, so it's up to the user to run it externally.

Nostr relays should not become surveillance honeypots. If clients and providers do all posting, retrieval, and swap coordination through them, the relays learn too much and can time swaps or selectively drop messages.

With taproot swaps, does it matter if the nostr relay can "time" swaps? What does that mean exactly? What do they compare the timing against? With current swaps I understand they can compare it against blockchain/mempool events because they see the scripts, but that does not apply to cooperative successful taproot swaps.

Regarding selectively dropping messages, what exactly do you have in mind? We already send/receive direct messages across all configured relays, not just one. Do you mean all relays could form a cartel and DDOS the ones that refuse to join the cartel - or what is your threat model?

then talks to it directly on its onion address (or an advertised clearnet URL)

If the client talks to the swap server over a clearnet URL, that too reveals the IPs of the counterparties to each other. (just like the onion message direct peer connection fallback)


As it would be much simpler to just keep using nostr for everything, the extra complexity really needs to be justified.
Also, it still seems to me the whole change of transport idea could be split off as it's orthogonal from taproot.

@ecdsa

ecdsa commented Jul 18, 2026

Copy link
Copy Markdown
Member

Also, it still seems to me the whole change of transport idea could be split off as it's orthogonal from taproot.

I agree. it looks like you are trying to smuggle features we do not necessarily want.

same UX as v1 [...], plus a route probe

why? there is a reason why I removed the old flow. With a central server, it was possible to assume that the server was (relatively) well maintained and did have liquidity to send the payment. We cannot make that assumption in a decentralized marketplace. So, reverting to the "client goes on-chain first" flow is out of question.

Three things I'd like to improve: [...]

I did express interest in taproot swaps. but not in the two other items in your list. As mentioned by @SomberNight, those other things are orthogonal from taproot, and should not be bundled to this pull request.

@m0wer

m0wer commented Jul 18, 2026

Copy link
Copy Markdown
Author

Understood. To be clear, there was no intent to smuggle features; I opened the draft to discuss the broader concept and listed those features explicitly. That said, the scope decision is clear, and I agree the transport and web work are orthogonal.

I will not add the broader work to this PR. I’ll preserve it separately and reduce the Electrum work to MuSig2 first, followed by a Taproot-only implementation that keeps the existing transport and hold-invoice flow. The Tor transport, web client, and standard-invoice flow will remain outside that scope.

@m0wer
m0wer force-pushed the feat/taproot-swaps branch from ddec196 to 9bd2f90 Compare July 19, 2026 14:11
@1440000bytes

Copy link
Copy Markdown

Added some bug fixes in this branch but did not test: https://github.com/1440000bytes/electrum/commits/taproot-swaps-review-fixes/

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.

5 participants