mirror of
https://github.com/lightningnetwork/lnd.git
synced 2024-11-19 01:43:16 +01:00
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:
commit
306695cd78
@ -259,8 +259,8 @@ issues:
|
||||
- forbidigo
|
||||
- godot
|
||||
|
||||
# Allow fmt.Printf() in lncli.
|
||||
- path: cmd/lncli/*
|
||||
# Allow fmt.Printf() in commands.
|
||||
- path: cmd/commands/*
|
||||
linters:
|
||||
- forbidigo
|
||||
|
||||
|
@ -1,4 +1,4 @@
|
||||
package main
|
||||
package commands
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
@ -42,7 +42,7 @@ func parseTime(s string, base time.Time) (uint64, error) {
|
||||
|
||||
var lightningPrefix = "lightning:"
|
||||
|
||||
// stripPrefix removes accidentally copied 'lightning:' prefix.
|
||||
func stripPrefix(s string) string {
|
||||
// StripPrefix removes accidentally copied 'lightning:' prefix.
|
||||
func StripPrefix(s string) string {
|
||||
return strings.TrimSpace(strings.TrimPrefix(s, lightningPrefix))
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
package main
|
||||
package commands
|
||||
|
||||
import (
|
||||
"testing"
|
||||
@ -111,7 +111,7 @@ func TestStripPrefix(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
for _, test := range stripPrefixTests {
|
||||
actual := stripPrefix(test.in)
|
||||
actual := StripPrefix(test.in)
|
||||
require.Equal(t, test.expected, actual)
|
||||
}
|
||||
}
|
@ -1,7 +1,7 @@
|
||||
//go:build autopilotrpc
|
||||
// +build autopilotrpc
|
||||
|
||||
package main
|
||||
package commands
|
||||
|
||||
import (
|
||||
"github.com/lightningnetwork/lnd/lnrpc/autopilotrpc"
|
@ -1,7 +1,7 @@
|
||||
//go:build !autopilotrpc
|
||||
// +build !autopilotrpc
|
||||
|
||||
package main
|
||||
package commands
|
||||
|
||||
import "github.com/urfave/cli"
|
||||
|
@ -1,7 +1,7 @@
|
||||
//go:build chainrpc
|
||||
// +build chainrpc
|
||||
|
||||
package main
|
||||
package commands
|
||||
|
||||
import (
|
||||
"bytes"
|
@ -1,7 +1,7 @@
|
||||
//go:build !chainrpc
|
||||
// +build !chainrpc
|
||||
|
||||
package main
|
||||
package commands
|
||||
|
||||
import "github.com/urfave/cli"
|
||||
|
@ -1,4 +1,4 @@
|
||||
package main
|
||||
package commands
|
||||
|
||||
import (
|
||||
"encoding/hex"
|
@ -1,4 +1,4 @@
|
||||
package main
|
||||
package commands
|
||||
|
||||
import (
|
||||
"bytes"
|
@ -1,4 +1,4 @@
|
||||
package main
|
||||
package commands
|
||||
|
||||
import (
|
||||
"context"
|
@ -1,4 +1,4 @@
|
||||
package main
|
||||
package commands
|
||||
|
||||
import (
|
||||
"encoding/hex"
|
||||
@ -9,7 +9,7 @@ import (
|
||||
"github.com/urfave/cli"
|
||||
)
|
||||
|
||||
var addInvoiceCommand = cli.Command{
|
||||
var AddInvoiceCommand = cli.Command{
|
||||
Name: "addinvoice",
|
||||
Category: "Invoices",
|
||||
Usage: "Add a new invoice.",
|
||||
@ -408,7 +408,7 @@ func decodePayReq(ctx *cli.Context) error {
|
||||
}
|
||||
|
||||
resp, err := client.DecodePayReq(ctxc, &lnrpc.PayReqString{
|
||||
PayReq: stripPrefix(payreq),
|
||||
PayReq: StripPrefix(payreq),
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
@ -1,4 +1,4 @@
|
||||
package main
|
||||
package commands
|
||||
|
||||
import (
|
||||
"bytes"
|
@ -1,4 +1,4 @@
|
||||
package main
|
||||
package commands
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
@ -265,6 +265,7 @@ func setCfg(ctx *cli.Context) error {
|
||||
Config: mcCfg.Config,
|
||||
},
|
||||
)
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
@ -366,5 +367,6 @@ func resetMissionControl(ctx *cli.Context) error {
|
||||
|
||||
req := &routerrpc.ResetMissionControlRequest{}
|
||||
_, err := client.ResetMissionControl(ctxc, req)
|
||||
|
||||
return err
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
package main
|
||||
package commands
|
||||
|
||||
import (
|
||||
"bytes"
|
@ -1,4 +1,4 @@
|
||||
package main
|
||||
package commands
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
@ -25,6 +25,7 @@ import (
|
||||
"github.com/lightningnetwork/lnd/record"
|
||||
"github.com/lightningnetwork/lnd/routing/route"
|
||||
"github.com/urfave/cli"
|
||||
"google.golang.org/grpc"
|
||||
)
|
||||
|
||||
const (
|
||||
@ -152,8 +153,8 @@ var (
|
||||
}
|
||||
)
|
||||
|
||||
// paymentFlags returns common flags for sendpayment and payinvoice.
|
||||
func paymentFlags() []cli.Flag {
|
||||
// PaymentFlags returns common flags for sendpayment and payinvoice.
|
||||
func PaymentFlags() []cli.Flag {
|
||||
return []cli.Flag{
|
||||
cli.StringFlag{
|
||||
Name: "pay_req",
|
||||
@ -202,7 +203,7 @@ func paymentFlags() []cli.Flag {
|
||||
}
|
||||
}
|
||||
|
||||
var sendPaymentCommand = cli.Command{
|
||||
var SendPaymentCommand = cli.Command{
|
||||
Name: "sendpayment",
|
||||
Category: "Payments",
|
||||
Usage: "Send a payment over lightning.",
|
||||
@ -226,7 +227,7 @@ var sendPaymentCommand = cli.Command{
|
||||
`,
|
||||
ArgsUsage: "dest amt payment_hash final_cltv_delta pay_addr | " +
|
||||
"--pay_req=R [--pay_addr=H]",
|
||||
Flags: append(paymentFlags(),
|
||||
Flags: append(PaymentFlags(),
|
||||
cli.StringFlag{
|
||||
Name: "dest, d",
|
||||
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]",
|
||||
},
|
||||
),
|
||||
Action: sendPayment,
|
||||
Action: SendPayment,
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
func sendPayment(ctx *cli.Context) error {
|
||||
func SendPayment(ctx *cli.Context) error {
|
||||
// Show command help if no arguments provided
|
||||
if ctx.NArg() == 0 && ctx.NumFlags() == 0 {
|
||||
_ = cli.ShowCommandHelp(ctx, "sendpayment")
|
||||
return nil
|
||||
}
|
||||
|
||||
conn := getClientConn(ctx, false)
|
||||
defer conn.Close()
|
||||
|
||||
args := ctx.Args()
|
||||
|
||||
// If a payment request was provided, we can exit early since all of the
|
||||
// details of the payment are encoded within the request.
|
||||
if ctx.IsSet("pay_req") {
|
||||
req := &routerrpc.SendPaymentRequest{
|
||||
PaymentRequest: stripPrefix(ctx.String("pay_req")),
|
||||
PaymentRequest: StripPrefix(ctx.String("pay_req")),
|
||||
Amt: ctx.Int64("amt"),
|
||||
DestCustomRecords: make(map[uint64][]byte),
|
||||
Amp: ctx.Bool(ampFlag.Name),
|
||||
@ -357,7 +361,9 @@ func sendPayment(ctx *cli.Context) error {
|
||||
|
||||
req.PaymentAddr = payAddr
|
||||
|
||||
return sendPaymentRequest(ctx, req)
|
||||
return SendPaymentRequest(
|
||||
ctx, req, conn, conn, routerRPCSendPayment,
|
||||
)
|
||||
}
|
||||
|
||||
var (
|
||||
@ -466,19 +472,29 @@ func sendPayment(ctx *cli.Context) error {
|
||||
|
||||
req.PaymentAddr = payAddr
|
||||
|
||||
return sendPaymentRequest(ctx, req)
|
||||
return SendPaymentRequest(ctx, req, conn, conn, routerRPCSendPayment)
|
||||
}
|
||||
|
||||
func sendPaymentRequest(ctx *cli.Context,
|
||||
req *routerrpc.SendPaymentRequest) error {
|
||||
// SendPaymentFn is a function type that abstracts the SendPaymentV2 call of the
|
||||
// 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()
|
||||
|
||||
conn := getClientConn(ctx, false)
|
||||
defer conn.Close()
|
||||
|
||||
client := lnrpc.NewLightningClient(conn)
|
||||
routerClient := routerrpc.NewRouterClient(conn)
|
||||
lnClient := lnrpc.NewLightningClient(lnConn)
|
||||
|
||||
outChan := ctx.Int64Slice("outgoing_chan_id")
|
||||
if len(outChan) != 0 {
|
||||
@ -558,7 +574,7 @@ func sendPaymentRequest(ctx *cli.Context,
|
||||
if req.PaymentRequest != "" {
|
||||
// Decode payment request to find out the amount.
|
||||
decodeReq := &lnrpc.PayReqString{PayReq: req.PaymentRequest}
|
||||
decodeResp, err := client.DecodePayReq(ctxc, decodeReq)
|
||||
decodeResp, err := lnClient.DecodePayReq(ctxc, decodeReq)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@ -602,14 +618,12 @@ func sendPaymentRequest(ctx *cli.Context,
|
||||
printJSON := ctx.Bool(jsonFlag.Name)
|
||||
req.NoInflightUpdates = !ctx.Bool(inflightUpdatesFlag.Name) && printJSON
|
||||
|
||||
stream, err := routerClient.SendPaymentV2(ctxc, req)
|
||||
stream, err := callSendPayment(ctxc, paymentConn, req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
finalState, err := printLivePayment(
|
||||
ctxc, stream, client, printJSON,
|
||||
)
|
||||
finalState, err := PrintLivePayment(ctxc, stream, lnClient, printJSON)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@ -667,24 +681,29 @@ func trackPayment(ctx *cli.Context) error {
|
||||
}
|
||||
|
||||
client := lnrpc.NewLightningClient(conn)
|
||||
_, err = printLivePayment(ctxc, stream, client, ctx.Bool(jsonFlag.Name))
|
||||
_, err = PrintLivePayment(ctxc, stream, client, ctx.Bool(jsonFlag.Name))
|
||||
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
|
||||
// option uses terminal control codes to rewrite the output. This call
|
||||
// terminates when the payment reaches a final state.
|
||||
func printLivePayment(ctxc context.Context,
|
||||
stream routerrpc.Router_TrackPaymentV2Client,
|
||||
client lnrpc.LightningClient, json bool) (*lnrpc.Payment, error) {
|
||||
func PrintLivePayment(ctxc context.Context, stream PaymentResultStream,
|
||||
lnClient lnrpc.LightningClient, json bool) (*lnrpc.Payment, error) {
|
||||
|
||||
// Terminal escape codes aren't supported on Windows, fall back to json.
|
||||
if !json && runtime.GOOS == "windows" {
|
||||
json = true
|
||||
}
|
||||
|
||||
aliases := newAliasCache(client)
|
||||
aliases := newAliasCache(lnClient)
|
||||
|
||||
first := true
|
||||
var lastLineCount int
|
||||
@ -706,17 +725,17 @@ func printLivePayment(ctxc context.Context,
|
||||
// Write raw json to stdout.
|
||||
printRespJSON(payment)
|
||||
} else {
|
||||
table := formatPayment(ctxc, payment, aliases)
|
||||
resultTable := formatPayment(ctxc, payment, aliases)
|
||||
|
||||
// Clear all previously written lines and print the
|
||||
// updated table.
|
||||
clearLines(lastLineCount)
|
||||
fmt.Print(table)
|
||||
fmt.Print(resultTable)
|
||||
|
||||
// Store the number of lines written for the next update
|
||||
// pass.
|
||||
lastLineCount = 0
|
||||
for _, b := range table {
|
||||
for _, b := range resultTable {
|
||||
if b == '\n' {
|
||||
lastLineCount++
|
||||
}
|
||||
@ -874,7 +893,7 @@ var payInvoiceCommand = cli.Command{
|
||||
This command is a shortcut for 'sendpayment --pay_req='.
|
||||
`,
|
||||
ArgsUsage: "pay_req",
|
||||
Flags: append(paymentFlags(),
|
||||
Flags: append(PaymentFlags(),
|
||||
cli.Int64Flag{
|
||||
Name: "amt",
|
||||
Usage: "(optional) number of satoshis to fulfill the " +
|
||||
@ -885,6 +904,9 @@ var payInvoiceCommand = cli.Command{
|
||||
}
|
||||
|
||||
func payInvoice(ctx *cli.Context) error {
|
||||
conn := getClientConn(ctx, false)
|
||||
defer conn.Close()
|
||||
|
||||
args := ctx.Args()
|
||||
|
||||
var payReq string
|
||||
@ -898,14 +920,14 @@ func payInvoice(ctx *cli.Context) error {
|
||||
}
|
||||
|
||||
req := &routerrpc.SendPaymentRequest{
|
||||
PaymentRequest: stripPrefix(payReq),
|
||||
PaymentRequest: StripPrefix(payReq),
|
||||
Amt: ctx.Int64("amt"),
|
||||
DestCustomRecords: make(map[uint64][]byte),
|
||||
Amp: ctx.Bool(ampFlag.Name),
|
||||
Cancelable: ctx.Bool(cancelableFlag.Name),
|
||||
}
|
||||
|
||||
return sendPaymentRequest(ctx, req)
|
||||
return SendPaymentRequest(ctx, req, conn, conn, routerRPCSendPayment)
|
||||
}
|
||||
|
||||
var sendToRouteCommand = cli.Command{
|
||||
@ -1900,7 +1922,7 @@ func estimateRouteFee(ctx *cli.Context) error {
|
||||
req.AmtSat = amtSat
|
||||
|
||||
case ctx.IsSet("pay_req"):
|
||||
req.PaymentRequest = stripPrefix(ctx.String("pay_req"))
|
||||
req.PaymentRequest = StripPrefix(ctx.String("pay_req"))
|
||||
if ctx.IsSet("timeout") {
|
||||
req.Timeout = uint32(ctx.Duration("timeout").Seconds())
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
package main
|
||||
package commands
|
||||
|
||||
import (
|
||||
"fmt"
|
@ -1,4 +1,4 @@
|
||||
package main
|
||||
package commands
|
||||
|
||||
import (
|
||||
"context"
|
@ -1,4 +1,4 @@
|
||||
package main
|
||||
package commands
|
||||
|
||||
import (
|
||||
"errors"
|
@ -1,4 +1,4 @@
|
||||
package main
|
||||
package commands
|
||||
|
||||
import (
|
||||
"fmt"
|
@ -1,4 +1,4 @@
|
||||
package main
|
||||
package commands
|
||||
|
||||
import (
|
||||
"bufio"
|
@ -1,4 +1,4 @@
|
||||
package main
|
||||
package commands
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
@ -11,6 +11,7 @@ import (
|
||||
"io"
|
||||
"math"
|
||||
"os"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
@ -41,8 +42,47 @@ const (
|
||||
defaultUtxoMinConf = 1
|
||||
)
|
||||
|
||||
var errBadChanPoint = errors.New("expecting chan_point to be in format of: " +
|
||||
"txid:index")
|
||||
var (
|
||||
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 {
|
||||
shutdownInterceptor, err := signal.Intercept()
|
||||
@ -66,9 +106,9 @@ func printJSON(resp interface{}) {
|
||||
}
|
||||
|
||||
var out bytes.Buffer
|
||||
json.Indent(&out, b, "", "\t")
|
||||
out.WriteString("\n")
|
||||
out.WriteTo(os.Stdout)
|
||||
_ = json.Indent(&out, b, "", " ")
|
||||
_, _ = out.WriteString("\n")
|
||||
_, _ = out.WriteTo(os.Stdout)
|
||||
}
|
||||
|
||||
func printRespJSON(resp proto.Message) {
|
||||
@ -78,7 +118,13 @@ func printRespJSON(resp proto.Message) {
|
||||
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
|
||||
@ -1442,15 +1488,15 @@ func walletBalance(ctx *cli.Context) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
var channelBalanceCommand = cli.Command{
|
||||
var ChannelBalanceCommand = cli.Command{
|
||||
Name: "channelbalance",
|
||||
Category: "Channels",
|
||||
Usage: "Returns the sum of the total available channel balance across " +
|
||||
"all open channels.",
|
||||
Action: actionDecorator(channelBalance),
|
||||
Action: actionDecorator(ChannelBalance),
|
||||
}
|
||||
|
||||
func channelBalance(ctx *cli.Context) error {
|
||||
func ChannelBalance(ctx *cli.Context) error {
|
||||
ctxc := getContext()
|
||||
client, cleanUp := getClient(ctx)
|
||||
defer cleanUp()
|
||||
@ -1575,7 +1621,7 @@ func pendingChannels(ctx *cli.Context) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
var listChannelsCommand = cli.Command{
|
||||
var ListChannelsCommand = cli.Command{
|
||||
Name: "listchannels",
|
||||
Category: "Channels",
|
||||
Usage: "List all open channels.",
|
||||
@ -1608,7 +1654,7 @@ var listChannelsCommand = cli.Command{
|
||||
"order to improve performance",
|
||||
},
|
||||
},
|
||||
Action: actionDecorator(listChannels),
|
||||
Action: actionDecorator(ListChannels),
|
||||
}
|
||||
|
||||
var listAliasesCommand = cli.Command{
|
||||
@ -1616,10 +1662,10 @@ var listAliasesCommand = cli.Command{
|
||||
Category: "Channels",
|
||||
Usage: "List all aliases.",
|
||||
Flags: []cli.Flag{},
|
||||
Action: actionDecorator(listaliases),
|
||||
Action: actionDecorator(listAliases),
|
||||
}
|
||||
|
||||
func listaliases(ctx *cli.Context) error {
|
||||
func listAliases(ctx *cli.Context) error {
|
||||
ctxc := getContext()
|
||||
client, cleanUp := getClient(ctx)
|
||||
defer cleanUp()
|
||||
@ -1636,7 +1682,7 @@ func listaliases(ctx *cli.Context) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func listChannels(ctx *cli.Context) error {
|
||||
func ListChannels(ctx *cli.Context) error {
|
||||
ctxc := getContext()
|
||||
client, cleanUp := getClient(ctx)
|
||||
defer cleanUp()
|
@ -1,4 +1,4 @@
|
||||
package main
|
||||
package commands
|
||||
|
||||
import (
|
||||
"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
|
||||
// +build dev
|
||||
|
||||
package main
|
||||
package commands
|
||||
|
||||
import (
|
||||
"fmt"
|
@ -1,7 +1,7 @@
|
||||
//go:build !dev
|
||||
// +build !dev
|
||||
|
||||
package main
|
||||
package commands
|
||||
|
||||
import "github.com/urfave/cli"
|
||||
|
@ -1,7 +1,7 @@
|
||||
//go:build invoicesrpc
|
||||
// +build invoicesrpc
|
||||
|
||||
package main
|
||||
package commands
|
||||
|
||||
import (
|
||||
"encoding/hex"
|
@ -1,7 +1,7 @@
|
||||
//go:build !invoicesrpc
|
||||
// +build !invoicesrpc
|
||||
|
||||
package main
|
||||
package commands
|
||||
|
||||
import "github.com/urfave/cli"
|
||||
|
@ -1,4 +1,4 @@
|
||||
package main
|
||||
package commands
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
@ -1,4 +1,4 @@
|
||||
package main
|
||||
package commands
|
||||
|
||||
import (
|
||||
"encoding/hex"
|
601
cmd/commands/main.go
Normal file
601
cmd/commands/main.go
Normal file
@ -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
|
||||
// +build neutrinorpc
|
||||
|
||||
package main
|
||||
package commands
|
||||
|
||||
import (
|
||||
"github.com/lightningnetwork/lnd/lnrpc/neutrinorpc"
|
@ -1,7 +1,7 @@
|
||||
//go:build !neutrinorpc
|
||||
// +build !neutrinorpc
|
||||
|
||||
package main
|
||||
package commands
|
||||
|
||||
import "github.com/urfave/cli"
|
||||
|
@ -1,7 +1,7 @@
|
||||
//go:build peersrpc
|
||||
// +build peersrpc
|
||||
|
||||
package main
|
||||
package commands
|
||||
|
||||
import (
|
||||
"fmt"
|
@ -1,7 +1,7 @@
|
||||
//go:build !peersrpc
|
||||
// +build !peersrpc
|
||||
|
||||
package main
|
||||
package commands
|
||||
|
||||
import "github.com/urfave/cli"
|
||||
|
@ -1,4 +1,4 @@
|
||||
package main
|
||||
package commands
|
||||
|
||||
import (
|
||||
"bytes"
|
@ -1,4 +1,4 @@
|
||||
package main
|
||||
package commands
|
||||
|
||||
import "github.com/urfave/cli"
|
||||
|
@ -1,4 +1,4 @@
|
||||
package main
|
||||
package commands
|
||||
|
||||
import (
|
||||
"encoding/hex"
|
@ -1,7 +1,7 @@
|
||||
//go:build walletrpc
|
||||
// +build walletrpc
|
||||
|
||||
package main
|
||||
package commands
|
||||
|
||||
import (
|
||||
"bytes"
|
@ -1,7 +1,7 @@
|
||||
//go:build !walletrpc
|
||||
// +build !walletrpc
|
||||
|
||||
package main
|
||||
package commands
|
||||
|
||||
import "github.com/urfave/cli"
|
||||
|
@ -1,4 +1,4 @@
|
||||
package main
|
||||
package commands
|
||||
|
||||
import "github.com/lightningnetwork/lnd/lnrpc/walletrpc"
|
||||
|
@ -1,7 +1,7 @@
|
||||
//go:build watchtowerrpc
|
||||
// +build watchtowerrpc
|
||||
|
||||
package main
|
||||
package commands
|
||||
|
||||
import (
|
||||
"github.com/lightningnetwork/lnd/lnrpc/watchtowerrpc"
|
@ -1,7 +1,7 @@
|
||||
//go:build !watchtowerrpc
|
||||
// +build !watchtowerrpc
|
||||
|
||||
package main
|
||||
package commands
|
||||
|
||||
import "github.com/urfave/cli"
|
||||
|
@ -1,4 +1,4 @@
|
||||
package main
|
||||
package commands
|
||||
|
||||
import (
|
||||
"encoding/hex"
|
@ -1,594 +1,11 @@
|
||||
// Copyright (c) 2013-2017 The btcsuite 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
|
||||
|
||||
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
|
||||
}
|
||||
import "github.com/lightningnetwork/lnd/cmd/commands"
|
||||
|
||||
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)
|
||||
}
|
||||
commands.Main()
|
||||
}
|
||||
|
@ -6,6 +6,7 @@ import (
|
||||
"fmt"
|
||||
"io"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/btcsuite/btcd/blockchain"
|
||||
@ -287,7 +288,7 @@ type InitFundingMsg struct {
|
||||
// PendingChanID is not all zeroes (the default value), then this will
|
||||
// be the pending channel ID used for the funding flow within the wire
|
||||
// protocol.
|
||||
PendingChanID [32]byte
|
||||
PendingChanID PendingChanID
|
||||
|
||||
// ChannelType allows the caller to use an explicit channel type for the
|
||||
// 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
|
||||
// pending single funded channels indexed by their pending channel identifier,
|
||||
// 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
|
||||
// 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
|
||||
// reservation created.
|
||||
chanIDNonce atomic.Uint64
|
||||
|
||||
// nonceMtx is a mutex that guards the pendingMusigNonces.
|
||||
nonceMtx sync.RWMutex
|
||||
chanIDNonce uint64
|
||||
|
||||
// pendingMusigNonces is used to store the musig2 nonce we generate to
|
||||
// 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
|
||||
// by its full channel ID once the commitment transactions have been
|
||||
// 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
|
||||
// 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
|
||||
// identify a particular future channel funding workflow.
|
||||
func (f *Manager) nextPendingChanID() [32]byte {
|
||||
// Obtain a fresh nonce. We do this by encoding the current nonce
|
||||
// counter, then incrementing it by one.
|
||||
f.nonceMtx.Lock()
|
||||
var nonce [8]byte
|
||||
binary.LittleEndian.PutUint64(nonce[:], f.chanIDNonce)
|
||||
f.chanIDNonce++
|
||||
f.nonceMtx.Unlock()
|
||||
func (f *Manager) nextPendingChanID() PendingChanID {
|
||||
// Obtain a fresh nonce. We do this by encoding the incremented nonce.
|
||||
nextNonce := f.chanIDNonce.Add(1)
|
||||
|
||||
var nonceBytes [8]byte
|
||||
binary.LittleEndian.PutUint64(nonceBytes[:], nextNonce)
|
||||
|
||||
// We'll generate the next pending channelID by "encrypting" 32-bytes
|
||||
// of zeroes which'll extract 32 random bytes from our stream cipher.
|
||||
var (
|
||||
nextChanID [32]byte
|
||||
nextChanID PendingChanID
|
||||
zeroes [32]byte
|
||||
)
|
||||
salsa20.XORKeyStream(nextChanID[:], zeroes[:], nonce[:], &f.chanIDKey)
|
||||
salsa20.XORKeyStream(
|
||||
nextChanID[:], zeroes[:], nonceBytes[:], &f.chanIDKey,
|
||||
)
|
||||
|
||||
return nextChanID
|
||||
}
|
||||
@ -1045,7 +1052,8 @@ func (f *Manager) reservationCoordinator() {
|
||||
//
|
||||
// NOTE: This MUST be run as a goroutine.
|
||||
func (f *Manager) advanceFundingState(channel *channeldb.OpenChannel,
|
||||
pendingChanID [32]byte, updateChan chan<- *lnrpc.OpenStatusUpdate) {
|
||||
pendingChanID PendingChanID,
|
||||
updateChan chan<- *lnrpc.OpenStatusUpdate) {
|
||||
|
||||
defer f.wg.Done()
|
||||
|
||||
@ -1115,7 +1123,7 @@ func (f *Manager) advanceFundingState(channel *channeldb.OpenChannel,
|
||||
// updateChan can be set non-nil to get OpenStatusUpdates.
|
||||
func (f *Manager) stateStep(channel *channeldb.OpenChannel,
|
||||
lnChannel *lnwallet.LightningChannel,
|
||||
shortChanID *lnwire.ShortChannelID, pendingChanID [32]byte,
|
||||
shortChanID *lnwire.ShortChannelID, pendingChanID PendingChanID,
|
||||
channelState channelOpeningState,
|
||||
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
|
||||
// confirm, and marks it open in the database when that happens.
|
||||
func (f *Manager) advancePendingChannelState(
|
||||
channel *channeldb.OpenChannel, pendingChanID [32]byte) error {
|
||||
channel *channeldb.OpenChannel, pendingChanID PendingChanID) error {
|
||||
|
||||
if channel.IsZeroConf() {
|
||||
// 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 flow.
|
||||
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
|
||||
// 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
|
||||
// let the channel start handling routing.
|
||||
func (f *Manager) handleChannelReadyReceived(channel *channeldb.OpenChannel,
|
||||
scid *lnwire.ShortChannelID, pendingChanID [32]byte,
|
||||
scid *lnwire.ShortChannelID, pendingChanID PendingChanID,
|
||||
updateChan chan<- *lnrpc.OpenStatusUpdate) error {
|
||||
|
||||
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.
|
||||
// Otherwise we'll generate a fresh one as normal. This will be used
|
||||
// to track this reservation throughout its lifetime.
|
||||
var chanID [32]byte
|
||||
var chanID PendingChanID
|
||||
if msg.PendingChanID == zeroID {
|
||||
chanID = f.nextPendingChanID()
|
||||
} else {
|
||||
@ -4942,7 +4950,8 @@ func (f *Manager) pruneZombieReservations() {
|
||||
// cancelReservationCtx does all needed work in order to securely cancel the
|
||||
// reservation.
|
||||
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, "+
|
||||
"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
|
||||
// target public key of the peer, and the specified pending channel ID.
|
||||
func (f *Manager) deleteReservationCtx(peerKey *btcec.PublicKey,
|
||||
pendingChanID [32]byte) {
|
||||
pendingChanID PendingChanID) {
|
||||
|
||||
peerIDKey := newSerializedKey(peerKey)
|
||||
f.resMtx.Lock()
|
||||
@ -5013,7 +5022,7 @@ func (f *Manager) deleteReservationCtx(peerKey *btcec.PublicKey,
|
||||
// getReservationCtx returns the reservation context for a particular pending
|
||||
// channel ID for a target peer.
|
||||
func (f *Manager) getReservationCtx(peerKey *btcec.PublicKey,
|
||||
pendingChanID [32]byte) (*reservationWithCtx, error) {
|
||||
pendingChanID PendingChanID) (*reservationWithCtx, error) {
|
||||
|
||||
peerIDKey := newSerializedKey(peerKey)
|
||||
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
|
||||
// channel will receive a new, permanent channel ID, and will no longer be
|
||||
// considered pending.
|
||||
func (f *Manager) IsPendingChannel(pendingChanID [32]byte,
|
||||
func (f *Manager) IsPendingChannel(pendingChanID PendingChanID,
|
||||
peer lnpeer.Peer) bool {
|
||||
|
||||
peerIDKey := newSerializedKey(peer.IdentityKey())
|
||||
|
2
go.mod
2
go.mod
@ -41,7 +41,7 @@ require (
|
||||
github.com/lightningnetwork/lnd/queue v1.1.1
|
||||
github.com/lightningnetwork/lnd/sqldb v1.0.3
|
||||
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/ltcsuite/ltcd v0.0.0-20190101042124-f37f8bf35796
|
||||
github.com/miekg/dns v1.1.43
|
||||
|
4
go.sum
4
go.sum
@ -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/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/tlv v1.2.3 h1:If5ibokA/UoCBGuCKaY6Vn2SJU0l9uAbehCnhTZjEP8=
|
||||
github.com/lightningnetwork/lnd/tlv v1.2.3/go.mod h1:zDkmqxOczP6LaLTvSFDQ1SJUfHcQRCMKFj93dn3eMB8=
|
||||
github.com/lightningnetwork/lnd/tlv v1.2.6 h1:icvQG2yDr6k3ZuZzfRdG3EJp6pHurcuh3R6dg0gv/Mw=
|
||||
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/go.mod h1:j7T9uJ2NLMaHwE7GiBGnpYLn4f7NRoTM6qj+ul6/ycA=
|
||||
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 {
|
||||
intercepted, err := s.heldHtlcSet.pop(res.Key)
|
||||
if err != nil {
|
||||
|
@ -331,7 +331,7 @@ type InterceptableHtlcForwarder interface {
|
||||
type ForwardInterceptor func(InterceptedPacket) error
|
||||
|
||||
// InterceptedPacket contains the relevant information for the interceptor about
|
||||
// an htlc.
|
||||
// an HTLC.
|
||||
type InterceptedPacket struct {
|
||||
// IncomingCircuit contains the incoming channel and htlc id of the
|
||||
// packet.
|
||||
|
@ -33,17 +33,17 @@ const (
|
||||
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
|
||||
// (witness script, etc), and also tapscript paths which have distinct
|
||||
// (witness script, etc.), and also tapscript paths which have distinct
|
||||
// tapscript leaves.
|
||||
type ScriptDescriptor interface {
|
||||
// PkScript is the public key script that commits to the final
|
||||
// contract.
|
||||
PkScript() []byte
|
||||
|
||||
// WitnessScript returns the witness script that we'll use when signing
|
||||
// for the remote party, and also verifying signatures on our
|
||||
// WitnessScriptToSign returns the witness script that we'll use when
|
||||
// signing for the remote party, and also verifying signatures on our
|
||||
// transactions. As an example, when we create an outgoing HTLC for the
|
||||
// remote party, we want to sign their success path.
|
||||
//
|
||||
@ -73,6 +73,9 @@ type TapscriptDescriptor interface {
|
||||
|
||||
// TapScriptTree returns the underlying tapscript tree.
|
||||
TapScriptTree() *txscript.IndexedTapScriptTree
|
||||
|
||||
// Tree returns the underlying ScriptTree.
|
||||
Tree() ScriptTree
|
||||
}
|
||||
|
||||
// ScriptTree holds the contents needed to spend a script within a tapscript
|
||||
|
@ -689,8 +689,8 @@ func (h *HtlcScriptTree) WitnessScriptForPath(path ScriptPath) ([]byte, error) {
|
||||
|
||||
// CtrlBlockForPath returns the control block for the given spending path. For
|
||||
// script types that don't have a control block, nil is returned.
|
||||
func (h *HtlcScriptTree) CtrlBlockForPath(path ScriptPath,
|
||||
) (*txscript.ControlBlock, error) {
|
||||
func (h *HtlcScriptTree) CtrlBlockForPath(
|
||||
path ScriptPath) (*txscript.ControlBlock, error) {
|
||||
|
||||
switch path {
|
||||
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
|
||||
// TapscriptMultiplexer interface.
|
||||
var _ TapscriptDescriptor = (*HtlcScriptTree)(nil)
|
||||
@ -1690,9 +1695,9 @@ func TaprootSecondLevelScriptTree(revokeKey, delayKey *btcec.PublicKey,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// WitnessScript returns the witness script that we'll use when signing for the
|
||||
// remote party, and also verifying signatures on our transactions. As an
|
||||
// example, when we create an outgoing HTLC for the remote party, we want to
|
||||
// WitnessScriptToSign returns the witness script that we'll use when signing
|
||||
// for the remote party, and also verifying signatures on our transactions. As
|
||||
// an example, when we create an outgoing HTLC for the remote party, we want to
|
||||
// sign their success path.
|
||||
func (s *SecondLevelScriptTree) WitnessScriptToSign() []byte {
|
||||
return s.SuccessTapLeaf.Script
|
||||
@ -1700,8 +1705,8 @@ func (s *SecondLevelScriptTree) WitnessScriptToSign() []byte {
|
||||
|
||||
// WitnessScriptForPath returns the witness script for the given spending path.
|
||||
// An error is returned if the path is unknown.
|
||||
func (s *SecondLevelScriptTree) WitnessScriptForPath(path ScriptPath,
|
||||
) ([]byte, error) {
|
||||
func (s *SecondLevelScriptTree) WitnessScriptForPath(
|
||||
path ScriptPath) ([]byte, error) {
|
||||
|
||||
switch path {
|
||||
case ScriptPathDelay:
|
||||
@ -1716,8 +1721,8 @@ func (s *SecondLevelScriptTree) WitnessScriptForPath(path ScriptPath,
|
||||
|
||||
// CtrlBlockForPath returns the control block for the given spending path. For
|
||||
// script types that don't have a control block, nil is returned.
|
||||
func (s *SecondLevelScriptTree) CtrlBlockForPath(path ScriptPath,
|
||||
) (*txscript.ControlBlock, error) {
|
||||
func (s *SecondLevelScriptTree) CtrlBlockForPath(
|
||||
path ScriptPath) (*txscript.ControlBlock, error) {
|
||||
|
||||
switch path {
|
||||
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
|
||||
// TapscriptDescriptor interface.
|
||||
var _ TapscriptDescriptor = (*SecondLevelScriptTree)(nil)
|
||||
@ -2069,9 +2079,9 @@ type CommitScriptTree struct {
|
||||
// TapscriptDescriptor interface.
|
||||
var _ TapscriptDescriptor = (*CommitScriptTree)(nil)
|
||||
|
||||
// WitnessScript returns the witness script that we'll use when signing for the
|
||||
// remote party, and also verifying signatures on our transactions. As an
|
||||
// example, when we create an outgoing HTLC for the remote party, we want to
|
||||
// WitnessScriptToSign returns the witness script that we'll use when signing
|
||||
// for the remote party, and also verifying signatures on our transactions. As
|
||||
// an example, when we create an outgoing HTLC for the remote party, we want to
|
||||
// sign their success path.
|
||||
func (c *CommitScriptTree) WitnessScriptToSign() []byte {
|
||||
// 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.
|
||||
// An error is returned if the path is unknown.
|
||||
func (c *CommitScriptTree) WitnessScriptForPath(path ScriptPath,
|
||||
) ([]byte, error) {
|
||||
func (c *CommitScriptTree) WitnessScriptForPath(
|
||||
path ScriptPath) ([]byte, error) {
|
||||
|
||||
switch path {
|
||||
// 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
|
||||
// script types that don't have a control block, nil is returned.
|
||||
func (c *CommitScriptTree) CtrlBlockForPath(path ScriptPath,
|
||||
) (*txscript.ControlBlock, error) {
|
||||
func (c *CommitScriptTree) CtrlBlockForPath(
|
||||
path ScriptPath) (*txscript.ControlBlock, error) {
|
||||
|
||||
switch path {
|
||||
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
|
||||
// create and spend the commitment output for the local party.
|
||||
func NewLocalCommitScriptTree(csvTimeout uint32,
|
||||
@ -2241,7 +2256,7 @@ func TaprootCommitScriptToSelf(csvTimeout uint32,
|
||||
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
|
||||
// of the leaf.
|
||||
func MakeTaprootCtrlBlock(leafScript []byte, internalKey *btcec.PublicKey,
|
||||
@ -2296,9 +2311,6 @@ func TaprootCommitSpendSuccess(signer Signer, signDesc *SignDescriptor,
|
||||
witnessStack[0] = maybeAppendSighash(sweepSig, signDesc.HashType)
|
||||
witnessStack[1] = signDesc.WitnessScript
|
||||
witnessStack[2] = ctrlBlockBytes
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return witnessStack, nil
|
||||
}
|
||||
@ -2773,8 +2785,8 @@ type AnchorScriptTree struct {
|
||||
|
||||
// NewAnchorScriptTree makes a new script tree for an anchor output with the
|
||||
// passed anchor key.
|
||||
func NewAnchorScriptTree(anchorKey *btcec.PublicKey,
|
||||
) (*AnchorScriptTree, error) {
|
||||
func NewAnchorScriptTree(
|
||||
anchorKey *btcec.PublicKey) (*AnchorScriptTree, error) {
|
||||
|
||||
// The main script used is just a OP_16 CSV (anyone can sweep after 16
|
||||
// blocks).
|
||||
@ -2810,9 +2822,9 @@ func NewAnchorScriptTree(anchorKey *btcec.PublicKey,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// WitnessScript returns the witness script that we'll use when signing for the
|
||||
// remote party, and also verifying signatures on our transactions. As an
|
||||
// example, when we create an outgoing HTLC for the remote party, we want to
|
||||
// WitnessScriptToSign returns the witness script that we'll use when signing
|
||||
// for the remote party, and also verifying signatures on our transactions. As
|
||||
// an example, when we create an outgoing HTLC for the remote party, we want to
|
||||
// sign their success path.
|
||||
func (a *AnchorScriptTree) WitnessScriptToSign() []byte {
|
||||
return a.SweepLeaf.Script
|
||||
@ -2820,8 +2832,8 @@ func (a *AnchorScriptTree) WitnessScriptToSign() []byte {
|
||||
|
||||
// WitnessScriptForPath returns the witness script for the given spending path.
|
||||
// An error is returned if the path is unknown.
|
||||
func (a *AnchorScriptTree) WitnessScriptForPath(path ScriptPath,
|
||||
) ([]byte, error) {
|
||||
func (a *AnchorScriptTree) WitnessScriptForPath(
|
||||
path ScriptPath) ([]byte, error) {
|
||||
|
||||
switch path {
|
||||
case ScriptPathDelay:
|
||||
@ -2836,8 +2848,8 @@ func (a *AnchorScriptTree) WitnessScriptForPath(path ScriptPath,
|
||||
|
||||
// CtrlBlockForPath returns the control block for the given spending path. For
|
||||
// script types that don't have a control block, nil is returned.
|
||||
func (a *AnchorScriptTree) CtrlBlockForPath(path ScriptPath,
|
||||
) (*txscript.ControlBlock, error) {
|
||||
func (a *AnchorScriptTree) CtrlBlockForPath(
|
||||
path ScriptPath) (*txscript.ControlBlock, error) {
|
||||
|
||||
switch path {
|
||||
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
|
||||
// TapscriptDescriptor interface.
|
||||
var _ TapscriptDescriptor = (*AnchorScriptTree)(nil)
|
||||
|
@ -22,7 +22,7 @@ var (
|
||||
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.
|
||||
// It is created when the stream opens and disconnects when the stream closes.
|
||||
type forwardInterceptor struct {
|
||||
@ -43,7 +43,7 @@ func newForwardInterceptor(htlcSwitch htlcswitch.InterceptableHtlcForwarder,
|
||||
}
|
||||
|
||||
// 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
|
||||
// to read from the client stream.
|
||||
// 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
|
||||
// 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
|
||||
}
|
||||
@ -1525,7 +1525,7 @@ func (s *Server) HtlcInterceptor(stream Router_HtlcInterceptorServer) error {
|
||||
}
|
||||
defer atomic.CompareAndSwapInt32(&s.forwardInterceptorActive, 1, 0)
|
||||
|
||||
// run the forward interceptor.
|
||||
// Run the forward interceptor.
|
||||
return newForwardInterceptor(
|
||||
s.cfg.RouterBackend.InterceptableForwarder, stream,
|
||||
).run()
|
||||
|
@ -4,9 +4,11 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/btcsuite/btcd/btcutil"
|
||||
"github.com/btcsuite/btcd/chaincfg/chainhash"
|
||||
"github.com/btcsuite/btcd/wire"
|
||||
"github.com/btcsuite/btcwallet/wallet"
|
||||
"github.com/btcsuite/btcwallet/wtxmgr"
|
||||
"github.com/lightningnetwork/lnd/fn"
|
||||
"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
|
||||
// funding output.
|
||||
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
|
@ -7694,7 +7694,7 @@ func NewAnchorResolution(chanState *channeldb.OpenChannel,
|
||||
WitnessScript: anchorWitnessScript,
|
||||
Output: &wire.TxOut{
|
||||
PkScript: localAnchor.PkScript(),
|
||||
Value: int64(anchorSize),
|
||||
Value: int64(AnchorSize),
|
||||
},
|
||||
HashType: sweepSigHash(chanState.ChanType),
|
||||
}
|
||||
@ -7706,7 +7706,7 @@ func NewAnchorResolution(chanState *channeldb.OpenChannel,
|
||||
|
||||
//nolint:lll
|
||||
signDesc.PrevOutputFetcher = txscript.NewCannedPrevOutputFetcher(
|
||||
localAnchor.PkScript(), int64(anchorSize),
|
||||
localAnchor.PkScript(), int64(AnchorSize),
|
||||
)
|
||||
|
||||
// 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
|
||||
// close transaction.
|
||||
if chanState.ChanType.HasAnchors() && chanState.IsInitiator {
|
||||
localBalance += 2 * anchorSize
|
||||
localBalance += 2 * AnchorSize
|
||||
}
|
||||
|
||||
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
|
||||
// close transaction.
|
||||
if chanState.ChanType.HasAnchors() && !chanState.IsInitiator {
|
||||
remoteBalance += 2 * anchorSize
|
||||
remoteBalance += 2 * AnchorSize
|
||||
}
|
||||
|
||||
return remoteBalance <= chanState.RemoteChanCfg.DustLimit
|
||||
|
@ -706,7 +706,7 @@ func TestCooperativeChannelClosure(t *testing.T) {
|
||||
testCoopClose(t, &coopCloseTestCase{
|
||||
chanType: channeldb.SingleFunderTweaklessBit |
|
||||
channeldb.AnchorOutputsBit,
|
||||
anchorAmt: anchorSize * 2,
|
||||
anchorAmt: AnchorSize * 2,
|
||||
})
|
||||
})
|
||||
}
|
||||
@ -816,7 +816,7 @@ func TestForceClose(t *testing.T) {
|
||||
chanType: channeldb.SingleFunderTweaklessBit |
|
||||
channeldb.AnchorOutputsBit,
|
||||
expectedCommitWeight: input.AnchorCommitWeight,
|
||||
anchorAmt: anchorSize * 2,
|
||||
anchorAmt: AnchorSize * 2,
|
||||
})
|
||||
})
|
||||
t.Run("taproot", func(t *testing.T) {
|
||||
@ -825,7 +825,7 @@ func TestForceClose(t *testing.T) {
|
||||
channeldb.AnchorOutputsBit |
|
||||
channeldb.SimpleTaprootFeatureBit,
|
||||
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")
|
||||
}
|
||||
if anchorRes.AnchorSignDescriptor.Output.Value !=
|
||||
int64(anchorSize) {
|
||||
int64(AnchorSize) {
|
||||
|
||||
t.Fatal("unexpected anchor size")
|
||||
}
|
||||
|
@ -17,8 +17,8 @@ import (
|
||||
"github.com/lightningnetwork/lnd/lnwire"
|
||||
)
|
||||
|
||||
// anchorSize is the constant anchor output size.
|
||||
const anchorSize = btcutil.Amount(330)
|
||||
// AnchorSize is the constant anchor output size.
|
||||
const AnchorSize = btcutil.Amount(330)
|
||||
|
||||
// DefaultAnchorsCommitMaxFeeRateSatPerVByte is the default max fee rate in
|
||||
// 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
|
||||
}
|
||||
|
||||
// WitnessScript returns the witness script that we'll use when signing for the
|
||||
// remote party, and also verifying signatures on our transactions. As an
|
||||
// example, when we create an outgoing HTLC for the remote party, we want to
|
||||
// WitnessScriptToSign returns the witness script that we'll use when signing
|
||||
// for the remote party, and also verifying signatures on our transactions. As
|
||||
// an example, when we create an outgoing HTLC for the remote party, we want to
|
||||
// sign their success path.
|
||||
func (w *WitnessScriptDesc) WitnessScriptToSign() []byte {
|
||||
return w.WitnessScript
|
||||
@ -210,10 +210,10 @@ func (w *WitnessScriptDesc) WitnessScriptToSign() []byte {
|
||||
|
||||
// WitnessScriptForPath returns the witness script for the given spending path.
|
||||
// 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.
|
||||
func (w *WitnessScriptDesc) WitnessScriptForPath(_ input.ScriptPath,
|
||||
) ([]byte, error) {
|
||||
func (w *WitnessScriptDesc) WitnessScriptForPath(
|
||||
_ input.ScriptPath) ([]byte, error) {
|
||||
|
||||
return w.WitnessScript, nil
|
||||
}
|
||||
@ -532,8 +532,8 @@ func CommitScriptAnchors(chanType channeldb.ChannelType,
|
||||
input.ScriptDescriptor, input.ScriptDescriptor, error) {
|
||||
|
||||
var (
|
||||
anchorScript func(key *btcec.PublicKey) (
|
||||
input.ScriptDescriptor, error)
|
||||
anchorScript func(
|
||||
key *btcec.PublicKey) (input.ScriptDescriptor, error)
|
||||
|
||||
keySelector func(*channeldb.ChannelConfig,
|
||||
bool) *btcec.PublicKey
|
||||
@ -544,12 +544,10 @@ func CommitScriptAnchors(chanType channeldb.ChannelType,
|
||||
// level key is now the (relative) local delay and remote public key,
|
||||
// since these are fully revealed once the commitment hits the chain.
|
||||
case chanType.IsTaproot():
|
||||
anchorScript = func(key *btcec.PublicKey,
|
||||
) (input.ScriptDescriptor, error) {
|
||||
anchorScript = func(
|
||||
key *btcec.PublicKey) (input.ScriptDescriptor, error) {
|
||||
|
||||
return input.NewAnchorScriptTree(
|
||||
key,
|
||||
)
|
||||
return input.NewAnchorScriptTree(key)
|
||||
}
|
||||
|
||||
keySelector = func(cfg *channeldb.ChannelConfig,
|
||||
@ -567,8 +565,8 @@ func CommitScriptAnchors(chanType channeldb.ChannelType,
|
||||
default:
|
||||
// For normal channels, we'll create a p2wsh script based on
|
||||
// the target key.
|
||||
anchorScript = func(key *btcec.PublicKey,
|
||||
) (input.ScriptDescriptor, error) {
|
||||
anchorScript = func(
|
||||
key *btcec.PublicKey) (input.ScriptDescriptor, error) {
|
||||
|
||||
script, err := input.CommitScriptAnchor(key)
|
||||
if err != nil {
|
||||
@ -942,7 +940,7 @@ func CreateCommitTx(chanType channeldb.ChannelType,
|
||||
if localOutput || numHTLCs > 0 {
|
||||
commitTx.AddTxOut(&wire.TxOut{
|
||||
PkScript: localAnchor.PkScript(),
|
||||
Value: int64(anchorSize),
|
||||
Value: int64(AnchorSize),
|
||||
})
|
||||
}
|
||||
|
||||
@ -951,7 +949,7 @@ func CreateCommitTx(chanType channeldb.ChannelType,
|
||||
if remoteOutput || numHTLCs > 0 {
|
||||
commitTx.AddTxOut(&wire.TxOut{
|
||||
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
|
||||
// anchor values, add that back in case this was an anchor commitment.
|
||||
if chanType.HasAnchors() {
|
||||
initiatorDelta += 2 * anchorSize
|
||||
initiatorDelta += 2 * AnchorSize
|
||||
}
|
||||
|
||||
// The initiator will pay the full coop close fee, subtract that value
|
||||
@ -1075,9 +1073,9 @@ func genSegwitV0HtlcScript(chanType channeldb.ChannelType,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// genTaprootHtlcScript generates the HTLC scripts for a taproot+musig2
|
||||
// GenTaprootHtlcScript generates the HTLC scripts for a taproot+musig2
|
||||
// channel.
|
||||
func genTaprootHtlcScript(isIncoming bool, whoseCommit lntypes.ChannelParty,
|
||||
func GenTaprootHtlcScript(isIncoming bool, whoseCommit lntypes.ChannelParty,
|
||||
timeout uint32, rHash [32]byte, keyRing *CommitmentKeyRing,
|
||||
) (*input.HtlcScriptTree, error) {
|
||||
|
||||
@ -1138,8 +1136,7 @@ func genTaprootHtlcScript(isIncoming bool, whoseCommit lntypes.ChannelParty,
|
||||
// along side the multiplexer.
|
||||
func genHtlcScript(chanType channeldb.ChannelType, isIncoming bool,
|
||||
whoseCommit lntypes.ChannelParty, timeout uint32, rHash [32]byte,
|
||||
keyRing *CommitmentKeyRing,
|
||||
) (input.ScriptDescriptor, error) {
|
||||
keyRing *CommitmentKeyRing) (input.ScriptDescriptor, error) {
|
||||
|
||||
if !chanType.IsTaproot() {
|
||||
return genSegwitV0HtlcScript(
|
||||
@ -1148,7 +1145,7 @@ func genHtlcScript(chanType channeldb.ChannelType, isIncoming bool,
|
||||
)
|
||||
}
|
||||
|
||||
return genTaprootHtlcScript(
|
||||
return GenTaprootHtlcScript(
|
||||
isIncoming, whoseCommit, timeout, rHash, keyRing,
|
||||
)
|
||||
}
|
||||
|
@ -261,7 +261,7 @@ func NewChannelReservation(capacity, localFundingAmt btcutil.Amount,
|
||||
// addition to the two anchor outputs.
|
||||
feeMSat := lnwire.NewMSatFromSatoshis(commitFee)
|
||||
if req.CommitType.HasAnchors() {
|
||||
feeMSat += 2 * lnwire.NewMSatFromSatoshis(anchorSize)
|
||||
feeMSat += 2 * lnwire.NewMSatFromSatoshis(AnchorSize)
|
||||
}
|
||||
|
||||
// Used to cut down on verbosity.
|
||||
|
@ -258,7 +258,7 @@ func CreateTestChannels(t *testing.T, chanType channeldb.ChannelType,
|
||||
commitFee := calcStaticFee(chanType, 0)
|
||||
var anchorAmt btcutil.Amount
|
||||
if chanType.HasAnchors() {
|
||||
anchorAmt += 2 * anchorSize
|
||||
anchorAmt += 2 * AnchorSize
|
||||
}
|
||||
|
||||
aliceBalance := lnwire.NewMSatFromSatoshis(
|
||||
|
@ -925,7 +925,7 @@ func createTestChannelsForVectors(tc *testContext, chanType channeldb.ChannelTyp
|
||||
|
||||
var anchorAmt btcutil.Amount
|
||||
if chanType.HasAnchors() {
|
||||
anchorAmt = 2 * anchorSize
|
||||
anchorAmt = 2 * AnchorSize
|
||||
}
|
||||
|
||||
remoteCommitTx, localCommitTx, err := CreateCommitmentTxns(
|
||||
|
176
lnwire/custom_records.go
Normal file
176
lnwire/custom_records.go
Normal file
@ -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
|
||||
}
|
198
lnwire/custom_records_test.go
Normal file
198
lnwire/custom_records_test.go
Normal file
@ -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
|
||||
|
||||
import "github.com/lightningnetwork/lnd/tlv"
|
||||
|
||||
// QueryEncoding is an enum-like type that represents exactly how a set data is
|
||||
// encoded on the wire.
|
||||
type QueryEncoding uint8
|
||||
@ -15,3 +17,17 @@ const (
|
||||
// NOTE: this should no longer be used or accepted.
|
||||
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
|
||||
// tlv.Records into a stream, and unpack them on the other side to obtain the
|
||||
// same set of records.
|
||||
|
11
server.go
11
server.go
@ -4113,11 +4113,12 @@ func (s *server) peerInitializer(p *peer.Brontide) {
|
||||
s.wg.Add(1)
|
||||
go s.peerTerminationWatcher(p, ready)
|
||||
|
||||
pubBytes := p.IdentityKey().SerializeCompressed()
|
||||
|
||||
// Start the peer! If an error occurs, we Disconnect the peer, which
|
||||
// will unblock the peerTerminationWatcher.
|
||||
if err := p.Start(); err != nil {
|
||||
srvrLog.Warnf("Starting peer=%v got error: %v",
|
||||
p.IdentityKey(), err)
|
||||
srvrLog.Warnf("Starting peer=%x got error: %v", pubBytes, err)
|
||||
|
||||
p.Disconnect(fmt.Errorf("unable to start peer: %w", err))
|
||||
return
|
||||
@ -4127,13 +4128,15 @@ func (s *server) peerInitializer(p *peer.Brontide) {
|
||||
// was successful, and to begin watching the peer's wait group.
|
||||
close(ready)
|
||||
|
||||
pubStr := string(p.IdentityKey().SerializeCompressed())
|
||||
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
// Check if there are listeners waiting for this peer to come online.
|
||||
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] {
|
||||
select {
|
||||
case peerChan <- p:
|
||||
|
Loading…
Reference in New Issue
Block a user