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:
Oliver Gugger 2024-08-23 05:04:46 -06:00 committed by GitHub
commit 306695cd78
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
64 changed files with 1382 additions and 795 deletions

View File

@ -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

View File

@ -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))
}

View File

@ -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)
}
}

View File

@ -1,7 +1,7 @@
//go:build autopilotrpc
// +build autopilotrpc
package main
package commands
import (
"github.com/lightningnetwork/lnd/lnrpc/autopilotrpc"

View File

@ -1,7 +1,7 @@
//go:build !autopilotrpc
// +build !autopilotrpc
package main
package commands
import "github.com/urfave/cli"

View File

@ -1,7 +1,7 @@
//go:build chainrpc
// +build chainrpc
package main
package commands
import (
"bytes"

View File

@ -1,7 +1,7 @@
//go:build !chainrpc
// +build !chainrpc
package main
package commands
import "github.com/urfave/cli"

View File

@ -1,4 +1,4 @@
package main
package commands
import (
"encoding/hex"

View File

@ -1,4 +1,4 @@
package main
package commands
import (
"bytes"

View File

@ -1,4 +1,4 @@
package main
package commands
import (
"context"

View File

@ -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

View File

@ -1,4 +1,4 @@
package main
package commands
import (
"bytes"

View File

@ -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
}

View File

@ -1,4 +1,4 @@
package main
package commands
import (
"bytes"

View File

@ -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())
}

View File

@ -1,4 +1,4 @@
package main
package commands
import (
"fmt"

View File

@ -1,4 +1,4 @@
package main
package commands
import (
"context"

View File

@ -1,4 +1,4 @@
package main
package commands
import (
"errors"

View File

@ -1,4 +1,4 @@
package main
package commands
import (
"fmt"

View File

@ -1,4 +1,4 @@
package main
package commands
import (
"bufio"

View File

@ -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()

View File

@ -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))
})
}
}

View File

@ -1,7 +1,7 @@
//go:build dev
// +build dev
package main
package commands
import (
"fmt"

View File

@ -1,7 +1,7 @@
//go:build !dev
// +build !dev
package main
package commands
import "github.com/urfave/cli"

View File

@ -1,7 +1,7 @@
//go:build invoicesrpc
// +build invoicesrpc
package main
package commands
import (
"encoding/hex"

View File

@ -1,7 +1,7 @@
//go:build !invoicesrpc
// +build !invoicesrpc
package main
package commands
import "github.com/urfave/cli"

View File

@ -1,4 +1,4 @@
package main
package commands
import (
"encoding/base64"

View File

@ -1,4 +1,4 @@
package main
package commands
import (
"encoding/hex"

601
cmd/commands/main.go Normal file
View 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)
}
}

View File

@ -1,7 +1,7 @@
//go:build neutrinorpc
// +build neutrinorpc
package main
package commands
import (
"github.com/lightningnetwork/lnd/lnrpc/neutrinorpc"

View File

@ -1,7 +1,7 @@
//go:build !neutrinorpc
// +build !neutrinorpc
package main
package commands
import "github.com/urfave/cli"

View File

@ -1,7 +1,7 @@
//go:build peersrpc
// +build peersrpc
package main
package commands
import (
"fmt"

View File

@ -1,7 +1,7 @@
//go:build !peersrpc
// +build !peersrpc
package main
package commands
import "github.com/urfave/cli"

View File

@ -1,4 +1,4 @@
package main
package commands
import (
"bytes"

View File

@ -1,4 +1,4 @@
package main
package commands
import "github.com/urfave/cli"

View File

@ -1,4 +1,4 @@
package main
package commands
import (
"encoding/hex"

View File

@ -1,7 +1,7 @@
//go:build walletrpc
// +build walletrpc
package main
package commands
import (
"bytes"

View File

@ -1,7 +1,7 @@
//go:build !walletrpc
// +build !walletrpc
package main
package commands
import "github.com/urfave/cli"

View File

@ -1,4 +1,4 @@
package main
package commands
import "github.com/lightningnetwork/lnd/lnrpc/walletrpc"

View File

@ -1,7 +1,7 @@
//go:build watchtowerrpc
// +build watchtowerrpc
package main
package commands
import (
"github.com/lightningnetwork/lnd/lnrpc/watchtowerrpc"

View File

@ -1,7 +1,7 @@
//go:build !watchtowerrpc
// +build !watchtowerrpc
package main
package commands
import "github.com/urfave/cli"

View File

@ -1,4 +1,4 @@
package main
package commands
import (
"encoding/hex"

View File

@ -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()
}

View File

@ -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.
nonceMtx sync.RWMutex
chanIDNonce uint64
chanIDNonce atomic.Uint64
// nonceMtx is a mutex that guards the pendingMusigNonces.
nonceMtx sync.RWMutex
// 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
View File

@ -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
View File

@ -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=

View File

@ -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 {

View File

@ -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.

View File

@ -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

View File

@ -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)

View File

@ -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

View File

@ -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()

View File

@ -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

View File

@ -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

View File

@ -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")
}

View File

@ -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,
)
}

View File

@ -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.

View File

@ -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(

View File

@ -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
View 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
}

View 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()
}

View File

@ -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)

View File

@ -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.

View File

@ -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: