Skip to main content

Overview

Submarine swaps enable trustless conversion from Lightning Network payments to on-chain Bitcoin. The libwallet implementation supports two versions: V1 (legacy) and V2 (current), each with different script structures and security models.

SubmarineSwap Interface

Defines the common interface for submarine swap operations.
type SubmarineSwap interface {
    Invoice() string
    Receiver() SubmarineSwapReceiver
    FundingOutput() SubmarineSwapFundingOutput
    PreimageInHex() string
}
Invoice
string
Lightning invoice being paid
Receiver
SubmarineSwapReceiver
Recipient information
FundingOutput
SubmarineSwapFundingOutput
On-chain output details
PreimageInHex
string
Payment preimage (hex encoded)

SubmarineSwapReceiver Interface

type SubmarineSwapReceiver interface {
    Alias() string
    PublicKey() string
}
Alias
string
Recipient node alias
PublicKey
string
Recipient node public key (hex)

SubmarineSwapFundingOutput Interface

type SubmarineSwapFundingOutput interface {
    ScriptVersion() int64
    OutputAddress() string
    OutputAmount() int64
    ConfirmationsNeeded() int
    ServerPaymentHashInHex() string
    ServerPublicKeyInHex() string
    UserLockTime() int64
    
    // V1 only
    UserRefundAddress() MuunAddress
    
    // V2 only
    ExpirationInBlocks() int64
    UserPublicKey() *HDPublicKey
    MuunPublicKey() *HDPublicKey
}
ScriptVersion
int64
Script version (AddressVersionSwapsV1 or AddressVersionSwapsV2)
OutputAddress
string
On-chain address to fund
OutputAmount
int64
Amount to send on-chain in satoshis
ConfirmationsNeeded
int
Required confirmations before swap completes
ServerPaymentHashInHex
string
Payment hash from the Lightning invoice
ServerPublicKeyInHex
string
Swap server’s public key
UserLockTime
int64
Block height when user can reclaim funds
UserRefundAddress
MuunAddress
V1 only: Refund address for failed swaps
ExpirationInBlocks
int64
V2 only: Blocks until expiration
UserPublicKey
*HDPublicKey
V2 only: User’s public key for swap
MuunPublicKey
*HDPublicKey
V2 only: Muun’s public key for swap

ValidateSubmarineSwap

Validates a submarine swap against expected parameters.
func ValidateSubmarineSwap(
    rawInvoice string,
    userPublicKey *HDPublicKey,
    muunPublicKey *HDPublicKey,
    swap SubmarineSwap,
    originalExpirationInBlocks int64,
    network *Network,
) error
rawInvoice
string
required
Original Lightning invoice string
userPublicKey
*HDPublicKey
required
User’s HD public key
muunPublicKey
*HDPublicKey
required
Muun’s HD public key
swap
SubmarineSwap
required
Swap data from server
originalExpirationInBlocks
int64
required
Expected expiration in blocks
network
*Network
required
Network configuration
Returns: error if validation fails
Validates:
  • Invoice matches swap invoice
  • Payment hash matches
  • Output address is correct
  • Amount matches invoice
  • Keys are properly derived
  • Expiration is as expected

Example

err := ValidateSubmarineSwap(
    "lnbc10u1p3pj257pp5yztkwjcz5ytjw...",
    userPubKey,
    muunPubKey,
    swap,
    144, // ~1 day expiration
    Mainnet(),
)
if err != nil {
    log.Fatalf("Swap validation failed: %v", err)
}

Submarine Swap V1

Script Structure

V1 swaps use nested SegWit (P2SH-P2WSH) with a refund address.
type coinSubmarineSwapV1 struct {
    Network         *chaincfg.Params
    OutPoint        wire.OutPoint
    KeyPath         string
    Amount          btcutil.Amount
    RefundAddress   string
    PaymentHash256  []byte
    ServerPublicKey []byte
    LockTime        int64
}

Witness Script

The V1 witness script allows:
  1. Server spend: With payment preimage (happy path)
  2. User refund: After locktime expires
witnessScript, err := swaps.CreateWitnessScriptSubmarineSwapV1(
    refundAddress,
    paymentHash256,
    serverPublicKey,
    lockTime,
    network,
)

Signing V1 Swaps

func (c *coinSubmarineSwapV1) SignInput(
    index int,
    tx *wire.MsgTx,
    userKey *HDPrivateKey,
    _ *HDPublicKey,
) error
index
int
required
Input index to sign
tx
*wire.MsgTx
required
Transaction to sign
userKey
*HDPrivateKey
required
User’s private key
Returns: error if signing fails
Note: V1 swaps cannot be fully signed (server cooperation required)

Example: V1 Swap Refund

// After locktime expires, user can reclaim funds
coin := &coinSubmarineSwapV1{
    Network:         network.network,
    OutPoint:        outpoint,
    KeyPath:         "m/schema:1'/recovery:1'/external:1/0",
    Amount:          btcutil.Amount(100000),
    RefundAddress:   "bc1q...",
    PaymentHash256:  paymentHash,
    ServerPublicKey: serverPubKey,
    LockTime:        700000,
}

err := coin.SignInput(0, refundTx, userKey, nil)
if err != nil {
    log.Fatal(err)
}

Submarine Swap V2

Script Structure

V2 swaps use native SegWit (P2WSH) with collaborative multisig.
type coinSubmarineSwapV2 struct {
    Network             *chaincfg.Params
    OutPoint            wire.OutPoint
    Amount              btcutil.Amount
    KeyPath             string
    PaymentHash256      []byte
    UserPublicKey       []byte
    MuunPublicKey       []byte
    ServerPublicKey     []byte
    BlocksForExpiration int64
    ServerSignature     []byte
}

Witness Script

The V2 witness script allows:
  1. Collaborative spend: User + Server signatures (happy path)
  2. Refund spend: User + Muun signatures after expiration
witnessScript, err := swaps.CreateWitnessScriptSubmarineSwapV2(
    paymentHash256,
    userPublicKey,
    muunPublicKey,
    serverPublicKey,
    blocksForExpiration,
)

Signing V2 Swaps

func (c *coinSubmarineSwapV2) SignInput(
    index int,
    tx *wire.MsgTx,
    userKey *HDPrivateKey,
    _ *HDPublicKey,
) error
index
int
required
Input index to sign
tx
*wire.MsgTx
required
Transaction to sign
userKey
*HDPrivateKey
required
User’s private key
Returns: error if signing fails
Requires: Server signature must be present in ServerSignature field

Example: V2 Swap with Server Signature

// Server provides signature after invoice is paid
coin := &coinSubmarineSwapV2{
    Network:             network.network,
    OutPoint:            outpoint,
    Amount:              btcutil.Amount(100000),
    KeyPath:             "m/schema:1'/recovery:1'/external:1/0",
    PaymentHash256:      paymentHash,
    UserPublicKey:       userPub,
    MuunPublicKey:       muunPub,
    ServerPublicKey:     serverPub,
    BlocksForExpiration: 144,
    ServerSignature:     serverSig, // Provided by swap server
}

err := coin.SignInput(0, tx, userKey, nil)
if err != nil {
    log.Fatal(err)
}

Version Comparison

// Nested SegWit
// Uses refund address
// Server OR User (after locktime)
ScriptVersion: AddressVersionSwapsV1

// Spend paths:
// 1. Server + preimage (payment)
// 2. User after locktime (refund)

Transaction Lifecycle

Happy Path (Payment Successful)

  1. User initiates swap
    • Provides Lightning invoice
    • Receives funding output details
  2. User funds output
    • Sends BTC to swap address
    • Waits for confirmations
  3. Server pays invoice
    • Obtains payment preimage
    • V1: Claims with preimage
    • V2: Provides signature
  4. User claims funds
    • V2 only: Signs with server signature
    • V1: Server claims directly

Refund Path (Payment Failed)

  1. Payment fails or expires
    • Invoice expires
    • Server doesn’t pay
  2. Locktime/expiration reached
    • V1: Absolute locktime block height
    • V2: Relative expiration in blocks
  3. User reclaims funds
    • V1: Signs refund to refund address
    • V2: Signs with Muun (collaborative refund)

Error Handling

Common errors during swap operations:
// V2 specific
if len(c.ServerSignature) == 0 {
    return errors.New("swap server must provide signature")
}

// Cannot fully sign
func FullySignInput(...) error {
    return errors.New("cannot fully sign submarine swap transactions")
}

Security Considerations

V1 Security

  • User controls refund address private key
  • Locktime must be far enough in the future
  • Server can claim anytime with preimage
  • No Muun cooperation needed for refund

V2 Security

  • Requires Muun cooperation for refunds
  • More efficient (native SegWit)
  • Better privacy (looks like normal multisig)
  • Collaborative model reduces trust assumptions
V1 swaps are legacy and should not be used for new implementations. Use V2 for all new submarine swaps.

Integration Example

// 1. Validate swap details
err := ValidateSubmarineSwap(
    invoice,
    userPub,
    muunPub,
    swap,
    144,
    Mainnet(),
)
if err != nil {
    log.Fatal(err)
}

// 2. Fund the swap output
// (implementation depends on wallet)

// 3. Wait for server signature
// (received via server API)

// 4. Sign and broadcast claim transaction
inputs := &InputList{}
inputs.Add(swapInput)

pst, err := NewPartiallySignedTransaction(
    inputs,
    claimTxBytes,
    nil, // No nonces for V2
)

tx, err := pst.Sign(userKey, muunPub)
if err != nil {
    log.Fatal(err)
}

// 5. Broadcast transaction
// (implementation depends on wallet)

Constants

const (
    AddressVersionSwapsV1 = addresses.SubmarineSwapV1
    AddressVersionSwapsV2 = addresses.SubmarineSwapV2
)

Build docs developers (and LLMs) love