From 682d45405c7dd600597a4ce7e1741746451351cb Mon Sep 17 00:00:00 2001 From: Boris Nagaev Date: Sat, 20 Jun 2026 18:33:48 -0500 Subject: [PATCH] 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 48a85f56..d65056cd 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 3861060d..56b535a1 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 00000000..2eb70154 --- /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 00000000..3315aa85 --- /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 aae2c6ac..ba1ca3e1 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 28bf8c0c..2f48e947 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 052c5aca..17f6c6a1 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 2ddc3cc2..a7bbbfc0 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. // ---------------------------------------------------------------------------