• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

lightningnetwork / lnd / 17774622768

16 Sep 2025 05:57PM UTC coverage: 66.505% (-0.2%) from 66.657%
17774622768

Pull #10067

github

web-flow
Merge 4ec7abb62 into cbed86e21
Pull Request #10067: add sats_per_kweight option when crafting a transaction (continue)

72 of 233 new or added lines in 13 files covered. (30.9%)

355 existing lines in 32 files now uncovered.

136113 of 204666 relevant lines covered (66.5%)

21396.84 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

0.0
/cmd/commands/cmd_open_channel.go
1
package commands
2

3
import (
4
        "bytes"
5
        "context"
6
        "crypto/rand"
7
        "encoding/base64"
8
        "encoding/hex"
9
        "encoding/json"
10
        "fmt"
11
        "io"
12
        "os"
13
        "strconv"
14
        "strings"
15

16
        "github.com/btcsuite/btcd/btcutil"
17
        "github.com/btcsuite/btcd/chaincfg/chainhash"
18
        "github.com/btcsuite/btcd/wire"
19
        "github.com/lightningnetwork/lnd/lnrpc"
20
        "github.com/lightningnetwork/lnd/lnwallet/chanfunding"
21
        "github.com/urfave/cli"
22
)
23

24
const (
25
        userMsgFund = `PSBT funding initiated with peer %x.
26
Please create a PSBT that sends %v (%d satoshi) to the funding address %s.
27

28
Note: The whole process should be completed within 10 minutes, otherwise there
29
is a risk of the remote node timing out and canceling the funding process.
30

31
Example with bitcoind:
32
        bitcoin-cli walletcreatefundedpsbt [] '[{"%s":%.8f}]'
33

34
If you are using a wallet that can fund a PSBT directly (currently not possible
35
with bitcoind), you can use this PSBT that contains the same address and amount:
36
%s
37

38
!!! WARNING !!!
39
DO NOT PUBLISH the finished transaction by yourself or with another tool.
40
lnd MUST publish it in the proper funding flow order OR THE FUNDS CAN BE LOST!
41

42
Paste the funded PSBT here to continue the funding flow.
43
If your PSBT is very long (specifically, more than 4096 characters), please save
44
it to a file and paste the full file path here instead as some terminals will
45
truncate the pasted text if it's too long.
46
Base64 encoded PSBT (or path to file): `
47

48
        userMsgSign = `
49
PSBT verified by lnd, please continue the funding flow by signing the PSBT by
50
all required parties/devices. Once the transaction is fully signed, paste it
51
again here either in base64 PSBT or hex encoded raw wire TX format.
52

53
Signed base64 encoded PSBT or hex encoded raw wire TX (or path to file): `
54

55
        // psbtMaxFileSize is the maximum file size we allow a PSBT file to be
56
        // in case we want to read a PSBT from a file. This is mainly to protect
57
        // the user from choosing a large file by accident and running into out
58
        // of memory issues or other weird errors.
59
        psbtMaxFileSize = 1024 * 1024
60

61
        channelTypeTweakless     = "tweakless"
62
        channelTypeAnchors       = "anchors"
63
        channelTypeSimpleTaproot = "taproot"
64
)
65

