diff --git a/.golangci.yml b/.golangci.yml index 5246c06c3..536c1b655 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -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 diff --git a/cmd/lncli/arg_parse.go b/cmd/commands/arg_parse.go similarity index 90% rename from cmd/lncli/arg_parse.go rename to cmd/commands/arg_parse.go index 49d165d55..1d8bbe198 100644 --- a/cmd/lncli/arg_parse.go +++ b/cmd/commands/arg_parse.go @@ -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)) } diff --git a/cmd/lncli/arg_parse_test.go b/cmd/commands/arg_parse_test.go similarity index 97% rename from cmd/lncli/arg_parse_test.go rename to cmd/commands/arg_parse_test.go index 571292d2c..35751098e 100644 --- a/cmd/lncli/arg_parse_test.go +++ b/cmd/commands/arg_parse_test.go @@ -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) } } diff --git a/cmd/lncli/autopilotrpc_active.go b/cmd/commands/autopilotrpc_active.go similarity index 99% rename from cmd/lncli/autopilotrpc_active.go rename to cmd/commands/autopilotrpc_active.go index 961e85994..212ef4579 100644 --- a/cmd/lncli/autopilotrpc_active.go +++ b/cmd/commands/autopilotrpc_active.go @@ -1,7 +1,7 @@ //go:build autopilotrpc // +build autopilotrpc -package main +package commands import ( "github.com/lightningnetwork/lnd/lnrpc/autopilotrpc" diff --git a/cmd/lncli/autopilotrpc_default.go b/cmd/commands/autopilotrpc_default.go similarity index 92% rename from cmd/lncli/autopilotrpc_default.go rename to cmd/commands/autopilotrpc_default.go index 7fb885217..393b6f124 100644 --- a/cmd/lncli/autopilotrpc_default.go +++ b/cmd/commands/autopilotrpc_default.go @@ -1,7 +1,7 @@ //go:build !autopilotrpc // +build !autopilotrpc -package main +package commands import "github.com/urfave/cli" diff --git a/cmd/lncli/chainrpc_active.go b/cmd/commands/chainrpc_active.go similarity index 99% rename from cmd/lncli/chainrpc_active.go rename to cmd/commands/chainrpc_active.go index 48946e0d5..0f1f8b612 100644 --- a/cmd/lncli/chainrpc_active.go +++ b/cmd/commands/chainrpc_active.go @@ -1,7 +1,7 @@ //go:build chainrpc // +build chainrpc -package main +package commands import ( "bytes" diff --git a/cmd/lncli/chainrpc_default.go b/cmd/commands/chainrpc_default.go similarity index 91% rename from cmd/lncli/chainrpc_default.go rename to cmd/commands/chainrpc_default.go index fa1ea99e2..28440a839 100644 --- a/cmd/lncli/chainrpc_default.go +++ b/cmd/commands/chainrpc_default.go @@ -1,7 +1,7 @@ //go:build !chainrpc // +build !chainrpc -package main +package commands import "github.com/urfave/cli" diff --git a/cmd/lncli/cmd_custom.go b/cmd/commands/cmd_custom.go similarity index 98% rename from cmd/lncli/cmd_custom.go rename to cmd/commands/cmd_custom.go index 7ff5d8a71..728d70bd3 100644 --- a/cmd/lncli/cmd_custom.go +++ b/cmd/commands/cmd_custom.go @@ -1,4 +1,4 @@ -package main +package commands import ( "encoding/hex" diff --git a/cmd/lncli/cmd_debug.go b/cmd/commands/cmd_debug.go similarity index 99% rename from cmd/lncli/cmd_debug.go rename to cmd/commands/cmd_debug.go index 758bff576..37024f5ec 100644 --- a/cmd/lncli/cmd_debug.go +++ b/cmd/commands/cmd_debug.go @@ -1,4 +1,4 @@ -package main +package commands import ( "bytes" diff --git a/cmd/lncli/cmd_import_mission_control.go b/cmd/commands/cmd_import_mission_control.go similarity index 99% rename from cmd/lncli/cmd_import_mission_control.go rename to cmd/commands/cmd_import_mission_control.go index 420396322..ac15b2f1f 100644 --- a/cmd/lncli/cmd_import_mission_control.go +++ b/cmd/commands/cmd_import_mission_control.go @@ -1,4 +1,4 @@ -package main +package commands import ( "context" diff --git a/cmd/lncli/cmd_invoice.go b/cmd/commands/cmd_invoice.go similarity index 99% rename from cmd/lncli/cmd_invoice.go rename to cmd/commands/cmd_invoice.go index a7ed2b8b2..5ea4c1e0b 100644 --- a/cmd/lncli/cmd_invoice.go +++ b/cmd/commands/cmd_invoice.go @@ -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 diff --git a/cmd/lncli/cmd_macaroon.go b/cmd/commands/cmd_macaroon.go similarity index 99% rename from cmd/lncli/cmd_macaroon.go rename to cmd/commands/cmd_macaroon.go index deea4e7ad..149c7db45 100644 --- a/cmd/lncli/cmd_macaroon.go +++ b/cmd/commands/cmd_macaroon.go @@ -1,4 +1,4 @@ -package main +package commands import ( "bytes" diff --git a/cmd/lncli/cmd_mission_control.go b/cmd/commands/cmd_mission_control.go similarity index 99% rename from cmd/lncli/cmd_mission_control.go rename to cmd/commands/cmd_mission_control.go index 323acdff6..fe4acb25c 100644 --- a/cmd/lncli/cmd_mission_control.go +++ b/cmd/commands/cmd_mission_control.go @@ -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 } diff --git a/cmd/lncli/cmd_open_channel.go b/cmd/commands/cmd_open_channel.go similarity index 99% rename from cmd/lncli/cmd_open_channel.go rename to cmd/commands/cmd_open_channel.go index f0585ed47..b4fe83f20 100644 --- a/cmd/lncli/cmd_open_channel.go +++ b/cmd/commands/cmd_open_channel.go @@ -1,4 +1,4 @@ -package main +package commands import ( "bytes" diff --git a/cmd/lncli/cmd_payments.go b/cmd/commands/cmd_payments.go similarity index 95% rename from cmd/lncli/cmd_payments.go rename to cmd/commands/cmd_payments.go index 228dd3415..c8787a6cc 100644 --- a/cmd/lncli/cmd_payments.go +++ b/cmd/commands/cmd_payments.go @@ -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()) } diff --git a/cmd/lncli/cmd_profile.go b/cmd/commands/cmd_profile.go similarity index 99% rename from cmd/lncli/cmd_profile.go rename to cmd/commands/cmd_profile.go index 3b1bf6487..6767964eb 100644 --- a/cmd/lncli/cmd_profile.go +++ b/cmd/commands/cmd_profile.go @@ -1,4 +1,4 @@ -package main +package commands import ( "fmt" diff --git a/cmd/lncli/cmd_state.go b/cmd/commands/cmd_state.go similarity index 98% rename from cmd/lncli/cmd_state.go rename to cmd/commands/cmd_state.go index afca13e9d..c2522b721 100644 --- a/cmd/lncli/cmd_state.go +++ b/cmd/commands/cmd_state.go @@ -1,4 +1,4 @@ -package main +package commands import ( "context" diff --git a/cmd/lncli/cmd_update_chan_status.go b/cmd/commands/cmd_update_chan_status.go similarity index 99% rename from cmd/lncli/cmd_update_chan_status.go rename to cmd/commands/cmd_update_chan_status.go index 23c22f0b1..3525f7c5c 100644 --- a/cmd/lncli/cmd_update_chan_status.go +++ b/cmd/commands/cmd_update_chan_status.go @@ -1,4 +1,4 @@ -package main +package commands import ( "errors" diff --git a/cmd/lncli/cmd_version.go b/cmd/commands/cmd_version.go similarity index 98% rename from cmd/lncli/cmd_version.go rename to cmd/commands/cmd_version.go index 99cc72995..9e7a2b077 100644 --- a/cmd/lncli/cmd_version.go +++ b/cmd/commands/cmd_version.go @@ -1,4 +1,4 @@ -package main +package commands import ( "fmt" diff --git a/cmd/lncli/cmd_walletunlocker.go b/cmd/commands/cmd_walletunlocker.go similarity index 99% rename from cmd/lncli/cmd_walletunlocker.go rename to cmd/commands/cmd_walletunlocker.go index 650ef80ea..844b48f3d 100644 --- a/cmd/lncli/cmd_walletunlocker.go +++ b/cmd/commands/cmd_walletunlocker.go @@ -1,4 +1,4 @@ -package main +package commands import ( "bufio" diff --git a/cmd/lncli/commands.go b/cmd/commands/commands.go similarity index 97% rename from cmd/lncli/commands.go rename to cmd/commands/commands.go index fefef6ab2..2c1191ab2 100644 --- a/cmd/lncli/commands.go +++ b/cmd/commands/commands.go @@ -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() diff --git a/cmd/lncli/commands_test.go b/cmd/commands/commands_test.go similarity index 58% rename from cmd/lncli/commands_test.go rename to cmd/commands/commands_test.go index a1f967561..bab2c8d2b 100644 --- a/cmd/lncli/commands_test.go +++ b/cmd/commands/commands_test.go @@ -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)) + }) + } +} diff --git a/cmd/lncli/devrpc_active.go b/cmd/commands/devrpc_active.go similarity index 98% rename from cmd/lncli/devrpc_active.go rename to cmd/commands/devrpc_active.go index da3f08a97..8d1960e46 100644 --- a/cmd/lncli/devrpc_active.go +++ b/cmd/commands/devrpc_active.go @@ -1,7 +1,7 @@ //go:build dev // +build dev -package main +package commands import ( "fmt" diff --git a/cmd/lncli/devrpc_default.go b/cmd/commands/devrpc_default.go similarity index 90% rename from cmd/lncli/devrpc_default.go rename to cmd/commands/devrpc_default.go index b9362cb42..1c5b482c3 100644 --- a/cmd/lncli/devrpc_default.go +++ b/cmd/commands/devrpc_default.go @@ -1,7 +1,7 @@ //go:build !dev // +build !dev -package main +package commands import "github.com/urfave/cli" diff --git a/cmd/lncli/invoicesrpc_active.go b/cmd/commands/invoicesrpc_active.go similarity index 99% rename from cmd/lncli/invoicesrpc_active.go rename to cmd/commands/invoicesrpc_active.go index 823af67bf..0ee767c8b 100644 --- a/cmd/lncli/invoicesrpc_active.go +++ b/cmd/commands/invoicesrpc_active.go @@ -1,7 +1,7 @@ //go:build invoicesrpc // +build invoicesrpc -package main +package commands import ( "encoding/hex" diff --git a/cmd/lncli/invoicesrpc_default.go b/cmd/commands/invoicesrpc_default.go similarity index 92% rename from cmd/lncli/invoicesrpc_default.go rename to cmd/commands/invoicesrpc_default.go index cca3c14e9..e925e55d6 100644 --- a/cmd/lncli/invoicesrpc_default.go +++ b/cmd/commands/invoicesrpc_default.go @@ -1,7 +1,7 @@ //go:build !invoicesrpc // +build !invoicesrpc -package main +package commands import "github.com/urfave/cli" diff --git a/cmd/lncli/macaroon_jar.go b/cmd/commands/macaroon_jar.go similarity index 99% rename from cmd/lncli/macaroon_jar.go rename to cmd/commands/macaroon_jar.go index f54f29a26..d3a4345b0 100644 --- a/cmd/lncli/macaroon_jar.go +++ b/cmd/commands/macaroon_jar.go @@ -1,4 +1,4 @@ -package main +package commands import ( "encoding/base64" diff --git a/cmd/lncli/macaroon_jar_test.go b/cmd/commands/macaroon_jar_test.go similarity index 99% rename from cmd/lncli/macaroon_jar_test.go rename to cmd/commands/macaroon_jar_test.go index 8e1d1c6bd..6d76dce84 100644 --- a/cmd/lncli/macaroon_jar_test.go +++ b/cmd/commands/macaroon_jar_test.go @@ -1,4 +1,4 @@ -package main +package commands import ( "encoding/hex" diff --git a/cmd/commands/main.go b/cmd/commands/main.go new file mode 100644 index 000000000..71a8531d9 --- /dev/null +++ b/cmd/commands/main.go @@ -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// 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) + } +} diff --git a/cmd/lncli/neutrino_active.go b/cmd/commands/neutrino_active.go similarity index 99% rename from cmd/lncli/neutrino_active.go rename to cmd/commands/neutrino_active.go index 099da46c6..f34c7cc0e 100644 --- a/cmd/lncli/neutrino_active.go +++ b/cmd/commands/neutrino_active.go @@ -1,7 +1,7 @@ //go:build neutrinorpc // +build neutrinorpc -package main +package commands import ( "github.com/lightningnetwork/lnd/lnrpc/neutrinorpc" diff --git a/cmd/lncli/neutrino_default.go b/cmd/commands/neutrino_default.go similarity index 92% rename from cmd/lncli/neutrino_default.go rename to cmd/commands/neutrino_default.go index f1f1de404..b269e1238 100644 --- a/cmd/lncli/neutrino_default.go +++ b/cmd/commands/neutrino_default.go @@ -1,7 +1,7 @@ //go:build !neutrinorpc // +build !neutrinorpc -package main +package commands import "github.com/urfave/cli" diff --git a/cmd/lncli/peersrpc_active.go b/cmd/commands/peersrpc_active.go similarity index 99% rename from cmd/lncli/peersrpc_active.go rename to cmd/commands/peersrpc_active.go index c044166d3..0736750c7 100644 --- a/cmd/lncli/peersrpc_active.go +++ b/cmd/commands/peersrpc_active.go @@ -1,7 +1,7 @@ //go:build peersrpc // +build peersrpc -package main +package commands import ( "fmt" diff --git a/cmd/lncli/peersrpc_default.go b/cmd/commands/peersrpc_default.go similarity index 91% rename from cmd/lncli/peersrpc_default.go rename to cmd/commands/peersrpc_default.go index 24cb2b813..57c8aa7a9 100644 --- a/cmd/lncli/peersrpc_default.go +++ b/cmd/commands/peersrpc_default.go @@ -1,7 +1,7 @@ //go:build !peersrpc // +build !peersrpc -package main +package commands import "github.com/urfave/cli" diff --git a/cmd/lncli/profile.go b/cmd/commands/profile.go similarity index 99% rename from cmd/lncli/profile.go rename to cmd/commands/profile.go index efbdfc147..0d63ca93c 100644 --- a/cmd/lncli/profile.go +++ b/cmd/commands/profile.go @@ -1,4 +1,4 @@ -package main +package commands import ( "bytes" diff --git a/cmd/lncli/routerrpc.go b/cmd/commands/routerrpc.go similarity index 95% rename from cmd/lncli/routerrpc.go rename to cmd/commands/routerrpc.go index 30b892224..82211affd 100644 --- a/cmd/lncli/routerrpc.go +++ b/cmd/commands/routerrpc.go @@ -1,4 +1,4 @@ -package main +package commands import "github.com/urfave/cli" diff --git a/cmd/lncli/types.go b/cmd/commands/types.go similarity index 99% rename from cmd/lncli/types.go rename to cmd/commands/types.go index a93d3e2c6..2a82e7100 100644 --- a/cmd/lncli/types.go +++ b/cmd/commands/types.go @@ -1,4 +1,4 @@ -package main +package commands import ( "encoding/hex" diff --git a/cmd/lncli/walletrpc_active.go b/cmd/commands/walletrpc_active.go similarity index 99% rename from cmd/lncli/walletrpc_active.go rename to cmd/commands/walletrpc_active.go index 15c62755f..ff56adf4d 100644 --- a/cmd/lncli/walletrpc_active.go +++ b/cmd/commands/walletrpc_active.go @@ -1,7 +1,7 @@ //go:build walletrpc // +build walletrpc -package main +package commands import ( "bytes" diff --git a/cmd/lncli/walletrpc_default.go b/cmd/commands/walletrpc_default.go similarity index 91% rename from cmd/lncli/walletrpc_default.go rename to cmd/commands/walletrpc_default.go index d6670e449..90c627c2a 100644 --- a/cmd/lncli/walletrpc_default.go +++ b/cmd/commands/walletrpc_default.go @@ -1,7 +1,7 @@ //go:build !walletrpc // +build !walletrpc -package main +package commands import "github.com/urfave/cli" diff --git a/cmd/lncli/walletrpc_types.go b/cmd/commands/walletrpc_types.go similarity index 99% rename from cmd/lncli/walletrpc_types.go rename to cmd/commands/walletrpc_types.go index ab251d18f..4369151fa 100644 --- a/cmd/lncli/walletrpc_types.go +++ b/cmd/commands/walletrpc_types.go @@ -1,4 +1,4 @@ -package main +package commands import "github.com/lightningnetwork/lnd/lnrpc/walletrpc" diff --git a/cmd/lncli/watchtower_active.go b/cmd/commands/watchtower_active.go similarity index 98% rename from cmd/lncli/watchtower_active.go rename to cmd/commands/watchtower_active.go index 9c31c6ec4..bc5cd1969 100644 --- a/cmd/lncli/watchtower_active.go +++ b/cmd/commands/watchtower_active.go @@ -1,7 +1,7 @@ //go:build watchtowerrpc // +build watchtowerrpc -package main +package commands import ( "github.com/lightningnetwork/lnd/lnrpc/watchtowerrpc" diff --git a/cmd/lncli/watchtower_default.go b/cmd/commands/watchtower_default.go similarity index 92% rename from cmd/lncli/watchtower_default.go rename to cmd/commands/watchtower_default.go index e3db3ccf3..c958a66bd 100644 --- a/cmd/lncli/watchtower_default.go +++ b/cmd/commands/watchtower_default.go @@ -1,7 +1,7 @@ //go:build !watchtowerrpc // +build !watchtowerrpc -package main +package commands import "github.com/urfave/cli" diff --git a/cmd/lncli/wtclient.go b/cmd/commands/wtclient.go similarity index 99% rename from cmd/lncli/wtclient.go rename to cmd/commands/wtclient.go index d73f6ca61..075861b98 100644 --- a/cmd/lncli/wtclient.go +++ b/cmd/commands/wtclient.go @@ -1,4 +1,4 @@ -package main +package commands import ( "encoding/hex" diff --git a/cmd/lncli/main.go b/cmd/lncli/main.go index b1554fb07..ea5195a0c 100644 --- a/cmd/lncli/main.go +++ b/cmd/lncli/main.go @@ -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// 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() } diff --git a/funding/manager.go b/funding/manager.go index 8ad16005c..61b03200e 100644 --- a/funding/manager.go +++ b/funding/manager.go @@ -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()) diff --git a/go.mod b/go.mod index f82781766..bbe3c8845 100644 --- a/go.mod +++ b/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 diff --git a/go.sum b/go.sum index d040d8cf8..a51e291b8 100644 --- a/go.sum +++ b/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= diff --git a/htlcswitch/interceptable_switch.go b/htlcswitch/interceptable_switch.go index 0ac19a36c..5517eb82d 100644 --- a/htlcswitch/interceptable_switch.go +++ b/htlcswitch/interceptable_switch.go @@ -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 { diff --git a/htlcswitch/interfaces.go b/htlcswitch/interfaces.go index 1311373a1..246c76902 100644 --- a/htlcswitch/interfaces.go +++ b/htlcswitch/interfaces.go @@ -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. diff --git a/input/script_desc.go b/input/script_desc.go index a32ae6631..17b58b721 100644 --- a/input/script_desc.go +++ b/input/script_desc.go @@ -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 diff --git a/input/script_utils.go b/input/script_utils.go index 104c24251..d801846f8 100644 --- a/input/script_utils.go +++ b/input/script_utils.go @@ -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) diff --git a/lnrpc/routerrpc/forward_interceptor.go b/lnrpc/routerrpc/forward_interceptor.go index 8af1a21f6..55c77d6d9 100644 --- a/lnrpc/routerrpc/forward_interceptor.go +++ b/lnrpc/routerrpc/forward_interceptor.go @@ -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 diff --git a/lnrpc/routerrpc/router_server.go b/lnrpc/routerrpc/router_server.go index a88bb4d0c..30dba2227 100644 --- a/lnrpc/routerrpc/router_server.go +++ b/lnrpc/routerrpc/router_server.go @@ -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() diff --git a/lnwallet/chanfunding/assembler.go b/lnwallet/chanfunding/interface.go similarity index 96% rename from lnwallet/chanfunding/assembler.go rename to lnwallet/chanfunding/interface.go index 2fdb87b32..3512b32ff 100644 --- a/lnwallet/chanfunding/assembler.go +++ b/lnwallet/chanfunding/interface.go @@ -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 diff --git a/lnwallet/channel.go b/lnwallet/channel.go index 65ae01356..7a6278522 100644 --- a/lnwallet/channel.go +++ b/lnwallet/channel.go @@ -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 diff --git a/lnwallet/channel_test.go b/lnwallet/channel_test.go index fcaf0fc08..757a808fe 100644 --- a/lnwallet/channel_test.go +++ b/lnwallet/channel_test.go @@ -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") } diff --git a/lnwallet/commitment.go b/lnwallet/commitment.go index 2cf58f494..f32408814 100644 --- a/lnwallet/commitment.go +++ b/lnwallet/commitment.go @@ -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, ) } diff --git a/lnwallet/reservation.go b/lnwallet/reservation.go index 529db1141..df6c8fd94 100644 --- a/lnwallet/reservation.go +++ b/lnwallet/reservation.go @@ -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. diff --git a/lnwallet/test_utils.go b/lnwallet/test_utils.go index 0e5d52723..126b76d6c 100644 --- a/lnwallet/test_utils.go +++ b/lnwallet/test_utils.go @@ -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( diff --git a/lnwallet/transactions_test.go b/lnwallet/transactions_test.go index 4c7fbed53..e9751c6b9 100644 --- a/lnwallet/transactions_test.go +++ b/lnwallet/transactions_test.go @@ -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( diff --git a/lnwire/custom_records.go b/lnwire/custom_records.go new file mode 100644 index 000000000..1e1988842 --- /dev/null +++ b/lnwire/custom_records.go @@ -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 +} diff --git a/lnwire/custom_records_test.go b/lnwire/custom_records_test.go new file mode 100644 index 000000000..0338d0159 --- /dev/null +++ b/lnwire/custom_records_test.go @@ -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() +} diff --git a/lnwire/encoding.go b/lnwire/encoding.go index e04b2b01d..72000f81d 100644 --- a/lnwire/encoding.go +++ b/lnwire/encoding.go @@ -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) diff --git a/lnwire/extra_bytes_test.go b/lnwire/extra_bytes_test.go index fd9f28841..97a908bce 100644 --- a/lnwire/extra_bytes_test.go +++ b/lnwire/extra_bytes_test.go @@ -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. diff --git a/server.go b/server.go index 02161bf73..26fdce6ea 100644 --- a/server.go +++ b/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: