loop/staticaddr/staticutil/utils.go

249 lines
7.6 KiB
Go
Raw Permalink Normal View History

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"
"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"
"github.com/lightningnetwork/lnd/lnwallet"
2025-05-21 14:17:14 +02:00
"github.com/lightningnetwork/lnd/lnwallet/chainfee"
)
// 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) {
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
}
prevOuts := make(map[wire.OutPoint]*wire.TxOut, len(deposits))
for i, d := range deposits {
outpoint := outpoints[i]
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,
addrParams *script.Parameters,
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) {
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,
addrParams *script.Parameters,
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,
signer lndclient.SignerClient, addrParams *script.Parameters,
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 {
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) {
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.
var depositSum btcutil.Amount
2025-05-21 14:17:14 +02:00
for _, d := range deposits {
depositSum += d.Value
}
2025-05-21 14:17:14 +02:00
if depositSum < btcutil.Amount(amount)+dustLimit {
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.
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: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 {
case lnrpc.CommitmentType_SIMPLE_TAPROOT,
lnrpc.CommitmentType_TAPROOT:
2025-05-21 14:17:14 +02:00
we.AddP2TROutput()
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())
}