mirror of
https://github.com/lightninglabs/loop.git
synced 2026-08-13 12:33:03 +02:00
reservations: add client requested fsm
This commit is contained in:
parent
af017413a3
commit
23e533eebc
4 changed files with 278 additions and 12 deletions
|
|
@ -2,16 +2,179 @@ package reservation
|
|||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/btcsuite/btcd/btcec/v2"
|
||||
"github.com/btcsuite/btcd/btcutil"
|
||||
"github.com/lightninglabs/lndclient"
|
||||
"github.com/lightninglabs/loop/fsm"
|
||||
"github.com/lightninglabs/loop/swap"
|
||||
"github.com/lightninglabs/loop/swapserverrpc"
|
||||
"github.com/lightningnetwork/lnd/chainntnfs"
|
||||
"github.com/lightningnetwork/lnd/lnrpc"
|
||||
)
|
||||
|
||||
// InitReservationContext contains the request parameters for a reservation.
|
||||
type InitReservationContext struct {
|
||||
const (
|
||||
// Define route independent max routing fees. We have currently no way
|
||||
// to get a reliable estimate of the routing fees. Best we can do is
|
||||
// the minimum routing fees, which is not very indicative.
|
||||
maxRoutingFeeBase = btcutil.Amount(10)
|
||||
|
||||
maxRoutingFeeRate = int64(20000)
|
||||
)
|
||||
|
||||
var (
|
||||
// The allowed delta between what we accept as the expiry height and
|
||||
// the actual expiry height.
|
||||
expiryDelta = uint32(3)
|
||||
|
||||
// defaultPrepayTimeout is the default timeout for the prepayment.
|
||||
DefaultPrepayTimeout = time.Minute * 120
|
||||
)
|
||||
|
||||
// ClientRequestedInitContext contains the request parameters for a reservation.
|
||||
type ClientRequestedInitContext struct {
|
||||
value btcutil.Amount
|
||||
relativeExpiry uint32
|
||||
heightHint uint32
|
||||
maxPrepaymentAmt btcutil.Amount
|
||||
}
|
||||
|
||||
// InitFromClientRequestAction is the action that is executed when the
|
||||
// reservation state machine is initialized from a client request. It creates
|
||||
// the reservation in the database and sends the reservation request to the
|
||||
// server.
|
||||
func (f *FSM) InitFromClientRequestAction(ctx context.Context,
|
||||
eventCtx fsm.EventContext) fsm.EventType {
|
||||
|
||||
// Check if the context is of the correct type.
|
||||
reservationRequest, ok := eventCtx.(*ClientRequestedInitContext)
|
||||
if !ok {
|
||||
return f.HandleError(fsm.ErrInvalidContextType)
|
||||
}
|
||||
|
||||
// Create the reservation in the database.
|
||||
keyRes, err := f.cfg.Wallet.DeriveNextKey(ctx, KeyFamily)
|
||||
if err != nil {
|
||||
return f.HandleError(err)
|
||||
}
|
||||
|
||||
// Send the request to the server.
|
||||
requestResponse, err := f.cfg.ReservationClient.RequestReservation(
|
||||
ctx, &swapserverrpc.RequestReservationRequest{
|
||||
Value: uint64(reservationRequest.value),
|
||||
Expiry: reservationRequest.relativeExpiry,
|
||||
ClientKey: keyRes.PubKey.SerializeCompressed(),
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
return f.HandleError(err)
|
||||
}
|
||||
|
||||
expectedExpiry := reservationRequest.relativeExpiry +
|
||||
reservationRequest.heightHint
|
||||
|
||||
// Check that the expiry is in the delta.
|
||||
if requestResponse.Expiry < expectedExpiry-expiryDelta ||
|
||||
requestResponse.Expiry > expectedExpiry+expiryDelta {
|
||||
|
||||
return f.HandleError(
|
||||
fmt.Errorf("unexpected expiry height: %v, expected %v",
|
||||
requestResponse.Expiry, expectedExpiry))
|
||||
}
|
||||
|
||||
prepayment, err := f.cfg.LightningClient.DecodePaymentRequest(
|
||||
ctx, requestResponse.Invoice,
|
||||
)
|
||||
if err != nil {
|
||||
return f.HandleError(err)
|
||||
}
|
||||
|
||||
if prepayment.Value.ToSatoshis() > reservationRequest.maxPrepaymentAmt {
|
||||
return f.HandleError(
|
||||
errors.New("prepayment amount too high"))
|
||||
}
|
||||
|
||||
serverKey, err := btcec.ParsePubKey(requestResponse.ServerKey)
|
||||
if err != nil {
|
||||
return f.HandleError(err)
|
||||
}
|
||||
|
||||
var Id ID
|
||||
copy(Id[:], requestResponse.ReservationId)
|
||||
|
||||
reservation, err := NewReservation(
|
||||
Id, serverKey, keyRes.PubKey, reservationRequest.value,
|
||||
requestResponse.Expiry, reservationRequest.heightHint,
|
||||
keyRes.KeyLocator, ProtocolVersionClientInitiated,
|
||||
)
|
||||
if err != nil {
|
||||
return f.HandleError(err)
|
||||
}
|
||||
reservation.PrepayInvoice = requestResponse.Invoice
|
||||
f.reservation = reservation
|
||||
|
||||
// Create the reservation in the database.
|
||||
err = f.cfg.Store.CreateReservation(ctx, reservation)
|
||||
if err != nil {
|
||||
return f.HandleError(err)
|
||||
}
|
||||
|
||||
return OnClientInitialized
|
||||
}
|
||||
|
||||
// SendPrepayment is the action that is executed when the reservation
|
||||
// is initialized from a client request. It dispatches the prepayment to the
|
||||
// server and wait for it to be settled, signaling confirmation of the
|
||||
// reservation.
|
||||
func (f *FSM) SendPrepayment(ctx context.Context,
|
||||
_ fsm.EventContext) fsm.EventType {
|
||||
|
||||
prepayment, err := f.cfg.LightningClient.DecodePaymentRequest(
|
||||
ctx, f.reservation.PrepayInvoice,
|
||||
)
|
||||
if err != nil {
|
||||
return f.HandleError(err)
|
||||
}
|
||||
|
||||
payReq := lndclient.SendPaymentRequest{
|
||||
Invoice: f.reservation.PrepayInvoice,
|
||||
Timeout: DefaultPrepayTimeout,
|
||||
MaxFee: getMaxRoutingFee(prepayment.Value.ToSatoshis()),
|
||||
}
|
||||
// Send the prepayment to the server.
|
||||
payChan, errChan, err := f.cfg.RouterClient.SendPayment(
|
||||
ctx, payReq,
|
||||
)
|
||||
if err != nil {
|
||||
return f.HandleError(err)
|
||||
}
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return fsm.NoOp
|
||||
|
||||
case err := <-errChan:
|
||||
return f.HandleError(err)
|
||||
|
||||
case prepayResp := <-payChan:
|
||||
if prepayResp.State == lnrpc.Payment_FAILED {
|
||||
return f.HandleError(
|
||||
fmt.Errorf("prepayment failed: %v",
|
||||
prepayResp.FailureReason))
|
||||
}
|
||||
if prepayResp.State == lnrpc.Payment_SUCCEEDED {
|
||||
return OnBroadcast
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ServerRequestedInitContext contains the request parameters for a reservation.
|
||||
type ServerRequestedInitContext struct {
|
||||
reservationID ID
|
||||
serverPubkey *btcec.PublicKey
|
||||
value btcutil.Amount
|
||||
|
|
@ -19,14 +182,14 @@ type InitReservationContext struct {
|
|||
heightHint uint32
|
||||
}
|
||||
|
||||
// InitAction is the action that is executed when the reservation state machine
|
||||
// is initialized. It creates the reservation in the database and dispatches the
|
||||
// payment to the server.
|
||||
func (f *FSM) InitAction(ctx context.Context,
|
||||
// InitFromServerRequestAction is the action that is executed when the
|
||||
// reservation state machine is initialized from a server request. It creates
|
||||
// the reservation in the database and dispatches the payment to the server.
|
||||
func (f *FSM) InitFromServerRequestAction(ctx context.Context,
|
||||
eventCtx fsm.EventContext) fsm.EventType {
|
||||
|
||||
// Check if the context is of the correct type.
|
||||
reservationRequest, ok := eventCtx.(*InitReservationContext)
|
||||
reservationRequest, ok := eventCtx.(*ServerRequestedInitContext)
|
||||
if !ok {
|
||||
return f.HandleError(fsm.ErrInvalidContextType)
|
||||
}
|
||||
|
|
@ -240,3 +403,7 @@ func (f *FSM) handleAsyncError(ctx context.Context, err error) {
|
|||
f.Errorf("Error sending event: %v", err2)
|
||||
}
|
||||
}
|
||||
|
||||
func getMaxRoutingFee(amt btcutil.Amount) btcutil.Amount {
|
||||
return swap.CalcFee(amt, maxRoutingFeeBase, maxRoutingFeeRate)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -31,8 +31,8 @@ var (
|
|||
defaultExpiry = uint32(100)
|
||||
)
|
||||
|
||||
func newValidInitReservationContext() *InitReservationContext {
|
||||
return &InitReservationContext{
|
||||
func newValidInitReservationContext() *ServerRequestedInitContext {
|
||||
return &ServerRequestedInitContext{
|
||||
reservationID: ID{0x01},
|
||||
serverPubkey: defaultPubkey,
|
||||
value: defaultValue,
|
||||
|
|
@ -174,7 +174,7 @@ func TestInitReservationAction(t *testing.T) {
|
|||
StateMachine: &fsm.StateMachine{},
|
||||
}
|
||||
|
||||
event := reservationFSM.InitAction(ctxb, tc.eventCtx)
|
||||
event := reservationFSM.InitFromServerRequestAction(ctxb, tc.eventCtx)
|
||||
require.Equal(t, tc.expectedEvent, event)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -22,7 +22,11 @@ const (
|
|||
|
||||
// ProtocolVersionServerInitiated is the protocol version where the
|
||||
// server initiates the reservation.
|
||||
ProtocolVersionServerInitiated ProtocolVersion = 0
|
||||
ProtocolVersionServerInitiated ProtocolVersion = 1
|
||||
|
||||
// ProtocolVersionClientInitiated is the protocol version where the
|
||||
// client initiates the reservation.
|
||||
ProtocolVersionClientInitiated ProtocolVersion = 2
|
||||
)
|
||||
|
||||
const (
|
||||
|
|
@ -45,6 +49,12 @@ type Config struct {
|
|||
// swap server.
|
||||
ReservationClient swapserverrpc.ReservationServiceClient
|
||||
|
||||
// LightningClient is the lnd client used to handle invoices decoding.
|
||||
LightningClient lndclient.LightningClient
|
||||
|
||||
// RouterClient is used to send the offchain payments.
|
||||
RouterClient lndclient.RouterClient
|
||||
|
||||
// NotificationManager is the manager that handles the notification
|
||||
// subscriptions.
|
||||
NotificationManager NotificationManager
|
||||
|
|
@ -81,6 +91,10 @@ func NewFSMFromReservation(cfg *Config, reservation *Reservation) *FSM {
|
|||
switch reservation.ProtocolVersion {
|
||||
case ProtocolVersionServerInitiated:
|
||||
states = reservationFsm.GetServerInitiatedReservationStates()
|
||||
|
||||
case ProtocolVersionClientInitiated:
|
||||
states = reservationFsm.GetClientInitiatedReservationStates()
|
||||
|
||||
default:
|
||||
states = make(fsm.States)
|
||||
}
|
||||
|
|
@ -99,6 +113,10 @@ var (
|
|||
// Init is the initial state of the reservation.
|
||||
Init = fsm.StateType("Init")
|
||||
|
||||
// SendPrepaymentPayment is the state where the client sends the payment to the
|
||||
// server.
|
||||
SendPrepaymentPayment = fsm.StateType("SendPayment")
|
||||
|
||||
// WaitForConfirmation is the state where we wait for the reservation
|
||||
// tx to be confirmed.
|
||||
WaitForConfirmation = fsm.StateType("WaitForConfirmation")
|
||||
|
|
@ -126,6 +144,10 @@ var (
|
|||
// requests a new reservation.
|
||||
OnServerRequest = fsm.EventType("OnServerRequest")
|
||||
|
||||
// OnClientInitialized is the event that is triggered when the client
|
||||
// has initialized the reservation.
|
||||
OnClientInitialized = fsm.EventType("OnClientInitialized")
|
||||
|
||||
// OnBroadcast is the event that is triggered when the reservation tx
|
||||
// has been broadcast.
|
||||
OnBroadcast = fsm.EventType("OnBroadcast")
|
||||
|
|
@ -159,6 +181,80 @@ var (
|
|||
OnUnlocked = fsm.EventType("OnUnlocked")
|
||||
)
|
||||
|
||||
// GetClientInitiatedReservationStates returns the statemap that defines the
|
||||
// reservation state machine, where the client initiates the reservation.
|
||||
func (f *FSM) GetClientInitiatedReservationStates() fsm.States {
|
||||
return fsm.States{
|
||||
fsm.EmptyState: fsm.State{
|
||||
Transitions: fsm.Transitions{
|
||||
OnClientInitialized: Init,
|
||||
},
|
||||
Action: nil,
|
||||
},
|
||||
Init: fsm.State{
|
||||
Transitions: fsm.Transitions{
|
||||
OnClientInitialized: SendPrepaymentPayment,
|
||||
OnRecover: Failed,
|
||||
fsm.OnError: Failed,
|
||||
},
|
||||
Action: f.InitFromClientRequestAction,
|
||||
},
|
||||
SendPrepaymentPayment: fsm.State{
|
||||
Transitions: fsm.Transitions{
|
||||
OnBroadcast: WaitForConfirmation,
|
||||
OnRecover: SendPrepaymentPayment,
|
||||
fsm.OnError: Failed,
|
||||
},
|
||||
Action: f.SendPrepayment,
|
||||
},
|
||||
WaitForConfirmation: fsm.State{
|
||||
Transitions: fsm.Transitions{
|
||||
OnRecover: WaitForConfirmation,
|
||||
OnConfirmed: Confirmed,
|
||||
OnTimedOut: TimedOut,
|
||||
},
|
||||
Action: f.SubscribeToConfirmationAction,
|
||||
},
|
||||
Confirmed: fsm.State{
|
||||
Transitions: fsm.Transitions{
|
||||
OnSpent: Spent,
|
||||
OnTimedOut: TimedOut,
|
||||
OnRecover: Confirmed,
|
||||
OnLocked: Locked,
|
||||
fsm.OnError: Confirmed,
|
||||
},
|
||||
Action: f.AsyncWaitForExpiredOrSweptAction,
|
||||
},
|
||||
Locked: fsm.State{
|
||||
Transitions: fsm.Transitions{
|
||||
OnUnlocked: Confirmed,
|
||||
OnTimedOut: TimedOut,
|
||||
OnRecover: Locked,
|
||||
OnSpent: Spent,
|
||||
fsm.OnError: Locked,
|
||||
},
|
||||
Action: f.AsyncWaitForExpiredOrSweptAction,
|
||||
},
|
||||
TimedOut: fsm.State{
|
||||
Transitions: fsm.Transitions{
|
||||
OnTimedOut: TimedOut,
|
||||
},
|
||||
Action: fsm.NoOpAction,
|
||||
},
|
||||
|
||||
Spent: fsm.State{
|
||||
Transitions: fsm.Transitions{
|
||||
OnSpent: Spent,
|
||||
},
|
||||
Action: fsm.NoOpAction,
|
||||
},
|
||||
|
||||
Failed: fsm.State{
|
||||
Action: fsm.NoOpAction,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// GetServerInitiatedReservationStates returns the statemap that defines the
|
||||
// reservation state machine, where the server initiates the reservation.
|
||||
func (f *FSM) GetServerInitiatedReservationStates() fsm.States {
|
||||
|
|
@ -175,7 +271,7 @@ func (f *FSM) GetServerInitiatedReservationStates() fsm.States {
|
|||
OnRecover: Failed,
|
||||
fsm.OnError: Failed,
|
||||
},
|
||||
Action: f.InitAction,
|
||||
Action: f.InitFromServerRequestAction,
|
||||
},
|
||||
WaitForConfirmation: fsm.State{
|
||||
Transitions: fsm.Transitions{
|
||||
|
|
|
|||
|
|
@ -62,6 +62,9 @@ type Reservation struct {
|
|||
// Outpoint is the outpoint of the reservation.
|
||||
Outpoint *wire.OutPoint
|
||||
|
||||
// PrepayInvoice is the invoice that the client paid as a prepayment.
|
||||
PrepayInvoice string
|
||||
|
||||
// InitiationHeight is the height at which the reservation was
|
||||
// initiated.
|
||||
InitiationHeight int32
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue