mirror of
https://github.com/lightninglabs/loop.git
synced 2026-08-13 12:33:03 +02:00
Merge pull request #1043 from hieblmi/withdraw-psbt
staticaddr: psbt withdrawals
This commit is contained in:
commit
c3f3199625
13 changed files with 2000 additions and 613 deletions
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
25
staticaddr/staticutil/outpoints.go
Normal file
25
staticaddr/staticutil/outpoints.go
Normal file
|
|
@ -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
|
||||
}
|
||||
205
staticaddr/staticutil/utils.go
Normal file
205
staticaddr/staticutil/utils.go
Normal file
|
|
@ -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
|
||||
}
|
||||
236
staticaddr/staticutil/utils_test.go
Normal file
236
staticaddr/staticutil/utils_test.go
Normal file
|
|
@ -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[:]))
|
||||
}
|
||||
}
|
||||
|
|
@ -1,10 +1,10 @@
|
|||
package withdraw
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
|
|
@ -12,6 +12,7 @@ import (
|
|||
"github.com/btcsuite/btcd/btcec/v2/schnorr"
|
||||
"github.com/btcsuite/btcd/btcec/v2/schnorr/musig2"
|
||||
"github.com/btcsuite/btcd/btcutil"
|
||||
"github.com/btcsuite/btcd/btcutil/psbt"
|
||||
"github.com/btcsuite/btcd/chaincfg"
|
||||
"github.com/btcsuite/btcd/chaincfg/chainhash"
|
||||
"github.com/btcsuite/btcd/txscript"
|
||||
|
|
@ -19,9 +20,12 @@ import (
|
|||
"github.com/btcsuite/btcwallet/chain"
|
||||
"github.com/lightninglabs/lndclient"
|
||||
"github.com/lightninglabs/loop/staticaddr/deposit"
|
||||
"github.com/lightninglabs/loop/staticaddr/staticutil"
|
||||
staticaddressrpc "github.com/lightninglabs/loop/swapserverrpc"
|
||||
"github.com/lightningnetwork/lnd/chainntnfs"
|
||||
"github.com/lightningnetwork/lnd/funding"
|
||||
"github.com/lightningnetwork/lnd/input"
|
||||
"github.com/lightningnetwork/lnd/lnrpc"
|
||||
"github.com/lightningnetwork/lnd/lnrpc/walletrpc"
|
||||
"github.com/lightningnetwork/lnd/lntypes"
|
||||
"github.com/lightningnetwork/lnd/lnwallet"
|
||||
|
|
@ -329,7 +333,7 @@ func (m *Manager) WithdrawDeposits(ctx context.Context,
|
|||
|
||||
// If not all passed outpoints are in state Deposited, we'll check if
|
||||
// they are all in state Withdrawing. If they are, then the user is
|
||||
// requesting a fee bump, if not we'll return an error as we only allow
|
||||
// requesting a fee bump, if not, we'll return an error as we only allow
|
||||
// fee bumping deposits in state Withdrawing.
|
||||
if !allDeposited {
|
||||
deposits, allWithdrawing = m.cfg.DepositManager.AllOutpointsActiveDeposits(
|
||||
|
|
@ -406,7 +410,7 @@ func (m *Manager) WithdrawDeposits(ctx context.Context,
|
|||
}
|
||||
}
|
||||
|
||||
finalizedTx, err := m.createFinalizedWithdrawalTx(
|
||||
finalizedTx, _, err := m.CreateFinalizedWithdrawalTx(
|
||||
ctx, deposits, withdrawalAddress, satPerVbyte, amount,
|
||||
)
|
||||
if err != nil {
|
||||
|
|
@ -501,17 +505,30 @@ func (m *Manager) WithdrawDeposits(ctx context.Context,
|
|||
return finalizedTx.TxID(), withdrawalAddress.String(), nil
|
||||
}
|
||||
|
||||
func (m *Manager) createFinalizedWithdrawalTx(ctx context.Context,
|
||||
// CreateFinalizedWithdrawalTx creates and signs a finalized withdrawal
|
||||
// transaction that can be broadcast to the network. It returns the
|
||||
// signed *wire.MsgTx representation and the unsigned psbt.
|
||||
func (m *Manager) CreateFinalizedWithdrawalTx(ctx context.Context,
|
||||
deposits []*deposit.Deposit, withdrawalAddress btcutil.Address,
|
||||
satPerVbyte int64, selectedWithdrawalAmount int64) (*wire.MsgTx,
|
||||
satPerVbyte int64, selectedWithdrawalAmount int64) (*wire.MsgTx, []byte,
|
||||
error) {
|
||||
|
||||
// Create a musig2 session for each deposit.
|
||||
withdrawalSessions, clientNonces, err := m.createMusig2Sessions(
|
||||
ctx, deposits,
|
||||
addrParams, err := m.cfg.AddressManager.GetStaticAddressParameters(ctx)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
staticAddress, err := m.cfg.AddressManager.GetStaticAddress(ctx)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
sessions, clientNonces, idx, err := staticutil.CreateMusig2SessionsPerDeposit(
|
||||
ctx, m.cfg.Signer, deposits, addrParams, staticAddress,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
var withdrawalSweepFeeRate chainfee.SatPerKWeight
|
||||
|
|
@ -521,7 +538,7 @@ func (m *Manager) createFinalizedWithdrawalTx(ctx context.Context,
|
|||
ctx, defaultConfTarget,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, nil, err
|
||||
}
|
||||
} else {
|
||||
withdrawalSweepFeeRate = chainfee.SatPerKVByte(
|
||||
|
|
@ -531,19 +548,23 @@ func (m *Manager) createFinalizedWithdrawalTx(ctx context.Context,
|
|||
|
||||
params, err := m.cfg.AddressManager.GetStaticAddressParameters(ctx)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("couldn't get confirmation height for "+
|
||||
"deposit, %w", err)
|
||||
return nil, nil, fmt.Errorf("couldn't get confirmation "+
|
||||
"height for deposit, %w", err)
|
||||
}
|
||||
|
||||
outpoints := toOutpoints(deposits)
|
||||
prevOuts := m.toPrevOuts(deposits, params.PkScript)
|
||||
withdrawalTx, withdrawAmount, changeAmount, err := m.createWithdrawalTx(
|
||||
ctx, outpoints, prevOuts,
|
||||
prevOuts, err := staticutil.ToPrevOuts(deposits, params.PkScript)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
withdrawalTx, unsignedPsbt, err := m.createWithdrawalTx(
|
||||
ctx, outpoints, deposits, prevOuts,
|
||||
btcutil.Amount(selectedWithdrawalAmount), withdrawalAddress,
|
||||
withdrawalSweepFeeRate,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
// Request the server to sign the withdrawal transaction.
|
||||
|
|
@ -552,45 +573,50 @@ func (m *Manager) createFinalizedWithdrawalTx(ctx context.Context,
|
|||
// expectation that the server just signs the transaction, without
|
||||
// performing fee calculations and dust considerations. The client is
|
||||
// responsible for that.
|
||||
resp, err := m.cfg.StaticAddressServerClient.ServerWithdrawDeposits(
|
||||
ctx, &staticaddressrpc.ServerWithdrawRequest{
|
||||
Outpoints: toPrevoutInfo(outpoints),
|
||||
ClientNonces: clientNonces,
|
||||
ClientSweepAddr: withdrawalAddress.String(),
|
||||
TxFeeRate: uint64(withdrawalSweepFeeRate),
|
||||
WithdrawAmount: int64(withdrawAmount),
|
||||
ChangeAmount: int64(changeAmount),
|
||||
// nolint:lll
|
||||
sigResp, err := m.cfg.StaticAddressServerClient.ServerPsbtWithdrawDeposits(
|
||||
ctx, &staticaddressrpc.ServerPsbtWithdrawRequest{
|
||||
WithdrawalPsbt: unsignedPsbt,
|
||||
DepositToNonces: clientNonces,
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
coopServerNonces, err := toNonces(resp.ServerNonces)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
// Do some sanity checks.
|
||||
txHash := withdrawalTx.TxHash()
|
||||
if !bytes.Equal(txHash.CloneBytes(), sigResp.Txid) {
|
||||
return nil, nil, errors.New("txid doesn't match")
|
||||
}
|
||||
|
||||
if len(sigResp.SigningInfo) != len(deposits) {
|
||||
return nil, nil, errors.New("invalid number of " +
|
||||
"deposit signatures")
|
||||
}
|
||||
|
||||
// Verify 1:1 matching between deposits and SigningInfo entries.
|
||||
// Each deposit must have exactly one corresponding entry in
|
||||
// SigningInfo.
|
||||
for _, d := range deposits {
|
||||
depositKey := d.OutPoint.String()
|
||||
if _, ok := sigResp.SigningInfo[depositKey]; !ok {
|
||||
return nil, nil, fmt.Errorf("missing signature for "+
|
||||
"deposit %s", depositKey)
|
||||
}
|
||||
}
|
||||
|
||||
// Next we'll get our sweep tx signatures.
|
||||
prevOutFetcher := txscript.NewMultiPrevOutFetcher(prevOuts)
|
||||
_, err = m.signMusig2Tx(
|
||||
ctx, prevOutFetcher, outpoints, m.cfg.Signer, withdrawalTx,
|
||||
withdrawalSessions, coopServerNonces,
|
||||
finalizedTx, err := m.signMusig2Tx(
|
||||
ctx, prevOutFetcher, m.cfg.Signer, withdrawalTx, sessions,
|
||||
sigResp.SigningInfo, idx,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
// Now we'll finalize the sweepless sweep transaction.
|
||||
finalizedTx, err := m.finalizeMusig2Transaction(
|
||||
ctx, outpoints, m.cfg.Signer, withdrawalSessions,
|
||||
withdrawalTx, resp.Musig2SweepSigs,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return finalizedTx, nil
|
||||
return finalizedTx, unsignedPsbt, nil
|
||||
}
|
||||
|
||||
func (m *Manager) publishFinalizedWithdrawalTx(ctx context.Context,
|
||||
|
|
@ -726,105 +752,105 @@ func toOutpoints(deposits []*deposit.Deposit) []wire.OutPoint {
|
|||
// signMusig2Tx adds the server nonces to the musig2 sessions and signs the
|
||||
// transaction.
|
||||
func (m *Manager) signMusig2Tx(ctx context.Context,
|
||||
prevOutFetcher *txscript.MultiPrevOutFetcher, outpoints []wire.OutPoint,
|
||||
prevOutFetcher *txscript.MultiPrevOutFetcher,
|
||||
signer lndclient.SignerClient, tx *wire.MsgTx,
|
||||
musig2sessions []*input.MuSig2SessionInfo,
|
||||
counterPartyNonces [][musig2.PubNonceSize]byte) ([][]byte, error) {
|
||||
sessions map[string]*input.MuSig2SessionInfo,
|
||||
sigInfo map[string]*staticaddressrpc.ServerPsbtWithdrawSigningInfo,
|
||||
depositsToIdx map[string]int) (*wire.MsgTx, error) {
|
||||
|
||||
sigHashes := txscript.NewTxSigHashes(tx, prevOutFetcher)
|
||||
sigs := make([][]byte, len(outpoints))
|
||||
|
||||
for idx, outpoint := range outpoints {
|
||||
if !reflect.DeepEqual(tx.TxIn[idx].PreviousOutPoint,
|
||||
outpoint) {
|
||||
// Create our digest.
|
||||
var sigHash [32]byte
|
||||
|
||||
return nil, fmt.Errorf("tx input does not match " +
|
||||
"deposits")
|
||||
if len(sigInfo) != len(depositsToIdx) {
|
||||
return nil, fmt.Errorf("unexpected number of partial " +
|
||||
"signatures from server")
|
||||
}
|
||||
|
||||
for txIndex, input := range tx.TxIn {
|
||||
outpoint := input.PreviousOutPoint.String()
|
||||
if i, ok := depositsToIdx[outpoint]; ok {
|
||||
if i != txIndex {
|
||||
return nil, fmt.Errorf("deposit index maps " +
|
||||
"wrong tx index")
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("tx outpoint not in deposit index map")
|
||||
}
|
||||
|
||||
// We'll now add the nonce to our session and sign the tx.
|
||||
for deposit, sigAndNonce := range sigInfo {
|
||||
session, ok := sessions[deposit]
|
||||
if !ok {
|
||||
return nil, errors.New("session not found")
|
||||
}
|
||||
|
||||
nonce := [musig2.PubNonceSize]byte{}
|
||||
copy(nonce[:], sigAndNonce.Nonce)
|
||||
haveAllNonces, err := signer.MuSig2RegisterNonces(
|
||||
ctx, session.SessionID,
|
||||
[][musig2.PubNonceSize]byte{nonce},
|
||||
)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error registering nonces: "+
|
||||
"%w", err)
|
||||
}
|
||||
|
||||
if !haveAllNonces {
|
||||
return nil, errors.New("expected all nonces to be " +
|
||||
"registered")
|
||||
}
|
||||
|
||||
taprootSigHash, err := txscript.CalcTaprootSignatureHash(
|
||||
sigHashes, txscript.SigHashDefault, tx, idx,
|
||||
prevOutFetcher,
|
||||
sigHashes, txscript.SigHashDefault, tx,
|
||||
depositsToIdx[deposit], prevOutFetcher,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, fmt.Errorf("error calculating taproot "+
|
||||
"sig hash: %w", err)
|
||||
}
|
||||
|
||||
var digest [32]byte
|
||||
copy(digest[:], taprootSigHash)
|
||||
copy(sigHash[:], taprootSigHash)
|
||||
|
||||
// Register the server's nonce before attempting to create our
|
||||
// partial signature.
|
||||
haveAllNonces, err := signer.MuSig2RegisterNonces(
|
||||
ctx, musig2sessions[idx].SessionID,
|
||||
[][musig2.PubNonceSize]byte{counterPartyNonces[idx]},
|
||||
// Sign the tx.
|
||||
_, err = signer.MuSig2Sign(
|
||||
ctx, session.SessionID, sigHash, false,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, fmt.Errorf("error signing tx: %w", err)
|
||||
}
|
||||
|
||||
// Sanity check that we have all the nonces.
|
||||
if !haveAllNonces {
|
||||
return nil, fmt.Errorf("invalid MuSig2 session: " +
|
||||
"nonces missing")
|
||||
}
|
||||
|
||||
// Since our MuSig2 session has all nonces, we can now create
|
||||
// the local partial signature by signing the sig hash.
|
||||
sig, err := signer.MuSig2Sign(
|
||||
ctx, musig2sessions[idx].SessionID, digest, false,
|
||||
// Combine the signature with the client signature.
|
||||
haveAllSigs, sig, err := signer.MuSig2CombineSig(
|
||||
ctx, session.SessionID,
|
||||
[][]byte{sigAndNonce.Sig},
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, fmt.Errorf("error combining signature: "+
|
||||
"%w", err)
|
||||
}
|
||||
|
||||
sigs[idx] = sig
|
||||
}
|
||||
|
||||
return sigs, nil
|
||||
}
|
||||
|
||||
func withdrawalValue(prevOuts map[wire.OutPoint]*wire.TxOut) btcutil.Amount {
|
||||
var totalValue btcutil.Amount
|
||||
for _, prevOut := range prevOuts {
|
||||
totalValue += btcutil.Amount(prevOut.Value)
|
||||
}
|
||||
return totalValue
|
||||
}
|
||||
|
||||
// toNonces converts a byte slice to a 66 byte slice.
|
||||
func toNonces(nonces [][]byte) ([][musig2.PubNonceSize]byte, error) {
|
||||
res := make([][musig2.PubNonceSize]byte, 0, len(nonces))
|
||||
for _, n := range nonces {
|
||||
nonce, err := byteSliceTo66ByteSlice(n)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
if !haveAllSigs {
|
||||
return nil, errors.New("expected all signatures to " +
|
||||
"be combined")
|
||||
}
|
||||
|
||||
res = append(res, nonce)
|
||||
tx.TxIn[depositsToIdx[deposit]].Witness = wire.TxWitness{
|
||||
sig,
|
||||
}
|
||||
}
|
||||
|
||||
return res, nil
|
||||
}
|
||||
|
||||
// byteSliceTo66ByteSlice converts a byte slice to a 66 byte slice.
|
||||
func byteSliceTo66ByteSlice(b []byte) ([musig2.PubNonceSize]byte, error) {
|
||||
if len(b) != musig2.PubNonceSize {
|
||||
return [musig2.PubNonceSize]byte{},
|
||||
fmt.Errorf("invalid byte slice length")
|
||||
}
|
||||
|
||||
var res [musig2.PubNonceSize]byte
|
||||
copy(res[:], b)
|
||||
|
||||
return res, nil
|
||||
return tx, nil
|
||||
}
|
||||
|
||||
func (m *Manager) createWithdrawalTx(ctx context.Context,
|
||||
outpoints []wire.OutPoint, prevOuts map[wire.OutPoint]*wire.TxOut,
|
||||
outpoints []wire.OutPoint, deposits []*deposit.Deposit,
|
||||
prevOuts map[wire.OutPoint]*wire.TxOut,
|
||||
selectedWithdrawalAmount btcutil.Amount, withdrawAddr btcutil.Address,
|
||||
feeRate chainfee.SatPerKWeight) (*wire.MsgTx, btcutil.Amount,
|
||||
btcutil.Amount, error) {
|
||||
feeRate chainfee.SatPerKWeight) (*wire.MsgTx, []byte, error) {
|
||||
|
||||
// First Create the tx.
|
||||
msgTx := wire.NewMsgTx(2)
|
||||
|
|
@ -837,81 +863,23 @@ func (m *Manager) createWithdrawalTx(ctx context.Context,
|
|||
})
|
||||
}
|
||||
|
||||
var (
|
||||
hasChange bool
|
||||
dustLimit = lnwallet.DustLimitForSize(input.P2TRSize)
|
||||
withdrawalAmount btcutil.Amount
|
||||
changeAmount btcutil.Amount
|
||||
withdrawalAmount, changeAmount, err := CalculateWithdrawalTxValues(
|
||||
deposits, selectedWithdrawalAmount, feeRate,
|
||||
withdrawAddr, lnrpc.CommitmentType_UNKNOWN_COMMITMENT_TYPE,
|
||||
)
|
||||
|
||||
// Estimate the transaction weight without change.
|
||||
weight, err := withdrawalTxWeight(len(outpoints), withdrawAddr, false)
|
||||
if err != nil {
|
||||
return nil, 0, 0, err
|
||||
}
|
||||
feeWithoutChange := feeRate.FeeForWeightRoundUp(weight)
|
||||
|
||||
// If the user selected a fraction of the sum of the selected deposits
|
||||
// to withdraw, check if a change output is needed.
|
||||
totalWithdrawalAmount := withdrawalValue(prevOuts)
|
||||
if selectedWithdrawalAmount > 0 {
|
||||
// Estimate the transaction weight with change.
|
||||
weight, err = withdrawalTxWeight(
|
||||
len(outpoints), withdrawAddr, true,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, 0, 0, err
|
||||
}
|
||||
feeWithChange := feeRate.FeeForWeightRoundUp(weight)
|
||||
|
||||
// The available change that can cover fees is the total
|
||||
// selected deposit amount minus the selected withdrawal amount.
|
||||
change := totalWithdrawalAmount - selectedWithdrawalAmount
|
||||
|
||||
switch {
|
||||
case change-feeWithChange >= dustLimit:
|
||||
// If the change can cover the fees without turning into
|
||||
// dust, add a non-dust change output.
|
||||
hasChange = true
|
||||
changeAmount = change - feeWithChange
|
||||
withdrawalAmount = selectedWithdrawalAmount
|
||||
|
||||
case change-feeWithoutChange >= 0:
|
||||
// If the change is dust, we give it to the miners.
|
||||
hasChange = false
|
||||
withdrawalAmount = selectedWithdrawalAmount
|
||||
|
||||
default:
|
||||
// If the fees eat into our withdrawal amount, we fail
|
||||
// the withdrawal.
|
||||
return nil, 0, 0, fmt.Errorf("the change doesn't " +
|
||||
"cover for fees. Consider lowering the fee " +
|
||||
"rate or decrease the withdrawal amount")
|
||||
}
|
||||
} else {
|
||||
// If the user wants to withdraw the full amount, we don't need
|
||||
// a change output.
|
||||
hasChange = false
|
||||
withdrawalAmount = totalWithdrawalAmount - feeWithoutChange
|
||||
return nil, nil, fmt.Errorf("error calculating funding tx "+
|
||||
"values: %w", err)
|
||||
}
|
||||
|
||||
if withdrawalAmount < dustLimit {
|
||||
return nil, 0, 0, fmt.Errorf("withdrawal amount is below " +
|
||||
"dust limit")
|
||||
}
|
||||
|
||||
if changeAmount < 0 {
|
||||
return nil, 0, 0, fmt.Errorf("change amount is negative")
|
||||
}
|
||||
|
||||
// For the users convenience we check that the change amount is lower
|
||||
// For the user's convenience, we check that the change amount is lower
|
||||
// than each input's value. If the change amount is higher than an
|
||||
// input's value, we wouldn't have to include that input into the
|
||||
// input's value, we wouldn't have to include that input in the
|
||||
// transaction, saving fees.
|
||||
for outpoint, txOut := range prevOuts {
|
||||
if changeAmount >= btcutil.Amount(txOut.Value) {
|
||||
return nil, 0, 0, fmt.Errorf("change amount %v is "+
|
||||
"higher than an input value %v of input %v",
|
||||
return nil, nil, fmt.Errorf("change amount %v "+
|
||||
"is higher than an input value %v of input %v",
|
||||
changeAmount, btcutil.Amount(txOut.Value),
|
||||
outpoint)
|
||||
}
|
||||
|
|
@ -919,7 +887,7 @@ func (m *Manager) createWithdrawalTx(ctx context.Context,
|
|||
|
||||
withdrawScript, err := txscript.PayToAddrScript(withdrawAddr)
|
||||
if err != nil {
|
||||
return nil, 0, 0, err
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
// Create the withdrawal output.
|
||||
|
|
@ -928,13 +896,13 @@ func (m *Manager) createWithdrawalTx(ctx context.Context,
|
|||
PkScript: withdrawScript,
|
||||
})
|
||||
|
||||
if hasChange {
|
||||
if changeAmount > 0 {
|
||||
// Send change back to the same static address.
|
||||
staticAddress, err := m.cfg.AddressManager.GetStaticAddress(ctx)
|
||||
if err != nil {
|
||||
log.Errorf("error retrieving taproot address %v", err)
|
||||
|
||||
return nil, 0, 0, fmt.Errorf("withdrawal failed")
|
||||
return nil, nil, fmt.Errorf("withdrawal failed")
|
||||
}
|
||||
|
||||
changeAddress, err := btcutil.NewAddressTaproot(
|
||||
|
|
@ -942,12 +910,12 @@ func (m *Manager) createWithdrawalTx(ctx context.Context,
|
|||
m.cfg.ChainParams,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, 0, 0, err
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
changeScript, err := txscript.PayToAddrScript(changeAddress)
|
||||
if err != nil {
|
||||
return nil, 0, 0, err
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
msgTx.AddTxOut(&wire.TxOut{
|
||||
|
|
@ -956,11 +924,146 @@ func (m *Manager) createWithdrawalTx(ctx context.Context,
|
|||
})
|
||||
}
|
||||
|
||||
return msgTx, withdrawalAmount, changeAmount, nil
|
||||
// Create psbt for the withdrawal.
|
||||
psbtx, err := psbt.NewFromUnsignedTx(msgTx)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
pInputs := make([]psbt.PInput, len(outpoints))
|
||||
for i, op := range outpoints {
|
||||
prevOut := prevOuts[op]
|
||||
pInputs[i] = psbt.PInput{
|
||||
WitnessUtxo: &wire.TxOut{
|
||||
Value: prevOut.Value,
|
||||
PkScript: prevOut.PkScript,
|
||||
},
|
||||
}
|
||||
}
|
||||
psbtx.Inputs = pInputs
|
||||
|
||||
// Serialize the psbt to send it to the client.
|
||||
var psbtBuf bytes.Buffer
|
||||
err = psbtx.Serialize(&psbtBuf)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
return msgTx, psbtBuf.Bytes(), nil
|
||||
}
|
||||
|
||||
// withdrawalFee returns the weight for the withdrawal transaction.
|
||||
func withdrawalTxWeight(numInputs int, sweepAddress btcutil.Address,
|
||||
func CalculateWithdrawalTxValues(deposits []*deposit.Deposit,
|
||||
localAmount btcutil.Amount, feeRate chainfee.SatPerKWeight,
|
||||
withdrawalAddress btcutil.Address,
|
||||
commitmentType lnrpc.CommitmentType) (btcutil.Amount, btcutil.Amount,
|
||||
error) {
|
||||
|
||||
if withdrawalAddress == nil &&
|
||||
commitmentType == lnrpc.CommitmentType_UNKNOWN_COMMITMENT_TYPE {
|
||||
|
||||
return 0, 0, fmt.Errorf("either address or commitment type " +
|
||||
"must be specified")
|
||||
}
|
||||
|
||||
var (
|
||||
err error
|
||||
withdrawalFundingAmt btcutil.Amount
|
||||
changeAmount btcutil.Amount
|
||||
dustLimit = lnwallet.DustLimitForSize(input.P2TRSize)
|
||||
isChannelOpen = commitmentType != lnrpc.CommitmentType_UNKNOWN_COMMITMENT_TYPE
|
||||
)
|
||||
|
||||
totalDepositAmount := btcutil.Amount(0)
|
||||
for _, d := range deposits {
|
||||
totalDepositAmount += d.Value
|
||||
}
|
||||
|
||||
// Estimate the open channel transaction fee without change.
|
||||
hasChange := false
|
||||
weight, err := WithdrawalTxWeight(
|
||||
len(deposits), withdrawalAddress, commitmentType, hasChange,
|
||||
)
|
||||
if err != nil {
|
||||
return 0, 0, err
|
||||
}
|
||||
feeWithoutChange := feeRate.FeeForWeight(weight)
|
||||
|
||||
// If the user selected a local amount for the channel, check if a
|
||||
// change output is needed.
|
||||
if localAmount > 0 {
|
||||
// Estimate the transaction weight with change.
|
||||
hasChange = true
|
||||
weightWithChange, err := WithdrawalTxWeight(
|
||||
len(deposits), withdrawalAddress, commitmentType,
|
||||
hasChange,
|
||||
)
|
||||
if err != nil {
|
||||
return 0, 0, err
|
||||
}
|
||||
feeWithChange := feeRate.FeeForWeight(weightWithChange)
|
||||
|
||||
// The available change that can cover fees is the total
|
||||
// selected deposit amount minus the local channel amount.
|
||||
change := totalDepositAmount - localAmount
|
||||
|
||||
switch {
|
||||
case change-feeWithChange >= dustLimit:
|
||||
// If the change can cover the fees without turning into
|
||||
// dust, add a non-dust change output.
|
||||
changeAmount = change - feeWithChange
|
||||
withdrawalFundingAmt = localAmount
|
||||
|
||||
case change-feeWithoutChange >= 0:
|
||||
// If the change is dust, we give it to the miners.
|
||||
withdrawalFundingAmt = localAmount
|
||||
|
||||
default:
|
||||
// If the fees eat into our local channel amount, we
|
||||
// fail to open the channel.
|
||||
return 0, 0, fmt.Errorf("the change doesn't " +
|
||||
"cover for fees. Consider lowering the fee " +
|
||||
"rate or decrease the local amount")
|
||||
}
|
||||
} else {
|
||||
// If the user wants to open the channel with the total value of
|
||||
// deposits, we don't need a change output.
|
||||
withdrawalFundingAmt = totalDepositAmount - feeWithoutChange
|
||||
}
|
||||
|
||||
if withdrawalFundingAmt < dustLimit {
|
||||
return 0, 0, fmt.Errorf("withdrawal amount is below dust limit")
|
||||
}
|
||||
|
||||
if changeAmount < 0 {
|
||||
return 0, 0, fmt.Errorf("change amount is negative")
|
||||
}
|
||||
|
||||
// Ensure that the channel funding amount is at least in the amount of
|
||||
// lnd's minimum channel size.
|
||||
if isChannelOpen && withdrawalFundingAmt < funding.MinChanFundingSize {
|
||||
return 0, 0, fmt.Errorf("channel funding amount %v is lower "+
|
||||
"than the minimum channel funding size %v",
|
||||
withdrawalFundingAmt, funding.MinChanFundingSize)
|
||||
}
|
||||
|
||||
// For the user's convenience, we check that the change amount is lower
|
||||
// than each input's value. If the change amount is higher than an
|
||||
// input's value, we wouldn't have to include that input in the
|
||||
// transaction, saving fees.
|
||||
for _, d := range deposits {
|
||||
if changeAmount >= d.Value {
|
||||
return 0, 0, fmt.Errorf("change amount %v is "+
|
||||
"higher than an input value %v of input %v",
|
||||
changeAmount, d.Value, d.OutPoint.String())
|
||||
}
|
||||
}
|
||||
|
||||
return withdrawalFundingAmt, changeAmount, nil
|
||||
}
|
||||
|
||||
// WithdrawalTxWeight returns the weight for the withdrawal transaction.
|
||||
func WithdrawalTxWeight(numInputs int, sweepAddress btcutil.Address,
|
||||
commitmentType lnrpc.CommitmentType,
|
||||
hasChange bool) (lntypes.WeightUnit, error) {
|
||||
|
||||
var weightEstimator input.TxWeightEstimator
|
||||
|
|
@ -970,17 +1073,30 @@ func withdrawalTxWeight(numInputs int, sweepAddress btcutil.Address,
|
|||
)
|
||||
}
|
||||
|
||||
// Get the weight of the sweep output.
|
||||
switch sweepAddress.(type) {
|
||||
case *btcutil.AddressWitnessPubKeyHash:
|
||||
weightEstimator.AddP2WKHOutput()
|
||||
if commitmentType != lnrpc.CommitmentType_UNKNOWN_COMMITMENT_TYPE {
|
||||
switch commitmentType {
|
||||
case lnrpc.CommitmentType_SIMPLE_TAPROOT:
|
||||
weightEstimator.AddP2TROutput()
|
||||
|
||||
case *btcutil.AddressTaproot:
|
||||
weightEstimator.AddP2TROutput()
|
||||
default:
|
||||
weightEstimator.AddP2WSHOutput()
|
||||
}
|
||||
} else {
|
||||
// Get the weight of the sweep output.
|
||||
switch sweepAddress.(type) {
|
||||
case *btcutil.AddressWitnessPubKeyHash:
|
||||
weightEstimator.AddP2WKHOutput()
|
||||
|
||||
default:
|
||||
return 0, fmt.Errorf("invalid sweep address type %T",
|
||||
sweepAddress)
|
||||
case *btcutil.AddressWitnessScriptHash:
|
||||
weightEstimator.AddP2WSHOutput()
|
||||
|
||||
case *btcutil.AddressTaproot:
|
||||
weightEstimator.AddP2TROutput()
|
||||
|
||||
default:
|
||||
return 0, fmt.Errorf("invalid sweep address type %T",
|
||||
sweepAddress)
|
||||
}
|
||||
}
|
||||
|
||||
// If there's a change output add the weight of the static address.
|
||||
|
|
@ -991,120 +1107,6 @@ func withdrawalTxWeight(numInputs int, sweepAddress btcutil.Address,
|
|||
return weightEstimator.Weight(), nil
|
||||
}
|
||||
|
||||
// finalizeMusig2Transaction creates the finalized transactions for either
|
||||
// the htlc or the cooperative close.
|
||||
func (m *Manager) finalizeMusig2Transaction(ctx context.Context,
|
||||
outpoints []wire.OutPoint, signer lndclient.SignerClient,
|
||||
musig2Sessions []*input.MuSig2SessionInfo,
|
||||
tx *wire.MsgTx, serverSigs [][]byte) (*wire.MsgTx, error) {
|
||||
|
||||
for idx := range outpoints {
|
||||
haveAllSigs, finalSig, err := signer.MuSig2CombineSig(
|
||||
ctx, musig2Sessions[idx].SessionID,
|
||||
[][]byte{serverSigs[idx]},
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if !haveAllSigs {
|
||||
return nil, fmt.Errorf("missing sigs")
|
||||
}
|
||||
|
||||
tx.TxIn[idx].Witness = wire.TxWitness{finalSig}
|
||||
}
|
||||
|
||||
return tx, nil
|
||||
}
|
||||
|
||||
func toPrevoutInfo(outpoints []wire.OutPoint) []*staticaddressrpc.PrevoutInfo {
|
||||
var result []*staticaddressrpc.PrevoutInfo
|
||||
for _, o := range outpoints {
|
||||
outP := o
|
||||
outpoint := &staticaddressrpc.PrevoutInfo{
|
||||
TxidBytes: outP.Hash[:],
|
||||
OutputIndex: outP.Index,
|
||||
}
|
||||
result = append(result, outpoint)
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// createMusig2Sessions creates a musig2 session for a number of deposits.
|
||||
func (m *Manager) createMusig2Sessions(ctx context.Context,
|
||||
deposits []*deposit.Deposit) ([]*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 := m.createMusig2Session(ctx)
|
||||
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 (m *Manager) createMusig2Session(ctx context.Context) (
|
||||
*input.MuSig2SessionInfo, error) {
|
||||
|
||||
addressParams, err := m.cfg.AddressManager.GetStaticAddressParameters(
|
||||
ctx,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("couldn't get confirmation height for "+
|
||||
"deposit, %w", err)
|
||||
}
|
||||
|
||||
signers := [][]byte{
|
||||
addressParams.ClientPubkey.SerializeCompressed(),
|
||||
addressParams.ServerPubkey.SerializeCompressed(),
|
||||
}
|
||||
|
||||
address, err := m.cfg.AddressManager.GetStaticAddress(ctx)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("couldn't get confirmation height for "+
|
||||
"deposit, %w", err)
|
||||
}
|
||||
|
||||
expiryLeaf := address.TimeoutLeaf
|
||||
|
||||
rootHash := expiryLeaf.TapHash()
|
||||
|
||||
return m.cfg.Signer.MuSig2CreateSession(
|
||||
ctx, input.MuSig2Version100RC2, &addressParams.KeyLocator,
|
||||
signers, lndclient.MuSig2TaprootTweakOpt(rootHash[:], false),
|
||||
)
|
||||
}
|
||||
|
||||
func (m *Manager) toPrevOuts(deposits []*deposit.Deposit,
|
||||
pkScript []byte) map[wire.OutPoint]*wire.TxOut {
|
||||
|
||||
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,
|
||||
}
|
||||
prevOuts[outpoint] = txOut
|
||||
}
|
||||
|
||||
return prevOuts
|
||||
}
|
||||
|
||||
func (m *Manager) republishWithdrawals(ctx context.Context) error {
|
||||
m.mu.Lock()
|
||||
txns := make([]*wire.MsgTx, 0, len(m.finalizedWithdrawalTxns))
|
||||
|
|
|
|||
|
|
@ -1,8 +1,22 @@
|
|||
package withdraw
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/btcsuite/btcd/btcutil"
|
||||
"github.com/btcsuite/btcd/chaincfg"
|
||||
"github.com/btcsuite/btcd/chaincfg/chainhash"
|
||||
"github.com/btcsuite/btcd/txscript"
|
||||
"github.com/btcsuite/btcd/wire"
|
||||
"github.com/lightninglabs/loop/staticaddr/deposit"
|
||||
"github.com/lightninglabs/loop/swapserverrpc"
|
||||
"github.com/lightninglabs/loop/test"
|
||||
"github.com/lightningnetwork/lnd/funding"
|
||||
"github.com/lightningnetwork/lnd/input"
|
||||
"github.com/lightningnetwork/lnd/lnrpc"
|
||||
"github.com/lightningnetwork/lnd/lnwallet"
|
||||
"github.com/lightningnetwork/lnd/lnwallet/chainfee"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
|
|
@ -19,3 +33,576 @@ func TestNewManagerHeightValidation(t *testing.T) {
|
|||
require.NoError(t, err)
|
||||
require.NotNil(t, manager)
|
||||
}
|
||||
|
||||
// TestSignMusig2Tx_MissingSigningInfo tests that signMusig2Tx should error
|
||||
// when sigInfo is missing an entry for one of the deposits.
|
||||
//
|
||||
// This test documents expected behavior. The function should validate that
|
||||
// len(sigInfo) == len(sessions) and all sessions have corresponding sigInfo
|
||||
// entries before attempting to sign, returning an error if validation fails.
|
||||
func TestSignMusig2Tx_MissingSigningInfo(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
// Create a dummy transaction with two inputs.
|
||||
tx := wire.NewMsgTx(2)
|
||||
tx.AddTxIn(&wire.TxIn{
|
||||
PreviousOutPoint: wire.OutPoint{
|
||||
Hash: [32]byte{1},
|
||||
Index: 0,
|
||||
},
|
||||
})
|
||||
tx.AddTxIn(&wire.TxIn{
|
||||
PreviousOutPoint: wire.OutPoint{
|
||||
Hash: [32]byte{2},
|
||||
Index: 0,
|
||||
},
|
||||
})
|
||||
|
||||
// Add a dummy output with a simple pkScript.
|
||||
pkScript := []byte{
|
||||
0x51, 0x20, // OP_1 OP_PUSHBYTES_32
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
}
|
||||
|
||||
tx.AddTxOut(&wire.TxOut{
|
||||
Value: 10000,
|
||||
PkScript: pkScript,
|
||||
})
|
||||
|
||||
// Create deposit keys for both inputs.
|
||||
deposit1Key := "0100000000000000000000000000000000000000000000000000000000000000:0"
|
||||
deposit2Key := "0200000000000000000000000000000000000000000000000000000000000000:0"
|
||||
|
||||
// Create sessions for both deposits.
|
||||
sessions := map[string]*input.MuSig2SessionInfo{
|
||||
deposit1Key: {
|
||||
SessionID: [32]byte{1},
|
||||
},
|
||||
deposit2Key: {
|
||||
SessionID: [32]byte{2},
|
||||
},
|
||||
}
|
||||
|
||||
// Create sigInfo with only one entry (missing deposit2).
|
||||
sigInfo := map[string]*swapserverrpc.ServerPsbtWithdrawSigningInfo{
|
||||
deposit1Key: {
|
||||
Nonce: make([]byte, 66),
|
||||
Sig: make([]byte, 64),
|
||||
},
|
||||
}
|
||||
|
||||
// Create depositsToIdx mapping both deposits.
|
||||
depositsToIdx := map[string]int{
|
||||
deposit1Key: 0,
|
||||
deposit2Key: 1,
|
||||
}
|
||||
|
||||
// Create prevOutFetcher.
|
||||
prevOuts := map[wire.OutPoint]*wire.TxOut{
|
||||
tx.TxIn[0].PreviousOutPoint: {
|
||||
Value: 5000,
|
||||
PkScript: pkScript,
|
||||
},
|
||||
tx.TxIn[1].PreviousOutPoint: {
|
||||
Value: 5000,
|
||||
PkScript: pkScript,
|
||||
},
|
||||
}
|
||||
prevOutFetcher := txscript.NewMultiPrevOutFetcher(prevOuts)
|
||||
|
||||
// Create a mock signer.
|
||||
lnd := test.NewMockLnd()
|
||||
signer := lnd.Signer
|
||||
|
||||
// Create a minimal manager.
|
||||
m := &Manager{
|
||||
cfg: &ManagerConfig{
|
||||
Signer: signer,
|
||||
},
|
||||
}
|
||||
|
||||
// Call signMusig2Tx - it should error because sigInfo is missing
|
||||
// an entry for deposit2.
|
||||
//
|
||||
// The function should validate that:
|
||||
// 1. len(sigInfo) == len(sessions) == len(tx.TxIn)
|
||||
// 2. All keys in sessions exist in sigInfo
|
||||
// 3. No partial signing is allowed
|
||||
//
|
||||
// This test verifies that the function errors when sigInfo is
|
||||
// incomplete, preventing a partially signed transaction.
|
||||
ctx := context.Background()
|
||||
_, err := m.signMusig2Tx(
|
||||
ctx, prevOutFetcher, signer, tx, sessions, sigInfo,
|
||||
depositsToIdx,
|
||||
)
|
||||
|
||||
// Expect an error. The function should validate that sigInfo has
|
||||
// entries for all sessions before attempting to sign.
|
||||
require.ErrorContains(t, err, "unexpected number of partial "+
|
||||
"signatures from server")
|
||||
}
|
||||
|
||||
// TestSignMusig2Tx_MismatchedIndex tests that signMusig2Tx should error when
|
||||
// sigInfo has all expected keys but one maps to the wrong index in depositsToIdx.
|
||||
//
|
||||
// This test documents expected behavior. The function should validate that
|
||||
// each deposit maps to a unique index in [0, len(tx.TxIn)-1] before signing,
|
||||
// returning an error if indices conflict or are out of bounds.
|
||||
func TestSignMusig2Tx_MismatchedIndex(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
// Create a dummy transaction with two inputs.
|
||||
tx := wire.NewMsgTx(2)
|
||||
tx.AddTxIn(&wire.TxIn{
|
||||
PreviousOutPoint: wire.OutPoint{
|
||||
Hash: [32]byte{1},
|
||||
Index: 0,
|
||||
},
|
||||
})
|
||||
tx.AddTxIn(&wire.TxIn{
|
||||
PreviousOutPoint: wire.OutPoint{
|
||||
Hash: [32]byte{2},
|
||||
Index: 0,
|
||||
},
|
||||
})
|
||||
|
||||
// Add a dummy output with a simple pkScript.
|
||||
pkScript := []byte{
|
||||
0x51, 0x20, // OP_1 OP_PUSHBYTES_32
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
}
|
||||
|
||||
tx.AddTxOut(&wire.TxOut{
|
||||
Value: 10000,
|
||||
PkScript: pkScript,
|
||||
})
|
||||
|
||||
// Create deposit keys for both inputs.
|
||||
deposit1Key := "0000000000000000000000000000000000000000000000000000000000000001:0"
|
||||
deposit2Key := "0000000000000000000000000000000000000000000000000000000000000002:0"
|
||||
|
||||
// Create sessions for both deposits.
|
||||
sessions := map[string]*input.MuSig2SessionInfo{
|
||||
deposit1Key: {
|
||||
SessionID: [32]byte{1},
|
||||
},
|
||||
deposit2Key: {
|
||||
SessionID: [32]byte{2},
|
||||
},
|
||||
}
|
||||
|
||||
// Create sigInfo with both entries.
|
||||
sigInfo := map[string]*swapserverrpc.ServerPsbtWithdrawSigningInfo{
|
||||
deposit1Key: {
|
||||
Nonce: make([]byte, 66),
|
||||
Sig: make([]byte, 64),
|
||||
},
|
||||
deposit2Key: {
|
||||
Nonce: make([]byte, 66),
|
||||
Sig: make([]byte, 64),
|
||||
},
|
||||
}
|
||||
|
||||
// Create depositsToIdx with WRONG mapping for deposit2.
|
||||
// deposit2 should map to index 1, but we map it to 0.
|
||||
depositsToIdx := map[string]int{
|
||||
deposit1Key: 0,
|
||||
deposit2Key: 0, // Wrong! Should be 1.
|
||||
}
|
||||
|
||||
// Create prevOutFetcher.
|
||||
prevOuts := map[wire.OutPoint]*wire.TxOut{
|
||||
tx.TxIn[0].PreviousOutPoint: {
|
||||
Value: 5000,
|
||||
PkScript: pkScript,
|
||||
},
|
||||
tx.TxIn[1].PreviousOutPoint: {
|
||||
Value: 5000,
|
||||
PkScript: pkScript,
|
||||
},
|
||||
}
|
||||
prevOutFetcher := txscript.NewMultiPrevOutFetcher(prevOuts)
|
||||
|
||||
// Create a mock signer.
|
||||
lnd := test.NewMockLnd()
|
||||
signer := lnd.Signer
|
||||
|
||||
// Create a minimal manager.
|
||||
m := &Manager{
|
||||
cfg: &ManagerConfig{
|
||||
Signer: signer,
|
||||
},
|
||||
}
|
||||
|
||||
// Call signMusig2Tx - it should error because depositsToIdx has
|
||||
// a mismatched index for deposit2.
|
||||
//
|
||||
// The function should validate that:
|
||||
// 1. Each deposit in depositsToIdx maps to a unique index
|
||||
// 2. All indices from 0 to len(tx.TxIn)-1 are covered exactly once
|
||||
// 3. No index is used twice (which would cause signature overwrites)
|
||||
//
|
||||
// In this case, both deposit1 and deposit2 map to index 0, which
|
||||
// is invalid. The second deposit would overwrite the witness of
|
||||
// the first input, resulting in an invalid transaction.
|
||||
ctx := context.Background()
|
||||
_, err := m.signMusig2Tx(
|
||||
ctx, prevOutFetcher, signer, tx, sessions, sigInfo,
|
||||
depositsToIdx,
|
||||
)
|
||||
|
||||
// Expect an error. The function should validate index uniqueness.
|
||||
require.ErrorContains(t, err, "deposit index maps wrong tx index")
|
||||
}
|
||||
|
||||
// TestSignMusig2Tx_MissingOutpointInDepositMap tests that signMusig2Tx errors
|
||||
// when a transaction input's outpoint is not present in depositsToIdx map.
|
||||
//
|
||||
// This test validates that the function checks all transaction inputs have
|
||||
// corresponding entries in the depositsToIdx map before signing.
|
||||
func TestSignMusig2Tx_MissingOutpointInDepositMap(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
// Create a dummy transaction with two inputs.
|
||||
tx := wire.NewMsgTx(2)
|
||||
tx.AddTxIn(&wire.TxIn{
|
||||
PreviousOutPoint: wire.OutPoint{
|
||||
Hash: [32]byte{1},
|
||||
Index: 0,
|
||||
},
|
||||
})
|
||||
tx.AddTxIn(&wire.TxIn{
|
||||
PreviousOutPoint: wire.OutPoint{
|
||||
Hash: [32]byte{2},
|
||||
Index: 0,
|
||||
},
|
||||
})
|
||||
|
||||
// Add a dummy output with a simple pkScript.
|
||||
pkScript := []byte{
|
||||
0x51, 0x20, // OP_1 OP_PUSHBYTES_32
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
}
|
||||
|
||||
tx.AddTxOut(&wire.TxOut{
|
||||
Value: 10000,
|
||||
PkScript: pkScript,
|
||||
})
|
||||
|
||||
// Create deposit keys for both inputs.
|
||||
deposit1Key := "0100000000000000000000000000000000000000000000000000000000000000:0"
|
||||
deposit2Key := "0200000000000000000000000000000000000000000000000000000000000000:0"
|
||||
|
||||
// Create sessions for both deposits.
|
||||
sessions := map[string]*input.MuSig2SessionInfo{
|
||||
deposit1Key: {
|
||||
SessionID: [32]byte{1},
|
||||
},
|
||||
deposit2Key: {
|
||||
SessionID: [32]byte{2},
|
||||
},
|
||||
}
|
||||
|
||||
// Create sigInfo with both entries.
|
||||
sigInfo := map[string]*swapserverrpc.ServerPsbtWithdrawSigningInfo{
|
||||
deposit1Key: {
|
||||
Nonce: make([]byte, 66),
|
||||
Sig: make([]byte, 64),
|
||||
},
|
||||
deposit2Key: {
|
||||
Nonce: make([]byte, 66),
|
||||
Sig: make([]byte, 64),
|
||||
},
|
||||
}
|
||||
|
||||
// Create a third deposit key that doesn't correspond to any tx input.
|
||||
deposit3Key := "0300000000000000000000000000000000000000000000000000000000000000:0"
|
||||
|
||||
// Create depositsToIdx with deposit1 at correct index, but deposit3
|
||||
// (which doesn't exist as a tx input) instead of deposit2.
|
||||
// This means when the function iterates through tx.TxIn, it will find
|
||||
// deposit1 in the map, but deposit2 (the actual second input) won't
|
||||
// be found.
|
||||
depositsToIdx := map[string]int{
|
||||
deposit1Key: 0,
|
||||
deposit3Key: 1, // Wrong key - this isn't a real tx input
|
||||
}
|
||||
|
||||
// Create prevOutFetcher.
|
||||
prevOuts := map[wire.OutPoint]*wire.TxOut{
|
||||
tx.TxIn[0].PreviousOutPoint: {
|
||||
Value: 5000,
|
||||
PkScript: pkScript,
|
||||
},
|
||||
tx.TxIn[1].PreviousOutPoint: {
|
||||
Value: 5000,
|
||||
PkScript: pkScript,
|
||||
},
|
||||
}
|
||||
prevOutFetcher := txscript.NewMultiPrevOutFetcher(prevOuts)
|
||||
|
||||
// Create a mock signer.
|
||||
lnd := test.NewMockLnd()
|
||||
signer := lnd.Signer
|
||||
|
||||
// Create a minimal manager.
|
||||
m := &Manager{
|
||||
cfg: &ManagerConfig{
|
||||
Signer: signer,
|
||||
},
|
||||
}
|
||||
|
||||
// Call signMusig2Tx - it should error because the second transaction
|
||||
// input's outpoint is not in depositsToIdx map.
|
||||
//
|
||||
// The function should validate that every transaction input has a
|
||||
// corresponding entry in depositsToIdx before attempting to sign.
|
||||
ctx := context.Background()
|
||||
_, err := m.signMusig2Tx(
|
||||
ctx, prevOutFetcher, signer, tx, sessions, sigInfo,
|
||||
depositsToIdx,
|
||||
)
|
||||
|
||||
// Expect an error indicating the missing outpoint.
|
||||
require.ErrorContains(t, err, "tx outpoint not in deposit index map")
|
||||
}
|
||||
|
||||
// TestCalculateWithdrawalTxValues tests various edge cases in withdrawal
|
||||
// transaction value calculations.
|
||||
func TestCalculateWithdrawalTxValues(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
// Create a taproot address for withdrawal.
|
||||
taprootAddr, err := btcutil.NewAddressTaproot(
|
||||
make([]byte, 32), &chaincfg.RegressionNetParams,
|
||||
)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Standard fee rate for testing.
|
||||
feeRate := chainfee.SatPerKWeight(1000)
|
||||
|
||||
// Helper to create deposits.
|
||||
createDeposit := func(value btcutil.Amount, idx uint32) *deposit.Deposit {
|
||||
hash := chainhash.Hash{}
|
||||
hash[0] = byte(idx)
|
||||
return &deposit.Deposit{
|
||||
OutPoint: wire.OutPoint{
|
||||
Hash: hash,
|
||||
Index: idx,
|
||||
},
|
||||
Value: value,
|
||||
}
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
deposits []*deposit.Deposit
|
||||
localAmount btcutil.Amount
|
||||
feeRate chainfee.SatPerKWeight
|
||||
withdrawAddr btcutil.Address
|
||||
commitmentType lnrpc.CommitmentType
|
||||
expectedErr string
|
||||
expectDustFee bool // change is dust, given to miners
|
||||
}{
|
||||
{
|
||||
name: "neither address nor commitment type specified",
|
||||
deposits: []*deposit.Deposit{
|
||||
createDeposit(100000, 0),
|
||||
},
|
||||
localAmount: 0,
|
||||
feeRate: feeRate,
|
||||
withdrawAddr: nil,
|
||||
commitmentType: lnrpc.CommitmentType_UNKNOWN_COMMITMENT_TYPE,
|
||||
expectedErr: "either address or commitment type must be specified",
|
||||
},
|
||||
{
|
||||
name: "change is dust - given to miners",
|
||||
deposits: []*deposit.Deposit{
|
||||
createDeposit(100000, 0),
|
||||
},
|
||||
// Set localAmount such that change after feeWithChange
|
||||
// would be dust, but change after feeWithoutChange >= 0.
|
||||
// This triggers case: change-feeWithoutChange >= 0
|
||||
localAmount: 99300, // Leaves ~700 sats which is dust
|
||||
feeRate: feeRate,
|
||||
withdrawAddr: taprootAddr,
|
||||
commitmentType: lnrpc.CommitmentType_UNKNOWN_COMMITMENT_TYPE,
|
||||
expectedErr: "",
|
||||
expectDustFee: true,
|
||||
},
|
||||
{
|
||||
name: "insufficient funds after dust and fee",
|
||||
deposits: []*deposit.Deposit{
|
||||
createDeposit(1000, 0),
|
||||
},
|
||||
localAmount: 900,
|
||||
feeRate: feeRate,
|
||||
withdrawAddr: taprootAddr,
|
||||
commitmentType: lnrpc.CommitmentType_UNKNOWN_COMMITMENT_TYPE,
|
||||
expectedErr: "doesn't cover for fees",
|
||||
},
|
||||
{
|
||||
name: "negative change after fees",
|
||||
deposits: []*deposit.Deposit{
|
||||
createDeposit(10000, 0),
|
||||
},
|
||||
localAmount: 15000,
|
||||
feeRate: feeRate,
|
||||
withdrawAddr: taprootAddr,
|
||||
commitmentType: lnrpc.CommitmentType_UNKNOWN_COMMITMENT_TYPE,
|
||||
expectedErr: "doesn't cover for fees",
|
||||
},
|
||||
{
|
||||
name: "min channel size guard - below minimum",
|
||||
deposits: []*deposit.Deposit{
|
||||
createDeposit(funding.MinChanFundingSize-10, 0),
|
||||
},
|
||||
localAmount: 0,
|
||||
feeRate: feeRate,
|
||||
withdrawAddr: nil,
|
||||
commitmentType: lnrpc.CommitmentType_SIMPLE_TAPROOT,
|
||||
expectedErr: "is lower than the minimum channel " +
|
||||
"funding size",
|
||||
},
|
||||
{
|
||||
name: "min channel size guard - exactly minimum",
|
||||
deposits: []*deposit.Deposit{
|
||||
createDeposit(funding.MinChanFundingSize+1000, 0),
|
||||
},
|
||||
localAmount: 0,
|
||||
feeRate: feeRate,
|
||||
withdrawAddr: nil,
|
||||
commitmentType: lnrpc.CommitmentType_SIMPLE_TAPROOT,
|
||||
expectedErr: "",
|
||||
},
|
||||
{
|
||||
name: "withdrawal amount below dust limit",
|
||||
deposits: []*deposit.Deposit{
|
||||
createDeposit(400, 0),
|
||||
},
|
||||
localAmount: 0,
|
||||
feeRate: feeRate,
|
||||
withdrawAddr: taprootAddr,
|
||||
commitmentType: lnrpc.CommitmentType_UNKNOWN_COMMITMENT_TYPE,
|
||||
expectedErr: "below dust limit",
|
||||
},
|
||||
{
|
||||
name: "change higher than input value",
|
||||
deposits: []*deposit.Deposit{
|
||||
createDeposit(10000, 0),
|
||||
createDeposit(5000, 1),
|
||||
},
|
||||
localAmount: 5000,
|
||||
feeRate: chainfee.SatPerKWeight(100),
|
||||
withdrawAddr: taprootAddr,
|
||||
commitmentType: lnrpc.CommitmentType_UNKNOWN_COMMITMENT_TYPE,
|
||||
expectedErr: "change amount",
|
||||
},
|
||||
{
|
||||
name: "successful withdrawal with change",
|
||||
deposits: []*deposit.Deposit{
|
||||
createDeposit(100000, 0),
|
||||
},
|
||||
localAmount: 50000,
|
||||
feeRate: feeRate,
|
||||
withdrawAddr: taprootAddr,
|
||||
commitmentType: lnrpc.CommitmentType_UNKNOWN_COMMITMENT_TYPE,
|
||||
expectedErr: "",
|
||||
},
|
||||
{
|
||||
name: "successful withdrawal no change",
|
||||
deposits: []*deposit.Deposit{
|
||||
createDeposit(100000, 0),
|
||||
},
|
||||
localAmount: 0,
|
||||
feeRate: feeRate,
|
||||
withdrawAddr: taprootAddr,
|
||||
commitmentType: lnrpc.CommitmentType_UNKNOWN_COMMITMENT_TYPE,
|
||||
expectedErr: "",
|
||||
},
|
||||
{
|
||||
name: "successful channel open above min size",
|
||||
deposits: []*deposit.Deposit{
|
||||
createDeposit(funding.MinChanFundingSize*2, 0),
|
||||
},
|
||||
localAmount: 0,
|
||||
feeRate: feeRate,
|
||||
withdrawAddr: nil,
|
||||
commitmentType: lnrpc.CommitmentType_SIMPLE_TAPROOT,
|
||||
expectedErr: "",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
withdrawAmt, changeAmt, err := CalculateWithdrawalTxValues(
|
||||
tc.deposits, tc.localAmount, tc.feeRate,
|
||||
tc.withdrawAddr, tc.commitmentType,
|
||||
)
|
||||
|
||||
if tc.expectedErr != "" {
|
||||
require.Error(t, err)
|
||||
require.ErrorContains(t, err, tc.expectedErr)
|
||||
return
|
||||
}
|
||||
|
||||
require.NoError(t, err)
|
||||
require.Greater(t, withdrawAmt, btcutil.Amount(0))
|
||||
require.GreaterOrEqual(t, changeAmt, btcutil.Amount(0))
|
||||
|
||||
// Verify that withdrawal amount meets dust threshold.
|
||||
dustLimit := lnwallet.DustLimitForSize(input.P2TRSize)
|
||||
require.GreaterOrEqual(t, withdrawAmt, dustLimit)
|
||||
|
||||
// If this is a channel open, verify min channel size.
|
||||
if tc.commitmentType != lnrpc.CommitmentType_UNKNOWN_COMMITMENT_TYPE {
|
||||
require.GreaterOrEqual(
|
||||
t, withdrawAmt, funding.MinChanFundingSize,
|
||||
)
|
||||
}
|
||||
|
||||
// If expecting dust to be given to miners, verify
|
||||
// changeAmt is 0.
|
||||
if tc.expectDustFee {
|
||||
require.Equal(t, btcutil.Amount(0), changeAmt,
|
||||
"change should be 0 when dust is given to miners")
|
||||
}
|
||||
|
||||
// Verify total accounting: inputs = withdrawal + change + fees.
|
||||
totalInputs := btcutil.Amount(0)
|
||||
for _, d := range tc.deposits {
|
||||
totalInputs += d.Value
|
||||
}
|
||||
|
||||
hasChange := changeAmt > 0
|
||||
weight, err := WithdrawalTxWeight(
|
||||
len(tc.deposits), tc.withdrawAddr,
|
||||
tc.commitmentType, hasChange,
|
||||
)
|
||||
require.NoError(t, err)
|
||||
|
||||
fee := tc.feeRate.FeeForWeight(weight)
|
||||
|
||||
// When dust is given to miners, the "fee" includes both
|
||||
// the transaction fee and the dust amount.
|
||||
if tc.expectDustFee {
|
||||
// Total should equal withdrawal + implicit fee (including dust)
|
||||
implicitFee := totalInputs - withdrawAmt - changeAmt
|
||||
require.Greater(t, implicitFee, fee,
|
||||
"implicit fee should be greater than tx fee when dust is given to miners")
|
||||
} else {
|
||||
require.Equal(t, totalInputs, withdrawAmt+changeAmt+fee)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -26,8 +26,20 @@ service StaticAddressServer {
|
|||
// ServerWithdrawDeposits allows to cooperatively sweep deposits that
|
||||
// haven't timed out yet to the client's wallet. The server will generate
|
||||
// the partial sigs for the client's selected deposits.
|
||||
//
|
||||
// Deprecated: use ServerPsbtWithdrawDeposits instead.
|
||||
rpc ServerWithdrawDeposits (ServerWithdrawRequest)
|
||||
returns (ServerWithdrawResponse);
|
||||
returns (ServerWithdrawResponse) {
|
||||
option deprecated = true;
|
||||
};
|
||||
|
||||
// ServerPsbtWithdrawDeposits allows to cooperatively sweep deposits that
|
||||
// haven't timed out yet to the client's wallet. In contrast to
|
||||
// ServerWithdrawDeposits which provides the paramters to form the
|
||||
// withdrawal transaction, this service method will provide a psbt which
|
||||
// is signed by the server.
|
||||
rpc ServerPsbtWithdrawDeposits (ServerPsbtWithdrawRequest)
|
||||
returns (ServerPsbtWithdrawResponse);
|
||||
|
||||
// ServerStaticAddressLoopIn initiates a static address loop-in swap. The
|
||||
// server will respond with htlc details that the client can use to
|
||||
|
|
@ -67,7 +79,10 @@ message ServerAddressParameters {
|
|||
uint32 expiry = 2;
|
||||
}
|
||||
|
||||
// Deprecated: use ServerPsbtWithdrawRequest instead.
|
||||
message ServerWithdrawRequest {
|
||||
option deprecated = true;
|
||||
|
||||
// The deposit outpoints the client wishes to withdraw.
|
||||
repeated PrevoutInfo outpoints = 1;
|
||||
|
||||
|
|
@ -91,14 +106,43 @@ message ServerWithdrawRequest {
|
|||
int64 change_amount = 6;
|
||||
}
|
||||
|
||||
// Deprecated: use ServerPsbtWithdrawResponse instead.
|
||||
message ServerWithdrawResponse {
|
||||
// The sweep sigs that the server generated for the htlc.
|
||||
option deprecated = true;
|
||||
|
||||
// The sweep sigs that the server generated for the withdrawal tx.
|
||||
repeated bytes musig2_sweep_sigs = 1;
|
||||
|
||||
// The nonces that the server used to generate the sweepless sweep sigs.
|
||||
// The nonces that the server used to generate the withdrawal sigs.
|
||||
repeated bytes server_nonces = 2;
|
||||
}
|
||||
|
||||
message ServerPsbtWithdrawRequest {
|
||||
// The withdrawal psbt.
|
||||
// Note that txscript.SigHashDefault will be enforced by default.
|
||||
bytes withdrawal_psbt = 1;
|
||||
|
||||
// The map of deposit txid:idx to the nonce used by the client.
|
||||
map<string, bytes> deposit_to_nonces = 2;
|
||||
}
|
||||
|
||||
message ServerPsbtWithdrawResponse {
|
||||
// The txid of the psbt that the client wants to push the sigs for.
|
||||
bytes txid = 1;
|
||||
|
||||
// A map of deposits in format txid:idx to the nonces.
|
||||
map<string, ServerPsbtWithdrawSigningInfo> signing_info = 2;
|
||||
}
|
||||
|
||||
message ServerPsbtWithdrawSigningInfo {
|
||||
// The nonces that the client used to generate the partial withdrawal tx
|
||||
// sigs.
|
||||
bytes nonce = 1;
|
||||
|
||||
// The musig2 htlc sigs that the client generated for the withdrawal tx.
|
||||
bytes sig = 2;
|
||||
}
|
||||
|
||||
message ServerStaticAddressLoopInRequest {
|
||||
// The client's public key for the htlc output.
|
||||
bytes htlc_client_pub_key = 1;
|
||||
|
|
|
|||
|
|
@ -22,10 +22,19 @@ type StaticAddressServerClient interface {
|
|||
// The server will generate the address and return the server key and the
|
||||
// address's CSV expiry.
|
||||
ServerNewAddress(ctx context.Context, in *ServerNewAddressRequest, opts ...grpc.CallOption) (*ServerNewAddressResponse, error)
|
||||
// Deprecated: Do not use.
|
||||
// ServerWithdrawDeposits allows to cooperatively sweep deposits that
|
||||
// haven't timed out yet to the client's wallet. The server will generate
|
||||
// the partial sigs for the client's selected deposits.
|
||||
//
|
||||
// Deprecated: use ServerPsbtWithdrawDeposits instead.
|
||||
ServerWithdrawDeposits(ctx context.Context, in *ServerWithdrawRequest, opts ...grpc.CallOption) (*ServerWithdrawResponse, error)
|
||||
// ServerPsbtWithdrawDeposits allows to cooperatively sweep deposits that
|
||||
// haven't timed out yet to the client's wallet. In contrast to
|
||||
// ServerWithdrawDeposits which provides the paramters to form the
|
||||
// withdrawal transaction, this service method will provide a psbt which
|
||||
// is signed by the server.
|
||||
ServerPsbtWithdrawDeposits(ctx context.Context, in *ServerPsbtWithdrawRequest, opts ...grpc.CallOption) (*ServerPsbtWithdrawResponse, error)
|
||||
// ServerStaticAddressLoopIn initiates a static address loop-in swap. The
|
||||
// server will respond with htlc details that the client can use to
|
||||
// construct and sign the htlc tx.
|
||||
|
|
@ -54,6 +63,7 @@ func (c *staticAddressServerClient) ServerNewAddress(ctx context.Context, in *Se
|
|||
return out, nil
|
||||
}
|
||||
|
||||
// Deprecated: Do not use.
|
||||
func (c *staticAddressServerClient) ServerWithdrawDeposits(ctx context.Context, in *ServerWithdrawRequest, opts ...grpc.CallOption) (*ServerWithdrawResponse, error) {
|
||||
out := new(ServerWithdrawResponse)
|
||||
err := c.cc.Invoke(ctx, "/looprpc.StaticAddressServer/ServerWithdrawDeposits", in, out, opts...)
|
||||
|
|
@ -63,6 +73,15 @@ func (c *staticAddressServerClient) ServerWithdrawDeposits(ctx context.Context,
|
|||
return out, nil
|
||||
}
|
||||
|
||||
func (c *staticAddressServerClient) ServerPsbtWithdrawDeposits(ctx context.Context, in *ServerPsbtWithdrawRequest, opts ...grpc.CallOption) (*ServerPsbtWithdrawResponse, error) {
|
||||
out := new(ServerPsbtWithdrawResponse)
|
||||
err := c.cc.Invoke(ctx, "/looprpc.StaticAddressServer/ServerPsbtWithdrawDeposits", in, out, opts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (c *staticAddressServerClient) ServerStaticAddressLoopIn(ctx context.Context, in *ServerStaticAddressLoopInRequest, opts ...grpc.CallOption) (*ServerStaticAddressLoopInResponse, error) {
|
||||
out := new(ServerStaticAddressLoopInResponse)
|
||||
err := c.cc.Invoke(ctx, "/looprpc.StaticAddressServer/ServerStaticAddressLoopIn", in, out, opts...)
|
||||
|
|
@ -98,10 +117,19 @@ type StaticAddressServerServer interface {
|
|||
// The server will generate the address and return the server key and the
|
||||
// address's CSV expiry.
|
||||
ServerNewAddress(context.Context, *ServerNewAddressRequest) (*ServerNewAddressResponse, error)
|
||||
// Deprecated: Do not use.
|
||||
// ServerWithdrawDeposits allows to cooperatively sweep deposits that
|
||||
// haven't timed out yet to the client's wallet. The server will generate
|
||||
// the partial sigs for the client's selected deposits.
|
||||
//
|
||||
// Deprecated: use ServerPsbtWithdrawDeposits instead.
|
||||
ServerWithdrawDeposits(context.Context, *ServerWithdrawRequest) (*ServerWithdrawResponse, error)
|
||||
// ServerPsbtWithdrawDeposits allows to cooperatively sweep deposits that
|
||||
// haven't timed out yet to the client's wallet. In contrast to
|
||||
// ServerWithdrawDeposits which provides the paramters to form the
|
||||
// withdrawal transaction, this service method will provide a psbt which
|
||||
// is signed by the server.
|
||||
ServerPsbtWithdrawDeposits(context.Context, *ServerPsbtWithdrawRequest) (*ServerPsbtWithdrawResponse, error)
|
||||
// ServerStaticAddressLoopIn initiates a static address loop-in swap. The
|
||||
// server will respond with htlc details that the client can use to
|
||||
// construct and sign the htlc tx.
|
||||
|
|
@ -124,6 +152,9 @@ func (UnimplementedStaticAddressServerServer) ServerNewAddress(context.Context,
|
|||
func (UnimplementedStaticAddressServerServer) ServerWithdrawDeposits(context.Context, *ServerWithdrawRequest) (*ServerWithdrawResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method ServerWithdrawDeposits not implemented")
|
||||
}
|
||||
func (UnimplementedStaticAddressServerServer) ServerPsbtWithdrawDeposits(context.Context, *ServerPsbtWithdrawRequest) (*ServerPsbtWithdrawResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method ServerPsbtWithdrawDeposits not implemented")
|
||||
}
|
||||
func (UnimplementedStaticAddressServerServer) ServerStaticAddressLoopIn(context.Context, *ServerStaticAddressLoopInRequest) (*ServerStaticAddressLoopInResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method ServerStaticAddressLoopIn not implemented")
|
||||
}
|
||||
|
|
@ -182,6 +213,24 @@ func _StaticAddressServer_ServerWithdrawDeposits_Handler(srv interface{}, ctx co
|
|||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
func _StaticAddressServer_ServerPsbtWithdrawDeposits_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||
in := new(ServerPsbtWithdrawRequest)
|
||||
if err := dec(in); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if interceptor == nil {
|
||||
return srv.(StaticAddressServerServer).ServerPsbtWithdrawDeposits(ctx, in)
|
||||
}
|
||||
info := &grpc.UnaryServerInfo{
|
||||
Server: srv,
|
||||
FullMethod: "/looprpc.StaticAddressServer/ServerPsbtWithdrawDeposits",
|
||||
}
|
||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.(StaticAddressServerServer).ServerPsbtWithdrawDeposits(ctx, req.(*ServerPsbtWithdrawRequest))
|
||||
}
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
func _StaticAddressServer_ServerStaticAddressLoopIn_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||
in := new(ServerStaticAddressLoopInRequest)
|
||||
if err := dec(in); err != nil {
|
||||
|
|
@ -251,6 +300,10 @@ var StaticAddressServer_ServiceDesc = grpc.ServiceDesc{
|
|||
MethodName: "ServerWithdrawDeposits",
|
||||
Handler: _StaticAddressServer_ServerWithdrawDeposits_Handler,
|
||||
},
|
||||
{
|
||||
MethodName: "ServerPsbtWithdrawDeposits",
|
||||
Handler: _StaticAddressServer_ServerPsbtWithdrawDeposits_Handler,
|
||||
},
|
||||
{
|
||||
MethodName: "ServerStaticAddressLoopIn",
|
||||
Handler: _StaticAddressServer_ServerStaticAddressLoopIn_Handler,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue