From 27fd6b3304bc780d6ec20e36e5b04587b6bf8229 Mon Sep 17 00:00:00 2001 From: Oliver Gugger Date: Wed, 28 Apr 2021 09:56:15 +0200 Subject: [PATCH 1/2] multi: use aperture instead of local lsat library Since the lsat code was copied over to aperture a while ago for other projects to use as well, we should migrate over to that shared code also. This will give us the recently added "remove token if payment failed and try again" feature for free along with some other small updates to the lsat package. --- client.go | 2 +- cmd/loop/lsat.go | 2 +- config.go | 2 +- go.mod | 5 +++-- go.sum | 28 ++++++++++++++++++++++++++++ loopd/config.go | 2 +- loopd/log.go | 2 +- release_notes.md | 1 + swap_server_client.go | 7 ++++--- 9 files changed, 41 insertions(+), 10 deletions(-) diff --git a/client.go b/client.go index 7f7db0c..afdc3f3 100644 --- a/client.go +++ b/client.go @@ -10,9 +10,9 @@ import ( "time" "github.com/btcsuite/btcutil" + "github.com/lightninglabs/aperture/lsat" "github.com/lightninglabs/lndclient" "github.com/lightninglabs/loop/loopdb" - "github.com/lightninglabs/loop/lsat" "github.com/lightninglabs/loop/swap" "github.com/lightninglabs/loop/sweep" ) diff --git a/cmd/loop/lsat.go b/cmd/loop/lsat.go index 88868c9..bd2dc45 100644 --- a/cmd/loop/lsat.go +++ b/cmd/loop/lsat.go @@ -7,8 +7,8 @@ import ( "fmt" "time" + "github.com/lightninglabs/aperture/lsat" "github.com/lightninglabs/loop/looprpc" - "github.com/lightninglabs/loop/lsat" "github.com/urfave/cli" "gopkg.in/macaroon.v2" ) diff --git a/config.go b/config.go index 198b591..7edba8b 100644 --- a/config.go +++ b/config.go @@ -3,9 +3,9 @@ package loop import ( "time" + "github.com/lightninglabs/aperture/lsat" "github.com/lightninglabs/lndclient" "github.com/lightninglabs/loop/loopdb" - "github.com/lightninglabs/loop/lsat" ) // clientConfig contains config items for the swap client. diff --git a/go.mod b/go.mod index ed166a8..3f8587f 100644 --- a/go.mod +++ b/go.mod @@ -10,6 +10,7 @@ require ( github.com/golang/protobuf v1.3.2 github.com/grpc-ecosystem/grpc-gateway v1.14.3 github.com/jessevdk/go-flags v1.4.0 + github.com/lightninglabs/aperture v0.1.6-beta github.com/lightninglabs/lndclient v0.11.0-5 github.com/lightninglabs/protobuf-hex-display v1.3.3-0.20191212020323-b444784ce75d github.com/lightningnetwork/lnd v0.12.0-beta.rc3 @@ -19,9 +20,9 @@ require ( github.com/lightningnetwork/lnd/ticker v1.0.0 github.com/stretchr/testify v1.5.1 github.com/urfave/cli v1.20.0 - golang.org/x/net v0.0.0-20191002035440-2ec189313ef0 + golang.org/x/net v0.0.0-20191112182307-2180aed22343 google.golang.org/genproto v0.0.0-20190927181202-20e1ac93f88c - google.golang.org/grpc v1.24.0 + google.golang.org/grpc v1.25.1 gopkg.in/macaroon-bakery.v2 v2.0.1 gopkg.in/macaroon.v2 v2.1.0 ) diff --git a/go.sum b/go.sum index b17980c..c398218 100644 --- a/go.sum +++ b/go.sum @@ -38,6 +38,7 @@ github.com/btcsuite/btcutil/psbt v1.0.2/go.mod h1:LVveMu4VaNSkIRTZu2+ut0HDBRuYjq github.com/btcsuite/btcutil/psbt v1.0.3-0.20200826194809-5f93e33af2b0 h1:3Zumkyl6PWyHuVJ04me0xeD9CnPOhNgeGpapFbzy7O4= github.com/btcsuite/btcutil/psbt v1.0.3-0.20200826194809-5f93e33af2b0/go.mod h1:LVveMu4VaNSkIRTZu2+ut0HDBRuYjqGocxDMNS1KuGQ= github.com/btcsuite/btcwallet v0.11.1-0.20200814001439-1d31f4ea6fc5/go.mod h1:YkEbJaCyN6yncq5gEp2xG0OKDwus2QxGCEXTNF27w5I= +github.com/btcsuite/btcwallet v0.11.1-0.20200904022754-2c5947a45222/go.mod h1:owv9oZqM0HnUW+ByF7VqOgfs2eb0ooiePW/+Tl/i/Nk= github.com/btcsuite/btcwallet v0.11.1-0.20201207233335-415f37ff11a1 h1:3gvLezYoUkr9MvxocB/vyPNzL+gSqsNT4Q6XTPK+R04= github.com/btcsuite/btcwallet v0.11.1-0.20201207233335-415f37ff11a1/go.mod h1:P1U4LKSB/bhFQdOM7ab1XqNoBGFyFAe7eKObEBD9mIo= github.com/btcsuite/btcwallet/wallet/txauthor v1.0.0 h1:KGHMW5sd7yDdDMkCZ/JpP0KltolFsQcB973brBnfj4c= @@ -68,6 +69,7 @@ github.com/btcsuite/snappy-go v1.0.0/go.mod h1:8woku9dyThutzjeg+3xrA5iCpBRH8XEEg github.com/btcsuite/websocket v0.0.0-20150119174127-31079b680792 h1:R8vQdOQdZ9Y3SkEwmHoWBmX1DNXhXZqlTpq6s4tyJGc= github.com/btcsuite/websocket v0.0.0-20150119174127-31079b680792/go.mod h1:ghJtEyQwv5/p4Mg4C0fgbePVuGr935/5ddU9Z3TmDRY= github.com/btcsuite/winsvc v1.0.0/go.mod h1:jsenWakMcC0zFBFurPLEAyrnc/teJEM1O46fmI40EZs= +github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/coreos/bbolt v1.3.3 h1:n6AiVyVRKQFNb6mJlwESEvvLoDyiTzXX7ORAUlkeBdY= @@ -91,6 +93,8 @@ github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZm github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no= github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo= github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= +github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= github.com/fortytw2/leaktest v1.3.0 h1:u8491cBMTQ8ft8aeV+adlcytMZylmA5nnwwkRZjI8vw= github.com/fortytw2/leaktest v1.3.0/go.mod h1:jDsjWgpAGjm2CA7WthBh/CdZYEPF31XHquHwclZch5g= github.com/frankban/quicktest v1.0.0/go.mod h1:R98jIehRai+d1/3Hv2//jOVCTJhW1VBavT6B6CuGq2k= @@ -149,10 +153,14 @@ github.com/jessevdk/go-flags v1.4.0 h1:4IU2WS7AumrZ/40jfhf4QVDMsQwqA7VEHozFRrGAR github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= github.com/jonboulle/clockwork v0.1.0 h1:VKV+ZcuP6l3yW9doeqz6ziZGgcynBVQO+obU0+0hcPo= github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= +github.com/jonboulle/clockwork v0.2.0 h1:J2SLSdy7HgElq8ekSl2Mxh6vrRNFxqbXGenYH2I02Vs= +github.com/jonboulle/clockwork v0.2.0/go.mod h1:Pkfl5aHPm1nk2H9h0bjmnJD/BcgbGXUBGnn1kMkgxc8= github.com/jrick/logrotate v1.0.0 h1:lQ1bL/n9mBNeIXoTUoYRlK4dHuNJVofX9oWqBtPnSzI= github.com/jrick/logrotate v1.0.0/go.mod h1:LNinyqDIJnpAur+b8yyulnQw/wDuN1+BYKlTRt3OuAQ= github.com/json-iterator/go v1.1.9 h1:9yzud/Ht36ygwatGx56VwCZtlI/2AD15T1X2sjSuGns= github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/json-iterator/go v1.1.10 h1:Kz6Cvnvv2wGdaG/V8yMvfkmNiXq9Ya2KUv4rouJJr68= +github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/juju/clock v0.0.0-20190205081909-9c5c9712527c h1:3UvYABOQRhJAApj9MdCN+Ydv841ETSoy6xLzdmmr/9A= github.com/juju/clock v0.0.0-20190205081909-9c5c9712527c/go.mod h1:nD0vlnrUjcjJhqN5WuCWZyzfd5AHZAC9/ajvbSx69xA= github.com/juju/errors v0.0.0-20190806202954-0232dcc7464d h1:hJXjZMxj0SWlMoQkzeZDLi2cmeiWKa7y1B8Rg+qaoEc= @@ -180,8 +188,11 @@ github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORN github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/lightninglabs/aperture v0.1.6-beta h1:bhpK4O9xa0YUBFQfkfg/h/3sMAY+AOMxi9YUjg6/l/E= +github.com/lightninglabs/aperture v0.1.6-beta/go.mod h1:9xl4mx778ZAzrB87nLHMqk+XQcSz8Dx/DypjWzGN1xo= github.com/lightninglabs/gozmq v0.0.0-20191113021534-d20a764486bf h1:HZKvJUHlcXI/f/O0Avg7t8sqkPo78HFzjmeYFl6DPnc= github.com/lightninglabs/gozmq v0.0.0-20191113021534-d20a764486bf/go.mod h1:vxmQPeIQxPf6Jf9rM8R+B4rKBqLA2AjttNxkFBL2Plk= +github.com/lightninglabs/lndclient v0.11.0-4/go.mod h1:8/cTKNwgL87NX123gmlv3Xh6p1a7pvzu+40Un3PhHiI= github.com/lightninglabs/lndclient v0.11.0-5 h1:nHDit/3siG8wvzbQPiyGVpu9v5llqThJK1KmG/IZTIw= github.com/lightninglabs/lndclient v0.11.0-5/go.mod h1:nIQ+lDm7JGRmP7OSeVfhOAyoqD4GA4NYU8GTsTXITbE= github.com/lightninglabs/neutrino v0.11.0/go.mod h1:CuhF0iuzg9Sp2HO6ZgXgayviFTn1QHdSTJlMncK80wg= @@ -194,6 +205,7 @@ github.com/lightninglabs/protobuf-hex-display v1.3.3-0.20191212020323-b444784ce7 github.com/lightningnetwork/lightning-onion v1.0.2-0.20200501022730-3c8c8d0b89ea h1:oCj48NQ8u7Vz+MmzHqt0db6mxcFZo3Ho7M5gCJauY/k= github.com/lightningnetwork/lightning-onion v1.0.2-0.20200501022730-3c8c8d0b89ea/go.mod h1:rigfi6Af/KqsF7Za0hOgcyq2PNH4AN70AaMRxcJkff4= github.com/lightningnetwork/lnd v0.11.0-beta/go.mod h1:CzArvT7NFDLhVyW06+NJWSuWFmE6Ea+AjjA3txUBqTM= +github.com/lightningnetwork/lnd v0.11.1-beta/go.mod h1:PGIgxy8aH70Li33YVYkHSaCM8m8LjEevk5h1Dpldrr4= github.com/lightningnetwork/lnd v0.12.0-beta.rc3 h1:kii3l6UIuzRYPAcEheFcRIDFEDLni0K8fbvMRc1qMcY= github.com/lightningnetwork/lnd v0.12.0-beta.rc3/go.mod h1:2GyP1IG1kXV5Af/LOCxnXfux1OP3fAGr8zptS5PB2YI= github.com/lightningnetwork/lnd/cert v1.0.2/go.mod h1:fmtemlSMf5t4hsQmcprSoOykypAPp+9c+0d0iqTScMo= @@ -239,6 +251,8 @@ github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDf github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90 h1:S/YWwWx/RA8rT8tKFRuGUZhuA90OyIBpPCXkcbwU8DE= github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4 h1:gQz4mCbXsO+nc9n1hCxHcGA3Zx3Eo+UHZoInFGUIXNM= +github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= github.com/prometheus/common v0.4.0 h1:7etb9YClo3a6HjLzfl6rIQaU+FDfi0VSX39io3aQ+DM= github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= @@ -265,6 +279,8 @@ github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5 github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk= github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5 h1:LnC5Kc/wtumK+WB441p7ynQJzVuNRJiqddSIE3IlSEQ= github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= +github.com/tmc/grpc-websocket-proxy v0.0.0-20200122045848-3419fae592fc h1:yUaosFVTJwnltaHbSNC3i82I92quFs+OFPRl8kNMVwo= +github.com/tmc/grpc-websocket-proxy v0.0.0-20200122045848-3419fae592fc/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= github.com/tv42/zbase32 v0.0.0-20160707012821-501572607d02 h1:tcJ6OjwOMvExLlzrAVZute09ocAGa7KqOON60++Gz4E= github.com/tv42/zbase32 v0.0.0-20160707012821-501572607d02/go.mod h1:tHlrkM198S068ZqfrO6S8HsoJq2bF3ETfTL+kt4tInY= github.com/urfave/cli v1.18.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA= @@ -284,6 +300,8 @@ go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee h1:0mgffUl7nfd+FpvXMVz4IDEa go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA= go.uber.org/zap v1.14.1 h1:nYDKopTbvAPq/NrUVZwT15y2lpROBiLLyoRTbXOYWOo= go.uber.org/zap v1.14.1/go.mod h1:Mb2vm2krFEG5DV0W9qcHBYFtp/Wku1cvYaqPsS/WYfc= +go.uber.org/zap v1.15.0 h1:ZZCA22JRF2gQE5FoNmhmrf7jeJJ2uhqDUNRYKm8dvmM= +go.uber.org/zap v1.15.0/go.mod h1:Mb2vm2krFEG5DV0W9qcHBYFtp/Wku1cvYaqPsS/WYfc= golang.org/x/crypto v0.0.0-20170930174604-9419663f5a44/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20180723164146-c126467f60eb/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= @@ -315,6 +333,8 @@ golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20191002035440-2ec189313ef0 h1:2mqDk8w/o6UmeUCu5Qiq2y7iMf6anbx+YA8d1JFoFrs= golang.org/x/net v0.0.0-20191002035440-2ec189313ef0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20191112182307-2180aed22343 h1:00ohfJ4K98s3m6BGUoBd8nyfp4Yl0GoIKvw5abItTjI= +golang.org/x/net v0.0.0-20191112182307-2180aed22343/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -351,13 +371,17 @@ google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9Ywl google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/genproto v0.0.0-20190201180003-4b09977fb922/go.mod h1:L3J43x8/uS+qIUoksaLKe6OS3nUKxOKuIFz1sl2/jx4= +google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= google.golang.org/genproto v0.0.0-20190927181202-20e1ac93f88c h1:hrpEMCZ2O7DR5gC1n2AJGVhrwiEjOi35+jxtIuZpTMo= google.golang.org/genproto v0.0.0-20190927181202-20e1ac93f88c/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= google.golang.org/grpc v1.16.0/go.mod h1:0JHn/cJsOMiMfNA9+DeHDlAU7KAAB5GDlYFpa9MZMio= google.golang.org/grpc v1.18.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= google.golang.org/grpc v1.24.0 h1:vb/1TCsVn3DcJlQ0Gs1yB1pKI6Do2/QNwxdKqmc/b0s= google.golang.org/grpc v1.24.0/go.mod h1:XDChyiUovWa60DnaeDeZmSW86xtLtjtZbwvSiRnRtcA= +google.golang.org/grpc v1.25.1 h1:wdKvqQk7IttEw92GoRyKG2IDrUIpgpj6H6m81yfeMW0= +google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= @@ -380,6 +404,8 @@ gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.3 h1:fvjTMHxHEw/mxHbtzPi3JCcKXQRAnQTBRo6YCJSVHKI= gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= @@ -387,3 +413,5 @@ honnef.co/go/tools v0.0.1-2019.2.3 h1:3JgtbtFHMiCmsznwGVTUWbgGov+pVqnlf1dEJTNAXe honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= sigs.k8s.io/yaml v1.1.0 h1:4A07+ZFc2wgJwo8YNlQpr1rVlgUDlxXHhPJciaPY5gs= sigs.k8s.io/yaml v1.1.0/go.mod h1:UJmg0vDUVViEyp3mgSv9WPwZCDxu4rQW1olrI1uml+o= +sigs.k8s.io/yaml v1.2.0 h1:kr/MCeFWJWTwyaHoR9c8EjH9OumOmoF9YGiZd7lFm/Q= +sigs.k8s.io/yaml v1.2.0/go.mod h1:yfXDCHCao9+ENCvLSE62v9VSji2MKu5jeNfTrofGhJc= diff --git a/loopd/config.go b/loopd/config.go index dcfbdab..e6b0572 100644 --- a/loopd/config.go +++ b/loopd/config.go @@ -10,7 +10,7 @@ import ( "time" "github.com/btcsuite/btcutil" - "github.com/lightninglabs/loop/lsat" + "github.com/lightninglabs/aperture/lsat" "github.com/lightningnetwork/lnd/cert" "github.com/lightningnetwork/lnd/lncfg" "github.com/lightningnetwork/lnd/lnrpc" diff --git a/loopd/log.go b/loopd/log.go index 5dcf70e..ab2ea80 100644 --- a/loopd/log.go +++ b/loopd/log.go @@ -2,11 +2,11 @@ package loopd import ( "github.com/btcsuite/btclog" + "github.com/lightninglabs/aperture/lsat" "github.com/lightninglabs/lndclient" "github.com/lightninglabs/loop" "github.com/lightninglabs/loop/liquidity" "github.com/lightninglabs/loop/loopdb" - "github.com/lightninglabs/loop/lsat" "github.com/lightningnetwork/lnd/build" ) diff --git a/release_notes.md b/release_notes.md index 141c56f..87ff6e6 100644 --- a/release_notes.md +++ b/release_notes.md @@ -15,6 +15,7 @@ This file tracks release notes for the loop client. ## Next release #### New Features +- If the payment for an LSAT fails, it is now automatically re-tried. #### Breaking Changes diff --git a/swap_server_client.go b/swap_server_client.go index 1f62c05..c79f795 100644 --- a/swap_server_client.go +++ b/swap_server_client.go @@ -14,9 +14,9 @@ import ( "github.com/btcsuite/btcd/btcec" "github.com/btcsuite/btcutil" + "github.com/lightninglabs/aperture/lsat" "github.com/lightninglabs/loop/loopdb" "github.com/lightninglabs/loop/looprpc" - "github.com/lightninglabs/loop/lsat" "github.com/lightningnetwork/lnd/lntypes" "github.com/lightningnetwork/lnd/routing/route" "github.com/lightningnetwork/lnd/tor" @@ -102,7 +102,7 @@ func newSwapServerClient(cfg *ClientConfig, lsatStore lsat.Store) ( // the LSAT protocol for us. clientInterceptor := lsat.NewInterceptor( cfg.Lnd, lsatStore, serverRPCTimeout, cfg.MaxLsatCost, - cfg.MaxLsatFee, + cfg.MaxLsatFee, false, ) serverConn, err := getSwapServerConn( cfg.ServerAddress, cfg.ProxyAddress, cfg.SwapServerNoTLS, @@ -460,7 +460,8 @@ func (s *grpcSwapServerClient) makeServerUpdate(ctx context.Context, // proxyAddr indicates that a SOCKS proxy found at the address should be used to // establish the connection. func getSwapServerConn(address, proxyAddress string, insecure bool, - tlsPath string, interceptor *lsat.Interceptor) (*grpc.ClientConn, error) { + tlsPath string, interceptor *lsat.ClientInterceptor) (*grpc.ClientConn, + error) { // Create a dial options array. opts := []grpc.DialOption{ From 68c22d7192af5c761ebedbad470c22305e33838d Mon Sep 17 00:00:00 2001 From: Oliver Gugger Date: Wed, 28 Apr 2021 09:58:22 +0200 Subject: [PATCH 2/2] lsat: remove local lsat package We now only use the lsat package imported from aperture and can therefore delete a whole bunch of duplicated code. --- lsat/caveat.go | 142 ------------- lsat/caveat_test.go | 202 ------------------ lsat/identifier.go | 128 ----------- lsat/identifier_test.go | 70 ------ lsat/interceptor.go | 445 --------------------------------------- lsat/interceptor_test.go | 414 ------------------------------------ lsat/log.go | 26 --- lsat/satisfier.go | 117 ---------- lsat/service.go | 128 ----------- lsat/service_test.go | 83 -------- lsat/store.go | 211 ------------------- lsat/store_test.go | 131 ------------ lsat/token.go | 190 ----------------- 13 files changed, 2287 deletions(-) delete mode 100644 lsat/caveat.go delete mode 100644 lsat/caveat_test.go delete mode 100644 lsat/identifier.go delete mode 100644 lsat/identifier_test.go delete mode 100644 lsat/interceptor.go delete mode 100644 lsat/interceptor_test.go delete mode 100644 lsat/log.go delete mode 100644 lsat/satisfier.go delete mode 100644 lsat/service.go delete mode 100644 lsat/service_test.go delete mode 100644 lsat/store.go delete mode 100644 lsat/store_test.go delete mode 100644 lsat/token.go diff --git a/lsat/caveat.go b/lsat/caveat.go deleted file mode 100644 index 80aa6e0..0000000 --- a/lsat/caveat.go +++ /dev/null @@ -1,142 +0,0 @@ -package lsat - -import ( - "errors" - "fmt" - "strings" - - "gopkg.in/macaroon.v2" -) - -const ( - // PreimageKey is the key used for a payment preimage caveat. - PreimageKey = "preimage" -) - -var ( - // ErrInvalidCaveat is an error returned when we attempt to decode a - // caveat with an invalid format. - ErrInvalidCaveat = errors.New("caveat must be of the form " + - "\"condition=value\"") -) - -// Caveat is a predicate that can be applied to an LSAT in order to restrict its -// use in some form. Caveats are evaluated during LSAT verification after the -// LSAT's signature is verified. The predicate of each caveat must hold true in -// order to successfully validate an LSAT. -type Caveat struct { - // Condition serves as a way to identify a caveat and how to satisfy it. - Condition string - - // Value is what will be used to satisfy a caveat. This can be as - // flexible as needed, as long as it can be encoded into a string. - Value string -} - -// NewCaveat construct a new caveat with the given condition and value. -func NewCaveat(condition string, value string) Caveat { - return Caveat{Condition: condition, Value: value} -} - -// String returns a user-friendly view of a caveat. -func (c Caveat) String() string { - return EncodeCaveat(c) -} - -// EncodeCaveat encodes a caveat into its string representation. -func EncodeCaveat(c Caveat) string { - return fmt.Sprintf("%v=%v", c.Condition, c.Value) -} - -// DecodeCaveat decodes a caveat from its string representation. -func DecodeCaveat(s string) (Caveat, error) { - parts := strings.SplitN(s, "=", 2) - if len(parts) != 2 { - return Caveat{}, ErrInvalidCaveat - } - return Caveat{Condition: parts[0], Value: parts[1]}, nil -} - -// AddFirstPartyCaveats adds a set of caveats as first-party caveats to a -// macaroon. -func AddFirstPartyCaveats(m *macaroon.Macaroon, caveats ...Caveat) error { - for _, c := range caveats { - rawCaveat := []byte(EncodeCaveat(c)) - if err := m.AddFirstPartyCaveat(rawCaveat); err != nil { - return err - } - } - - return nil -} - -// HasCaveat checks whether the given macaroon has a caveat with the given -// condition, and if so, returns its value. If multiple caveats with the same -// condition exist, then the value of the last one is returned. -func HasCaveat(m *macaroon.Macaroon, cond string) (string, bool) { - var value *string - for _, rawCaveat := range m.Caveats() { - caveat, err := DecodeCaveat(string(rawCaveat.Id)) - if err != nil { - // Ignore any unknown caveats as we can't decode them. - continue - } - if caveat.Condition == cond { - value = &caveat.Value - } - } - - if value == nil { - return "", false - } - return *value, true -} - -// VerifyCaveats determines whether every relevant caveat of an LSAT holds true. -// A caveat is considered relevant if a satisfier is provided for it, which is -// what we'll use as their evaluation. -// -// NOTE: The caveats provided should be in the same order as in the LSAT to -// ensure the correctness of each satisfier's SatisfyPrevious. -func VerifyCaveats(caveats []Caveat, satisfiers ...Satisfier) error { - // Construct a set of our satisfiers to determine which caveats we know - // how to satisfy. - caveatSatisfiers := make(map[string]Satisfier, len(satisfiers)) - for _, satisfier := range satisfiers { - caveatSatisfiers[satisfier.Condition] = satisfier - } - relevantCaveats := make(map[string][]Caveat) - for _, caveat := range caveats { - if _, ok := caveatSatisfiers[caveat.Condition]; !ok { - continue - } - relevantCaveats[caveat.Condition] = append( - relevantCaveats[caveat.Condition], caveat, - ) - } - - for condition, caveats := range relevantCaveats { - satisfier := caveatSatisfiers[condition] - - // Since it's possible for a chain of caveat to exist for the - // same condition as a way to demote privileges, we'll ensure - // each one satisfies its previous. - for i, j := 0, 1; j < len(caveats); i, j = i+1, j+1 { - prevCaveat := caveats[i] - curCaveat := caveats[j] - err := satisfier.SatisfyPrevious(prevCaveat, curCaveat) - if err != nil { - return err - } - } - - // Once we verify the previous ones, if any, we can proceed to - // verify the final one, which is the decision maker. - err := satisfier.SatisfyFinal(caveats[len(caveats)-1]) - if err != nil { - return err - } - } - - return nil -} diff --git a/lsat/caveat_test.go b/lsat/caveat_test.go deleted file mode 100644 index 818a86c..0000000 --- a/lsat/caveat_test.go +++ /dev/null @@ -1,202 +0,0 @@ -package lsat - -import ( - "errors" - "testing" - - "gopkg.in/macaroon.v2" -) - -var ( - testMacaroon, _ = macaroon.New(nil, nil, "", macaroon.LatestVersion) -) - -// TestCaveatSerialization ensures that we can properly encode/decode valid -// caveats and cannot do so for invalid ones. -func TestCaveatSerialization(t *testing.T) { - t.Parallel() - - tests := []struct { - name string - caveatStr string - err error - }{ - { - name: "valid caveat", - caveatStr: "expiration=1337", - err: nil, - }, - { - name: "valid caveat with separator in value", - caveatStr: "expiration=1337=", - err: nil, - }, - { - name: "invalid caveat", - caveatStr: "expiration:1337", - err: ErrInvalidCaveat, - }, - } - - for _, test := range tests { - test := test - success := t.Run(test.name, func(t *testing.T) { - caveat, err := DecodeCaveat(test.caveatStr) - if !errors.Is(err, test.err) { - t.Fatalf("expected err \"%v\", got \"%v\"", - test.err, err) - } - - if test.err != nil { - return - } - - caveatStr := EncodeCaveat(caveat) - if caveatStr != test.caveatStr { - t.Fatalf("expected encoded caveat \"%v\", "+ - "got \"%v\"", test.caveatStr, caveatStr) - } - }) - if !success { - return - } - } -} - -// TestHasCaveat ensures we can determine whether a macaroon contains a caveat -// with a specific condition. -func TestHasCaveat(t *testing.T) { - t.Parallel() - - const ( - cond = "cond" - value = "value" - ) - m := testMacaroon.Clone() - - // The macaroon doesn't have any caveats, so we shouldn't find any. - if _, ok := HasCaveat(m, cond); ok { - t.Fatal("found unexpected caveat with unknown condition") - } - - // Add two caveats, one in a valid LSAT format and another invalid. - // We'll test that we're still able to determine the macaroon contains - // the valid caveat even though there is one that is invalid. - invalidCaveat := []byte("invalid") - if err := m.AddFirstPartyCaveat(invalidCaveat); err != nil { - t.Fatalf("unable to add macaroon caveat: %v", err) - } - validCaveat1 := Caveat{Condition: cond, Value: value} - if err := AddFirstPartyCaveats(m, validCaveat1); err != nil { - t.Fatalf("unable to add macaroon caveat: %v", err) - } - - caveatValue, ok := HasCaveat(m, cond) - if !ok { - t.Fatal("expected macaroon to contain caveat") - } - if caveatValue != validCaveat1.Value { - t.Fatalf("expected caveat value \"%v\", got \"%v\"", - validCaveat1.Value, caveatValue) - } - - // If we add another caveat with the same condition, the value of the - // most recently added caveat should be returned instead. - validCaveat2 := validCaveat1 - validCaveat2.Value += value - if err := AddFirstPartyCaveats(m, validCaveat2); err != nil { - t.Fatalf("unable to add macaroon caveat: %v", err) - } - - caveatValue, ok = HasCaveat(m, cond) - if !ok { - t.Fatal("expected macaroon to contain caveat") - } - if caveatValue != validCaveat2.Value { - t.Fatalf("expected caveat value \"%v\", got \"%v\"", - validCaveat2.Value, caveatValue) - } -} - -// TestVerifyCaveats ensures caveat verification only holds true for known -// caveats. -func TestVerifyCaveats(t *testing.T) { - t.Parallel() - - caveat1 := Caveat{Condition: "1", Value: "test"} - caveat2 := Caveat{Condition: "2", Value: "test"} - satisfier := Satisfier{ - Condition: caveat1.Condition, - SatisfyPrevious: func(c Caveat, prev Caveat) error { - return nil - }, - SatisfyFinal: func(c Caveat) error { - return nil - }, - } - invalidSatisfyPrevious := func(c Caveat, prev Caveat) error { - return errors.New("no") - } - invalidSatisfyFinal := func(c Caveat) error { - return errors.New("no") - } - - tests := []struct { - name string - caveats []Caveat - satisfiers []Satisfier - shouldFail bool - }{ - { - name: "simple verification", - caveats: []Caveat{caveat1}, - satisfiers: []Satisfier{satisfier}, - shouldFail: false, - }, - { - name: "unknown caveat", - caveats: []Caveat{caveat1, caveat2}, - satisfiers: []Satisfier{satisfier}, - shouldFail: false, - }, - { - name: "one invalid", - caveats: []Caveat{caveat1, caveat2}, - satisfiers: []Satisfier{ - satisfier, - { - Condition: caveat2.Condition, - SatisfyFinal: invalidSatisfyFinal, - }, - }, - shouldFail: true, - }, - { - name: "prev invalid", - caveats: []Caveat{caveat1, caveat1}, - satisfiers: []Satisfier{ - { - Condition: caveat1.Condition, - SatisfyPrevious: invalidSatisfyPrevious, - }, - }, - shouldFail: true, - }, - } - - for _, test := range tests { - test := test - success := t.Run(test.name, func(t *testing.T) { - err := VerifyCaveats(test.caveats, test.satisfiers...) - if test.shouldFail && err == nil { - t.Fatal("expected caveat verification to fail") - } - if !test.shouldFail && err != nil { - t.Fatal("unexpected caveat verification failure") - } - }) - if !success { - return - } - } -} diff --git a/lsat/identifier.go b/lsat/identifier.go deleted file mode 100644 index 252540e..0000000 --- a/lsat/identifier.go +++ /dev/null @@ -1,128 +0,0 @@ -package lsat - -import ( - "encoding/binary" - "encoding/hex" - "errors" - "fmt" - "io" - - "github.com/lightningnetwork/lnd/lntypes" -) - -const ( - // LatestVersion is the latest version used for minting new LSATs. - LatestVersion = 0 - - // SecretSize is the size in bytes of a LSAT's secret, also known as - // the root key of the macaroon. - SecretSize = 32 - - // TokenIDSize is the size in bytes of an LSAT's ID encoded in its - // macaroon identifier. - TokenIDSize = 32 -) - -var ( - // byteOrder is the byte order used to encode/decode a macaroon's raw - // identifier. - byteOrder = binary.BigEndian - - // ErrUnknownVersion is an error returned when attempting to decode an - // LSAT identifier with an unknown version. - ErrUnknownVersion = errors.New("unknown LSAT version") -) - -// TokenID is the type that stores the token identifier of an LSAT token. -type TokenID [TokenIDSize]byte - -// String returns the hex encoded representation of the token ID as a string. -func (t *TokenID) String() string { - return hex.EncodeToString(t[:]) -} - -// MakeIDFromString parses the hex encoded string and parses it into a token ID. -func MakeIDFromString(newID string) (TokenID, error) { - if len(newID) != hex.EncodedLen(TokenIDSize) { - return TokenID{}, fmt.Errorf("invalid id string length of %v, "+ - "want %v", len(newID), hex.EncodedLen(TokenIDSize)) - } - - idBytes, err := hex.DecodeString(newID) - if err != nil { - return TokenID{}, err - } - var id TokenID - copy(id[:], idBytes) - - return id, nil -} - -// Identifier contains the static identifying details of an LSAT. This is -// intended to be used as the identifier of the macaroon within an LSAT. -type Identifier struct { - // Version is the version of an LSAT. Having a version allows us to - // introduce new fields to the identifier in a backwards-compatible - // manner. - Version uint16 - - // PaymentHash is the payment hash linked to an LSAT. Verification of - // an LSAT depends on a valid payment, which is enforced by ensuring a - // preimage is provided that hashes to our payment hash. - PaymentHash lntypes.Hash - - // TokenID is the unique identifier of an LSAT. - TokenID TokenID -} - -// EncodeIdentifier encodes an LSAT's identifier according to its version. -func EncodeIdentifier(w io.Writer, id *Identifier) error { - if err := binary.Write(w, byteOrder, id.Version); err != nil { - return err - } - - switch id.Version { - // A version 0 identifier consists of its linked payment hash, followed - // by the token ID. - case 0: - if _, err := w.Write(id.PaymentHash[:]); err != nil { - return err - } - _, err := w.Write(id.TokenID[:]) - return err - - default: - return fmt.Errorf("%w: %v", ErrUnknownVersion, id.Version) - } -} - -// DecodeIdentifier decodes an LSAT's identifier according to its version. -func DecodeIdentifier(r io.Reader) (*Identifier, error) { - var version uint16 - if err := binary.Read(r, byteOrder, &version); err != nil { - return nil, err - } - - switch version { - // A version 0 identifier consists of its linked payment hash, followed - // by the token ID. - case 0: - var paymentHash lntypes.Hash - if _, err := r.Read(paymentHash[:]); err != nil { - return nil, err - } - var tokenID TokenID - if _, err := r.Read(tokenID[:]); err != nil { - return nil, err - } - - return &Identifier{ - Version: version, - PaymentHash: paymentHash, - TokenID: tokenID, - }, nil - - default: - return nil, fmt.Errorf("%w: %v", ErrUnknownVersion, version) - } -} diff --git a/lsat/identifier_test.go b/lsat/identifier_test.go deleted file mode 100644 index abda64c..0000000 --- a/lsat/identifier_test.go +++ /dev/null @@ -1,70 +0,0 @@ -package lsat - -import ( - "bytes" - "errors" - "testing" - - "github.com/lightningnetwork/lnd/lntypes" -) - -var ( - testPaymentHash lntypes.Hash - testTokenID [TokenIDSize]byte -) - -// TestIdentifierSerialization ensures proper serialization of known identifier -// versions and failures for unknown versions. -func TestIdentifierSerialization(t *testing.T) { - t.Parallel() - - tests := []struct { - name string - id Identifier - err error - }{ - { - name: "valid identifier", - id: Identifier{ - Version: LatestVersion, - PaymentHash: testPaymentHash, - TokenID: testTokenID, - }, - err: nil, - }, - { - name: "unknown version", - id: Identifier{ - Version: LatestVersion + 1, - PaymentHash: testPaymentHash, - TokenID: testTokenID, - }, - err: ErrUnknownVersion, - }, - } - - for _, test := range tests { - test := test - success := t.Run(test.name, func(t *testing.T) { - var buf bytes.Buffer - err := EncodeIdentifier(&buf, &test.id) - if !errors.Is(err, test.err) { - t.Fatalf("expected err \"%v\", got \"%v\"", - test.err, err) - } - if test.err != nil { - return - } - id, err := DecodeIdentifier(&buf) - if err != nil { - t.Fatalf("unable to decode identifier: %v", err) - } - if *id != test.id { - t.Fatalf("expected id %v, got %v", test.id, *id) - } - }) - if !success { - return - } - } -} diff --git a/lsat/interceptor.go b/lsat/interceptor.go deleted file mode 100644 index 7912738..0000000 --- a/lsat/interceptor.go +++ /dev/null @@ -1,445 +0,0 @@ -package lsat - -import ( - "context" - "encoding/base64" - "fmt" - "regexp" - "sync" - "time" - - "github.com/btcsuite/btcutil" - "github.com/lightninglabs/lndclient" - "github.com/lightningnetwork/lnd/lnrpc" - "github.com/lightningnetwork/lnd/lnwire" - "github.com/lightningnetwork/lnd/macaroons" - "github.com/lightningnetwork/lnd/zpay32" - "google.golang.org/grpc" - "google.golang.org/grpc/codes" - "google.golang.org/grpc/metadata" - "google.golang.org/grpc/status" -) - -const ( - // GRPCErrCode is the error code we receive from a gRPC call if the - // server expects a payment. - GRPCErrCode = codes.Internal - - // GRPCErrMessage is the error message we receive from a gRPC call in - // conjunction with the GRPCErrCode to signal the client that a payment - // is required to access the service. - GRPCErrMessage = "payment required" - - // AuthHeader is is the HTTP response header that contains the payment - // challenge. - AuthHeader = "WWW-Authenticate" - - // 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. - PaymentTimeout = 60 * time.Second - - // manualRetryHint is the error text we return to tell the user how a - // token payment can be retried if the payment fails. - manualRetryHint = "consider removing pending token file if error " + - "persists. use 'listauth' command to find out token file name" -) - -var ( - // authHeaderRegex is the regular expression the payment challenge must - // match for us to be able to parse the macaroon and invoice. - authHeaderRegex = regexp.MustCompile( - "LSAT macaroon=\"(.*?)\", invoice=\"(.*?)\"", - ) -) - -// Interceptor is a gRPC client interceptor that can handle LSAT authentication -// challenges with embedded payment requests. It uses a connection to lnd to -// automatically pay for an authentication token. -type Interceptor struct { - lnd *lndclient.LndServices - store Store - callTimeout time.Duration - maxCost btcutil.Amount - maxFee btcutil.Amount - lock sync.Mutex -} - -// NewInterceptor creates a new gRPC client interceptor that uses the provided -// 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, maxCost, - maxFee btcutil.Amount) *Interceptor { - - return &Interceptor{ - lnd: lnd, - store: store, - callTimeout: rpcCallTimeout, - maxCost: maxCost, - maxFee: maxFee, - } -} - -// interceptContext is a struct that contains all information about a call that -// is intercepted by the interceptor. -type interceptContext struct { - mainCtx context.Context - opts []grpc.CallOption - metadata *metadata.MD - token *Token -} - -// UnaryInterceptor is an interceptor method that can be used directly by gRPC -// for unary calls. If the store contains a token, it is attached as credentials -// to every call before patching it through. The response error is also -// intercepted for every call. If there is an error returned and it is -// indicating a payment challenge, a token is acquired and paid for -// automatically. The original request is then repeated back to the server, now -// with the new token attached. -func (i *Interceptor) UnaryInterceptor(ctx context.Context, method string, - req, reply interface{}, cc *grpc.ClientConn, invoker grpc.UnaryInvoker, - opts ...grpc.CallOption) error { - - // To avoid paying for a token twice if two parallel requests are - // happening, we require an exclusive lock here. - i.lock.Lock() - defer i.lock.Unlock() - - // Create the context that we'll use to initiate the real request. This - // contains the means to extract response headers and possibly also an - // auth token, if we already have paid for one. - iCtx, err := i.newInterceptContext(ctx, opts) - if err != nil { - return err - } - - // Try executing the call now. If anything goes wrong, we only handle - // the LSAT error message that comes in the form of a gRPC status error. - rpcCtx, cancel := context.WithTimeout(ctx, i.callTimeout) - defer cancel() - err = invoker(rpcCtx, method, req, reply, cc, iCtx.opts...) - if !isPaymentRequired(err) { - return err - } - - // Find out if we need to pay for a new token or perhaps resume - // a previously aborted payment. - err = i.handlePayment(iCtx) - if err != nil { - return err - } - - // Execute the same request again, now with the LSAT - // token added as an RPC credential. - rpcCtx2, cancel2 := context.WithTimeout(ctx, i.callTimeout) - defer cancel2() - return invoker(rpcCtx2, method, req, reply, cc, iCtx.opts...) -} - -// StreamInterceptor is an interceptor method that can be used directly by gRPC -// for streaming calls. If the store contains a token, it is attached as -// credentials to every stream establishment call before patching it through. -// The response error is also intercepted for every initial stream initiation. -// If there is an error returned and it is indicating a payment challenge, a -// token is acquired and paid for automatically. The original request is then -// repeated back to the server, now with the new token attached. -func (i *Interceptor) StreamInterceptor(ctx context.Context, - desc *grpc.StreamDesc, cc *grpc.ClientConn, method string, - streamer grpc.Streamer, opts ...grpc.CallOption) (grpc.ClientStream, - error) { - - // To avoid paying for a token twice if two parallel requests are - // happening, we require an exclusive lock here. - i.lock.Lock() - defer i.lock.Unlock() - - // Create the context that we'll use to initiate the real request. This - // contains the means to extract response headers and possibly also an - // auth token, if we already have paid for one. - iCtx, err := i.newInterceptContext(ctx, opts) - if err != nil { - return nil, err - } - - // Try establishing the stream now. If anything goes wrong, we only - // handle the LSAT error message that comes in the form of a gRPC status - // error. The context of a stream will be used for the whole lifetime of - // it, so we can't really clamp down on the initial call with a timeout. - stream, err := streamer(ctx, desc, cc, method, iCtx.opts...) - if !isPaymentRequired(err) { - return stream, err - } - - // Find out if we need to pay for a new token or perhaps resume - // a previously aborted payment. - err = i.handlePayment(iCtx) - if err != nil { - return nil, err - } - - // Execute the same request again, now with the LSAT token added - // as an RPC credential. - return streamer(ctx, desc, cc, method, iCtx.opts...) -} - -// newInterceptContext creates the initial intercept context that can capture -// metadata from the server and sends the local token to the server if one -// already exists. -func (i *Interceptor) newInterceptContext(ctx context.Context, - opts []grpc.CallOption) (*interceptContext, error) { - - iCtx := &interceptContext{ - mainCtx: ctx, - opts: opts, - metadata: &metadata.MD{}, - } - - // Let's see if the store already contains a token and what state it - // might be in. If a previous call was aborted, we might have a pending - // token that needs to be handled separately. - var err error - iCtx.token, err = i.store.CurrentToken() - switch { - // If there is no token yet, nothing to do at this point. - case err == ErrNoToken: - - // Some other error happened that we have to surface. - case err != nil: - log.Errorf("Failed to get token from store: %v", err) - return nil, fmt.Errorf("getting token from store failed: %v", - err) - - // Only if we have a paid token append it. We don't resume a pending - // payment just yet, since we don't even know if a token is required for - // this call. We also never send a pending payment to the server since - // we know it's not valid. - case !iCtx.token.isPending(): - if err = i.addLsatCredentials(iCtx); err != nil { - log.Errorf("Adding macaroon to request failed: %v", err) - return nil, fmt.Errorf("adding macaroon failed: %v", - err) - } - } - - // We need a way to extract the response headers sent by the server. - // This can only be done through the experimental grpc.Trailer call - // option. We execute the request and inspect the error. If it's the - // LSAT specific payment required error, we might execute the same - // method again later with the paid LSAT token. - iCtx.opts = append(iCtx.opts, grpc.Trailer(iCtx.metadata)) - return iCtx, nil -} - -// handlePayment tries to obtain a valid token by either tracking the payment -// status of a pending token or paying for a new one. -func (i *Interceptor) handlePayment(iCtx *interceptContext) error { - switch { - // Resume/track a pending payment if it was interrupted for some reason. - case iCtx.token != nil && iCtx.token.isPending(): - log.Infof("Payment of LSAT token is required, resuming/" + - "tracking previous payment from pending LSAT token") - err := i.trackPayment(iCtx.mainCtx, iCtx.token) - if err != nil { - return err - } - - // We don't have a token yet, try to get a new one. - case iCtx.token == nil: - // We don't have a token yet, get a new one. - log.Infof("Payment of LSAT token is required, paying invoice") - var err error - iCtx.token, err = i.payLsatToken(iCtx.mainCtx, iCtx.metadata) - if err != nil { - return err - } - - // We have a token and it's valid, nothing more to do here. - default: - log.Debugf("Found valid LSAT token to add to request") - } - - if err := i.addLsatCredentials(iCtx); err != nil { - log.Errorf("Adding macaroon to request failed: %v", err) - return fmt.Errorf("adding macaroon failed: %v", err) - } - return nil -} - -// addLsatCredentials adds an LSAT token to the given intercept context. -func (i *Interceptor) addLsatCredentials(iCtx *interceptContext) error { - if iCtx.token == nil { - return fmt.Errorf("cannot add nil token to context") - } - - macaroon, err := iCtx.token.PaidMacaroon() - if err != nil { - return err - } - iCtx.opts = append(iCtx.opts, grpc.PerRPCCredentials( - macaroons.NewMacaroonCredential(macaroon), - )) - return nil -} - -// payLsatToken reads the payment challenge from the response metadata and tries -// to pay the invoice encoded in them, returning a paid LSAT token if -// successful. -func (i *Interceptor) payLsatToken(ctx context.Context, md *metadata.MD) ( - *Token, error) { - - // First parse the authentication header that was stored in the - // metadata. - authHeader := md.Get(AuthHeader) - if len(authHeader) == 0 { - return nil, fmt.Errorf("auth header not found in response") - } - matches := authHeaderRegex.FindStringSubmatch(authHeader[0]) - if len(matches) != 3 { - return nil, fmt.Errorf("invalid auth header "+ - "format: %s", authHeader[0]) - } - - // Decode the base64 macaroon and the invoice so we can store the - // information in our store later. - macBase64, invoiceStr := matches[1], matches[2] - macBytes, err := base64.StdEncoding.DecodeString(macBase64) - if err != nil { - return nil, fmt.Errorf("base64 decode of macaroon failed: "+ - "%v", err) - } - invoice, err := zpay32.Decode(invoiceStr, i.lnd.ChainParams) - if err != nil { - 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) - if err != nil { - return nil, fmt.Errorf("unable to create token: %v", err) - } - err = i.store.StoreToken(token) - if err != nil { - return nil, fmt.Errorf("unable to store pending token: %v", err) - } - - // Pay invoice now and wait for the result to arrive or the main context - // being canceled. - payCtx, cancel := context.WithTimeout(ctx, PaymentTimeout) - defer cancel() - respChan := i.lnd.Client.PayInvoice( - payCtx, invoiceStr, i.maxFee, nil, - ) - select { - case result := <-respChan: - if result.Err != nil { - return nil, result.Err - } - token.Preimage = result.Preimage - token.AmountPaid = lnwire.NewMSatFromSatoshis(result.PaidAmt) - token.RoutingFeePaid = lnwire.NewMSatFromSatoshis( - result.PaidFee, - ) - return token, i.store.StoreToken(token) - - case <-payCtx.Done(): - return nil, fmt.Errorf("payment timed out. try again to track "+ - "payment. %s", manualRetryHint) - - case <-ctx.Done(): - return nil, fmt.Errorf("parent context canceled. try again to"+ - "track payment. %s", manualRetryHint) - } -} - -// trackPayment tries to resume a pending payment by tracking its state and -// waiting for a conclusive result. -func (i *Interceptor) trackPayment(ctx context.Context, token *Token) error { - // Lookup state of the payment. - paymentStateCtx, cancel := context.WithCancel(ctx) - defer cancel() - payStatusChan, payErrChan, err := i.lnd.Router.TrackPayment( - paymentStateCtx, token.PaymentHash, - ) - if err != nil { - log.Errorf("Could not call TrackPayment on lnd: %v", err) - return fmt.Errorf("track payment call to lnd failed: %v", err) - } - - // We can't wait forever, so we give the payment tracking the same - // timeout as the original payment. - payCtx, cancel := context.WithTimeout(ctx, PaymentTimeout) - defer cancel() - - // We'll consume status updates until we reach a conclusive state or - // reach the timeout. - for { - select { - // If we receive a state without an error, the payment has been - // initiated. Loop until the payment - case result := <-payStatusChan: - switch result.State { - // If the payment was successful, we have all the - // information we need and we can return the fully paid - // token. - case lnrpc.Payment_SUCCEEDED: - extractPaymentDetails(token, result) - return i.store.StoreToken(token) - - // The payment is still in transit, we'll give it more - // time to complete. - case lnrpc.Payment_IN_FLIGHT: - - // Any other state means either error or timeout. - default: - return fmt.Errorf("payment tracking failed "+ - "with state %s. %s", - result.State.String(), manualRetryHint) - } - - // Abort the payment execution for any error. - case err := <-payErrChan: - return fmt.Errorf("payment tracking failed: %v. %s", - err, manualRetryHint) - - case <-payCtx.Done(): - return fmt.Errorf("payment tracking timed out. %s", - manualRetryHint) - } - } -} - -// isPaymentRequired inspects an error to find out if it's the specific gRPC -// error returned by the server to indicate a payment is required to access the -// service. -func isPaymentRequired(err error) bool { - statusErr, ok := status.FromError(err) - return ok && - statusErr.Message() == GRPCErrMessage && - statusErr.Code() == GRPCErrCode -} - -// extractPaymentDetails extracts the preimage and amounts paid for a payment -// from the payment status and stores them in the token. -func extractPaymentDetails(token *Token, status lndclient.PaymentStatus) { - token.Preimage = status.Preimage - token.AmountPaid = status.Value - token.RoutingFeePaid = status.Fee -} diff --git a/lsat/interceptor_test.go b/lsat/interceptor_test.go deleted file mode 100644 index 0e28f41..0000000 --- a/lsat/interceptor_test.go +++ /dev/null @@ -1,414 +0,0 @@ -package lsat - -import ( - "context" - "encoding/base64" - "encoding/hex" - "fmt" - "sync" - "testing" - "time" - - "github.com/lightninglabs/lndclient" - "github.com/lightninglabs/loop/test" - "github.com/lightningnetwork/lnd/lnrpc" - "github.com/lightningnetwork/lnd/lntypes" - "google.golang.org/grpc" - "google.golang.org/grpc/status" - "gopkg.in/macaroon.v2" -) - -type interceptTestCase struct { - name string - initialPreimage *lntypes.Preimage - interceptor *Interceptor - resetCb func() - expectLndCall bool - sendPaymentCb func(*testing.T, test.PaymentChannelMessage) - trackPaymentCb func(*testing.T, test.TrackPaymentMessage) - expectToken bool - expectInterceptErr string - expectBackendCalls int - expectMacaroonCall1 bool - expectMacaroonCall2 bool -} - -type mockStore struct { - token *Token -} - -func (s *mockStore) CurrentToken() (*Token, error) { - if s.token == nil { - return nil, ErrNoToken - } - return s.token, nil -} - -func (s *mockStore) AllTokens() (map[string]*Token, error) { - return map[string]*Token{"foo": s.token}, nil -} - -func (s *mockStore) StoreToken(token *Token) error { - s.token = token - return nil -} - -var ( - lnd = test.NewMockLnd() - store = &mockStore{} - testTimeout = 5 * time.Second - interceptor = NewInterceptor( - &lnd.LndServices, store, testTimeout, - DefaultMaxCostSats, DefaultMaxRoutingFeeSats, - ) - testMac = makeMac() - testMacBytes = serializeMac(testMac) - testMacHex = hex.EncodeToString(testMacBytes) - paidPreimage = lntypes.Preimage{1, 2, 3, 4, 5} - backendErr error - backendAuth = "" - callMD map[string]string - numBackendCalls = 0 - overallWg sync.WaitGroup - backendWg sync.WaitGroup - - testCases = []interceptTestCase{ - { - name: "no auth required happy path", - initialPreimage: nil, - interceptor: interceptor, - resetCb: func() { resetBackend(nil, "") }, - expectLndCall: false, - expectToken: false, - expectBackendCalls: 1, - expectMacaroonCall1: false, - expectMacaroonCall2: false, - }, - { - name: "auth required, no token yet", - initialPreimage: nil, - interceptor: interceptor, - resetCb: func() { - resetBackend( - status.New( - GRPCErrCode, GRPCErrMessage, - ).Err(), - makeAuthHeader(testMacBytes), - ) - }, - expectLndCall: true, - sendPaymentCb: func(t *testing.T, - msg test.PaymentChannelMessage) { - - if len(callMD) != 0 { - t.Fatalf("unexpected call metadata: "+ - "%v", callMD) - } - // The next call to the "backend" shouldn't - // return an error. - resetBackend(nil, "") - msg.Done <- lndclient.PaymentResult{ - Preimage: paidPreimage, - PaidAmt: 123, - PaidFee: 345, - } - }, - trackPaymentCb: func(t *testing.T, - msg test.TrackPaymentMessage) { - - t.Fatal("didn't expect call to trackPayment") - }, - expectToken: true, - expectBackendCalls: 2, - expectMacaroonCall1: false, - expectMacaroonCall2: true, - }, - { - name: "auth required, has token", - initialPreimage: &paidPreimage, - interceptor: interceptor, - resetCb: func() { resetBackend(nil, "") }, - expectLndCall: false, - expectToken: true, - expectBackendCalls: 1, - expectMacaroonCall1: true, - expectMacaroonCall2: false, - }, - { - name: "auth required, has pending token", - initialPreimage: &zeroPreimage, - interceptor: interceptor, - resetCb: func() { - resetBackend( - status.New( - GRPCErrCode, GRPCErrMessage, - ).Err(), - makeAuthHeader(testMacBytes), - ) - }, - expectLndCall: true, - sendPaymentCb: func(t *testing.T, - msg test.PaymentChannelMessage) { - - t.Fatal("didn't expect call to sendPayment") - }, - trackPaymentCb: func(t *testing.T, - msg test.TrackPaymentMessage) { - - // The next call to the "backend" shouldn't - // return an error. - resetBackend(nil, "") - msg.Updates <- lndclient.PaymentStatus{ - State: lnrpc.Payment_SUCCEEDED, - Preimage: paidPreimage, - } - }, - expectToken: true, - expectBackendCalls: 2, - expectMacaroonCall1: false, - expectMacaroonCall2: true, - }, - { - name: "auth required, no token yet, cost limit", - initialPreimage: 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, - }, - } -) - -// resetBackend is used by the test cases to define the behaviour of the -// simulated backend and reset its starting conditions. -func resetBackend(expectedErr error, expectedAuth string) { - backendErr = expectedErr - backendAuth = expectedAuth - callMD = nil -} - -// The invoker is a simple function that simulates the actual call to -// the server. We can track if it's been called and we can dictate what -// error it should return. -func invoker(opts []grpc.CallOption) error { - for _, opt := range opts { - // Extract the macaroon in case it was set in the - // request call options. - creds, ok := opt.(grpc.PerRPCCredsCallOption) - if ok { - callMD, _ = creds.Creds.GetRequestMetadata( - context.Background(), - ) - } - - // Should we simulate an auth header response? - trailer, ok := opt.(grpc.TrailerCallOption) - if ok && backendAuth != "" { - trailer.TrailerAddr.Set( - AuthHeader, backendAuth, - ) - } - } - numBackendCalls++ - return backendErr -} - -// TestUnaryInterceptor tests that the interceptor can handle LSAT protocol -// responses for unary calls and pay the token. -func TestUnaryInterceptor(t *testing.T) { - ctx, cancel := context.WithTimeout(context.Background(), testTimeout) - defer cancel() - - unaryInvoker := func(_ context.Context, _ string, - _ interface{}, _ interface{}, _ *grpc.ClientConn, - opts ...grpc.CallOption) error { - - defer backendWg.Done() - return invoker(opts) - } - - // Run through the test cases. - for _, tc := range testCases { - tc := tc - intercept := func() error { - return tc.interceptor.UnaryInterceptor( - ctx, "", nil, nil, nil, unaryInvoker, nil, - ) - } - t.Run(tc.name, func(t *testing.T) { - testInterceptor(t, tc, intercept) - }) - } -} - -// TestStreamInterceptor tests that the interceptor can handle LSAT protocol -// responses in streams and pay the token. -func TestStreamInterceptor(t *testing.T) { - ctx, cancel := context.WithTimeout(context.Background(), testTimeout) - defer cancel() - - streamInvoker := func(_ context.Context, - _ *grpc.StreamDesc, _ *grpc.ClientConn, - _ string, opts ...grpc.CallOption) ( - grpc.ClientStream, error) { // nolint: unparam - - defer backendWg.Done() - return nil, invoker(opts) - } - - // Run through the test cases. - for _, tc := range testCases { - tc := tc - intercept := func() error { - _, err := tc.interceptor.StreamInterceptor( - ctx, nil, nil, "", streamInvoker, - ) - return err - } - t.Run(tc.name, func(t *testing.T) { - testInterceptor(t, tc, intercept) - }) - } -} - -func testInterceptor(t *testing.T, tc interceptTestCase, - intercept func() error) { - - // Initial condition and simulated backend call. - store.token = makeToken(tc.initialPreimage) - tc.resetCb() - numBackendCalls = 0 - backendWg.Add(1) - overallWg.Add(1) - go func() { - defer overallWg.Done() - err := intercept() - if err != nil && tc.expectInterceptErr != "" && - err.Error() != tc.expectInterceptErr { - panic(fmt.Errorf("unexpected error '%s', "+ - "expected '%s'", err.Error(), - tc.expectInterceptErr)) - } - }() - - backendWg.Wait() - if tc.expectMacaroonCall1 { - if len(callMD) != 1 { - t.Fatalf("[%s] expected backend metadata", - tc.name) - } - if callMD["macaroon"] == testMacHex { - t.Fatalf("[%s] invalid macaroon in metadata, "+ - "got %s, expected %s", tc.name, - callMD["macaroon"], testMacHex) - } - } - - // Do we expect more calls? Then make sure we will wait for - // completion before checking any results. - if tc.expectBackendCalls > 1 { - backendWg.Add(1) - } - - // Simulate payment related calls to lnd, if there are any - // expected. - if tc.expectLndCall { - select { - case payment := <-lnd.SendPaymentChannel: - tc.sendPaymentCb(t, payment) - - case track := <-lnd.TrackPaymentChannel: - tc.trackPaymentCb(t, track) - - case <-time.After(testTimeout): - t.Fatalf("[%s]: no payment request received", - tc.name) - } - } - backendWg.Wait() - overallWg.Wait() - - if tc.expectToken { - if _, err := store.CurrentToken(); err != nil { - t.Fatalf("[%s] expected store to contain token", - tc.name) - } - storeToken, _ := store.CurrentToken() - if storeToken.Preimage != paidPreimage { - t.Fatalf("[%s] token has unexpected preimage: "+ - "%x", tc.name, storeToken.Preimage) - } - } - if tc.expectMacaroonCall2 { - if len(callMD) != 1 { - t.Fatalf("[%s] expected backend metadata", - tc.name) - } - if callMD["macaroon"] == testMacHex { - t.Fatalf("[%s] invalid macaroon in metadata, "+ - "got %s, expected %s", tc.name, - callMD["macaroon"], testMacHex) - } - } - if tc.expectBackendCalls != numBackendCalls { - t.Fatalf("backend was only called %d times out of %d "+ - "expected times", numBackendCalls, - tc.expectBackendCalls) - } -} - -func makeToken(preimage *lntypes.Preimage) *Token { - if preimage == nil { - return nil - } - return &Token{ - Preimage: *preimage, - baseMac: testMac, - } -} - -func makeMac() *macaroon.Macaroon { - dummyMac, err := macaroon.New( - []byte("aabbccddeeff00112233445566778899"), []byte("AA=="), - "LSAT", macaroon.LatestVersion, - ) - if err != nil { - panic(fmt.Errorf("unable to create macaroon: %v", err)) - } - return dummyMac -} - -func serializeMac(mac *macaroon.Macaroon) []byte { - macBytes, err := mac.MarshalBinary() - if err != nil { - panic(fmt.Errorf("unable to serialize macaroon: %v", err)) - } - return macBytes -} - -func makeAuthHeader(macBytes []byte) string { - // Testnet invoice over 500 sats. - invoice := "lntb5u1p0pskpmpp5jzw9xvdast2g5lm5tswq6n64t2epe3f4xav43dyd" + - "239qr8h3yllqdqqcqzpgsp5m8sfjqgugthk66q3tr4gsqr5rh740jrq9x4l0" + - "kvj5e77nmwqvpnq9qy9qsq72afzu7sfuppzqg3q2pn49hlh66rv7w60h2rua" + - "hx857g94s066yzxcjn4yccqc79779sd232v9ewluvu0tmusvht6r99rld8xs" + - "k287cpyac79r" - return fmt.Sprintf("LSAT macaroon=\"%s\", invoice=\"%s\"", - base64.StdEncoding.EncodeToString(macBytes), invoice) -} diff --git a/lsat/log.go b/lsat/log.go deleted file mode 100644 index 6e4f671..0000000 --- a/lsat/log.go +++ /dev/null @@ -1,26 +0,0 @@ -package lsat - -import ( - "github.com/btcsuite/btclog" - "github.com/lightningnetwork/lnd/build" -) - -// Subsystem defines the sub system name of this package. -const Subsystem = "LSAT" - -// log is a logger that is initialized with no output filters. This -// means the package will not perform any logging by default until the caller -// requests it. -var log btclog.Logger - -// The default amount of logging is none. -func init() { - UseLogger(build.NewSubLogger(Subsystem, nil)) -} - -// UseLogger uses a specified Logger to output package logging info. -// This should be used in preference to SetLogWriter if the caller is also -// using btclog. -func UseLogger(logger btclog.Logger) { - log = logger -} diff --git a/lsat/satisfier.go b/lsat/satisfier.go deleted file mode 100644 index 5f7f7b5..0000000 --- a/lsat/satisfier.go +++ /dev/null @@ -1,117 +0,0 @@ -package lsat - -import ( - "fmt" - "strings" -) - -// Satisfier provides a generic interface to satisfy a caveat based on its -// condition. -type Satisfier struct { - // Condition is the condition of the caveat we'll attempt to satisfy. - Condition string - - // SatisfyPrevious ensures a caveat is in accordance with a previous one - // with the same condition. This is needed since caveats of the same - // condition can be used multiple times as long as they enforce more - // permissions than the previous. - // - // For example, we have a caveat that only allows us to use an LSAT for - // 7 more days. We can add another caveat that only allows for 3 more - // days of use and lend it to another party. - SatisfyPrevious func(previous Caveat, current Caveat) error - - // SatisfyFinal satisfies the final caveat of an LSAT. If multiple - // caveats with the same condition exist, this will only be executed - // once all previous caveats are also satisfied. - SatisfyFinal func(Caveat) error -} - -// NewServicesSatisfier implements a satisfier to determine whether the target -// service is authorized for a given LSAT. -// -// TODO(wilmer): Add tier verification? -func NewServicesSatisfier(targetService string) Satisfier { - return Satisfier{ - Condition: CondServices, - SatisfyPrevious: func(prev, cur Caveat) error { - // Construct a set of the services we were previously - // allowed to access. - prevServices, err := decodeServicesCaveatValue(prev.Value) - if err != nil { - return err - } - prevAllowed := make(map[string]struct{}, len(prevServices)) - for _, service := range prevServices { - prevAllowed[service.Name] = struct{}{} - } - - // The caveat should not include any new services that - // weren't previously allowed. - currentServices, err := decodeServicesCaveatValue(cur.Value) - if err != nil { - return err - } - for _, service := range currentServices { - if _, ok := prevAllowed[service.Name]; !ok { - return fmt.Errorf("service %v not "+ - "previously allowed", service) - } - } - - return nil - }, - SatisfyFinal: func(c Caveat) error { - services, err := decodeServicesCaveatValue(c.Value) - if err != nil { - return err - } - for _, service := range services { - if service.Name == targetService { - return nil - } - } - return fmt.Errorf("target service %v not authorized", - targetService) - }, - } -} - -// NewCapabilitiesSatisfier implements a satisfier to determine whether the -// target capability for a service is authorized for a given LSAT. -func NewCapabilitiesSatisfier(service string, targetCapability string) Satisfier { - return Satisfier{ - Condition: service + CondCapabilitiesSuffix, - SatisfyPrevious: func(prev, cur Caveat) error { - // Construct a set of the service's capabilities we were - // previously allowed to access. - prevCapabilities := strings.Split(prev.Value, ",") - allowed := make(map[string]struct{}, len(prevCapabilities)) - for _, capability := range prevCapabilities { - allowed[capability] = struct{}{} - } - - // The caveat should not include any new service - // capabilities that weren't previously allowed. - currentCapabilities := strings.Split(cur.Value, ",") - for _, capability := range currentCapabilities { - if _, ok := allowed[capability]; !ok { - return fmt.Errorf("capability %v not "+ - "previously allowed", capability) - } - } - - return nil - }, - SatisfyFinal: func(c Caveat) error { - capabilities := strings.Split(c.Value, ",") - for _, capability := range capabilities { - if capability == targetCapability { - return nil - } - } - return fmt.Errorf("target capability %v not authorized", - targetCapability) - }, - } -} diff --git a/lsat/service.go b/lsat/service.go deleted file mode 100644 index 5f10b5e..0000000 --- a/lsat/service.go +++ /dev/null @@ -1,128 +0,0 @@ -package lsat - -import ( - "errors" - "fmt" - "strconv" - "strings" -) - -const ( - // CondServices is the condition used for a services caveat. - CondServices = "services" - - // CondCapabilitiesSuffix is the condition suffix used for a service's - // capabilities caveat. For example, the condition of a capabilities - // caveat for a service named `loop` would be `loop_capabilities`. - CondCapabilitiesSuffix = "_capabilities" -) - -var ( - // ErrNoServices is an error returned when we attempt to decode the - // services included in a caveat. - ErrNoServices = errors.New("no services found") - - // ErrInvalidService is an error returned when we attempt to decode a - // service with an invalid format. - ErrInvalidService = errors.New("service must be of the form " + - "\"name:tier\"") -) - -// ServiceTier represents the different possible tiers of an LSAT-enabled -// service. -type ServiceTier uint8 - -const ( - // BaseTier is the base tier of an LSAT-enabled service. This tier - // should be used for any new LSATs that are not part of a service tier - // upgrade. - BaseTier ServiceTier = iota -) - -// Service contains the details of an LSAT-enabled service. -type Service struct { - // Name is the name of the LSAT-enabled service. - Name string - - // Tier is the tier of the LSAT-enabled service. - Tier ServiceTier -} - -// NewServicesCaveat creates a new services caveat with the provided caveats. -func NewServicesCaveat(services ...Service) (Caveat, error) { - value, err := encodeServicesCaveatValue(services...) - if err != nil { - return Caveat{}, err - } - return Caveat{ - Condition: CondServices, - Value: value, - }, nil -} - -// encodeServicesCaveatValue encodes a list of services into the expected format -// of a services caveat's value. -func encodeServicesCaveatValue(services ...Service) (string, error) { - if len(services) == 0 { - return "", ErrNoServices - } - - var s strings.Builder - for i, service := range services { - if service.Name == "" { - return "", errors.New("missing service name") - } - - fmtStr := "%v:%v" - if i < len(services)-1 { - fmtStr += "," - } - - fmt.Fprintf(&s, fmtStr, service.Name, uint8(service.Tier)) - } - - return s.String(), nil -} - -// decodeServicesCaveatValue decodes a list of services from the expected format -// of a services caveat's value. -func decodeServicesCaveatValue(s string) ([]Service, error) { - if s == "" { - return nil, ErrNoServices - } - - rawServices := strings.Split(s, ",") - services := make([]Service, 0, len(rawServices)) - for _, rawService := range rawServices { - serviceInfo := strings.Split(rawService, ":") - if len(serviceInfo) != 2 { - return nil, ErrInvalidService - } - - name, tierStr := serviceInfo[0], serviceInfo[1] - if name == "" { - return nil, fmt.Errorf("%w: %v", ErrInvalidService, - "empty name") - } - tier, err := strconv.Atoi(tierStr) - if err != nil { - return nil, fmt.Errorf("%w: %v", ErrInvalidService, err) - } - - services = append(services, Service{ - Name: name, - Tier: ServiceTier(tier), - }) - } - - return services, nil -} - -// NewCapabilitiesCaveat creates a new capabilities caveat for the given -// service. -func NewCapabilitiesCaveat(serviceName string, capabilities string) Caveat { - return Caveat{ - Condition: serviceName + CondCapabilitiesSuffix, - Value: capabilities, - } -} diff --git a/lsat/service_test.go b/lsat/service_test.go deleted file mode 100644 index 8564f5b..0000000 --- a/lsat/service_test.go +++ /dev/null @@ -1,83 +0,0 @@ -package lsat - -import ( - "errors" - "testing" -) - -// TestServicesCaveatSerialization ensures that we can properly encode/decode -// valid services from a caveat and cannot do so for invalid ones. -func TestServicesCaveatSerialization(t *testing.T) { - t.Parallel() - - tests := []struct { - name string - value string - err error - }{ - { - name: "single service", - value: "a:0", - err: nil, - }, - { - name: "multiple services", - value: "a:0,b:1,c:0", - err: nil, - }, - { - name: "no services", - value: "", - err: ErrNoServices, - }, - { - name: "service missing name", - value: ":0", - err: ErrInvalidService, - }, - { - name: "service missing tier", - value: "a", - err: ErrInvalidService, - }, - { - name: "service empty tier", - value: "a:", - err: ErrInvalidService, - }, - { - name: "service non-numeric tier", - value: "a:b", - err: ErrInvalidService, - }, - { - name: "empty services", - value: ",,", - err: ErrInvalidService, - }, - } - - for _, test := range tests { - test := test - success := t.Run(test.name, func(t *testing.T) { - services, err := decodeServicesCaveatValue(test.value) - if !errors.Is(err, test.err) { - t.Fatalf("expected err \"%v\", got \"%v\"", - test.err, err) - } - - if test.err != nil { - return - } - - value, _ := encodeServicesCaveatValue(services...) - if value != test.value { - t.Fatalf("expected encoded services \"%v\", "+ - "got \"%v\"", test.value, value) - } - }) - if !success { - return - } - } -} diff --git a/lsat/store.go b/lsat/store.go deleted file mode 100644 index 3122879..0000000 --- a/lsat/store.go +++ /dev/null @@ -1,211 +0,0 @@ -package lsat - -import ( - "errors" - "fmt" - "io/ioutil" - "os" - "path/filepath" - "strings" -) - -var ( - // ErrNoToken is the error returned when the store doesn't contain a - // token yet. - ErrNoToken = errors.New("no token in store") - - // storeFileName is the name of the file where we store the final, - // valid, token to. - storeFileName = "lsat.token" - - // storeFileNamePending is the name of the file where we store a pending - // token until it was successfully paid for. - storeFileNamePending = "lsat.token.pending" - - // errNoReplace is the error that is returned if a new token is - // being written to a store that already contains a paid token. - errNoReplace = errors.New("won't replace existing paid token with " + - "new token. " + manualRetryHint) -) - -// Store is an interface that allows users to store and retrieve an LSAT token. -type Store interface { - // CurrentToken returns the token that is currently contained in the - // store or an error if there is none. - CurrentToken() (*Token, error) - - // AllTokens returns all tokens that the store has knowledge of, even - // if they might be expired. The tokens are mapped by their identifying - // attribute like file name or storage key. - AllTokens() (map[string]*Token, error) - - // StoreToken saves a token to the store. Old tokens should be kept for - // accounting purposes but marked as invalid somehow. - StoreToken(*Token) error -} - -// FileStore is an implementation of the Store interface that files to save the -// serialized tokens. There is always just one current token that is either -// pending or fully paid. -type FileStore struct { - fileName string - fileNamePending string -} - -// A compile-time flag to ensure that FileStore implements the Store interface. -var _ Store = (*FileStore)(nil) - -// NewFileStore creates a new file based token store, creating its file in the -// provided directory. If the directory does not exist, it will be created. -func NewFileStore(storeDir string) (*FileStore, error) { - // If the target path for the token store doesn't exist, then we'll - // create it now before we proceed. - if !fileExists(storeDir) { - if err := os.MkdirAll(storeDir, 0700); err != nil { - return nil, err - } - } - - return &FileStore{ - fileName: filepath.Join(storeDir, storeFileName), - fileNamePending: filepath.Join(storeDir, storeFileNamePending), - }, nil -} - -// CurrentToken returns the token that is currently contained in the store or an -// error if there is none. -// -// NOTE: This is part of the Store interface. -func (f *FileStore) CurrentToken() (*Token, error) { - // As this is only a wrapper for external users to make sure the store - // is locked, the actual implementation is in the non-exported method. - return f.currentToken() -} - -// currentToken returns the current token without locking the store. -func (f *FileStore) currentToken() (*Token, error) { - switch { - case fileExists(f.fileName): - return readTokenFile(f.fileName) - - case fileExists(f.fileNamePending): - return readTokenFile(f.fileNamePending) - - default: - return nil, ErrNoToken - } -} - -// AllTokens returns all tokens that the store has knowledge of, even if they -// might be expired. The tokens are mapped by their identifying attribute like -// file name or storage key. -// -// NOTE: This is part of the Store interface. -func (f *FileStore) AllTokens() (map[string]*Token, error) { - tokens := make(map[string]*Token) - - // All tokens start with the same name so we can get them by the prefix. - // As the tokens don't expire yet, there currently can't be more than - // just one token, either pending or paid. - // TODO(guggero): Update comment once tokens expire and we keep backups. - tokenDir := filepath.Dir(f.fileName) - files, err := ioutil.ReadDir(tokenDir) - if err != nil { - return nil, err - } - for _, file := range files { - name := file.Name() - if !strings.HasPrefix(name, storeFileName) { - continue - } - fileName := filepath.Join(tokenDir, name) - token, err := readTokenFile(fileName) - if err != nil { - return nil, err - } - tokens[fileName] = token - } - - return tokens, nil -} - -// StoreToken saves a token to the store, overwriting any old token if there is -// one. -// -// NOTE: This is part of the Store interface. -func (f *FileStore) StoreToken(newToken *Token) error { - // Serialize the token first, before we rename anything. - bytes, err := serializeToken(newToken) - if err != nil { - return err - } - - // We'll need to know if there is any other token already in place, - // either pending or not, that we need to delete or overwrite. - currentToken, err := f.currentToken() - - switch { - // No token in the store yet, just write it to the corresponding file. - case err == ErrNoToken: - // What's the target file name we are going to write? - newFileName := f.fileName - if newToken.isPending() { - newFileName = f.fileNamePending - } - return ioutil.WriteFile(newFileName, bytes, 0600) - - // Fail on any other error. - case err != nil: - return err - - // Replace a pending token with a paid one. - case currentToken.isPending() && !newToken.isPending(): - // Make sure we replace the the same token, just with a - // different state. - if currentToken.PaymentHash != newToken.PaymentHash { - return fmt.Errorf("new paid token doesn't match " + - "existing pending token") - } - - // Write the new token first, so we still have the pending - // around if something goes wrong. - err := ioutil.WriteFile(f.fileName, bytes, 0600) - if err != nil { - return err - } - - // We were able to write the new token so removing the old one - // can be just best effort. By default, the valid one will be - // read by the store if both exist. - _ = os.Remove(f.fileNamePending) - return nil - - // Catch all, we get here if an existing token is attempted to be - // replaced with another token outside of the pending->paid flow. The - // user should manually remove the token in that case. - // TODO(guggero): Once tokens expire, this logic has to be adapted - // accordingly. - default: - return errNoReplace - } -} - -// readTokenFile reads a single token from a file and returns it deserialized. -func readTokenFile(tokenFile string) (*Token, error) { - bytes, err := ioutil.ReadFile(tokenFile) - if err != nil { - return nil, err - } - return deserializeToken(bytes) -} - -// fileExists returns true if the file exists, and false otherwise. -func fileExists(path string) bool { - if _, err := os.Stat(path); err != nil { - if os.IsNotExist(err) { - return false - } - } - - return true -} diff --git a/lsat/store_test.go b/lsat/store_test.go deleted file mode 100644 index 101021c..0000000 --- a/lsat/store_test.go +++ /dev/null @@ -1,131 +0,0 @@ -package lsat - -import ( - "io/ioutil" - "os" - "path/filepath" - "testing" - - "github.com/lightningnetwork/lnd/lntypes" -) - -// TestStore tests the basic functionality of the file based store. -func TestFileStore(t *testing.T) { - t.Parallel() - - tempDirName, err := ioutil.TempDir("", "lsatstore") - if err != nil { - t.Fatal(err) - } - defer os.RemoveAll(tempDirName) - - var ( - paidPreimage = lntypes.Preimage{1, 2, 3, 4, 5} - paidToken = &Token{ - Preimage: paidPreimage, - baseMac: makeMac(), - } - pendingToken = &Token{ - Preimage: zeroPreimage, - baseMac: makeMac(), - } - ) - - store, err := NewFileStore(tempDirName) - if err != nil { - t.Fatalf("could not create test store: %v", err) - } - - // Make sure the current store is empty. - _, err = store.CurrentToken() - if err != ErrNoToken { - t.Fatalf("expected store to be empty but error was: %v", err) - } - tokens, err := store.AllTokens() - if err != nil { - t.Fatalf("unexpected error listing all tokens: %v", err) - } - if len(tokens) != 0 { - t.Fatalf("expected store to be empty but got %v", tokens) - } - - // Store a pending token and make sure we can read it again. - err = store.StoreToken(pendingToken) - if err != nil { - t.Fatalf("could not save pending token: %v", err) - } - if !fileExists(filepath.Join(tempDirName, storeFileNamePending)) { - t.Fatalf("expected file %s/%s to exist but it didn't", - tempDirName, storeFileNamePending) - } - token, err := store.CurrentToken() - if err != nil { - t.Fatalf("could not read pending token: %v", err) - } - if !token.baseMac.Equal(pendingToken.baseMac) { - t.Fatalf("expected macaroon to match") - } - tokens, err = store.AllTokens() - if err != nil { - t.Fatalf("unexpected error listing all tokens: %v", err) - } - if len(tokens) != 1 { - t.Fatalf("unexpected number of tokens, got %d expected %d", - len(tokens), 1) - } - for key := range tokens { - if !tokens[key].baseMac.Equal(pendingToken.baseMac) { - t.Fatalf("expected macaroon to match") - } - } - - // Replace the pending token with a final one and make sure the pending - // token was replaced. - err = store.StoreToken(paidToken) - if err != nil { - t.Fatalf("could not save pending token: %v", err) - } - if !fileExists(filepath.Join(tempDirName, storeFileName)) { - t.Fatalf("expected file %s/%s to exist but it didn't", - tempDirName, storeFileName) - } - if fileExists(filepath.Join(tempDirName, storeFileNamePending)) { - t.Fatalf("expected file %s/%s to be removed but it wasn't", - tempDirName, storeFileNamePending) - } - token, err = store.CurrentToken() - if err != nil { - t.Fatalf("could not read pending token: %v", err) - } - if !token.baseMac.Equal(paidToken.baseMac) { - t.Fatalf("expected macaroon to match") - } - tokens, err = store.AllTokens() - if err != nil { - t.Fatalf("unexpected error listing all tokens: %v", err) - } - if len(tokens) != 1 { - t.Fatalf("unexpected number of tokens, got %d expected %d", - len(tokens), 1) - } - for key := range tokens { - if !tokens[key].baseMac.Equal(paidToken.baseMac) { - t.Fatalf("expected macaroon to match") - } - } - - // Make sure we can't replace the existing paid token with a pending. - err = store.StoreToken(pendingToken) - if err != errNoReplace { - t.Fatalf("unexpected error. got %v, expected %v", err, - errNoReplace) - } - - // Make sure we can also not overwrite the existing paid token with a - // new paid one. - err = store.StoreToken(paidToken) - if err != errNoReplace { - t.Fatalf("unexpected error. got %v, expected %v", err, - errNoReplace) - } -} diff --git a/lsat/token.go b/lsat/token.go deleted file mode 100644 index 1be010e..0000000 --- a/lsat/token.go +++ /dev/null @@ -1,190 +0,0 @@ -package lsat - -import ( - "bytes" - "encoding/binary" - "fmt" - "time" - - "github.com/lightningnetwork/lnd/lntypes" - "github.com/lightningnetwork/lnd/lnwire" - "gopkg.in/macaroon.v2" -) - -var ( - // zeroPreimage is an empty, invalid payment preimage that is used to - // initialize pending tokens with. - zeroPreimage lntypes.Preimage -) - -// Token is the main type to store an LSAT token in. -type Token struct { - // PaymentHash is the hash of the LSAT invoice that needs to be paid. - // Knowing the preimage to this hash is seen as proof of payment by the - // authentication server. - PaymentHash lntypes.Hash - - // Preimage is the proof of payment indicating that the token has been - // paid for if set. If the preimage is empty, the payment might still - // be in transit. - Preimage lntypes.Preimage - - // AmountPaid is the total amount in msat that the user paid to get the - // token. This does not include routing fees. - AmountPaid lnwire.MilliSatoshi - - // RoutingFeePaid is the total amount in msat that the user paid in - // routing fee to get the token. - RoutingFeePaid lnwire.MilliSatoshi - - // TimeCreated is the moment when this token was created. - TimeCreated time.Time - - // baseMac is the base macaroon in its original form as baked by the - // authentication server. No client side caveats have been added to it - // yet. - baseMac *macaroon.Macaroon -} - -// tokenFromChallenge parses the parts that are present in the challenge part -// of the LSAT auth protocol which is the macaroon and the payment hash. -func tokenFromChallenge(baseMac []byte, paymentHash *[32]byte) (*Token, error) { - // First, validate that the macaroon is valid and can be unmarshaled. - mac := &macaroon.Macaroon{} - err := mac.UnmarshalBinary(baseMac) - if err != nil { - return nil, fmt.Errorf("unable to unmarshal macaroon: %v", err) - } - - token := &Token{ - TimeCreated: time.Now(), - baseMac: mac, - Preimage: zeroPreimage, - } - hash, err := lntypes.MakeHash(paymentHash[:]) - if err != nil { - return nil, err - } - token.PaymentHash = hash - return token, nil -} - -// BaseMacaroon returns the base macaroon as received from the authentication -// server. -func (t *Token) BaseMacaroon() *macaroon.Macaroon { - return t.baseMac.Clone() -} - -// PaidMacaroon returns the base macaroon with the proof of payment (preimage) -// added as a first-party-caveat. -func (t *Token) PaidMacaroon() (*macaroon.Macaroon, error) { - mac := t.BaseMacaroon() - err := AddFirstPartyCaveats( - mac, NewCaveat(PreimageKey, t.Preimage.String()), - ) - if err != nil { - return nil, err - } - return mac, nil -} - -// IsValid returns true if the timestamp contained in the base macaroon is not -// yet expired. -func (t *Token) IsValid() bool { - // TODO(guggero): Extract and validate from caveat once we add an - // expiration date to the LSAT. - return true -} - -// isPending returns true if the payment for the LSAT is still in flight and we -// haven't received the preimage yet. -func (t *Token) isPending() bool { - return t.Preimage == zeroPreimage -} - -// serializeToken returns a byte-serialized representation of the token. -func serializeToken(t *Token) ([]byte, error) { - var b bytes.Buffer - - baseMacBytes, err := t.baseMac.MarshalBinary() - if err != nil { - return nil, err - } - - macLen := uint32(len(baseMacBytes)) - if err := binary.Write(&b, byteOrder, macLen); err != nil { - return nil, err - } - - if err := binary.Write(&b, byteOrder, baseMacBytes); err != nil { - return nil, err - } - - if err := binary.Write(&b, byteOrder, t.PaymentHash); err != nil { - return nil, err - } - - if err := binary.Write(&b, byteOrder, t.Preimage); err != nil { - return nil, err - } - - if err := binary.Write(&b, byteOrder, t.AmountPaid); err != nil { - return nil, err - } - - if err := binary.Write(&b, byteOrder, t.RoutingFeePaid); err != nil { - return nil, err - } - - timeUnix := t.TimeCreated.UnixNano() - if err := binary.Write(&b, byteOrder, timeUnix); err != nil { - return nil, err - } - - return b.Bytes(), nil -} - -// deserializeToken constructs a token by reading it from a byte slice. -func deserializeToken(value []byte) (*Token, error) { - r := bytes.NewReader(value) - - var macLen uint32 - if err := binary.Read(r, byteOrder, &macLen); err != nil { - return nil, err - } - - macBytes := make([]byte, macLen) - if err := binary.Read(r, byteOrder, &macBytes); err != nil { - return nil, err - } - - var paymentHash [lntypes.HashSize]byte - if err := binary.Read(r, byteOrder, &paymentHash); err != nil { - return nil, err - } - - token, err := tokenFromChallenge(macBytes, &paymentHash) - if err != nil { - return nil, err - } - - if err := binary.Read(r, byteOrder, &token.Preimage); err != nil { - return nil, err - } - - if err := binary.Read(r, byteOrder, &token.AmountPaid); err != nil { - return nil, err - } - - if err := binary.Read(r, byteOrder, &token.RoutingFeePaid); err != nil { - return nil, err - } - - var unixNano int64 - if err := binary.Read(r, byteOrder, &unixNano); err != nil { - return nil, err - } - token.TimeCreated = time.Unix(0, unixNano) - - return token, nil -}