66
// TODO(roasbeef): change default number of confirmations.
67
var openChannelCommand = cli.Command{
68
        Name:     "openchannel",
69
        Category: "Channels",
70
        Usage:    "Open a channel to a node or an existing peer.",
71
        Description: `
72
        Attempt to open a new channel to an existing peer with the key node-key
73
        optionally blocking until the channel is 'open'.
74

75
        One can also connect to a node before opening a new channel to it by
76
        setting its host:port via the --connect argument. For this to work,
77
        the node_key must be provided, rather than the peer_id. This is
78
        optional.
79

80
        The channel will be initialized with local-amt satoshis locally and
81
        push-amt satoshis for the remote node. Note that the push-amt is
82
        deducted from the specified local-amt which implies that the local-amt
83
        must be greater than the push-amt. Also note that specifying push-amt
84
        means you give that amount to the remote node as part of the channel
85
        opening. Once the channel is open, a channelPoint (txid:vout) of the
86
        funding output is returned.
87

88
        If the remote peer supports the option upfront shutdown feature bit
89
        (query listpeers to see their supported feature bits), an address to
90
        enforce payout of funds on cooperative close can optionally be provided.
91
        Note that if you set this value, you will not be able to cooperatively
92
        close out to another address.
93

94
        One can manually set the fee to be used for the funding transaction via
95
        either the --conf_target or --sat_per_vbyte arguments. This is
96
        optional.
97

98
        One can also specify a short string memo to record some useful
99
        information about the channel using the --memo argument. This is stored
100
        locally only, and is purely for reference. It has no bearing on the
101
        channel's operation. Max allowed length is 500 characters.`,
102
        ArgsUsage: "node-key local-amt push-amt",
103
        Flags: []cli.Flag{
104
                cli.StringFlag{
105
                        Name: "node_key",
106
                        Usage: "the identity public key of the target " +
107
                                "node/peer serialized in compressed format",
108
                },
109
                cli.StringFlag{
110
                        Name:  "connect",
111
                        Usage: "(optional) the host:port of the target node",
112
                },
113
                cli.IntFlag{
114
                        Name: "local_amt",
115
                        Usage: "the number of satoshis the wallet should " +
116
                                "commit to the channel",
117
                },
118
                cli.BoolFlag{
119
                        Name: "fundmax",
120
                        Usage: "if set, the wallet will attempt to commit " +
121
                                "the maximum possible local amount to the " +
122
                                "channel. This must not be set at the same " +
123
                                "time as local_amt",
124
                },
125
                cli.StringSliceFlag{
126
                        Name: "utxo",
127
                        Usage: "a utxo specified as outpoint(tx:idx) which " +
128
                                "will be used to fund a channel. This flag " +
129
                                "can be repeatedly used to fund a channel " +
130
                                "with a selection of utxos. The selected " +
131
                                "funds can either be entirely spent by " +
132
                                "specifying the fundmax flag or partially by " +
133
                                "selecting a fraction of the sum of the " +
134
                                "outpoints in local_amt",
135
                },
136
                cli.Uint64Flag{
137
                        Name: "base_fee_msat",
138
                        Usage: "the base fee in milli-satoshis that will " +
139
                                "be charged for each forwarded HTLC, " +
140
                                "regardless of payment size",
141
                },
142
                cli.Uint64Flag{
143
                        Name: "fee_rate_ppm",
144
                        Usage: "the fee rate ppm (parts per million) that " +
145
                                "will be charged proportionally based on the " +
146
                                "value of each forwarded HTLC, the lowest " +
147
                                "possible rate is 0 with a granularity of " +
148
                                "0.000001 (millionths)",
149
                },
150
                cli.IntFlag{
151
                        Name: "push_amt",
152
                        Usage: "the number of satoshis to give the remote " +
153
                                "side as part of the initial commitment " +
154
                                "state, this is equivalent to first opening " +
155
                                "a channel and sending the remote party " +
156
                                "funds, but done all in one step",
157
                },
158
                cli.BoolFlag{
159
                        Name:  "block",
160
                        Usage: "block and wait until the channel is fully open",
161
                },
162
                cli.Int64Flag{
163
                        Name: "conf_target",
164
                        Usage: "(optional) the number of blocks that the " +
165
                                "transaction *should* confirm in, will be " +
166
                                "used for fee estimation",
167
                },
168
                cli.Int64Flag{
169
                        Name:   "sat_per_byte",
170
                        Usage:  "Deprecated, use sat_per_vbyte instead.",
171
                        Hidden: true,
172
                },
173
                cli.Float64Flag{
174
                        Name: "sat_per_vbyte",
175
                        Usage: "(optional) a manual fee expressed in " +
176
                                "sat/vbyte that should be used when crafting " +
177
                                "the transaction",
178
                },
179
                cli.BoolFlag{
180
                        Name: "private",
181
                        Usage: "make the channel private, such that it won't " +
182
                                "be announced to the greater network, and " +
183
                                "nodes other than the two channel endpoints " +
184
                                "must be explicitly told about it to be able " +
185
                                "to route through it",
186
                },
187
                cli.Int64Flag{
188
                        Name: "min_htlc_msat",
189
                        Usage: "(optional) the minimum value we will require " +
190
                                "for incoming HTLCs on the channel",
191
                },
192
                cli.Uint64Flag{
193
                        Name: "remote_csv_delay",
194
                        Usage: "(optional) the number of blocks we will " +
195
                                "require our channel counterparty to wait " +
196
                                "before accessing its funds in case of " +
197
                                "unilateral close. If this is not set, we " +
198
                                "will scale the value according to the " +
199
                                "channel size",
200
                },
201
                cli.Uint64Flag{
202
                        Name: "max_local_csv",
203
                        Usage: "(optional) the maximum number of blocks that " +
204
                                "we will allow the remote peer to require we " +
205
                                "wait before accessing our funds in the case " +
206
                                "of a unilateral close.",
207
                },
208
                cli.Uint64Flag{
209
                        Name: "min_confs",
210
                        Usage: "(optional) the minimum number of " +
211
                                "confirmations each one of your outputs used " +
212
                                "for the funding transaction must satisfy",
213
                        Value: defaultUtxoMinConf,
214
                },
215
                cli.StringFlag{
216
                        Name: "close_address",
217
                        Usage: "(optional) an address to enforce payout of " +
218
                                "our funds to on cooperative close. Note " +
219
                                "that if this value is set on channel open, " +
220
                                "you will *not* be able to cooperatively " +
221
                                "close to a different address.",
222
                },
223
                cli.BoolFlag{
224
                        Name: "psbt",
225
                        Usage: "start an interactive mode that initiates " +
226
                                "funding through a partially signed bitcoin " +
227
                                "transaction (PSBT), allowing the channel " +
228
                                "funds to be added and signed from a " +
229
                                "hardware or other offline device.",
230
                },
231
                cli.StringFlag{
232
                        Name: "base_psbt",
233
                        Usage: "when using the interactive PSBT mode to open " +
234
                                "a new channel, use this base64 encoded PSBT " +
235
                                "as a base and add the new channel output to " +
236
                                "it instead of creating a new, empty one.",
237
                },
238
                cli.BoolFlag{
239
                        Name: "no_publish",
240
                        Usage: "when using the interactive PSBT mode to open " +
241
                                "multiple channels in a batch, this flag " +
242
                                "instructs lnd to not publish the full batch " +
243
                                "transaction just yet. For safety reasons " +
244
                                "this flag should be set for each of the " +
245
                                "batch's transactions except the very last",
246
                },
247
                cli.Uint64Flag{
248
                        Name: "remote_max_value_in_flight_msat",
249
                        Usage: "(optional) the maximum value in msat that " +
250
                                "can be pending within the channel at any " +
251
                                "given time",
252
                },
253
                cli.StringFlag{
254
                        Name: "channel_type",
255
                        Usage: fmt.Sprintf("(optional) the type of channel to "+
256
                                "propose to the remote peer (%q, %q, %q)",
257
                                channelTypeTweakless, channelTypeAnchors,
258
                                channelTypeSimpleTaproot),
259
                },
260
                cli.BoolFlag{
261
                        Name: "zero_conf",
262
                        Usage: "(optional) whether a zero-conf channel open " +
263
                                "should be attempted.",
264
                },
265
                cli.BoolFlag{
266
                        Name: "scid_alias",
267
                        Usage: "(optional) whether a scid-alias channel type" +
268
                                " should be negotiated.",
269
                },
270
                cli.Uint64Flag{
271
                        Name: "remote_reserve_sats",
272
                        Usage: "(optional) the minimum number of satoshis we " +
273
                                "require the remote node to keep as a direct " +
274
                                "payment. If not specified, a default of 1% " +
275
                                "of the channel capacity will be used.",
276
                },
277
                cli.StringFlag{
278
                        Name: "memo",
279
                        Usage: `(optional) a note-to-self containing some useful
280
                                information about the channel. This is stored
281
                                locally only, and is purely for reference. It
282
                                has no bearing on the channel's operation. Max
283
                                allowed length is 500 characters`,
284
                },
285
        },
286
        Action: actionDecorator(openChannel),
287
}
288

289
func openChannel(ctx *cli.Context) error {
×
290
        // TODO(roasbeef): add deadline to context
×
291
        ctxc := getContext()
×
292
        client, cleanUp := getClient(ctx)
×
293
        defer cleanUp()
×
294

×
295
        args := ctx.Args()
×
296
        var err error
×
297

×
298
        // Show command help if no arguments provided
×
299
        if ctx.NArg() == 0 && ctx.NumFlags() == 0 {
×
300
                _ = cli.ShowCommandHelp(ctx, "openchannel")
×
301
                return nil
×
302
        }
×
303

304
        // Check that only the field sat_per_vbyte or the deprecated field
305
        // sat_per_byte is used.
NEW
306
        _, err = checkNotBothSet(
×
307
                ctx, "sat_per_vbyte", "sat_per_byte",
×
308
        )
×
309
        if err != nil {
×
310
                return err
×
311
        }
×
312

313
        // Parse fee rate from --sat_per_vbyte and convert to sat/kw.
NEW
314
        satPerKw, err := parseFeeRate(ctx, "sat_per_vbyte")
×
NEW
315
        if err != nil {
×
NEW
316
                return err
×
NEW
317
        }
×
318

319
        minConfs := int32(ctx.Uint64("min_confs"))
×
320
        req := &lnrpc.OpenChannelRequest{
×
321
                TargetConf:                 int32(ctx.Int64("conf_target")),
×
322
                MinHtlcMsat:                ctx.Int64("min_htlc_msat"),
×
323
                RemoteCsvDelay:             uint32(ctx.Uint64("remote_csv_delay")),
×
324
                MinConfs:                   minConfs,
×
325
                SpendUnconfirmed:           minConfs == 0,
×
326
                CloseAddress:               ctx.String("close_address"),
×
327
                RemoteMaxValueInFlightMsat: ctx.Uint64("remote_max_value_in_flight_msat"),
×
328
                MaxLocalCsv:                uint32(ctx.Uint64("max_local_csv")),
×
329
                ZeroConf:                   ctx.Bool("zero_conf"),
×
330
                ScidAlias:                  ctx.Bool("scid_alias"),
×
331
                RemoteChanReserveSat:       ctx.Uint64("remote_reserve_sats"),
×
332
                FundMax:                    ctx.Bool("fundmax"),
×
333
                Memo:                       ctx.String("memo"),
×
NEW
334
                SatPerKw:                   satPerKw,
×
335
        }
×
336

×
337
        switch {
×
338
        case ctx.IsSet("node_key"):
×
339
                nodePubHex, err := hex.DecodeString(ctx.String("node_key"))
×
340
                if err != nil {
×
341
                        return fmt.Errorf("unable to decode node public key: "+
×
342
                                "%v", err)
×
343
                }
×
344
                req.NodePubkey = nodePubHex
×
345

346
        case args.Present():
×
347
                nodePubHex, err := hex.DecodeString(args.First())
×
348
                if err != nil {
×
349
                        return fmt.Errorf("unable to decode node public key: "+
×
350
                                "%v", err)
×
351
                }
×
352
                args = args.Tail()
×
353
                req.NodePubkey = nodePubHex
×
354
        default:
×
355
                return fmt.Errorf("node id argument missing")
×
356
        }
357

358
        // As soon as we can confirm that the node's node_key was set, rather
359
        // than the peer_id, we can check if the host:port was also set to
360
        // connect to it before opening the channel.
361
        if req.NodePubkey != nil && ctx.IsSet("connect") {
×
362
                addr := &lnrpc.LightningAddress{
×
363
                        Pubkey: hex.EncodeToString(req.NodePubkey),
×
364
                        Host:   ctx.String("connect"),
×
365
                }
×
366

×
367
                req := &lnrpc.ConnectPeerRequest{
×
368
                        Addr: addr,
×
369
                        Perm: false,
×
370
                }
×
371

×
372
                // Check if connecting to the node was successful.
×
373
                // We discard the peer id returned as it is not needed.
×
374
                _, err := client.ConnectPeer(ctxc, req)
×
375
                if err != nil &&
×
376
                        !strings.Contains(err.Error(), "already connected") {
×
377

×
378
                        return err
×
379
                }
×
380
        }
381

382
        switch {
×
383
        case ctx.IsSet("local_amt"):
×
384
                req.LocalFundingAmount = int64(ctx.Int("local_amt"))
×
385
        case args.Present():
×
386
                req.LocalFundingAmount, err = strconv.ParseInt(
×
387
                        args.First(), 10, 64,
×
388
                )
×
389
                if err != nil {
×
390
                        return fmt.Errorf("unable to decode local amt: %w", err)
×
391
                }
×
392
                args = args.Tail()
×
393
        case !ctx.Bool("fundmax"):
×
394
                return fmt.Errorf("either local_amt or fundmax must be " +
×
395
                        "specified")
×
396
        }
397

398
        // The fundmax flag is NOT allowed to be combined with local_amt above.
399
        // It is allowed to be combined with push_amt, but only if explicitly
400
        // set.
401
        if ctx.Bool("fundmax") && req.LocalFundingAmount != 0 {
×
402
                return fmt.Errorf("local amount cannot be set if attempting " +
×
403
                        "to commit the maximum amount out of the wallet")
×
404
        }
×
405

406
        // The fundmax flag is NOT allowed to be combined with the psbt flag.
407
        if ctx.Bool("fundmax") && ctx.Bool("psbt") {
×
408
                return fmt.Errorf("psbt cannot be set if attempting " +
×
409
                        "to commit the maximum amount out of the wallet")
×
410
        }
×
411

412
        if ctx.IsSet("utxo") {
×
413
                utxos := ctx.StringSlice("utxo")
×
414

×
415
                outpoints, err := UtxosToOutpoints(utxos)
×
416
                if err != nil {
×
417
                        return fmt.Errorf("unable to decode utxos: %w", err)
×
418
                }
×
419

420
                req.Outpoints = outpoints
×
421
        }
422

423
        if ctx.IsSet("push_amt") {
×
424
                req.PushSat = int64(ctx.Int("push_amt"))
×
425
        } else if args.Present() {
×
426
                req.PushSat, err = strconv.ParseInt(args.First(), 10, 64)
×
427
                if err != nil {
×
428
                        return fmt.Errorf("unable to decode push amt: %w", err)
×
429
                }
×
430
        }
431

432
        if ctx.IsSet("base_fee_msat") {
×
433
                req.BaseFee = ctx.Uint64("base_fee_msat")
×
434
                req.UseBaseFee = true
×
435
        }
×
436

437
        if ctx.IsSet("fee_rate_ppm") {
×
438
                req.FeeRate = ctx.Uint64("fee_rate_ppm")
×
439
                req.UseFeeRate = true
×
440
        }
×
441

442
        req.Private = ctx.Bool("private")
×
443

×
444
        // Parse the channel type and map it to its RPC representation.
×
445
        channelType := ctx.String("channel_type")
×
446
        switch channelType {
×
447
        case "":
×
448
                break
×
449
        case channelTypeTweakless:
×
450
                req.CommitmentType = lnrpc.CommitmentType_STATIC_REMOTE_KEY
×
451
        case channelTypeAnchors:
×
452
                req.CommitmentType = lnrpc.CommitmentType_ANCHORS
×
453
        case channelTypeSimpleTaproot:
×
454
                req.CommitmentType = lnrpc.CommitmentType_SIMPLE_TAPROOT
×
455
        default:
×
456
                return fmt.Errorf("unsupported channel type %v", channelType)
×
457
        }
458

459
        // PSBT funding is a more involved, interactive process that is too
460
        // large to also fit into this already long function.
461
        if ctx.Bool("psbt") {
×
462
                return openChannelPsbt(ctxc, ctx, client, req)
×
463
        }
×
464
        if !ctx.Bool("psbt") && ctx.Bool("no_publish") {
×
465
                return fmt.Errorf("the --no_publish flag can only be used in " +
×
466
                        "combination with the --psbt flag")
×
467
        }
×
468

469
        stream, err := client.OpenChannel(ctxc, req)
×
470
        if err != nil {
×
471
                return err
×
472
        }
×
473

474
        for {
×
475
                resp, err := stream.Recv()
×
476
                if err == io.EOF {
×
477
                        return nil
×
478
                } else if err != nil {
×
479
                        return err
×
480
                }
×
481

482
                switch update := resp.Update.(type) {
×
483
                case *lnrpc.OpenStatusUpdate_ChanPending:
×
484
                        err := printChanPending(update)
×
485
                        if err != nil {
×
486
                                return err
×
487
                        }
×
488

489
                        if !ctx.Bool("block") {
×
490
                                return nil
×
491
                        }
×
492

493
                case *lnrpc.OpenStatusUpdate_ChanOpen:
×
494
                        return printChanOpen(update)
×
495
                }
496
        }
497
}
498

499
// openChannelPsbt starts an interactive channel open protocol that uses a
500
// partially signed bitcoin transaction (PSBT) to fund the channel output. The
501
// protocol involves several steps between the RPC server and the CLI client:
502
//
503
// RPC server                           CLI client
504
//
505
//        |                                    |
506
//        |  |<------open channel (stream)-----|
507
//        |  |-------ready for funding----->|  |
508
//        |  |<------PSBT verify------------|  |
509
//        |  |-------ready for signing----->|  |
510
//        |  |<------PSBT finalize----------|  |
511
//        |  |-------channel pending------->|  |
512
//        |  |-------channel open------------->|
513
//        |                                    |
514
func openChannelPsbt(rpcCtx context.Context, ctx *cli.Context,
515
        client lnrpc.LightningClient,
516
        req *lnrpc.OpenChannelRequest) error {
×
517

×
518
        var (
×
519
                pendingChanID [32]byte
×
520
                shimPending   = true
×
521
                basePsbtBytes []byte
×
522
                quit          = make(chan struct{})
×
523
                srvMsg        = make(chan *lnrpc.OpenStatusUpdate, 1)
×
524
                srvErr        = make(chan error, 1)
×
525
                ctxc, cancel  = context.WithCancel(rpcCtx)
×
526
        )
×
527
        defer cancel()
×
528

×
529
        // Make sure the user didn't supply any command line flags that are
×
530
        // incompatible with PSBT funding.
×
531
        err := checkPsbtFlags(req)
×
532
        if err != nil {
×
533
                return err
×
534
        }
×
535

536
        // If the user supplied a base PSBT, only make sure it's valid base64.
537
        // The RPC server will make sure it's also a valid PSBT.
538
        basePsbt := ctx.String("base_psbt")
×
539
        if basePsbt != "" {
×
540
                basePsbtBytes, err = base64.StdEncoding.DecodeString(basePsbt)
×
541
                if err != nil {
×
542
                        return fmt.Errorf("error parsing base PSBT: %w", err)
×
543
                }
×
544
        }
545

546
        // Generate a new, random pending channel ID that we'll use as the main
547
        // identifier when sending update messages to the RPC server.
548
        if _, err := rand.Read(pendingChanID[:]); err != nil {
×
549
                return fmt.Errorf("unable to generate random chan ID: %w", err)
×
550
        }
×
551
        fmt.Printf("Starting PSBT funding flow with pending channel ID %x.\n",
×
552
                pendingChanID)
×
553

×
554
        // maybeCancelShim is a helper function that cancels the funding shim
×
555
        // with the RPC server in case we end up aborting early.
×
556
        maybeCancelShim := func() {
×
557
                // If the user canceled while there was still a shim registered
×
558
                // with the wallet, release the resources now.
×
559
                if shimPending {
×
560
                        fmt.Printf("Canceling PSBT funding flow for pending "+
×
561
                                "channel ID %x.\n", pendingChanID)
×
562
                        cancelMsg := &lnrpc.FundingTransitionMsg{
×
563
                                Trigger: &lnrpc.FundingTransitionMsg_ShimCancel{
×
564
                                        ShimCancel: &lnrpc.FundingShimCancel{
×
565
                                                PendingChanId: pendingChanID[:],
×
566
                                        },
×
567
                                },
×
568
                        }
×
569
                        err := sendFundingState(ctxc, ctx, cancelMsg)
×
570
                        if err != nil {
×
571
                                fmt.Printf("Error canceling shim: %v\n", err)
×
572
                        }
×
573
                        shimPending = false
×
574
                }
575

576
                // Abort the stream connection to the server.
577
                cancel()
×
578
        }
579
        defer maybeCancelShim()
×
580

×
581
        // Create the PSBT funding shim that will tell the funding manager we
×
582
        // want to use a PSBT.
×
583
        req.FundingShim = &lnrpc.FundingShim{
×
584
                Shim: &lnrpc.FundingShim_PsbtShim{
×
585
                        PsbtShim: &lnrpc.PsbtShim{
×
586
                                PendingChanId: pendingChanID[:],
×
587
                                BasePsbt:      basePsbtBytes,
×
588
                                NoPublish:     ctx.Bool("no_publish"),
×
589
                        },
×
590
                },
×
591
        }
×
592

×
593
        // Start the interactive process by opening the stream connection to the
×
594
        // daemon. If the user cancels by pressing <Ctrl+C> we need to cancel
×
595
        // the shim. To not just kill the process on interrupt, we need to
×
596
        // explicitly capture the signal.
×
597
        stream, err := client.OpenChannel(ctxc, req)
×
598
        if err != nil {
×
599
                return fmt.Errorf("opening stream to server failed: %w", err)
×
600
        }
×
601

602
        // We also need to spawn a goroutine that reads from the server. This
603
        // will copy the messages to the channel as long as they come in or add
604
        // exactly one error to the error stream and then bail out.
605
        go func() {
×
606
                for {
×
607
                        // Recv blocks until a message or error arrives.
×
608
                        resp, err := stream.Recv()
×
609
                        if err == io.EOF {
×
610
                                srvErr <- fmt.Errorf("lnd shutting down: %w",
×
611
                                        err)
×
612
                                return
×
613
                        } else if err != nil {
×
614
                                srvErr <- fmt.Errorf("got error from server: "+
×
615
                                        "%v", err)
×
616
                                return
×
617
                        }
×
618

619
                        // Don't block on sending in case of shutting down.
620
                        select {
×
621
                        case srvMsg <- resp:
×
622
                        case <-quit:
×
623
                                return
×
624
                        }
625
                }
626
        }()
627

628
        // Spawn another goroutine that only handles abort from user or errors
629
        // from the server. Both will trigger an attempt to cancel the shim with
630
        // the server.
631
        go func() {
×
632
                select {
×
633
                case <-rpcCtx.Done():
×
634
                        fmt.Printf("\nInterrupt signal received.\n")
×
635
                        close(quit)
×
636

637
                case err := <-srvErr:
×
638
                        fmt.Printf("\nError received: %v\n", err)
×
639

×
640
                        // If the remote peer canceled on us, the reservation
×
641
                        // has already been deleted. We don't need to try to
×
642
                        // remove it again, this would just produce another
×
643
                        // error.
×
644
                        cancelErr := chanfunding.ErrRemoteCanceled.Error()
×
645
                        if err != nil && strings.Contains(
×
646
                                err.Error(), cancelErr,
×
647
                        ) {
×
648

×
649
                                shimPending = false
×
650
                        }
×
651
                        close(quit)
×
652

653
                case <-quit:
×
654
                }
655
        }()
656

657
        // Our main event loop where we wait for triggers
658
        for {
×
659
                var srvResponse *lnrpc.OpenStatusUpdate
×
660
                select {
×
661
                case srvResponse = <-srvMsg:
×
662
                case <-quit:
×
663
                        return nil
×
664
                }
665

666
                switch update := srvResponse.Update.(type) {
×
667
                case *lnrpc.OpenStatusUpdate_PsbtFund:
×
668
                        // First tell the user how to create the PSBT with the
×
669
                        // address and amount we now know.
×
670
                        amt := btcutil.Amount(update.PsbtFund.FundingAmount)
×
671
                        addr := update.PsbtFund.FundingAddress
×
672
                        fmt.Printf(
×
673
                                userMsgFund, req.NodePubkey, amt, amt, addr,
×
674
                                addr, amt.ToBTC(),
×
675
                                base64.StdEncoding.EncodeToString(
×
676
                                        update.PsbtFund.Psbt,
×
677
                                ),
×
678
                        )
×
679

×
680
                        // Read the user's response and send it to the server to
×
681
                        // verify everything's correct before anything is
×
682
                        // signed.
×
683
                        inputPsbt, err := readTerminalOrFile(quit)
×
684
                        if err == io.EOF {
×
685
                                return nil
×
686
                        }
×
687
                        if err != nil {
×
688
                                return fmt.Errorf("reading from terminal or "+
×
689
                                        "file failed: %v", err)
×
690
                        }
×
691
                        fundedPsbt, err := decodePsbt(inputPsbt)
×
692
                        if err != nil {
×
693
                                return fmt.Errorf("psbt decode failed: %w",
×
694
                                        err)
×
695
                        }
×
696
                        verifyMsg := &lnrpc.FundingTransitionMsg{
×
697
                                Trigger: &lnrpc.FundingTransitionMsg_PsbtVerify{
×
698
                                        PsbtVerify: &lnrpc.FundingPsbtVerify{
×
699
                                                FundedPsbt:    fundedPsbt,
×
700
                                                PendingChanId: pendingChanID[:],
×
701
                                        },
×
702
                                },
×
703
                        }
×
704
                        err = sendFundingState(ctxc, ctx, verifyMsg)
×
705
                        if err != nil {
×
706
                                return fmt.Errorf("verifying PSBT by lnd "+
×
707
                                        "failed: %v", err)
×
708
                        }
×
709

710
                        // Now that we know the PSBT looks good, we can let it
711
                        // be signed by the user.
712
                        fmt.Print(userMsgSign)
×
713

×
714
                        // Read the signed PSBT and send it to lnd.
×
715
                        finalTxStr, err := readTerminalOrFile(quit)
×
716
                        if err == io.EOF {
×
717
                                return nil
×
718
                        }
×
719
                        if err != nil {
×
720
                                return fmt.Errorf("reading from terminal or "+
×
721
                                        "file failed: %v", err)
×
722
                        }
×
723
                        finalizeMsg, err := finalizeMsgFromString(
×
724
                                finalTxStr, pendingChanID[:],
×
725
                        )
×
726
                        if err != nil {
×
727
                                return err
×
728
                        }
×
729
                        transitionMsg := &lnrpc.FundingTransitionMsg{
×
730
                                Trigger: finalizeMsg,
×
731
                        }
×
732
                        err = sendFundingState(ctxc, ctx, transitionMsg)
×
733
                        if err != nil {
×
734
                                return fmt.Errorf("finalizing PSBT funding "+
×
735
                                        "flow failed: %v", err)
×
736
                        }
×
737

738
                case *lnrpc.OpenStatusUpdate_ChanPending:
×
739
                        // As soon as the channel is pending, there is no more
×
740
                        // shim that needs to be canceled. If the user
×
741
                        // interrupts now, we don't need to clean up anything.
×
742
                        shimPending = false
×
743

×
744
                        err := printChanPending(update)
×
745
                        if err != nil {
×
746
                                return err
×
747
                        }
×
748

749
                        if !ctx.Bool("block") {
×
750
                                return nil
×
751
                        }
×
752

753
                case *lnrpc.OpenStatusUpdate_ChanOpen:
×
754
                        return printChanOpen(update)
×
755
                }
756
        }
757
}
758

