mirror of
https://github.com/lightninglabs/loop.git
synced 2026-08-13 12:33:03 +02:00
Merge pull request #1148 from hieblmi/harden-musig2-handling
multi: validate server-provided signing data and clean up comments
This commit is contained in:
commit
e0e1da5551
16 changed files with 389 additions and 44 deletions
|
|
@ -78,7 +78,7 @@ func TestAutoLoopDisabled(t *testing.T) {
|
|||
c.stop()
|
||||
}
|
||||
|
||||
// TestAutoLoopEnabled tests enabling the liquidity manger's autolooper. To keep
|
||||
// TestAutoLoopEnabled tests enabling the liquidity manager's autolooper. To keep
|
||||
// the test simple, we do not update actual lnd channel balances, but rather
|
||||
// run our mock with two channels that will always require a loop out according
|
||||
// to our rules. This allows us to test the other restrictions placed on the
|
||||
|
|
|
|||
|
|
@ -340,7 +340,7 @@ func (m *Manager) Run(ctx context.Context) error {
|
|||
}
|
||||
}
|
||||
|
||||
// Try to automatically dispach an asset auto-loop.
|
||||
// Try to automatically dispatch an asset auto-loop.
|
||||
for assetID := range m.params.AssetAutoloopParams {
|
||||
err = m.easyAssetAutoloop(ctx, assetID)
|
||||
if err != nil {
|
||||
|
|
|
|||
|
|
@ -39,7 +39,7 @@ type PostgresConfig struct {
|
|||
RequireSSL bool `long:"requiressl" description:"Whether to require using SSL (mode: require) when connecting to the server."`
|
||||
}
|
||||
|
||||
// DSN returns the dns to connect to the database.
|
||||
// DSN returns the data source name used to connect to the database.
|
||||
func (s *PostgresConfig) DSN(hidePassword bool) string {
|
||||
var sslMode = "disable"
|
||||
if s.RequireSSL {
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ const (
|
|||
ProtocolVersionSegwitLoopIn ProtocolVersion = 2
|
||||
|
||||
// ProtocolVersionPreimagePush indicates that the client will push loop
|
||||
// out preimages to the sever to speed up claim.
|
||||
// out preimages to the server to speed up claim.
|
||||
ProtocolVersionPreimagePush ProtocolVersion = 3
|
||||
|
||||
// ProtocolVersionUserExpiryLoopOut indicates that the client will
|
||||
|
|
|
|||
|
|
@ -478,7 +478,7 @@ func testCustomSweepConfTarget(t *testing.T) {
|
|||
require.Equal(t, swap.Preimage, preimage)
|
||||
}
|
||||
|
||||
// Now that we have pushed our preimage to the sever, we send an update
|
||||
// Now that we have pushed our preimage to the server, we send an update
|
||||
// indicating that our off chain htlc is settled. We do this so that
|
||||
// we don't have to keep consuming preimage pushes from our server mock
|
||||
// for every sweep attempt.
|
||||
|
|
@ -779,22 +779,22 @@ func testPreimagePush(t *testing.T) {
|
|||
require.NoError(t, <-errChan)
|
||||
}
|
||||
|
||||
// TestFailedOffChainCancelation tests sending of a cancelation message to
|
||||
// TestFailedOffChainCancellation tests sending of a cancellation message to
|
||||
// the server when a swap fails due to off-chain routing.
|
||||
func TestFailedOffChainCancelation(t *testing.T) {
|
||||
func TestFailedOffChainCancellation(t *testing.T) {
|
||||
t.Run("stable protocol", func(t *testing.T) {
|
||||
testFailedOffChainCancelation(t)
|
||||
testFailedOffChainCancellation(t)
|
||||
})
|
||||
|
||||
t.Run("experimental protocol", func(t *testing.T) {
|
||||
loopdb.EnableExperimentalProtocol()
|
||||
defer loopdb.ResetCurrentProtocolVersion()
|
||||
|
||||
testFailedOffChainCancelation(t)
|
||||
testFailedOffChainCancellation(t)
|
||||
})
|
||||
}
|
||||
|
||||
func testFailedOffChainCancelation(t *testing.T) {
|
||||
func testFailedOffChainCancellation(t *testing.T) {
|
||||
defer test.Guard(t)()
|
||||
|
||||
lnd := test.NewMockLnd()
|
||||
|
|
@ -873,7 +873,7 @@ func testFailedOffChainCancelation(t *testing.T) {
|
|||
FailureSourceIndex: 1,
|
||||
},
|
||||
},
|
||||
// Add one htlc that failed in the network at wide.
|
||||
// Add one htlc that failed in the network at large.
|
||||
{
|
||||
Status: lnrpc.HTLCAttempt_FAILED,
|
||||
Route: &lnrpc.Route{
|
||||
|
|
@ -892,7 +892,7 @@ func testFailedOffChainCancelation(t *testing.T) {
|
|||
State: lnrpc.Payment_SUCCEEDED,
|
||||
}
|
||||
|
||||
// We want to fail our swap payment and succeed the prepush, so we send
|
||||
// We want to fail our swap payment and succeed the prepayment, so we send
|
||||
// a failure update to the payment that has the larger amount.
|
||||
if pmt1.Amount > pmt2.Amount {
|
||||
pmt1.TrackPaymentMessage.Updates <- failUpdate
|
||||
|
|
@ -908,7 +908,7 @@ func testFailedOffChainCancelation(t *testing.T) {
|
|||
require.NoError(t, err)
|
||||
|
||||
payAddr := invoice.PaymentAddr.UnwrapOrFail(t)
|
||||
swapCancelation := &outCancelDetails{
|
||||
swapCancellation := &outCancelDetails{
|
||||
hash: swap.hash,
|
||||
paymentAddr: payAddr,
|
||||
metadata: routeCancelMetadata{
|
||||
|
|
@ -920,7 +920,7 @@ func testFailedOffChainCancelation(t *testing.T) {
|
|||
},
|
||||
},
|
||||
}
|
||||
server.assertSwapCanceled(t, swapCancelation)
|
||||
server.assertSwapCanceled(t, swapCancellation)
|
||||
|
||||
// Finally, the swap should be recorded with failed off chain timeout.
|
||||
cfg.store.(*loopdb.StoreMock).AssertLoopOutState(
|
||||
|
|
|
|||
|
|
@ -6,12 +6,14 @@ import (
|
|||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/btcsuite/btcd/btcec/v2/schnorr/musig2"
|
||||
"github.com/btcsuite/btcd/btcutil"
|
||||
"github.com/btcsuite/btcd/chaincfg"
|
||||
"github.com/btcsuite/btcd/wire"
|
||||
"github.com/lightninglabs/lndclient"
|
||||
"github.com/lightninglabs/loop/loopdb"
|
||||
"github.com/lightninglabs/loop/test"
|
||||
"github.com/lightningnetwork/lnd/input"
|
||||
invpkg "github.com/lightningnetwork/lnd/invoices"
|
||||
"github.com/lightningnetwork/lnd/lntypes"
|
||||
"github.com/lightningnetwork/lnd/lnwire"
|
||||
|
|
@ -32,6 +34,13 @@ var (
|
|||
testMaxSwapAmount = btcutil.Amount(1000000)
|
||||
)
|
||||
|
||||
// mockMuSig2SigningData returns size-correct placeholder data. Client tests
|
||||
// only assert response handling, not MuSig2 cryptographic validity.
|
||||
func mockMuSig2SigningData() ([]byte, []byte, error) {
|
||||
return make([]byte, musig2.PubNonceSize),
|
||||
make([]byte, input.MuSig2PartialSigSize), nil
|
||||
}
|
||||
|
||||
// serverMock is used in client unit tests to simulate swap server behaviour.
|
||||
type serverMock struct {
|
||||
expectedSwapAmt btcutil.Amount
|
||||
|
|
@ -276,7 +285,7 @@ func (s *serverMock) MuSig2SignSweep(_ context.Context, _ loopdb.ProtocolVersion
|
|||
_ lntypes.Hash, _ [32]byte, _ []byte, _ []byte) ([]byte,
|
||||
[]byte, error) {
|
||||
|
||||
return nil, nil, nil
|
||||
return mockMuSig2SigningData()
|
||||
}
|
||||
|
||||
func (s *serverMock) MultiMuSig2SignSweep(ctx context.Context,
|
||||
|
|
@ -285,7 +294,7 @@ func (s *serverMock) MultiMuSig2SignSweep(ctx context.Context,
|
|||
prevoutMap map[wire.OutPoint]*wire.TxOut) (
|
||||
[]byte, []byte, error) {
|
||||
|
||||
return nil, nil, nil
|
||||
return mockMuSig2SigningData()
|
||||
}
|
||||
|
||||
func (s *serverMock) PushKey(_ context.Context, _ loopdb.ProtocolVersion,
|
||||
|
|
|
|||
|
|
@ -379,12 +379,14 @@ func (m *Manager) handleLoopInSweepReq(ctx context.Context,
|
|||
return err
|
||||
}
|
||||
|
||||
var (
|
||||
serverNonce [musig2.PubNonceSize]byte
|
||||
sigHash [32]byte
|
||||
)
|
||||
var sigHash [32]byte
|
||||
|
||||
serverNonce, err := byteSliceTo66ByteSlice(nonce)
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid server nonce for "+
|
||||
"deposit %v: %w", depositOutpoint, err)
|
||||
}
|
||||
|
||||
copy(serverNonce[:], nonce)
|
||||
musig2Session, err := staticutil.CreateMusig2Session(
|
||||
ctx, m.cfg.Signer, loopIn.AddressParams, loopIn.Address,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,11 +1,14 @@
|
|||
package loopin
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
"github.com/btcsuite/btcd/btcec/v2/schnorr/musig2"
|
||||
"github.com/btcsuite/btcd/btcutil"
|
||||
"github.com/btcsuite/btcd/btcutil/psbt"
|
||||
"github.com/btcsuite/btcd/chaincfg/chainhash"
|
||||
"github.com/btcsuite/btcd/wire"
|
||||
"github.com/lightninglabs/loop"
|
||||
|
|
@ -14,6 +17,7 @@ import (
|
|||
"github.com/lightninglabs/loop/staticaddr/deposit"
|
||||
"github.com/lightninglabs/loop/staticaddr/script"
|
||||
"github.com/lightninglabs/loop/swap"
|
||||
"github.com/lightninglabs/loop/swapserverrpc"
|
||||
"github.com/lightningnetwork/lnd/lntypes"
|
||||
"github.com/lightningnetwork/lnd/routing/route"
|
||||
"github.com/lightningnetwork/lnd/zpay32"
|
||||
|
|
@ -220,6 +224,80 @@ func TestInitiateLoopInAllowsReservedAutoloopLabel(t *testing.T) {
|
|||
require.Equal(t, selectedDeposit.Value, quoteGetter.amount)
|
||||
}
|
||||
|
||||
// TestHandleLoopInSweepReqRejectsInvalidServerNonce ensures that a malformed
|
||||
// MuSig2 nonce returned by the server is rejected before it reaches the signer.
|
||||
func TestHandleLoopInSweepReqRejectsInvalidServerNonce(t *testing.T) {
|
||||
ctx := t.Context()
|
||||
|
||||
changeAddr := &script.Parameters{
|
||||
PkScript: []byte{0xaa, 0xbb},
|
||||
}
|
||||
|
||||
const confirmationHeight = 0
|
||||
dep := makeDeposit(7, 0, 10_000, confirmationHeight)
|
||||
depOutpoint := outpointString(dep)
|
||||
|
||||
swapHash := lntypes.Hash{9}
|
||||
loopIn := &StaticAddressLoopIn{
|
||||
SwapHash: swapHash,
|
||||
DepositOutpoints: []string{depOutpoint},
|
||||
SelectedAmount: dep.Value,
|
||||
}
|
||||
loopIn.SetState(Succeeded)
|
||||
|
||||
sweepTx := makeSweepTx(
|
||||
[]wire.OutPoint{dep.OutPoint},
|
||||
[]*wire.TxOut{{
|
||||
Value: int64(dep.Value),
|
||||
PkScript: []byte{0xcc, 0xdd},
|
||||
}},
|
||||
)
|
||||
sweepPacket, err := psbt.NewFromUnsignedTx(sweepTx)
|
||||
require.NoError(t, err)
|
||||
|
||||
var psbtBuf bytes.Buffer
|
||||
require.NoError(t, sweepPacket.Serialize(&psbtBuf))
|
||||
|
||||
mgr := &Manager{
|
||||
cfg: &Config{
|
||||
AddressManager: &mockAddressManager{
|
||||
params: changeAddr,
|
||||
},
|
||||
DepositManager: &mockDepositManager{
|
||||
byOutpoint: map[string]*deposit.Deposit{
|
||||
depOutpoint: dep,
|
||||
},
|
||||
},
|
||||
Store: &mockStore{
|
||||
loopIns: map[lntypes.Hash]*StaticAddressLoopIn{
|
||||
swapHash: loopIn,
|
||||
},
|
||||
mapIDs: map[lntypes.Hash][]deposit.ID{
|
||||
swapHash: {dep.ID},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
req := &swapserverrpc.ServerStaticLoopInSweepNotification{
|
||||
SweepTxPsbt: psbtBuf.Bytes(),
|
||||
SwapHash: swapHash[:],
|
||||
DepositToNonces: map[string][]byte{
|
||||
depOutpoint: make([]byte, musig2.PubNonceSize-1),
|
||||
},
|
||||
PrevoutInfo: []*swapserverrpc.PrevoutInfo{{
|
||||
Value: uint64(dep.Value),
|
||||
PkScript: changeAddr.PkScript,
|
||||
TxidBytes: dep.Hash[:],
|
||||
OutputIndex: dep.Index,
|
||||
}},
|
||||
}
|
||||
|
||||
err = mgr.handleLoopInSweepReq(ctx, req)
|
||||
require.ErrorContains(t, err, "invalid server nonce")
|
||||
require.ErrorContains(t, err, depOutpoint)
|
||||
}
|
||||
|
||||
// mockDepositManager implements DepositManager for tests.
|
||||
type mockDepositManager struct {
|
||||
// activeDeposits is the set returned by GetActiveDepositsInState.
|
||||
|
|
|
|||
|
|
@ -18,8 +18,8 @@ type Parameters struct {
|
|||
// used for the 2-of-2 funding output.
|
||||
ServerPubkey *btcec.PublicKey
|
||||
|
||||
// Expiry is the CSV timout value at which the client can claim the
|
||||
// static address's timout path.
|
||||
// Expiry is the CSV timeout value at which the client can claim the
|
||||
// static address's timeout path.
|
||||
Expiry uint32
|
||||
|
||||
// PkScript is the unique static address's output script.
|
||||
|
|
|
|||
|
|
@ -793,13 +793,32 @@ func (m *Manager) signMusig2Tx(ctx context.Context,
|
|||
|
||||
// We'll now add the nonce to our session and sign the tx.
|
||||
for deposit, sigAndNonce := range sigInfo {
|
||||
if sigAndNonce == nil {
|
||||
return nil, fmt.Errorf("missing signing info for "+
|
||||
"deposit %v", deposit)
|
||||
}
|
||||
|
||||
session, ok := sessions[deposit]
|
||||
if !ok {
|
||||
return nil, errors.New("session not found")
|
||||
}
|
||||
|
||||
nonce := [musig2.PubNonceSize]byte{}
|
||||
if len(sigAndNonce.Nonce) != musig2.PubNonceSize {
|
||||
return nil, fmt.Errorf("invalid nonce length for "+
|
||||
"deposit %v: got %d, want %d", deposit,
|
||||
len(sigAndNonce.Nonce), musig2.PubNonceSize)
|
||||
}
|
||||
|
||||
if len(sigAndNonce.Sig) != input.MuSig2PartialSigSize {
|
||||
return nil, fmt.Errorf("invalid partial signature "+
|
||||
"length for deposit %v: got %d, want %d",
|
||||
deposit, len(sigAndNonce.Sig),
|
||||
input.MuSig2PartialSigSize)
|
||||
}
|
||||
|
||||
var nonce [musig2.PubNonceSize]byte
|
||||
copy(nonce[:], sigAndNonce.Nonce)
|
||||
|
||||
haveAllNonces, err := signer.MuSig2RegisterNonces(
|
||||
ctx, session.SessionID,
|
||||
[][musig2.PubNonceSize]byte{nonce},
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import (
|
|||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/btcsuite/btcd/btcec/v2/schnorr/musig2"
|
||||
"github.com/btcsuite/btcd/btcutil"
|
||||
"github.com/btcsuite/btcd/chaincfg"
|
||||
"github.com/btcsuite/btcd/chaincfg/chainhash"
|
||||
|
|
@ -377,6 +378,105 @@ func TestSignMusig2Tx_MissingOutpointInDepositMap(t *testing.T) {
|
|||
require.ErrorContains(t, err, "tx outpoint not in deposit index map")
|
||||
}
|
||||
|
||||
// TestSignMusig2Tx_InvalidServerSigningInfo tests that malformed server
|
||||
// signing data is rejected before it is passed to the signer.
|
||||
func TestSignMusig2Tx_InvalidServerSigningInfo(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tx := wire.NewMsgTx(2)
|
||||
outpoint := wire.OutPoint{
|
||||
Hash: [32]byte{1},
|
||||
Index: 0,
|
||||
}
|
||||
tx.AddTxIn(&wire.TxIn{
|
||||
PreviousOutPoint: outpoint,
|
||||
})
|
||||
|
||||
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,
|
||||
})
|
||||
|
||||
depositKey := outpoint.String()
|
||||
sessions := map[string]*input.MuSig2SessionInfo{
|
||||
depositKey: {
|
||||
SessionID: [32]byte{1},
|
||||
},
|
||||
}
|
||||
depositsToIdx := map[string]int{
|
||||
depositKey: 0,
|
||||
}
|
||||
prevOutFetcher := txscript.NewMultiPrevOutFetcher(
|
||||
map[wire.OutPoint]*wire.TxOut{
|
||||
outpoint: {
|
||||
Value: 5000,
|
||||
PkScript: pkScript,
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
validNonce := make([]byte, musig2.PubNonceSize)
|
||||
validSig := make([]byte, input.MuSig2PartialSigSize)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
signingInfo *swapserverrpc.ServerPsbtWithdrawSigningInfo
|
||||
errContains string
|
||||
}{
|
||||
{
|
||||
name: "nil signing info",
|
||||
signingInfo: nil,
|
||||
errContains: "missing signing info",
|
||||
},
|
||||
{
|
||||
name: "invalid nonce length",
|
||||
signingInfo: &swapserverrpc.ServerPsbtWithdrawSigningInfo{
|
||||
Nonce: validNonce[:musig2.PubNonceSize-1],
|
||||
Sig: validSig,
|
||||
},
|
||||
errContains: "invalid nonce length",
|
||||
},
|
||||
{
|
||||
name: "invalid partial signature length",
|
||||
signingInfo: &swapserverrpc.ServerPsbtWithdrawSigningInfo{
|
||||
Nonce: validNonce,
|
||||
Sig: validSig[:input.MuSig2PartialSigSize-1],
|
||||
},
|
||||
errContains: "invalid partial signature length",
|
||||
},
|
||||
}
|
||||
|
||||
lnd := test.NewMockLnd()
|
||||
m := &Manager{
|
||||
cfg: &ManagerConfig{
|
||||
Signer: lnd.Signer,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
sigInfo := map[string]*swapserverrpc.ServerPsbtWithdrawSigningInfo{
|
||||
depositKey: tc.signingInfo,
|
||||
}
|
||||
|
||||
_, err := m.signMusig2Tx(
|
||||
context.Background(), prevOutFetcher, lnd.Signer,
|
||||
tx.Copy(), sessions, sigInfo, depositsToIdx,
|
||||
)
|
||||
require.ErrorContains(t, err, tc.errContains)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestCalculateWithdrawalTxValues tests various edge cases in withdrawal
|
||||
// transaction value calculations.
|
||||
func TestCalculateWithdrawalTxValues(t *testing.T) {
|
||||
|
|
|
|||
|
|
@ -405,13 +405,9 @@ func (s *grpcSwapServerClient) NewLoopOutSwap(ctx context.Context,
|
|||
return nil, err
|
||||
}
|
||||
|
||||
var senderKey [33]byte
|
||||
copy(senderKey[:], swapResp.SenderKey)
|
||||
|
||||
// Validate sender key.
|
||||
_, err = btcec.ParsePubKey(senderKey[:])
|
||||
senderKey, err := parseServerPubKey("sender key", swapResp.SenderKey)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid sender key: %v", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &newLoopOutResponse{
|
||||
|
|
@ -470,14 +466,22 @@ func (s *grpcSwapServerClient) NewLoopInSwap(ctx context.Context,
|
|||
return nil, err
|
||||
}
|
||||
|
||||
var receiverKey, receiverInternalKey [33]byte
|
||||
copy(receiverKey[:], swapResp.ReceiverKey)
|
||||
copy(receiverInternalKey[:], swapResp.ReceiverInternalPubkey)
|
||||
|
||||
// Validate receiver key.
|
||||
_, err = btcec.ParsePubKey(receiverKey[:])
|
||||
receiverKey, err := parseServerPubKey(
|
||||
"receiver key", swapResp.ReceiverKey,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid sender key: %v", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var receiverInternalKey [btcec.PubKeyBytesLenCompressed]byte
|
||||
if loopdb.CurrentProtocolVersion() >= loopdb.ProtocolVersionMuSig2 {
|
||||
receiverInternalKey, err = parseServerPubKey(
|
||||
"receiver internal key",
|
||||
swapResp.ReceiverInternalPubkey,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return &newLoopInResponse{
|
||||
|
|
@ -488,6 +492,29 @@ func (s *grpcSwapServerClient) NewLoopInSwap(ctx context.Context,
|
|||
}, nil
|
||||
}
|
||||
|
||||
// parseServerPubKey validates that keyBytes is a well-formed compressed public
|
||||
// key received from the server and returns it as a fixed-size array. The name
|
||||
// argument is used to produce a descriptive error if validation fails.
|
||||
func parseServerPubKey(name string,
|
||||
keyBytes []byte) ([btcec.PubKeyBytesLenCompressed]byte, error) {
|
||||
|
||||
var key [btcec.PubKeyBytesLenCompressed]byte
|
||||
|
||||
if len(keyBytes) != btcec.PubKeyBytesLenCompressed {
|
||||
return key, fmt.Errorf("invalid %s length: got %d, want %d",
|
||||
name, len(keyBytes), btcec.PubKeyBytesLenCompressed)
|
||||
}
|
||||
|
||||
_, err := btcec.ParsePubKey(keyBytes)
|
||||
if err != nil {
|
||||
return key, fmt.Errorf("invalid %s: %v", name, err)
|
||||
}
|
||||
|
||||
copy(key[:], keyBytes)
|
||||
|
||||
return key, nil
|
||||
}
|
||||
|
||||
// ServerUpdate summarizes an update from the swap server.
|
||||
type ServerUpdate struct {
|
||||
// State is the state that the server has sent us.
|
||||
|
|
|
|||
28
swap_server_client_test.go
Normal file
28
swap_server_client_test.go
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
package loop
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
looptest "github.com/lightninglabs/loop/test"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// TestParseServerPubKey ensures that parseServerPubKey accepts a valid
|
||||
// compressed public key and rejects keys with an invalid length or contents.
|
||||
func TestParseServerPubKey(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
_, pubKey := looptest.CreateKey(1)
|
||||
pubKeyBytes := pubKey.SerializeCompressed()
|
||||
|
||||
parsedKey, err := parseServerPubKey("test key", pubKeyBytes)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, pubKeyBytes, parsedKey[:])
|
||||
|
||||
_, err = parseServerPubKey("test key", pubKeyBytes[:32])
|
||||
require.ErrorContains(t, err, "invalid test key length")
|
||||
|
||||
invalidKey := make([]byte, 33)
|
||||
_, err = parseServerPubKey("test key", invalidKey)
|
||||
require.ErrorContains(t, err, "invalid test key")
|
||||
}
|
||||
|
|
@ -1882,6 +1882,12 @@ func (b *batch) musig2sign(ctx context.Context, inputIndex int, sweep sweep,
|
|||
return nil, err
|
||||
}
|
||||
|
||||
if err := validateServerMuSig2SigningData(
|
||||
serverNonce, serverSig,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var serverPublicNonce [musig2.PubNonceSize]byte
|
||||
copy(serverPublicNonce[:], serverNonce)
|
||||
|
||||
|
|
@ -1934,6 +1940,26 @@ func (b *batch) musig2sign(ctx context.Context, inputIndex int, sweep sweep,
|
|||
return finalSig, nil
|
||||
}
|
||||
|
||||
// validateServerMuSig2SigningData rejects malformed MuSig2 cosigning data
|
||||
// received from the server by checking that the nonce and partial signature
|
||||
// have the expected lengths before they are passed to the signer.
|
||||
func validateServerMuSig2SigningData(serverNonce,
|
||||
serverSig []byte) error {
|
||||
|
||||
if len(serverNonce) != musig2.PubNonceSize {
|
||||
return fmt.Errorf("invalid server nonce length: got %d, "+
|
||||
"want %d", len(serverNonce), musig2.PubNonceSize)
|
||||
}
|
||||
|
||||
if len(serverSig) != input.MuSig2PartialSigSize {
|
||||
return fmt.Errorf("invalid server partial signature "+
|
||||
"length: got %d, want %d", len(serverSig),
|
||||
input.MuSig2PartialSigSize)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// updateRbfRate updates the fee rate we should use for the new batch
|
||||
// transaction. This fee rate does not guarantee RBF success, but the continuous
|
||||
// increase leads to an eventual successful RBF replacement.
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ import (
|
|||
|
||||
"github.com/btcsuite/btcd/blockchain"
|
||||
"github.com/btcsuite/btcd/btcec/v2"
|
||||
"github.com/btcsuite/btcd/btcec/v2/schnorr/musig2"
|
||||
"github.com/btcsuite/btcd/btcutil"
|
||||
"github.com/btcsuite/btcd/chaincfg"
|
||||
"github.com/btcsuite/btcd/chaincfg/chainhash"
|
||||
|
|
@ -87,7 +88,62 @@ func testMuSig2SignSweep(ctx context.Context,
|
|||
prevoutMap map[wire.OutPoint]*wire.TxOut) (
|
||||
[]byte, []byte, error) {
|
||||
|
||||
return nil, nil, nil
|
||||
return testMuSig2SigningData()
|
||||
}
|
||||
|
||||
// testMuSig2SigningData returns size-correct placeholder data. These tests
|
||||
// only exercise control flow around the signing response, not cryptographic
|
||||
// validity.
|
||||
func testMuSig2SigningData() ([]byte, []byte, error) {
|
||||
return make([]byte, musig2.PubNonceSize),
|
||||
make([]byte, input.MuSig2PartialSigSize), nil
|
||||
}
|
||||
|
||||
// TestValidateServerMuSig2SigningData ensures that MuSig2 cosigning data from
|
||||
// the server is accepted when well-formed and rejected when the nonce or
|
||||
// partial signature has an unexpected length.
|
||||
func TestValidateServerMuSig2SigningData(t *testing.T) {
|
||||
validNonce, validSig, err := testMuSig2SigningData()
|
||||
require.NoError(t, err)
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
serverNonce []byte
|
||||
serverSig []byte
|
||||
errContains string
|
||||
}{
|
||||
{
|
||||
name: "valid signing data",
|
||||
serverNonce: validNonce,
|
||||
serverSig: validSig,
|
||||
},
|
||||
{
|
||||
name: "invalid nonce length",
|
||||
serverNonce: validNonce[:musig2.PubNonceSize-1],
|
||||
serverSig: validSig,
|
||||
errContains: "invalid server nonce length",
|
||||
},
|
||||
{
|
||||
name: "invalid partial signature length",
|
||||
serverNonce: validNonce,
|
||||
serverSig: validSig[:input.MuSig2PartialSigSize-1],
|
||||
errContains: "invalid server partial signature length",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
err := validateServerMuSig2SigningData(
|
||||
tc.serverNonce, tc.serverSig,
|
||||
)
|
||||
if tc.errContains == "" {
|
||||
require.NoError(t, err)
|
||||
return
|
||||
}
|
||||
|
||||
require.ErrorContains(t, err, tc.errContains)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
var customSignature = func() []byte {
|
||||
|
|
@ -5024,7 +5080,7 @@ func testWithMixedBatch(t *testing.T, store testStore,
|
|||
[]byte, []byte, error) {
|
||||
|
||||
if swapHash == swapHashes[2] {
|
||||
return nil, nil, nil
|
||||
return testMuSig2SigningData()
|
||||
} else {
|
||||
return nil, nil, fmt.Errorf("test error")
|
||||
}
|
||||
|
|
@ -5377,14 +5433,14 @@ func testWithMixedBatchLarge(t *testing.T, store testStore,
|
|||
} else {
|
||||
swapHash2Used = true
|
||||
|
||||
return nil, nil, nil
|
||||
return testMuSig2SigningData()
|
||||
}
|
||||
|
||||
case swapHash == preimages[5].Hash():
|
||||
return nil, nil, nil
|
||||
return testMuSig2SigningData()
|
||||
|
||||
case swapHash == preimages[8].Hash():
|
||||
return nil, nil, nil
|
||||
return testMuSig2SigningData()
|
||||
|
||||
default:
|
||||
return nil, nil, fmt.Errorf("test error")
|
||||
|
|
@ -5431,7 +5487,7 @@ func testWithMixedBatchCoopOnly(t *testing.T, store testStore,
|
|||
prevoutMap map[wire.OutPoint]*wire.TxOut) (
|
||||
[]byte, []byte, error) {
|
||||
|
||||
return nil, nil, nil
|
||||
return testMuSig2SigningData()
|
||||
}
|
||||
|
||||
// All the sweeps are cooperative.
|
||||
|
|
|
|||
|
|
@ -67,7 +67,7 @@ func mockMuSig2SignSweep(ctx context.Context,
|
|||
prevoutMap map[wire.OutPoint]*wire.TxOut) (
|
||||
[]byte, []byte, error) {
|
||||
|
||||
return nil, nil, nil
|
||||
return mockMuSig2SigningData()
|
||||
}
|
||||
|
||||
func newSwapClient(t *testing.T, config *clientConfig) *Client {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue