Skip to content

feat(boltz): Boltz submarine & reverse swaps#116

Draft
coreyphillips wants to merge 1 commit into
masterfrom
feat/boltz-swaps-deterministic-keys
Draft

feat(boltz): Boltz submarine & reverse swaps#116
coreyphillips wants to merge 1 commit into
masterfrom
feat/boltz-swaps-deterministic-keys

Conversation

@coreyphillips

Copy link
Copy Markdown
Collaborator

Summary

Adds a boltz module 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-client crate. 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.db stores only a monotonic per-swap derivation index.

Consequences:

  • A leaked database cannot move funds (it holds no key material).
  • Swaps are recoverable two ways: same-device (index + in-memory seed), or seed-only via Boltz's rescue API if boltz.db is lost.
  • The wallet 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.

Reviewers: please confirm threading the seed through these entry points fits Bitkit's key-handling conventions on the Swift/Kotlin side.

What's included

  • Submarine create/refund and reverse create/claim, with cooperative key-path → script-path fallback.
  • Managed WebSocket updates stream; auto-claims reverse swaps on transaction.confirmed (not mempool, to avoid revealing the preimage against an unconfirmed lockup).
  • Atomic, collision-free swap-index reservation; PRAGMA user_version migration anchor; input validation on create.
  • Idempotent claim/refund — returns the recorded txid without re-broadcasting.
  • Typed lifecycle status with forward-compatible Unknown { raw }; recovery/listing APIs.
  • Only one updates stream (one network) runs at a time — documented.

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.

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.
@coreyphillips coreyphillips changed the title feat(boltz): Boltz submarine & reverse swaps with deterministic keys feat(boltz): Boltz submarine & reverse swaps Jun 26, 2026

@ovitrif ovitrif left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

reviewed the current draft, added a few considerations in scoped review comments

Comment thread src/modules/boltz/api.rs
referral_id: None,
webhook: None,
};
let response = client

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Could we validate the Boltz create response before persisting or returning it? boltz-client exposes response.validate(...) for both submarine and reverse swaps, and that check proves the returned script/address matches our derived key and invoice/preimage before the app sends funds or pays the invoice.

Comment thread src/lib.rs

/// Fetch fees and limits for submarine swaps (onchain -> Lightning).
#[uniffi::export]
pub async fn boltz_get_submarine_limits(

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Since this adds new UniFFI exports and public types, could we bump the package version and regenerate the mobile/Python bindings in this PR? Right now Rust builds, but downstream iOS/Android consumers would not get the new boltz* APIs from the checked-in artifacts.

Comment thread src/lib.rs
})?;
// Idempotent: don't re-broadcast a swap that already has a claim tx
// (e.g. an auto-claim that ran first).
if let Some(existing) = record.claim_tx_id.clone() {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Could we serialize claim/refund attempts per swap, or mark a claim/refund as in progress before broadcasting? As written, auto-claim and a manual recovery call can both see no recorded txid and try to broadcast before either path persists the result.

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.

2 participants