759
var batchOpenChannelCommand = cli.Command{
760
        Name:     "batchopenchannel",
761
        Category: "Channels",
762
        Usage: "Open multiple channels to existing peers in a single " +
763
                "transaction.",
764
        Description: `
765
        Attempt to open one or more new channels to an existing peer with the
766
        given node-keys.
767

768
        Example:
769
        lncli batchopenchannel --sat_per_vbyte=5 '[{
770
                "node_pubkey": "02abcdef...",
771
                "local_funding_amount": 500000,
772
                "private": true,
773
                "close_address": "bc1qxxx..."
774
        }, {
775
                "node_pubkey": "03fedcba...",
776
                "local_funding_amount": 200000,
777
                "remote_csv_delay": 288
778
        }]'
779

780
        All nodes listed must already be connected peers, otherwise funding will
781
        fail.
782

783
        The channel will be initialized with local_funding_amount satoshis 
784
        locally and push_sat satoshis for the remote node. Note that specifying 
785
        push_sat means you give that amount to the remote node as part of the 
786
        channel        opening. Once the channel is open, a channelPoint (txid:vout) of 
787
        the funding output is returned.
788

789
        If the remote peer supports the option upfront shutdown feature bit
790
        (query listpeers to see their supported feature bits), an address to
791
        enforce        payout of funds on cooperative close can optionally be provided.
792
        Note that if you set this value, you will not be able to cooperatively
793
        close out to another address.
794

795
        One can manually set the fee to be used for the funding transaction via
796
        either the --conf_target or --sat_per_vbyte arguments. This is optional.
797
`,
798
        ArgsUsage: "channels-json",
799
        Flags: []cli.Flag{
800
                cli.Int64Flag{
801
                        Name: "conf_target",
802
                        Usage: "(optional) the number of blocks that the " +
803
                                "transaction *should* confirm in, will be " +
804
                                "used for fee estimation",
805
                },
806
                cli.Float64Flag{
807
                        Name: "sat_per_vbyte",
808
                        Usage: "(optional) a manual fee expressed in " +
809
                                "sat/vByte that should be used when crafting " +
810
                                "the transaction",
811
                },
812
                cli.Uint64Flag{
813
                        Name: "min_confs",
814
                        Usage: "(optional) the minimum number of " +
815
                                "confirmations each one of your outputs used " +
816
                                "for the funding transaction must satisfy",
817
                        Value: defaultUtxoMinConf,
818
                },
819
                cli.StringFlag{
820
                        Name: "label",
821
                        Usage: "(optional) a label to attach to the batch " +
822
                                "transaction when storing it to the local " +
823
                                "wallet after publishing it",
824
                },
825
                coinSelectionStrategyFlag,
826
        },
827
        Action: actionDecorator(batchOpenChannel),
828
}
829

