Skip to content

Commit ea43a16

Browse files
authored
Merge pull request #132 from guggero/validate-lsat-cost
lsat: validate cost of auth token
2 parents e4ffb08 + 202edd0 commit ea43a16

File tree

8 files changed

+88
-33
lines changed

8 files changed

+88
-33
lines changed

client.go

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -79,8 +79,8 @@ type Client struct {
7979

8080
// NewClient returns a new instance to initiate swaps with.
8181
func NewClient(dbDir string, serverAddress string, insecure bool,
82-
tlsPathServer string, lnd *lndclient.LndServices) (*Client, func(),
83-
error) {
82+
tlsPathServer string, lnd *lndclient.LndServices, maxLSATCost,
83+
maxLSATFee btcutil.Amount) (*Client, func(), error) {
8484

8585
store, err := loopdb.NewBoltSwapStore(dbDir, lnd.ChainParams)
8686
if err != nil {
@@ -93,6 +93,7 @@ func NewClient(dbDir string, serverAddress string, insecure bool,
9393

9494
swapServerClient, err := newSwapServerClient(
9595
serverAddress, insecure, tlsPathServer, lsatStore, lnd,
96+
maxLSATCost, maxLSATFee,
9697
)
9798
if err != nil {
9899
return nil, nil, err

loopd/config.go

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"path/filepath"
55

66
"github.com/btcsuite/btcutil"
7+
"github.com/lightninglabs/loop/lsat"
78
)
89

910
var (
@@ -39,7 +40,9 @@ type config struct {
3940
MaxLogFiles int `long:"maxlogfiles" description:"Maximum logfiles to keep (0 for no rotation)"`
4041
MaxLogFileSize int `long:"maxlogfilesize" description:"Maximum logfile size in MB"`
4142

42-
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"`
43+
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"`
44+
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."`
45+
MaxLSATFee uint32 `long:"maxlsatfee" description:"Maximum routing fee in satoshis that we are willing to pay while paying for an LSAT token."`
4346

4447
Lnd *lndConfig `group:"lnd" namespace:"lnd"`
4548

@@ -60,6 +63,8 @@ var defaultConfig = config{
6063
MaxLogFiles: defaultMaxLogFiles,
6164
MaxLogFileSize: defaultMaxLogFileSize,
6265
DebugLevel: defaultLogLevel,
66+
MaxLSATCost: lsat.DefaultMaxCostSats,
67+
MaxLSATFee: lsat.DefaultMaxRoutingFeeSats,
6368
Lnd: &lndConfig{
6469
Host: "localhost:10009",
6570
},

loopd/daemon.go

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -56,10 +56,7 @@ func daemon(config *config, lisCfg *listenerCfg) error {
5656
log.Infof("Swap server address: %v", config.SwapServer)
5757

5858
// Create an instance of the loop client library.
59-
swapClient, cleanup, err := getClient(
60-
config.Network, config.SwapServer, config.Insecure,
61-
config.TLSPathSwapSrv, &lnd.LndServices,
62-
)
59+
swapClient, cleanup, err := getClient(config, &lnd.LndServices)
6360
if err != nil {
6461
return err
6562
}

loopd/utils.go

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,21 +4,24 @@ import (
44
"os"
55
"path/filepath"
66

7+
"github.com/btcsuite/btcutil"
78
"github.com/lightninglabs/loop"
89
"github.com/lightninglabs/loop/lndclient"
910
)
1011

1112
// getClient returns an instance of the swap client.
12-
func getClient(network, swapServer string, insecure bool, tlsPathServer string,
13-
lnd *lndclient.LndServices) (*loop.Client, func(), error) {
13+
func getClient(config *config, lnd *lndclient.LndServices) (*loop.Client,
14+
func(), error) {
1415

15-
storeDir, err := getStoreDir(network)
16+
storeDir, err := getStoreDir(config.Network)
1617
if err != nil {
1718
return nil, nil, err
1819
}
1920

2021
swapClient, cleanUp, err := loop.NewClient(
21-
storeDir, swapServer, insecure, tlsPathServer, lnd,
22+
storeDir, config.SwapServer, config.Insecure,
23+
config.TLSPathSwapSrv, lnd, btcutil.Amount(config.MaxLSATCost),
24+
btcutil.Amount(config.MaxLSATFee),
2225
)
2326
if err != nil {
2427
return nil, nil, err

loopd/view.go

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -23,10 +23,7 @@ func view(config *config, lisCfg *listenerCfg) error {
2323
}
2424
defer lnd.Close()
2525

26-
swapClient, cleanup, err := getClient(
27-
config.Network, config.SwapServer, config.Insecure,
28-
config.TLSPathSwapSrv, &lnd.LndServices,
29-
)
26+
swapClient, cleanup, err := getClient(config, &lnd.LndServices)
3027
if err != nil {
3128
return err
3229
}

lsat/interceptor.go

Lines changed: 24 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88
"sync"
99
"time"
1010

11+
"github.com/btcsuite/btcutil"
1112
"github.com/lightninglabs/loop/lndclient"
1213
"github.com/lightningnetwork/lnd/lnrpc/routerrpc"
1314
"github.com/lightningnetwork/lnd/lnwire"
@@ -33,10 +34,14 @@ const (
3334
// challenge.
3435
AuthHeader = "WWW-Authenticate"
3536

36-
// MaxRoutingFee is the maximum routing fee in satoshis that we are
37-
// going to pay to acquire an LSAT token.
38-
// TODO(guggero): make this configurable
39-
MaxRoutingFeeSats = 10
37+
// DefaultMaxCostSats is the default maximum amount in satoshis that we
38+
// are going to pay for an LSAT automatically. Does not include routing
39+
// fees.
40+
DefaultMaxCostSats = 1000
41+
42+
// DefaultMaxRoutingFeeSats is the default maximum routing fee in
43+
// satoshis that we are going to pay to acquire an LSAT token.
44+
DefaultMaxRoutingFeeSats = 10
4045

4146
// PaymentTimeout is the maximum time we allow a payment to take before
4247
// we stop waiting for it.
@@ -63,19 +68,24 @@ type Interceptor struct {
6368
lnd *lndclient.LndServices
6469
store Store
6570
callTimeout time.Duration
71+
maxCost btcutil.Amount
72+
maxFee btcutil.Amount
6673
lock sync.Mutex
6774
}
6875

6976
// NewInterceptor creates a new gRPC client interceptor that uses the provided
7077
// lnd connection to automatically acquire and pay for LSAT tokens, unless the
7178
// indicated store already contains a usable token.
7279
func NewInterceptor(lnd *lndclient.LndServices, store Store,
73-
rpcCallTimeout time.Duration) *Interceptor {
80+
rpcCallTimeout time.Duration, maxCost,
81+
maxFee btcutil.Amount) *Interceptor {
7482

7583
return &Interceptor{
7684
lnd: lnd,
7785
store: store,
7886
callTimeout: rpcCallTimeout,
87+
maxCost: maxCost,
88+
maxFee: maxFee,
7989
}
8090
}
8191

@@ -222,6 +232,14 @@ func (i *Interceptor) payLsatToken(ctx context.Context, md *metadata.MD) (
222232
return nil, fmt.Errorf("unable to decode invoice: %v", err)
223233
}
224234

235+
// Check that the charged amount does not exceed our maximum cost.
236+
maxCostMsat := lnwire.NewMSatFromSatoshis(i.maxCost)
237+
if invoice.MilliSat != nil && *invoice.MilliSat > maxCostMsat {
238+
return nil, fmt.Errorf("cannot pay for LSAT automatically, "+
239+
"cost of %d msat exceeds configured max cost of %d "+
240+
"msat", *invoice.MilliSat, maxCostMsat)
241+
}
242+
225243
// Create and store the pending token so we can resume the payment in
226244
// case the payment is interrupted somehow.
227245
token, err := tokenFromChallenge(macBytes, invoice.PaymentHash)
@@ -238,7 +256,7 @@ func (i *Interceptor) payLsatToken(ctx context.Context, md *metadata.MD) (
238256
payCtx, cancel := context.WithTimeout(ctx, PaymentTimeout)
239257
defer cancel()
240258
respChan := i.lnd.Client.PayInvoice(
241-
payCtx, invoiceStr, MaxRoutingFeeSats, nil,
259+
payCtx, invoiceStr, i.maxFee, nil,
242260
)
243261
select {
244262
case result := <-respChan:

lsat/interceptor_test.go

Lines changed: 43 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ func TestInterceptor(t *testing.T) {
5050
testTimeout = 5 * time.Second
5151
interceptor = NewInterceptor(
5252
&lnd.LndServices, store, testTimeout,
53+
DefaultMaxCostSats, DefaultMaxRoutingFeeSats,
5354
)
5455
testMac = makeMac(t)
5556
testMacBytes = serializeMac(t, testMac)
@@ -84,18 +85,21 @@ func TestInterceptor(t *testing.T) {
8485
testCases := []struct {
8586
name string
8687
initialToken *Token
88+
interceptor *Interceptor
8789
resetCb func()
8890
expectLndCall bool
8991
sendPaymentCb func(msg test.PaymentChannelMessage)
9092
trackPaymentCb func(msg test.TrackPaymentMessage)
9193
expectToken bool
94+
expectInterceptErr string
9295
expectBackendCalls int
9396
expectMacaroonCall1 bool
9497
expectMacaroonCall2 bool
9598
}{
9699
{
97100
name: "no auth required happy path",
98101
initialToken: nil,
102+
interceptor: interceptor,
99103
resetCb: func() { resetBackend(nil, "") },
100104
expectLndCall: false,
101105
expectToken: false,
@@ -106,6 +110,7 @@ func TestInterceptor(t *testing.T) {
106110
{
107111
name: "auth required, no token yet",
108112
initialToken: nil,
113+
interceptor: interceptor,
109114
resetCb: func() {
110115
resetBackend(
111116
status.New(
@@ -140,6 +145,7 @@ func TestInterceptor(t *testing.T) {
140145
{
141146
name: "auth required, has token",
142147
initialToken: paidToken,
148+
interceptor: interceptor,
143149
resetCb: func() { resetBackend(nil, "") },
144150
expectLndCall: false,
145151
expectToken: true,
@@ -150,6 +156,7 @@ func TestInterceptor(t *testing.T) {
150156
{
151157
name: "auth required, has pending token",
152158
initialToken: pendingToken,
159+
interceptor: interceptor,
153160
resetCb: func() {
154161
resetBackend(
155162
status.New(
@@ -177,6 +184,30 @@ func TestInterceptor(t *testing.T) {
177184
expectMacaroonCall1: false,
178185
expectMacaroonCall2: true,
179186
},
187+
{
188+
name: "auth required, no token yet, cost limit",
189+
initialToken: nil,
190+
interceptor: NewInterceptor(
191+
&lnd.LndServices, store, testTimeout,
192+
100, DefaultMaxRoutingFeeSats,
193+
),
194+
resetCb: func() {
195+
resetBackend(
196+
status.New(
197+
GRPCErrCode, GRPCErrMessage,
198+
).Err(),
199+
makeAuthHeader(testMacBytes),
200+
)
201+
},
202+
expectLndCall: false,
203+
expectToken: false,
204+
expectInterceptErr: "cannot pay for LSAT " +
205+
"automatically, cost of 500000 msat exceeds " +
206+
"configured max cost of 100000 msat",
207+
expectBackendCalls: 1,
208+
expectMacaroonCall1: false,
209+
expectMacaroonCall2: false,
210+
},
180211
}
181212

182213
// The invoker is a simple function that simulates the actual call to
@@ -219,11 +250,14 @@ func TestInterceptor(t *testing.T) {
219250
backendWg.Add(1)
220251
overallWg.Add(1)
221252
go func() {
222-
err := interceptor.UnaryInterceptor(
253+
err := tc.interceptor.UnaryInterceptor(
223254
ctx, "", nil, nil, nil, invoker, nil,
224255
)
225-
if err != nil {
226-
panic(err)
256+
if err != nil && tc.expectInterceptErr != "" &&
257+
err.Error() != tc.expectInterceptErr {
258+
panic(fmt.Errorf("unexpected error '%s', "+
259+
"expected '%s'", err.Error(),
260+
tc.expectInterceptErr))
227261
}
228262
overallWg.Done()
229263
}()
@@ -318,12 +352,12 @@ func serializeMac(t *testing.T, mac *macaroon.Macaroon) []byte {
318352
}
319353

320354
func makeAuthHeader(macBytes []byte) string {
321-
// Testnet invoice, copied from lnd/zpay32/invoice_test.go
322-
invoice := "lntb20m1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqc" +
323-
"yq5rqwzqfqypqhp58yjmdan79s6qqdhdzgynm4zwqd5d7xmw5fk98klysy04" +
324-
"3l2ahrqsfpp3x9et2e20v6pu37c5d9vax37wxq72un98k6vcx9fz94w0qf23" +
325-
"7cm2rqv9pmn5lnexfvf5579slr4zq3u8kmczecytdx0xg9rwzngp7e6guwqp" +
326-
"qlhssu04sucpnz4axcv2dstmknqq6jsk2l"
355+
// Testnet invoice over 500 sats.
356+
invoice := "lntb5u1p0pskpmpp5jzw9xvdast2g5lm5tswq6n64t2epe3f4xav43dyd" +
357+
"239qr8h3yllqdqqcqzpgsp5m8sfjqgugthk66q3tr4gsqr5rh740jrq9x4l0" +
358+
"kvj5e77nmwqvpnq9qy9qsq72afzu7sfuppzqg3q2pn49hlh66rv7w60h2rua" +
359+
"hx857g94s066yzxcjn4yccqc79779sd232v9ewluvu0tmusvht6r99rld8xs" +
360+
"k287cpyac79r"
327361
return fmt.Sprintf("LSAT macaroon=\"%s\", invoice=\"%s\"",
328362
base64.StdEncoding.EncodeToString(macBytes), invoice)
329363
}

swap_server_client.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -52,13 +52,13 @@ type grpcSwapServerClient struct {
5252
var _ swapServerClient = (*grpcSwapServerClient)(nil)
5353

5454
func newSwapServerClient(address string, insecure bool, tlsPath string,
55-
lsatStore lsat.Store, lnd *lndclient.LndServices) (
56-
*grpcSwapServerClient, error) {
55+
lsatStore lsat.Store, lnd *lndclient.LndServices,
56+
maxLSATCost, maxLSATFee btcutil.Amount) (*grpcSwapServerClient, error) {
5757

5858
// Create the server connection with the interceptor that will handle
5959
// the LSAT protocol for us.
6060
clientInterceptor := lsat.NewInterceptor(
61-
lnd, lsatStore, serverRPCTimeout,
61+
lnd, lsatStore, serverRPCTimeout, maxLSATCost, maxLSATFee,
6262
)
6363
serverConn, err := getSwapServerConn(
6464
address, insecure, tlsPath, clientInterceptor,

0 commit comments

Comments
 (0)