mirror of
https://github.com/lightninglabs/loop
synced 2024-11-16 00:12:52 +00:00
89b5c00cfa
This commit updates the reservation statemachine to allow for locking and spending of the initial reservation.
460 lines
11 KiB
Go
460 lines
11 KiB
Go
package reservation
|
|
|
|
import (
|
|
"context"
|
|
"encoding/hex"
|
|
"errors"
|
|
"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/lndclient"
|
|
"github.com/lightninglabs/loop/fsm"
|
|
"github.com/lightninglabs/loop/swapserverrpc"
|
|
"github.com/lightninglabs/loop/test"
|
|
"github.com/lightningnetwork/lnd/chainntnfs"
|
|
"github.com/stretchr/testify/mock"
|
|
"github.com/stretchr/testify/require"
|
|
"google.golang.org/grpc"
|
|
)
|
|
|
|
var (
|
|
defaultPubkeyBytes, _ = hex.DecodeString("021c97a90a411ff2b10dc2a8e32de2f29d2fa49d41bfbb52bd416e460db0747d0d")
|
|
defaultPubkey, _ = btcec.ParsePubKey(defaultPubkeyBytes)
|
|
|
|
defaultValue = btcutil.Amount(100)
|
|
|
|
defaultExpiry = uint32(100)
|
|
)
|
|
|
|
func newValidInitReservationContext() *InitReservationContext {
|
|
return &InitReservationContext{
|
|
reservationID: ID{0x01},
|
|
serverPubkey: defaultPubkey,
|
|
value: defaultValue,
|
|
expiry: defaultExpiry,
|
|
heightHint: 0,
|
|
}
|
|
}
|
|
|
|
func newValidClientReturn() *swapserverrpc.ServerOpenReservationResponse {
|
|
return &swapserverrpc.ServerOpenReservationResponse{}
|
|
}
|
|
|
|
type mockReservationClient struct {
|
|
mock.Mock
|
|
}
|
|
|
|
func (m *mockReservationClient) OpenReservation(ctx context.Context,
|
|
in *swapserverrpc.ServerOpenReservationRequest,
|
|
opts ...grpc.CallOption) (*swapserverrpc.ServerOpenReservationResponse,
|
|
error) {
|
|
|
|
args := m.Called(ctx, in, opts)
|
|
return args.Get(0).(*swapserverrpc.ServerOpenReservationResponse),
|
|
args.Error(1)
|
|
}
|
|
|
|
func (m *mockReservationClient) ReservationNotificationStream(
|
|
ctx context.Context, in *swapserverrpc.ReservationNotificationRequest,
|
|
opts ...grpc.CallOption,
|
|
) (swapserverrpc.ReservationService_ReservationNotificationStreamClient,
|
|
error) {
|
|
|
|
args := m.Called(ctx, in, opts)
|
|
return args.Get(0).(swapserverrpc.ReservationService_ReservationNotificationStreamClient),
|
|
args.Error(1)
|
|
}
|
|
|
|
func (m *mockReservationClient) FetchL402(ctx context.Context,
|
|
in *swapserverrpc.FetchL402Request,
|
|
opts ...grpc.CallOption) (*swapserverrpc.FetchL402Response, error) {
|
|
|
|
args := m.Called(ctx, in, opts)
|
|
|
|
return args.Get(0).(*swapserverrpc.FetchL402Response),
|
|
args.Error(1)
|
|
}
|
|
|
|
type mockStore struct {
|
|
mock.Mock
|
|
|
|
Store
|
|
}
|
|
|
|
func (m *mockStore) CreateReservation(ctx context.Context,
|
|
reservation *Reservation) error {
|
|
|
|
args := m.Called(ctx, reservation)
|
|
return args.Error(0)
|
|
}
|
|
|
|
// TestInitReservationAction tests the InitReservationAction of the reservation
|
|
// state machine.
|
|
func TestInitReservationAction(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
eventCtx fsm.EventContext
|
|
mockStoreErr error
|
|
mockClientReturn *swapserverrpc.ServerOpenReservationResponse
|
|
mockClientErr error
|
|
expectedEvent fsm.EventType
|
|
}{
|
|
{
|
|
name: "success",
|
|
eventCtx: newValidInitReservationContext(),
|
|
mockClientReturn: newValidClientReturn(),
|
|
expectedEvent: OnBroadcast,
|
|
},
|
|
{
|
|
name: "invalid context",
|
|
eventCtx: struct{}{},
|
|
expectedEvent: fsm.OnError,
|
|
},
|
|
{
|
|
name: "reservation server error",
|
|
eventCtx: newValidInitReservationContext(),
|
|
mockClientErr: errors.New("reservation server error"),
|
|
expectedEvent: fsm.OnError,
|
|
},
|
|
{
|
|
name: "store error",
|
|
eventCtx: newValidInitReservationContext(),
|
|
mockClientReturn: newValidClientReturn(),
|
|
mockStoreErr: errors.New("store error"),
|
|
expectedEvent: fsm.OnError,
|
|
},
|
|
}
|
|
|
|
for _, tc := range tests {
|
|
tc := tc
|
|
ctxb := context.Background()
|
|
mockLnd := test.NewMockLnd()
|
|
mockReservationClient := new(mockReservationClient)
|
|
mockReservationClient.On(
|
|
"OpenReservation", mock.Anything,
|
|
mock.Anything, mock.Anything,
|
|
).Return(tc.mockClientReturn, tc.mockClientErr)
|
|
|
|
mockStore := new(mockStore)
|
|
mockStore.On(
|
|
"CreateReservation", mock.Anything, mock.Anything,
|
|
).Return(tc.mockStoreErr)
|
|
|
|
reservationFSM := &FSM{
|
|
ctx: ctxb,
|
|
cfg: &Config{
|
|
Wallet: mockLnd.WalletKit,
|
|
ChainNotifier: mockLnd.ChainNotifier,
|
|
ReservationClient: mockReservationClient,
|
|
Store: mockStore,
|
|
},
|
|
StateMachine: &fsm.StateMachine{},
|
|
}
|
|
|
|
event := reservationFSM.InitAction(tc.eventCtx)
|
|
require.Equal(t, tc.expectedEvent, event)
|
|
}
|
|
}
|
|
|
|
type MockChainNotifier struct {
|
|
mock.Mock
|
|
}
|
|
|
|
func (m *MockChainNotifier) RegisterConfirmationsNtfn(ctx context.Context,
|
|
txid *chainhash.Hash, pkScript []byte, numConfs, heightHint int32,
|
|
options ...lndclient.NotifierOption) (chan *chainntnfs.TxConfirmation,
|
|
chan error, error) {
|
|
|
|
args := m.Called(ctx, txid, pkScript, numConfs, heightHint)
|
|
return args.Get(0).(chan *chainntnfs.TxConfirmation), args.Get(1).(chan error), args.Error(2)
|
|
}
|
|
|
|
func (m *MockChainNotifier) RegisterBlockEpochNtfn(ctx context.Context) (
|
|
chan int32, chan error, error) {
|
|
|
|
args := m.Called(ctx)
|
|
return args.Get(0).(chan int32), args.Get(1).(chan error), args.Error(2)
|
|
}
|
|
|
|
func (m *MockChainNotifier) RegisterSpendNtfn(ctx context.Context,
|
|
outpoint *wire.OutPoint, pkScript []byte, heightHint int32) (
|
|
chan *chainntnfs.SpendDetail, chan error, error) {
|
|
|
|
args := m.Called(ctx, pkScript, heightHint)
|
|
return args.Get(0).(chan *chainntnfs.SpendDetail), args.Get(1).(chan error), args.Error(2)
|
|
}
|
|
|
|
// TestSubscribeToConfirmationAction tests the SubscribeToConfirmationAction of
|
|
// the reservation state machine.
|
|
func TestSubscribeToConfirmationAction(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
blockHeight int32
|
|
blockErr error
|
|
sendTxConf bool
|
|
confErr error
|
|
expectedEvent fsm.EventType
|
|
}{
|
|
{
|
|
name: "success",
|
|
blockHeight: 0,
|
|
sendTxConf: true,
|
|
expectedEvent: OnConfirmed,
|
|
},
|
|
{
|
|
name: "expired",
|
|
blockHeight: 100,
|
|
expectedEvent: OnTimedOut,
|
|
},
|
|
{
|
|
name: "block error",
|
|
blockHeight: 0,
|
|
blockErr: errors.New("block error"),
|
|
expectedEvent: fsm.OnError,
|
|
},
|
|
{
|
|
name: "tx confirmation error",
|
|
blockHeight: 0,
|
|
confErr: errors.New("tx confirmation error"),
|
|
expectedEvent: fsm.OnError,
|
|
},
|
|
}
|
|
|
|
for _, tc := range tests {
|
|
tc := tc
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
chainNotifier := new(MockChainNotifier)
|
|
|
|
// Create the FSM.
|
|
r := NewFSMFromReservation(
|
|
context.Background(), &Config{
|
|
ChainNotifier: chainNotifier,
|
|
},
|
|
&Reservation{
|
|
Expiry: defaultExpiry,
|
|
ServerPubkey: defaultPubkey,
|
|
ClientPubkey: defaultPubkey,
|
|
Value: defaultValue,
|
|
},
|
|
)
|
|
pkScript, err := r.reservation.GetPkScript()
|
|
require.NoError(t, err)
|
|
|
|
confChan := make(chan *chainntnfs.TxConfirmation)
|
|
confErrChan := make(chan error)
|
|
blockChan := make(chan int32)
|
|
blockErrChan := make(chan error)
|
|
|
|
// Define the expected return values for the mocks.
|
|
chainNotifier.On(
|
|
"RegisterConfirmationsNtfn", mock.Anything, mock.Anything,
|
|
mock.Anything, mock.Anything, mock.Anything,
|
|
).Return(confChan, confErrChan, nil)
|
|
|
|
chainNotifier.On("RegisterBlockEpochNtfn", mock.Anything).Return(
|
|
blockChan, blockErrChan, nil,
|
|
)
|
|
|
|
go func() {
|
|
// Send the tx confirmation.
|
|
if tc.sendTxConf {
|
|
confChan <- &chainntnfs.TxConfirmation{
|
|
Tx: &wire.MsgTx{
|
|
TxIn: []*wire.TxIn{},
|
|
TxOut: []*wire.TxOut{
|
|
{
|
|
Value: int64(defaultValue),
|
|
PkScript: pkScript,
|
|
},
|
|
},
|
|
},
|
|
}
|
|
}
|
|
}()
|
|
|
|
go func() {
|
|
// Send the block notification.
|
|
if tc.blockHeight != 0 {
|
|
blockChan <- tc.blockHeight
|
|
}
|
|
}()
|
|
|
|
go func() {
|
|
// Send the block notification error.
|
|
if tc.blockErr != nil {
|
|
blockErrChan <- tc.blockErr
|
|
}
|
|
}()
|
|
|
|
go func() {
|
|
// Send the tx confirmation error.
|
|
if tc.confErr != nil {
|
|
confErrChan <- tc.confErr
|
|
}
|
|
}()
|
|
|
|
eventType := r.SubscribeToConfirmationAction(nil)
|
|
// Assert that the return value is as expected
|
|
require.Equal(t, tc.expectedEvent, eventType)
|
|
|
|
// Assert that the expected functions were called on the mocks
|
|
chainNotifier.AssertExpectations(t)
|
|
})
|
|
}
|
|
}
|
|
|
|
// AsyncWaitForExpiredOrSweptAction tests the AsyncWaitForExpiredOrSweptAction
|
|
// of the reservation state machine.
|
|
func TestAsyncWaitForExpiredOrSweptAction(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
blockErr error
|
|
spendErr error
|
|
expectedEvent fsm.EventType
|
|
}{
|
|
{
|
|
name: "noop",
|
|
expectedEvent: fsm.NoOp,
|
|
},
|
|
{
|
|
name: "block error",
|
|
blockErr: errors.New("block error"),
|
|
expectedEvent: fsm.OnError,
|
|
},
|
|
{
|
|
name: "spend error",
|
|
spendErr: errors.New("spend error"),
|
|
expectedEvent: fsm.OnError,
|
|
},
|
|
}
|
|
|
|
for _, tc := range tests {
|
|
tc := tc
|
|
t.Run(tc.name, func(t *testing.T) { // Create a mock ChainNotifier and Reservation
|
|
chainNotifier := new(MockChainNotifier)
|
|
|
|
// Define your FSM
|
|
r := NewFSMFromReservation(
|
|
context.Background(), &Config{
|
|
ChainNotifier: chainNotifier,
|
|
},
|
|
&Reservation{
|
|
ServerPubkey: defaultPubkey,
|
|
ClientPubkey: defaultPubkey,
|
|
Expiry: defaultExpiry,
|
|
},
|
|
)
|
|
|
|
// Define the expected return values for your mocks
|
|
chainNotifier.On("RegisterBlockEpochNtfn", mock.Anything).Return(
|
|
make(chan int32), make(chan error), tc.blockErr,
|
|
)
|
|
|
|
chainNotifier.On(
|
|
"RegisterSpendNtfn", mock.Anything,
|
|
mock.Anything, mock.Anything,
|
|
).Return(
|
|
make(chan *chainntnfs.SpendDetail),
|
|
make(chan error), tc.spendErr,
|
|
)
|
|
|
|
eventType := r.AsyncWaitForExpiredOrSweptAction(nil)
|
|
// Assert that the return value is as expected
|
|
require.Equal(t, tc.expectedEvent, eventType)
|
|
})
|
|
}
|
|
}
|
|
|
|
// TesthandleSubcriptions tests the handleSubcriptions function of the
|
|
// reservation state machine.
|
|
func TestHandleSubcriptions(t *testing.T) {
|
|
var (
|
|
blockErr = errors.New("block error")
|
|
spendErr = errors.New("spend error")
|
|
)
|
|
tests := []struct {
|
|
name string
|
|
blockHeight int32
|
|
blockErr error
|
|
spendDetail *chainntnfs.SpendDetail
|
|
spendErr error
|
|
expectedEvent fsm.EventType
|
|
expectedErr error
|
|
}{
|
|
{
|
|
name: "expired",
|
|
blockHeight: 100,
|
|
expectedEvent: OnTimedOut,
|
|
},
|
|
{
|
|
name: "block error",
|
|
blockErr: blockErr,
|
|
expectedEvent: fsm.OnError,
|
|
expectedErr: blockErr,
|
|
},
|
|
{
|
|
name: "spent",
|
|
spendDetail: &chainntnfs.SpendDetail{},
|
|
expectedEvent: OnSpent,
|
|
},
|
|
{
|
|
name: "spend error",
|
|
spendErr: spendErr,
|
|
expectedEvent: fsm.OnError,
|
|
expectedErr: spendErr,
|
|
},
|
|
}
|
|
|
|
for _, tc := range tests {
|
|
tc := tc
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
chainNotifier := new(MockChainNotifier)
|
|
|
|
// Create the FSM.
|
|
r := NewFSMFromReservation(
|
|
context.Background(), &Config{
|
|
ChainNotifier: chainNotifier,
|
|
},
|
|
&Reservation{
|
|
ServerPubkey: defaultPubkey,
|
|
ClientPubkey: defaultPubkey,
|
|
Expiry: defaultExpiry,
|
|
},
|
|
)
|
|
|
|
blockChan := make(chan int32)
|
|
blockErrChan := make(chan error)
|
|
|
|
spendChan := make(chan *chainntnfs.SpendDetail)
|
|
spendErrChan := make(chan error)
|
|
|
|
go func() {
|
|
if tc.blockHeight != 0 {
|
|
blockChan <- tc.blockHeight
|
|
}
|
|
|
|
if tc.blockErr != nil {
|
|
blockErrChan <- tc.blockErr
|
|
}
|
|
|
|
if tc.spendDetail != nil {
|
|
spendChan <- tc.spendDetail
|
|
}
|
|
if tc.spendErr != nil {
|
|
spendErrChan <- tc.spendErr
|
|
}
|
|
}()
|
|
|
|
eventType, err := r.handleSubcriptions(
|
|
context.Background(), blockChan, spendChan,
|
|
blockErrChan, spendErrChan,
|
|
)
|
|
require.Equal(t, tc.expectedErr, err)
|
|
require.Equal(t, tc.expectedEvent, eventType)
|
|
})
|
|
}
|
|
}
|