mirror of
https://github.com/lightninglabs/loop.git
synced 2026-08-16 13:00:37 +02:00
Merge pull request #132 from guggero/validate-lsat-cost
lsat: validate cost of auth token
This commit is contained in:
commit
ea43a16577
8 changed files with 88 additions and 33 deletions
|
|
@ -79,8 +79,8 @@ type Client struct {
|
|||
|
||||
// NewClient returns a new instance to initiate swaps with.
|
||||
func NewClient(dbDir string, serverAddress string, insecure bool,
|
||||
tlsPathServer string, lnd *lndclient.LndServices) (*Client, func(),
|
||||
error) {
|
||||
tlsPathServer string, lnd *lndclient.LndServices, maxLSATCost,
|
||||
maxLSATFee btcutil.Amount) (*Client, func(), error) {
|
||||
|
||||
store, err := loopdb.NewBoltSwapStore(dbDir, lnd.ChainParams)
|
||||
if err != nil {
|
||||
|
|
@ -93,6 +93,7 @@ func NewClient(dbDir string, serverAddress string, insecure bool,
|
|||
|
||||
swapServerClient, err := newSwapServerClient(
|
||||
serverAddress, insecure, tlsPathServer, lsatStore, lnd,
|
||||
maxLSATCost, maxLSATFee,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import (
|
|||
"path/filepath"
|
||||
|
||||
"github.com/btcsuite/btcutil"
|
||||
"github.com/lightninglabs/loop/lsat"
|
||||
)
|
||||
|
||||
var (
|
||||
|
|
@ -39,7 +40,9 @@ type config struct {
|
|||
MaxLogFiles int `long:"maxlogfiles" description:"Maximum logfiles to keep (0 for no rotation)"`
|
||||
MaxLogFileSize int `long:"maxlogfilesize" description:"Maximum logfile size in MB"`
|
||||
|
||||
DebugLevel string `short:"d" long:"debuglevel" description:"Logging level for all subsystems {trace, debug, info, warn, error, critical} -- You may also specify <subsystem>=<level>,<subsystem2>=<level>,... to set the log level for individual subsystems -- Use show to list available subsystems"`
|
||||
DebugLevel string `short:"d" long:"debuglevel" description:"Logging level for all subsystems {trace, debug, info, warn, error, critical} -- You may also specify <subsystem>=<level>,<subsystem2>=<level>,... to set the log level for individual subsystems -- Use show to list available subsystems"`
|
||||
MaxLSATCost uint32 `long:"maxlsatcost" description:"Maximum cost in satoshis that loopd is going to pay for an LSAT token automatically. Does not include routing fees."`
|
||||
MaxLSATFee uint32 `long:"maxlsatfee" description:"Maximum routing fee in satoshis that we are willing to pay while paying for an LSAT token."`
|
||||
|
||||
Lnd *lndConfig `group:"lnd" namespace:"lnd"`
|
||||
|
||||
|
|
@ -60,6 +63,8 @@ var defaultConfig = config{
|
|||
MaxLogFiles: defaultMaxLogFiles,
|
||||
MaxLogFileSize: defaultMaxLogFileSize,
|
||||
DebugLevel: defaultLogLevel,
|
||||
MaxLSATCost: lsat.DefaultMaxCostSats,
|
||||
MaxLSATFee: lsat.DefaultMaxRoutingFeeSats,
|
||||
Lnd: &lndConfig{
|
||||
Host: "localhost:10009",
|
||||
},
|
||||
|
|
|
|||
|
|
@ -56,10 +56,7 @@ func daemon(config *config, lisCfg *listenerCfg) error {
|
|||
log.Infof("Swap server address: %v", config.SwapServer)
|
||||
|
||||
// Create an instance of the loop client library.
|
||||
swapClient, cleanup, err := getClient(
|
||||
config.Network, config.SwapServer, config.Insecure,
|
||||
config.TLSPathSwapSrv, &lnd.LndServices,
|
||||
)
|
||||
swapClient, cleanup, err := getClient(config, &lnd.LndServices)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,21 +4,24 @@ import (
|
|||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/btcsuite/btcutil"
|
||||
"github.com/lightninglabs/loop"
|
||||
"github.com/lightninglabs/loop/lndclient"
|
||||
)
|
||||
|
||||
// getClient returns an instance of the swap client.
|
||||
func getClient(network, swapServer string, insecure bool, tlsPathServer string,
|
||||
lnd *lndclient.LndServices) (*loop.Client, func(), error) {
|
||||
func getClient(config *config, lnd *lndclient.LndServices) (*loop.Client,
|
||||
func(), error) {
|
||||
|
||||
storeDir, err := getStoreDir(network)
|
||||
storeDir, err := getStoreDir(config.Network)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
swapClient, cleanUp, err := loop.NewClient(
|
||||
storeDir, swapServer, insecure, tlsPathServer, lnd,
|
||||
storeDir, config.SwapServer, config.Insecure,
|
||||
config.TLSPathSwapSrv, lnd, btcutil.Amount(config.MaxLSATCost),
|
||||
btcutil.Amount(config.MaxLSATFee),
|
||||
)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
|
|
|
|||
|
|
@ -23,10 +23,7 @@ func view(config *config, lisCfg *listenerCfg) error {
|
|||
}
|
||||
defer lnd.Close()
|
||||
|
||||
swapClient, cleanup, err := getClient(
|
||||
config.Network, config.SwapServer, config.Insecure,
|
||||
config.TLSPathSwapSrv, &lnd.LndServices,
|
||||
)
|
||||
swapClient, cleanup, err := getClient(config, &lnd.LndServices)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import (
|
|||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/btcsuite/btcutil"
|
||||
"github.com/lightninglabs/loop/lndclient"
|
||||
"github.com/lightningnetwork/lnd/lnrpc/routerrpc"
|
||||
"github.com/lightningnetwork/lnd/lnwire"
|
||||
|
|
@ -33,10 +34,14 @@ const (
|
|||
// challenge.
|
||||
AuthHeader = "WWW-Authenticate"
|
||||
|
||||
// MaxRoutingFee is the maximum routing fee in satoshis that we are
|
||||
// going to pay to acquire an LSAT token.
|
||||
// TODO(guggero): make this configurable
|
||||
MaxRoutingFeeSats = 10
|
||||
// DefaultMaxCostSats is the default maximum amount in satoshis that we
|
||||
// are going to pay for an LSAT automatically. Does not include routing
|
||||
// fees.
|
||||
DefaultMaxCostSats = 1000
|
||||
|
||||
// DefaultMaxRoutingFeeSats is the default maximum routing fee in
|
||||
// satoshis that we are going to pay to acquire an LSAT token.
|
||||
DefaultMaxRoutingFeeSats = 10
|
||||
|
||||
// PaymentTimeout is the maximum time we allow a payment to take before
|
||||
// we stop waiting for it.
|
||||
|
|
@ -63,6 +68,8 @@ type Interceptor struct {
|
|||
lnd *lndclient.LndServices
|
||||
store Store
|
||||
callTimeout time.Duration
|
||||
maxCost btcutil.Amount
|
||||
maxFee btcutil.Amount
|
||||
lock sync.Mutex
|
||||
}
|
||||
|
||||
|
|
@ -70,12 +77,15 @@ type Interceptor struct {
|
|||
// lnd connection to automatically acquire and pay for LSAT tokens, unless the
|
||||
// indicated store already contains a usable token.
|
||||
func NewInterceptor(lnd *lndclient.LndServices, store Store,
|
||||
rpcCallTimeout time.Duration) *Interceptor {
|
||||
rpcCallTimeout time.Duration, maxCost,
|
||||
maxFee btcutil.Amount) *Interceptor {
|
||||
|
||||
return &Interceptor{
|
||||
lnd: lnd,
|
||||
store: store,
|
||||
callTimeout: rpcCallTimeout,
|
||||
maxCost: maxCost,
|
||||
maxFee: maxFee,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -222,6 +232,14 @@ func (i *Interceptor) payLsatToken(ctx context.Context, md *metadata.MD) (
|
|||
return nil, fmt.Errorf("unable to decode invoice: %v", err)
|
||||
}
|
||||
|
||||
// Check that the charged amount does not exceed our maximum cost.
|
||||
maxCostMsat := lnwire.NewMSatFromSatoshis(i.maxCost)
|
||||
if invoice.MilliSat != nil && *invoice.MilliSat > maxCostMsat {
|
||||
return nil, fmt.Errorf("cannot pay for LSAT automatically, "+
|
||||
"cost of %d msat exceeds configured max cost of %d "+
|
||||
"msat", *invoice.MilliSat, maxCostMsat)
|
||||
}
|
||||
|
||||
// Create and store the pending token so we can resume the payment in
|
||||
// case the payment is interrupted somehow.
|
||||
token, err := tokenFromChallenge(macBytes, invoice.PaymentHash)
|
||||
|
|
@ -238,7 +256,7 @@ func (i *Interceptor) payLsatToken(ctx context.Context, md *metadata.MD) (
|
|||
payCtx, cancel := context.WithTimeout(ctx, PaymentTimeout)
|
||||
defer cancel()
|
||||
respChan := i.lnd.Client.PayInvoice(
|
||||
payCtx, invoiceStr, MaxRoutingFeeSats, nil,
|
||||
payCtx, invoiceStr, i.maxFee, nil,
|
||||
)
|
||||
select {
|
||||
case result := <-respChan:
|
||||
|
|
|
|||
|
|
@ -50,6 +50,7 @@ func TestInterceptor(t *testing.T) {
|
|||
testTimeout = 5 * time.Second
|
||||
interceptor = NewInterceptor(
|
||||
&lnd.LndServices, store, testTimeout,
|
||||
DefaultMaxCostSats, DefaultMaxRoutingFeeSats,
|
||||
)
|
||||
testMac = makeMac(t)
|
||||
testMacBytes = serializeMac(t, testMac)
|
||||
|
|
@ -84,11 +85,13 @@ func TestInterceptor(t *testing.T) {
|
|||
testCases := []struct {
|
||||
name string
|
||||
initialToken *Token
|
||||
interceptor *Interceptor
|
||||
resetCb func()
|
||||
expectLndCall bool
|
||||
sendPaymentCb func(msg test.PaymentChannelMessage)
|
||||
trackPaymentCb func(msg test.TrackPaymentMessage)
|
||||
expectToken bool
|
||||
expectInterceptErr string
|
||||
expectBackendCalls int
|
||||
expectMacaroonCall1 bool
|
||||
expectMacaroonCall2 bool
|
||||
|
|
@ -96,6 +99,7 @@ func TestInterceptor(t *testing.T) {
|
|||
{
|
||||
name: "no auth required happy path",
|
||||
initialToken: nil,
|
||||
interceptor: interceptor,
|
||||
resetCb: func() { resetBackend(nil, "") },
|
||||
expectLndCall: false,
|
||||
expectToken: false,
|
||||
|
|
@ -106,6 +110,7 @@ func TestInterceptor(t *testing.T) {
|
|||
{
|
||||
name: "auth required, no token yet",
|
||||
initialToken: nil,
|
||||
interceptor: interceptor,
|
||||
resetCb: func() {
|
||||
resetBackend(
|
||||
status.New(
|
||||
|
|
@ -140,6 +145,7 @@ func TestInterceptor(t *testing.T) {
|
|||
{
|
||||
name: "auth required, has token",
|
||||
initialToken: paidToken,
|
||||
interceptor: interceptor,
|
||||
resetCb: func() { resetBackend(nil, "") },
|
||||
expectLndCall: false,
|
||||
expectToken: true,
|
||||
|
|
@ -150,6 +156,7 @@ func TestInterceptor(t *testing.T) {
|
|||
{
|
||||
name: "auth required, has pending token",
|
||||
initialToken: pendingToken,
|
||||
interceptor: interceptor,
|
||||
resetCb: func() {
|
||||
resetBackend(
|
||||
status.New(
|
||||
|
|
@ -177,6 +184,30 @@ func TestInterceptor(t *testing.T) {
|
|||
expectMacaroonCall1: false,
|
||||
expectMacaroonCall2: true,
|
||||
},
|
||||
{
|
||||
name: "auth required, no token yet, cost limit",
|
||||
initialToken: nil,
|
||||
interceptor: NewInterceptor(
|
||||
&lnd.LndServices, store, testTimeout,
|
||||
100, DefaultMaxRoutingFeeSats,
|
||||
),
|
||||
resetCb: func() {
|
||||
resetBackend(
|
||||
status.New(
|
||||
GRPCErrCode, GRPCErrMessage,
|
||||
).Err(),
|
||||
makeAuthHeader(testMacBytes),
|
||||
)
|
||||
},
|
||||
expectLndCall: false,
|
||||
expectToken: false,
|
||||
expectInterceptErr: "cannot pay for LSAT " +
|
||||
"automatically, cost of 500000 msat exceeds " +
|
||||
"configured max cost of 100000 msat",
|
||||
expectBackendCalls: 1,
|
||||
expectMacaroonCall1: false,
|
||||
expectMacaroonCall2: false,
|
||||
},
|
||||
}
|
||||
|
||||
// The invoker is a simple function that simulates the actual call to
|
||||
|
|
@ -219,11 +250,14 @@ func TestInterceptor(t *testing.T) {
|
|||
backendWg.Add(1)
|
||||
overallWg.Add(1)
|
||||
go func() {
|
||||
err := interceptor.UnaryInterceptor(
|
||||
err := tc.interceptor.UnaryInterceptor(
|
||||
ctx, "", nil, nil, nil, invoker, nil,
|
||||
)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
if err != nil && tc.expectInterceptErr != "" &&
|
||||
err.Error() != tc.expectInterceptErr {
|
||||
panic(fmt.Errorf("unexpected error '%s', "+
|
||||
"expected '%s'", err.Error(),
|
||||
tc.expectInterceptErr))
|
||||
}
|
||||
overallWg.Done()
|
||||
}()
|
||||
|
|
@ -318,12 +352,12 @@ func serializeMac(t *testing.T, mac *macaroon.Macaroon) []byte {
|
|||
}
|
||||
|
||||
func makeAuthHeader(macBytes []byte) string {
|
||||
// Testnet invoice, copied from lnd/zpay32/invoice_test.go
|
||||
invoice := "lntb20m1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqc" +
|
||||
"yq5rqwzqfqypqhp58yjmdan79s6qqdhdzgynm4zwqd5d7xmw5fk98klysy04" +
|
||||
"3l2ahrqsfpp3x9et2e20v6pu37c5d9vax37wxq72un98k6vcx9fz94w0qf23" +
|
||||
"7cm2rqv9pmn5lnexfvf5579slr4zq3u8kmczecytdx0xg9rwzngp7e6guwqp" +
|
||||
"qlhssu04sucpnz4axcv2dstmknqq6jsk2l"
|
||||
// Testnet invoice over 500 sats.
|
||||
invoice := "lntb5u1p0pskpmpp5jzw9xvdast2g5lm5tswq6n64t2epe3f4xav43dyd" +
|
||||
"239qr8h3yllqdqqcqzpgsp5m8sfjqgugthk66q3tr4gsqr5rh740jrq9x4l0" +
|
||||
"kvj5e77nmwqvpnq9qy9qsq72afzu7sfuppzqg3q2pn49hlh66rv7w60h2rua" +
|
||||
"hx857g94s066yzxcjn4yccqc79779sd232v9ewluvu0tmusvht6r99rld8xs" +
|
||||
"k287cpyac79r"
|
||||
return fmt.Sprintf("LSAT macaroon=\"%s\", invoice=\"%s\"",
|
||||
base64.StdEncoding.EncodeToString(macBytes), invoice)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -52,13 +52,13 @@ type grpcSwapServerClient struct {
|
|||
var _ swapServerClient = (*grpcSwapServerClient)(nil)
|
||||
|
||||
func newSwapServerClient(address string, insecure bool, tlsPath string,
|
||||
lsatStore lsat.Store, lnd *lndclient.LndServices) (
|
||||
*grpcSwapServerClient, error) {
|
||||
lsatStore lsat.Store, lnd *lndclient.LndServices,
|
||||
maxLSATCost, maxLSATFee btcutil.Amount) (*grpcSwapServerClient, error) {
|
||||
|
||||
// Create the server connection with the interceptor that will handle
|
||||
// the LSAT protocol for us.
|
||||
clientInterceptor := lsat.NewInterceptor(
|
||||
lnd, lsatStore, serverRPCTimeout,
|
||||
lnd, lsatStore, serverRPCTimeout, maxLSATCost, maxLSATFee,
|
||||
)
|
||||
serverConn, err := getSwapServerConn(
|
||||
address, insecure, tlsPath, clientInterceptor,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue