From 8aeeaefbafe7887ff06b81055a117b239cdda43b Mon Sep 17 00:00:00 2001 From: Oliver Gugger Date: Fri, 10 Jan 2020 11:21:55 +0100 Subject: [PATCH 1/3] loopd+lsat: add LSAT cost configuration parameters --- loopd/config.go | 7 ++++++- lsat/interceptor.go | 14 +++++++++----- 2 files changed, 15 insertions(+), 6 deletions(-) diff --git a/loopd/config.go b/loopd/config.go index fdeb496..24e0714 100644 --- a/loopd/config.go +++ b/loopd/config.go @@ -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 =,=,... 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 =,=,... 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", }, diff --git a/lsat/interceptor.go b/lsat/interceptor.go index 131a50b..5c70065 100644 --- a/lsat/interceptor.go +++ b/lsat/interceptor.go @@ -33,10 +33,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. @@ -238,7 +242,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, DefaultMaxRoutingFeeSats, nil, ) select { case result := <-respChan: From ccdbc3b21bad3df75cece0b8ea904c140d220304 Mon Sep 17 00:00:00 2001 From: Oliver Gugger Date: Fri, 10 Jan 2020 12:13:39 +0100 Subject: [PATCH 2/3] multi: thread new config values through client --- client.go | 5 +++-- loopd/daemon.go | 5 +---- loopd/utils.go | 11 +++++++---- loopd/view.go | 5 +---- lsat/interceptor.go | 18 ++++++++++++++++-- swap_server_client.go | 6 +++--- 6 files changed, 31 insertions(+), 19 deletions(-) diff --git a/client.go b/client.go index 4f32173..27f7e6a 100644 --- a/client.go +++ b/client.go @@ -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 diff --git a/loopd/daemon.go b/loopd/daemon.go index a72219e..4989579 100644 --- a/loopd/daemon.go +++ b/loopd/daemon.go @@ -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 } diff --git a/loopd/utils.go b/loopd/utils.go index 9b66685..a03c477 100644 --- a/loopd/utils.go +++ b/loopd/utils.go @@ -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 diff --git a/loopd/view.go b/loopd/view.go index b9a6b30..433158c 100644 --- a/loopd/view.go +++ b/loopd/view.go @@ -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 } diff --git a/lsat/interceptor.go b/lsat/interceptor.go index 5c70065..ba7066f 100644 --- a/lsat/interceptor.go +++ b/lsat/interceptor.go @@ -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" @@ -67,6 +68,8 @@ type Interceptor struct { lnd *lndclient.LndServices store Store callTimeout time.Duration + maxCost btcutil.Amount + maxFee btcutil.Amount lock sync.Mutex } @@ -74,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, } } @@ -226,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) @@ -242,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, DefaultMaxRoutingFeeSats, nil, + payCtx, invoiceStr, i.maxFee, nil, ) select { case result := <-respChan: diff --git a/swap_server_client.go b/swap_server_client.go index b8e67f6..95c4786 100644 --- a/swap_server_client.go +++ b/swap_server_client.go @@ -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, From 202edd05f2f22f60e7b8f1d3661df36e3ee21b94 Mon Sep 17 00:00:00 2001 From: Oliver Gugger Date: Fri, 10 Jan 2020 12:14:04 +0100 Subject: [PATCH 3/3] lsat: test cost maximum in interceptor --- lsat/interceptor_test.go | 52 +++++++++++++++++++++++++++++++++------- 1 file changed, 43 insertions(+), 9 deletions(-) diff --git a/lsat/interceptor_test.go b/lsat/interceptor_test.go index 2e16a3c..42a26a3 100644 --- a/lsat/interceptor_test.go +++ b/lsat/interceptor_test.go @@ -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) }