830
type batchChannelJSON struct {
831
        NodePubkey         string `json:"node_pubkey,omitempty"`
832
        LocalFundingAmount int64  `json:"local_funding_amount,omitempty"`
833
        PushSat            int64  `json:"push_sat,omitempty"`
834
        Private            bool   `json:"private,omitempty"`
835
        MinHtlcMsat        int64  `json:"min_htlc_msat,omitempty"`
836
        RemoteCsvDelay     uint32 `json:"remote_csv_delay,omitempty"`
837
        CloseAddress       string `json:"close_address,omitempty"`
838
        PendingChanID      string `json:"pending_chan_id,omitempty"`
839
}
840

841
func batchOpenChannel(ctx *cli.Context) error {
×
842
        ctxc := getContext()
×
843
        client, cleanUp := getClient(ctx)
×
844
        defer cleanUp()
×
845

×
846
        args := ctx.Args()
×
847

×
848
        // Show command help if no arguments provided
×
849
        if ctx.NArg() == 0 {
×
850
                _ = cli.ShowCommandHelp(ctx, "batchopenchannel")
×
851
                return nil
×
852
        }
×
853

854
        coinSelectionStrategy, err := parseCoinSelectionStrategy(ctx)
×
855
        if err != nil {
×
856
                return err
×
857
        }
×
858

859
        // Parse fee rate from --sat_per_vbyte and convert to sat/kw.
NEW
860
        satPerKw, err := parseFeeRate(ctx, "sat_per_vbyte")
×
NEW
861
        if err != nil {
×
NEW
862
                return err
×
NEW
863
        }
×
864

865
        minConfs := int32(ctx.Uint64("min_confs"))
×
866
        req := &lnrpc.BatchOpenChannelRequest{
×
867
                TargetConf:            int32(ctx.Int64("conf_target")),
×
868
                MinConfs:              minConfs,
×
869
                SpendUnconfirmed:      minConfs == 0,
×
870
                Label:                 ctx.String("label"),
×
871
                CoinSelectionStrategy: coinSelectionStrategy,
×
NEW
872
                SatPerKw:              satPerKw,
×
873
        }
×
874

×
875
        // Let's try and parse the JSON part of the CLI now. Fortunately we can
×
876
        // parse it directly into the RPC struct if we use the correct
×
877
        // marshaler that keeps the original snake case.
×
878
        var jsonChannels []*batchChannelJSON
×
879
        if err := json.Unmarshal([]byte(args.First()), &jsonChannels); err != nil {
×
880
                return fmt.Errorf("error parsing channels JSON: %w", err)
×
881
        }
×
882

883
        req.Channels = make([]*lnrpc.BatchOpenChannel, len(jsonChannels))
×
884
        for idx, jsonChannel := range jsonChannels {
×
885
                pubKeyBytes, err := hex.DecodeString(jsonChannel.NodePubkey)
×
886
                if err != nil {
×
887
                        return fmt.Errorf("error parsing node pubkey hex: %w",
×
888
                                err)
×
889
                }
×
890
                pendingChanBytes, err := hex.DecodeString(
×
891
                        jsonChannel.PendingChanID,
×
892
                )
×
893
                if err != nil {
×
894
                        return fmt.Errorf("error parsing pending chan ID: %w",
×
895
                                err)
×
896
                }
×
897

898
                req.Channels[idx] = &lnrpc.BatchOpenChannel{
×
899
                        NodePubkey:         pubKeyBytes,
×
900
                        LocalFundingAmount: jsonChannel.LocalFundingAmount,
×
901
                        PushSat:            jsonChannel.PushSat,
×
902
                        Private:            jsonChannel.Private,
×
903
                        MinHtlcMsat:        jsonChannel.MinHtlcMsat,
×
904
                        RemoteCsvDelay:     jsonChannel.RemoteCsvDelay,
×
905
                        CloseAddress:       jsonChannel.CloseAddress,
×
906
                        PendingChanId:      pendingChanBytes,
×
907
                }
×
908
        }
909

910
        resp, err := client.BatchOpenChannel(ctxc, req)
×
911
        if err != nil {
×
912
                return err
×
913
        }
×
914

915
        for _, pending := range resp.PendingChannels {
×
916
                txid, err := chainhash.NewHash(pending.Txid)
×
917
                if err != nil {
×
918
                        return err
×
919
                }
×
920

921
                printJSON(struct {
×
922
                        FundingTxid        string `json:"funding_txid"`
×
923
                        FundingOutputIndex uint32 `json:"funding_output_index"`
×
924
                }{
×
925
                        FundingTxid:        txid.String(),
×
926
                        FundingOutputIndex: pending.OutputIndex,
×
927
                })
×
928
        }
929

930
        return nil
×
931
}
932

