-
Notifications
You must be signed in to change notification settings - Fork 2.1k
/
cmd_open_channel.go
988 lines (884 loc) · 29.7 KB
/
cmd_open_channel.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
package main
import (
"bytes"
"context"
"crypto/rand"
"encoding/base64"
"encoding/hex"
"encoding/json"
"fmt"
"io"
"io/ioutil"
"os"
"strconv"
"strings"
"github.com/btcsuite/btcd/chaincfg/chainhash"
"github.com/btcsuite/btcd/wire"
"github.com/btcsuite/btcutil"
"github.com/lightningnetwork/lnd/lnrpc"
"github.com/lightningnetwork/lnd/lnwallet/chanfunding"
"github.com/urfave/cli"
)
const (
userMsgFund = `PSBT funding initiated with peer %x.
Please create a PSBT that sends %v (%d satoshi) to the funding address %s.
Note: The whole process should be completed within 10 minutes, otherwise there
is a risk of the remote node timing out and canceling the funding process.
Example with bitcoind:
bitcoin-cli walletcreatefundedpsbt [] '[{"%s":%.8f}]'
If you are using a wallet that can fund a PSBT directly (currently not possible
with bitcoind), you can use this PSBT that contains the same address and amount:
%s
!!! WARNING !!!
DO NOT PUBLISH the finished transaction by yourself or with another tool.
lnd MUST publish it in the proper funding flow order OR THE FUNDS CAN BE LOST!
Paste the funded PSBT here to continue the funding flow.
If your PSBT is very long (specifically, more than 4096 characters), please save
it to a file and paste the full file path here instead as some terminals will
truncate the pasted text if it's too long.
Base64 encoded PSBT (or path to text file): `
userMsgSign = `
PSBT verified by lnd, please continue the funding flow by signing the PSBT by
all required parties/devices. Once the transaction is fully signed, paste it
again here either in base64 PSBT or hex encoded raw wire TX format.
Signed base64 encoded PSBT or hex encoded raw wire TX (or path to text file): `
// psbtMaxFileSize is the maximum file size we allow a PSBT file to be
// in case we want to read a PSBT from a file. This is mainly to protect
// the user from choosing a large file by accident and running into out
// of memory issues or other weird errors.
psbtMaxFileSize = 1024 * 1024
channelTypeTweakless = "tweakless"
channelTypeAnchors = "anchors"
)
// TODO(roasbeef): change default number of confirmations
var openChannelCommand = cli.Command{
Name: "openchannel",
Category: "Channels",
Usage: "Open a channel to a node or an existing peer.",
Description: `
Attempt to open a new channel to an existing peer with the key node-key
optionally blocking until the channel is 'open'.
One can also connect to a node before opening a new channel to it by
setting its host:port via the --connect argument. For this to work,
the node_key must be provided, rather than the peer_id. This is optional.
The channel will be initialized with local-amt satoshis local and push-amt
satoshis for the remote node. Note that specifying push-amt means you give that
amount to the remote node as part of the channel opening. Once the channel is open,
a channelPoint (txid:vout) of the funding output is returned.
If the remote peer supports the option upfront shutdown feature bit (query
listpeers to see their supported feature bits), an address to enforce
payout of funds on cooperative close can optionally be provided. Note that
if you set this value, you will not be able to cooperatively close out to
another address.
One can manually set the fee to be used for the funding transaction via either
the --conf_target or --sat_per_vbyte arguments. This is optional.`,
ArgsUsage: "node-key local-amt push-amt",
Flags: []cli.Flag{
cli.StringFlag{
Name: "node_key",
Usage: "the identity public key of the target node/peer " +
"serialized in compressed format",
},
cli.StringFlag{
Name: "connect",
Usage: "(optional) the host:port of the target node",
},
cli.IntFlag{
Name: "local_amt",
Usage: "the number of satoshis the wallet should commit to the channel",
},
cli.IntFlag{
Name: "push_amt",
Usage: "the number of satoshis to give the remote side " +
"as part of the initial commitment state, " +
"this is equivalent to first opening a " +
"channel and sending the remote party funds, " +
"but done all in one step",
},
cli.BoolFlag{
Name: "block",
Usage: "block and wait until the channel is fully open",
},
cli.Int64Flag{
Name: "conf_target",
Usage: "(optional) the number of blocks that the " +
"transaction *should* confirm in, will be " +
"used for fee estimation",
},
cli.Int64Flag{
Name: "sat_per_byte",
Usage: "Deprecated, use sat_per_vbyte instead.",
Hidden: true,
},
cli.Int64Flag{
Name: "sat_per_vbyte",
Usage: "(optional) a manual fee expressed in " +
"sat/vbyte that should be used when crafting " +
"the transaction",
},
cli.BoolFlag{
Name: "private",
Usage: "make the channel private, such that it won't " +
"be announced to the greater network, and " +
"nodes other than the two channel endpoints " +
"must be explicitly told about it to be able " +
"to route through it",
},
cli.Int64Flag{
Name: "min_htlc_msat",
Usage: "(optional) the minimum value we will require " +
"for incoming HTLCs on the channel",
},
cli.Uint64Flag{
Name: "remote_csv_delay",
Usage: "(optional) the number of blocks we will require " +
"our channel counterparty to wait before accessing " +
"its funds in case of unilateral close. If this is " +
"not set, we will scale the value according to the " +
"channel size",
},
cli.Uint64Flag{
Name: "max_local_csv",
Usage: "(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.",
},
cli.Uint64Flag{
Name: "min_confs",
Usage: "(optional) the minimum number of confirmations " +
"each one of your outputs used for the funding " +
"transaction must satisfy",
Value: defaultUtxoMinConf,
},
cli.StringFlag{
Name: "close_address",
Usage: "(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.",
},
cli.BoolFlag{
Name: "psbt",
Usage: "start an interactive mode that initiates " +
"funding through a partially signed bitcoin " +
"transaction (PSBT), allowing the channel " +
"funds to be added and signed from a hardware " +
"or other offline device.",
},
cli.StringFlag{
Name: "base_psbt",
Usage: "when using the interactive PSBT mode to open " +
"a new channel, use this base64 encoded PSBT " +
"as a base and add the new channel output to " +
"it instead of creating a new, empty one.",
},
cli.BoolFlag{
Name: "no_publish",
Usage: "when using the interactive PSBT mode to open " +
"multiple channels in a batch, this flag " +
"instructs lnd to not publish the full batch " +
"transaction just yet. For safety reasons " +
"this flag should be set for each of the " +
"batch's transactions except the very last",
},
cli.Uint64Flag{
Name: "remote_max_value_in_flight_msat",
Usage: "(optional) the maximum value in msat that " +
"can be pending within the channel at any given time",
},
cli.StringFlag{
Name: "channel_type",
Usage: fmt.Sprintf("(optional) the type of channel to "+
"propose to the remote peer (%q, %q)",
channelTypeTweakless, channelTypeAnchors),
},
},
Action: actionDecorator(openChannel),
}
func openChannel(ctx *cli.Context) error {
// TODO(roasbeef): add deadline to context
ctxc := getContext()
client, cleanUp := getClient(ctx)
defer cleanUp()
args := ctx.Args()
var err error
// Show command help if no arguments provided
if ctx.NArg() == 0 && ctx.NumFlags() == 0 {
_ = cli.ShowCommandHelp(ctx, "openchannel")
return nil
}
// Check that only the field sat_per_vbyte or the deprecated field
// sat_per_byte is used.
feeRateFlag, err := checkNotBothSet(
ctx, "sat_per_vbyte", "sat_per_byte",
)
if err != nil {
return err
}
minConfs := int32(ctx.Uint64("min_confs"))
req := &lnrpc.OpenChannelRequest{
TargetConf: int32(ctx.Int64("conf_target")),
SatPerVbyte: ctx.Uint64(feeRateFlag),
MinHtlcMsat: ctx.Int64("min_htlc_msat"),
RemoteCsvDelay: uint32(ctx.Uint64("remote_csv_delay")),
MinConfs: minConfs,
SpendUnconfirmed: minConfs == 0,
CloseAddress: ctx.String("close_address"),
RemoteMaxValueInFlightMsat: ctx.Uint64("remote_max_value_in_flight_msat"),
MaxLocalCsv: uint32(ctx.Uint64("max_local_csv")),
}
switch {
case ctx.IsSet("node_key"):
nodePubHex, err := hex.DecodeString(ctx.String("node_key"))
if err != nil {
return fmt.Errorf("unable to decode node public key: %v", err)
}
req.NodePubkey = nodePubHex
case args.Present():
nodePubHex, err := hex.DecodeString(args.First())
if err != nil {
return fmt.Errorf("unable to decode node public key: %v", err)
}
args = args.Tail()
req.NodePubkey = nodePubHex
default:
return fmt.Errorf("node id argument missing")
}
// As soon as we can confirm that the node's node_key was set, rather
// than the peer_id, we can check if the host:port was also set to
// connect to it before opening the channel.
if req.NodePubkey != nil && ctx.IsSet("connect") {
addr := &lnrpc.LightningAddress{
Pubkey: hex.EncodeToString(req.NodePubkey),
Host: ctx.String("connect"),
}
req := &lnrpc.ConnectPeerRequest{
Addr: addr,
Perm: false,
}
// Check if connecting to the node was successful.
// We discard the peer id returned as it is not needed.
_, err := client.ConnectPeer(ctxc, req)
if err != nil &&
!strings.Contains(err.Error(), "already connected") {
return err
}
}
switch {
case ctx.IsSet("local_amt"):
req.LocalFundingAmount = int64(ctx.Int("local_amt"))
case args.Present():
req.LocalFundingAmount, err = strconv.ParseInt(args.First(), 10, 64)
if err != nil {
return fmt.Errorf("unable to decode local amt: %v", err)
}
args = args.Tail()
default:
return fmt.Errorf("local amt argument missing")
}
if ctx.IsSet("push_amt") {
req.PushSat = int64(ctx.Int("push_amt"))
} else if args.Present() {
req.PushSat, err = strconv.ParseInt(args.First(), 10, 64)
if err != nil {
return fmt.Errorf("unable to decode push amt: %v", err)
}
}
req.Private = ctx.Bool("private")
// Parse the channel type and map it to its RPC representation.
channelType := ctx.String("channel_type")
switch channelType {
case "":
break
case channelTypeTweakless:
req.CommitmentType = lnrpc.CommitmentType_STATIC_REMOTE_KEY
case channelTypeAnchors:
req.CommitmentType = lnrpc.CommitmentType_ANCHORS
default:
return fmt.Errorf("unsupported channel type %v", channelType)
}
// PSBT funding is a more involved, interactive process that is too
// large to also fit into this already long function.
if ctx.Bool("psbt") {
return openChannelPsbt(ctxc, ctx, client, req)
}
if !ctx.Bool("psbt") && ctx.Bool("no_publish") {
return fmt.Errorf("the --no_publish flag can only be used in " +
"combination with the --psbt flag")
}
stream, err := client.OpenChannel(ctxc, req)
if err != nil {
return err
}
for {
resp, err := stream.Recv()
if err == io.EOF {
return nil
} else if err != nil {
return err
}
switch update := resp.Update.(type) {
case *lnrpc.OpenStatusUpdate_ChanPending:
err := printChanPending(update)
if err != nil {
return err
}
if !ctx.Bool("block") {
return nil
}
case *lnrpc.OpenStatusUpdate_ChanOpen:
return printChanOpen(update)
}
}
}
// openChannelPsbt starts an interactive channel open protocol that uses a
// partially signed bitcoin transaction (PSBT) to fund the channel output. The
// protocol involves several steps between the RPC server and the CLI client:
//
// RPC server CLI client
// | |
// | |<------open channel (stream)-----|
// | |-------ready for funding----->| |
// | |<------PSBT verify------------| |
// | |-------ready for signing----->| |
// | |<------PSBT finalize----------| |
// | |-------channel pending------->| |
// | |-------channel open------------->|
// | |
func openChannelPsbt(rpcCtx context.Context, ctx *cli.Context,
client lnrpc.LightningClient,
req *lnrpc.OpenChannelRequest) error {
var (
pendingChanID [32]byte
shimPending = true
basePsbtBytes []byte
quit = make(chan struct{})
srvMsg = make(chan *lnrpc.OpenStatusUpdate, 1)
srvErr = make(chan error, 1)
ctxc, cancel = context.WithCancel(rpcCtx)
)
defer cancel()
// Make sure the user didn't supply any command line flags that are
// incompatible with PSBT funding.
err := checkPsbtFlags(req)
if err != nil {
return err
}
// If the user supplied a base PSBT, only make sure it's valid base64.
// The RPC server will make sure it's also a valid PSBT.
basePsbt := ctx.String("base_psbt")
if basePsbt != "" {
basePsbtBytes, err = base64.StdEncoding.DecodeString(basePsbt)
if err != nil {
return fmt.Errorf("error parsing base PSBT: %v", err)
}
}
// Generate a new, random pending channel ID that we'll use as the main
// identifier when sending update messages to the RPC server.
if _, err := rand.Read(pendingChanID[:]); err != nil {
return fmt.Errorf("unable to generate random chan ID: %v", err)
}
fmt.Printf("Starting PSBT funding flow with pending channel ID %x.\n",
pendingChanID)
// maybeCancelShim is a helper function that cancels the funding shim
// with the RPC server in case we end up aborting early.
maybeCancelShim := func() {
// If the user canceled while there was still a shim registered
// with the wallet, release the resources now.
if shimPending {
fmt.Printf("Canceling PSBT funding flow for pending "+
"channel ID %x.\n", pendingChanID)
cancelMsg := &lnrpc.FundingTransitionMsg{
Trigger: &lnrpc.FundingTransitionMsg_ShimCancel{
ShimCancel: &lnrpc.FundingShimCancel{
PendingChanId: pendingChanID[:],
},
},
}
err := sendFundingState(ctxc, ctx, cancelMsg)
if err != nil {
fmt.Printf("Error canceling shim: %v\n", err)
}
shimPending = false
}
// Abort the stream connection to the server.
cancel()
}
defer maybeCancelShim()
// Create the PSBT funding shim that will tell the funding manager we
// want to use a PSBT.
req.FundingShim = &lnrpc.FundingShim{
Shim: &lnrpc.FundingShim_PsbtShim{
PsbtShim: &lnrpc.PsbtShim{
PendingChanId: pendingChanID[:],
BasePsbt: basePsbtBytes,
NoPublish: ctx.Bool("no_publish"),
},
},
}
// Start the interactive process by opening the stream connection to the
// daemon. If the user cancels by pressing <Ctrl+C> we need to cancel
// the shim. To not just kill the process on interrupt, we need to
// explicitly capture the signal.
stream, err := client.OpenChannel(ctxc, req)
if err != nil {
return fmt.Errorf("opening stream to server failed: %v", err)
}
// We also need to spawn a goroutine that reads from the server. This
// will copy the messages to the channel as long as they come in or add
// exactly one error to the error stream and then bail out.
go func() {
for {
// Recv blocks until a message or error arrives.
resp, err := stream.Recv()
if err == io.EOF {
srvErr <- fmt.Errorf("lnd shutting down: %v",
err)
return
} else if err != nil {
srvErr <- fmt.Errorf("got error from server: "+
"%v", err)
return
}
// Don't block on sending in case of shutting down.
select {
case srvMsg <- resp:
case <-quit:
return
}
}
}()
// Spawn another goroutine that only handles abort from user or errors
// from the server. Both will trigger an attempt to cancel the shim with
// the server.
go func() {
select {
case <-rpcCtx.Done():
fmt.Printf("\nInterrupt signal received.\n")
close(quit)
case err := <-srvErr:
fmt.Printf("\nError received: %v\n", err)
// If the remote peer canceled on us, the reservation
// has already been deleted. We don't need to try to
// remove it again, this would just produce another
// error.
cancelErr := chanfunding.ErrRemoteCanceled.Error()
if err != nil && strings.Contains(err.Error(), cancelErr) {
shimPending = false
}
close(quit)
case <-quit:
}
}()
// Our main event loop where we wait for triggers
for {
var srvResponse *lnrpc.OpenStatusUpdate
select {
case srvResponse = <-srvMsg:
case <-quit:
return nil
}
switch update := srvResponse.Update.(type) {
case *lnrpc.OpenStatusUpdate_PsbtFund:
// First tell the user how to create the PSBT with the
// address and amount we now know.
amt := btcutil.Amount(update.PsbtFund.FundingAmount)
addr := update.PsbtFund.FundingAddress
fmt.Printf(
userMsgFund, req.NodePubkey, amt, amt, addr,
addr, amt.ToBTC(),
base64.StdEncoding.EncodeToString(
update.PsbtFund.Psbt,
),
)
// Read the user's response and send it to the server to
// verify everything's correct before anything is
// signed.
psbtBase64, err := readTerminalOrFile(quit)
if err == io.EOF {
return nil
}
if err != nil {
return fmt.Errorf("reading from terminal or "+
"file failed: %v", err)
}
fundedPsbt, err := base64.StdEncoding.DecodeString(
strings.TrimSpace(psbtBase64),
)
if err != nil {
return fmt.Errorf("base64 decode failed: %v",
err)
}
verifyMsg := &lnrpc.FundingTransitionMsg{
Trigger: &lnrpc.FundingTransitionMsg_PsbtVerify{
PsbtVerify: &lnrpc.FundingPsbtVerify{
FundedPsbt: fundedPsbt,
PendingChanId: pendingChanID[:],
},
},
}
err = sendFundingState(ctxc, ctx, verifyMsg)
if err != nil {
return fmt.Errorf("verifying PSBT by lnd "+
"failed: %v", err)
}
// Now that we know the PSBT looks good, we can let it
// be signed by the user.
fmt.Print(userMsgSign)
// Read the signed PSBT and send it to lnd.
finalTxStr, err := readTerminalOrFile(quit)
if err == io.EOF {
return nil
}
if err != nil {
return fmt.Errorf("reading from terminal or "+
"file failed: %v", err)
}
finalizeMsg, err := finalizeMsgFromString(
finalTxStr, pendingChanID[:],
)
if err != nil {
return err
}
transitionMsg := &lnrpc.FundingTransitionMsg{
Trigger: finalizeMsg,
}
err = sendFundingState(ctxc, ctx, transitionMsg)
if err != nil {
return fmt.Errorf("finalizing PSBT funding "+
"flow failed: %v", err)
}
case *lnrpc.OpenStatusUpdate_ChanPending:
// As soon as the channel is pending, there is no more
// shim that needs to be canceled. If the user
// interrupts now, we don't need to clean up anything.
shimPending = false
err := printChanPending(update)
if err != nil {
return err
}
if !ctx.Bool("block") {
return nil
}
case *lnrpc.OpenStatusUpdate_ChanOpen:
return printChanOpen(update)
}
}
}
var batchOpenChannelCommand = cli.Command{
Name: "batchopenchannel",
Category: "Channels",
Usage: "Open multiple channels to existing peers in a single " +
"transaction.",
Description: `
Attempt to open one or more new channels to an existing peer with the
given node-keys.
Example:
lncli batchopenchannel --sat_per_vbyte=5 '[{
"node_pubkey": "02abcdef...",
"local_funding_amount": 500000,
"private": true,
"close_address": "bc1qxxx..."
}, {
"node_pubkey": "03fedcba...",
"local_funding_amount": 200000,
"remote_csv_delay": 288
}]'
All nodes listed must already be connected peers, otherwise funding will
fail.
The channel will be initialized with local-amt satoshis local and
push-amt satoshis for the remote node. Note that specifying push-amt
means you give that amount to the remote node as part of the channel
opening. Once the channel is open, a channelPoint (txid:vout) of the
funding output is returned.
If the remote peer supports the option upfront shutdown feature bit
(query listpeers to see their supported feature bits), an address to
enforce payout of funds on cooperative close can optionally be provided.
Note that if you set this value, you will not be able to cooperatively
close out to another address.
One can manually set the fee to be used for the funding transaction via
either the --conf_target or --sat_per_vbyte arguments. This is optional.
`,
ArgsUsage: "channels-json",
Flags: []cli.Flag{
cli.Int64Flag{
Name: "conf_target",
Usage: "(optional) the number of blocks that the " +
"transaction *should* confirm in, will be " +
"used for fee estimation",
},
cli.Int64Flag{
Name: "sat_per_vbyte",
Usage: "(optional) a manual fee expressed in " +
"sat/vByte that should be used when crafting " +
"the transaction",
},
cli.Uint64Flag{
Name: "min_confs",
Usage: "(optional) the minimum number of " +
"confirmations each one of your outputs used " +
"for the funding transaction must satisfy",
Value: defaultUtxoMinConf,
},
cli.StringFlag{
Name: "label",
Usage: "(optional) a label to attach to the batch " +
"transaction when storing it to the local " +
"wallet after publishing it",
},
},
Action: actionDecorator(batchOpenChannel),
}
type batchChannelJSON struct {
NodePubkey string `json:"node_pubkey,omitempty"`
LocalFundingAmount int64 `json:"local_funding_amount,omitempty"`
PushSat int64 `json:"push_sat,omitempty"`
Private bool `json:"private,omitempty"`
MinHtlcMsat int64 `json:"min_htlc_msat,omitempty"`
RemoteCsvDelay uint32 `json:"remote_csv_delay,omitempty"`
CloseAddress string `json:"close_address,omitempty"`
PendingChanID string `json:"pending_chan_id,omitempty"`
}
func batchOpenChannel(ctx *cli.Context) error {
ctxc := getContext()
client, cleanUp := getClient(ctx)
defer cleanUp()
args := ctx.Args()
// Show command help if no arguments provided
if ctx.NArg() == 0 {
_ = cli.ShowCommandHelp(ctx, "batchopenchannel")
return nil
}
minConfs := int32(ctx.Uint64("min_confs"))
req := &lnrpc.BatchOpenChannelRequest{
TargetConf: int32(ctx.Int64("conf_target")),
SatPerVbyte: int64(ctx.Uint64("sat_per_vbyte")),
MinConfs: minConfs,
SpendUnconfirmed: minConfs == 0,
Label: ctx.String("label"),
}
// Let's try and parse the JSON part of the CLI now. Fortunately we can
// parse it directly into the RPC struct if we use the correct
// marshaler that keeps the original snake case.
var jsonChannels []*batchChannelJSON
if err := json.Unmarshal([]byte(args.First()), &jsonChannels); err != nil {
return fmt.Errorf("error parsing channels JSON: %v", err)
}
req.Channels = make([]*lnrpc.BatchOpenChannel, len(jsonChannels))
for idx, jsonChannel := range jsonChannels {
pubKeyBytes, err := hex.DecodeString(jsonChannel.NodePubkey)
if err != nil {
return fmt.Errorf("error parsing node pubkey hex: %v",
err)
}
pendingChanBytes, err := hex.DecodeString(
jsonChannel.PendingChanID,
)
if err != nil {
return fmt.Errorf("error parsing pending chan ID: %v",
err)
}
req.Channels[idx] = &lnrpc.BatchOpenChannel{
NodePubkey: pubKeyBytes,
LocalFundingAmount: jsonChannel.LocalFundingAmount,
PushSat: jsonChannel.PushSat,
Private: jsonChannel.Private,
MinHtlcMsat: jsonChannel.MinHtlcMsat,
RemoteCsvDelay: jsonChannel.RemoteCsvDelay,
CloseAddress: jsonChannel.CloseAddress,
PendingChanId: pendingChanBytes,
}
}
resp, err := client.BatchOpenChannel(ctxc, req)
if err != nil {
return err
}
for _, pending := range resp.PendingChannels {
txid, err := chainhash.NewHash(pending.Txid)
if err != nil {
return err
}
printJSON(struct {
FundingTxid string `json:"funding_txid"`
FundingOutputIndex uint32 `json:"funding_output_index"`
}{
FundingTxid: txid.String(),
FundingOutputIndex: pending.OutputIndex,
})
}
return nil
}
// printChanOpen prints the channel point of the channel open message.
func printChanOpen(update *lnrpc.OpenStatusUpdate_ChanOpen) error {
channelPoint := update.ChanOpen.ChannelPoint
// A channel point's funding txid can be get/set as a
// byte slice or a string. In the case it is a string,
// decode it.
var txidHash []byte
switch channelPoint.GetFundingTxid().(type) {
case *lnrpc.ChannelPoint_FundingTxidBytes:
txidHash = channelPoint.GetFundingTxidBytes()
case *lnrpc.ChannelPoint_FundingTxidStr:
s := channelPoint.GetFundingTxidStr()
h, err := chainhash.NewHashFromStr(s)
if err != nil {
return err
}
txidHash = h[:]
}
txid, err := chainhash.NewHash(txidHash)
if err != nil {
return err
}
index := channelPoint.OutputIndex
printJSON(struct {
ChannelPoint string `json:"channel_point"`
}{
ChannelPoint: fmt.Sprintf("%v:%v", txid, index),
})
return nil
}
// printChanPending prints the funding transaction ID of the channel pending
// message.
func printChanPending(update *lnrpc.OpenStatusUpdate_ChanPending) error {
txid, err := chainhash.NewHash(update.ChanPending.Txid)
if err != nil {
return err
}
printJSON(struct {
FundingTxid string `json:"funding_txid"`
}{
FundingTxid: txid.String(),
})
return nil
}
// readTerminalOrFile reads a single line from the terminal. If the line read is
// short enough to be a file and a file with that exact name exists, the content
// of that file is read and returned as a string. If the content is longer or no
// file exists, the string read from the terminal is returned directly. This
// function can be used to circumvent the N_TTY_BUF_SIZE kernel parameter that
// prevents pasting more than 4096 characters (on most systems) into a terminal.
func readTerminalOrFile(quit chan struct{}) (string, error) {
maybeFile, err := readLine(quit)
if err != nil {
return "", err
}
// Absolute file paths normally can't be longer than 255 characters so
// we don't even check if it's a file in that case.
if len(maybeFile) > 255 {
return maybeFile, nil
}
// It might be a file since the length is small enough. Calling os.Stat
// should be safe with any arbitrary input as it will only query info
// about the file, not open or execute it directly.
stat, err := os.Stat(maybeFile)
// The file doesn't exist, we must assume this wasn't a file path after
// all.
if err != nil && os.IsNotExist(err) {
return maybeFile, nil
}
// Some other error, perhaps access denied or something similar, let's
// surface that to the user.
if err != nil {
return "", err
}
// Make sure we don't read a huge file by accident which might lead to
// undesired side effects. Even very large PSBTs should still only be a
// few hundred kilobytes so it makes sense to put a cap here.
if stat.Size() > psbtMaxFileSize {
return "", fmt.Errorf("error reading file %s: size of %d "+
"bytes exceeds max PSBT file size of %d", maybeFile,
stat.Size(), psbtMaxFileSize)
}
// If it's a path to an existing file and it's small enough, let's try
// to read its content now.
content, err := ioutil.ReadFile(maybeFile)
if err != nil {
return "", err
}
return string(content), nil
}
// readLine reads a line from standard in but does not block in case of a
// system interrupt like syscall.SIGINT (Ctrl+C).
func readLine(quit chan struct{}) (string, error) {
msg := make(chan string, 1)
// In a normal console, reading from stdin won't signal EOF when the
// user presses Ctrl+C. That's why we need to put this in a separate
// goroutine so it doesn't block.
go func() {
for {
var str string
_, _ = fmt.Scan(&str)
msg <- str
return
}
}()
for {
select {
case <-quit:
return "", io.EOF
case str := <-msg:
return str, nil
}
}
}
// 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 {
if req.MinConfs != defaultUtxoMinConf || req.SpendUnconfirmed {
return fmt.Errorf("specifying minimum confirmations for PSBT " +
"funding is not supported")
}
if req.TargetConf != 0 || req.SatPerByte != 0 || req.SatPerVbyte != 0 { // nolint:staticcheck
return fmt.Errorf("setting fee estimation parameters not " +
"supported for PSBT funding")
}
return nil
}
// sendFundingState sends a single funding state step message by using a new
// client connection. This is necessary if the whole funding flow takes longer
// than the default macaroon timeout, then we cannot use a single client
// connection.
func sendFundingState(cancelCtx context.Context, cliCtx *cli.Context,
msg *lnrpc.FundingTransitionMsg) error {
client, cleanUp := getClient(cliCtx)
defer cleanUp()
_, err := client.FundingStateStep(cancelCtx, msg)
return err
}
// finalizeMsgFromString creates the final message for the PsbtFinalize step
// from either a hex encoded raw wire transaction or a base64 encoded PSBT
// packet.
func finalizeMsgFromString(tx string,
pendingChanID []byte) (*lnrpc.FundingTransitionMsg_PsbtFinalize, error) {
rawTx, err := hex.DecodeString(strings.TrimSpace(tx))
if err == nil {
// Hex decoding succeeded so we assume we have a raw wire format
// transaction. Let's submit that instead of a PSBT packet.
tx := &wire.MsgTx{}
err := tx.Deserialize(bytes.NewReader(rawTx))
if err != nil {
return nil, fmt.Errorf("deserializing as raw wire "+
"transaction failed: %v", err)
}
return &lnrpc.FundingTransitionMsg_PsbtFinalize{
PsbtFinalize: &lnrpc.FundingPsbtFinalize{
FinalRawTx: rawTx,
PendingChanId: pendingChanID,
},
}, nil
}
// If the string isn't a hex encoded transaction, we assume it must be
// a base64 encoded PSBT packet.
psbtBytes, err := base64.StdEncoding.DecodeString(strings.TrimSpace(tx))
if err != nil {
return nil, fmt.Errorf("base64 decode failed: %v", err)
}
return &lnrpc.FundingTransitionMsg_PsbtFinalize{
PsbtFinalize: &lnrpc.FundingPsbtFinalize{
SignedPsbt: psbtBytes,
PendingChanId: pendingChanID,
},
}, nil
}