From a92f6bc875cda91d42dd5cb8be8d8df066e09fc1 Mon Sep 17 00:00:00 2001 From: Boris Nagaev Date: Sat, 20 Jun 2026 16:23:57 -0500 Subject: [PATCH 1/5] build: bump lndclient to v0.21.0-2 Use the tagged lndclient release that exposes the ListPayments request fields needed by Loop optimizations. --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) 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= From 09c92527ea50b57bd472a34eae7fd65f00128d59 Mon Sep 17 00:00:00 2001 From: Boris Nagaev Date: Thu, 11 Jun 2026 14:22:43 -0500 Subject: [PATCH 2/5] staticaddr: accept production taproot channels LND v0.21 exposes CommitmentType_TAPROOT as the production taproot channel commitment type, while SIMPLE_TAPROOT remains a legacy taproot enum. Static address channel opens previously rejected TAPROOT and only classified SIMPLE_TAPROOT as a taproot output for fee and weight estimates. Accept TAPROOT in the static address open-channel validator and keep accepting SIMPLE_TAPROOT for compatibility. Treat both taproot commitment enums as P2TR outputs for deposit-selection and withdrawal fee estimates. Callers using the production enum then get the same weight accounting as the legacy taproot enum. This does not change the CLI mapping for user-facing channel_type=taproot. It only makes the static address path compatible with callers that already send LND production taproot commitment type. --- staticaddr/openchannel/manager.go | 3 ++ staticaddr/openchannel/manager_test.go | 5 +++ staticaddr/staticutil/utils.go | 4 +- staticaddr/staticutil/utils_test.go | 51 ++++++++++++---------- staticaddr/withdraw/funding_values_test.go | 5 +++ staticaddr/withdraw/manager.go | 3 +- 6 files changed, 46 insertions(+), 25 deletions(-) diff --git a/staticaddr/openchannel/manager.go b/staticaddr/openchannel/manager.go index 2274ce506..21102e694 100644 --- a/staticaddr/openchannel/manager.go +++ b/staticaddr/openchannel/manager.go @@ -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, diff --git a/staticaddr/openchannel/manager_test.go b/staticaddr/openchannel/manager_test.go index f408da169..2ddc3cc25 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, 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: From d324b4bfd89d1f6978bc007cf884050e49617abf Mon Sep 17 00:00:00 2001 From: Boris Nagaev Date: Sat, 20 Jun 2026 16:24:35 -0500 Subject: [PATCH 3/5] loop: omit payment hops in cost migration The cost cleanup migration pages through LND payments only to build a payment-hash to fee map. It does not inspect HTLC attempts, routes, or per-hop data; pagination still uses the top-level index offsets returned by ListPayments. Setting OmitHops is safe for this migration because LND only strips hop-level route data from HTLC attempts, while preserving the top-level payment fields the migration reads: hash, fee, and response offsets. This reduces response size and query cost for nodes with many or large MPP payments without changing the calculated swap costs. The migration test records the mocked ListPayments requests and asserts that OmitHops is set. --- cost_migration.go | 1 + cost_migration_test.go | 5 +++++ test/lightning_client_mock.go | 8 +++++++- test/lnd_services_mock.go | 27 +++++++++++++++++++++------ 4 files changed, 34 insertions(+), 7 deletions(-) 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/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() From 5d8a5019cf75bcc3573afd8033225f2afc067033 Mon Sep 17 00:00:00 2001 From: Boris Nagaev Date: Sat, 20 Jun 2026 17:55:35 -0500 Subject: [PATCH 4/5] lint: enable deprecation checks Enable staticcheck's SA1019 check in golangci-lint so deprecated identifiers are caught in CI. Replace deprecated standard library and bbolt APIs with their current equivalents. Keep intentional compatibility reads and writes of deprecated Loop RPC fields behind narrow nolint annotations, because older clients and persisted liquidity parameters still depend on those fields. --- .golangci.yml | 8 ++++--- cmd/loop/loopin.go | 2 +- cmd/loop/main.go | 3 +-- liquidity/parameters.go | 5 +++- loopd/daemon.go | 8 +++---- loopd/swapclient_server.go | 7 +++--- loopdb/migration_04_updates_test.go | 6 +---- loopdb/store.go | 5 ++-- loopdb/store_test.go | 37 +++++++---------------------- staticaddr/address/manager_test.go | 4 ++++ staticaddr/deposit/manager_test.go | 4 ++++ staticaddr/openchannel/manager.go | 5 ++-- 12 files changed, 42 insertions(+), 52 deletions(-) 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/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 21102e694..052c5aca2 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. } } From 682d45405c7dd600597a4ce7e1741746451351cb Mon Sep 17 00:00:00 2001 From: Boris Nagaev Date: Sat, 20 Jun 2026 18:33:48 -0500 Subject: [PATCH 5/5] cmd/loop: expose both taproot channel types LND v0.21 added the production TAPROOT commitment type while SIMPLE_TAPROOT remains available as the legacy enum. The static open-channel CLI previously used "taproot" for SIMPLE_TAPROOT. Keep both choices available by renaming that legacy spelling to "simple-taproot" and mapping "taproot" to TAPROOT. This makes the CLI spelling match the channel type it requests while still leaving an explicit path for users that need SIMPLE_TAPROOT. --- cmd/loop/openchannel.go | 17 +-- .../01_loop-static-openchannel-help.json | 2 +- ...op-static-openchannel-taproot-success.json | 104 +++++++++++++++++ ...c-openchannel-taproot-unsupported-lnd.json | 106 ++++++++++++++++++ docs/loop.1 | 2 +- docs/loop.md | 2 +- staticaddr/openchannel/manager.go | 25 +++++ staticaddr/openchannel/manager_test.go | 27 +++++ 8 files changed, 275 insertions(+), 10 deletions(-) create mode 100644 cmd/loop/testdata/sessions/static-openchannel/07_loop-static-openchannel-taproot-success.json create mode 100644 cmd/loop/testdata/sessions/static-openchannel/08_loop-static-openchannel-taproot-unsupported-lnd.json 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/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/staticaddr/openchannel/manager.go b/staticaddr/openchannel/manager.go index 052c5aca2..17f6c6a11 100644 --- a/staticaddr/openchannel/manager.go +++ b/staticaddr/openchannel/manager.go @@ -368,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) @@ -782,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 2ddc3cc25..a7bbbfc02 100644 --- a/staticaddr/openchannel/manager_test.go +++ b/staticaddr/openchannel/manager_test.go @@ -601,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. // ---------------------------------------------------------------------------