feat(boltz): Boltz submarine & reverse swaps#116
Open
coreyphillips wants to merge 14 commits into
Open
Conversation
Integrate Boltz submarine (onchain -> Lightning) and reverse
(Lightning -> onchain) swaps behind the UniFFI surface.
Swap keys and reverse-swap preimages are derived deterministically from
the wallet seed via Boltz's BIP85 scheme (SwapMasterKey/derive_swapkey,
Preimage::from_swap_key). No key material is persisted: boltz.db stores
only a monotonic per-swap derivation index, so a leaked database cannot
move funds and swaps are recoverable from the seed alone (or via Boltz's
rescue API if boltz.db is lost).
- Submarine create/refund and reverse create/claim, with cooperative
key-path then script-path fallback (delegated to boltz-client).
- Managed WebSocket updates stream that auto-claims confirmed reverse
swaps; mnemonic held in memory only for the stream's lifetime.
- Atomic, collision-free swap-index reservation; schema user_version
anchor; input validation on create.
- Idempotent claim/refund (returns the recorded txid without
re-broadcasting).
- SQLite persistence, typed lifecycle status with forward-compatible
Unknown { raw }, and recovery/listing APIs.
- Unit tests for status mapping, DB round-trip, index reservation, and
deterministic derivation; ignored live E2E test against the Boltz API.
ovitrif
reviewed
Jun 30, 2026
ovitrif
left a comment
Collaborator
There was a problem hiding this comment.
reviewed the current draft, added a few considerations in scoped review comments
Add fee_rate_sat_per_vb to boltz_start_swap_updates so the wallet provides the fee rate used for automatic reverse-swap claims; core does no fee estimation. The rate is threaded through AutoClaimConfig into the auto-claim and falls back to the conservative default when None. Update the README signature and examples accordingly.
set_claim_tx and set_refund_tx previously updated only the tx id column, so a claimed or refunded swap kept its pre-claim status (the status column is otherwise advanced only by the live updates stream). They now also set the terminal transaction.claimed / transaction.refunded status, so claimed and refunded swaps report the correct state and drop out of the pending set.
Recording a claim or refund tx id now also advances the swap to its terminal status, so the test assertions for the pre-claim status and the pending set were stale. Also covers set_refund_tx, which was untested.
Rebuild the swap script from our own key and check it against the address Boltz returned, before the record is stored or the response reaches the caller. A submarine caller is about to fund the lockup address and a reverse caller is about to pay the invoice, so validating any later would still let the app act on a bad response.
Reading claim_tx_id, broadcasting, then recording the txid spans several await points, so the automatic claim from the updates stream and a manual recovery call could both see no recorded txid and both broadcast. Route both through guarded entry points that hold a per-swap lock across the whole read-broadcast-record sequence and re-read the record under it, so the second caller returns the first one's txid instead of broadcasting again.
Adds new public API (the boltz exports), so this is a minor bump. The iOS, Android and Python bindings now carry the boltz surface, which the checked-in artifacts were missing. Stop tracking the xcframework's static libraries. Linking boltz-client grew them past GitHub's 100 MB file limit, and they only duplicate the compressed copies inside BitkitCore.xcframework.zip, which is the archive Package.swift downloads from the release.
coreyphillips
marked this pull request as ready for review
July 14, 2026 16:16
…terministic-keys # Conflicts: # Cargo.lock # Cargo.toml # Package.swift # bindings/android/gradle.properties # bindings/android/lib/src/main/jniLibs/arm64-v8a/libbitkitcore.so # bindings/android/lib/src/main/jniLibs/armeabi-v7a/libbitkitcore.so # bindings/android/lib/src/main/jniLibs/x86/libbitkitcore.so # bindings/android/lib/src/main/jniLibs/x86_64/libbitkitcore.so # bindings/ios/BitkitCore.xcframework.zip # bindings/ios/BitkitCore.xcframework/ios-arm64-simulator/libbitkitcore.a # bindings/ios/BitkitCore.xcframework/ios-arm64/libbitkitcore.a # bindings/python/bitkitcore/libbitkitcore.dylib # bindings/python/setup.py
Collaborator
Author
|
Latest release for testing can be found as v0.5.1 here. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Adds a
boltzmodule integrating Boltz submarine (onchain → Lightning) and reverse (Lightning → onchain) swaps behind the UniFFI surface, for iOS/Android/Python.The dangerous cryptography (MuSig2 Taproot cooperative signing, swap scripts, claim/refund tx construction) is delegated to the
boltz-clientcrate. This module adds deterministic key management, SQLite persistence, lifecycle tracking, automatic claiming, and the FFI surface.Key design decision: deterministic keys, no stored secrets
Swap keys and reverse-swap preimages are derived from the wallet seed via Boltz's BIP85 scheme (
SwapMasterKey/derive_swapkey,Preimage::from_swap_key) — never random, never persisted.boltz.dbstores only a monotonic per-swap derivation index.Consequences:
boltz.dbis lost.mnemonic(+ optional BIP39 passphrase) now flows through the create/claim/refund/start-updates FFI calls. The background updates stream holds the mnemonic in memory only for its lifetime (dropped on stop) to auto-claim. The passphrase must match the wallet's, or derived keys won't control the funds.What's included
transaction.confirmed(not mempool, to avoid revealing the preimage against an unconfirmed lockup).PRAGMA user_versionmigration anchor; input validation on create.Unknown { raw }; recovery/listing APIs.Testing
cargo build, all 9 boltz unit tests, clippy, and fmt are clean. Tests cover status mapping, DB round-trip/recovery, monotonic index reservation, and deterministic derivation. An ignored live E2E test creates a real reverse swap and cryptographically validates the locally-derived redeem script + invoice against Boltz's response (no broadcast).Known follow-up: the claim/refund broadcast paths are not yet covered by an automated test — they need a regtest Boltz + Electrum stack. Recommended as a follow-up.