From a918842374a77a21617e29a65f94992f7cab6671 Mon Sep 17 00:00:00 2001 From: Slyghtning Date: Wed, 21 May 2025 14:08:54 +0200 Subject: [PATCH] staticutil: refactor methods into utils --- staticaddr/address/manager_test.go | 11 ++ staticaddr/deposit/manager_test.go | 11 ++ staticaddr/loopin/actions.go | 14 +- staticaddr/loopin/loopin.go | 69 +------- staticaddr/loopin/manager.go | 5 +- staticaddr/staticutil/outpoints.go | 25 +++ staticaddr/staticutil/utils.go | 205 ++++++++++++++++++++++++ staticaddr/staticutil/utils_test.go | 236 ++++++++++++++++++++++++++++ 8 files changed, 505 insertions(+), 71 deletions(-) create mode 100644 staticaddr/staticutil/outpoints.go create mode 100644 staticaddr/staticutil/utils.go create mode 100644 staticaddr/staticutil/utils_test.go diff --git a/staticaddr/address/manager_test.go b/staticaddr/address/manager_test.go index 05f2a5b8..83cbff5d 100644 --- a/staticaddr/address/manager_test.go +++ b/staticaddr/address/manager_test.go @@ -76,6 +76,17 @@ func (m *mockStaticAddressClient) ServerWithdrawDeposits(ctx context.Context, args.Error(1) } +func (m *mockStaticAddressClient) ServerPsbtWithdrawDeposits(ctx context.Context, + in *swapserverrpc.ServerPsbtWithdrawRequest, + opts ...grpc.CallOption) (*swapserverrpc.ServerPsbtWithdrawResponse, + error) { + + args := m.Called(ctx, in, opts) + + return args.Get(0).(*swapserverrpc.ServerPsbtWithdrawResponse), + args.Error(1) +} + func (m *mockStaticAddressClient) ServerNewAddress(ctx context.Context, in *swapserverrpc.ServerNewAddressRequest, opts ...grpc.CallOption) ( *swapserverrpc.ServerNewAddressResponse, error) { diff --git a/staticaddr/deposit/manager_test.go b/staticaddr/deposit/manager_test.go index 1f0b2cf9..61079142 100644 --- a/staticaddr/deposit/manager_test.go +++ b/staticaddr/deposit/manager_test.go @@ -93,6 +93,17 @@ func (m *mockStaticAddressClient) ServerWithdrawDeposits(ctx context.Context, args.Error(1) } +func (m *mockStaticAddressClient) ServerPsbtWithdrawDeposits(ctx context.Context, + in *swapserverrpc.ServerPsbtWithdrawRequest, + opts ...grpc.CallOption) (*swapserverrpc.ServerPsbtWithdrawResponse, + error) { + + args := m.Called(ctx, in, opts) + + return args.Get(0).(*swapserverrpc.ServerPsbtWithdrawResponse), + args.Error(1) +} + func (m *mockStaticAddressClient) ServerNewAddress(ctx context.Context, in *swapserverrpc.ServerNewAddressRequest, opts ...grpc.CallOption) ( *swapserverrpc.ServerNewAddressResponse, error) { diff --git a/staticaddr/loopin/actions.go b/staticaddr/loopin/actions.go index bf896330..9286485b 100644 --- a/staticaddr/loopin/actions.go +++ b/staticaddr/loopin/actions.go @@ -17,6 +17,7 @@ import ( "github.com/lightninglabs/loop" "github.com/lightninglabs/loop/fsm" "github.com/lightninglabs/loop/staticaddr/deposit" + "github.com/lightninglabs/loop/staticaddr/staticutil" "github.com/lightninglabs/loop/staticaddr/version" "github.com/lightninglabs/loop/swap" "github.com/lightninglabs/loop/swapserverrpc" @@ -318,8 +319,11 @@ func (f *FSM) SignHtlcTxAction(ctx context.Context, // Create a musig2 session for each deposit and different htlc tx fee // rates. - createSession := f.loopIn.createMusig2Sessions - htlcSessions, clientHtlcNonces, err := createSession(ctx, f.cfg.Signer) + createSession := staticutil.CreateMusig2Sessions + htlcSessions, clientHtlcNonces, err := createSession( + ctx, f.cfg.Signer, f.loopIn.Deposits, f.loopIn.AddressParams, + f.loopIn.Address, + ) if err != nil { err = fmt.Errorf("unable to create musig2 sessions: %w", err) @@ -328,7 +332,8 @@ func (f *FSM) SignHtlcTxAction(ctx context.Context, defer f.cleanUpSessions(ctx, htlcSessions) htlcSessionsHighFee, highFeeNonces, err := createSession( - ctx, f.cfg.Signer, + ctx, f.cfg.Signer, f.loopIn.Deposits, f.loopIn.AddressParams, + f.loopIn.Address, ) if err != nil { return f.HandleError(err) @@ -336,7 +341,8 @@ func (f *FSM) SignHtlcTxAction(ctx context.Context, defer f.cleanUpSessions(ctx, htlcSessionsHighFee) htlcSessionsExtremelyHighFee, extremelyHighNonces, err := createSession( - ctx, f.cfg.Signer, + ctx, f.cfg.Signer, f.loopIn.Deposits, f.loopIn.AddressParams, + f.loopIn.Address, ) if err != nil { err = fmt.Errorf("unable to convert nonces: %w", err) diff --git a/staticaddr/loopin/loopin.go b/staticaddr/loopin/loopin.go index cca73bab..9be25730 100644 --- a/staticaddr/loopin/loopin.go +++ b/staticaddr/loopin/loopin.go @@ -21,6 +21,7 @@ import ( "github.com/lightninglabs/loop/staticaddr/address" "github.com/lightninglabs/loop/staticaddr/deposit" "github.com/lightninglabs/loop/staticaddr/script" + "github.com/lightninglabs/loop/staticaddr/staticutil" "github.com/lightninglabs/loop/staticaddr/version" "github.com/lightninglabs/loop/swap" "github.com/lightningnetwork/lnd/input" @@ -169,47 +170,6 @@ func (l *StaticAddressLoopIn) getHtlc(chainParams *chaincfg.Params) (*swap.Htlc, ) } -// createMusig2Sessions creates a musig2 session for a number of deposits. -func (l *StaticAddressLoopIn) createMusig2Sessions(ctx context.Context, - signer lndclient.SignerClient) ([]*input.MuSig2SessionInfo, [][]byte, - error) { - - musig2Sessions := make([]*input.MuSig2SessionInfo, len(l.Deposits)) - clientNonces := make([][]byte, len(l.Deposits)) - - // Create the sessions and nonces from the deposits. - for i := 0; i < len(l.Deposits); i++ { - session, err := l.createMusig2Session(ctx, signer) - if err != nil { - return nil, nil, err - } - - musig2Sessions[i] = session - clientNonces[i] = session.PublicNonce[:] - } - - return musig2Sessions, clientNonces, nil -} - -// Musig2CreateSession creates a musig2 session for the deposit. -func (l *StaticAddressLoopIn) createMusig2Session(ctx context.Context, - signer lndclient.SignerClient) (*input.MuSig2SessionInfo, error) { - - signers := [][]byte{ - l.AddressParams.ClientPubkey.SerializeCompressed(), - l.AddressParams.ServerPubkey.SerializeCompressed(), - } - - expiryLeaf := l.Address.TimeoutLeaf - - rootHash := expiryLeaf.TapHash() - - return signer.MuSig2CreateSession( - ctx, input.MuSig2Version100RC2, &l.AddressParams.KeyLocator, - signers, lndclient.MuSig2TaprootTweakOpt(rootHash[:], false), - ) -} - // signMusig2Tx adds the server nonces to the musig2 sessions and signs the // transaction. func (l *StaticAddressLoopIn) signMusig2Tx(ctx context.Context, @@ -217,7 +177,9 @@ func (l *StaticAddressLoopIn) signMusig2Tx(ctx context.Context, musig2sessions []*input.MuSig2SessionInfo, counterPartyNonces [][musig2.PubNonceSize]byte) ([][]byte, error) { - prevOuts, err := l.toPrevOuts(l.Deposits, l.AddressParams.PkScript) + prevOuts, err := staticutil.ToPrevOuts( + l.Deposits, l.AddressParams.PkScript, + ) if err != nil { return nil, err } @@ -523,29 +485,6 @@ func (l *StaticAddressLoopIn) Outpoints() []wire.OutPoint { return outpoints } -func (l *StaticAddressLoopIn) toPrevOuts(deposits []*deposit.Deposit, - pkScript []byte) (map[wire.OutPoint]*wire.TxOut, error) { - - prevOuts := make(map[wire.OutPoint]*wire.TxOut, len(deposits)) - for _, d := range deposits { - outpoint := wire.OutPoint{ - Hash: d.Hash, - Index: d.Index, - } - txOut := &wire.TxOut{ - Value: int64(d.Value), - PkScript: pkScript, - } - if _, ok := prevOuts[outpoint]; ok { - return nil, fmt.Errorf("duplicate outpoint %v", - outpoint) - } - prevOuts[outpoint] = txOut - } - - return prevOuts, nil -} - // GetState returns the current state of the loop-in swap. func (l *StaticAddressLoopIn) GetState() fsm.StateType { l.mu.Lock() diff --git a/staticaddr/loopin/manager.go b/staticaddr/loopin/manager.go index 2cd1a85b..a08cb353 100644 --- a/staticaddr/loopin/manager.go +++ b/staticaddr/loopin/manager.go @@ -21,6 +21,7 @@ import ( "github.com/lightninglabs/loop/labels" "github.com/lightninglabs/loop/staticaddr/address" "github.com/lightninglabs/loop/staticaddr/deposit" + "github.com/lightninglabs/loop/staticaddr/staticutil" "github.com/lightninglabs/loop/swapserverrpc" "github.com/lightningnetwork/lnd/input" "github.com/lightningnetwork/lnd/lntypes" @@ -391,8 +392,8 @@ func (m *Manager) handleLoopInSweepReq(ctx context.Context, ) copy(serverNonce[:], nonce) - musig2Session, err := loopIn.createMusig2Session( - ctx, m.cfg.Signer, + musig2Session, err := staticutil.CreateMusig2Session( + ctx, m.cfg.Signer, loopIn.AddressParams, loopIn.Address, ) if err != nil { return err diff --git a/staticaddr/staticutil/outpoints.go b/staticaddr/staticutil/outpoints.go new file mode 100644 index 00000000..69325dad --- /dev/null +++ b/staticaddr/staticutil/outpoints.go @@ -0,0 +1,25 @@ +package staticutil + +import ( + "fmt" + + "github.com/btcsuite/btcd/wire" + "github.com/lightningnetwork/lnd/lnrpc" +) + +// ToWireOutpoints converts lnrpc.OutPoint protos into wire.OutPoint structs so +// they can be consumed by lower level transaction building code. +func ToWireOutpoints(outpoints []*lnrpc.OutPoint) ([]wire.OutPoint, error) { + serverOutpoints := make([]wire.OutPoint, 0, len(outpoints)) + for _, o := range outpoints { + outpointStr := fmt.Sprintf("%s:%d", o.TxidStr, o.OutputIndex) + newOutpoint, err := wire.NewOutPointFromString(outpointStr) + if err != nil { + return nil, err + } + + serverOutpoints = append(serverOutpoints, *newOutpoint) + } + + return serverOutpoints, nil +} diff --git a/staticaddr/staticutil/utils.go b/staticaddr/staticutil/utils.go new file mode 100644 index 00000000..0d4b8c5c --- /dev/null +++ b/staticaddr/staticutil/utils.go @@ -0,0 +1,205 @@ +package staticutil + +import ( + "bytes" + "context" + "fmt" + "sort" + + "github.com/btcsuite/btcd/btcutil" + "github.com/btcsuite/btcd/chaincfg/chainhash" + "github.com/btcsuite/btcd/wire" + "github.com/lightninglabs/lndclient" + "github.com/lightninglabs/loop/staticaddr/address" + "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/lnwallet" +) + +// 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) { + + prevOuts := make(map[wire.OutPoint]*wire.TxOut, len(deposits)) + for _, d := range deposits { + outpoint := wire.OutPoint{ + Hash: d.Hash, + Index: d.Index, + } + txOut := &wire.TxOut{ + Value: int64(d.Value), + PkScript: pkScript, + } + if _, ok := prevOuts[outpoint]; ok { + return nil, fmt.Errorf("duplicate outpoint %v", + outpoint) + } + 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 *address.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 := 0; i < len(deposits); i++ { + 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 *address.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 *address.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 := 0; b < hashSize/2; b++ { + 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 amount requested without +// leaving a dust change. It returns an error if the sum of deposits minus dust +// is less than the requested amount. +func SelectDeposits(deposits []*deposit.Deposit, amount int64) ( + []*deposit.Deposit, error) { + + // Check that sum of deposits covers the swap amount while leaving no + // dust change. + dustLimit := lnwallet.DustLimitForSize(input.P2TRSize) + var depositSum btcutil.Amount + for _, deposit := range deposits { + depositSum += deposit.Value + } + if depositSum-dustLimit < btcutil.Amount(amount) { + 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 the deposits that are needed to cover the swap amount without + // leaving a dust change. + var selectedDeposits []*deposit.Deposit + var selectedAmount btcutil.Amount + for _, deposit := range deposits { + if selectedAmount >= btcutil.Amount(amount)+dustLimit { + break + } + selectedDeposits = append(selectedDeposits, deposit) + selectedAmount += deposit.Value + } + + return selectedDeposits, nil +} diff --git a/staticaddr/staticutil/utils_test.go b/staticaddr/staticutil/utils_test.go new file mode 100644 index 00000000..43da817f --- /dev/null +++ b/staticaddr/staticutil/utils_test.go @@ -0,0 +1,236 @@ +package staticutil + +import ( + "bytes" + "context" + "testing" + + "github.com/btcsuite/btcd/btcec/v2" + "github.com/btcsuite/btcd/btcutil" + "github.com/btcsuite/btcd/chaincfg/chainhash" + "github.com/btcsuite/btcd/wire" + "github.com/lightninglabs/loop/staticaddr/address" + "github.com/lightninglabs/loop/staticaddr/deposit" + "github.com/lightninglabs/loop/staticaddr/script" + "github.com/lightninglabs/loop/swapserverrpc" + looptest "github.com/lightninglabs/loop/test" + "github.com/lightningnetwork/lnd/input" + "github.com/lightningnetwork/lnd/keychain" + "github.com/stretchr/testify/require" +) + +// mustHash converts a hex string to a chainhash.Hash and panics on error. +func mustHash(t *testing.T, s string) chainhash.Hash { + t.Helper() + h, err := chainhash.NewHashFromStr(s) + require.NoError(t, err) + return *h +} + +func TestToPrevOuts_Success(t *testing.T) { + // Prepare two distinct deposits with different outpoints and values. + d1 := &deposit.Deposit{ + OutPoint: wire.OutPoint{ + Hash: mustHash(t, "0000000000000000000000000000000000000000000000000000000000000001"), + Index: 0, + }, + Value: btcutil.Amount(12345), + } + + d2 := &deposit.Deposit{ + OutPoint: wire.OutPoint{ + Hash: mustHash(t, "1111111111111111111111111111111111111111111111111111111111111111"), + Index: 7, + }, + Value: btcutil.Amount(987654321), + } + + pkScript := []byte{0x51, 0x21, 0x02, 0x52} // arbitrary bytes + + prevOuts, err := ToPrevOuts([]*deposit.Deposit{d1, d2}, pkScript) + require.NoError(t, err) + + // We expect two entries. + require.Len(t, prevOuts, 2) + + // Check the first outpoint mapping. + txOut1, ok := prevOuts[d1.OutPoint] + require.True(t, ok, "expected outpoint d1 to be present") + require.EqualValues(t, int64(d1.Value), txOut1.Value) + require.Equal(t, pkScript, txOut1.PkScript) + + // Check the second outpoint mapping. + txOut2, ok := prevOuts[d2.OutPoint] + require.True(t, ok, "expected outpoint d2 to be present") + require.EqualValues(t, int64(d2.Value), txOut2.Value) + require.Equal(t, pkScript, txOut2.PkScript) + + // Ensure the keys in the map are exactly the outpoints we provided. + for op := range prevOuts { + require.True(t, op == d1.OutPoint || op == d2.OutPoint) + } +} + +func TestToPrevOuts_DuplicateOutpoint(t *testing.T) { + // Two deposits that share the exact same outpoint should cause an error. + shared := wire.OutPoint{ + Hash: mustHash(t, "2222222222222222222222222222222222222222222222222222222222222222"), + Index: 2, + } + + d1 := &deposit.Deposit{OutPoint: shared, Value: btcutil.Amount(100)} + d2 := &deposit.Deposit{OutPoint: shared, Value: btcutil.Amount(200)} + + _, err := ToPrevOuts([]*deposit.Deposit{d1, d2}, []byte{0x00}) + require.Error(t, err) +} + +func TestGetPrevoutInfo_ConversionAndSorting(t *testing.T) { + // Helper to create a hash from string. + must := func(s string) chainhash.Hash { + h, err := chainhash.NewHashFromStr(s) + require.NoError(t, err) + return *h + } + + // Choose txids such that after reversal, ordering is determined by the + // last byte of the original hex string. + txidA := must("0000000000000000000000000000000000000000000000000000000000000001") + txidB := must("0000000000000000000000000000000000000000000000000000000000000002") + + pkScript := []byte{0xaa, 0xbb} + + prevOuts := map[wire.OutPoint]*wire.TxOut{ + {Hash: txidA, Index: 5}: {Value: 11, PkScript: pkScript}, + {Hash: txidA, Index: 2}: {Value: 22, PkScript: pkScript}, + {Hash: txidB, Index: 0}: {Value: 33, PkScript: pkScript}, + } + + infos := GetPrevoutInfo(prevOuts) + + // Expect deterministic ordering: + // 1) All entries with txidA (..01) before txidB (..02) due to BIP-69 + // compare on reversed hashes. + // 2) Within txidA, index 2 before index 5. + require.Len(t, infos, 3) + + require.Equal(t, &swapserverrpc.PrevoutInfo{ + TxidBytes: txidA[:], + OutputIndex: 2, + Value: 22, + PkScript: pkScript, + }, infos[0]) + + require.Equal(t, &swapserverrpc.PrevoutInfo{ + TxidBytes: txidA[:], + OutputIndex: 5, + Value: 11, + PkScript: pkScript, + }, infos[1]) + + require.Equal(t, &swapserverrpc.PrevoutInfo{ + TxidBytes: txidB[:], + OutputIndex: 0, + Value: 33, + PkScript: pkScript, + }, infos[2]) +} + +func TestBip69InputLess_SameHashIndexOrder(t *testing.T) { + txid := make([]byte, 32) + txid[31] = 0x7f // Arbitrary value. + + a := &swapserverrpc.PrevoutInfo{TxidBytes: txid, OutputIndex: 1} + b := &swapserverrpc.PrevoutInfo{TxidBytes: txid, OutputIndex: 3} + + require.True(t, bip69inputLess(a, b)) + require.False(t, bip69inputLess(b, a)) +} + +func TestBip69InputLess_DifferentHashes(t *testing.T) { + // txid1 ends with 0x01, txid2 ends with 0x02. After reversing for + // comparison, txid1 should still come before txid2 in lexicographic + // order. + h1, _ := chainhash.NewHashFromStr("0000000000000000000000000000000000000000000000000000000000000001") + h2, _ := chainhash.NewHashFromStr("0000000000000000000000000000000000000000000000000000000000000002") + + a := &swapserverrpc.PrevoutInfo{TxidBytes: h1[:], OutputIndex: 9} + b := &swapserverrpc.PrevoutInfo{TxidBytes: h2[:], OutputIndex: 0} + + require.True(t, bip69inputLess(a, b)) + require.False(t, bip69inputLess(b, a)) +} + +func TestCreateMusig2Session_Success(t *testing.T) { + // Set up mock signer from loop/test package. + lnd := looptest.NewMockLnd() + signer := lnd.Signer + + // Create dummy key material for address parameters. + clientKey, err := btcec.NewPrivateKey() + require.NoError(t, err) + serverKey, err := btcec.NewPrivateKey() + require.NoError(t, err) + + params := &address.Parameters{ + ClientPubkey: clientKey.PubKey(), + ServerPubkey: serverKey.PubKey(), + Expiry: 10, + PkScript: []byte{0x51}, + KeyLocator: keychain.KeyLocator{Family: 1, Index: 2}, + } + + // Build a static address for tweak options. + staticAddr, err := script.NewStaticAddress( + input.MuSig2Version100RC2, int64(params.Expiry), params.ClientPubkey, params.ServerPubkey, + ) + require.NoError(t, err) + + sess, err := CreateMusig2Session(context.Background(), signer, params, staticAddr) + require.NoError(t, err) + require.NotNil(t, sess) +} + +func TestCreateMusig2Sessions_Multiple(t *testing.T) { + lnd := looptest.NewMockLnd() + signer := lnd.Signer + + // Keys/params/static address. + clientKey, err := btcec.NewPrivateKey() + require.NoError(t, err) + serverKey, err := btcec.NewPrivateKey() + require.NoError(t, err) + + params := &address.Parameters{ + ClientPubkey: clientKey.PubKey(), + ServerPubkey: serverKey.PubKey(), + Expiry: 12, + PkScript: []byte{0xaa}, + KeyLocator: keychain.KeyLocator{Family: 9, Index: 8}, + } + + staticAddr, err := script.NewStaticAddress( + input.MuSig2Version100RC2, int64(params.Expiry), params.ClientPubkey, params.ServerPubkey, + ) + require.NoError(t, err) + + // Prepare N deposits; only the length matters for session count. + deposits := []*deposit.Deposit{ + {OutPoint: wire.OutPoint{Index: 0}}, + {OutPoint: wire.OutPoint{Index: 1}}, + {OutPoint: wire.OutPoint{Index: 2}}, + } + + sessions, nonces, err := CreateMusig2Sessions( + context.Background(), signer, deposits, params, staticAddr, + ) + require.NoError(t, err) + require.Len(t, sessions, len(deposits)) + require.Len(t, nonces, len(deposits)) + + // The mock signer returns a zero-value PublicNonce; assert consistency. + for i := range sessions { + require.NotNil(t, sessions[i]) + require.True(t, bytes.Equal(nonces[i], sessions[i].PublicNonce[:])) + } +}