Merge pull request #9025 from lightningnetwork/extract-from-staging-branch
[custom channels]: extract some independent commits from mega staging branch
This commit is contained in:
64 files changed
+1382
-795
No files matched your search
+2
-2
@@ -259,8 +259,8 @@ issues:
|
|||||||
- forbidigo
|
- forbidigo
|
||||||
- godot
|
- godot
|
||||||
|
|
||||||
# Allow fmt.Printf() in lncli.
|
# Allow fmt.Printf() in commands.
|
||||||
- path: cmd/lncli/*
|
- path: cmd/commands/*
|
||||||
linters:
|
linters:
|
||||||
- forbidigo
|
- forbidigo
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
package main
|
package commands
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"regexp"
|
"regexp"
|
||||||
@@ -42,7 +42,7 @@ func parseTime(s string, base time.Time) (uint64, error) {
|
|||||||
|
|
||||||
var lightningPrefix = "lightning:"
|
var lightningPrefix = "lightning:"
|
||||||
|
|
||||||
// stripPrefix removes accidentally copied 'lightning:' prefix.
|
// StripPrefix removes accidentally copied 'lightning:' prefix.
|
||||||
func stripPrefix(s string) string {
|
func StripPrefix(s string) string {
|
||||||
return strings.TrimSpace(strings.TrimPrefix(s, lightningPrefix))
|
return strings.TrimSpace(strings.TrimPrefix(s, lightningPrefix))
|
||||||
}
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package main
|
package commands
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"testing"
|
"testing"
|
||||||
@@ -111,7 +111,7 @@ func TestStripPrefix(t *testing.T) {
|
|||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
for _, test := range stripPrefixTests {
|
for _, test := range stripPrefixTests {
|
||||||
actual := stripPrefix(test.in)
|
actual := StripPrefix(test.in)
|
||||||
require.Equal(t, test.expected, actual)
|
require.Equal(t, test.expected, actual)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
//go:build autopilotrpc
|
//go:build autopilotrpc
|
||||||
// +build autopilotrpc
|
// +build autopilotrpc
|
||||||
|
|
||||||
package main
|
package commands
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/lightningnetwork/lnd/lnrpc/autopilotrpc"
|
"github.com/lightningnetwork/lnd/lnrpc/autopilotrpc"
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
//go:build !autopilotrpc
|
//go:build !autopilotrpc
|
||||||
// +build !autopilotrpc
|
// +build !autopilotrpc
|
||||||
|
|
||||||
package main
|
package commands
|
||||||
|
|
||||||
import "github.com/urfave/cli"
|
import "github.com/urfave/cli"
|
||||||
|
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
//go:build chainrpc
|
//go:build chainrpc
|
||||||
// +build chainrpc
|
// +build chainrpc
|
||||||
|
|
||||||
package main
|
package commands
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
//go:build !chainrpc
|
//go:build !chainrpc
|
||||||
// +build !chainrpc
|
// +build !chainrpc
|
||||||
|
|
||||||
package main
|
package commands
|
||||||
|
|
||||||
import "github.com/urfave/cli"
|
import "github.com/urfave/cli"
|
||||||
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package main
|
package commands
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package main
|
package commands
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
package main
|
package commands
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package main
|
package commands
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
@@ -9,7 +9,7 @@ import (
|
|||||||
"github.com/urfave/cli"
|
"github.com/urfave/cli"
|
||||||
)
|
)
|
||||||
|
|
||||||
var addInvoiceCommand = cli.Command{
|
var AddInvoiceCommand = cli.Command{
|
||||||
Name: "addinvoice",
|
Name: "addinvoice",
|
||||||
Category: "Invoices",
|
Category: "Invoices",
|
||||||
Usage: "Add a new invoice.",
|
Usage: "Add a new invoice.",
|
||||||
@@ -408,7 +408,7 @@ func decodePayReq(ctx *cli.Context) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
resp, err := client.DecodePayReq(ctxc, &lnrpc.PayReqString{
|
resp, err := client.DecodePayReq(ctxc, &lnrpc.PayReqString{
|
||||||
PayReq: stripPrefix(payreq),
|
PayReq: StripPrefix(payreq),
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package main
|
package commands
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package main
|
package commands
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
@@ -265,6 +265,7 @@ func setCfg(ctx *cli.Context) error {
|
|||||||
Config: mcCfg.Config,
|
Config: mcCfg.Config,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -366,5 +367,6 @@ func resetMissionControl(ctx *cli.Context) error {
|
|||||||
|
|
||||||
req := &routerrpc.ResetMissionControlRequest{}
|
req := &routerrpc.ResetMissionControlRequest{}
|
||||||
_, err := client.ResetMissionControl(ctxc, req)
|
_, err := client.ResetMissionControl(ctxc, req)
|
||||||
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package main
|
package commands
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package main
|
package commands
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
@@ -25,6 +25,7 @@ import (
|
|||||||
"github.com/lightningnetwork/lnd/record"
|
"github.com/lightningnetwork/lnd/record"
|
||||||
"github.com/lightningnetwork/lnd/routing/route"
|
"github.com/lightningnetwork/lnd/routing/route"
|
||||||
"github.com/urfave/cli"
|
"github.com/urfave/cli"
|
||||||
|
"google.golang.org/grpc"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -152,8 +153,8 @@ var (
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
// paymentFlags returns common flags for sendpayment and payinvoice.
|
// PaymentFlags returns common flags for sendpayment and payinvoice.
|
||||||
func paymentFlags() []cli.Flag {
|
func PaymentFlags() []cli.Flag {
|
||||||
return []cli.Flag{
|
return []cli.Flag{
|
||||||
cli.StringFlag{
|
cli.StringFlag{
|
||||||
Name: "pay_req",
|
Name: "pay_req",
|
||||||
@@ -202,7 +203,7 @@ func paymentFlags() []cli.Flag {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var sendPaymentCommand = cli.Command{
|
var SendPaymentCommand = cli.Command{
|
||||||
Name: "sendpayment",
|
Name: "sendpayment",
|
||||||
Category: "Payments",
|
Category: "Payments",
|
||||||
Usage: "Send a payment over lightning.",
|
Usage: "Send a payment over lightning.",
|
||||||
@@ -226,7 +227,7 @@ var sendPaymentCommand = cli.Command{
|
|||||||
`,
|
`,
|
||||||
ArgsUsage: "dest amt payment_hash final_cltv_delta pay_addr | " +
|
ArgsUsage: "dest amt payment_hash final_cltv_delta pay_addr | " +
|
||||||
"--pay_req=R [--pay_addr=H]",
|
"--pay_req=R [--pay_addr=H]",
|
||||||
Flags: append(paymentFlags(),
|
Flags: append(PaymentFlags(),
|
||||||
cli.StringFlag{
|
cli.StringFlag{
|
||||||
Name: "dest, d",
|
Name: "dest, d",
|
||||||
Usage: "the compressed identity pubkey of the " +
|
Usage: "the compressed identity pubkey of the " +
|
||||||
@@ -253,7 +254,7 @@ var sendPaymentCommand = cli.Command{
|
|||||||
Usage: "will generate a pre-image and encode it in the sphinx packet, a dest must be set [experimental]",
|
Usage: "will generate a pre-image and encode it in the sphinx packet, a dest must be set [experimental]",
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
Action: sendPayment,
|
Action: SendPayment,
|
||||||
}
|
}
|
||||||
|
|
||||||
// retrieveFeeLimit retrieves the fee limit based on the different fee limit
|
// retrieveFeeLimit retrieves the fee limit based on the different fee limit
|
||||||
@@ -324,20 +325,23 @@ func parsePayAddr(ctx *cli.Context, args cli.Args) ([]byte, error) {
|
|||||||
return payAddr, nil
|
return payAddr, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func sendPayment(ctx *cli.Context) error {
|
func SendPayment(ctx *cli.Context) error {
|
||||||
// Show command help if no arguments provided
|
// Show command help if no arguments provided
|
||||||
if ctx.NArg() == 0 && ctx.NumFlags() == 0 {
|
if ctx.NArg() == 0 && ctx.NumFlags() == 0 {
|
||||||
_ = cli.ShowCommandHelp(ctx, "sendpayment")
|
_ = cli.ShowCommandHelp(ctx, "sendpayment")
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
conn := getClientConn(ctx, false)
|
||||||
|
defer conn.Close()
|
||||||
|
|
||||||
args := ctx.Args()
|
args := ctx.Args()
|
||||||
|
|
||||||
// If a payment request was provided, we can exit early since all of the
|
// If a payment request was provided, we can exit early since all of the
|
||||||
// details of the payment are encoded within the request.
|
// details of the payment are encoded within the request.
|
||||||
if ctx.IsSet("pay_req") {
|
if ctx.IsSet("pay_req") {
|
||||||
req := &routerrpc.SendPaymentRequest{
|
req := &routerrpc.SendPaymentRequest{
|
||||||
PaymentRequest: stripPrefix(ctx.String("pay_req")),
|
PaymentRequest: StripPrefix(ctx.String("pay_req")),
|
||||||
Amt: ctx.Int64("amt"),
|
Amt: ctx.Int64("amt"),
|
||||||
DestCustomRecords: make(map[uint64][]byte),
|
DestCustomRecords: make(map[uint64][]byte),
|
||||||
Amp: ctx.Bool(ampFlag.Name),
|
Amp: ctx.Bool(ampFlag.Name),
|
||||||
@@ -357,7 +361,9 @@ func sendPayment(ctx *cli.Context) error {
|
|||||||
|
|
||||||
req.PaymentAddr = payAddr
|
req.PaymentAddr = payAddr
|
||||||
|
|
||||||
return sendPaymentRequest(ctx, req)
|
return SendPaymentRequest(
|
||||||
|
ctx, req, conn, conn, routerRPCSendPayment,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@@ -466,19 +472,29 @@ func sendPayment(ctx *cli.Context) error {
|
|||||||
|
|
||||||
req.PaymentAddr = payAddr
|
req.PaymentAddr = payAddr
|
||||||
|
|
||||||
return sendPaymentRequest(ctx, req)
|
return SendPaymentRequest(ctx, req, conn, conn, routerRPCSendPayment)
|
||||||
}
|
}
|
||||||
|
|
||||||
func sendPaymentRequest(ctx *cli.Context,
|
// SendPaymentFn is a function type that abstracts the SendPaymentV2 call of the
|
||||||
req *routerrpc.SendPaymentRequest) error {
|
// router client.
|
||||||
|
type SendPaymentFn func(ctx context.Context, payConn grpc.ClientConnInterface,
|
||||||
|
req *routerrpc.SendPaymentRequest) (PaymentResultStream, error)
|
||||||
|
|
||||||
|
// routerRPCSendPayment is the default implementation of the SendPaymentFn type
|
||||||
|
// that uses the lnd routerrpc.SendPaymentV2 call.
|
||||||
|
func routerRPCSendPayment(ctx context.Context, payConn grpc.ClientConnInterface,
|
||||||
|
req *routerrpc.SendPaymentRequest) (PaymentResultStream, error) {
|
||||||
|
|
||||||
|
return routerrpc.NewRouterClient(payConn).SendPaymentV2(ctx, req)
|
||||||
|
}
|
||||||
|
|
||||||
|
func SendPaymentRequest(ctx *cli.Context, req *routerrpc.SendPaymentRequest,
|
||||||
|
lnConn, paymentConn grpc.ClientConnInterface,
|
||||||
|
callSendPayment SendPaymentFn) error {
|
||||||
|
|
||||||
ctxc := getContext()
|
ctxc := getContext()
|
||||||
|
|
||||||
conn := getClientConn(ctx, false)
|
lnClient := lnrpc.NewLightningClient(lnConn)
|
||||||
defer conn.Close()
|
|
||||||
|
|
||||||
client := lnrpc.NewLightningClient(conn)
|
|
||||||
routerClient := routerrpc.NewRouterClient(conn)
|
|
||||||
|
|
||||||
outChan := ctx.Int64Slice("outgoing_chan_id")
|
outChan := ctx.Int64Slice("outgoing_chan_id")
|
||||||
if len(outChan) != 0 {
|
if len(outChan) != 0 {
|
||||||
@@ -558,7 +574,7 @@ func sendPaymentRequest(ctx *cli.Context,
|
|||||||
if req.PaymentRequest != "" {
|
if req.PaymentRequest != "" {
|
||||||
// Decode payment request to find out the amount.
|
// Decode payment request to find out the amount.
|
||||||
decodeReq := &lnrpc.PayReqString{PayReq: req.PaymentRequest}
|
decodeReq := &lnrpc.PayReqString{PayReq: req.PaymentRequest}
|
||||||
decodeResp, err := client.DecodePayReq(ctxc, decodeReq)
|
decodeResp, err := lnClient.DecodePayReq(ctxc, decodeReq)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -602,14 +618,12 @@ func sendPaymentRequest(ctx *cli.Context,
|
|||||||
printJSON := ctx.Bool(jsonFlag.Name)
|
printJSON := ctx.Bool(jsonFlag.Name)
|
||||||
req.NoInflightUpdates = !ctx.Bool(inflightUpdatesFlag.Name) && printJSON
|
req.NoInflightUpdates = !ctx.Bool(inflightUpdatesFlag.Name) && printJSON
|
||||||
|
|
||||||
stream, err := routerClient.SendPaymentV2(ctxc, req)
|
stream, err := callSendPayment(ctxc, paymentConn, req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
finalState, err := printLivePayment(
|
finalState, err := PrintLivePayment(ctxc, stream, lnClient, printJSON)
|
||||||
ctxc, stream, client, printJSON,
|
|
||||||
)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -667,24 +681,29 @@ func trackPayment(ctx *cli.Context) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
client := lnrpc.NewLightningClient(conn)
|
client := lnrpc.NewLightningClient(conn)
|
||||||
_, err = printLivePayment(ctxc, stream, client, ctx.Bool(jsonFlag.Name))
|
_, err = PrintLivePayment(ctxc, stream, client, ctx.Bool(jsonFlag.Name))
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// printLivePayment receives payment updates from the given stream and either
|
// PaymentResultStream is an interface that abstracts the Recv method of the
|
||||||
|
// SendPaymentV2 or TrackPaymentV2 client stream.
|
||||||
|
type PaymentResultStream interface {
|
||||||
|
Recv() (*lnrpc.Payment, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// PrintLivePayment receives payment updates from the given stream and either
|
||||||
// outputs them as json or as a more user-friendly formatted table. The table
|
// outputs them as json or as a more user-friendly formatted table. The table
|
||||||
// option uses terminal control codes to rewrite the output. This call
|
// option uses terminal control codes to rewrite the output. This call
|
||||||
// terminates when the payment reaches a final state.
|
// terminates when the payment reaches a final state.
|
||||||
func printLivePayment(ctxc context.Context,
|
func PrintLivePayment(ctxc context.Context, stream PaymentResultStream,
|
||||||
stream routerrpc.Router_TrackPaymentV2Client,
|
lnClient lnrpc.LightningClient, json bool) (*lnrpc.Payment, error) {
|
||||||
client lnrpc.LightningClient, json bool) (*lnrpc.Payment, error) {
|
|
||||||
|
|
||||||
// Terminal escape codes aren't supported on Windows, fall back to json.
|
// Terminal escape codes aren't supported on Windows, fall back to json.
|
||||||
if !json && runtime.GOOS == "windows" {
|
if !json && runtime.GOOS == "windows" {
|
||||||
json = true
|
json = true
|
||||||
}
|
}
|
||||||
|
|
||||||
aliases := newAliasCache(client)
|
aliases := newAliasCache(lnClient)
|
||||||
|
|
||||||
first := true
|
first := true
|
||||||
var lastLineCount int
|
var lastLineCount int
|
||||||
@@ -706,17 +725,17 @@ func printLivePayment(ctxc context.Context,
|
|||||||
// Write raw json to stdout.
|
// Write raw json to stdout.
|
||||||
printRespJSON(payment)
|
printRespJSON(payment)
|
||||||
} else {
|
} else {
|
||||||
table := formatPayment(ctxc, payment, aliases)
|
resultTable := formatPayment(ctxc, payment, aliases)
|
||||||
|
|
||||||
// Clear all previously written lines and print the
|
// Clear all previously written lines and print the
|
||||||
// updated table.
|
// updated table.
|
||||||
clearLines(lastLineCount)
|
clearLines(lastLineCount)
|
||||||
fmt.Print(table)
|
fmt.Print(resultTable)
|
||||||
|
|
||||||
// Store the number of lines written for the next update
|
// Store the number of lines written for the next update
|
||||||
// pass.
|
// pass.
|
||||||
lastLineCount = 0
|
lastLineCount = 0
|
||||||
for _, b := range table {
|
for _, b := range resultTable {
|
||||||
if b == '\n' {
|
if b == '\n' {
|
||||||
lastLineCount++
|
lastLineCount++
|
||||||
}
|
}
|
||||||
@@ -874,7 +893,7 @@ var payInvoiceCommand = cli.Command{
|
|||||||
This command is a shortcut for 'sendpayment --pay_req='.
|
This command is a shortcut for 'sendpayment --pay_req='.
|
||||||
`,
|
`,
|
||||||
ArgsUsage: "pay_req",
|
ArgsUsage: "pay_req",
|
||||||
Flags: append(paymentFlags(),
|
Flags: append(PaymentFlags(),
|
||||||
cli.Int64Flag{
|
cli.Int64Flag{
|
||||||
Name: "amt",
|
Name: "amt",
|
||||||
Usage: "(optional) number of satoshis to fulfill the " +
|
Usage: "(optional) number of satoshis to fulfill the " +
|
||||||
@@ -885,6 +904,9 @@ var payInvoiceCommand = cli.Command{
|
|||||||
}
|
}
|
||||||
|
|
||||||
func payInvoice(ctx *cli.Context) error {
|
func payInvoice(ctx *cli.Context) error {
|
||||||
|
conn := getClientConn(ctx, false)
|
||||||
|
defer conn.Close()
|
||||||
|
|
||||||
args := ctx.Args()
|
args := ctx.Args()
|
||||||
|
|
||||||
var payReq string
|
var payReq string
|
||||||
@@ -898,14 +920,14 @@ func payInvoice(ctx *cli.Context) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
req := &routerrpc.SendPaymentRequest{
|
req := &routerrpc.SendPaymentRequest{
|
||||||
PaymentRequest: stripPrefix(payReq),
|
PaymentRequest: StripPrefix(payReq),
|
||||||
Amt: ctx.Int64("amt"),
|
Amt: ctx.Int64("amt"),
|
||||||
DestCustomRecords: make(map[uint64][]byte),
|
DestCustomRecords: make(map[uint64][]byte),
|
||||||
Amp: ctx.Bool(ampFlag.Name),
|
Amp: ctx.Bool(ampFlag.Name),
|
||||||
Cancelable: ctx.Bool(cancelableFlag.Name),
|
Cancelable: ctx.Bool(cancelableFlag.Name),
|
||||||
}
|
}
|
||||||
|
|
||||||
return sendPaymentRequest(ctx, req)
|
return SendPaymentRequest(ctx, req, conn, conn, routerRPCSendPayment)
|
||||||
}
|
}
|
||||||
|
|
||||||
var sendToRouteCommand = cli.Command{
|
var sendToRouteCommand = cli.Command{
|
||||||
@@ -1900,7 +1922,7 @@ func estimateRouteFee(ctx *cli.Context) error {
|
|||||||
req.AmtSat = amtSat
|
req.AmtSat = amtSat
|
||||||
|
|
||||||
case ctx.IsSet("pay_req"):
|
case ctx.IsSet("pay_req"):
|
||||||
req.PaymentRequest = stripPrefix(ctx.String("pay_req"))
|
req.PaymentRequest = StripPrefix(ctx.String("pay_req"))
|
||||||
if ctx.IsSet("timeout") {
|
if ctx.IsSet("timeout") {
|
||||||
req.Timeout = uint32(ctx.Duration("timeout").Seconds())
|
req.Timeout = uint32(ctx.Duration("timeout").Seconds())
|
||||||
}
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package main
|
package commands
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package main
|
package commands
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package main
|
package commands
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package main
|
package commands
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package main
|
package commands
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bufio"
|
"bufio"
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package main
|
package commands
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bufio"
|
"bufio"
|
||||||
@@ -11,6 +11,7 @@ import (
|
|||||||
"io"
|
"io"
|
||||||
"math"
|
"math"
|
||||||
"os"
|
"os"
|
||||||
|
"regexp"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
@@ -41,8 +42,47 @@ const (
|
|||||||
defaultUtxoMinConf = 1
|
defaultUtxoMinConf = 1
|
||||||
)
|
)
|
||||||
|
|
||||||
var errBadChanPoint = errors.New("expecting chan_point to be in format of: " +
|
var (
|
||||||
"txid:index")
|
errBadChanPoint = errors.New(
|
||||||
|
"expecting chan_point to be in format of: txid:index",
|
||||||
|
)
|
||||||
|
|
||||||
|
customDataPattern = regexp.MustCompile(
|
||||||
|
`"custom_channel_data":\s*"([0-9a-f]+)"`,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
// replaceCustomData replaces the custom channel data hex string with the
|
||||||
|
// decoded custom channel data in the JSON response.
|
||||||
|
func replaceCustomData(jsonBytes []byte) ([]byte, error) {
|
||||||
|
// If there's nothing to replace, return the original JSON.
|
||||||
|
if !customDataPattern.Match(jsonBytes) {
|
||||||
|
return jsonBytes, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
replacedBytes := customDataPattern.ReplaceAllFunc(
|
||||||
|
jsonBytes, func(match []byte) []byte {
|
||||||
|
encoded := customDataPattern.FindStringSubmatch(
|
||||||
|
string(match),
|
||||||
|
)[1]
|
||||||
|
decoded, err := hex.DecodeString(encoded)
|
||||||
|
if err != nil {
|
||||||
|
return match
|
||||||
|
}
|
||||||
|
|
||||||
|
return []byte("\"custom_channel_data\":" +
|
||||||
|
string(decoded))
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
var buf bytes.Buffer
|
||||||
|
err := json.Indent(&buf, replacedBytes, "", " ")
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return buf.Bytes(), nil
|
||||||
|
}
|
||||||
|
|
||||||
func getContext() context.Context {
|
func getContext() context.Context {
|
||||||
shutdownInterceptor, err := signal.Intercept()
|
shutdownInterceptor, err := signal.Intercept()
|
||||||
@@ -66,9 +106,9 @@ func printJSON(resp interface{}) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var out bytes.Buffer
|
var out bytes.Buffer
|
||||||
json.Indent(&out, b, "", "\t")
|
_ = json.Indent(&out, b, "", " ")
|
||||||
out.WriteString("\n")
|
_, _ = out.WriteString("\n")
|
||||||
out.WriteTo(os.Stdout)
|
_, _ = out.WriteTo(os.Stdout)
|
||||||
}
|
}
|
||||||
|
|
||||||
func printRespJSON(resp proto.Message) {
|
func printRespJSON(resp proto.Message) {
|
||||||
@@ -78,7 +118,13 @@ func printRespJSON(resp proto.Message) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Printf("%s\n", jsonBytes)
|
jsonBytesReplaced, err := replaceCustomData(jsonBytes)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println("unable to replace custom data: ", err)
|
||||||
|
jsonBytesReplaced = jsonBytes
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("%s\n", jsonBytesReplaced)
|
||||||
}
|
}
|
||||||
|
|
||||||
// actionDecorator is used to add additional information and error handling
|
// actionDecorator is used to add additional information and error handling
|
||||||
@@ -1442,15 +1488,15 @@ func walletBalance(ctx *cli.Context) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
var channelBalanceCommand = cli.Command{
|
var ChannelBalanceCommand = cli.Command{
|
||||||
Name: "channelbalance",
|
Name: "channelbalance",
|
||||||
Category: "Channels",
|
Category: "Channels",
|
||||||
Usage: "Returns the sum of the total available channel balance across " +
|
Usage: "Returns the sum of the total available channel balance across " +
|
||||||
"all open channels.",
|
"all open channels.",
|
||||||
Action: actionDecorator(channelBalance),
|
Action: actionDecorator(ChannelBalance),
|
||||||
}
|
}
|
||||||
|
|
||||||
func channelBalance(ctx *cli.Context) error {
|
func ChannelBalance(ctx *cli.Context) error {
|
||||||
ctxc := getContext()
|
ctxc := getContext()
|
||||||
client, cleanUp := getClient(ctx)
|
client, cleanUp := getClient(ctx)
|
||||||
defer cleanUp()
|
defer cleanUp()
|
||||||
@@ -1575,7 +1621,7 @@ func pendingChannels(ctx *cli.Context) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
var listChannelsCommand = cli.Command{
|
var ListChannelsCommand = cli.Command{
|
||||||
Name: "listchannels",
|
Name: "listchannels",
|
||||||
Category: "Channels",
|
Category: "Channels",
|
||||||
Usage: "List all open channels.",
|
Usage: "List all open channels.",
|
||||||
@@ -1608,7 +1654,7 @@ var listChannelsCommand = cli.Command{
|
|||||||
"order to improve performance",
|
"order to improve performance",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
Action: actionDecorator(listChannels),
|
Action: actionDecorator(ListChannels),
|
||||||
}
|
}
|
||||||
|
|
||||||
var listAliasesCommand = cli.Command{
|
var listAliasesCommand = cli.Command{
|
||||||
@@ -1616,10 +1662,10 @@ var listAliasesCommand = cli.Command{
|
|||||||
Category: "Channels",
|
Category: "Channels",
|
||||||
Usage: "List all aliases.",
|
Usage: "List all aliases.",
|
||||||
Flags: []cli.Flag{},
|
Flags: []cli.Flag{},
|
||||||
Action: actionDecorator(listaliases),
|
Action: actionDecorator(listAliases),
|
||||||
}
|
}
|
||||||
|
|
||||||
func listaliases(ctx *cli.Context) error {
|
func listAliases(ctx *cli.Context) error {
|
||||||
ctxc := getContext()
|
ctxc := getContext()
|
||||||
client, cleanUp := getClient(ctx)
|
client, cleanUp := getClient(ctx)
|
||||||
defer cleanUp()
|
defer cleanUp()
|
||||||
@@ -1636,7 +1682,7 @@ func listaliases(ctx *cli.Context) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func listChannels(ctx *cli.Context) error {
|
func ListChannels(ctx *cli.Context) error {
|
||||||
ctxc := getContext()
|
ctxc := getContext()
|
||||||
client, cleanUp := getClient(ctx)
|
client, cleanUp := getClient(ctx)
|
||||||
defer cleanUp()
|
defer cleanUp()
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package main
|
package commands
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
@@ -120,3 +120,82 @@ func TestParseTimeLockDelta(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TestReplaceCustomData tests that hex encoded custom data can be formatted as
|
||||||
|
// JSON in the console output.
|
||||||
|
func TestReplaceCustomData(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
testCases := []struct {
|
||||||
|
name string
|
||||||
|
data string
|
||||||
|
replaceData string
|
||||||
|
expected string
|
||||||
|
expectedErr string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "no replacement necessary",
|
||||||
|
data: "foo",
|
||||||
|
expected: "foo",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "valid json with replacement",
|
||||||
|
data: "{\"foo\":\"bar\",\"custom_channel_data\":\"" +
|
||||||
|
hex.EncodeToString([]byte(
|
||||||
|
"{\"bar\":\"baz\"}",
|
||||||
|
)) + "\"}",
|
||||||
|
expected: `{
|
||||||
|
"foo": "bar",
|
||||||
|
"custom_channel_data": {
|
||||||
|
"bar": "baz"
|
||||||
|
}
|
||||||
|
}`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "valid json with replacement and space",
|
||||||
|
data: "{\"foo\":\"bar\",\"custom_channel_data\": \"" +
|
||||||
|
hex.EncodeToString([]byte(
|
||||||
|
"{\"bar\":\"baz\"}",
|
||||||
|
)) + "\"}",
|
||||||
|
expected: `{
|
||||||
|
"foo": "bar",
|
||||||
|
"custom_channel_data": {
|
||||||
|
"bar": "baz"
|
||||||
|
}
|
||||||
|
}`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "doesn't match pattern, returned identical",
|
||||||
|
data: "this ain't even json, and no custom data " +
|
||||||
|
"either",
|
||||||
|
expected: "this ain't even json, and no custom data " +
|
||||||
|
"either",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "invalid json",
|
||||||
|
data: "this ain't json, " +
|
||||||
|
"\"custom_channel_data\":\"a\"",
|
||||||
|
expectedErr: "invalid character 'h' in literal true",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "valid json, invalid hex, just formatted",
|
||||||
|
data: "{\"custom_channel_data\":\"f\"}",
|
||||||
|
expected: "{\n \"custom_channel_data\": \"f\"\n}",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range testCases {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
result, err := replaceCustomData([]byte(tc.data))
|
||||||
|
|
||||||
|
if tc.expectedErr != "" {
|
||||||
|
require.ErrorContains(t, err, tc.expectedErr)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
require.Equal(t, tc.expected, string(result))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
//go:build dev
|
//go:build dev
|
||||||
// +build dev
|
// +build dev
|
||||||
|
|
||||||
package main
|
package commands
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
//go:build !dev
|
//go:build !dev
|
||||||
// +build !dev
|
// +build !dev
|
||||||
|
|
||||||
package main
|
package commands
|
||||||
|
|
||||||
import "github.com/urfave/cli"
|
import "github.com/urfave/cli"
|
||||||
|
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
//go:build invoicesrpc
|
//go:build invoicesrpc
|
||||||
// +build invoicesrpc
|
// +build invoicesrpc
|
||||||
|
|
||||||
package main
|
package commands
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
//go:build !invoicesrpc
|
//go:build !invoicesrpc
|
||||||
// +build !invoicesrpc
|
// +build !invoicesrpc
|
||||||
|
|
||||||
package main
|
package commands
|
||||||
|
|
||||||
import "github.com/urfave/cli"
|
import "github.com/urfave/cli"
|
||||||
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package main
|
package commands
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package main
|
package commands
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
@@ -0,0 +1,601 @@
|
|||||||
|
// Copyright (c) 2013-2017 The btcsuite developers
|
||||||
|
// Copyright (c) 2015-2016 The Decred developers
|
||||||
|
// Copyright (C) 2015-2024 The Lightning Network Developers
|
||||||
|
|
||||||
|
package commands
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"crypto/tls"
|
||||||
|
"fmt"
|
||||||
|
"net"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
"syscall"
|
||||||
|
|
||||||
|
"github.com/btcsuite/btcd/btcutil"
|
||||||
|
"github.com/btcsuite/btcd/chaincfg"
|
||||||
|
"github.com/lightningnetwork/lnd"
|
||||||
|
"github.com/lightningnetwork/lnd/build"
|
||||||
|
"github.com/lightningnetwork/lnd/lncfg"
|
||||||
|
"github.com/lightningnetwork/lnd/lnrpc"
|
||||||
|
"github.com/lightningnetwork/lnd/macaroons"
|
||||||
|
"github.com/lightningnetwork/lnd/tor"
|
||||||
|
"github.com/urfave/cli"
|
||||||
|
"golang.org/x/term"
|
||||||
|
"google.golang.org/grpc"
|
||||||
|
"google.golang.org/grpc/credentials"
|
||||||
|
"google.golang.org/grpc/metadata"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
defaultDataDir = "data"
|
||||||
|
defaultChainSubDir = "chain"
|
||||||
|
defaultTLSCertFilename = "tls.cert"
|
||||||
|
defaultMacaroonFilename = "admin.macaroon"
|
||||||
|
defaultRPCPort = "10009"
|
||||||
|
defaultRPCHostPort = "localhost:" + defaultRPCPort
|
||||||
|
|
||||||
|
envVarRPCServer = "LNCLI_RPCSERVER"
|
||||||
|
envVarLNDDir = "LNCLI_LNDDIR"
|
||||||
|
envVarSOCKSProxy = "LNCLI_SOCKSPROXY"
|
||||||
|
envVarTLSCertPath = "LNCLI_TLSCERTPATH"
|
||||||
|
envVarChain = "LNCLI_CHAIN"
|
||||||
|
envVarNetwork = "LNCLI_NETWORK"
|
||||||
|
envVarMacaroonPath = "LNCLI_MACAROONPATH"
|
||||||
|
envVarMacaroonTimeout = "LNCLI_MACAROONTIMEOUT"
|
||||||
|
envVarMacaroonIP = "LNCLI_MACAROONIP"
|
||||||
|
envVarProfile = "LNCLI_PROFILE"
|
||||||
|
envVarMacFromJar = "LNCLI_MACFROMJAR"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
DefaultLndDir = btcutil.AppDataDir("lnd", false)
|
||||||
|
defaultTLSCertPath = filepath.Join(
|
||||||
|
DefaultLndDir, defaultTLSCertFilename,
|
||||||
|
)
|
||||||
|
|
||||||
|
// maxMsgRecvSize is the largest message our client will receive. We
|
||||||
|
// set this to 200MiB atm.
|
||||||
|
maxMsgRecvSize = grpc.MaxCallRecvMsgSize(lnrpc.MaxGrpcMsgSize)
|
||||||
|
)
|
||||||
|
|
||||||
|
func fatal(err error) {
|
||||||
|
fmt.Fprintf(os.Stderr, "[lncli] %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
func getWalletUnlockerClient(ctx *cli.Context) (lnrpc.WalletUnlockerClient,
|
||||||
|
func()) {
|
||||||
|
|
||||||
|
conn := getClientConn(ctx, true)
|
||||||
|
|
||||||
|
cleanUp := func() {
|
||||||
|
conn.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
return lnrpc.NewWalletUnlockerClient(conn), cleanUp
|
||||||
|
}
|
||||||
|
|
||||||
|
func getStateServiceClient(ctx *cli.Context) (lnrpc.StateClient, func()) {
|
||||||
|
conn := getClientConn(ctx, true)
|
||||||
|
|
||||||
|
cleanUp := func() {
|
||||||
|
conn.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
return lnrpc.NewStateClient(conn), cleanUp
|
||||||
|
}
|
||||||
|
|
||||||
|
func getClient(ctx *cli.Context) (lnrpc.LightningClient, func()) {
|
||||||
|
conn := getClientConn(ctx, false)
|
||||||
|
|
||||||
|
cleanUp := func() {
|
||||||
|
conn.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
return lnrpc.NewLightningClient(conn), cleanUp
|
||||||
|
}
|
||||||
|
|
||||||
|
func getClientConn(ctx *cli.Context, skipMacaroons bool) *grpc.ClientConn {
|
||||||
|
// First, we'll get the selected stored profile or an ephemeral one
|
||||||
|
// created from the global options in the CLI context.
|
||||||
|
profile, err := getGlobalOptions(ctx, skipMacaroons)
|
||||||
|
if err != nil {
|
||||||
|
fatal(fmt.Errorf("could not load global options: %w", err))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a dial options array.
|
||||||
|
opts := []grpc.DialOption{
|
||||||
|
grpc.WithUnaryInterceptor(
|
||||||
|
addMetadataUnaryInterceptor(profile.Metadata),
|
||||||
|
),
|
||||||
|
grpc.WithStreamInterceptor(
|
||||||
|
addMetaDataStreamInterceptor(profile.Metadata),
|
||||||
|
),
|
||||||
|
}
|
||||||
|
|
||||||
|
if profile.Insecure {
|
||||||
|
opts = append(opts, grpc.WithInsecure())
|
||||||
|
} else {
|
||||||
|
// Load the specified TLS certificate.
|
||||||
|
certPool, err := profile.cert()
|
||||||
|
if err != nil {
|
||||||
|
fatal(fmt.Errorf("could not create cert pool: %w", err))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build transport credentials from the certificate pool. If
|
||||||
|
// there is no certificate pool, we expect the server to use a
|
||||||
|
// non-self-signed certificate such as a certificate obtained
|
||||||
|
// from Let's Encrypt.
|
||||||
|
var creds credentials.TransportCredentials
|
||||||
|
if certPool != nil {
|
||||||
|
creds = credentials.NewClientTLSFromCert(certPool, "")
|
||||||
|
} else {
|
||||||
|
// Fallback to the system pool. Using an empty tls
|
||||||
|
// config is an alternative to x509.SystemCertPool().
|
||||||
|
// That call is not supported on Windows.
|
||||||
|
creds = credentials.NewTLS(&tls.Config{})
|
||||||
|
}
|
||||||
|
|
||||||
|
opts = append(opts, grpc.WithTransportCredentials(creds))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only process macaroon credentials if --no-macaroons isn't set and
|
||||||
|
// if we're not skipping macaroon processing.
|
||||||
|
if !profile.NoMacaroons && !skipMacaroons {
|
||||||
|
// Find out which macaroon to load.
|
||||||
|
macName := profile.Macaroons.Default
|
||||||
|
if ctx.GlobalIsSet("macfromjar") {
|
||||||
|
macName = ctx.GlobalString("macfromjar")
|
||||||
|
}
|
||||||
|
var macEntry *macaroonEntry
|
||||||
|
for _, entry := range profile.Macaroons.Jar {
|
||||||
|
if entry.Name == macName {
|
||||||
|
macEntry = entry
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if macEntry == nil {
|
||||||
|
fatal(fmt.Errorf("macaroon with name '%s' not found "+
|
||||||
|
"in profile", macName))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get and possibly decrypt the specified macaroon.
|
||||||
|
//
|
||||||
|
// TODO(guggero): Make it possible to cache the password so we
|
||||||
|
// don't need to ask for it every time.
|
||||||
|
mac, err := macEntry.loadMacaroon(readPassword)
|
||||||
|
if err != nil {
|
||||||
|
fatal(fmt.Errorf("could not load macaroon: %w", err))
|
||||||
|
}
|
||||||
|
|
||||||
|
macConstraints := []macaroons.Constraint{
|
||||||
|
// We add a time-based constraint to prevent replay of
|
||||||
|
// the macaroon. It's good for 60 seconds by default to
|
||||||
|
// make up for any discrepancy between client and server
|
||||||
|
// clocks, but leaking the macaroon before it becomes
|
||||||
|
// invalid makes it possible for an attacker to reuse
|
||||||
|
// the macaroon. In addition, the validity time of the
|
||||||
|
// macaroon is extended by the time the server clock is
|
||||||
|
// behind the client clock, or shortened by the time the
|
||||||
|
// server clock is ahead of the client clock (or invalid
|
||||||
|
// altogether if, in the latter case, this time is more
|
||||||
|
// than 60 seconds).
|
||||||
|
// TODO(aakselrod): add better anti-replay protection.
|
||||||
|
macaroons.TimeoutConstraint(profile.Macaroons.Timeout),
|
||||||
|
|
||||||
|
// Lock macaroon down to a specific IP address.
|
||||||
|
macaroons.IPLockConstraint(profile.Macaroons.IP),
|
||||||
|
|
||||||
|
// ... Add more constraints if needed.
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply constraints to the macaroon.
|
||||||
|
constrainedMac, err := macaroons.AddConstraints(
|
||||||
|
mac, macConstraints...,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Now we append the macaroon credentials to the dial options.
|
||||||
|
cred, err := macaroons.NewMacaroonCredential(constrainedMac)
|
||||||
|
if err != nil {
|
||||||
|
fatal(fmt.Errorf("error cloning mac: %w", err))
|
||||||
|
}
|
||||||
|
opts = append(opts, grpc.WithPerRPCCredentials(cred))
|
||||||
|
}
|
||||||
|
|
||||||
|
// If a socksproxy server is specified we use a tor dialer
|
||||||
|
// to connect to the grpc server.
|
||||||
|
if ctx.GlobalIsSet("socksproxy") {
|
||||||
|
socksProxy := ctx.GlobalString("socksproxy")
|
||||||
|
torDialer := func(_ context.Context, addr string) (net.Conn,
|
||||||
|
error) {
|
||||||
|
|
||||||
|
return tor.Dial(
|
||||||
|
addr, socksProxy, false, false,
|
||||||
|
tor.DefaultConnTimeout,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
opts = append(opts, grpc.WithContextDialer(torDialer))
|
||||||
|
} else {
|
||||||
|
// We need to use a custom dialer so we can also connect to
|
||||||
|
// unix sockets and not just TCP addresses.
|
||||||
|
genericDialer := lncfg.ClientAddressDialer(defaultRPCPort)
|
||||||
|
opts = append(opts, grpc.WithContextDialer(genericDialer))
|
||||||
|
}
|
||||||
|
|
||||||
|
opts = append(opts, grpc.WithDefaultCallOptions(maxMsgRecvSize))
|
||||||
|
|
||||||
|
conn, err := grpc.Dial(profile.RPCServer, opts...)
|
||||||
|
if err != nil {
|
||||||
|
fatal(fmt.Errorf("unable to connect to RPC server: %w", err))
|
||||||
|
}
|
||||||
|
|
||||||
|
return conn
|
||||||
|
}
|
||||||
|
|
||||||
|
// addMetadataUnaryInterceptor returns a grpc client side interceptor that
|
||||||
|
// appends any key-value metadata strings to the outgoing context of a grpc
|
||||||
|
// unary call.
|
||||||
|
func addMetadataUnaryInterceptor(
|
||||||
|
md map[string]string) grpc.UnaryClientInterceptor {
|
||||||
|
|
||||||
|
return func(ctx context.Context, method string, req, reply interface{},
|
||||||
|
cc *grpc.ClientConn, invoker grpc.UnaryInvoker,
|
||||||
|
opts ...grpc.CallOption) error {
|
||||||
|
|
||||||
|
outCtx := contextWithMetadata(ctx, md)
|
||||||
|
return invoker(outCtx, method, req, reply, cc, opts...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// addMetaDataStreamInterceptor returns a grpc client side interceptor that
|
||||||
|
// appends any key-value metadata strings to the outgoing context of a grpc
|
||||||
|
// stream call.
|
||||||
|
func addMetaDataStreamInterceptor(
|
||||||
|
md map[string]string) grpc.StreamClientInterceptor {
|
||||||
|
|
||||||
|
return func(ctx context.Context, desc *grpc.StreamDesc,
|
||||||
|
cc *grpc.ClientConn, method string, streamer grpc.Streamer,
|
||||||
|
opts ...grpc.CallOption) (grpc.ClientStream, error) {
|
||||||
|
|
||||||
|
outCtx := contextWithMetadata(ctx, md)
|
||||||
|
return streamer(outCtx, desc, cc, method, opts...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// contextWithMetaData appends the given metadata key-value pairs to the given
|
||||||
|
// context.
|
||||||
|
func contextWithMetadata(ctx context.Context,
|
||||||
|
md map[string]string) context.Context {
|
||||||
|
|
||||||
|
kvPairs := make([]string, 0, 2*len(md))
|
||||||
|
for k, v := range md {
|
||||||
|
kvPairs = append(kvPairs, k, v)
|
||||||
|
}
|
||||||
|
|
||||||
|
return metadata.AppendToOutgoingContext(ctx, kvPairs...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// extractPathArgs parses the TLS certificate and macaroon paths from the
|
||||||
|
// command.
|
||||||
|
func extractPathArgs(ctx *cli.Context) (string, string, error) {
|
||||||
|
network := strings.ToLower(ctx.GlobalString("network"))
|
||||||
|
switch network {
|
||||||
|
case "mainnet", "testnet", "regtest", "simnet", "signet":
|
||||||
|
default:
|
||||||
|
return "", "", fmt.Errorf("unknown network: %v", network)
|
||||||
|
}
|
||||||
|
|
||||||
|
// We'll now fetch the lnddir so we can make a decision on how to
|
||||||
|
// properly read the macaroons (if needed) and also the cert. This will
|
||||||
|
// either be the default, or will have been overwritten by the end
|
||||||
|
// user.
|
||||||
|
lndDir := lncfg.CleanAndExpandPath(ctx.GlobalString("lnddir"))
|
||||||
|
|
||||||
|
// If the macaroon path as been manually provided, then we'll only
|
||||||
|
// target the specified file.
|
||||||
|
var macPath string
|
||||||
|
if ctx.GlobalString("macaroonpath") != "" {
|
||||||
|
macPath = lncfg.CleanAndExpandPath(ctx.GlobalString(
|
||||||
|
"macaroonpath",
|
||||||
|
))
|
||||||
|
} else {
|
||||||
|
// Otherwise, we'll go into the path:
|
||||||
|
// lnddir/data/chain/<chain>/<network> in order to fetch the
|
||||||
|
// macaroon that we need.
|
||||||
|
macPath = filepath.Join(
|
||||||
|
lndDir, defaultDataDir, defaultChainSubDir,
|
||||||
|
lnd.BitcoinChainName, network, defaultMacaroonFilename,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
tlsCertPath := lncfg.CleanAndExpandPath(ctx.GlobalString("tlscertpath"))
|
||||||
|
|
||||||
|
// If a custom lnd directory was set, we'll also check if custom paths
|
||||||
|
// for the TLS cert and macaroon file were set as well. If not, we'll
|
||||||
|
// override their paths so they can be found within the custom lnd
|
||||||
|
// directory set. This allows us to set a custom lnd directory, along
|
||||||
|
// with custom paths to the TLS cert and macaroon file.
|
||||||
|
if lndDir != DefaultLndDir {
|
||||||
|
tlsCertPath = filepath.Join(lndDir, defaultTLSCertFilename)
|
||||||
|
}
|
||||||
|
|
||||||
|
return tlsCertPath, macPath, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// checkNotBothSet accepts two flag names, a and b, and checks that only flag a
|
||||||
|
// or flag b can be set, but not both. It returns the name of the flag or an
|
||||||
|
// error.
|
||||||
|
func checkNotBothSet(ctx *cli.Context, a, b string) (string, error) {
|
||||||
|
if ctx.IsSet(a) && ctx.IsSet(b) {
|
||||||
|
return "", fmt.Errorf(
|
||||||
|
"either %s or %s should be set, but not both", a, b,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if ctx.IsSet(a) {
|
||||||
|
return a, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return b, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func Main() {
|
||||||
|
app := cli.NewApp()
|
||||||
|
app.Name = "lncli"
|
||||||
|
app.Version = build.Version() + " commit=" + build.Commit
|
||||||
|
app.Usage = "control plane for your Lightning Network Daemon (lnd)"
|
||||||
|
app.Flags = []cli.Flag{
|
||||||
|
cli.StringFlag{
|
||||||
|
Name: "rpcserver",
|
||||||
|
Value: defaultRPCHostPort,
|
||||||
|
Usage: "The host:port of LN daemon.",
|
||||||
|
EnvVar: envVarRPCServer,
|
||||||
|
},
|
||||||
|
cli.StringFlag{
|
||||||
|
Name: "lnddir",
|
||||||
|
Value: DefaultLndDir,
|
||||||
|
Usage: "The path to lnd's base directory.",
|
||||||
|
TakesFile: true,
|
||||||
|
EnvVar: envVarLNDDir,
|
||||||
|
},
|
||||||
|
cli.StringFlag{
|
||||||
|
Name: "socksproxy",
|
||||||
|
Usage: "The host:port of a SOCKS proxy through " +
|
||||||
|
"which all connections to the LN " +
|
||||||
|
"daemon will be established over.",
|
||||||
|
EnvVar: envVarSOCKSProxy,
|
||||||
|
},
|
||||||
|
cli.StringFlag{
|
||||||
|
Name: "tlscertpath",
|
||||||
|
Value: defaultTLSCertPath,
|
||||||
|
Usage: "The path to lnd's TLS certificate.",
|
||||||
|
TakesFile: true,
|
||||||
|
EnvVar: envVarTLSCertPath,
|
||||||
|
},
|
||||||
|
cli.StringFlag{
|
||||||
|
Name: "chain, c",
|
||||||
|
Usage: "The chain lnd is running on, e.g. bitcoin.",
|
||||||
|
Value: "bitcoin",
|
||||||
|
EnvVar: envVarChain,
|
||||||
|
},
|
||||||
|
cli.StringFlag{
|
||||||
|
Name: "network, n",
|
||||||
|
Usage: "The network lnd is running on, e.g. mainnet, " +
|
||||||
|
"testnet, etc.",
|
||||||
|
Value: "mainnet",
|
||||||
|
EnvVar: envVarNetwork,
|
||||||
|
},
|
||||||
|
cli.BoolFlag{
|
||||||
|
Name: "no-macaroons",
|
||||||
|
Usage: "Disable macaroon authentication.",
|
||||||
|
},
|
||||||
|
cli.StringFlag{
|
||||||
|
Name: "macaroonpath",
|
||||||
|
Usage: "The path to macaroon file.",
|
||||||
|
TakesFile: true,
|
||||||
|
EnvVar: envVarMacaroonPath,
|
||||||
|
},
|
||||||
|
cli.Int64Flag{
|
||||||
|
Name: "macaroontimeout",
|
||||||
|
Value: 60,
|
||||||
|
Usage: "Anti-replay macaroon validity time in " +
|
||||||
|
"seconds.",
|
||||||
|
EnvVar: envVarMacaroonTimeout,
|
||||||
|
},
|
||||||
|
cli.StringFlag{
|
||||||
|
Name: "macaroonip",
|
||||||
|
Usage: "If set, lock macaroon to specific IP address.",
|
||||||
|
EnvVar: envVarMacaroonIP,
|
||||||
|
},
|
||||||
|
cli.StringFlag{
|
||||||
|
Name: "profile, p",
|
||||||
|
Usage: "Instead of reading settings from command " +
|
||||||
|
"line parameters or using the default " +
|
||||||
|
"profile, use a specific profile. If " +
|
||||||
|
"a default profile is set, this flag can be " +
|
||||||
|
"set to an empty string to disable reading " +
|
||||||
|
"values from the profiles file.",
|
||||||
|
EnvVar: envVarProfile,
|
||||||
|
},
|
||||||
|
cli.StringFlag{
|
||||||
|
Name: "macfromjar",
|
||||||
|
Usage: "Use this macaroon from the profile's " +
|
||||||
|
"macaroon jar instead of the default one. " +
|
||||||
|
"Can only be used if profiles are defined.",
|
||||||
|
EnvVar: envVarMacFromJar,
|
||||||
|
},
|
||||||
|
cli.StringSliceFlag{
|
||||||
|
Name: "metadata",
|
||||||
|
Usage: "This flag can be used to specify a key-value " +
|
||||||
|
"pair that should be appended to the " +
|
||||||
|
"outgoing context before the request is sent " +
|
||||||
|
"to lnd. This flag may be specified multiple " +
|
||||||
|
"times. The format is: \"key:value\".",
|
||||||
|
},
|
||||||
|
cli.BoolFlag{
|
||||||
|
Name: "insecure",
|
||||||
|
Usage: "Connect to the rpc server without TLS " +
|
||||||
|
"authentication",
|
||||||
|
Hidden: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
app.Commands = []cli.Command{
|
||||||
|
createCommand,
|
||||||
|
createWatchOnlyCommand,
|
||||||
|
unlockCommand,
|
||||||
|
changePasswordCommand,
|
||||||
|
newAddressCommand,
|
||||||
|
estimateFeeCommand,
|
||||||
|
sendManyCommand,
|
||||||
|
sendCoinsCommand,
|
||||||
|
listUnspentCommand,
|
||||||
|
connectCommand,
|
||||||
|
disconnectCommand,
|
||||||
|
openChannelCommand,
|
||||||
|
batchOpenChannelCommand,
|
||||||
|
closeChannelCommand,
|
||||||
|
closeAllChannelsCommand,
|
||||||
|
abandonChannelCommand,
|
||||||
|
listPeersCommand,
|
||||||
|
walletBalanceCommand,
|
||||||
|
ChannelBalanceCommand,
|
||||||
|
getInfoCommand,
|
||||||
|
getDebugInfoCommand,
|
||||||
|
encryptDebugPackageCommand,
|
||||||
|
decryptDebugPackageCommand,
|
||||||
|
getRecoveryInfoCommand,
|
||||||
|
pendingChannelsCommand,
|
||||||
|
SendPaymentCommand,
|
||||||
|
payInvoiceCommand,
|
||||||
|
sendToRouteCommand,
|
||||||
|
AddInvoiceCommand,
|
||||||
|
lookupInvoiceCommand,
|
||||||
|
listInvoicesCommand,
|
||||||
|
ListChannelsCommand,
|
||||||
|
closedChannelsCommand,
|
||||||
|
listPaymentsCommand,
|
||||||
|
describeGraphCommand,
|
||||||
|
getNodeMetricsCommand,
|
||||||
|
getChanInfoCommand,
|
||||||
|
getNodeInfoCommand,
|
||||||
|
queryRoutesCommand,
|
||||||
|
getNetworkInfoCommand,
|
||||||
|
debugLevelCommand,
|
||||||
|
decodePayReqCommand,
|
||||||
|
listChainTxnsCommand,
|
||||||
|
stopCommand,
|
||||||
|
signMessageCommand,
|
||||||
|
verifyMessageCommand,
|
||||||
|
feeReportCommand,
|
||||||
|
updateChannelPolicyCommand,
|
||||||
|
forwardingHistoryCommand,
|
||||||
|
exportChanBackupCommand,
|
||||||
|
verifyChanBackupCommand,
|
||||||
|
restoreChanBackupCommand,
|
||||||
|
bakeMacaroonCommand,
|
||||||
|
listMacaroonIDsCommand,
|
||||||
|
deleteMacaroonIDCommand,
|
||||||
|
listPermissionsCommand,
|
||||||
|
printMacaroonCommand,
|
||||||
|
constrainMacaroonCommand,
|
||||||
|
trackPaymentCommand,
|
||||||
|
versionCommand,
|
||||||
|
profileSubCommand,
|
||||||
|
getStateCommand,
|
||||||
|
deletePaymentsCommand,
|
||||||
|
sendCustomCommand,
|
||||||
|
subscribeCustomCommand,
|
||||||
|
fishCompletionCommand,
|
||||||
|
listAliasesCommand,
|
||||||
|
estimateRouteFeeCommand,
|
||||||
|
generateManPageCommand,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add any extra commands determined by build flags.
|
||||||
|
app.Commands = append(app.Commands, autopilotCommands()...)
|
||||||
|
app.Commands = append(app.Commands, invoicesCommands()...)
|
||||||
|
app.Commands = append(app.Commands, neutrinoCommands()...)
|
||||||
|
app.Commands = append(app.Commands, routerCommands()...)
|
||||||
|
app.Commands = append(app.Commands, walletCommands()...)
|
||||||
|
app.Commands = append(app.Commands, watchtowerCommands()...)
|
||||||
|
app.Commands = append(app.Commands, wtclientCommands()...)
|
||||||
|
app.Commands = append(app.Commands, devCommands()...)
|
||||||
|
app.Commands = append(app.Commands, peersCommands()...)
|
||||||
|
app.Commands = append(app.Commands, chainCommands()...)
|
||||||
|
|
||||||
|
if err := app.Run(os.Args); err != nil {
|
||||||
|
fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// readPassword reads a password from the terminal. This requires there to be an
|
||||||
|
// actual TTY so passing in a password from stdin won't work.
|
||||||
|
func readPassword(text string) ([]byte, error) {
|
||||||
|
fmt.Print(text)
|
||||||
|
|
||||||
|
// The variable syscall.Stdin is of a different type in the Windows API
|
||||||
|
// that's why we need the explicit cast. And of course the linter
|
||||||
|
// doesn't like it either.
|
||||||
|
pw, err := term.ReadPassword(int(syscall.Stdin)) //nolint:unconvert
|
||||||
|
fmt.Println()
|
||||||
|
|
||||||
|
return pw, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// networkParams parses the global network flag into a chaincfg.Params.
|
||||||
|
func networkParams(ctx *cli.Context) (*chaincfg.Params, error) {
|
||||||
|
network := strings.ToLower(ctx.GlobalString("network"))
|
||||||
|
switch network {
|
||||||
|
case "mainnet":
|
||||||
|
return &chaincfg.MainNetParams, nil
|
||||||
|
|
||||||
|
case "testnet":
|
||||||
|
return &chaincfg.TestNet3Params, nil
|
||||||
|
|
||||||
|
case "regtest":
|
||||||
|
return &chaincfg.RegressionNetParams, nil
|
||||||
|
|
||||||
|
case "simnet":
|
||||||
|
return &chaincfg.SimNetParams, nil
|
||||||
|
|
||||||
|
case "signet":
|
||||||
|
return &chaincfg.SigNetParams, nil
|
||||||
|
|
||||||
|
default:
|
||||||
|
return nil, fmt.Errorf("unknown network: %v", network)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseCoinSelectionStrategy parses a coin selection strategy string
|
||||||
|
// from the CLI to its lnrpc.CoinSelectionStrategy counterpart proto type.
|
||||||
|
func parseCoinSelectionStrategy(ctx *cli.Context) (
|
||||||
|
lnrpc.CoinSelectionStrategy, error) {
|
||||||
|
|
||||||
|
strategy := ctx.String(coinSelectionStrategyFlag.Name)
|
||||||
|
if !ctx.IsSet(coinSelectionStrategyFlag.Name) {
|
||||||
|
return lnrpc.CoinSelectionStrategy_STRATEGY_USE_GLOBAL_CONFIG,
|
||||||
|
nil
|
||||||
|
}
|
||||||
|
|
||||||
|
switch strategy {
|
||||||
|
case "global-config":
|
||||||
|
return lnrpc.CoinSelectionStrategy_STRATEGY_USE_GLOBAL_CONFIG,
|
||||||
|
nil
|
||||||
|
|
||||||
|
case "largest":
|
||||||
|
return lnrpc.CoinSelectionStrategy_STRATEGY_LARGEST, nil
|
||||||
|
|
||||||
|
case "random":
|
||||||
|
return lnrpc.CoinSelectionStrategy_STRATEGY_RANDOM, nil
|
||||||
|
|
||||||
|
default:
|
||||||
|
return 0, fmt.Errorf("unknown coin selection strategy "+
|
||||||
|
"%v", strategy)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
//go:build neutrinorpc
|
//go:build neutrinorpc
|
||||||
// +build neutrinorpc
|
// +build neutrinorpc
|
||||||
|
|
||||||
package main
|
package commands
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/lightningnetwork/lnd/lnrpc/neutrinorpc"
|
"github.com/lightningnetwork/lnd/lnrpc/neutrinorpc"
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
//go:build !neutrinorpc
|
//go:build !neutrinorpc
|
||||||
// +build !neutrinorpc
|
// +build !neutrinorpc
|
||||||
|
|
||||||
package main
|
package commands
|
||||||
|
|
||||||
import "github.com/urfave/cli"
|
import "github.com/urfave/cli"
|
||||||
|
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
//go:build peersrpc
|
//go:build peersrpc
|
||||||
// +build peersrpc
|
// +build peersrpc
|
||||||
|
|
||||||
package main
|
package commands
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
//go:build !peersrpc
|
//go:build !peersrpc
|
||||||
// +build !peersrpc
|
// +build !peersrpc
|
||||||
|
|
||||||
package main
|
package commands
|
||||||
|
|
||||||
import "github.com/urfave/cli"
|
import "github.com/urfave/cli"
|
||||||
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package main
|
package commands
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package main
|
package commands
|
||||||
|
|
||||||
import "github.com/urfave/cli"
|
import "github.com/urfave/cli"
|
||||||
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package main
|
package commands
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
//go:build walletrpc
|
//go:build walletrpc
|
||||||
// +build walletrpc
|
// +build walletrpc
|
||||||
|
|
||||||
package main
|
package commands
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
//go:build !walletrpc
|
//go:build !walletrpc
|
||||||
// +build !walletrpc
|
// +build !walletrpc
|
||||||
|
|
||||||
package main
|
package commands
|
||||||
|
|
||||||
import "github.com/urfave/cli"
|
import "github.com/urfave/cli"
|
||||||
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package main
|
package commands
|
||||||
|
|
||||||
import "github.com/lightningnetwork/lnd/lnrpc/walletrpc"
|
import "github.com/lightningnetwork/lnd/lnrpc/walletrpc"
|
||||||
|
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
//go:build watchtowerrpc
|
//go:build watchtowerrpc
|
||||||
// +build watchtowerrpc
|
// +build watchtowerrpc
|
||||||
|
|
||||||
package main
|
package commands
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/lightningnetwork/lnd/lnrpc/watchtowerrpc"
|
"github.com/lightningnetwork/lnd/lnrpc/watchtowerrpc"
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
//go:build !watchtowerrpc
|
//go:build !watchtowerrpc
|
||||||
// +build !watchtowerrpc
|
// +build !watchtowerrpc
|
||||||
|
|
||||||
package main
|
package commands
|
||||||
|
|
||||||
import "github.com/urfave/cli"
|
import "github.com/urfave/cli"
|
||||||
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package main
|
package commands
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
+3
-586
@@ -1,594 +1,11 @@
|
|||||||
// Copyright (c) 2013-2017 The btcsuite developers
|
// Copyright (c) 2013-2017 The btcsuite developers
|
||||||
// Copyright (c) 2015-2016 The Decred developers
|
// Copyright (c) 2015-2016 The Decred developers
|
||||||
// Copyright (C) 2015-2022 The Lightning Network Developers
|
// Copyright (C) 2015-2024 The Lightning Network Developers
|
||||||
|
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import "github.com/lightningnetwork/lnd/cmd/commands"
|
||||||
"context"
|
|
||||||
"crypto/tls"
|
|
||||||
"fmt"
|
|
||||||
"net"
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
"strings"
|
|
||||||
"syscall"
|
|
||||||
|
|
||||||
"github.com/btcsuite/btcd/btcutil"
|
|
||||||
"github.com/btcsuite/btcd/chaincfg"
|
|
||||||
"github.com/lightningnetwork/lnd"
|
|
||||||
"github.com/lightningnetwork/lnd/build"
|
|
||||||
"github.com/lightningnetwork/lnd/lncfg"
|
|
||||||
"github.com/lightningnetwork/lnd/lnrpc"
|
|
||||||
"github.com/lightningnetwork/lnd/macaroons"
|
|
||||||
"github.com/lightningnetwork/lnd/tor"
|
|
||||||
"github.com/urfave/cli"
|
|
||||||
"golang.org/x/term"
|
|
||||||
"google.golang.org/grpc"
|
|
||||||
"google.golang.org/grpc/credentials"
|
|
||||||
"google.golang.org/grpc/metadata"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
defaultDataDir = "data"
|
|
||||||
defaultChainSubDir = "chain"
|
|
||||||
defaultTLSCertFilename = "tls.cert"
|
|
||||||
defaultMacaroonFilename = "admin.macaroon"
|
|
||||||
defaultRPCPort = "10009"
|
|
||||||
defaultRPCHostPort = "localhost:" + defaultRPCPort
|
|
||||||
|
|
||||||
envVarRPCServer = "LNCLI_RPCSERVER"
|
|
||||||
envVarLNDDir = "LNCLI_LNDDIR"
|
|
||||||
envVarSOCKSProxy = "LNCLI_SOCKSPROXY"
|
|
||||||
envVarTLSCertPath = "LNCLI_TLSCERTPATH"
|
|
||||||
envVarChain = "LNCLI_CHAIN"
|
|
||||||
envVarNetwork = "LNCLI_NETWORK"
|
|
||||||
envVarMacaroonPath = "LNCLI_MACAROONPATH"
|
|
||||||
envVarMacaroonTimeout = "LNCLI_MACAROONTIMEOUT"
|
|
||||||
envVarMacaroonIP = "LNCLI_MACAROONIP"
|
|
||||||
envVarProfile = "LNCLI_PROFILE"
|
|
||||||
envVarMacFromJar = "LNCLI_MACFROMJAR"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
defaultLndDir = btcutil.AppDataDir("lnd", false)
|
|
||||||
defaultTLSCertPath = filepath.Join(defaultLndDir, defaultTLSCertFilename)
|
|
||||||
|
|
||||||
// maxMsgRecvSize is the largest message our client will receive. We
|
|
||||||
// set this to 200MiB atm.
|
|
||||||
maxMsgRecvSize = grpc.MaxCallRecvMsgSize(lnrpc.MaxGrpcMsgSize)
|
|
||||||
)
|
|
||||||
|
|
||||||
func fatal(err error) {
|
|
||||||
fmt.Fprintf(os.Stderr, "[lncli] %v\n", err)
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
func getWalletUnlockerClient(ctx *cli.Context) (lnrpc.WalletUnlockerClient, func()) {
|
|
||||||
conn := getClientConn(ctx, true)
|
|
||||||
|
|
||||||
cleanUp := func() {
|
|
||||||
conn.Close()
|
|
||||||
}
|
|
||||||
|
|
||||||
return lnrpc.NewWalletUnlockerClient(conn), cleanUp
|
|
||||||
}
|
|
||||||
|
|
||||||
func getStateServiceClient(ctx *cli.Context) (lnrpc.StateClient, func()) {
|
|
||||||
conn := getClientConn(ctx, true)
|
|
||||||
|
|
||||||
cleanUp := func() {
|
|
||||||
conn.Close()
|
|
||||||
}
|
|
||||||
|
|
||||||
return lnrpc.NewStateClient(conn), cleanUp
|
|
||||||
}
|
|
||||||
|
|
||||||
func getClient(ctx *cli.Context) (lnrpc.LightningClient, func()) {
|
|
||||||
conn := getClientConn(ctx, false)
|
|
||||||
|
|
||||||
cleanUp := func() {
|
|
||||||
conn.Close()
|
|
||||||
}
|
|
||||||
|
|
||||||
return lnrpc.NewLightningClient(conn), cleanUp
|
|
||||||
}
|
|
||||||
|
|
||||||
func getClientConn(ctx *cli.Context, skipMacaroons bool) *grpc.ClientConn {
|
|
||||||
// First, we'll get the selected stored profile or an ephemeral one
|
|
||||||
// created from the global options in the CLI context.
|
|
||||||
profile, err := getGlobalOptions(ctx, skipMacaroons)
|
|
||||||
if err != nil {
|
|
||||||
fatal(fmt.Errorf("could not load global options: %w", err))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create a dial options array.
|
|
||||||
opts := []grpc.DialOption{
|
|
||||||
grpc.WithUnaryInterceptor(
|
|
||||||
addMetadataUnaryInterceptor(profile.Metadata),
|
|
||||||
),
|
|
||||||
grpc.WithStreamInterceptor(
|
|
||||||
addMetaDataStreamInterceptor(profile.Metadata),
|
|
||||||
),
|
|
||||||
}
|
|
||||||
|
|
||||||
if profile.Insecure {
|
|
||||||
opts = append(opts, grpc.WithInsecure())
|
|
||||||
} else {
|
|
||||||
// Load the specified TLS certificate.
|
|
||||||
certPool, err := profile.cert()
|
|
||||||
if err != nil {
|
|
||||||
fatal(fmt.Errorf("could not create cert pool: %w", err))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Build transport credentials from the certificate pool. If
|
|
||||||
// there is no certificate pool, we expect the server to use a
|
|
||||||
// non-self-signed certificate such as a certificate obtained
|
|
||||||
// from Let's Encrypt.
|
|
||||||
var creds credentials.TransportCredentials
|
|
||||||
if certPool != nil {
|
|
||||||
creds = credentials.NewClientTLSFromCert(certPool, "")
|
|
||||||
} else {
|
|
||||||
// Fallback to the system pool. Using an empty tls
|
|
||||||
// config is an alternative to x509.SystemCertPool().
|
|
||||||
// That call is not supported on Windows.
|
|
||||||
creds = credentials.NewTLS(&tls.Config{})
|
|
||||||
}
|
|
||||||
|
|
||||||
opts = append(opts, grpc.WithTransportCredentials(creds))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Only process macaroon credentials if --no-macaroons isn't set and
|
|
||||||
// if we're not skipping macaroon processing.
|
|
||||||
if !profile.NoMacaroons && !skipMacaroons {
|
|
||||||
// Find out which macaroon to load.
|
|
||||||
macName := profile.Macaroons.Default
|
|
||||||
if ctx.GlobalIsSet("macfromjar") {
|
|
||||||
macName = ctx.GlobalString("macfromjar")
|
|
||||||
}
|
|
||||||
var macEntry *macaroonEntry
|
|
||||||
for _, entry := range profile.Macaroons.Jar {
|
|
||||||
if entry.Name == macName {
|
|
||||||
macEntry = entry
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if macEntry == nil {
|
|
||||||
fatal(fmt.Errorf("macaroon with name '%s' not found "+
|
|
||||||
"in profile", macName))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get and possibly decrypt the specified macaroon.
|
|
||||||
//
|
|
||||||
// TODO(guggero): Make it possible to cache the password so we
|
|
||||||
// don't need to ask for it every time.
|
|
||||||
mac, err := macEntry.loadMacaroon(readPassword)
|
|
||||||
if err != nil {
|
|
||||||
fatal(fmt.Errorf("could not load macaroon: %w", err))
|
|
||||||
}
|
|
||||||
|
|
||||||
macConstraints := []macaroons.Constraint{
|
|
||||||
// We add a time-based constraint to prevent replay of
|
|
||||||
// the macaroon. It's good for 60 seconds by default to
|
|
||||||
// make up for any discrepancy between client and server
|
|
||||||
// clocks, but leaking the macaroon before it becomes
|
|
||||||
// invalid makes it possible for an attacker to reuse
|
|
||||||
// the macaroon. In addition, the validity time of the
|
|
||||||
// macaroon is extended by the time the server clock is
|
|
||||||
// behind the client clock, or shortened by the time the
|
|
||||||
// server clock is ahead of the client clock (or invalid
|
|
||||||
// altogether if, in the latter case, this time is more
|
|
||||||
// than 60 seconds).
|
|
||||||
// TODO(aakselrod): add better anti-replay protection.
|
|
||||||
macaroons.TimeoutConstraint(profile.Macaroons.Timeout),
|
|
||||||
|
|
||||||
// Lock macaroon down to a specific IP address.
|
|
||||||
macaroons.IPLockConstraint(profile.Macaroons.IP),
|
|
||||||
|
|
||||||
// ... Add more constraints if needed.
|
|
||||||
}
|
|
||||||
|
|
||||||
// Apply constraints to the macaroon.
|
|
||||||
constrainedMac, err := macaroons.AddConstraints(
|
|
||||||
mac, macConstraints...,
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Now we append the macaroon credentials to the dial options.
|
|
||||||
cred, err := macaroons.NewMacaroonCredential(constrainedMac)
|
|
||||||
if err != nil {
|
|
||||||
fatal(fmt.Errorf("error cloning mac: %w", err))
|
|
||||||
}
|
|
||||||
opts = append(opts, grpc.WithPerRPCCredentials(cred))
|
|
||||||
}
|
|
||||||
|
|
||||||
// If a socksproxy server is specified we use a tor dialer
|
|
||||||
// to connect to the grpc server.
|
|
||||||
if ctx.GlobalIsSet("socksproxy") {
|
|
||||||
socksProxy := ctx.GlobalString("socksproxy")
|
|
||||||
torDialer := func(_ context.Context, addr string) (net.Conn,
|
|
||||||
error) {
|
|
||||||
|
|
||||||
return tor.Dial(
|
|
||||||
addr, socksProxy, false, false,
|
|
||||||
tor.DefaultConnTimeout,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
opts = append(opts, grpc.WithContextDialer(torDialer))
|
|
||||||
} else {
|
|
||||||
// We need to use a custom dialer so we can also connect to
|
|
||||||
// unix sockets and not just TCP addresses.
|
|
||||||
genericDialer := lncfg.ClientAddressDialer(defaultRPCPort)
|
|
||||||
opts = append(opts, grpc.WithContextDialer(genericDialer))
|
|
||||||
}
|
|
||||||
|
|
||||||
opts = append(opts, grpc.WithDefaultCallOptions(maxMsgRecvSize))
|
|
||||||
|
|
||||||
conn, err := grpc.Dial(profile.RPCServer, opts...)
|
|
||||||
if err != nil {
|
|
||||||
fatal(fmt.Errorf("unable to connect to RPC server: %w", err))
|
|
||||||
}
|
|
||||||
|
|
||||||
return conn
|
|
||||||
}
|
|
||||||
|
|
||||||
// addMetadataUnaryInterceptor returns a grpc client side interceptor that
|
|
||||||
// appends any key-value metadata strings to the outgoing context of a grpc
|
|
||||||
// unary call.
|
|
||||||
func addMetadataUnaryInterceptor(
|
|
||||||
md map[string]string) grpc.UnaryClientInterceptor {
|
|
||||||
|
|
||||||
return func(ctx context.Context, method string, req, reply interface{},
|
|
||||||
cc *grpc.ClientConn, invoker grpc.UnaryInvoker,
|
|
||||||
opts ...grpc.CallOption) error {
|
|
||||||
|
|
||||||
outCtx := contextWithMetadata(ctx, md)
|
|
||||||
return invoker(outCtx, method, req, reply, cc, opts...)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// addMetaDataStreamInterceptor returns a grpc client side interceptor that
|
|
||||||
// appends any key-value metadata strings to the outgoing context of a grpc
|
|
||||||
// stream call.
|
|
||||||
func addMetaDataStreamInterceptor(
|
|
||||||
md map[string]string) grpc.StreamClientInterceptor {
|
|
||||||
|
|
||||||
return func(ctx context.Context, desc *grpc.StreamDesc,
|
|
||||||
cc *grpc.ClientConn, method string, streamer grpc.Streamer,
|
|
||||||
opts ...grpc.CallOption) (grpc.ClientStream, error) {
|
|
||||||
|
|
||||||
outCtx := contextWithMetadata(ctx, md)
|
|
||||||
return streamer(outCtx, desc, cc, method, opts...)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// contextWithMetaData appends the given metadata key-value pairs to the given
|
|
||||||
// context.
|
|
||||||
func contextWithMetadata(ctx context.Context,
|
|
||||||
md map[string]string) context.Context {
|
|
||||||
|
|
||||||
kvPairs := make([]string, 0, 2*len(md))
|
|
||||||
for k, v := range md {
|
|
||||||
kvPairs = append(kvPairs, k, v)
|
|
||||||
}
|
|
||||||
|
|
||||||
return metadata.AppendToOutgoingContext(ctx, kvPairs...)
|
|
||||||
}
|
|
||||||
|
|
||||||
// extractPathArgs parses the TLS certificate and macaroon paths from the
|
|
||||||
// command.
|
|
||||||
func extractPathArgs(ctx *cli.Context) (string, string, error) {
|
|
||||||
network := strings.ToLower(ctx.GlobalString("network"))
|
|
||||||
switch network {
|
|
||||||
case "mainnet", "testnet", "regtest", "simnet", "signet":
|
|
||||||
default:
|
|
||||||
return "", "", fmt.Errorf("unknown network: %v", network)
|
|
||||||
}
|
|
||||||
|
|
||||||
// We'll now fetch the lnddir so we can make a decision on how to
|
|
||||||
// properly read the macaroons (if needed) and also the cert. This will
|
|
||||||
// either be the default, or will have been overwritten by the end
|
|
||||||
// user.
|
|
||||||
lndDir := lncfg.CleanAndExpandPath(ctx.GlobalString("lnddir"))
|
|
||||||
|
|
||||||
// If the macaroon path as been manually provided, then we'll only
|
|
||||||
// target the specified file.
|
|
||||||
var macPath string
|
|
||||||
if ctx.GlobalString("macaroonpath") != "" {
|
|
||||||
macPath = lncfg.CleanAndExpandPath(ctx.GlobalString("macaroonpath"))
|
|
||||||
} else {
|
|
||||||
// Otherwise, we'll go into the path:
|
|
||||||
// lnddir/data/chain/<chain>/<network> in order to fetch the
|
|
||||||
// macaroon that we need.
|
|
||||||
macPath = filepath.Join(
|
|
||||||
lndDir, defaultDataDir, defaultChainSubDir,
|
|
||||||
lnd.BitcoinChainName, network, defaultMacaroonFilename,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
tlsCertPath := lncfg.CleanAndExpandPath(ctx.GlobalString("tlscertpath"))
|
|
||||||
|
|
||||||
// If a custom lnd directory was set, we'll also check if custom paths
|
|
||||||
// for the TLS cert and macaroon file were set as well. If not, we'll
|
|
||||||
// override their paths so they can be found within the custom lnd
|
|
||||||
// directory set. This allows us to set a custom lnd directory, along
|
|
||||||
// with custom paths to the TLS cert and macaroon file.
|
|
||||||
if lndDir != defaultLndDir {
|
|
||||||
tlsCertPath = filepath.Join(lndDir, defaultTLSCertFilename)
|
|
||||||
}
|
|
||||||
|
|
||||||
return tlsCertPath, macPath, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// checkNotBothSet accepts two flag names, a and b, and checks that only flag a
|
|
||||||
// or flag b can be set, but not both. It returns the name of the flag or an
|
|
||||||
// error.
|
|
||||||
func checkNotBothSet(ctx *cli.Context, a, b string) (string, error) {
|
|
||||||
if ctx.IsSet(a) && ctx.IsSet(b) {
|
|
||||||
return "", fmt.Errorf(
|
|
||||||
"either %s or %s should be set, but not both", a, b,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
if ctx.IsSet(a) {
|
|
||||||
return a, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return b, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
app := cli.NewApp()
|
commands.Main()
|
||||||
app.Name = "lncli"
|
|
||||||
app.Version = build.Version() + " commit=" + build.Commit
|
|
||||||
app.Usage = "control plane for your Lightning Network Daemon (lnd)"
|
|
||||||
app.Flags = []cli.Flag{
|
|
||||||
cli.StringFlag{
|
|
||||||
Name: "rpcserver",
|
|
||||||
Value: defaultRPCHostPort,
|
|
||||||
Usage: "The host:port of LN daemon.",
|
|
||||||
EnvVar: envVarRPCServer,
|
|
||||||
},
|
|
||||||
cli.StringFlag{
|
|
||||||
Name: "lnddir",
|
|
||||||
Value: defaultLndDir,
|
|
||||||
Usage: "The path to lnd's base directory.",
|
|
||||||
TakesFile: true,
|
|
||||||
EnvVar: envVarLNDDir,
|
|
||||||
},
|
|
||||||
cli.StringFlag{
|
|
||||||
Name: "socksproxy",
|
|
||||||
Usage: "The host:port of a SOCKS proxy through " +
|
|
||||||
"which all connections to the LN " +
|
|
||||||
"daemon will be established over.",
|
|
||||||
EnvVar: envVarSOCKSProxy,
|
|
||||||
},
|
|
||||||
cli.StringFlag{
|
|
||||||
Name: "tlscertpath",
|
|
||||||
Value: defaultTLSCertPath,
|
|
||||||
Usage: "The path to lnd's TLS certificate.",
|
|
||||||
TakesFile: true,
|
|
||||||
EnvVar: envVarTLSCertPath,
|
|
||||||
},
|
|
||||||
cli.StringFlag{
|
|
||||||
Name: "chain, c",
|
|
||||||
Usage: "The chain lnd is running on, e.g. bitcoin.",
|
|
||||||
Value: "bitcoin",
|
|
||||||
EnvVar: envVarChain,
|
|
||||||
},
|
|
||||||
cli.StringFlag{
|
|
||||||
Name: "network, n",
|
|
||||||
Usage: "The network lnd is running on, e.g. mainnet, " +
|
|
||||||
"testnet, etc.",
|
|
||||||
Value: "mainnet",
|
|
||||||
EnvVar: envVarNetwork,
|
|
||||||
},
|
|
||||||
cli.BoolFlag{
|
|
||||||
Name: "no-macaroons",
|
|
||||||
Usage: "Disable macaroon authentication.",
|
|
||||||
},
|
|
||||||
cli.StringFlag{
|
|
||||||
Name: "macaroonpath",
|
|
||||||
Usage: "The path to macaroon file.",
|
|
||||||
TakesFile: true,
|
|
||||||
EnvVar: envVarMacaroonPath,
|
|
||||||
},
|
|
||||||
cli.Int64Flag{
|
|
||||||
Name: "macaroontimeout",
|
|
||||||
Value: 60,
|
|
||||||
Usage: "Anti-replay macaroon validity time in " +
|
|
||||||
"seconds.",
|
|
||||||
EnvVar: envVarMacaroonTimeout,
|
|
||||||
},
|
|
||||||
cli.StringFlag{
|
|
||||||
Name: "macaroonip",
|
|
||||||
Usage: "If set, lock macaroon to specific IP address.",
|
|
||||||
EnvVar: envVarMacaroonIP,
|
|
||||||
},
|
|
||||||
cli.StringFlag{
|
|
||||||
Name: "profile, p",
|
|
||||||
Usage: "Instead of reading settings from command " +
|
|
||||||
"line parameters or using the default " +
|
|
||||||
"profile, use a specific profile. If " +
|
|
||||||
"a default profile is set, this flag can be " +
|
|
||||||
"set to an empty string to disable reading " +
|
|
||||||
"values from the profiles file.",
|
|
||||||
EnvVar: envVarProfile,
|
|
||||||
},
|
|
||||||
cli.StringFlag{
|
|
||||||
Name: "macfromjar",
|
|
||||||
Usage: "Use this macaroon from the profile's " +
|
|
||||||
"macaroon jar instead of the default one. " +
|
|
||||||
"Can only be used if profiles are defined.",
|
|
||||||
EnvVar: envVarMacFromJar,
|
|
||||||
},
|
|
||||||
cli.StringSliceFlag{
|
|
||||||
Name: "metadata",
|
|
||||||
Usage: "This flag can be used to specify a key-value " +
|
|
||||||
"pair that should be appended to the " +
|
|
||||||
"outgoing context before the request is sent " +
|
|
||||||
"to lnd. This flag may be specified multiple " +
|
|
||||||
"times. The format is: \"key:value\".",
|
|
||||||
},
|
|
||||||
cli.BoolFlag{
|
|
||||||
Name: "insecure",
|
|
||||||
Usage: "Connect to the rpc server without TLS " +
|
|
||||||
"authentication",
|
|
||||||
Hidden: true,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
app.Commands = []cli.Command{
|
|
||||||
createCommand,
|
|
||||||
createWatchOnlyCommand,
|
|
||||||
unlockCommand,
|
|
||||||
changePasswordCommand,
|
|
||||||
newAddressCommand,
|
|
||||||
estimateFeeCommand,
|
|
||||||
sendManyCommand,
|
|
||||||
sendCoinsCommand,
|
|
||||||
listUnspentCommand,
|
|
||||||
connectCommand,
|
|
||||||
disconnectCommand,
|
|
||||||
openChannelCommand,
|
|
||||||
batchOpenChannelCommand,
|
|
||||||
closeChannelCommand,
|
|
||||||
closeAllChannelsCommand,
|
|
||||||
abandonChannelCommand,
|
|
||||||
listPeersCommand,
|
|
||||||
walletBalanceCommand,
|
|
||||||
channelBalanceCommand,
|
|
||||||
getInfoCommand,
|
|
||||||
getDebugInfoCommand,
|
|
||||||
encryptDebugPackageCommand,
|
|
||||||
decryptDebugPackageCommand,
|
|
||||||
getRecoveryInfoCommand,
|
|
||||||
pendingChannelsCommand,
|
|
||||||
sendPaymentCommand,
|
|
||||||
payInvoiceCommand,
|
|
||||||
sendToRouteCommand,
|
|
||||||
addInvoiceCommand,
|
|
||||||
lookupInvoiceCommand,
|
|
||||||
listInvoicesCommand,
|
|
||||||
listChannelsCommand,
|
|
||||||
closedChannelsCommand,
|
|
||||||
listPaymentsCommand,
|
|
||||||
describeGraphCommand,
|
|
||||||
getNodeMetricsCommand,
|
|
||||||
getChanInfoCommand,
|
|
||||||
getNodeInfoCommand,
|
|
||||||
queryRoutesCommand,
|
|
||||||
getNetworkInfoCommand,
|
|
||||||
debugLevelCommand,
|
|
||||||
decodePayReqCommand,
|
|
||||||
listChainTxnsCommand,
|
|
||||||
stopCommand,
|
|
||||||
signMessageCommand,
|
|
||||||
verifyMessageCommand,
|
|
||||||
feeReportCommand,
|
|
||||||
updateChannelPolicyCommand,
|
|
||||||
forwardingHistoryCommand,
|
|
||||||
exportChanBackupCommand,
|
|
||||||
verifyChanBackupCommand,
|
|
||||||
restoreChanBackupCommand,
|
|
||||||
bakeMacaroonCommand,
|
|
||||||
listMacaroonIDsCommand,
|
|
||||||
deleteMacaroonIDCommand,
|
|
||||||
listPermissionsCommand,
|
|
||||||
printMacaroonCommand,
|
|
||||||
constrainMacaroonCommand,
|
|
||||||
trackPaymentCommand,
|
|
||||||
versionCommand,
|
|
||||||
profileSubCommand,
|
|
||||||
getStateCommand,
|
|
||||||
deletePaymentsCommand,
|
|
||||||
sendCustomCommand,
|
|
||||||
subscribeCustomCommand,
|
|
||||||
fishCompletionCommand,
|
|
||||||
listAliasesCommand,
|
|
||||||
estimateRouteFeeCommand,
|
|
||||||
generateManPageCommand,
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add any extra commands determined by build flags.
|
|
||||||
app.Commands = append(app.Commands, autopilotCommands()...)
|
|
||||||
app.Commands = append(app.Commands, invoicesCommands()...)
|
|
||||||
app.Commands = append(app.Commands, neutrinoCommands()...)
|
|
||||||
app.Commands = append(app.Commands, routerCommands()...)
|
|
||||||
app.Commands = append(app.Commands, walletCommands()...)
|
|
||||||
app.Commands = append(app.Commands, watchtowerCommands()...)
|
|
||||||
app.Commands = append(app.Commands, wtclientCommands()...)
|
|
||||||
app.Commands = append(app.Commands, devCommands()...)
|
|
||||||
app.Commands = append(app.Commands, peersCommands()...)
|
|
||||||
app.Commands = append(app.Commands, chainCommands()...)
|
|
||||||
|
|
||||||
if err := app.Run(os.Args); err != nil {
|
|
||||||
fatal(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// readPassword reads a password from the terminal. This requires there to be an
|
|
||||||
// actual TTY so passing in a password from stdin won't work.
|
|
||||||
func readPassword(text string) ([]byte, error) {
|
|
||||||
fmt.Print(text)
|
|
||||||
|
|
||||||
// The variable syscall.Stdin is of a different type in the Windows API
|
|
||||||
// that's why we need the explicit cast. And of course the linter
|
|
||||||
// doesn't like it either.
|
|
||||||
pw, err := term.ReadPassword(int(syscall.Stdin)) // nolint:unconvert
|
|
||||||
fmt.Println()
|
|
||||||
return pw, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// networkParams parses the global network flag into a chaincfg.Params.
|
|
||||||
func networkParams(ctx *cli.Context) (*chaincfg.Params, error) {
|
|
||||||
network := strings.ToLower(ctx.GlobalString("network"))
|
|
||||||
switch network {
|
|
||||||
case "mainnet":
|
|
||||||
return &chaincfg.MainNetParams, nil
|
|
||||||
|
|
||||||
case "testnet":
|
|
||||||
return &chaincfg.TestNet3Params, nil
|
|
||||||
|
|
||||||
case "regtest":
|
|
||||||
return &chaincfg.RegressionNetParams, nil
|
|
||||||
|
|
||||||
case "simnet":
|
|
||||||
return &chaincfg.SimNetParams, nil
|
|
||||||
|
|
||||||
case "signet":
|
|
||||||
return &chaincfg.SigNetParams, nil
|
|
||||||
|
|
||||||
default:
|
|
||||||
return nil, fmt.Errorf("unknown network: %v", network)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// parseCoinSelectionStrategy parses a coin selection strategy string
|
|
||||||
// from the CLI to its lnrpc.CoinSelectionStrategy counterpart proto type.
|
|
||||||
func parseCoinSelectionStrategy(ctx *cli.Context) (
|
|
||||||
lnrpc.CoinSelectionStrategy, error) {
|
|
||||||
|
|
||||||
strategy := ctx.String(coinSelectionStrategyFlag.Name)
|
|
||||||
if !ctx.IsSet(coinSelectionStrategyFlag.Name) {
|
|
||||||
return lnrpc.CoinSelectionStrategy_STRATEGY_USE_GLOBAL_CONFIG,
|
|
||||||
nil
|
|
||||||
}
|
|
||||||
|
|
||||||
switch strategy {
|
|
||||||
case "global-config":
|
|
||||||
return lnrpc.CoinSelectionStrategy_STRATEGY_USE_GLOBAL_CONFIG,
|
|
||||||
nil
|
|
||||||
|
|
||||||
case "largest":
|
|
||||||
return lnrpc.CoinSelectionStrategy_STRATEGY_LARGEST, nil
|
|
||||||
|
|
||||||
case "random":
|
|
||||||
return lnrpc.CoinSelectionStrategy_STRATEGY_RANDOM, nil
|
|
||||||
|
|
||||||
default:
|
|
||||||
return 0, fmt.Errorf("unknown coin selection strategy "+
|
|
||||||
"%v", strategy)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
+34
-25
@@ -6,6 +6,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"sync"
|
"sync"
|
||||||
|
"sync/atomic"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/btcsuite/btcd/blockchain"
|
"github.com/btcsuite/btcd/blockchain"
|
||||||
@@ -287,7 +288,7 @@ type InitFundingMsg struct {
|
|||||||
// PendingChanID is not all zeroes (the default value), then this will
|
// PendingChanID is not all zeroes (the default value), then this will
|
||||||
// be the pending channel ID used for the funding flow within the wire
|
// be the pending channel ID used for the funding flow within the wire
|
||||||
// protocol.
|
// protocol.
|
||||||
PendingChanID [32]byte
|
PendingChanID PendingChanID
|
||||||
|
|
||||||
// ChannelType allows the caller to use an explicit channel type for the
|
// ChannelType allows the caller to use an explicit channel type for the
|
||||||
// funding negotiation. This type will only be observed if BOTH sides
|
// funding negotiation. This type will only be observed if BOTH sides
|
||||||
@@ -317,7 +318,7 @@ type fundingMsg struct {
|
|||||||
// pendingChannels is a map instantiated per-peer which tracks all active
|
// pendingChannels is a map instantiated per-peer which tracks all active
|
||||||
// pending single funded channels indexed by their pending channel identifier,
|
// pending single funded channels indexed by their pending channel identifier,
|
||||||
// which is a set of 32-bytes generated via a CSPRNG.
|
// which is a set of 32-bytes generated via a CSPRNG.
|
||||||
type pendingChannels map[[32]byte]*reservationWithCtx
|
type pendingChannels map[PendingChanID]*reservationWithCtx
|
||||||
|
|
||||||
// serializedPubKey is used within the FundingManager's activeReservations list
|
// serializedPubKey is used within the FundingManager's activeReservations list
|
||||||
// to identify the nodes with which the FundingManager is actively working to
|
// to identify the nodes with which the FundingManager is actively working to
|
||||||
@@ -568,8 +569,10 @@ type Manager struct {
|
|||||||
|
|
||||||
// chanIDNonce is a nonce that's incremented for each new funding
|
// chanIDNonce is a nonce that's incremented for each new funding
|
||||||
// reservation created.
|
// reservation created.
|
||||||
nonceMtx sync.RWMutex
|
chanIDNonce atomic.Uint64
|
||||||
chanIDNonce uint64
|
|
||||||
|
// nonceMtx is a mutex that guards the pendingMusigNonces.
|
||||||
|
nonceMtx sync.RWMutex
|
||||||
|
|
||||||
// pendingMusigNonces is used to store the musig2 nonce we generate to
|
// pendingMusigNonces is used to store the musig2 nonce we generate to
|
||||||
// send funding locked until we receive a funding locked message from
|
// send funding locked until we receive a funding locked message from
|
||||||
@@ -591,7 +594,7 @@ type Manager struct {
|
|||||||
// required as mid funding flow, we switch to referencing the channel
|
// required as mid funding flow, we switch to referencing the channel
|
||||||
// by its full channel ID once the commitment transactions have been
|
// by its full channel ID once the commitment transactions have been
|
||||||
// signed by both parties.
|
// signed by both parties.
|
||||||
signedReservations map[lnwire.ChannelID][32]byte
|
signedReservations map[lnwire.ChannelID]PendingChanID
|
||||||
|
|
||||||
// resMtx guards both of the maps above to ensure that all access is
|
// resMtx guards both of the maps above to ensure that all access is
|
||||||
// goroutine safe.
|
// goroutine safe.
|
||||||
@@ -798,24 +801,28 @@ func (f *Manager) rebroadcastFundingTx(c *channeldb.OpenChannel) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// PendingChanID is a type that represents a pending channel ID. This might be
|
||||||
|
// selected by the caller, but if not, will be automatically selected.
|
||||||
|
type PendingChanID = [32]byte
|
||||||
|
|
||||||
// nextPendingChanID returns the next free pending channel ID to be used to
|
// nextPendingChanID returns the next free pending channel ID to be used to
|
||||||
// identify a particular future channel funding workflow.
|
// identify a particular future channel funding workflow.
|
||||||
func (f *Manager) nextPendingChanID() [32]byte {
|
func (f *Manager) nextPendingChanID() PendingChanID {
|
||||||
// Obtain a fresh nonce. We do this by encoding the current nonce
|
// Obtain a fresh nonce. We do this by encoding the incremented nonce.
|
||||||
// counter, then incrementing it by one.
|
nextNonce := f.chanIDNonce.Add(1)
|
||||||
f.nonceMtx.Lock()
|
|
||||||
var nonce [8]byte
|
var nonceBytes [8]byte
|
||||||
binary.LittleEndian.PutUint64(nonce[:], f.chanIDNonce)
|
binary.LittleEndian.PutUint64(nonceBytes[:], nextNonce)
|
||||||
f.chanIDNonce++
|
|
||||||
f.nonceMtx.Unlock()
|
|
||||||
|
|
||||||
// We'll generate the next pending channelID by "encrypting" 32-bytes
|
// We'll generate the next pending channelID by "encrypting" 32-bytes
|
||||||
// of zeroes which'll extract 32 random bytes from our stream cipher.
|
// of zeroes which'll extract 32 random bytes from our stream cipher.
|
||||||
var (
|
var (
|
||||||
nextChanID [32]byte
|
nextChanID PendingChanID
|
||||||
zeroes [32]byte
|
zeroes [32]byte
|
||||||
)
|
)
|
||||||
salsa20.XORKeyStream(nextChanID[:], zeroes[:], nonce[:], &f.chanIDKey)
|
salsa20.XORKeyStream(
|
||||||
|
nextChanID[:], zeroes[:], nonceBytes[:], &f.chanIDKey,
|
||||||
|
)
|
||||||
|
|
||||||
return nextChanID
|
return nextChanID
|
||||||
}
|
}
|
||||||
@@ -1045,7 +1052,8 @@ func (f *Manager) reservationCoordinator() {
|
|||||||
//
|
//
|
||||||
// NOTE: This MUST be run as a goroutine.
|
// NOTE: This MUST be run as a goroutine.
|
||||||
func (f *Manager) advanceFundingState(channel *channeldb.OpenChannel,
|
func (f *Manager) advanceFundingState(channel *channeldb.OpenChannel,
|
||||||
pendingChanID [32]byte, updateChan chan<- *lnrpc.OpenStatusUpdate) {
|
pendingChanID PendingChanID,
|
||||||
|
updateChan chan<- *lnrpc.OpenStatusUpdate) {
|
||||||
|
|
||||||
defer f.wg.Done()
|
defer f.wg.Done()
|
||||||
|
|
||||||
@@ -1115,7 +1123,7 @@ func (f *Manager) advanceFundingState(channel *channeldb.OpenChannel,
|
|||||||
// updateChan can be set non-nil to get OpenStatusUpdates.
|
// updateChan can be set non-nil to get OpenStatusUpdates.
|
||||||
func (f *Manager) stateStep(channel *channeldb.OpenChannel,
|
func (f *Manager) stateStep(channel *channeldb.OpenChannel,
|
||||||
lnChannel *lnwallet.LightningChannel,
|
lnChannel *lnwallet.LightningChannel,
|
||||||
shortChanID *lnwire.ShortChannelID, pendingChanID [32]byte,
|
shortChanID *lnwire.ShortChannelID, pendingChanID PendingChanID,
|
||||||
channelState channelOpeningState,
|
channelState channelOpeningState,
|
||||||
updateChan chan<- *lnrpc.OpenStatusUpdate) error {
|
updateChan chan<- *lnrpc.OpenStatusUpdate) error {
|
||||||
|
|
||||||
@@ -1239,7 +1247,7 @@ func (f *Manager) stateStep(channel *channeldb.OpenChannel,
|
|||||||
// advancePendingChannelState waits for a pending channel's funding tx to
|
// advancePendingChannelState waits for a pending channel's funding tx to
|
||||||
// confirm, and marks it open in the database when that happens.
|
// confirm, and marks it open in the database when that happens.
|
||||||
func (f *Manager) advancePendingChannelState(
|
func (f *Manager) advancePendingChannelState(
|
||||||
channel *channeldb.OpenChannel, pendingChanID [32]byte) error {
|
channel *channeldb.OpenChannel, pendingChanID PendingChanID) error {
|
||||||
|
|
||||||
if channel.IsZeroConf() {
|
if channel.IsZeroConf() {
|
||||||
// Persist the alias to the alias database.
|
// Persist the alias to the alias database.
|
||||||
@@ -2770,7 +2778,7 @@ type confirmedChannel struct {
|
|||||||
// channel as closed. The error is only returned for the responder of the
|
// channel as closed. The error is only returned for the responder of the
|
||||||
// channel flow.
|
// channel flow.
|
||||||
func (f *Manager) fundingTimeout(c *channeldb.OpenChannel,
|
func (f *Manager) fundingTimeout(c *channeldb.OpenChannel,
|
||||||
pendingID [32]byte) error {
|
pendingID PendingChanID) error {
|
||||||
|
|
||||||
// We'll get a timeout if the number of blocks mined since the channel
|
// We'll get a timeout if the number of blocks mined since the channel
|
||||||
// was initiated reaches MaxWaitNumBlocksFundingConf and we are not the
|
// was initiated reaches MaxWaitNumBlocksFundingConf and we are not the
|
||||||
@@ -3995,7 +4003,7 @@ func (f *Manager) handleChannelReady(peer lnpeer.Peer, //nolint:funlen
|
|||||||
// channel is now active, thus we change its state to `addedToGraph` to
|
// channel is now active, thus we change its state to `addedToGraph` to
|
||||||
// let the channel start handling routing.
|
// let the channel start handling routing.
|
||||||
func (f *Manager) handleChannelReadyReceived(channel *channeldb.OpenChannel,
|
func (f *Manager) handleChannelReadyReceived(channel *channeldb.OpenChannel,
|
||||||
scid *lnwire.ShortChannelID, pendingChanID [32]byte,
|
scid *lnwire.ShortChannelID, pendingChanID PendingChanID,
|
||||||
updateChan chan<- *lnrpc.OpenStatusUpdate) error {
|
updateChan chan<- *lnrpc.OpenStatusUpdate) error {
|
||||||
|
|
||||||
chanID := lnwire.NewChanIDFromOutPoint(channel.FundingOutpoint)
|
chanID := lnwire.NewChanIDFromOutPoint(channel.FundingOutpoint)
|
||||||
@@ -4519,7 +4527,7 @@ func (f *Manager) handleInitFundingMsg(msg *InitFundingMsg) {
|
|||||||
// If the caller specified their own channel ID, then we'll use that.
|
// If the caller specified their own channel ID, then we'll use that.
|
||||||
// Otherwise we'll generate a fresh one as normal. This will be used
|
// Otherwise we'll generate a fresh one as normal. This will be used
|
||||||
// to track this reservation throughout its lifetime.
|
// to track this reservation throughout its lifetime.
|
||||||
var chanID [32]byte
|
var chanID PendingChanID
|
||||||
if msg.PendingChanID == zeroID {
|
if msg.PendingChanID == zeroID {
|
||||||
chanID = f.nextPendingChanID()
|
chanID = f.nextPendingChanID()
|
||||||
} else {
|
} else {
|
||||||
@@ -4942,7 +4950,8 @@ func (f *Manager) pruneZombieReservations() {
|
|||||||
// cancelReservationCtx does all needed work in order to securely cancel the
|
// cancelReservationCtx does all needed work in order to securely cancel the
|
||||||
// reservation.
|
// reservation.
|
||||||
func (f *Manager) cancelReservationCtx(peerKey *btcec.PublicKey,
|
func (f *Manager) cancelReservationCtx(peerKey *btcec.PublicKey,
|
||||||
pendingChanID [32]byte, byRemote bool) (*reservationWithCtx, error) {
|
pendingChanID PendingChanID,
|
||||||
|
byRemote bool) (*reservationWithCtx, error) {
|
||||||
|
|
||||||
log.Infof("Cancelling funding reservation for node_key=%x, "+
|
log.Infof("Cancelling funding reservation for node_key=%x, "+
|
||||||
"chan_id=%x", peerKey.SerializeCompressed(), pendingChanID[:])
|
"chan_id=%x", peerKey.SerializeCompressed(), pendingChanID[:])
|
||||||
@@ -4990,7 +4999,7 @@ func (f *Manager) cancelReservationCtx(peerKey *btcec.PublicKey,
|
|||||||
// deleteReservationCtx deletes the reservation uniquely identified by the
|
// deleteReservationCtx deletes the reservation uniquely identified by the
|
||||||
// target public key of the peer, and the specified pending channel ID.
|
// target public key of the peer, and the specified pending channel ID.
|
||||||
func (f *Manager) deleteReservationCtx(peerKey *btcec.PublicKey,
|
func (f *Manager) deleteReservationCtx(peerKey *btcec.PublicKey,
|
||||||
pendingChanID [32]byte) {
|
pendingChanID PendingChanID) {
|
||||||
|
|
||||||
peerIDKey := newSerializedKey(peerKey)
|
peerIDKey := newSerializedKey(peerKey)
|
||||||
f.resMtx.Lock()
|
f.resMtx.Lock()
|
||||||
@@ -5013,7 +5022,7 @@ func (f *Manager) deleteReservationCtx(peerKey *btcec.PublicKey,
|
|||||||
// getReservationCtx returns the reservation context for a particular pending
|
// getReservationCtx returns the reservation context for a particular pending
|
||||||
// channel ID for a target peer.
|
// channel ID for a target peer.
|
||||||
func (f *Manager) getReservationCtx(peerKey *btcec.PublicKey,
|
func (f *Manager) getReservationCtx(peerKey *btcec.PublicKey,
|
||||||
pendingChanID [32]byte) (*reservationWithCtx, error) {
|
pendingChanID PendingChanID) (*reservationWithCtx, error) {
|
||||||
|
|
||||||
peerIDKey := newSerializedKey(peerKey)
|
peerIDKey := newSerializedKey(peerKey)
|
||||||
f.resMtx.RLock()
|
f.resMtx.RLock()
|
||||||
@@ -5033,7 +5042,7 @@ func (f *Manager) getReservationCtx(peerKey *btcec.PublicKey,
|
|||||||
// of being funded. After the funding transaction has been confirmed, the
|
// of being funded. After the funding transaction has been confirmed, the
|
||||||
// channel will receive a new, permanent channel ID, and will no longer be
|
// channel will receive a new, permanent channel ID, and will no longer be
|
||||||
// considered pending.
|
// considered pending.
|
||||||
func (f *Manager) IsPendingChannel(pendingChanID [32]byte,
|
func (f *Manager) IsPendingChannel(pendingChanID PendingChanID,
|
||||||
peer lnpeer.Peer) bool {
|
peer lnpeer.Peer) bool {
|
||||||
|
|
||||||
peerIDKey := newSerializedKey(peer.IdentityKey())
|
peerIDKey := newSerializedKey(peer.IdentityKey())
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ require (
|
|||||||
github.com/lightningnetwork/lnd/queue v1.1.1
|
github.com/lightningnetwork/lnd/queue v1.1.1
|
||||||
github.com/lightningnetwork/lnd/sqldb v1.0.3
|
github.com/lightningnetwork/lnd/sqldb v1.0.3
|
||||||
github.com/lightningnetwork/lnd/ticker v1.1.1
|
github.com/lightningnetwork/lnd/ticker v1.1.1
|
||||||
github.com/lightningnetwork/lnd/tlv v1.2.3
|
github.com/lightningnetwork/lnd/tlv v1.2.6
|
||||||
github.com/lightningnetwork/lnd/tor v1.1.2
|
github.com/lightningnetwork/lnd/tor v1.1.2
|
||||||
github.com/ltcsuite/ltcd v0.0.0-20190101042124-f37f8bf35796
|
github.com/ltcsuite/ltcd v0.0.0-20190101042124-f37f8bf35796
|
||||||
github.com/miekg/dns v1.1.43
|
github.com/miekg/dns v1.1.43
|
||||||
|
|||||||
@@ -462,8 +462,8 @@ github.com/lightningnetwork/lnd/sqldb v1.0.3 h1:zLfAwOvM+6+3+hahYO9Q3h8pVV0TghAR
|
|||||||
github.com/lightningnetwork/lnd/sqldb v1.0.3/go.mod h1:4cQOkdymlZ1znnjuRNvMoatQGJkRneTj2CoPSPaQhWo=
|
github.com/lightningnetwork/lnd/sqldb v1.0.3/go.mod h1:4cQOkdymlZ1znnjuRNvMoatQGJkRneTj2CoPSPaQhWo=
|
||||||
github.com/lightningnetwork/lnd/ticker v1.1.1 h1:J/b6N2hibFtC7JLV77ULQp++QLtCwT6ijJlbdiZFbSM=
|
github.com/lightningnetwork/lnd/ticker v1.1.1 h1:J/b6N2hibFtC7JLV77ULQp++QLtCwT6ijJlbdiZFbSM=
|
||||||
github.com/lightningnetwork/lnd/ticker v1.1.1/go.mod h1:waPTRAAcwtu7Ji3+3k+u/xH5GHovTsCoSVpho0KDvdA=
|
github.com/lightningnetwork/lnd/ticker v1.1.1/go.mod h1:waPTRAAcwtu7Ji3+3k+u/xH5GHovTsCoSVpho0KDvdA=
|
||||||
github.com/lightningnetwork/lnd/tlv v1.2.3 h1:If5ibokA/UoCBGuCKaY6Vn2SJU0l9uAbehCnhTZjEP8=
|
github.com/lightningnetwork/lnd/tlv v1.2.6 h1:icvQG2yDr6k3ZuZzfRdG3EJp6pHurcuh3R6dg0gv/Mw=
|
||||||
github.com/lightningnetwork/lnd/tlv v1.2.3/go.mod h1:zDkmqxOczP6LaLTvSFDQ1SJUfHcQRCMKFj93dn3eMB8=
|
github.com/lightningnetwork/lnd/tlv v1.2.6/go.mod h1:/CmY4VbItpOldksocmGT4lxiJqRP9oLxwSZOda2kzNQ=
|
||||||
github.com/lightningnetwork/lnd/tor v1.1.2 h1:3zv9z/EivNFaMF89v3ciBjCS7kvCj4ZFG7XvD2Qq0/k=
|
github.com/lightningnetwork/lnd/tor v1.1.2 h1:3zv9z/EivNFaMF89v3ciBjCS7kvCj4ZFG7XvD2Qq0/k=
|
||||||
github.com/lightningnetwork/lnd/tor v1.1.2/go.mod h1:j7T9uJ2NLMaHwE7GiBGnpYLn4f7NRoTM6qj+ul6/ycA=
|
github.com/lightningnetwork/lnd/tor v1.1.2/go.mod h1:j7T9uJ2NLMaHwE7GiBGnpYLn4f7NRoTM6qj+ul6/ycA=
|
||||||
github.com/ltcsuite/ltcd v0.0.0-20190101042124-f37f8bf35796 h1:sjOGyegMIhvgfq5oaue6Td+hxZuf3tDC8lAPrFldqFw=
|
github.com/ltcsuite/ltcd v0.0.0-20190101042124-f37f8bf35796 h1:sjOGyegMIhvgfq5oaue6Td+hxZuf3tDC8lAPrFldqFw=
|
||||||
|
|||||||
@@ -387,6 +387,8 @@ func (s *InterceptableSwitch) setInterceptor(interceptor ForwardInterceptor) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// resolve processes a HTLC given the resolution type specified by the
|
||||||
|
// intercepting client.
|
||||||
func (s *InterceptableSwitch) resolve(res *FwdResolution) error {
|
func (s *InterceptableSwitch) resolve(res *FwdResolution) error {
|
||||||
intercepted, err := s.heldHtlcSet.pop(res.Key)
|
intercepted, err := s.heldHtlcSet.pop(res.Key)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -331,7 +331,7 @@ type InterceptableHtlcForwarder interface {
|
|||||||
type ForwardInterceptor func(InterceptedPacket) error
|
type ForwardInterceptor func(InterceptedPacket) error
|
||||||
|
|
||||||
// InterceptedPacket contains the relevant information for the interceptor about
|
// InterceptedPacket contains the relevant information for the interceptor about
|
||||||
// an htlc.
|
// an HTLC.
|
||||||
type InterceptedPacket struct {
|
type InterceptedPacket struct {
|
||||||
// IncomingCircuit contains the incoming channel and htlc id of the
|
// IncomingCircuit contains the incoming channel and htlc id of the
|
||||||
// packet.
|
// packet.
|
||||||
|
|||||||
@@ -33,17 +33,17 @@ const (
|
|||||||
ScriptPathDelay
|
ScriptPathDelay
|
||||||
)
|
)
|
||||||
|
|
||||||
// ScriptDesciptor is an interface that abstracts over the various ways a
|
// ScriptDescriptor is an interface that abstracts over the various ways a
|
||||||
// pkScript can be spent from an output. This supports both normal p2wsh
|
// pkScript can be spent from an output. This supports both normal p2wsh
|
||||||
// (witness script, etc), and also tapscript paths which have distinct
|
// (witness script, etc.), and also tapscript paths which have distinct
|
||||||
// tapscript leaves.
|
// tapscript leaves.
|
||||||
type ScriptDescriptor interface {
|
type ScriptDescriptor interface {
|
||||||
// PkScript is the public key script that commits to the final
|
// PkScript is the public key script that commits to the final
|
||||||
// contract.
|
// contract.
|
||||||
PkScript() []byte
|
PkScript() []byte
|
||||||
|
|
||||||
// WitnessScript returns the witness script that we'll use when signing
|
// WitnessScriptToSign returns the witness script that we'll use when
|
||||||
// for the remote party, and also verifying signatures on our
|
// signing for the remote party, and also verifying signatures on our
|
||||||
// transactions. As an example, when we create an outgoing HTLC for the
|
// transactions. As an example, when we create an outgoing HTLC for the
|
||||||
// remote party, we want to sign their success path.
|
// remote party, we want to sign their success path.
|
||||||
//
|
//
|
||||||
@@ -73,6 +73,9 @@ type TapscriptDescriptor interface {
|
|||||||
|
|
||||||
// TapScriptTree returns the underlying tapscript tree.
|
// TapScriptTree returns the underlying tapscript tree.
|
||||||
TapScriptTree() *txscript.IndexedTapScriptTree
|
TapScriptTree() *txscript.IndexedTapScriptTree
|
||||||
|
|
||||||
|
// Tree returns the underlying ScriptTree.
|
||||||
|
Tree() ScriptTree
|
||||||
}
|
}
|
||||||
|
|
||||||
// ScriptTree holds the contents needed to spend a script within a tapscript
|
// ScriptTree holds the contents needed to spend a script within a tapscript
|
||||||
|
|||||||
+46
-29
@@ -689,8 +689,8 @@ func (h *HtlcScriptTree) WitnessScriptForPath(path ScriptPath) ([]byte, error) {
|
|||||||
|
|
||||||
// CtrlBlockForPath returns the control block for the given spending path. For
|
// CtrlBlockForPath returns the control block for the given spending path. For
|
||||||
// script types that don't have a control block, nil is returned.
|
// script types that don't have a control block, nil is returned.
|
||||||
func (h *HtlcScriptTree) CtrlBlockForPath(path ScriptPath,
|
func (h *HtlcScriptTree) CtrlBlockForPath(
|
||||||
) (*txscript.ControlBlock, error) {
|
path ScriptPath) (*txscript.ControlBlock, error) {
|
||||||
|
|
||||||
switch path {
|
switch path {
|
||||||
case ScriptPathSuccess:
|
case ScriptPathSuccess:
|
||||||
@@ -708,6 +708,11 @@ func (h *HtlcScriptTree) CtrlBlockForPath(path ScriptPath,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Tree returns the underlying ScriptTree of the HtlcScriptTree.
|
||||||
|
func (h *HtlcScriptTree) Tree() ScriptTree {
|
||||||
|
return h.ScriptTree
|
||||||
|
}
|
||||||
|
|
||||||
// A compile time check to ensure HtlcScriptTree implements the
|
// A compile time check to ensure HtlcScriptTree implements the
|
||||||
// TapscriptMultiplexer interface.
|
// TapscriptMultiplexer interface.
|
||||||
var _ TapscriptDescriptor = (*HtlcScriptTree)(nil)
|
var _ TapscriptDescriptor = (*HtlcScriptTree)(nil)
|
||||||
@@ -1690,9 +1695,9 @@ func TaprootSecondLevelScriptTree(revokeKey, delayKey *btcec.PublicKey,
|
|||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// WitnessScript returns the witness script that we'll use when signing for the
|
// WitnessScriptToSign returns the witness script that we'll use when signing
|
||||||
// remote party, and also verifying signatures on our transactions. As an
|
// for the remote party, and also verifying signatures on our transactions. As
|
||||||
// example, when we create an outgoing HTLC for the remote party, we want to
|
// an example, when we create an outgoing HTLC for the remote party, we want to
|
||||||
// sign their success path.
|
// sign their success path.
|
||||||
func (s *SecondLevelScriptTree) WitnessScriptToSign() []byte {
|
func (s *SecondLevelScriptTree) WitnessScriptToSign() []byte {
|
||||||
return s.SuccessTapLeaf.Script
|
return s.SuccessTapLeaf.Script
|
||||||
@@ -1700,8 +1705,8 @@ func (s *SecondLevelScriptTree) WitnessScriptToSign() []byte {
|
|||||||
|
|
||||||
// WitnessScriptForPath returns the witness script for the given spending path.
|
// WitnessScriptForPath returns the witness script for the given spending path.
|
||||||
// An error is returned if the path is unknown.
|
// An error is returned if the path is unknown.
|
||||||
func (s *SecondLevelScriptTree) WitnessScriptForPath(path ScriptPath,
|
func (s *SecondLevelScriptTree) WitnessScriptForPath(
|
||||||
) ([]byte, error) {
|
path ScriptPath) ([]byte, error) {
|
||||||
|
|
||||||
switch path {
|
switch path {
|
||||||
case ScriptPathDelay:
|
case ScriptPathDelay:
|
||||||
@@ -1716,8 +1721,8 @@ func (s *SecondLevelScriptTree) WitnessScriptForPath(path ScriptPath,
|
|||||||
|
|
||||||
// CtrlBlockForPath returns the control block for the given spending path. For
|
// CtrlBlockForPath returns the control block for the given spending path. For
|
||||||
// script types that don't have a control block, nil is returned.
|
// script types that don't have a control block, nil is returned.
|
||||||
func (s *SecondLevelScriptTree) CtrlBlockForPath(path ScriptPath,
|
func (s *SecondLevelScriptTree) CtrlBlockForPath(
|
||||||
) (*txscript.ControlBlock, error) {
|
path ScriptPath) (*txscript.ControlBlock, error) {
|
||||||
|
|
||||||
switch path {
|
switch path {
|
||||||
case ScriptPathDelay:
|
case ScriptPathDelay:
|
||||||
@@ -1733,6 +1738,11 @@ func (s *SecondLevelScriptTree) CtrlBlockForPath(path ScriptPath,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Tree returns the underlying ScriptTree of the SecondLevelScriptTree.
|
||||||
|
func (s *SecondLevelScriptTree) Tree() ScriptTree {
|
||||||
|
return s.ScriptTree
|
||||||
|
}
|
||||||
|
|
||||||
// A compile time check to ensure SecondLevelScriptTree implements the
|
// A compile time check to ensure SecondLevelScriptTree implements the
|
||||||
// TapscriptDescriptor interface.
|
// TapscriptDescriptor interface.
|
||||||
var _ TapscriptDescriptor = (*SecondLevelScriptTree)(nil)
|
var _ TapscriptDescriptor = (*SecondLevelScriptTree)(nil)
|
||||||
@@ -2069,9 +2079,9 @@ type CommitScriptTree struct {
|
|||||||
// TapscriptDescriptor interface.
|
// TapscriptDescriptor interface.
|
||||||
var _ TapscriptDescriptor = (*CommitScriptTree)(nil)
|
var _ TapscriptDescriptor = (*CommitScriptTree)(nil)
|
||||||
|
|
||||||
// WitnessScript returns the witness script that we'll use when signing for the
|
// WitnessScriptToSign returns the witness script that we'll use when signing
|
||||||
// remote party, and also verifying signatures on our transactions. As an
|
// for the remote party, and also verifying signatures on our transactions. As
|
||||||
// example, when we create an outgoing HTLC for the remote party, we want to
|
// an example, when we create an outgoing HTLC for the remote party, we want to
|
||||||
// sign their success path.
|
// sign their success path.
|
||||||
func (c *CommitScriptTree) WitnessScriptToSign() []byte {
|
func (c *CommitScriptTree) WitnessScriptToSign() []byte {
|
||||||
// TODO(roasbeef): abstraction leak here? always dependent
|
// TODO(roasbeef): abstraction leak here? always dependent
|
||||||
@@ -2080,8 +2090,8 @@ func (c *CommitScriptTree) WitnessScriptToSign() []byte {
|
|||||||
|
|
||||||
// WitnessScriptForPath returns the witness script for the given spending path.
|
// WitnessScriptForPath returns the witness script for the given spending path.
|
||||||
// An error is returned if the path is unknown.
|
// An error is returned if the path is unknown.
|
||||||
func (c *CommitScriptTree) WitnessScriptForPath(path ScriptPath,
|
func (c *CommitScriptTree) WitnessScriptForPath(
|
||||||
) ([]byte, error) {
|
path ScriptPath) ([]byte, error) {
|
||||||
|
|
||||||
switch path {
|
switch path {
|
||||||
// For the commitment output, the delay and success path are the same,
|
// For the commitment output, the delay and success path are the same,
|
||||||
@@ -2099,8 +2109,8 @@ func (c *CommitScriptTree) WitnessScriptForPath(path ScriptPath,
|
|||||||
|
|
||||||
// CtrlBlockForPath returns the control block for the given spending path. For
|
// CtrlBlockForPath returns the control block for the given spending path. For
|
||||||
// script types that don't have a control block, nil is returned.
|
// script types that don't have a control block, nil is returned.
|
||||||
func (c *CommitScriptTree) CtrlBlockForPath(path ScriptPath,
|
func (c *CommitScriptTree) CtrlBlockForPath(
|
||||||
) (*txscript.ControlBlock, error) {
|
path ScriptPath) (*txscript.ControlBlock, error) {
|
||||||
|
|
||||||
switch path {
|
switch path {
|
||||||
case ScriptPathDelay:
|
case ScriptPathDelay:
|
||||||
@@ -2120,6 +2130,11 @@ func (c *CommitScriptTree) CtrlBlockForPath(path ScriptPath,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Tree returns the underlying ScriptTree of the CommitScriptTree.
|
||||||
|
func (c *CommitScriptTree) Tree() ScriptTree {
|
||||||
|
return c.ScriptTree
|
||||||
|
}
|
||||||
|
|
||||||
// NewLocalCommitScriptTree returns a new CommitScript tree that can be used to
|
// NewLocalCommitScriptTree returns a new CommitScript tree that can be used to
|
||||||
// create and spend the commitment output for the local party.
|
// create and spend the commitment output for the local party.
|
||||||
func NewLocalCommitScriptTree(csvTimeout uint32,
|
func NewLocalCommitScriptTree(csvTimeout uint32,
|
||||||
@@ -2241,7 +2256,7 @@ func TaprootCommitScriptToSelf(csvTimeout uint32,
|
|||||||
return commitScriptTree.TaprootKey, nil
|
return commitScriptTree.TaprootKey, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// MakeTaprootSCtrlBlock takes a leaf script, the internal key (usually the
|
// MakeTaprootCtrlBlock takes a leaf script, the internal key (usually the
|
||||||
// revoke key), and a script tree and creates a valid control block for a spend
|
// revoke key), and a script tree and creates a valid control block for a spend
|
||||||
// of the leaf.
|
// of the leaf.
|
||||||
func MakeTaprootCtrlBlock(leafScript []byte, internalKey *btcec.PublicKey,
|
func MakeTaprootCtrlBlock(leafScript []byte, internalKey *btcec.PublicKey,
|
||||||
@@ -2296,9 +2311,6 @@ func TaprootCommitSpendSuccess(signer Signer, signDesc *SignDescriptor,
|
|||||||
witnessStack[0] = maybeAppendSighash(sweepSig, signDesc.HashType)
|
witnessStack[0] = maybeAppendSighash(sweepSig, signDesc.HashType)
|
||||||
witnessStack[1] = signDesc.WitnessScript
|
witnessStack[1] = signDesc.WitnessScript
|
||||||
witnessStack[2] = ctrlBlockBytes
|
witnessStack[2] = ctrlBlockBytes
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return witnessStack, nil
|
return witnessStack, nil
|
||||||
}
|
}
|
||||||
@@ -2773,8 +2785,8 @@ type AnchorScriptTree struct {
|
|||||||
|
|
||||||
// NewAnchorScriptTree makes a new script tree for an anchor output with the
|
// NewAnchorScriptTree makes a new script tree for an anchor output with the
|
||||||
// passed anchor key.
|
// passed anchor key.
|
||||||
func NewAnchorScriptTree(anchorKey *btcec.PublicKey,
|
func NewAnchorScriptTree(
|
||||||
) (*AnchorScriptTree, error) {
|
anchorKey *btcec.PublicKey) (*AnchorScriptTree, error) {
|
||||||
|
|
||||||
// The main script used is just a OP_16 CSV (anyone can sweep after 16
|
// The main script used is just a OP_16 CSV (anyone can sweep after 16
|
||||||
// blocks).
|
// blocks).
|
||||||
@@ -2810,9 +2822,9 @@ func NewAnchorScriptTree(anchorKey *btcec.PublicKey,
|
|||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// WitnessScript returns the witness script that we'll use when signing for the
|
// WitnessScriptToSign returns the witness script that we'll use when signing
|
||||||
// remote party, and also verifying signatures on our transactions. As an
|
// for the remote party, and also verifying signatures on our transactions. As
|
||||||
// example, when we create an outgoing HTLC for the remote party, we want to
|
// an example, when we create an outgoing HTLC for the remote party, we want to
|
||||||
// sign their success path.
|
// sign their success path.
|
||||||
func (a *AnchorScriptTree) WitnessScriptToSign() []byte {
|
func (a *AnchorScriptTree) WitnessScriptToSign() []byte {
|
||||||
return a.SweepLeaf.Script
|
return a.SweepLeaf.Script
|
||||||
@@ -2820,8 +2832,8 @@ func (a *AnchorScriptTree) WitnessScriptToSign() []byte {
|
|||||||
|
|
||||||
// WitnessScriptForPath returns the witness script for the given spending path.
|
// WitnessScriptForPath returns the witness script for the given spending path.
|
||||||
// An error is returned if the path is unknown.
|
// An error is returned if the path is unknown.
|
||||||
func (a *AnchorScriptTree) WitnessScriptForPath(path ScriptPath,
|
func (a *AnchorScriptTree) WitnessScriptForPath(
|
||||||
) ([]byte, error) {
|
path ScriptPath) ([]byte, error) {
|
||||||
|
|
||||||
switch path {
|
switch path {
|
||||||
case ScriptPathDelay:
|
case ScriptPathDelay:
|
||||||
@@ -2836,8 +2848,8 @@ func (a *AnchorScriptTree) WitnessScriptForPath(path ScriptPath,
|
|||||||
|
|
||||||
// CtrlBlockForPath returns the control block for the given spending path. For
|
// CtrlBlockForPath returns the control block for the given spending path. For
|
||||||
// script types that don't have a control block, nil is returned.
|
// script types that don't have a control block, nil is returned.
|
||||||
func (a *AnchorScriptTree) CtrlBlockForPath(path ScriptPath,
|
func (a *AnchorScriptTree) CtrlBlockForPath(
|
||||||
) (*txscript.ControlBlock, error) {
|
path ScriptPath) (*txscript.ControlBlock, error) {
|
||||||
|
|
||||||
switch path {
|
switch path {
|
||||||
case ScriptPathDelay:
|
case ScriptPathDelay:
|
||||||
@@ -2853,6 +2865,11 @@ func (a *AnchorScriptTree) CtrlBlockForPath(path ScriptPath,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Tree returns the underlying ScriptTree of the AnchorScriptTree.
|
||||||
|
func (a *AnchorScriptTree) Tree() ScriptTree {
|
||||||
|
return a.ScriptTree
|
||||||
|
}
|
||||||
|
|
||||||
// A compile time check to ensure AnchorScriptTree implements the
|
// A compile time check to ensure AnchorScriptTree implements the
|
||||||
// TapscriptDescriptor interface.
|
// TapscriptDescriptor interface.
|
||||||
var _ TapscriptDescriptor = (*AnchorScriptTree)(nil)
|
var _ TapscriptDescriptor = (*AnchorScriptTree)(nil)
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ var (
|
|||||||
ErrMissingPreimage = errors.New("missing preimage")
|
ErrMissingPreimage = errors.New("missing preimage")
|
||||||
)
|
)
|
||||||
|
|
||||||
// forwardInterceptor is a helper struct that handles the lifecycle of an rpc
|
// forwardInterceptor is a helper struct that handles the lifecycle of an RPC
|
||||||
// interceptor streaming session.
|
// interceptor streaming session.
|
||||||
// It is created when the stream opens and disconnects when the stream closes.
|
// It is created when the stream opens and disconnects when the stream closes.
|
||||||
type forwardInterceptor struct {
|
type forwardInterceptor struct {
|
||||||
@@ -43,7 +43,7 @@ func newForwardInterceptor(htlcSwitch htlcswitch.InterceptableHtlcForwarder,
|
|||||||
}
|
}
|
||||||
|
|
||||||
// run sends the intercepted packets to the client and receives the
|
// run sends the intercepted packets to the client and receives the
|
||||||
// corersponding responses. On one hand it registered itself as an interceptor
|
// corresponding responses. On one hand it registered itself as an interceptor
|
||||||
// that receives the switch packets and on the other hand launches a go routine
|
// that receives the switch packets and on the other hand launches a go routine
|
||||||
// to read from the client stream.
|
// to read from the client stream.
|
||||||
// To coordinate all this and make sure it is safe for concurrent access all
|
// To coordinate all this and make sure it is safe for concurrent access all
|
||||||
|
|||||||
@@ -1313,7 +1313,7 @@ func (s *Server) trackPayment(subscription routing.ControlTowerSubscriber,
|
|||||||
|
|
||||||
// Otherwise, we will log and return the error as the stream has
|
// Otherwise, we will log and return the error as the stream has
|
||||||
// received an error from the payment lifecycle.
|
// received an error from the payment lifecycle.
|
||||||
log.Errorf("TrackPayment got error for payment %x: %v", identifier, err)
|
log.Errorf("TrackPayment got error for payment %v: %v", identifier, err)
|
||||||
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -1525,7 +1525,7 @@ func (s *Server) HtlcInterceptor(stream Router_HtlcInterceptorServer) error {
|
|||||||
}
|
}
|
||||||
defer atomic.CompareAndSwapInt32(&s.forwardInterceptorActive, 1, 0)
|
defer atomic.CompareAndSwapInt32(&s.forwardInterceptorActive, 1, 0)
|
||||||
|
|
||||||
// run the forward interceptor.
|
// Run the forward interceptor.
|
||||||
return newForwardInterceptor(
|
return newForwardInterceptor(
|
||||||
s.cfg.RouterBackend.InterceptableForwarder, stream,
|
s.cfg.RouterBackend.InterceptableForwarder, stream,
|
||||||
).run()
|
).run()
|
||||||
|
|||||||
@@ -4,9 +4,11 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/btcsuite/btcd/btcutil"
|
"github.com/btcsuite/btcd/btcutil"
|
||||||
|
"github.com/btcsuite/btcd/chaincfg/chainhash"
|
||||||
"github.com/btcsuite/btcd/wire"
|
"github.com/btcsuite/btcd/wire"
|
||||||
"github.com/btcsuite/btcwallet/wallet"
|
"github.com/btcsuite/btcwallet/wallet"
|
||||||
"github.com/btcsuite/btcwallet/wtxmgr"
|
"github.com/btcsuite/btcwallet/wtxmgr"
|
||||||
|
"github.com/lightningnetwork/lnd/fn"
|
||||||
"github.com/lightningnetwork/lnd/lnwallet/chainfee"
|
"github.com/lightningnetwork/lnd/lnwallet/chainfee"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -119,6 +121,11 @@ type Request struct {
|
|||||||
// output. By definition, this'll also use segwit v1 (taproot) for the
|
// output. By definition, this'll also use segwit v1 (taproot) for the
|
||||||
// funding output.
|
// funding output.
|
||||||
Musig2 bool
|
Musig2 bool
|
||||||
|
|
||||||
|
// TapscriptRoot is the root of the tapscript tree that will be used to
|
||||||
|
// create the funding output. This field will only be utilized if the
|
||||||
|
// Musig2 flag above is set to true.
|
||||||
|
TapscriptRoot fn.Option[chainhash.Hash]
|
||||||
}
|
}
|
||||||
|
|
||||||
// Intent is returned by an Assembler and represents the base functionality the
|
// Intent is returned by an Assembler and represents the base functionality the
|
||||||
+4
-4
@@ -7694,7 +7694,7 @@ func NewAnchorResolution(chanState *channeldb.OpenChannel,
|
|||||||
WitnessScript: anchorWitnessScript,
|
WitnessScript: anchorWitnessScript,
|
||||||
Output: &wire.TxOut{
|
Output: &wire.TxOut{
|
||||||
PkScript: localAnchor.PkScript(),
|
PkScript: localAnchor.PkScript(),
|
||||||
Value: int64(anchorSize),
|
Value: int64(AnchorSize),
|
||||||
},
|
},
|
||||||
HashType: sweepSigHash(chanState.ChanType),
|
HashType: sweepSigHash(chanState.ChanType),
|
||||||
}
|
}
|
||||||
@@ -7706,7 +7706,7 @@ func NewAnchorResolution(chanState *channeldb.OpenChannel,
|
|||||||
|
|
||||||
//nolint:lll
|
//nolint:lll
|
||||||
signDesc.PrevOutputFetcher = txscript.NewCannedPrevOutputFetcher(
|
signDesc.PrevOutputFetcher = txscript.NewCannedPrevOutputFetcher(
|
||||||
localAnchor.PkScript(), int64(anchorSize),
|
localAnchor.PkScript(), int64(AnchorSize),
|
||||||
)
|
)
|
||||||
|
|
||||||
// For anchor outputs with taproot channels, the key desc is
|
// For anchor outputs with taproot channels, the key desc is
|
||||||
@@ -8249,7 +8249,7 @@ func (lc *LightningChannel) LocalBalanceDust() bool {
|
|||||||
// regain the stats allocated to the anchor outputs with the co-op
|
// regain the stats allocated to the anchor outputs with the co-op
|
||||||
// close transaction.
|
// close transaction.
|
||||||
if chanState.ChanType.HasAnchors() && chanState.IsInitiator {
|
if chanState.ChanType.HasAnchors() && chanState.IsInitiator {
|
||||||
localBalance += 2 * anchorSize
|
localBalance += 2 * AnchorSize
|
||||||
}
|
}
|
||||||
|
|
||||||
return localBalance <= chanState.LocalChanCfg.DustLimit
|
return localBalance <= chanState.LocalChanCfg.DustLimit
|
||||||
@@ -8269,7 +8269,7 @@ func (lc *LightningChannel) RemoteBalanceDust() bool {
|
|||||||
// regain the stats allocated to the anchor outputs with the co-op
|
// regain the stats allocated to the anchor outputs with the co-op
|
||||||
// close transaction.
|
// close transaction.
|
||||||
if chanState.ChanType.HasAnchors() && !chanState.IsInitiator {
|
if chanState.ChanType.HasAnchors() && !chanState.IsInitiator {
|
||||||
remoteBalance += 2 * anchorSize
|
remoteBalance += 2 * AnchorSize
|
||||||
}
|
}
|
||||||
|
|
||||||
return remoteBalance <= chanState.RemoteChanCfg.DustLimit
|
return remoteBalance <= chanState.RemoteChanCfg.DustLimit
|
||||||
|
|||||||
@@ -706,7 +706,7 @@ func TestCooperativeChannelClosure(t *testing.T) {
|
|||||||
testCoopClose(t, &coopCloseTestCase{
|
testCoopClose(t, &coopCloseTestCase{
|
||||||
chanType: channeldb.SingleFunderTweaklessBit |
|
chanType: channeldb.SingleFunderTweaklessBit |
|
||||||
channeldb.AnchorOutputsBit,
|
channeldb.AnchorOutputsBit,
|
||||||
anchorAmt: anchorSize * 2,
|
anchorAmt: AnchorSize * 2,
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -816,7 +816,7 @@ func TestForceClose(t *testing.T) {
|
|||||||
chanType: channeldb.SingleFunderTweaklessBit |
|
chanType: channeldb.SingleFunderTweaklessBit |
|
||||||
channeldb.AnchorOutputsBit,
|
channeldb.AnchorOutputsBit,
|
||||||
expectedCommitWeight: input.AnchorCommitWeight,
|
expectedCommitWeight: input.AnchorCommitWeight,
|
||||||
anchorAmt: anchorSize * 2,
|
anchorAmt: AnchorSize * 2,
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
t.Run("taproot", func(t *testing.T) {
|
t.Run("taproot", func(t *testing.T) {
|
||||||
@@ -825,7 +825,7 @@ func TestForceClose(t *testing.T) {
|
|||||||
channeldb.AnchorOutputsBit |
|
channeldb.AnchorOutputsBit |
|
||||||
channeldb.SimpleTaprootFeatureBit,
|
channeldb.SimpleTaprootFeatureBit,
|
||||||
expectedCommitWeight: input.TaprootCommitWeight,
|
expectedCommitWeight: input.TaprootCommitWeight,
|
||||||
anchorAmt: anchorSize * 2,
|
anchorAmt: AnchorSize * 2,
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -911,7 +911,7 @@ func testForceClose(t *testing.T, testCase *forceCloseTestCase) {
|
|||||||
t.Fatal("commit tx not referenced by anchor res")
|
t.Fatal("commit tx not referenced by anchor res")
|
||||||
}
|
}
|
||||||
if anchorRes.AnchorSignDescriptor.Output.Value !=
|
if anchorRes.AnchorSignDescriptor.Output.Value !=
|
||||||
int64(anchorSize) {
|
int64(AnchorSize) {
|
||||||
|
|
||||||
t.Fatal("unexpected anchor size")
|
t.Fatal("unexpected anchor size")
|
||||||
}
|
}
|
||||||
|
|||||||
+22
-25
@@ -17,8 +17,8 @@ import (
|
|||||||
"github.com/lightningnetwork/lnd/lnwire"
|
"github.com/lightningnetwork/lnd/lnwire"
|
||||||
)
|
)
|
||||||
|
|
||||||
// anchorSize is the constant anchor output size.
|
// AnchorSize is the constant anchor output size.
|
||||||
const anchorSize = btcutil.Amount(330)
|
const AnchorSize = btcutil.Amount(330)
|
||||||
|
|
||||||
// DefaultAnchorsCommitMaxFeeRateSatPerVByte is the default max fee rate in
|
// DefaultAnchorsCommitMaxFeeRateSatPerVByte is the default max fee rate in
|
||||||
// sat/vbyte the initiator will use for anchor channels. This should be enough
|
// sat/vbyte the initiator will use for anchor channels. This should be enough
|
||||||
@@ -200,9 +200,9 @@ func (w *WitnessScriptDesc) PkScript() []byte {
|
|||||||
return w.OutputScript
|
return w.OutputScript
|
||||||
}
|
}
|
||||||
|
|
||||||
// WitnessScript returns the witness script that we'll use when signing for the
|
// WitnessScriptToSign returns the witness script that we'll use when signing
|
||||||
// remote party, and also verifying signatures on our transactions. As an
|
// for the remote party, and also verifying signatures on our transactions. As
|
||||||
// example, when we create an outgoing HTLC for the remote party, we want to
|
// an example, when we create an outgoing HTLC for the remote party, we want to
|
||||||
// sign their success path.
|
// sign their success path.
|
||||||
func (w *WitnessScriptDesc) WitnessScriptToSign() []byte {
|
func (w *WitnessScriptDesc) WitnessScriptToSign() []byte {
|
||||||
return w.WitnessScript
|
return w.WitnessScript
|
||||||
@@ -210,10 +210,10 @@ func (w *WitnessScriptDesc) WitnessScriptToSign() []byte {
|
|||||||
|
|
||||||
// WitnessScriptForPath returns the witness script for the given spending path.
|
// WitnessScriptForPath returns the witness script for the given spending path.
|
||||||
// An error is returned if the path is unknown. This is useful as when
|
// An error is returned if the path is unknown. This is useful as when
|
||||||
// constructing a contrl block for a given path, one also needs witness script
|
// constructing a control block for a given path, one also needs witness script
|
||||||
// being signed.
|
// being signed.
|
||||||
func (w *WitnessScriptDesc) WitnessScriptForPath(_ input.ScriptPath,
|
func (w *WitnessScriptDesc) WitnessScriptForPath(
|
||||||
) ([]byte, error) {
|
_ input.ScriptPath) ([]byte, error) {
|
||||||
|
|
||||||
return w.WitnessScript, nil
|
return w.WitnessScript, nil
|
||||||
}
|
}
|
||||||
@@ -532,8 +532,8 @@ func CommitScriptAnchors(chanType channeldb.ChannelType,
|
|||||||
input.ScriptDescriptor, input.ScriptDescriptor, error) {
|
input.ScriptDescriptor, input.ScriptDescriptor, error) {
|
||||||
|
|
||||||
var (
|
var (
|
||||||
anchorScript func(key *btcec.PublicKey) (
|
anchorScript func(
|
||||||
input.ScriptDescriptor, error)
|
key *btcec.PublicKey) (input.ScriptDescriptor, error)
|
||||||
|
|
||||||
keySelector func(*channeldb.ChannelConfig,
|
keySelector func(*channeldb.ChannelConfig,
|
||||||
bool) *btcec.PublicKey
|
bool) *btcec.PublicKey
|
||||||
@@ -544,12 +544,10 @@ func CommitScriptAnchors(chanType channeldb.ChannelType,
|
|||||||
// level key is now the (relative) local delay and remote public key,
|
// level key is now the (relative) local delay and remote public key,
|
||||||
// since these are fully revealed once the commitment hits the chain.
|
// since these are fully revealed once the commitment hits the chain.
|
||||||
case chanType.IsTaproot():
|
case chanType.IsTaproot():
|
||||||
anchorScript = func(key *btcec.PublicKey,
|
anchorScript = func(
|
||||||
) (input.ScriptDescriptor, error) {
|
key *btcec.PublicKey) (input.ScriptDescriptor, error) {
|
||||||
|
|
||||||
return input.NewAnchorScriptTree(
|
return input.NewAnchorScriptTree(key)
|
||||||
key,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
keySelector = func(cfg *channeldb.ChannelConfig,
|
keySelector = func(cfg *channeldb.ChannelConfig,
|
||||||
@@ -567,8 +565,8 @@ func CommitScriptAnchors(chanType channeldb.ChannelType,
|
|||||||
default:
|
default:
|
||||||
// For normal channels, we'll create a p2wsh script based on
|
// For normal channels, we'll create a p2wsh script based on
|
||||||
// the target key.
|
// the target key.
|
||||||
anchorScript = func(key *btcec.PublicKey,
|
anchorScript = func(
|
||||||
) (input.ScriptDescriptor, error) {
|
key *btcec.PublicKey) (input.ScriptDescriptor, error) {
|
||||||
|
|
||||||
script, err := input.CommitScriptAnchor(key)
|
script, err := input.CommitScriptAnchor(key)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -942,7 +940,7 @@ func CreateCommitTx(chanType channeldb.ChannelType,
|
|||||||
if localOutput || numHTLCs > 0 {
|
if localOutput || numHTLCs > 0 {
|
||||||
commitTx.AddTxOut(&wire.TxOut{
|
commitTx.AddTxOut(&wire.TxOut{
|
||||||
PkScript: localAnchor.PkScript(),
|
PkScript: localAnchor.PkScript(),
|
||||||
Value: int64(anchorSize),
|
Value: int64(AnchorSize),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -951,7 +949,7 @@ func CreateCommitTx(chanType channeldb.ChannelType,
|
|||||||
if remoteOutput || numHTLCs > 0 {
|
if remoteOutput || numHTLCs > 0 {
|
||||||
commitTx.AddTxOut(&wire.TxOut{
|
commitTx.AddTxOut(&wire.TxOut{
|
||||||
PkScript: remoteAnchor.PkScript(),
|
PkScript: remoteAnchor.PkScript(),
|
||||||
Value: int64(anchorSize),
|
Value: int64(AnchorSize),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -976,7 +974,7 @@ func CoopCloseBalance(chanType channeldb.ChannelType, isInitiator bool,
|
|||||||
// Since the initiator's balance also is stored after subtracting the
|
// Since the initiator's balance also is stored after subtracting the
|
||||||
// anchor values, add that back in case this was an anchor commitment.
|
// anchor values, add that back in case this was an anchor commitment.
|
||||||
if chanType.HasAnchors() {
|
if chanType.HasAnchors() {
|
||||||
initiatorDelta += 2 * anchorSize
|
initiatorDelta += 2 * AnchorSize
|
||||||
}
|
}
|
||||||
|
|
||||||
// The initiator will pay the full coop close fee, subtract that value
|
// The initiator will pay the full coop close fee, subtract that value
|
||||||
@@ -1075,9 +1073,9 @@ func genSegwitV0HtlcScript(chanType channeldb.ChannelType,
|
|||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// genTaprootHtlcScript generates the HTLC scripts for a taproot+musig2
|
// GenTaprootHtlcScript generates the HTLC scripts for a taproot+musig2
|
||||||
// channel.
|
// channel.
|
||||||
func genTaprootHtlcScript(isIncoming bool, whoseCommit lntypes.ChannelParty,
|
func GenTaprootHtlcScript(isIncoming bool, whoseCommit lntypes.ChannelParty,
|
||||||
timeout uint32, rHash [32]byte, keyRing *CommitmentKeyRing,
|
timeout uint32, rHash [32]byte, keyRing *CommitmentKeyRing,
|
||||||
) (*input.HtlcScriptTree, error) {
|
) (*input.HtlcScriptTree, error) {
|
||||||
|
|
||||||
@@ -1138,8 +1136,7 @@ func genTaprootHtlcScript(isIncoming bool, whoseCommit lntypes.ChannelParty,
|
|||||||
// along side the multiplexer.
|
// along side the multiplexer.
|
||||||
func genHtlcScript(chanType channeldb.ChannelType, isIncoming bool,
|
func genHtlcScript(chanType channeldb.ChannelType, isIncoming bool,
|
||||||
whoseCommit lntypes.ChannelParty, timeout uint32, rHash [32]byte,
|
whoseCommit lntypes.ChannelParty, timeout uint32, rHash [32]byte,
|
||||||
keyRing *CommitmentKeyRing,
|
keyRing *CommitmentKeyRing) (input.ScriptDescriptor, error) {
|
||||||
) (input.ScriptDescriptor, error) {
|
|
||||||
|
|
||||||
if !chanType.IsTaproot() {
|
if !chanType.IsTaproot() {
|
||||||
return genSegwitV0HtlcScript(
|
return genSegwitV0HtlcScript(
|
||||||
@@ -1148,7 +1145,7 @@ func genHtlcScript(chanType channeldb.ChannelType, isIncoming bool,
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
return genTaprootHtlcScript(
|
return GenTaprootHtlcScript(
|
||||||
isIncoming, whoseCommit, timeout, rHash, keyRing,
|
isIncoming, whoseCommit, timeout, rHash, keyRing,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -261,7 +261,7 @@ func NewChannelReservation(capacity, localFundingAmt btcutil.Amount,
|
|||||||
// addition to the two anchor outputs.
|
// addition to the two anchor outputs.
|
||||||
feeMSat := lnwire.NewMSatFromSatoshis(commitFee)
|
feeMSat := lnwire.NewMSatFromSatoshis(commitFee)
|
||||||
if req.CommitType.HasAnchors() {
|
if req.CommitType.HasAnchors() {
|
||||||
feeMSat += 2 * lnwire.NewMSatFromSatoshis(anchorSize)
|
feeMSat += 2 * lnwire.NewMSatFromSatoshis(AnchorSize)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Used to cut down on verbosity.
|
// Used to cut down on verbosity.
|
||||||
|
|||||||
@@ -258,7 +258,7 @@ func CreateTestChannels(t *testing.T, chanType channeldb.ChannelType,
|
|||||||
commitFee := calcStaticFee(chanType, 0)
|
commitFee := calcStaticFee(chanType, 0)
|
||||||
var anchorAmt btcutil.Amount
|
var anchorAmt btcutil.Amount
|
||||||
if chanType.HasAnchors() {
|
if chanType.HasAnchors() {
|
||||||
anchorAmt += 2 * anchorSize
|
anchorAmt += 2 * AnchorSize
|
||||||
}
|
}
|
||||||
|
|
||||||
aliceBalance := lnwire.NewMSatFromSatoshis(
|
aliceBalance := lnwire.NewMSatFromSatoshis(
|
||||||
|
|||||||
@@ -925,7 +925,7 @@ func createTestChannelsForVectors(tc *testContext, chanType channeldb.ChannelTyp
|
|||||||
|
|
||||||
var anchorAmt btcutil.Amount
|
var anchorAmt btcutil.Amount
|
||||||
if chanType.HasAnchors() {
|
if chanType.HasAnchors() {
|
||||||
anchorAmt = 2 * anchorSize
|
anchorAmt = 2 * AnchorSize
|
||||||
}
|
}
|
||||||
|
|
||||||
remoteCommitTx, localCommitTx, err := CreateCommitmentTxns(
|
remoteCommitTx, localCommitTx, err := CreateCommitmentTxns(
|
||||||
|
|||||||
@@ -0,0 +1,176 @@
|
|||||||
|
package lnwire
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
"sort"
|
||||||
|
|
||||||
|
"github.com/lightningnetwork/lnd/tlv"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
// MinCustomRecordsTlvType is the minimum custom records TLV type as
|
||||||
|
// defined in BOLT 01.
|
||||||
|
MinCustomRecordsTlvType = 65536
|
||||||
|
)
|
||||||
|
|
||||||
|
// CustomRecords stores a set of custom key/value pairs. Map keys are TLV types
|
||||||
|
// which must be greater than or equal to MinCustomRecordsTlvType.
|
||||||
|
type CustomRecords map[uint64][]byte
|
||||||
|
|
||||||
|
// NewCustomRecords creates a new CustomRecords instance from a
|
||||||
|
// tlv.TypeMap.
|
||||||
|
func NewCustomRecords(tlvMap tlv.TypeMap) (CustomRecords, error) {
|
||||||
|
// Make comparisons in unit tests easy by returning nil if the map is
|
||||||
|
// empty.
|
||||||
|
if len(tlvMap) == 0 {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
customRecords := make(CustomRecords, len(tlvMap))
|
||||||
|
for k, v := range tlvMap {
|
||||||
|
customRecords[uint64(k)] = v
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate the custom records.
|
||||||
|
err := customRecords.Validate()
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("custom records from tlv map "+
|
||||||
|
"validation error: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return customRecords, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ParseCustomRecords creates a new CustomRecords instance from a tlv.Blob.
|
||||||
|
func ParseCustomRecords(b tlv.Blob) (CustomRecords, error) {
|
||||||
|
stream, err := tlv.NewStream()
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("error creating stream: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
typeMap, err := stream.DecodeWithParsedTypes(bytes.NewReader(b))
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("error decoding HTLC record: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return NewCustomRecords(typeMap)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate checks that all custom records are in the custom type range.
|
||||||
|
func (c CustomRecords) Validate() error {
|
||||||
|
if c == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
for key := range c {
|
||||||
|
if key < MinCustomRecordsTlvType {
|
||||||
|
return fmt.Errorf("custom records entry with TLV "+
|
||||||
|
"type below min: %d", MinCustomRecordsTlvType)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Copy returns a copy of the custom records.
|
||||||
|
func (c CustomRecords) Copy() CustomRecords {
|
||||||
|
if c == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
customRecords := make(CustomRecords, len(c))
|
||||||
|
for k, v := range c {
|
||||||
|
customRecords[k] = v
|
||||||
|
}
|
||||||
|
|
||||||
|
return customRecords
|
||||||
|
}
|
||||||
|
|
||||||
|
// ExtendRecordProducers extends the given records slice with the custom
|
||||||
|
// records. The resultant records slice will be sorted if the given records
|
||||||
|
// slice contains TLV types greater than or equal to MinCustomRecordsTlvType.
|
||||||
|
func (c CustomRecords) ExtendRecordProducers(
|
||||||
|
producers []tlv.RecordProducer) ([]tlv.RecordProducer, error) {
|
||||||
|
|
||||||
|
// If the custom records are nil or empty, there is nothing to do.
|
||||||
|
if len(c) == 0 {
|
||||||
|
return producers, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate the custom records.
|
||||||
|
err := c.Validate()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure that the existing records slice TLV types are not also present
|
||||||
|
// in the custom records. If they are, the resultant extended records
|
||||||
|
// slice would erroneously contain duplicate TLV types.
|
||||||
|
for _, rp := range producers {
|
||||||
|
record := rp.Record()
|
||||||
|
recordTlvType := uint64(record.Type())
|
||||||
|
|
||||||
|
_, foundDuplicateTlvType := c[recordTlvType]
|
||||||
|
if foundDuplicateTlvType {
|
||||||
|
return nil, fmt.Errorf("custom records contains a TLV "+
|
||||||
|
"type that is already present in the "+
|
||||||
|
"existing records: %d", recordTlvType)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert the custom records map to a TLV record producer slice and
|
||||||
|
// append them to the exiting records slice.
|
||||||
|
crRecords := tlv.MapToRecords(c)
|
||||||
|
for _, record := range crRecords {
|
||||||
|
r := recordProducer{record}
|
||||||
|
producers = append(producers, &r)
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the records slice which was given as an argument included TLV
|
||||||
|
// values greater than or equal to the minimum custom records TLV type
|
||||||
|
// we will sort the extended records slice to ensure that it is ordered
|
||||||
|
// correctly.
|
||||||
|
sort.Slice(producers, func(i, j int) bool {
|
||||||
|
recordI := producers[i].Record()
|
||||||
|
recordJ := producers[j].Record()
|
||||||
|
return recordI.Type() < recordJ.Type()
|
||||||
|
})
|
||||||
|
|
||||||
|
return producers, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// RecordProducers returns a slice of record producers for the custom records.
|
||||||
|
func (c CustomRecords) RecordProducers() []tlv.RecordProducer {
|
||||||
|
// If the custom records are nil or empty, return an empty slice.
|
||||||
|
if len(c) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert the custom records map to a TLV record producer slice.
|
||||||
|
records := tlv.MapToRecords(c)
|
||||||
|
|
||||||
|
// Convert the records to record producers.
|
||||||
|
producers := make([]tlv.RecordProducer, len(records))
|
||||||
|
for i, record := range records {
|
||||||
|
producers[i] = &recordProducer{record}
|
||||||
|
}
|
||||||
|
|
||||||
|
return producers
|
||||||
|
}
|
||||||
|
|
||||||
|
// Serialize serializes the custom records into a byte slice.
|
||||||
|
func (c CustomRecords) Serialize() ([]byte, error) {
|
||||||
|
records := tlv.MapToRecords(c)
|
||||||
|
stream, err := tlv.NewStream(records...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("error creating stream: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var b bytes.Buffer
|
||||||
|
if err := stream.Encode(&b); err != nil {
|
||||||
|
return nil, fmt.Errorf("error encoding custom records: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return b.Bytes(), nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,198 @@
|
|||||||
|
package lnwire
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/lightningnetwork/lnd/fn"
|
||||||
|
"github.com/lightningnetwork/lnd/tlv"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
// TestCustomRecords tests the custom records serialization and deserialization,
|
||||||
|
// as well as copying and producing records.
|
||||||
|
func TestCustomRecords(t *testing.T) {
|
||||||
|
testCases := []struct {
|
||||||
|
name string
|
||||||
|
customTypes tlv.TypeMap
|
||||||
|
expectedRecords CustomRecords
|
||||||
|
expectedErr string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "empty custom records",
|
||||||
|
customTypes: tlv.TypeMap{},
|
||||||
|
expectedRecords: nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "custom record with invalid type",
|
||||||
|
customTypes: tlv.TypeMap{
|
||||||
|
123: []byte{1, 2, 3},
|
||||||
|
},
|
||||||
|
expectedErr: "TLV type below min: 65536",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "valid custom record",
|
||||||
|
customTypes: tlv.TypeMap{
|
||||||
|
65536: []byte{1, 2, 3},
|
||||||
|
},
|
||||||
|
expectedRecords: map[uint64][]byte{
|
||||||
|
65536: {1, 2, 3},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "valid custom records, wrong order",
|
||||||
|
customTypes: tlv.TypeMap{
|
||||||
|
65537: []byte{3, 4, 5},
|
||||||
|
65536: []byte{1, 2, 3},
|
||||||
|
},
|
||||||
|
expectedRecords: map[uint64][]byte{
|
||||||
|
65536: {1, 2, 3},
|
||||||
|
65537: {3, 4, 5},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range testCases {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
records, err := NewCustomRecords(tc.customTypes)
|
||||||
|
|
||||||
|
if tc.expectedErr != "" {
|
||||||
|
require.ErrorContains(t, err, tc.expectedErr)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, tc.expectedRecords, records)
|
||||||
|
|
||||||
|
// Serialize, then parse the records again.
|
||||||
|
blob, err := records.Serialize()
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
parsedRecords, err := ParseCustomRecords(blob)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
require.Equal(t, tc.expectedRecords, parsedRecords)
|
||||||
|
|
||||||
|
// Copy() should also return the same records.
|
||||||
|
require.Equal(
|
||||||
|
t, tc.expectedRecords, parsedRecords.Copy(),
|
||||||
|
)
|
||||||
|
|
||||||
|
// RecordProducers() should also allow us to serialize
|
||||||
|
// the records again.
|
||||||
|
serializedProducers := serializeRecordProducers(
|
||||||
|
t, parsedRecords.RecordProducers(),
|
||||||
|
)
|
||||||
|
|
||||||
|
require.Equal(t, blob, serializedProducers)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestCustomRecordsExtendRecordProducers tests that we can extend a slice of
|
||||||
|
// record producers with custom records.
|
||||||
|
func TestCustomRecordsExtendRecordProducers(t *testing.T) {
|
||||||
|
testCases := []struct {
|
||||||
|
name string
|
||||||
|
existingTypes map[uint64][]byte
|
||||||
|
customRecords CustomRecords
|
||||||
|
expectedResult tlv.TypeMap
|
||||||
|
expectedErr string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "normal merge",
|
||||||
|
existingTypes: map[uint64][]byte{
|
||||||
|
123: {3, 4, 5},
|
||||||
|
345: {1, 2, 3},
|
||||||
|
},
|
||||||
|
customRecords: CustomRecords{
|
||||||
|
65536: {1, 2, 3},
|
||||||
|
},
|
||||||
|
expectedResult: tlv.TypeMap{
|
||||||
|
123: {3, 4, 5},
|
||||||
|
345: {1, 2, 3},
|
||||||
|
65536: {1, 2, 3},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "duplicates",
|
||||||
|
existingTypes: map[uint64][]byte{
|
||||||
|
123: {3, 4, 5},
|
||||||
|
345: {1, 2, 3},
|
||||||
|
65536: {1, 2, 3},
|
||||||
|
},
|
||||||
|
customRecords: CustomRecords{
|
||||||
|
65536: {1, 2, 3},
|
||||||
|
},
|
||||||
|
expectedErr: "contains a TLV type that is already " +
|
||||||
|
"present in the existing records: 65536",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "non custom type in custom records",
|
||||||
|
existingTypes: map[uint64][]byte{
|
||||||
|
123: {3, 4, 5},
|
||||||
|
345: {1, 2, 3},
|
||||||
|
65536: {1, 2, 3},
|
||||||
|
},
|
||||||
|
customRecords: CustomRecords{
|
||||||
|
123: {1, 2, 3},
|
||||||
|
},
|
||||||
|
expectedErr: "TLV type below min: 65536",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range testCases {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
nonCustomRecords := tlv.MapToRecords(tc.existingTypes)
|
||||||
|
nonCustomProducers := fn.Map(
|
||||||
|
func(r tlv.Record) tlv.RecordProducer {
|
||||||
|
return &recordProducer{r}
|
||||||
|
}, nonCustomRecords,
|
||||||
|
)
|
||||||
|
|
||||||
|
combined, err := tc.customRecords.ExtendRecordProducers(
|
||||||
|
nonCustomProducers,
|
||||||
|
)
|
||||||
|
|
||||||
|
if tc.expectedErr != "" {
|
||||||
|
require.ErrorContains(t, err, tc.expectedErr)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
serializedProducers := serializeRecordProducers(
|
||||||
|
t, combined,
|
||||||
|
)
|
||||||
|
|
||||||
|
stream, err := tlv.NewStream()
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
parsedMap, err := stream.DecodeWithParsedTypes(
|
||||||
|
bytes.NewReader(serializedProducers),
|
||||||
|
)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
require.Equal(t, tc.expectedResult, parsedMap)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// serializeRecordProducers is a helper function that serializes a slice of
|
||||||
|
// record producers into a byte slice.
|
||||||
|
func serializeRecordProducers(t *testing.T,
|
||||||
|
producers []tlv.RecordProducer) []byte {
|
||||||
|
|
||||||
|
tlvRecords := fn.Map(func(p tlv.RecordProducer) tlv.Record {
|
||||||
|
return p.Record()
|
||||||
|
}, producers)
|
||||||
|
|
||||||
|
stream, err := tlv.NewStream(tlvRecords...)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
var b bytes.Buffer
|
||||||
|
err = stream.Encode(&b)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
return b.Bytes()
|
||||||
|
}
|
||||||
@@ -1,5 +1,7 @@
|
|||||||
package lnwire
|
package lnwire
|
||||||
|
|
||||||
|
import "github.com/lightningnetwork/lnd/tlv"
|
||||||
|
|
||||||
// QueryEncoding is an enum-like type that represents exactly how a set data is
|
// QueryEncoding is an enum-like type that represents exactly how a set data is
|
||||||
// encoded on the wire.
|
// encoded on the wire.
|
||||||
type QueryEncoding uint8
|
type QueryEncoding uint8
|
||||||
@@ -15,3 +17,17 @@ const (
|
|||||||
// NOTE: this should no longer be used or accepted.
|
// NOTE: this should no longer be used or accepted.
|
||||||
EncodingSortedZlib QueryEncoding = 1
|
EncodingSortedZlib QueryEncoding = 1
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// recordProducer is a simple helper struct that implements the
|
||||||
|
// tlv.RecordProducer interface.
|
||||||
|
type recordProducer struct {
|
||||||
|
record tlv.Record
|
||||||
|
}
|
||||||
|
|
||||||
|
// Record returns the underlying record.
|
||||||
|
func (r *recordProducer) Record() tlv.Record {
|
||||||
|
return r.record
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure that recordProducer implements the tlv.RecordProducer interface.
|
||||||
|
var _ tlv.RecordProducer = (*recordProducer)(nil)
|
||||||
@@ -86,14 +86,6 @@ func TestExtraOpaqueDataEncodeDecode(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
type recordProducer struct {
|
|
||||||
record tlv.Record
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *recordProducer) Record() tlv.Record {
|
|
||||||
return r.record
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestExtraOpaqueDataPackUnpackRecords tests that we're able to pack a set of
|
// TestExtraOpaqueDataPackUnpackRecords tests that we're able to pack a set of
|
||||||
// tlv.Records into a stream, and unpack them on the other side to obtain the
|
// tlv.Records into a stream, and unpack them on the other side to obtain the
|
||||||
// same set of records.
|
// same set of records.
|
||||||
|
|||||||
@@ -4113,11 +4113,12 @@ func (s *server) peerInitializer(p *peer.Brontide) {
|
|||||||
s.wg.Add(1)
|
s.wg.Add(1)
|
||||||
go s.peerTerminationWatcher(p, ready)
|
go s.peerTerminationWatcher(p, ready)
|
||||||
|
|
||||||
|
pubBytes := p.IdentityKey().SerializeCompressed()
|
||||||
|
|
||||||
// Start the peer! If an error occurs, we Disconnect the peer, which
|
// Start the peer! If an error occurs, we Disconnect the peer, which
|
||||||
// will unblock the peerTerminationWatcher.
|
// will unblock the peerTerminationWatcher.
|
||||||
if err := p.Start(); err != nil {
|
if err := p.Start(); err != nil {
|
||||||
srvrLog.Warnf("Starting peer=%v got error: %v",
|
srvrLog.Warnf("Starting peer=%x got error: %v", pubBytes, err)
|
||||||
p.IdentityKey(), err)
|
|
||||||
|
|
||||||
p.Disconnect(fmt.Errorf("unable to start peer: %w", err))
|
p.Disconnect(fmt.Errorf("unable to start peer: %w", err))
|
||||||
return
|
return
|
||||||
@@ -4127,13 +4128,15 @@ func (s *server) peerInitializer(p *peer.Brontide) {
|
|||||||
// was successful, and to begin watching the peer's wait group.
|
// was successful, and to begin watching the peer's wait group.
|
||||||
close(ready)
|
close(ready)
|
||||||
|
|
||||||
pubStr := string(p.IdentityKey().SerializeCompressed())
|
|
||||||
|
|
||||||
s.mu.Lock()
|
s.mu.Lock()
|
||||||
defer s.mu.Unlock()
|
defer s.mu.Unlock()
|
||||||
|
|
||||||
// Check if there are listeners waiting for this peer to come online.
|
// Check if there are listeners waiting for this peer to come online.
|
||||||
srvrLog.Debugf("Notifying that peer %v is online", p)
|
srvrLog.Debugf("Notifying that peer %v is online", p)
|
||||||
|
|
||||||
|
// TODO(guggero): Do a proper conversion to a string everywhere, or use
|
||||||
|
// route.Vertex as the key type of peerConnectedListeners.
|
||||||
|
pubStr := string(pubBytes)
|
||||||
for _, peerChan := range s.peerConnectedListeners[pubStr] {
|
for _, peerChan := range s.peerConnectedListeners[pubStr] {
|
||||||
select {
|
select {
|
||||||
case peerChan <- p:
|
case peerChan <- p:
|
||||||
|
|||||||
Reference in new issue
Block a user