diff --git a/.golangci.yml b/.golangci.yml index 914e57897..5b870ccbe 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -55,14 +55,16 @@ linters: - wsl_v5 - noinlineerr settings: + staticcheck: + checks: + - all + - -QF* + - -ST* gosec: excludes: - G402 - G306 - G115 - staticcheck: - checks: - - -SA1019 tagliatelle: case: rules: diff --git a/cmd/loop/loopin.go b/cmd/loop/loopin.go index 0f1401d28..d0512040d 100644 --- a/cmd/loop/loopin.go +++ b/cmd/loop/loopin.go @@ -207,7 +207,7 @@ func loopIn(ctx context.Context, cmd *cli.Command) error { } fmt.Printf("Swap initiated\n") - fmt.Printf("ID: %v\n", resp.Id) + fmt.Printf("ID: %x\n", resp.IdBytes) if resp.HtlcAddressP2Tr != "" { fmt.Printf("HTLC address (P2TR): %v\n", resp.HtlcAddressP2Tr) diff --git a/cmd/loop/main.go b/cmd/loop/main.go index 113404fdc..294095ca0 100644 --- a/cmd/loop/main.go +++ b/cmd/loop/main.go @@ -6,7 +6,6 @@ import ( "encoding/json" "errors" "fmt" - "io/ioutil" "os" "os/signal" "path/filepath" @@ -646,7 +645,7 @@ func getClientConn(address, tlsCertPath, macaroonPath string) (daemonConn, // gRPC dial options from it. func readMacaroon(macPath string) (grpc.DialOption, error) { // Load the specified macaroon file. - macBytes, err := ioutil.ReadFile(macPath) + macBytes, err := os.ReadFile(macPath) if err != nil { return nil, fmt.Errorf("unable to read macaroon path : %v", err) } diff --git a/cmd/loop/openchannel.go b/cmd/loop/openchannel.go index 48a85f56f..d65056cde 100644 --- a/cmd/loop/openchannel.go +++ b/cmd/loop/openchannel.go @@ -13,13 +13,11 @@ import ( ) const ( - defaultUtxoMinConf = 1 -) - -var ( + defaultUtxoMinConf = 1 channelTypeTweakless = "tweakless" channelTypeAnchors = "anchors" - channelTypeSimpleTaproot = "taproot" + channelTypeSimpleTaproot = "simple-taproot" + channelTypeTaproot = "taproot" ) var openChannelCommand = &cli.Command{ @@ -137,9 +135,9 @@ var openChannelCommand = &cli.Command{ &cli.StringFlag{ Name: "channel_type", Usage: fmt.Sprintf("(optional) the type of channel to "+ - "propose to the remote peer (%q, %q, %q)", + "propose to the remote peer (%q, %q, %q, %q)", channelTypeTweakless, channelTypeAnchors, - channelTypeSimpleTaproot), + channelTypeSimpleTaproot, channelTypeTaproot), }, &cli.BoolFlag{ Name: "zero_conf", @@ -322,6 +320,7 @@ func openChannel(ctx context.Context, cmd *cli.Command) error { switch channelType { case "": break + case channelTypeTweakless: req.CommitmentType = lnrpc.CommitmentType_STATIC_REMOTE_KEY @@ -330,6 +329,10 @@ func openChannel(ctx context.Context, cmd *cli.Command) error { case channelTypeSimpleTaproot: req.CommitmentType = lnrpc.CommitmentType_SIMPLE_TAPROOT + + case channelTypeTaproot: + req.CommitmentType = lnrpc.CommitmentType_TAPROOT + default: return fmt.Errorf("unsupported channel type %v", channelType) } diff --git a/cmd/loop/testdata/sessions/static-openchannel/01_loop-static-openchannel-help.json b/cmd/loop/testdata/sessions/static-openchannel/01_loop-static-openchannel-help.json index 3861060df..56b535a17 100644 --- a/cmd/loop/testdata/sessions/static-openchannel/01_loop-static-openchannel-help.json +++ b/cmd/loop/testdata/sessions/static-openchannel/01_loop-static-openchannel-help.json @@ -67,7 +67,7 @@ " --max_local_csv uint (optional) the maximum number of blocks that we will allow the remote peer to require we wait before accessing our funds in the case of a unilateral close. (default: 0)\n", " --close_address string (optional) an address to enforce payout of our funds to on cooperative close. Note that if this value is set on channel open, you will *not* be able to cooperatively close to a different address.\n", " --remote_max_value_in_flight_msat uint (optional) the maximum value in msat that can be pending within the channel at any given time (default: 0)\n", - " --channel_type string (optional) the type of channel to propose to the remote peer (\"tweakless\", \"anchors\", \"taproot\")\n", + " --channel_type string (optional) the type of channel to propose to the remote peer (\"tweakless\", \"anchors\", \"simple-taproot\", \"taproot\")\n", " --zero_conf (optional) whether a zero-conf channel open should be attempted. (default: false)\n", " --scid_alias (optional) whether a scid-alias channel type should be negotiated. (default: false)\n", " --remote_reserve_sats uint (optional) the minimum number of satoshis we require the remote node to keep as a direct payment. If not specified, a default of 1% of the channel capacity will be used. (default: 0)\n", diff --git a/cmd/loop/testdata/sessions/static-openchannel/07_loop-static-openchannel-taproot-success.json b/cmd/loop/testdata/sessions/static-openchannel/07_loop-static-openchannel-taproot-success.json new file mode 100644 index 000000000..2eb701545 --- /dev/null +++ b/cmd/loop/testdata/sessions/static-openchannel/07_loop-static-openchannel-taproot-success.json @@ -0,0 +1,104 @@ +{ + "metadata": { + "args": [ + "/home/user/bin/loop", + "--rpcserver=localhost:11010", + "--loopdir=/redacted/loop", + "--tlscertpath=/redacted/loop/regtest/tls.cert", + "--macaroonpath=/redacted/loop/regtest/loop.macaroon", + "static", + "openchannel", + "--node_key", + "03465f68fd39358667678f8353a31e0d99475e3fd2fb4e58daf7dbfabe04c011f4", + "--fundmax", + "--utxo", + "89f6fd2ee96445c6e48278e7eaaca7de8e342904f20609ce72c0d27afb443e2d:1", + "--channel_type", + "taproot", + "--private", + "--network", + "regtest" + ], + "env": {}, + "version": "0.33.2-beta commit=v0.33.2-beta-bump-lnd-21-a-7-g21019e684ed06e2267382f473dfeaafb81a33310 commit_hash=21019e684ed06e2267382f473dfeaafb81a33310", + "duration": 295879956, + "clock_start_unix": 1782002722 + }, + "events": [ + { + "time_ms": 2, + "kind": "grpc", + "data": { + "method": "/looprpc.SwapClient/StaticOpenChannel", + "event": "request", + "message_type": "looprpc.StaticOpenChannelRequest", + "payload": { + "open_channel_request": { + "sat_per_vbyte": "0", + "node_pubkey": "A0ZfaP05NYZnZ4+DU6MeDZlHXj/S+05Y2vfb+r4EwBH0", + "node_pubkey_string": "", + "local_funding_amount": "0", + "push_sat": "0", + "target_conf": 0, + "sat_per_byte": "0", + "private": true, + "min_htlc_msat": "0", + "remote_csv_delay": 0, + "min_confs": 1, + "spend_unconfirmed": false, + "close_address": "", + "funding_shim": null, + "remote_max_value_in_flight_msat": "0", + "remote_max_htlcs": 0, + "max_local_csv": 0, + "commitment_type": "TAPROOT", + "zero_conf": false, + "scid_alias": false, + "base_fee": "0", + "fee_rate": "0", + "use_base_fee": false, + "use_fee_rate": false, + "remote_chan_reserve_sat": "0", + "fund_max": true, + "memo": "", + "outpoints": [ + { + "txid_bytes": "", + "txid_str": "89f6fd2ee96445c6e48278e7eaaca7de8e342904f20609ce72c0d27afb443e2d", + "output_index": 1 + } + ] + } + } + } + }, + { + "time_ms": 295, + "kind": "grpc", + "data": { + "method": "/looprpc.SwapClient/StaticOpenChannel", + "event": "response", + "message_type": "looprpc.StaticOpenChannelResponse", + "payload": { + "channel_open_outpoint": "302a41b53946494306e415bdf4724e88b2cf2b37ad2f36dc075db00cc6499e9f:0" + } + } + }, + { + "time_ms": 295, + "kind": "stdout", + "data": { + "lines": [ + "{\n", + " \"channel_open_outpoint\": \"302a41b53946494306e415bdf4724e88b2cf2b37ad2f36dc075db00cc6499e9f:0\"\n", + "}\n" + ] + } + }, + { + "time_ms": 295, + "kind": "exit", + "data": {} + } + ] +} diff --git a/cmd/loop/testdata/sessions/static-openchannel/08_loop-static-openchannel-taproot-unsupported-lnd.json b/cmd/loop/testdata/sessions/static-openchannel/08_loop-static-openchannel-taproot-unsupported-lnd.json new file mode 100644 index 000000000..3315aa853 --- /dev/null +++ b/cmd/loop/testdata/sessions/static-openchannel/08_loop-static-openchannel-taproot-unsupported-lnd.json @@ -0,0 +1,106 @@ +{ + "metadata": { + "args": [ + "/home/user/bin/loop", + "--rpcserver=localhost:11010", + "--loopdir=/redacted/loop", + "--tlscertpath=/redacted/loop/regtest/tls.cert", + "--macaroonpath=/redacted/loop/regtest/loop.macaroon", + "static", + "openchannel", + "--node_key", + "026eca9330cc3e589505c4a240ea1f7f551e5765b1aae9040c6667141da657eae6", + "--fundmax", + "--utxo", + "dc942b3252ded50abc30ef4e8614d392ab0f091acab75fdd09e18a07f6ac36e6:0", + "--channel_type", + "taproot", + "--private", + "--network", + "regtest" + ], + "env": {}, + "version": "0.33.3-beta commit= commit_hash=", + "run_error": "rpc error: code = Unknown desc = channel_type=taproot is not supported by the connected lnd; update LND to v0.21.0-beta or later to use this channel type: got error from server: rpc error: code = Unknown desc = unhandled request channel type 7", + "duration": 39639744, + "clock_start_unix": 1782158339 + }, + "events": [ + { + "time_ms": 7, + "kind": "grpc", + "data": { + "method": "/looprpc.SwapClient/StaticOpenChannel", + "event": "request", + "message_type": "looprpc.StaticOpenChannelRequest", + "payload": { + "open_channel_request": { + "sat_per_vbyte": "0", + "node_pubkey": "Am7KkzDMPliVBcSiQOoff1UeV2WxqukEDGZnFB2mV+rm", + "node_pubkey_string": "", + "local_funding_amount": "0", + "push_sat": "0", + "target_conf": 0, + "sat_per_byte": "0", + "private": true, + "min_htlc_msat": "0", + "remote_csv_delay": 0, + "min_confs": 1, + "spend_unconfirmed": false, + "close_address": "", + "funding_shim": null, + "remote_max_value_in_flight_msat": "0", + "remote_max_htlcs": 0, + "max_local_csv": 0, + "commitment_type": "TAPROOT", + "zero_conf": false, + "scid_alias": false, + "base_fee": "0", + "fee_rate": "0", + "use_base_fee": false, + "use_fee_rate": false, + "remote_chan_reserve_sat": "0", + "fund_max": true, + "memo": "", + "outpoints": [ + { + "txid_bytes": "", + "txid_str": "dc942b3252ded50abc30ef4e8614d392ab0f091acab75fdd09e18a07f6ac36e6", + "output_index": 0 + } + ] + } + } + } + }, + { + "time_ms": 39, + "kind": "grpc", + "data": { + "method": "/looprpc.SwapClient/StaticOpenChannel", + "event": "error", + "error": "rpc error: code = Unknown desc = channel_type=taproot is not supported by the connected lnd; update LND to v0.21.0-beta or later to use this channel type: got error from server: rpc error: code = Unknown desc = unhandled request channel type 7", + "status": { + "code": 2, + "message": "channel_type=taproot is not supported by the connected lnd; update LND to v0.21.0-beta or later to use this channel type: got error from server: rpc error: code = Unknown desc = unhandled request channel type 7" + } + } + }, + { + "time_ms": 39, + "kind": "stderr", + "data": { + "lines": [ + "[loop] rpc error: code = Unknown desc = channel_type=taproot is not supported by the connected lnd; update LND to v0.21.0-beta or later to use this channel type: got error from server: rpc error: code = Unknown desc = unhandled request channel type 7\n" + ] + } + }, + { + "time_ms": 39, + "kind": "exit", + "data": { + "run_error": "rpc error: code = Unknown desc = channel_type=taproot is not supported by the connected lnd; update LND to v0.21.0-beta or later to use this channel type: got error from server: rpc error: code = Unknown desc = unhandled request channel type 7" + } + } + ] +} diff --git a/cost_migration.go b/cost_migration.go index 14fa2c870..d99ef58b7 100644 --- a/cost_migration.go +++ b/cost_migration.go @@ -142,6 +142,7 @@ func MigrateLoopOutCosts(ctx context.Context, lnd lndclient.LndServices, ctx, lndclient.ListPaymentsRequest{ Offset: offset, MaxPayments: uint64(paymentBatchSize), + OmitHops: true, }, ) if err != nil { diff --git a/cost_migration_test.go b/cost_migration_test.go index 082895f2e..abdc80d3b 100644 --- a/cost_migration_test.go +++ b/cost_migration_test.go @@ -163,6 +163,11 @@ func TestCostMigration(t *testing.T) { // Now we can run the migration. err = MigrateLoopOutCosts(context.Background(), lnd.LndServices, 1, store) require.NoError(t, err) + listPaymentsRequests := lnd.ListPaymentsRequestsSnapshot() + require.NotEmpty(t, listPaymentsRequests) + for _, req := range listPaymentsRequests { + require.True(t, req.OmitHops) + } // Finally check that the swap cost has been updated correctly. swap, err := store.FetchLoopOutSwap( diff --git a/docs/loop.1 b/docs/loop.1 index aae2c6ace..ba1ca3e16 100644 --- a/docs/loop.1 +++ b/docs/loop.1 @@ -589,7 +589,7 @@ Open a channel to an existing peer. \fB--base_fee_msat\fP="": the base fee in milli-satoshis that will be charged for each forwarded HTLC, regardless of payment size (default: 0) .PP -\fB--channel_type\fP="": (optional) the type of channel to propose to the remote peer ("tweakless", "anchors", "taproot") +\fB--channel_type\fP="": (optional) the type of channel to propose to the remote peer ("tweakless", "anchors", "simple-taproot", "taproot") .PP \fB--close_address\fP="": (optional) an address to enforce payout of our funds to on cooperative close. Note that if this value is set on channel open, you will \fInot\fP be able to cooperatively close to a different address. diff --git a/docs/loop.md b/docs/loop.md index 28bf8c0c7..2f48e9471 100644 --- a/docs/loop.md +++ b/docs/loop.md @@ -715,7 +715,7 @@ The following flags are supported: | `--max_local_csv="…"` | (optional) the maximum number of blocks that we will allow the remote peer to require we wait before accessing our funds in the case of a unilateral close | uint | `0` | | `--close_address="…"` | (optional) an address to enforce payout of our funds to on cooperative close. Note that if this value is set on channel open, you will *not* be able to cooperatively close to a different address | string | | `--remote_max_value_in_flight_msat="…"` | (optional) the maximum value in msat that can be pending within the channel at any given time | uint | `0` | -| `--channel_type="…"` | (optional) the type of channel to propose to the remote peer ("tweakless", "anchors", "taproot") | string | +| `--channel_type="…"` | (optional) the type of channel to propose to the remote peer ("tweakless", "anchors", "simple-taproot", "taproot") | string | | `--zero_conf` | (optional) whether a zero-conf channel open should be attempted | bool | `false` | | `--scid_alias` | (optional) whether a scid-alias channel type should be negotiated | bool | `false` | | `--remote_reserve_sats="…"` | (optional) the minimum number of satoshis we require the remote node to keep as a direct payment. If not specified, a default of 1% of the channel capacity will be used | uint | `0` | diff --git a/go.mod b/go.mod index 2cef1e7b4..1e312f90e 100644 --- a/go.mod +++ b/go.mod @@ -20,7 +20,7 @@ require ( github.com/jessevdk/go-flags v1.6.1 github.com/lib/pq v1.10.9 github.com/lightninglabs/aperture v0.4.0 - github.com/lightninglabs/lndclient v0.21.0-1 + github.com/lightninglabs/lndclient v0.21.0-2 github.com/lightninglabs/loop/looprpc v1.0.7 github.com/lightninglabs/loop/swapserverrpc v1.0.14 github.com/lightninglabs/taproot-assets v0.8.0 diff --git a/go.sum b/go.sum index 68ac806c2..39c3734a7 100644 --- a/go.sum +++ b/go.sum @@ -1116,8 +1116,8 @@ github.com/lightninglabs/gozmq v0.0.0-20191113021534-d20a764486bf h1:HZKvJUHlcXI github.com/lightninglabs/gozmq v0.0.0-20191113021534-d20a764486bf/go.mod h1:vxmQPeIQxPf6Jf9rM8R+B4rKBqLA2AjttNxkFBL2Plk= github.com/lightninglabs/lightning-node-connect/hashmailrpc v1.0.4-0.20250610182311-2f1d46ef18b7 h1:373o5lNr1udAdhcf5+zq/0dYpRtkvYLl8Lk6wG7I0DY= github.com/lightninglabs/lightning-node-connect/hashmailrpc v1.0.4-0.20250610182311-2f1d46ef18b7/go.mod h1:bDnEKRN1u13NFBuy/C+bFLhxA5bfd3clT25y76QY0AM= -github.com/lightninglabs/lndclient v0.21.0-1 h1:NuyccCK7tbMaH7hhqtewcx+qeBel4/RLJrlnQ/lMkkY= -github.com/lightninglabs/lndclient v0.21.0-1/go.mod h1:RUIcfPr82HrvZr3pu9f8nbD5v6VFbm+KgExqNNp5bE4= +github.com/lightninglabs/lndclient v0.21.0-2 h1:gA1utFMoKV6OZfHpYAQ1TX0wVs5cAtgTORRVavHR/ls= +github.com/lightninglabs/lndclient v0.21.0-2/go.mod h1:RUIcfPr82HrvZr3pu9f8nbD5v6VFbm+KgExqNNp5bE4= github.com/lightninglabs/migrate/v4 v4.18.2-9023d66a-fork-pr-2.0.20251211093704-71c1eef09789 h1:7kX7vUgHUazAHcCJ6uzBDa4/2MEGEbMEfa01GtfqmTQ= github.com/lightninglabs/migrate/v4 v4.18.2-9023d66a-fork-pr-2.0.20251211093704-71c1eef09789/go.mod h1:99BKpIi6ruaaXRM1A77eqZ+FWPQ3cfRa+ZVy5bmWMaY= github.com/lightninglabs/neutrino v0.17.1 h1:lNhgq7ix/N81R6oATroP/kHMzH1qzVVF2dEGcTlN2t4= diff --git a/liquidity/parameters.go b/liquidity/parameters.go index 44c555db6..9e8e5cc05 100644 --- a/liquidity/parameters.go +++ b/liquidity/parameters.go @@ -559,8 +559,11 @@ func RpcToParameters(req *clientrpc.LiquidityParameters) (*Parameters, req.AutoloopBudgetSat != 0 { params.AutoFeeRefreshPeriod = InfiniteDuration + // Keep reading the legacy start field so old stored + // liquidity parameters migrate to the refresh-period model. + budgetStartSec := req.AutoloopBudgetStartSec //nolint:staticcheck params.AutoloopBudgetLastRefresh = time.Unix( - int64(req.AutoloopBudgetStartSec), 0) + int64(budgetStartSec), 0) } for _, rule := range req.Rules { diff --git a/loopd/daemon.go b/loopd/daemon.go index 241d62e0f..91267811e 100644 --- a/loopd/daemon.go +++ b/loopd/daemon.go @@ -33,7 +33,7 @@ import ( "github.com/lightningnetwork/lnd/clock" "github.com/lightningnetwork/lnd/lntypes" "github.com/lightningnetwork/lnd/macaroons" - "go.etcd.io/bbolt" + bbolterrors "go.etcd.io/bbolt/errors" "google.golang.org/grpc" "google.golang.org/protobuf/encoding/protojson" "gopkg.in/macaroon-bakery.v2/bakery" @@ -161,7 +161,7 @@ func (d *Daemon) Start() error { // and error handlers. If this fails, then nothing has been started yet, // and we can just return the error. err = d.initialize(true) - if errors.Is(err, bbolt.ErrTimeout) { + if errors.Is(err, bbolterrors.ErrTimeout) { // We're trying to be started as a standalone Loop daemon, most // likely LiT is already running and blocking the DB return fmt.Errorf("%v: make sure no other loop daemon process "+ @@ -211,7 +211,7 @@ func (d *Daemon) StartAsSubserver(lndGrpc *lndclient.GrpcLndServices, // handlers. If this fails, then nothing has been started yet, and we // can just return the error. err := d.initialize(withMacaroonService) - if errors.Is(err, bbolt.ErrTimeout) { + if errors.Is(err, bbolterrors.ErrTimeout) { // We're trying to be started inside LiT so there most likely is // another standalone Loop process blocking the DB. return fmt.Errorf("%v: make sure no other loop daemon "+ @@ -994,7 +994,7 @@ func (d *Daemon) initialize(withMacaroonService bool) error { d.wg.Go(func() { infof("Starting static address open channel manager") err := openChannelManager.Run(d.mainCtx) - if err != nil && !errors.Is(context.Canceled, err) { + if err != nil && !errors.Is(err, context.Canceled) { d.internalErrChan <- err } infof("Static address open channel manager stopped") diff --git a/loopd/swapclient_server.go b/loopd/swapclient_server.go index bed6efa80..86d44a52a 100644 --- a/loopd/swapclient_server.go +++ b/loopd/swapclient_server.go @@ -251,6 +251,7 @@ func (s *swapClientServer) LoopOut(ctx context.Context, req.AssetSwapRfqId = in.AssetRfqInfo.SwapRfqId } + // Keep accepting the deprecated single-channel field for older clients. switch { case in.LoopOutChannel != 0 && len(in.OutgoingChanSet) > 0: // nolint:staticcheck return nil, errors.New("loop_out_channel and outgoing_" + @@ -273,7 +274,7 @@ func (s *swapClientServer) LoopOut(ctx context.Context, resp := &looprpc.SwapResponse{ Id: info.SwapHash.String(), IdBytes: info.SwapHash[:], - HtlcAddress: htlcAddress, + HtlcAddress: htlcAddress, //nolint:staticcheck ServerMessage: info.ServerMessage, } @@ -1182,11 +1183,11 @@ func (s *swapClientServer) LoopIn(ctx context.Context, if loopdb.CurrentProtocolVersion() < loopdb.ProtocolVersionHtlcV3 { p2wshAddr := swapInfo.HtlcAddressP2WSH.String() - response.HtlcAddress = p2wshAddr + response.HtlcAddress = p2wshAddr //nolint:staticcheck response.HtlcAddressP2Wsh = p2wshAddr } else { p2trAddr := swapInfo.HtlcAddressP2TR.String() - response.HtlcAddress = p2trAddr + response.HtlcAddress = p2trAddr //nolint:staticcheck response.HtlcAddressP2Tr = p2trAddr } diff --git a/loopdb/migration_04_updates_test.go b/loopdb/migration_04_updates_test.go index 37f05466c..f0e85f49f 100644 --- a/loopdb/migration_04_updates_test.go +++ b/loopdb/migration_04_updates_test.go @@ -2,8 +2,6 @@ package loopdb import ( "context" - "io/ioutil" - "os" "path/filepath" "testing" @@ -48,9 +46,7 @@ func TestMigrationUpdates(t *testing.T) { ctxb := context.Background() // Restore a legacy database. - tempDirName, err := ioutil.TempDir("", "clientstore") - require.NoError(t, err) - defer os.RemoveAll(tempDirName) + tempDirName := t.TempDir() tempPath := filepath.Join(tempDirName, dbFileName) db, err := bbolt.Open(tempPath, 0600, nil) diff --git a/loopdb/store.go b/loopdb/store.go index fe71c19cf..1fec57eb5 100644 --- a/loopdb/store.go +++ b/loopdb/store.go @@ -15,6 +15,7 @@ import ( "github.com/btcsuite/btcd/chaincfg/chainhash" "github.com/lightningnetwork/lnd/lntypes" "go.etcd.io/bbolt" + bbolterrors "go.etcd.io/bbolt/errors" ) var ( @@ -197,9 +198,9 @@ func NewBoltSwapStore(dbPath string, chainParams *chaincfg.Params) ( bdb, err := bboltOpen(path, 0600, &bbolt.Options{ Timeout: DefaultLoopDBTimeout, }) - if errors.Is(err, bbolt.ErrTimeout) { + if errors.Is(err, bbolterrors.ErrTimeout) { return nil, fmt.Errorf("%w: couldn't obtain exclusive lock on "+ - "%s, timed out after %v", bbolt.ErrTimeout, path, + "%s, timed out after %v", bbolterrors.ErrTimeout, path, DefaultLoopDBTimeout) } if err != nil { diff --git a/loopdb/store_test.go b/loopdb/store_test.go index 4a61062b2..ec9feb2a3 100644 --- a/loopdb/store_test.go +++ b/loopdb/store_test.go @@ -4,7 +4,6 @@ import ( "context" "crypto/sha256" "fmt" - "io/ioutil" "os" "path/filepath" "testing" @@ -18,6 +17,7 @@ import ( "github.com/lightningnetwork/lnd/routing/route" "github.com/stretchr/testify/require" "go.etcd.io/bbolt" + bbolterrors "go.etcd.io/bbolt/errors" ) var ( @@ -62,7 +62,7 @@ func TestNewBoltSwapStoreTimeout(t *testing.T) { bboltOpen = origOpen }) - wrappedErr := fmt.Errorf("wrapped: %w", bbolt.ErrTimeout) + wrappedErr := fmt.Errorf("wrapped: %w", bbolterrors.ErrTimeout) bboltOpen = func(path string, mode os.FileMode, options *bbolt.Options) (*bbolt.DB, error) { @@ -74,7 +74,7 @@ func TestNewBoltSwapStoreTimeout(t *testing.T) { store, err := NewBoltSwapStore(tempDir, &chaincfg.MainNetParams) require.Nil(t, store) - require.ErrorIs(t, err, bbolt.ErrTimeout) + require.ErrorIs(t, err, bbolterrors.ErrTimeout) require.ErrorContains(t, err, "couldn't obtain exclusive lock") } @@ -142,10 +142,7 @@ func TestLoopOutStore(t *testing.T) { // testLoopOutStore tests the basic functionality of the current bbolt // swap store for specific swap parameters. func testLoopOutStore(t *testing.T, pendingSwap *LoopOutContract) { - tempDirName, err := ioutil.TempDir("", "clientstore") - require.NoError(t, err) - - defer os.RemoveAll(tempDirName) + tempDirName := t.TempDir() store, err := NewBoltSwapStore(tempDirName, &chaincfg.MainNetParams) require.NoError(t, err) @@ -284,9 +281,7 @@ func TestLoopInStore(t *testing.T) { } func testLoopInStore(t *testing.T, pendingSwap LoopInContract) { - tempDirName, err := ioutil.TempDir("", "clientstore") - require.NoError(t, err) - defer os.RemoveAll(tempDirName) + tempDirName := t.TempDir() store, err := NewBoltSwapStore(tempDirName, &chaincfg.MainNetParams) require.NoError(t, err) @@ -366,11 +361,7 @@ func testLoopInStore(t *testing.T, pendingSwap LoopInContract) { // TestVersionNew tests that a new database is initialized with the current // version. func TestVersionNew(t *testing.T) { - tempDirName, err := ioutil.TempDir("", "clientstore") - if err != nil { - t.Fatal(err) - } - defer os.RemoveAll(tempDirName) + tempDirName := t.TempDir() store, err := NewBoltSwapStore(tempDirName, &chaincfg.MainNetParams) if err != nil { @@ -390,11 +381,7 @@ func TestVersionNew(t *testing.T) { // TestVersionMigrated tests that an existing version zero database is migrated // to the latest version. func TestVersionMigrated(t *testing.T) { - tempDirName, err := ioutil.TempDir("", "clientstore") - if err != nil { - t.Fatal(err) - } - defer os.RemoveAll(tempDirName) + tempDirName := t.TempDir() createVersionZeroDb(t, tempDirName) @@ -459,11 +446,7 @@ func TestLegacyOutgoingChannel(t *testing.T) { } // Restore a legacy database. - tempDirName, err := ioutil.TempDir("", "clientstore") - if err != nil { - t.Fatal(err) - } - defer os.RemoveAll(tempDirName) + tempDirName := t.TempDir() tempPath := filepath.Join(tempDirName, dbFileName) db, err := bbolt.Open(tempPath, 0600, nil) @@ -498,9 +481,7 @@ func TestLegacyOutgoingChannel(t *testing.T) { // TestLiquidityParams checks that reading and writing to liquidty bucket are // as expected. func TestLiquidityParams(t *testing.T) { - tempDirName, err := ioutil.TempDir("", "clientstore") - require.NoError(t, err, "failed to db") - defer os.RemoveAll(tempDirName) + tempDirName := t.TempDir() ctxb := context.Background() diff --git a/staticaddr/address/manager_test.go b/staticaddr/address/manager_test.go index 5881bf848..b7bbf79ae 100644 --- a/staticaddr/address/manager_test.go +++ b/staticaddr/address/manager_test.go @@ -66,6 +66,10 @@ func (m *mockStaticAddressClient) PushStaticAddressHtlcSigs(ctx context.Context, args.Error(1) } +// ServerWithdrawDeposits implements the deprecated RPC required by the +// generated client interface. Production code uses ServerPsbtWithdrawDeposits. +// +//nolint:staticcheck func (m *mockStaticAddressClient) ServerWithdrawDeposits(ctx context.Context, in *swapserverrpc.ServerWithdrawRequest, opts ...grpc.CallOption) (*swapserverrpc.ServerWithdrawResponse, diff --git a/staticaddr/deposit/manager_test.go b/staticaddr/deposit/manager_test.go index 0ab793021..1e798ea38 100644 --- a/staticaddr/deposit/manager_test.go +++ b/staticaddr/deposit/manager_test.go @@ -71,6 +71,10 @@ func (m *mockStaticAddressClient) PushStaticAddressHtlcSigs(ctx context.Context, args.Error(1) } +// ServerWithdrawDeposits implements the deprecated RPC required by the +// generated client interface. Production code uses ServerPsbtWithdrawDeposits. +// +//nolint:staticcheck func (m *mockStaticAddressClient) ServerWithdrawDeposits(ctx context.Context, in *swapserverrpc.ServerWithdrawRequest, opts ...grpc.CallOption) (*swapserverrpc.ServerWithdrawResponse, diff --git a/staticaddr/openchannel/manager.go b/staticaddr/openchannel/manager.go index 2274ce506..17f6c6a11 100644 --- a/staticaddr/openchannel/manager.go +++ b/staticaddr/openchannel/manager.go @@ -310,6 +310,8 @@ func (m *Manager) OpenChannel(ctx context.Context, return nil, err } + // If a local funding amount is set, coin-select deposits to + // cover it. Otherwise fundmax uses all available deposits. if req.LocalFundingAmount != 0 { deposits, err = staticutil.SelectDeposits( deposits, req.LocalFundingAmount, @@ -319,9 +321,6 @@ func (m *Manager) OpenChannel(ctx context.Context, return nil, fmt.Errorf("error selecting "+ "deposits: %w", err) } - } else { - // The fundmax flag is set, hence we select all deposits - // for funding the channel. } } @@ -369,6 +368,7 @@ func (m *Manager) OpenChannel(ctx context.Context, if err == nil { return chanOutpoint, nil } + err = maybeWrapTaprootUnsupportedError(reqClone, err) log.Infof("error opening channel: %v", err) @@ -773,6 +773,9 @@ func resolveCommitmentType(commitmentType lnrpc.CommitmentType) ( case lnrpc.CommitmentType_SIMPLE_TAPROOT: return lnrpc.CommitmentType_SIMPLE_TAPROOT, nil + case lnrpc.CommitmentType_TAPROOT: + return lnrpc.CommitmentType_TAPROOT, nil + default: return lnrpc.CommitmentType_UNKNOWN_COMMITMENT_TYPE, fmt.Errorf( "unsupported commitment type %v", commitmentType, @@ -780,6 +783,30 @@ func resolveCommitmentType(commitmentType lnrpc.CommitmentType) ( } } +// maybeWrapTaprootUnsupportedError turns lnd's generic unknown channel type +// error into a user-actionable message for Loop's production taproot channel +// type. +func maybeWrapTaprootUnsupportedError(req *lnrpc.OpenChannelRequest, + err error) error { + + if err == nil || req.CommitmentType != lnrpc.CommitmentType_TAPROOT { + return err + } + + errMsg := strings.ToLower(err.Error()) + switch { + case strings.Contains(errMsg, "unhandled request channel type"), + strings.Contains(errMsg, "unknown channel type"), + strings.Contains(errMsg, "unsupported channel type"): + + return fmt.Errorf("channel_type=taproot is not supported "+ + "by the connected lnd; update LND to v0.21.0-beta "+ + "or later to use this channel type: %w", err) + } + + return err +} + // checkPsbtFlags make sure a request to open a channel doesn't set any // parameters that are incompatible with the PSBT funding flow. func checkPsbtFlags(req *lnrpc.OpenChannelRequest) error { diff --git a/staticaddr/openchannel/manager_test.go b/staticaddr/openchannel/manager_test.go index f408da169..a7bbbfc02 100644 --- a/staticaddr/openchannel/manager_test.go +++ b/staticaddr/openchannel/manager_test.go @@ -573,6 +573,11 @@ func TestResolveCommitmentType(t *testing.T) { commitmentType: lnrpc.CommitmentType_SIMPLE_TAPROOT, expectedType: lnrpc.CommitmentType_SIMPLE_TAPROOT, }, + { + name: "production taproot supported", + commitmentType: lnrpc.CommitmentType_TAPROOT, + expectedType: lnrpc.CommitmentType_TAPROOT, + }, { name: "legacy rejected", commitmentType: lnrpc.CommitmentType_LEGACY, @@ -596,6 +601,33 @@ func TestResolveCommitmentType(t *testing.T) { } } +// TestMaybeWrapTaprootUnsupportedError verifies that generic old-lnd channel +// type rejections become actionable for Loop users selecting production +// taproot channels. +func TestMaybeWrapTaprootUnsupportedError(t *testing.T) { + t.Parallel() + + baseErr := errors.New("got error from server: rpc error: " + + "code = Unknown desc = unhandled request channel type 7") + req := &lnrpc.OpenChannelRequest{ + CommitmentType: lnrpc.CommitmentType_TAPROOT, + } + + err := maybeWrapTaprootUnsupportedError(req, baseErr) + require.ErrorContains( + t, err, "channel_type=taproot is not supported", + ) + require.ErrorContains( + t, err, "update LND to v0.21.0-beta or later", + ) + require.ErrorIs(t, err, baseErr) + + req.CommitmentType = lnrpc.CommitmentType_SIMPLE_TAPROOT + err = maybeWrapTaprootUnsupportedError(req, baseErr) + require.ErrorIs(t, err, baseErr) + require.NotContains(t, err.Error(), "update LND") +} + // --------------------------------------------------------------------------- // Mock types for PSBT channel open flow tests. // --------------------------------------------------------------------------- diff --git a/staticaddr/staticutil/utils.go b/staticaddr/staticutil/utils.go index daf97c467..a8a5e4041 100644 --- a/staticaddr/staticutil/utils.go +++ b/staticaddr/staticutil/utils.go @@ -232,8 +232,10 @@ func estimateFee(numInputs int, feeRate chainfee.SatPerKWeight, // Add the funding output based on commitment type. switch commitmentType { - case lnrpc.CommitmentType_SIMPLE_TAPROOT: + case lnrpc.CommitmentType_SIMPLE_TAPROOT, + lnrpc.CommitmentType_TAPROOT: we.AddP2TROutput() + default: we.AddP2WSHOutput() } diff --git a/staticaddr/staticutil/utils_test.go b/staticaddr/staticutil/utils_test.go index d0ab8b1fd..ae68b4895 100644 --- a/staticaddr/staticutil/utils_test.go +++ b/staticaddr/staticutil/utils_test.go @@ -271,9 +271,6 @@ func TestSelectDeposits(t *testing.T) { // High fee rate: 100 sat/vbyte = 25000 sat/kw. highFeeRate := chainfee.SatPerKVByte(100_000).FeePerKWeight() - anchors := lnrpc.CommitmentType_ANCHORS - taproot := lnrpc.CommitmentType_SIMPLE_TAPROOT - tests := []struct { name string deposits []*deposit.Deposit @@ -290,7 +287,7 @@ func TestSelectDeposits(t *testing.T) { deposits: makeDeposits(1_000, 2_000), amount: 1_000_000, feeRate: lowFeeRate, - commitmentType: anchors, + commitmentType: lnrpc.CommitmentType_ANCHORS, wantErr: "insufficient funds", }, { @@ -298,7 +295,7 @@ func TestSelectDeposits(t *testing.T) { deposits: makeDeposits(100_000), amount: 100_000, feeRate: lowFeeRate, - commitmentType: anchors, + commitmentType: lnrpc.CommitmentType_ANCHORS, wantErr: "insufficient funds", }, { @@ -310,7 +307,7 @@ func TestSelectDeposits(t *testing.T) { deposits: makeDeposits(51_000), amount: 50_000, feeRate: highFeeRate, - commitmentType: anchors, + commitmentType: lnrpc.CommitmentType_ANCHORS, wantErr: "insufficient funds", }, { @@ -327,7 +324,7 @@ func TestSelectDeposits(t *testing.T) { ), amount: 400_000, feeRate: highFeeRate, - commitmentType: anchors, + commitmentType: lnrpc.CommitmentType_ANCHORS, wantCount: 2, validate: func(t *testing.T, selected []*deposit.Deposit) { require.Equal( @@ -345,7 +342,7 @@ func TestSelectDeposits(t *testing.T) { deposits: makeDeposits(500_000), amount: 100_000, feeRate: lowFeeRate, - commitmentType: anchors, + commitmentType: lnrpc.CommitmentType_ANCHORS, wantCount: 1, }, { @@ -353,7 +350,7 @@ func TestSelectDeposits(t *testing.T) { deposits: makeDeposits(60_000, 60_000), amount: 100_000, feeRate: lowFeeRate, - commitmentType: anchors, + commitmentType: lnrpc.CommitmentType_ANCHORS, wantCount: 2, }, { @@ -361,7 +358,7 @@ func TestSelectDeposits(t *testing.T) { deposits: makeDeposits(10_000, 200_000, 50_000), amount: 100_000, feeRate: lowFeeRate, - commitmentType: anchors, + commitmentType: lnrpc.CommitmentType_ANCHORS, wantCount: 1, validate: func(t *testing.T, selected []*deposit.Deposit) { // Should pick the 200k deposit. @@ -388,13 +385,13 @@ func TestSelectDeposits(t *testing.T) { deposits: makeDeposits(35_500, 35_500, 10_000), amount: 50_000, feeRate: highFeeRate, - commitmentType: anchors, + commitmentType: lnrpc.CommitmentType_ANCHORS, wantCount: 3, validate: func(t *testing.T, selected []*deposit.Deposit) { total := depositSum(selected) fee := estimateFee( len(selected), highFeeRate, - anchors, + lnrpc.CommitmentType_ANCHORS, ) require.GreaterOrEqual( t, total, @@ -407,7 +404,7 @@ func TestSelectDeposits(t *testing.T) { deposits: makeDeposits(40_000, 40_000, 40_000), amount: 100_000, feeRate: lowFeeRate, - commitmentType: anchors, + commitmentType: lnrpc.CommitmentType_ANCHORS, wantCount: 3, }, { @@ -415,7 +412,7 @@ func TestSelectDeposits(t *testing.T) { deposits: makeDeposits(100_000, 50_000), amount: 99_000, feeRate: 0, - commitmentType: anchors, + commitmentType: lnrpc.CommitmentType_ANCHORS, wantCount: 1, validate: func(t *testing.T, selected []*deposit.Deposit) { // With zero fee, 100k covers 99k + 0 + dust. @@ -430,12 +427,12 @@ func TestSelectDeposits(t *testing.T) { deposits: makeDeposits(200_000, 100_000, 50_000), amount: 100_000, feeRate: highFeeRate, - commitmentType: anchors, + commitmentType: lnrpc.CommitmentType_ANCHORS, validate: func(t *testing.T, selected []*deposit.Deposit) { total := depositSum(selected) fee := estimateFee( len(selected), highFeeRate, - anchors, + lnrpc.CommitmentType_ANCHORS, ) require.GreaterOrEqual( t, total, @@ -448,7 +445,15 @@ func TestSelectDeposits(t *testing.T) { deposits: makeDeposits(500_000), amount: 100_000, feeRate: lowFeeRate, - commitmentType: taproot, + commitmentType: lnrpc.CommitmentType_SIMPLE_TAPROOT, + wantCount: 1, + }, + { + name: "production taproot commitment type", + deposits: makeDeposits(500_000), + amount: 100_000, + feeRate: lowFeeRate, + commitmentType: lnrpc.CommitmentType_TAPROOT, wantCount: 1, }, { @@ -459,12 +464,12 @@ func TestSelectDeposits(t *testing.T) { ), amount: 50_000, feeRate: lowFeeRate, - commitmentType: anchors, + commitmentType: lnrpc.CommitmentType_ANCHORS, validate: func(t *testing.T, selected []*deposit.Deposit) { total := depositSum(selected) fee := estimateFee( len(selected), lowFeeRate, - anchors, + lnrpc.CommitmentType_ANCHORS, ) require.GreaterOrEqual( t, total, @@ -484,12 +489,12 @@ func TestSelectDeposits(t *testing.T) { ), amount: 150_000, feeRate: lowFeeRate, - commitmentType: anchors, + commitmentType: lnrpc.CommitmentType_ANCHORS, validate: func(t *testing.T, selected []*deposit.Deposit) { total := depositSum(selected) fee := estimateFee( len(selected), lowFeeRate, - anchors, + lnrpc.CommitmentType_ANCHORS, ) // Core invariant: selected amount covers // requested amount + fee + dust. @@ -506,7 +511,7 @@ func TestSelectDeposits(t *testing.T) { deposits: makeDeposits(10_000, 20_000, 300_000), amount: 100_000, feeRate: lowFeeRate, - commitmentType: anchors, + commitmentType: lnrpc.CommitmentType_ANCHORS, wantCount: 1, validate: func(t *testing.T, selected []*deposit.Deposit) { require.Equal( @@ -525,7 +530,7 @@ func TestSelectDeposits(t *testing.T) { deposits: makeDeposits(60_000, 60_000), amount: 50_000, feeRate: highFeeRate, - commitmentType: anchors, + commitmentType: lnrpc.CommitmentType_ANCHORS, wantCount: 2, }, } diff --git a/staticaddr/withdraw/funding_values_test.go b/staticaddr/withdraw/funding_values_test.go index 97e5fd149..67c16a1a4 100644 --- a/staticaddr/withdraw/funding_values_test.go +++ b/staticaddr/withdraw/funding_values_test.go @@ -235,6 +235,11 @@ func TestCalculateWithdrawalTxValuesCommitmentTypeParity(t *testing.T) { commitmentType: lnrpc.CommitmentType_SIMPLE_TAPROOT, addr: taprootAddr, }, + { + name: "production taproot and p2tr", + commitmentType: lnrpc.CommitmentType_TAPROOT, + addr: taprootAddr, + }, } selectedAmounts := []btcutil.Amount{ diff --git a/staticaddr/withdraw/manager.go b/staticaddr/withdraw/manager.go index 6cd941496..719676237 100644 --- a/staticaddr/withdraw/manager.go +++ b/staticaddr/withdraw/manager.go @@ -1108,7 +1108,8 @@ func WithdrawalTxWeight(numInputs int, sweepAddress btcutil.Address, if commitmentType != lnrpc.CommitmentType_UNKNOWN_COMMITMENT_TYPE { switch commitmentType { - case lnrpc.CommitmentType_SIMPLE_TAPROOT: + case lnrpc.CommitmentType_SIMPLE_TAPROOT, + lnrpc.CommitmentType_TAPROOT: weightEstimator.AddP2TROutput() default: diff --git a/test/lightning_client_mock.go b/test/lightning_client_mock.go index 9a9b9047a..27f3fada3 100644 --- a/test/lightning_client_mock.go +++ b/test/lightning_client_mock.go @@ -266,6 +266,11 @@ func (h *mockLightningClient) ListPayments(_ context.Context, req lndclient.ListPaymentsRequest) (*lndclient.ListPaymentsResponse, error) { + h.lnd.lock.Lock() + defer h.lnd.lock.Unlock() + + h.lnd.ListPaymentsRequests = append(h.lnd.ListPaymentsRequests, req) + if req.Offset >= uint64(len(h.lnd.Payments)) { return &lndclient.ListPaymentsResponse{}, nil } @@ -273,7 +278,8 @@ func (h *mockLightningClient) ListPayments(_ context.Context, lastIndexOffset := req.Offset + req.MaxPayments lastIndexOffset = min(lastIndexOffset, uint64(len(h.lnd.Payments))) - result := h.lnd.Payments[req.Offset:lastIndexOffset] + result := make([]lndclient.Payment, lastIndexOffset-req.Offset) + copy(result, h.lnd.Payments[req.Offset:lastIndexOffset]) return &lndclient.ListPaymentsResponse{ Payments: result, diff --git a/test/lnd_services_mock.go b/test/lnd_services_mock.go index 164bea181..63aa34c26 100644 --- a/test/lnd_services_mock.go +++ b/test/lnd_services_mock.go @@ -162,12 +162,13 @@ type LndMockServices struct { // keyed by hash string. Invoices map[lntypes.Hash]*lndclient.Invoice - Channels []lndclient.ChannelInfo - ChannelEdges map[uint64]*lndclient.ChannelEdge - ClosedChannels []lndclient.ClosedChannel - ForwardingEvents []lndclient.ForwardingEvent - Payments []lndclient.Payment - MissionControlState []lndclient.MissionControlEntry + Channels []lndclient.ChannelInfo + ChannelEdges map[uint64]*lndclient.ChannelEdge + ClosedChannels []lndclient.ClosedChannel + ForwardingEvents []lndclient.ForwardingEvent + Payments []lndclient.Payment + ListPaymentsRequests []lndclient.ListPaymentsRequest + MissionControlState []lndclient.MissionControlEntry WaitForFinished func() @@ -185,6 +186,20 @@ func (s *LndMockServices) EpochSubscribers() int32 { return int32(len(s.blockHeightListeners)) } +// ListPaymentsRequestsSnapshot returns a copy of all ListPayments requests +// recorded by the mock. +func (s *LndMockServices) ListPaymentsRequestsSnapshot() []lndclient.ListPaymentsRequest { + s.lock.Lock() + defer s.lock.Unlock() + + requests := make( + []lndclient.ListPaymentsRequest, len(s.ListPaymentsRequests), + ) + copy(requests, s.ListPaymentsRequests) + + return requests +} + // NotifyHeight notifies a new block height. func (s *LndMockServices) NotifyHeight(height int32) error { s.lock.Lock()