933
// printChanOpen prints the channel point of the channel open message.
934
func printChanOpen(update *lnrpc.OpenStatusUpdate_ChanOpen) error {
×
935
        channelPoint := update.ChanOpen.ChannelPoint
×
936

×
937
        // A channel point's funding txid can be get/set as a
×
938
        // byte slice or a string. In the case it is a string,
×
939
        // decode it.
×
940
        var txidHash []byte
×
941
        switch channelPoint.GetFundingTxid().(type) {
×
942
        case *lnrpc.ChannelPoint_FundingTxidBytes:
×
943
                txidHash = channelPoint.GetFundingTxidBytes()
×
944
        case *lnrpc.ChannelPoint_FundingTxidStr:
×
945
                s := channelPoint.GetFundingTxidStr()
×
946
                h, err := chainhash.NewHashFromStr(s)
×
947
                if err != nil {
×
948
                        return err
×
949
                }
×
950

951
                txidHash = h[:]
×
952
        }
953

954
        txid, err := chainhash.NewHash(txidHash)
×
955
        if err != nil {
×
956
                return err
×
957
        }
×
958

959
        index := channelPoint.OutputIndex
×
960
        printJSON(struct {
×
961
                ChannelPoint string `json:"channel_point"`
×
962
        }{
×
963
                ChannelPoint: fmt.Sprintf("%v:%v", txid, index),
×
964
        })
×
965
        return nil
×
966
}
967

968
// printChanPending prints the funding transaction ID of the channel pending
969
// message.
970
func printChanPending(update *lnrpc.OpenStatusUpdate_ChanPending) error {
×
971
        txid, err := chainhash.NewHash(update.ChanPending.Txid)
×
972
        if err != nil {
×
973
                return err
×
974
        }
×
975

976
        printJSON(struct {
×
977
                FundingTxid string `json:"funding_txid"`
×
978
        }{
×
979
                FundingTxid: txid.String(),
×
980
        })
×
981
        return nil
×
982
}
983

984
// readTerminalOrFile reads a single line from the terminal. If the line read is
985
// short enough to be a file and a file with that exact name exists, the content
986
// of that file is read and returned as a string. If the content is longer or no
987
// file exists, the string read from the terminal is returned directly. This
988
// function can be used to circumvent the N_TTY_BUF_SIZE kernel parameter that
989
// prevents pasting more than 4096 characters (on most systems) into a terminal.
990
func readTerminalOrFile(quit chan struct{}) (string, error) {
×
991
        maybeFile, err := readLine(quit)
×
992
        if err != nil {
×
993
                return "", err
×
994
        }
×
995

996
        // Absolute file paths normally can't be longer than 255 characters so
997
        // we don't even check if it's a file in that case.
998
        if len(maybeFile) > 255 {
×
999
                return maybeFile, nil
×
1000
        }
×
1001

1002
        // It might be a file since the length is small enough. Calling os.Stat
1003
        // should be safe with any arbitrary input as it will only query info
1004
        // about the file, not open or execute it directly.
1005
        stat, err := os.Stat(maybeFile)
×
1006

×
1007
        // The file doesn't exist, we must assume this wasn't a file path after
×
1008
        // all.
×
1009
        if err != nil && os.IsNotExist(err) {
×
1010
                return maybeFile, nil
×
1011
        }
×
1012

1013
        // Some other error, perhaps access denied or something similar, let's
1014
        // surface that to the user.
1015
        if err != nil {
×
1016
                return "", err
×
1017
        }
×
1018

1019
        // Make sure we don't read a huge file by accident which might lead to
1020
        // undesired side effects. Even very large PSBTs should still only be a
1021
        // few hundred kilobytes so it makes sense to put a cap here.
1022
        if stat.Size() > psbtMaxFileSize {
×
1023
                return "", fmt.Errorf("error reading file %s: size of %d "+
×
1024
                        "bytes exceeds max PSBT file size of %d", maybeFile,
×
1025
                        stat.Size(), psbtMaxFileSize)
×
1026
        }
×
1027

1028
        // If it's a path to an existing file and it's small enough, let's try
1029
        // to read its content now.
1030
        content, err := os.ReadFile(maybeFile)
×
1031
        if err != nil {
×
1032
                return "", err
×
1033
        }
×
1034

1035
        return string(content), nil
×
1036
}
1037

1038
// readLine reads a line from standard in but does not block in case of a
1039
// system interrupt like syscall.SIGINT (Ctrl+C).
1040
func readLine(quit chan struct{}) (string, error) {
×
1041
        msg := make(chan string, 1)
×
1042

×
1043
        // In a normal console, reading from stdin won't signal EOF when the
×
1044
        // user presses Ctrl+C. That's why we need to put this in a separate
×
1045
        // goroutine so it doesn't block.
×
1046
        go func() {
×
1047
                for {
×
1048
                        var str string
×
1049
                        _, _ = fmt.Scan(&str)
×
1050
                        msg <- str
×
1051
                        return
×
1052
                }
×
1053
        }()
1054
        for {
×
1055
                select {
×
1056
                case <-quit:
×
1057
                        return "", io.EOF
×
1058

1059
                case str := <-msg:
×
1060
                        return str, nil
×
1061
                }
1062
        }
1063
}
1064

1065
// checkPsbtFlags make sure a request to open a channel doesn't set any
1066
// parameters that are incompatible with the PSBT funding flow.
1067
func checkPsbtFlags(req *lnrpc.OpenChannelRequest) error {
×
1068
        if req.MinConfs != defaultUtxoMinConf || req.SpendUnconfirmed {
×
1069
                return fmt.Errorf("specifying minimum confirmations for PSBT " +
×
1070
                        "funding is not supported")
×
1071
        }
×
NEW
1072
        if req.TargetConf != 0 || req.SatPerByte != 0 || req.SatPerVbyte != 0 ||
×
NEW
1073
                req.SatPerKw != 0 {
×
NEW
1074

×
1075
                return fmt.Errorf("setting fee estimation parameters not " +
×
1076
                        "supported for PSBT funding")
×
1077
        }
×
1078
        return nil
×
1079
}
1080

1081
// sendFundingState sends a single funding state step message by using a new
1082
// client connection. This is necessary if the whole funding flow takes longer
1083
// than the default macaroon timeout, then we cannot use a single client
1084
// connection.
1085
func sendFundingState(cancelCtx context.Context, cliCtx *cli.Context,
1086
        msg *lnrpc.FundingTransitionMsg) error {
×
1087

×
1088
        client, cleanUp := getClient(cliCtx)
×
1089
        defer cleanUp()
×
1090

×
1091
        _, err := client.FundingStateStep(cancelCtx, msg)
×
1092
        return err
×
1093
}
×
1094

1095
// finalizeMsgFromString creates the final message for the PsbtFinalize step
1096
// from either a hex encoded raw wire transaction or a base64/binary encoded
1097
// PSBT packet.
1098
func finalizeMsgFromString(tx string,
1099
        pendingChanID []byte) (*lnrpc.FundingTransitionMsg_PsbtFinalize,
1100
        error) {
×
1101

×
1102
        psbtBytes, err := decodePsbt(tx)
×
1103
        if err == nil {
×
1104
                return &lnrpc.FundingTransitionMsg_PsbtFinalize{
×
1105
                        PsbtFinalize: &lnrpc.FundingPsbtFinalize{
×
1106
                                SignedPsbt:    psbtBytes,
×
1107
                                PendingChanId: pendingChanID,
×
1108
                        },
×
1109
                }, nil
×
1110
        }
×
1111

1112
        // PSBT decode failed, try to parse it as a hex encoded Bitcoin
1113
        // transaction
1114
        rawTx, err := hex.DecodeString(strings.TrimSpace(tx))
×
1115
        if err != nil {
×
1116
                return nil, fmt.Errorf("hex decode failed: %w", err)
×
1117
        }
×
1118
        msgtx := &wire.MsgTx{}
×
1119
        err = msgtx.Deserialize(bytes.NewReader(rawTx))
×
1120
        if err != nil {
×
1121
                return nil, fmt.Errorf("deserializing as raw wire "+
×
1122
                        "transaction failed: %v", err)
×
1123
        }
×
1124
        return &lnrpc.FundingTransitionMsg_PsbtFinalize{
×
1125
                PsbtFinalize: &lnrpc.FundingPsbtFinalize{
×
1126
                        FinalRawTx:    rawTx,
×
1127
                        PendingChanId: pendingChanID,
×
1128
                },
×
1129
        }, nil
×
1130
}
1131

1132
// decodePsbt tries to decode the input as a binary or base64 PSBT. If this
1133
// succeeded, the PSBT bytes are returned, an error otherwise.
1134
func decodePsbt(psbt string) ([]byte, error) {
×
1135
        switch {
×
1136
        case strings.HasPrefix(psbt, "psbt\xff"):
×
1137
                // A binary PSBT (read from a file) always starts with the PSBT
×
1138
                // magic "psbt\xff" according to BIP 174
×
1139
                return []byte(psbt), nil
×
1140

1141
        case strings.HasPrefix(strings.TrimSpace(psbt), "cHNidP"):
×
1142
                // A base64 PSBT always starts with "cHNidP". This is the
×
1143
                // longest base64 representation of the PSBT magic that is not
×
1144
                // dependent on the byte after it.
×
1145
                psbtBytes, err := base64.StdEncoding.DecodeString(
×
1146
                        strings.TrimSpace(psbt),
×
1147
                )
×
1148
                if err != nil {
×
1149
                        return nil, fmt.Errorf("base64 decode failed: %w", err)
×
1150
                }
×
1151

1152
                return psbtBytes, nil
×
1153

1154
        default:
×
1155
                return nil, fmt.Errorf("not a PSBT")
×
1156
        }
1157
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc