2025-05-21 14:08:54 +02:00
|
|
|
package staticutil
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"bytes"
|
|
|
|
|
"context"
|
|
|
|
|
"fmt"
|
|
|
|
|
"sort"
|
|
|
|
|
|
|
|
|
|
"github.com/btcsuite/btcd/btcutil"
|
|
|
|
|
"github.com/btcsuite/btcd/chaincfg/chainhash"
|
2025-05-21 14:17:14 +02:00
|
|
|
"github.com/btcsuite/btcd/txscript"
|
2025-05-21 14:08:54 +02:00
|
|
|
"github.com/btcsuite/btcd/wire"
|
|
|
|
|
"github.com/lightninglabs/lndclient"
|
|
|
|
|
"github.com/lightninglabs/loop/staticaddr/deposit"
|
|
|
|
|
"github.com/lightninglabs/loop/staticaddr/script"
|
|
|
|
|
"github.com/lightninglabs/loop/swapserverrpc"
|
|
|
|
|
"github.com/lightningnetwork/lnd/input"
|
2025-05-21 14:17:14 +02:00
|
|
|
"github.com/lightningnetwork/lnd/lnrpc"
|
2025-05-21 14:08:54 +02:00
|
|
|
"github.com/lightningnetwork/lnd/lnwallet"
|
2025-05-21 14:17:14 +02:00
|
|
|
"github.com/lightningnetwork/lnd/lnwallet/chainfee"
|
2025-05-21 14:08:54 +02:00
|
|
|
)
|
|
|
|
|
|
|
|
|
|
// ToPrevOuts converts a slice of deposits to a map of outpoints to TxOuts.
|
|
|
|
|
func ToPrevOuts(deposits []*deposit.Deposit,
|
|
|
|
|
pkScript []byte) (map[wire.OutPoint]*wire.TxOut, error) {
|
|
|
|
|
|
2026-07-01 07:16:34 +02:00
|
|
|
outpoints := make([]wire.OutPoint, len(deposits))
|
|
|
|
|
for i, d := range deposits {
|
|
|
|
|
outpoints[i] = d.OutPoint
|
|
|
|
|
}
|
|
|
|
|
if err := deposit.CheckDuplicates(outpoints); err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
|
2025-05-21 14:08:54 +02:00
|
|
|
prevOuts := make(map[wire.OutPoint]*wire.TxOut, len(deposits))
|
2026-07-01 07:16:34 +02:00
|
|
|
for i, d := range deposits {
|
|
|
|
|
outpoint := outpoints[i]
|
2025-05-21 14:08:54 +02:00
|
|
|
txOut := &wire.TxOut{
|
|
|
|
|
Value: int64(d.Value),
|
|
|
|
|
PkScript: pkScript,
|
|
|
|
|
}
|
|
|
|
|
prevOuts[outpoint] = txOut
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return prevOuts, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// CreateMusig2Sessions creates a musig2 session for a number of deposits.
|
|
|
|
|
func CreateMusig2Sessions(ctx context.Context,
|
|
|
|
|
signer lndclient.SignerClient, deposits []*deposit.Deposit,
|
2026-05-01 15:54:37 +05:30
|
|
|
addrParams *script.Parameters,
|
2025-05-21 14:08:54 +02:00
|
|
|
staticAddress *script.StaticAddress) ([]*input.MuSig2SessionInfo,
|
|
|
|
|
[][]byte, error) {
|
|
|
|
|
|
|
|
|
|
musig2Sessions := make([]*input.MuSig2SessionInfo, len(deposits))
|
|
|
|
|
clientNonces := make([][]byte, len(deposits))
|
|
|
|
|
|
|
|
|
|
// Create the sessions and nonces from the deposits.
|
2026-03-05 12:18:05 +01:00
|
|
|
for i := range len(deposits) {
|
2025-05-21 14:08:54 +02:00
|
|
|
session, err := CreateMusig2Session(
|
|
|
|
|
ctx, signer, addrParams, staticAddress,
|
|
|
|
|
)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, nil, err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
musig2Sessions[i] = session
|
|
|
|
|
clientNonces[i] = session.PublicNonce[:]
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return musig2Sessions, clientNonces, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// CreateMusig2SessionsPerDeposit creates a musig2 session for a number of
|
|
|
|
|
// deposits.
|
|
|
|
|
func CreateMusig2SessionsPerDeposit(ctx context.Context,
|
|
|
|
|
signer lndclient.SignerClient, deposits []*deposit.Deposit,
|
2026-05-01 15:54:37 +05:30
|
|
|
addrParams *script.Parameters,
|
2025-05-21 14:08:54 +02:00
|
|
|
staticAddress *script.StaticAddress) (
|
|
|
|
|
map[string]*input.MuSig2SessionInfo, map[string][]byte, map[string]int,
|
|
|
|
|
error) {
|
|
|
|
|
|
|
|
|
|
sessions := make(map[string]*input.MuSig2SessionInfo)
|
|
|
|
|
nonces := make(map[string][]byte)
|
|
|
|
|
depositToIdx := make(map[string]int)
|
|
|
|
|
|
|
|
|
|
// Create the musig2 sessions for the sweepless sweep tx.
|
|
|
|
|
for i, deposit := range deposits {
|
|
|
|
|
session, err := CreateMusig2Session(
|
|
|
|
|
ctx, signer, addrParams, staticAddress,
|
|
|
|
|
)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, nil, nil, err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
sessions[deposit.String()] = session
|
|
|
|
|
nonces[deposit.String()] = session.PublicNonce[:]
|
|
|
|
|
depositToIdx[deposit.String()] = i
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return sessions, nonces, depositToIdx, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// CreateMusig2Session creates a musig2 session for the deposit.
|
|
|
|
|
func CreateMusig2Session(ctx context.Context,
|
2026-05-01 15:54:37 +05:30
|
|
|
signer lndclient.SignerClient, addrParams *script.Parameters,
|
2025-05-21 14:08:54 +02:00
|
|
|
staticAddress *script.StaticAddress) (*input.MuSig2SessionInfo, error) {
|
|
|
|
|
|
|
|
|
|
signers := [][]byte{
|
|
|
|
|
addrParams.ClientPubkey.SerializeCompressed(),
|
|
|
|
|
addrParams.ServerPubkey.SerializeCompressed(),
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
expiryLeaf := staticAddress.TimeoutLeaf
|
|
|
|
|
|
|
|
|
|
rootHash := expiryLeaf.TapHash()
|
|
|
|
|
|
|
|
|
|
return signer.MuSig2CreateSession(
|
|
|
|
|
ctx, input.MuSig2Version100RC2, &addrParams.KeyLocator,
|
|
|
|
|
signers, lndclient.MuSig2TaprootTweakOpt(rootHash[:], false),
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// GetPrevoutInfo converts a map of prevOuts to protobuf.
|
|
|
|
|
func GetPrevoutInfo(prevOuts map[wire.OutPoint]*wire.TxOut,
|
|
|
|
|
) []*swapserverrpc.PrevoutInfo {
|
|
|
|
|
|
|
|
|
|
prevoutInfos := make([]*swapserverrpc.PrevoutInfo, 0, len(prevOuts))
|
|
|
|
|
|
|
|
|
|
for outpoint, txOut := range prevOuts {
|
|
|
|
|
prevoutInfo := &swapserverrpc.PrevoutInfo{
|
|
|
|
|
TxidBytes: outpoint.Hash[:],
|
|
|
|
|
OutputIndex: outpoint.Index,
|
|
|
|
|
Value: uint64(txOut.Value),
|
|
|
|
|
PkScript: txOut.PkScript,
|
|
|
|
|
}
|
|
|
|
|
prevoutInfos = append(prevoutInfos, prevoutInfo)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Sort UTXOs by txid:index using BIP-0069 rule. The function is used
|
|
|
|
|
// in unit tests a lot, and it is useful to make it deterministic.
|
|
|
|
|
sort.Slice(prevoutInfos, func(i, j int) bool {
|
|
|
|
|
return bip69inputLess(prevoutInfos[i], prevoutInfos[j])
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
return prevoutInfos
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// bip69inputLess returns true if input1 < input2 according to BIP-0069
|
|
|
|
|
// First sort based on input hash (reversed / rpc-style), then index.
|
|
|
|
|
// The code is based on btcd/btcutil/txsort/txsort.go.
|
|
|
|
|
func bip69inputLess(input1, input2 *swapserverrpc.PrevoutInfo) bool {
|
|
|
|
|
// Input hashes are the same, so compare the index.
|
|
|
|
|
var ihash, jhash chainhash.Hash
|
|
|
|
|
copy(ihash[:], input1.TxidBytes)
|
|
|
|
|
copy(jhash[:], input2.TxidBytes)
|
|
|
|
|
if ihash == jhash {
|
|
|
|
|
return input1.OutputIndex < input2.OutputIndex
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// At this point, the hashes are not equal, so reverse them to
|
|
|
|
|
// big-endian and return the result of the comparison.
|
|
|
|
|
const hashSize = chainhash.HashSize
|
2026-03-05 12:18:05 +01:00
|
|
|
for b := range hashSize / 2 {
|
2025-05-21 14:08:54 +02:00
|
|
|
ihash[b], ihash[hashSize-1-b] = ihash[hashSize-1-b], ihash[b]
|
|
|
|
|
jhash[b], jhash[hashSize-1-b] = jhash[hashSize-1-b], jhash[b]
|
|
|
|
|
}
|
|
|
|
|
return bytes.Compare(ihash[:], jhash[:]) == -1
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// SelectDeposits sorts the deposits by amount in descending order. It then
|
2025-05-21 14:17:14 +02:00
|
|
|
// selects the deposits that are needed to cover the requested amount plus
|
|
|
|
|
// transaction fees and dust. The fee rate and commitment type are used to
|
|
|
|
|
// estimate the transaction fee for the current selection, since each
|
|
|
|
|
// additional input increases the fee.
|
|
|
|
|
func SelectDeposits(deposits []*deposit.Deposit, amount int64,
|
|
|
|
|
feeRate chainfee.SatPerKWeight,
|
|
|
|
|
commitmentType lnrpc.CommitmentType) ([]*deposit.Deposit, error) {
|
|
|
|
|
|
2025-05-21 14:08:54 +02:00
|
|
|
dustLimit := lnwallet.DustLimitForSize(input.P2TRSize)
|
2025-05-21 14:17:14 +02:00
|
|
|
|
|
|
|
|
// Quick check: if total deposits can't even cover amount + dust
|
|
|
|
|
// (ignoring fees), there's no way to succeed.
|
2025-05-21 14:08:54 +02:00
|
|
|
var depositSum btcutil.Amount
|
2025-05-21 14:17:14 +02:00
|
|
|
for _, d := range deposits {
|
|
|
|
|
depositSum += d.Value
|
2025-05-21 14:08:54 +02:00
|
|
|
}
|
2025-05-21 14:17:14 +02:00
|
|
|
if depositSum < btcutil.Amount(amount)+dustLimit {
|
2025-05-21 14:08:54 +02:00
|
|
|
return nil, fmt.Errorf("insufficient funds to cover swap " +
|
|
|
|
|
"amount, try manually selecting deposits")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Sort the deposits by amount in descending order.
|
|
|
|
|
sort.Slice(deposits, func(i, j int) bool {
|
|
|
|
|
return deposits[i].Value > deposits[j].Value
|
|
|
|
|
})
|
|
|
|
|
|
2025-05-21 14:17:14 +02:00
|
|
|
// Select deposits until the total covers the requested amount plus
|
|
|
|
|
// the estimated fee and dust reserve. We estimate the fee
|
|
|
|
|
// pessimistically with a change output to ensure we always select
|
|
|
|
|
// enough.
|
2025-05-21 14:08:54 +02:00
|
|
|
var selectedDeposits []*deposit.Deposit
|
|
|
|
|
var selectedAmount btcutil.Amount
|
2025-05-21 14:17:14 +02:00
|
|
|
for _, d := range deposits {
|
|
|
|
|
selectedDeposits = append(selectedDeposits, d)
|
|
|
|
|
selectedAmount += d.Value
|
|
|
|
|
|
|
|
|
|
fee := estimateFee(
|
|
|
|
|
len(selectedDeposits), feeRate, commitmentType,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
if selectedAmount >= btcutil.Amount(amount)+fee+dustLimit {
|
|
|
|
|
return selectedDeposits, nil
|
2025-05-21 14:08:54 +02:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-05-21 14:17:14 +02:00
|
|
|
// We exhausted all deposits without meeting the threshold.
|
|
|
|
|
return nil, fmt.Errorf("insufficient funds to cover swap " +
|
|
|
|
|
"amount plus fees, try manually selecting deposits")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// estimateFee returns the estimated fee for a transaction with the given
|
|
|
|
|
// number of taproot keyspend inputs and a single output determined by
|
|
|
|
|
// the commitment type. It includes a change output in the estimate to
|
|
|
|
|
// be conservative.
|
|
|
|
|
func estimateFee(numInputs int, feeRate chainfee.SatPerKWeight,
|
|
|
|
|
commitmentType lnrpc.CommitmentType) btcutil.Amount {
|
|
|
|
|
|
|
|
|
|
var we input.TxWeightEstimator
|
2026-03-05 12:18:05 +01:00
|
|
|
for range numInputs {
|
2025-05-21 14:17:14 +02:00
|
|
|
we.AddTaprootKeySpendInput(txscript.SigHashDefault)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Add the funding output based on commitment type.
|
|
|
|
|
switch commitmentType {
|
2026-06-11 14:22:43 -05:00
|
|
|
case lnrpc.CommitmentType_SIMPLE_TAPROOT,
|
|
|
|
|
lnrpc.CommitmentType_TAPROOT:
|
2025-05-21 14:17:14 +02:00
|
|
|
we.AddP2TROutput()
|
2026-06-11 14:22:43 -05:00
|
|
|
|
2025-05-21 14:17:14 +02:00
|
|
|
default:
|
|
|
|
|
we.AddP2WSHOutput()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Add a change output (P2TR) to be conservative.
|
|
|
|
|
we.AddP2TROutput()
|
|
|
|
|
|
|
|
|
|
return feeRate.FeeForWeight(we.Weight())
|
2025-05-21 14:08:54 +02:00
|
|
|
}
|