Merge pull request #132 from guggero/validate-lsat-cost

lsat: validate cost of auth token
pull/133/head
Oliver Gugger 4 years ago committed by GitHub
commit ea43a16577
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -79,8 +79,8 @@ type Client struct {
// NewClient returns a new instance to initiate swaps with. // NewClient returns a new instance to initiate swaps with.
func NewClient(dbDir string, serverAddress string, insecure bool, func NewClient(dbDir string, serverAddress string, insecure bool,
tlsPathServer string, lnd *lndclient.LndServices) (*Client, func(), tlsPathServer string, lnd *lndclient.LndServices, maxLSATCost,
error) { maxLSATFee btcutil.Amount) (*Client, func(), error) {
store, err := loopdb.NewBoltSwapStore(dbDir, lnd.ChainParams) store, err := loopdb.NewBoltSwapStore(dbDir, lnd.ChainParams)
if err != nil { if err != nil {
@ -93,6 +93,7 @@ func NewClient(dbDir string, serverAddress string, insecure bool,
swapServerClient, err := newSwapServerClient( swapServerClient, err := newSwapServerClient(
serverAddress, insecure, tlsPathServer, lsatStore, lnd, serverAddress, insecure, tlsPathServer, lsatStore, lnd,
maxLSATCost, maxLSATFee,
) )
if err != nil { if err != nil {
return nil, nil, err return nil, nil, err

@ -4,6 +4,7 @@ import (
"path/filepath" "path/filepath"
"github.com/btcsuite/btcutil" "github.com/btcsuite/btcutil"
"github.com/lightninglabs/loop/lsat"
) )
var ( var (
@ -39,7 +40,9 @@ type config struct {
MaxLogFiles int `long:"maxlogfiles" description:"Maximum logfiles to keep (0 for no rotation)"` MaxLogFiles int `long:"maxlogfiles" description:"Maximum logfiles to keep (0 for no rotation)"`
MaxLogFileSize int `long:"maxlogfilesize" description:"Maximum logfile size in MB"` 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"` Lnd *lndConfig `group:"lnd" namespace:"lnd"`
@ -60,6 +63,8 @@ var defaultConfig = config{
MaxLogFiles: defaultMaxLogFiles, MaxLogFiles: defaultMaxLogFiles,
MaxLogFileSize: defaultMaxLogFileSize, MaxLogFileSize: defaultMaxLogFileSize,
DebugLevel: defaultLogLevel, DebugLevel: defaultLogLevel,
MaxLSATCost: lsat.DefaultMaxCostSats,
MaxLSATFee: lsat.DefaultMaxRoutingFeeSats,
Lnd: &lndConfig{ Lnd: &lndConfig{
Host: "localhost:10009", Host: "localhost:10009",
}, },

@ -56,10 +56,7 @@ func daemon(config *config, lisCfg *listenerCfg) error {
log.Infof("Swap server address: %v", config.SwapServer) log.Infof("Swap server address: %v", config.SwapServer)
// Create an instance of the loop client library. // Create an instance of the loop client library.
swapClient, cleanup, err := getClient( swapClient, cleanup, err := getClient(config, &lnd.LndServices)
config.Network, config.SwapServer, config.Insecure,
config.TLSPathSwapSrv, &lnd.LndServices,
)
if err != nil { if err != nil {
return err return err
} }

@ -4,21 +4,24 @@ import (
"os" "os"
"path/filepath" "path/filepath"
"github.com/btcsuite/btcutil"
"github.com/lightninglabs/loop" "github.com/lightninglabs/loop"
"github.com/lightninglabs/loop/lndclient" "github.com/lightninglabs/loop/lndclient"
) )
// getClient returns an instance of the swap client. // getClient returns an instance of the swap client.
func getClient(network, swapServer string, insecure bool, tlsPathServer string, func getClient(config *config, lnd *lndclient.LndServices) (*loop.Client,
lnd *lndclient.LndServices) (*loop.Client, func(), error) { func(), error) {
storeDir, err := getStoreDir(network) storeDir, err := getStoreDir(config.Network)
if err != nil { if err != nil {
return nil, nil, err return nil, nil, err
} }
swapClient, cleanUp, err := loop.NewClient( 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 { if err != nil {
return nil, nil, err return nil, nil, err

@ -23,10 +23,7 @@ func view(config *config, lisCfg *listenerCfg) error {
} }
defer lnd.Close() defer lnd.Close()
swapClient, cleanup, err := getClient( swapClient, cleanup, err := getClient(config, &lnd.LndServices)
config.Network, config.SwapServer, config.Insecure,
config.TLSPathSwapSrv, &lnd.LndServices,
)
if err != nil { if err != nil {
return err return err
} }

@ -8,6 +8,7 @@ import (
"sync" "sync"
"time" "time"
"github.com/btcsuite/btcutil"
"github.com/lightninglabs/loop/lndclient" "github.com/lightninglabs/loop/lndclient"
"github.com/lightningnetwork/lnd/lnrpc/routerrpc" "github.com/lightningnetwork/lnd/lnrpc/routerrpc"
"github.com/lightningnetwork/lnd/lnwire" "github.com/lightningnetwork/lnd/lnwire"
@ -33,10 +34,14 @@ const (
// challenge. // challenge.
AuthHeader = "WWW-Authenticate" AuthHeader = "WWW-Authenticate"
// MaxRoutingFee is the maximum routing fee in satoshis that we are // DefaultMaxCostSats is the default maximum amount in satoshis that we
// going to pay to acquire an LSAT token. // are going to pay for an LSAT automatically. Does not include routing
// TODO(guggero): make this configurable // fees.
MaxRoutingFeeSats = 10 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 // PaymentTimeout is the maximum time we allow a payment to take before
// we stop waiting for it. // we stop waiting for it.
@ -63,6 +68,8 @@ type Interceptor struct {
lnd *lndclient.LndServices lnd *lndclient.LndServices
store Store store Store
callTimeout time.Duration callTimeout time.Duration
maxCost btcutil.Amount
maxFee btcutil.Amount
lock sync.Mutex lock sync.Mutex
} }
@ -70,12 +77,15 @@ type Interceptor struct {
// lnd connection to automatically acquire and pay for LSAT tokens, unless the // lnd connection to automatically acquire and pay for LSAT tokens, unless the
// indicated store already contains a usable token. // indicated store already contains a usable token.
func NewInterceptor(lnd *lndclient.LndServices, store Store, func NewInterceptor(lnd *lndclient.LndServices, store Store,
rpcCallTimeout time.Duration) *Interceptor { rpcCallTimeout time.Duration, maxCost,
maxFee btcutil.Amount) *Interceptor {
return &Interceptor{ return &Interceptor{
lnd: lnd, lnd: lnd,
store: store, store: store,
callTimeout: rpcCallTimeout, 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) 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 // Create and store the pending token so we can resume the payment in
// case the payment is interrupted somehow. // case the payment is interrupted somehow.
token, err := tokenFromChallenge(macBytes, invoice.PaymentHash) 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) payCtx, cancel := context.WithTimeout(ctx, PaymentTimeout)
defer cancel() defer cancel()
respChan := i.lnd.Client.PayInvoice( respChan := i.lnd.Client.PayInvoice(
payCtx, invoiceStr, MaxRoutingFeeSats, nil, payCtx, invoiceStr, i.maxFee, nil,
) )
select { select {
case result := <-respChan: case result := <-respChan:

@ -50,6 +50,7 @@ func TestInterceptor(t *testing.T) {
testTimeout = 5 * time.Second testTimeout = 5 * time.Second
interceptor = NewInterceptor( interceptor = NewInterceptor(
&lnd.LndServices, store, testTimeout, &lnd.LndServices, store, testTimeout,
DefaultMaxCostSats, DefaultMaxRoutingFeeSats,
) )
testMac = makeMac(t) testMac = makeMac(t)
testMacBytes = serializeMac(t, testMac) testMacBytes = serializeMac(t, testMac)
@ -84,11 +85,13 @@ func TestInterceptor(t *testing.T) {
testCases := []struct { testCases := []struct {
name string name string
initialToken *Token initialToken *Token
interceptor *Interceptor
resetCb func() resetCb func()
expectLndCall bool expectLndCall bool
sendPaymentCb func(msg test.PaymentChannelMessage) sendPaymentCb func(msg test.PaymentChannelMessage)
trackPaymentCb func(msg test.TrackPaymentMessage) trackPaymentCb func(msg test.TrackPaymentMessage)
expectToken bool expectToken bool
expectInterceptErr string
expectBackendCalls int expectBackendCalls int
expectMacaroonCall1 bool expectMacaroonCall1 bool
expectMacaroonCall2 bool expectMacaroonCall2 bool
@ -96,6 +99,7 @@ func TestInterceptor(t *testing.T) {
{ {
name: "no auth required happy path", name: "no auth required happy path",
initialToken: nil, initialToken: nil,
interceptor: interceptor,
resetCb: func() { resetBackend(nil, "") }, resetCb: func() { resetBackend(nil, "") },
expectLndCall: false, expectLndCall: false,
expectToken: false, expectToken: false,
@ -106,6 +110,7 @@ func TestInterceptor(t *testing.T) {
{ {
name: "auth required, no token yet", name: "auth required, no token yet",
initialToken: nil, initialToken: nil,
interceptor: interceptor,
resetCb: func() { resetCb: func() {
resetBackend( resetBackend(
status.New( status.New(
@ -140,6 +145,7 @@ func TestInterceptor(t *testing.T) {
{ {
name: "auth required, has token", name: "auth required, has token",
initialToken: paidToken, initialToken: paidToken,
interceptor: interceptor,
resetCb: func() { resetBackend(nil, "") }, resetCb: func() { resetBackend(nil, "") },
expectLndCall: false, expectLndCall: false,
expectToken: true, expectToken: true,
@ -150,6 +156,7 @@ func TestInterceptor(t *testing.T) {
{ {
name: "auth required, has pending token", name: "auth required, has pending token",
initialToken: pendingToken, initialToken: pendingToken,
interceptor: interceptor,
resetCb: func() { resetCb: func() {
resetBackend( resetBackend(
status.New( status.New(
@ -177,6 +184,30 @@ func TestInterceptor(t *testing.T) {
expectMacaroonCall1: false, expectMacaroonCall1: false,
expectMacaroonCall2: true, 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 // The invoker is a simple function that simulates the actual call to
@ -219,11 +250,14 @@ func TestInterceptor(t *testing.T) {
backendWg.Add(1) backendWg.Add(1)
overallWg.Add(1) overallWg.Add(1)
go func() { go func() {
err := interceptor.UnaryInterceptor( err := tc.interceptor.UnaryInterceptor(
ctx, "", nil, nil, nil, invoker, nil, ctx, "", nil, nil, nil, invoker, nil,
) )
if err != nil { if err != nil && tc.expectInterceptErr != "" &&
panic(err) err.Error() != tc.expectInterceptErr {
panic(fmt.Errorf("unexpected error '%s', "+
"expected '%s'", err.Error(),
tc.expectInterceptErr))
} }
overallWg.Done() overallWg.Done()
}() }()
@ -318,12 +352,12 @@ func serializeMac(t *testing.T, mac *macaroon.Macaroon) []byte {
} }
func makeAuthHeader(macBytes []byte) string { func makeAuthHeader(macBytes []byte) string {
// Testnet invoice, copied from lnd/zpay32/invoice_test.go // Testnet invoice over 500 sats.
invoice := "lntb20m1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqc" + invoice := "lntb5u1p0pskpmpp5jzw9xvdast2g5lm5tswq6n64t2epe3f4xav43dyd" +
"yq5rqwzqfqypqhp58yjmdan79s6qqdhdzgynm4zwqd5d7xmw5fk98klysy04" + "239qr8h3yllqdqqcqzpgsp5m8sfjqgugthk66q3tr4gsqr5rh740jrq9x4l0" +
"3l2ahrqsfpp3x9et2e20v6pu37c5d9vax37wxq72un98k6vcx9fz94w0qf23" + "kvj5e77nmwqvpnq9qy9qsq72afzu7sfuppzqg3q2pn49hlh66rv7w60h2rua" +
"7cm2rqv9pmn5lnexfvf5579slr4zq3u8kmczecytdx0xg9rwzngp7e6guwqp" + "hx857g94s066yzxcjn4yccqc79779sd232v9ewluvu0tmusvht6r99rld8xs" +
"qlhssu04sucpnz4axcv2dstmknqq6jsk2l" "k287cpyac79r"
return fmt.Sprintf("LSAT macaroon=\"%s\", invoice=\"%s\"", return fmt.Sprintf("LSAT macaroon=\"%s\", invoice=\"%s\"",
base64.StdEncoding.EncodeToString(macBytes), invoice) base64.StdEncoding.EncodeToString(macBytes), invoice)
} }

@ -52,13 +52,13 @@ type grpcSwapServerClient struct {
var _ swapServerClient = (*grpcSwapServerClient)(nil) var _ swapServerClient = (*grpcSwapServerClient)(nil)
func newSwapServerClient(address string, insecure bool, tlsPath string, func newSwapServerClient(address string, insecure bool, tlsPath string,
lsatStore lsat.Store, lnd *lndclient.LndServices) ( lsatStore lsat.Store, lnd *lndclient.LndServices,
*grpcSwapServerClient, error) { maxLSATCost, maxLSATFee btcutil.Amount) (*grpcSwapServerClient, error) {
// Create the server connection with the interceptor that will handle // Create the server connection with the interceptor that will handle
// the LSAT protocol for us. // the LSAT protocol for us.
clientInterceptor := lsat.NewInterceptor( clientInterceptor := lsat.NewInterceptor(
lnd, lsatStore, serverRPCTimeout, lnd, lsatStore, serverRPCTimeout, maxLSATCost, maxLSATFee,
) )
serverConn, err := getSwapServerConn( serverConn, err := getSwapServerConn(
address, insecure, tlsPath, clientInterceptor, address, insecure, tlsPath, clientInterceptor,

Loading…
Cancel
Save