loop/staticaddr/staticutil/utils.go
Boris Nagaev d554b42ef6
multi: migrate to btcd v2 modules
Update LND, Aperture, and Taproot Assets to revisions using the
btcd v2 modules, and update lndclient to v0.21.0-3. Migrate Loop
chain, transaction, and address types to their corresponding v2
packages.

The lndclient release includes the migration from:
https://github.com/lightninglabs/lndclient/pull/280

Taproot Assets is temporarily replaced with its btcd v2 revision
because the v0.8 release branch has not adopted the new modules.

This raises the minimum Go version to 1.26 and changes exported
address types.
2026-08-12 23:39:26 +00:00

248 lines
7.6 KiB
Go

package staticutil
import (
"bytes"
"context"
"fmt"
"sort"
"github.com/btcsuite/btcd/btcutil/v2"
"github.com/btcsuite/btcd/chainhash/v2"
"github.com/btcsuite/btcd/txscript/v2"
"github.com/btcsuite/btcd/wire/v2"
"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"
"github.com/lightningnetwork/lnd/lnrpc"
"github.com/lightningnetwork/lnd/lnwallet"
"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.
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
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
// 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)
// Quick check: if total deposits can't even cover amount + dust
// (ignoring fees), there's no way to succeed.
var depositSum btcutil.Amount
for _, d := range deposits {
depositSum += d.Value
}
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
})
// 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
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
}
}
// 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
for range numInputs {
we.AddTaprootKeySpendInput(txscript.SigHashDefault)
}
// Add the funding output based on commitment type.
switch commitmentType {
case lnrpc.CommitmentType_SIMPLE_TAPROOT,
lnrpc.CommitmentType_TAPROOT:
we.AddP2TROutput()
default:
we.AddP2WSHOutput()
}
// Add a change output (P2TR) to be conservative.
we.AddP2TROutput()
return feeRate.FeeForWeight(we.Weight())
}