Skip to content

Commit 86b5490

Browse files
committed
reservations: add client requested fsm
1 parent b374a8b commit 86b5490

File tree

4 files changed

+277
-11
lines changed

4 files changed

+277
-11
lines changed

instantout/reservation/actions.go

Lines changed: 174 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,31 +2,194 @@ package reservation
22

33
import (
44
"context"
5+
"errors"
6+
"fmt"
7+
"time"
58

69
"github.com/btcsuite/btcd/btcec/v2"
710
"github.com/btcsuite/btcd/btcutil"
11+
"github.com/lightninglabs/lndclient"
812
"github.com/lightninglabs/loop/fsm"
13+
"github.com/lightninglabs/loop/swap"
914
"github.com/lightninglabs/loop/swapserverrpc"
1015
"github.com/lightningnetwork/lnd/chainntnfs"
16+
"github.com/lightningnetwork/lnd/lnrpc"
1117
)
1218

13-
// InitReservationContext contains the request parameters for a reservation.
14-
type InitReservationContext struct {
19+
const (
20+
// Define route independent max routing fees. We have currently no way
21+
// to get a reliable estimate of the routing fees. Best we can do is
22+
// the minimum routing fees, which is not very indicative.
23+
maxRoutingFeeBase = btcutil.Amount(10)
24+
25+
maxRoutingFeeRate = int64(20000)
26+
)
27+
28+
var (
29+
// The allowed delta between what we accept as the expiry height and
30+
// the actual expiry height.
31+
expiryDelta = uint32(3)
32+
33+
// defaultPrepayTimeout is the default timeout for the prepayment.
34+
DefaultPrepayTimeout = time.Minute * 120
35+
)
36+
37+
// ClientRequestedInitContext contains the request parameters for a reservation.
38+
type ClientRequestedInitContext struct {
39+
value btcutil.Amount
40+
relativeExpiry uint32
41+
heightHint uint32
42+
maxPrepaymentAmt btcutil.Amount
43+
}
44+
45+
// InitFromClientRequestAction is the action that is executed when the
46+
// reservation state machine is initialized from a client request. It creates
47+
// the reservation in the database and sends the reservation request to the
48+
// server.
49+
func (f *FSM) InitFromClientRequestAction(ctx context.Context,
50+
eventCtx fsm.EventContext) fsm.EventType {
51+
52+
// Check if the context is of the correct type.
53+
reservationRequest, ok := eventCtx.(*ClientRequestedInitContext)
54+
if !ok {
55+
return f.HandleError(fsm.ErrInvalidContextType)
56+
}
57+
58+
// Create the reservation in the database.
59+
keyRes, err := f.cfg.Wallet.DeriveNextKey(ctx, KeyFamily)
60+
if err != nil {
61+
return f.HandleError(err)
62+
}
63+
64+
// Send the request to the server.
65+
requestResponse, err := f.cfg.ReservationClient.RequestReservation(
66+
ctx, &swapserverrpc.RequestReservationRequest{
67+
Value: uint64(reservationRequest.value),
68+
Expiry: reservationRequest.relativeExpiry,
69+
ClientKey: keyRes.PubKey.SerializeCompressed(),
70+
},
71+
)
72+
if err != nil {
73+
return f.HandleError(err)
74+
}
75+
76+
expectedExpiry := reservationRequest.relativeExpiry +
77+
reservationRequest.heightHint
78+
79+
// Check that the expiry is in the delta.
80+
if requestResponse.Expiry < expectedExpiry-expiryDelta ||
81+
requestResponse.Expiry > expectedExpiry+expiryDelta {
82+
83+
return f.HandleError(
84+
fmt.Errorf("unexpected expiry height: %v, expected %v",
85+
requestResponse.Expiry, expectedExpiry))
86+
}
87+
88+
prepayment, err := f.cfg.LightningClient.DecodePaymentRequest(
89+
ctx, requestResponse.Invoice,
90+
)
91+
if err != nil {
92+
return f.HandleError(err)
93+
}
94+
95+
if prepayment.Value.ToSatoshis() > reservationRequest.maxPrepaymentAmt {
96+
return f.HandleError(
97+
errors.New("prepayment amount too high"))
98+
}
99+
100+
serverKey, err := btcec.ParsePubKey(requestResponse.ServerKey)
101+
if err != nil {
102+
return f.HandleError(err)
103+
}
104+
105+
var Id ID
106+
copy(Id[:], requestResponse.ReservationId)
107+
108+
reservation, err := NewReservation(
109+
Id, serverKey, keyRes.PubKey, reservationRequest.value,
110+
requestResponse.Expiry, reservationRequest.heightHint,
111+
keyRes.KeyLocator, ProtocolVersionClientInitiated,
112+
)
113+
if err != nil {
114+
return f.HandleError(err)
115+
}
116+
reservation.PrepayInvoice = requestResponse.Invoice
117+
f.reservation = reservation
118+
119+
// Create the reservation in the database.
120+
err = f.cfg.Store.CreateReservation(ctx, reservation)
121+
if err != nil {
122+
return f.HandleError(err)
123+
}
124+
125+
return OnClientInitialized
126+
}
127+
128+
// SendPrepayment is the action that is executed when the reservation
129+
// is initialized from a client request. It dispatches the prepayment to the
130+
// server and wait for it to be settled, signaling confirmation of the
131+
// reservation.
132+
func (f *FSM) SendPrepayment(ctx context.Context,
133+
_ fsm.EventContext) fsm.EventType {
134+
135+
prepayment, err := f.cfg.LightningClient.DecodePaymentRequest(
136+
ctx, f.reservation.PrepayInvoice,
137+
)
138+
if err != nil {
139+
return f.HandleError(err)
140+
}
141+
142+
payReq := lndclient.SendPaymentRequest{
143+
Invoice: f.reservation.PrepayInvoice,
144+
Timeout: DefaultPrepayTimeout,
145+
MaxFee: getMaxRoutingFee(prepayment.Value.ToSatoshis()),
146+
}
147+
// Send the prepayment to the server.
148+
payChan, errChan, err := f.cfg.RouterClient.SendPayment(
149+
ctx, payReq,
150+
)
151+
if err != nil {
152+
return f.HandleError(err)
153+
}
154+
155+
for {
156+
select {
157+
case <-ctx.Done():
158+
return fsm.NoOp
159+
160+
case err := <-errChan:
161+
return f.HandleError(err)
162+
163+
case prepayResp := <-payChan:
164+
if prepayResp.State == lnrpc.Payment_FAILED {
165+
return f.HandleError(
166+
fmt.Errorf("prepayment failed: %v",
167+
prepayResp.FailureReason))
168+
}
169+
if prepayResp.State == lnrpc.Payment_SUCCEEDED {
170+
return OnBroadcast
171+
}
172+
}
173+
}
174+
}
175+
176+
// ServerRequestedInitContext contains the request parameters for a reservation.
177+
type ServerRequestedInitContext struct {
15178
reservationID ID
16179
serverPubkey *btcec.PublicKey
17180
value btcutil.Amount
18181
expiry uint32
19182
heightHint uint32
20183
}
21184

22-
// InitAction is the action that is executed when the reservation state machine
23-
// is initialized. It creates the reservation in the database and dispatches the
24-
// payment to the server.
25-
func (f *FSM) InitAction(ctx context.Context,
185+
// InitFromServerRequestAction is the action that is executed when the
186+
// reservation state machine is initialized from a server request. It creates
187+
// the reservation in the database and dispatches the payment to the server.
188+
func (f *FSM) InitFromServerRequestAction(ctx context.Context,
26189
eventCtx fsm.EventContext) fsm.EventType {
27190

28191
// Check if the context is of the correct type.
29-
reservationRequest, ok := eventCtx.(*InitReservationContext)
192+
reservationRequest, ok := eventCtx.(*ServerRequestedInitContext)
30193
if !ok {
31194
return f.HandleError(fsm.ErrInvalidContextType)
32195
}
@@ -240,3 +403,7 @@ func (f *FSM) handleAsyncError(ctx context.Context, err error) {
240403
f.Errorf("Error sending event: %v", err2)
241404
}
242405
}
406+
407+
func getMaxRoutingFee(amt btcutil.Amount) btcutil.Amount {
408+
return swap.CalcFee(amt, maxRoutingFeeBase, maxRoutingFeeRate)
409+
}

instantout/reservation/actions_test.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -31,8 +31,8 @@ var (
3131
defaultExpiry = uint32(100)
3232
)
3333

34-
func newValidInitReservationContext() *InitReservationContext {
35-
return &InitReservationContext{
34+
func newValidInitReservationContext() *ServerRequestedInitContext {
35+
return &ServerRequestedInitContext{
3636
reservationID: ID{0x01},
3737
serverPubkey: defaultPubkey,
3838
value: defaultValue,
@@ -174,7 +174,7 @@ func TestInitReservationAction(t *testing.T) {
174174
StateMachine: &fsm.StateMachine{},
175175
}
176176

177-
event := reservationFSM.InitAction(ctxb, tc.eventCtx)
177+
event := reservationFSM.InitFromServerRequestAction(ctxb, tc.eventCtx)
178178
require.Equal(t, tc.expectedEvent, event)
179179
}
180180
}

instantout/reservation/fsm.go

Lines changed: 97 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,10 @@ const (
1717
// ProtocolVersionServerInitiated is the protocol version where the
1818
// server initiates the reservation.
1919
ProtocolVersionServerInitiated ProtocolVersion = 1
20+
21+
// ProtocolVersionClientInitiated is the protocol version where the
22+
// client initiates the reservation.
23+
ProtocolVersionClientInitiated ProtocolVersion = 2
2024
)
2125

2226
const (
@@ -39,6 +43,12 @@ type Config struct {
3943
// swap server.
4044
ReservationClient swapserverrpc.ReservationServiceClient
4145

46+
// LightningClient is the lnd client used to handle invoices decoding.
47+
LightningClient lndclient.LightningClient
48+
49+
// RouterClient is used to send the offchain payments.
50+
RouterClient lndclient.RouterClient
51+
4252
// NotificationManager is the manager that handles the notification
4353
// subscriptions.
4454
NotificationManager NotificationManager
@@ -75,6 +85,10 @@ func NewFSMFromReservation(cfg *Config, reservation *Reservation) *FSM {
7585
switch reservation.ProtocolVersion {
7686
case ProtocolVersionServerInitiated:
7787
states = reservationFsm.GetServerInitiatedReservationStates()
88+
89+
case ProtocolVersionClientInitiated:
90+
states = reservationFsm.GetClientInitiatedReservationStates()
91+
7892
default:
7993
states = make(fsm.States)
8094
}
@@ -93,6 +107,10 @@ var (
93107
// Init is the initial state of the reservation.
94108
Init = fsm.StateType("Init")
95109

110+
// SendPrepaymentPayment is the state where the client sends the payment to the
111+
// server.
112+
SendPrepaymentPayment = fsm.StateType("SendPayment")
113+
96114
// WaitForConfirmation is the state where we wait for the reservation
97115
// tx to be confirmed.
98116
WaitForConfirmation = fsm.StateType("WaitForConfirmation")
@@ -120,6 +138,10 @@ var (
120138
// requests a new reservation.
121139
OnServerRequest = fsm.EventType("OnServerRequest")
122140

141+
// OnClientInitialized is the event that is triggered when the client
142+
// has initialized the reservation.
143+
OnClientInitialized = fsm.EventType("OnClientInitialized")
144+
123145
// OnBroadcast is the event that is triggered when the reservation tx
124146
// has been broadcast.
125147
OnBroadcast = fsm.EventType("OnBroadcast")
@@ -153,6 +175,80 @@ var (
153175
OnUnlocked = fsm.EventType("OnUnlocked")
154176
)
155177

178+
// GetClientInitiatedReservationStates returns the statemap that defines the
179+
// reservation state machine, where the client initiates the reservation.
180+
func (f *FSM) GetClientInitiatedReservationStates() fsm.States {
181+
return fsm.States{
182+
fsm.EmptyState: fsm.State{
183+
Transitions: fsm.Transitions{
184+
OnClientInitialized: Init,
185+
},
186+
Action: nil,
187+
},
188+
Init: fsm.State{
189+
Transitions: fsm.Transitions{
190+
OnClientInitialized: SendPrepaymentPayment,
191+
OnRecover: Failed,
192+
fsm.OnError: Failed,
193+
},
194+
Action: f.InitFromClientRequestAction,
195+
},
196+
SendPrepaymentPayment: fsm.State{
197+
Transitions: fsm.Transitions{
198+
OnBroadcast: WaitForConfirmation,
199+
OnRecover: SendPrepaymentPayment,
200+
fsm.OnError: Failed,
201+
},
202+
Action: f.SendPrepayment,
203+
},
204+
WaitForConfirmation: fsm.State{
205+
Transitions: fsm.Transitions{
206+
OnRecover: WaitForConfirmation,
207+
OnConfirmed: Confirmed,
208+
OnTimedOut: TimedOut,
209+
},
210+
Action: f.SubscribeToConfirmationAction,
211+
},
212+
Confirmed: fsm.State{
213+
Transitions: fsm.Transitions{
214+
OnSpent: Spent,
215+
OnTimedOut: TimedOut,
216+
OnRecover: Confirmed,
217+
OnLocked: Locked,
218+
fsm.OnError: Confirmed,
219+
},
220+
Action: f.AsyncWaitForExpiredOrSweptAction,
221+
},
222+
Locked: fsm.State{
223+
Transitions: fsm.Transitions{
224+
OnUnlocked: Confirmed,
225+
OnTimedOut: TimedOut,
226+
OnRecover: Locked,
227+
OnSpent: Spent,
228+
fsm.OnError: Locked,
229+
},
230+
Action: f.AsyncWaitForExpiredOrSweptAction,
231+
},
232+
TimedOut: fsm.State{
233+
Transitions: fsm.Transitions{
234+
OnTimedOut: TimedOut,
235+
},
236+
Action: fsm.NoOpAction,
237+
},
238+
239+
Spent: fsm.State{
240+
Transitions: fsm.Transitions{
241+
OnSpent: Spent,
242+
},
243+
Action: fsm.NoOpAction,
244+
},
245+
246+
Failed: fsm.State{
247+
Action: fsm.NoOpAction,
248+
},
249+
}
250+
}
251+
156252
// GetServerInitiatedReservationStates returns the statemap that defines the
157253
// reservation state machine, where the server initiates the reservation.
158254
func (f *FSM) GetServerInitiatedReservationStates() fsm.States {
@@ -169,7 +265,7 @@ func (f *FSM) GetServerInitiatedReservationStates() fsm.States {
169265
OnRecover: Failed,
170266
fsm.OnError: Failed,
171267
},
172-
Action: f.InitAction,
268+
Action: f.InitFromServerRequestAction,
173269
},
174270
WaitForConfirmation: fsm.State{
175271
Transitions: fsm.Transitions{

instantout/reservation/reservation.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,9 @@ type Reservation struct {
6262
// Outpoint is the outpoint of the reservation.
6363
Outpoint *wire.OutPoint
6464

65+
// PrepayInvoice is the invoice that the client paid as a prepayment.
66+
PrepayInvoice string
67+
6568
// InitiationHeight is the height at which the reservation was
6669
// initiated.
6770
InitiationHeight int32

0 commit comments

Comments
 (0)