Skip to content

Commit 0d08f31

Browse files
committed
reservations: add client requested fsm
1 parent 37cf31a commit 0d08f31

File tree

4 files changed

+278
-12
lines changed

4 files changed

+278
-12
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: 98 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,11 @@ type ProtocolVersion uint32
1313
const (
1414
// ProtocolVersionServerInitiated is the protocol version where the
1515
// server initiates the reservation.
16-
ProtocolVersionServerInitiated ProtocolVersion = 0
16+
ProtocolVersionServerInitiated ProtocolVersion = 1
17+
18+
// ProtocolVersionClientInitiated is the protocol version where the
19+
// client initiates the reservation.
20+
ProtocolVersionClientInitiated ProtocolVersion = 2
1721
)
1822

1923
const (
@@ -36,6 +40,12 @@ type Config struct {
3640
// swap server.
3741
ReservationClient swapserverrpc.ReservationServiceClient
3842

43+
// LightningClient is the lnd client used to handle invoices decoding.
44+
LightningClient lndclient.LightningClient
45+
46+
// RouterClient is used to send the offchain payments.
47+
RouterClient lndclient.RouterClient
48+
3949
// NotificationManager is the manager that handles the notification
4050
// subscriptions.
4151
NotificationManager NotificationManager
@@ -72,6 +82,10 @@ func NewFSMFromReservation(cfg *Config, reservation *Reservation) *FSM {
7282
switch reservation.ProtocolVersion {
7383
case ProtocolVersionServerInitiated:
7484
states = reservationFsm.GetServerInitiatedReservationStates()
85+
86+
case ProtocolVersionClientInitiated:
87+
states = reservationFsm.GetClientInitiatedReservationStates()
88+
7589
default:
7690
states = make(fsm.States)
7791
}
@@ -90,6 +104,10 @@ var (
90104
// Init is the initial state of the reservation.
91105
Init = fsm.StateType("Init")
92106

107+
// SendPrepaymentPayment is the state where the client sends the payment to the
108+
// server.
109+
SendPrepaymentPayment = fsm.StateType("SendPayment")
110+
93111
// WaitForConfirmation is the state where we wait for the reservation
94112
// tx to be confirmed.
95113
WaitForConfirmation = fsm.StateType("WaitForConfirmation")
@@ -117,6 +135,10 @@ var (
117135
// requests a new reservation.
118136
OnServerRequest = fsm.EventType("OnServerRequest")
119137

138+
// OnClientInitialized is the event that is triggered when the client
139+
// has initialized the reservation.
140+
OnClientInitialized = fsm.EventType("OnClientInitialized")
141+
120142
// OnBroadcast is the event that is triggered when the reservation tx
121143
// has been broadcast.
122144
OnBroadcast = fsm.EventType("OnBroadcast")
@@ -150,6 +172,80 @@ var (
150172
OnUnlocked = fsm.EventType("OnUnlocked")
151173
)
152174

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