feat(boltz): Boltz submarine & reverse swaps - #116
Merged
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. |
ovitrif
self-requested a review
July 27, 2026 14:12
ovitrif
reviewed
Jul 27, 2026
ovitrif
left a comment
Collaborator
There was a problem hiding this comment.
I focused this review on the Boltz feature behavior and its fund-recovery paths. I left six scoped comments covering response validation, recovery state, updates lifecycle, input validation, and the documented FFI signature.
…m lifecycle - Bind the Bitcoin response terms boltz-client's validate leaves open: submarine script hashlock vs invoice payment hash, reverse script hashlock vs our preimage, and reverse invoice amount vs the requested amount_sat, with consistent-but-wrong response fixtures covering each - Validate the reverse claim address against the selected network before the create request; same for refund addresses before construction - Require a finite, positive fee rate before starting updates or constructing a claim/refund - Judge recovery by local completion: a settled reverse swap stays in the pending set until its claim txid is recorded locally, the updates stream retries such claims, and server updates no longer overwrite a locally recorded completion status - Serialize updates-stream replacement under the stream slot lock and abort superseded streams' tasks, with a concurrent-start test - Document the acceptZeroConf argument in the README table and examples
Takes master's Android publishing fixes (ELF build ids in .cargo/config.toml and the 16 KB-compatible gradle-publish workflow) and resolves the version and binary-artifact conflicts in favor of this branch: - Version stays 0.5.2 (master moved to 0.4.4 on the 0.4.x line; this branch bumped minor for the new boltz API) across Cargo.toml, Package.swift, gradle.properties and setup.py - Checked-in binaries (Android .so, iOS xcframework zip, Python dylib) stay at this branch's builds since they carry the boltz exports the committed binding sources reference; the next release build will regenerate them with master's linker flags included - The expanded xcframework static libraries stay deleted and gitignored (they exceed GitHub's 100 MB file limit; the zip is the SPM source)
- Bumps version to 0.5.3 - Updates bindings accordingly
ovitrif
approved these changes
Jul 30, 2026
ovitrif
left a comment
Collaborator
There was a problem hiding this comment.
Looks good overall. I left two non-blocking nits around refund metadata persistence and the stream-lifecycle test.
Store the validated refund address as the swap's onchain_address when recording a submarine refund, so boltzGetSwap and boltzListSwaps return the refund destination after completion, and cover the round-trip in the db tests. Await both wait_for_live_tasks calls in the concurrent-starts test; the futures were previously dropped without running, so the shutdown checks never executed.
Collaborator
Author
|
New build and version bump incoming... |
- Bumps version to 0.5.4 - Updates bindings accordingly
Collaborator
Author
|
Updated build/bindings 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.