diff --git a/chainreg/chainregistry.go b/chainreg/chainregistry.go index 934a84996b8..8e0142eec7a 100644 --- a/chainreg/chainregistry.go +++ b/chainreg/chainregistry.go @@ -68,6 +68,10 @@ type Config struct { // leaves for certain custom channel types. AuxLeafStore fn.Option[lnwallet.AuxLeafStore] + // AuxSigner is an optional signer that can be used to sign auxiliary + // leaves for certain custom channel types. + AuxSigner fn.Option[lnwallet.AuxSigner] + // BlockCache is the main cache for storing block information. BlockCache *blockcache.BlockCache diff --git a/cmd/lncli/arg_parse.go b/cmd/commands/arg_parse.go similarity index 98% rename from cmd/lncli/arg_parse.go rename to cmd/commands/arg_parse.go index 49d165d5569..045f35509a2 100644 --- a/cmd/lncli/arg_parse.go +++ b/cmd/commands/arg_parse.go @@ -1,4 +1,4 @@ -package main +package commands import ( "regexp" diff --git a/cmd/lncli/arg_parse_test.go b/cmd/commands/arg_parse_test.go similarity index 99% rename from cmd/lncli/arg_parse_test.go rename to cmd/commands/arg_parse_test.go index 571292d2c66..ead411fe61b 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" 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 961e8599473..212ef45797d 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 7fb88521709..393b6f124f4 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 48946e0d5d3..0f1f8b61210 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 fa1ea99e2c9..28440a839e6 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 7ff5d8a71e3..728d70bd39a 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 758bff576de..37024f5ecf7 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 23935753d29..b4aa19931f3 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 4c60294caad..785f6a6abe9 100644 --- a/cmd/lncli/cmd_invoice.go +++ b/cmd/commands/cmd_invoice.go @@ -1,4 +1,4 @@ -package main +package commands import ( "encoding/hex" 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 54e03057ab9..ed299182f42 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 323acdff6d2..da56339a45f 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" 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 74cb1668afb..0583b1c062b 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" @@ -65,7 +65,8 @@ Signed base64 encoded PSBT or hex encoded raw wire TX (or path to file): ` ) // TODO(roasbeef): change default number of confirmations. -var openChannelCommand = cli.Command{ + +var OpenChannelCommand = cli.Command{ Name: "openchannel", Category: "Channels", Usage: "Open a channel to a node or an existing peer.", @@ -284,10 +285,14 @@ var openChannelCommand = cli.Command{ allowed length is 500 characters`, }, }, - Action: actionDecorator(openChannel), + Action: actionDecorator(func(c *cli.Context) error { + return OpenChannel(c, nil) + }), } -func openChannel(ctx *cli.Context) error { +func OpenChannel(ctx *cli.Context, + reqDecorator RequestDecorator[*lnrpc.OpenChannelRequest]) error { + // TODO(roasbeef): add deadline to context ctxc := getContext() client, cleanUp := getClient(ctx) @@ -451,6 +456,13 @@ func openChannel(ctx *cli.Context) error { return fmt.Errorf("unsupported channel type %v", channelType) } + if reqDecorator != nil { + err = reqDecorator(ctx, req) + if err != nil { + return err + } + } + // PSBT funding is a more involved, interactive process that is too // large to also fit into this already long function. if ctx.Bool("psbt") { diff --git a/cmd/lncli/cmd_payments.go b/cmd/commands/cmd_payments.go similarity index 99% rename from cmd/lncli/cmd_payments.go rename to cmd/commands/cmd_payments.go index 550bb8f3e7b..87316ef4935 100644 --- a/cmd/lncli/cmd_payments.go +++ b/cmd/commands/cmd_payments.go @@ -1,4 +1,4 @@ -package main +package commands import ( "bytes" 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 21666be9645..45bd6904199 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 afca13e9d6e..c2522b721be 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 23c22f0b166..3525f7c5c60 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 99cc7299539..9e7a2b0775b 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 9227d10d4ff..8a9393adcd7 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 5ffca5c7393..3746ddaace3 100644 --- a/cmd/lncli/commands.go +++ b/cmd/commands/commands.go @@ -1,4 +1,4 @@ -package main +package commands import ( "bufio" @@ -12,6 +12,7 @@ import ( "io/ioutil" "math" "os" + "regexp" "strconv" "strings" "sync" @@ -42,8 +43,44 @@ 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-z]+)"`, + ) +) + +func replaceCustomData(jsonBytes []byte) ([]byte, error) { + if customDataPattern.Match(jsonBytes) { + jsonBytes = 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, jsonBytes, "", " ") + if err != nil { + return nil, err + } + + jsonBytes = buf.Bytes() + } + + return jsonBytes, nil +} func getContext() context.Context { shutdownInterceptor, err := signal.Intercept() @@ -67,7 +104,7 @@ func printJSON(resp interface{}) { } var out bytes.Buffer - json.Indent(&out, b, "", "\t") + json.Indent(&out, b, "", " ") out.WriteString("\n") out.WriteTo(os.Stdout) } @@ -79,7 +116,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 @@ -1553,7 +1596,11 @@ func pendingChannels(ctx *cli.Context) error { return nil } -var listChannelsCommand = cli.Command{ +type RequestDecorator[T proto.Message] func(*cli.Context, T) error + +type ResponseDecorator[T proto.Message] func(*cli.Context, T) error + +var ListChannelsCommand = cli.Command{ Name: "listchannels", Category: "Channels", Usage: "List all open channels.", @@ -1586,35 +1633,14 @@ var listChannelsCommand = cli.Command{ "order to improve performance", }, }, - Action: actionDecorator(listChannels), + Action: actionDecorator(func(c *cli.Context) error { + return ListChannels(c, nil) + }), } -var listAliasesCommand = cli.Command{ - Name: "listaliases", - Category: "Channels", - Usage: "List all aliases.", - Flags: []cli.Flag{}, - Action: actionDecorator(listaliases), -} +func ListChannels(ctx *cli.Context, + respDecorator ResponseDecorator[*lnrpc.ListChannelsResponse]) error { -func listaliases(ctx *cli.Context) error { - ctxc := getContext() - client, cleanUp := getClient(ctx) - defer cleanUp() - - req := &lnrpc.ListAliasesRequest{} - - resp, err := client.ListAliases(ctxc, req) - if err != nil { - return err - } - - printRespJSON(resp) - - return nil -} - -func listChannels(ctx *cli.Context) error { ctxc := getContext() client, cleanUp := getClient(ctx) defer cleanUp() @@ -1651,6 +1677,38 @@ func listChannels(ctx *cli.Context) error { return err } + if respDecorator != nil { + err = respDecorator(ctx, resp) + if err != nil { + return err + } + } + + printRespJSON(resp) + + return nil +} + +var listAliasesCommand = cli.Command{ + Name: "listaliases", + Category: "Channels", + Usage: "List all aliases.", + Flags: []cli.Flag{}, + Action: actionDecorator(listAliases), +} + +func listAliases(ctx *cli.Context) error { + ctxc := getContext() + client, cleanUp := getClient(ctx) + defer cleanUp() + + req := &lnrpc.ListAliasesRequest{} + + resp, err := client.ListAliases(ctxc, req) + if err != nil { + return err + } + printRespJSON(resp) return nil diff --git a/cmd/lncli/commands_test.go b/cmd/commands/commands_test.go similarity index 71% rename from cmd/lncli/commands_test.go rename to cmd/commands/commands_test.go index a1f967561e4..76ed624eb85 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,55 @@ func TestParseTimeLockDelta(t *testing.T) { } } } + +func TestReplaceCustomData(t *testing.T) { + t.Parallel() + + testCases := []struct { + name string + data string + replaceData string + expected 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" + } +}`, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + result, err := replaceCustomData([]byte(tc.data)) + 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 da3f08a97d7..8d1960e461b 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 b9362cb421f..1c5b482c324 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 2ce90069563..d0dbc011b0e 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 cca3c14e9f6..e925e55d694 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 f54f29a26c1..d3a4345b0ba 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 8e1d1c6bd41..6d76dce848b 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 00000000000..4aed13d7cd6 --- /dev/null +++ b/cmd/commands/main.go @@ -0,0 +1,594 @@ +// Copyright (c) 2013-2017 The btcsuite developers +// Copyright (c) 2015-2016 The Decred developers +// Copyright (C) 2015-2022 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 099da46c6e8..f34c7cc0e27 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 f1f1de404ba..b269e123863 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 c044166d360..0736750c734 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 24cb2b8134c..57c8aa7a97f 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 90ac69c0ee6..e9f6369a250 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 30b8922249b..82211affdff 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 b878811a0a3..1faed7f9b1f 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 e9e5cc5757f..2be509a4bbf 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 d6670e44997..90c627c2a5a 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 98% rename from cmd/lncli/walletrpc_types.go rename to cmd/commands/walletrpc_types.go index b6680a6ede0..8edc31d4871 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 9c31c6ec4b2..bc5cd196958 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 e3db3ccf36d..c958a66bdc1 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 d73f6ca612a..075861b981c 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 b1554fb0705..52d682eab0b 100644 --- a/cmd/lncli/main.go +++ b/cmd/lncli/main.go @@ -4,591 +4,8 @@ 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/config_builder.go b/config_builder.go index 7af32738243..7c9045d7eb8 100644 --- a/config_builder.go +++ b/config_builder.go @@ -35,6 +35,7 @@ import ( "github.com/lightningnetwork/lnd/channeldb" "github.com/lightningnetwork/lnd/clock" "github.com/lightningnetwork/lnd/fn" + "github.com/lightningnetwork/lnd/funding" "github.com/lightningnetwork/lnd/invoices" "github.com/lightningnetwork/lnd/keychain" "github.com/lightningnetwork/lnd/kvdb" @@ -44,6 +45,7 @@ import ( "github.com/lightningnetwork/lnd/lnwallet/btcwallet" "github.com/lightningnetwork/lnd/lnwallet/rpcwallet" "github.com/lightningnetwork/lnd/macaroons" + "github.com/lightningnetwork/lnd/protofsm" "github.com/lightningnetwork/lnd/rpcperms" "github.com/lightningnetwork/lnd/signal" "github.com/lightningnetwork/lnd/sqldb" @@ -157,6 +159,20 @@ type AuxComponents struct { // AuxLeafStore is an optional data source that can be used by custom // channels to fetch+store various data. AuxLeafStore fn.Option[lnwallet.AuxLeafStore] + + // MsgRouter is an optional message router that if set will be used in + // place of a new balnk default message router. + MsgRouter fn.Option[protofsm.MsgRouter] + + // AuxFundingController is an optional controller that can be used to + // modify the way we handle certain custom channel types. It's also + // able to automatically handle new custom protocol messages related to + // the funding process. + AuxFundingController fn.Option[funding.AuxFundingController] + + // AuxSigner is an optional signer that can be used to sign auxiliary + // leaves for certain custom channel types. + AuxSigner fn.Option[lnwallet.AuxSigner] } // DefaultWalletImpl is the default implementation of our normal, btcwallet @@ -563,6 +579,7 @@ func (d *DefaultWalletImpl) BuildWalletConfig(ctx context.Context, ChanStateDB: dbs.ChanStateDB.ChannelStateDB(), NeutrinoCS: neutrinoCS, AuxLeafStore: aux.AuxLeafStore, + AuxSigner: aux.AuxSigner, ActiveNetParams: d.cfg.ActiveNetParams, FeeURL: d.cfg.FeeURL, Dialer: func(addr string) (net.Conn, error) { @@ -715,6 +732,7 @@ func (d *DefaultWalletImpl) BuildChainControl( NetParams: *walletConfig.NetParams, CoinSelectionStrategy: walletConfig.CoinSelectionStrategy, AuxLeafStore: partialChainControl.Cfg.AuxLeafStore, + AuxSigner: partialChainControl.Cfg.AuxSigner, } // The broadcast is already always active for neutrino nodes, so we @@ -894,10 +912,6 @@ type DatabaseInstances struct { // for native SQL queries for tables that already support it. This may // be nil if the use-native-sql flag was not set. NativeSQLStore *sqldb.BaseDB - - // AuxLeafStore is an optional data source that can be used by custom - // channels to fetch+store various data. - AuxLeafStore fn.Option[lnwallet.AuxLeafStore] } // DefaultDatabaseBuilder is a type that builds the default database backends diff --git a/contractcourt/chain_arbitrator.go b/contractcourt/chain_arbitrator.go index 3245162cb59..d7d9e93cfd3 100644 --- a/contractcourt/chain_arbitrator.go +++ b/contractcourt/chain_arbitrator.go @@ -221,6 +221,10 @@ type ChainArbitratorConfig struct { // AuxLeafStore is an optional store that can be used to store auxiliary // leaves for certain custom channel types. AuxLeafStore fn.Option[lnwallet.AuxLeafStore] + + // AuxSigner is an optional signer that can be used to sign auxiliary + // leaves for certain custom channel types. + AuxSigner fn.Option[lnwallet.AuxSigner] } // ChainArbitrator is a sub-system that oversees the on-chain resolution of all @@ -307,6 +311,9 @@ func (a *arbChannel) NewAnchorResolutions() (*lnwallet.AnchorResolutions, a.c.cfg.AuxLeafStore.WhenSome(func(s lnwallet.AuxLeafStore) { chanOpts = append(chanOpts, lnwallet.WithLeafStore(s)) }) + a.c.cfg.AuxSigner.WhenSome(func(s lnwallet.AuxSigner) { + chanOpts = append(chanOpts, lnwallet.WithAuxSigner(s)) + }) chanMachine, err := lnwallet.NewLightningChannel( a.c.cfg.Signer, channel, nil, chanOpts..., @@ -357,6 +364,9 @@ func (a *arbChannel) ForceCloseChan() (*lnwallet.LocalForceCloseSummary, error) a.c.cfg.AuxLeafStore.WhenSome(func(s lnwallet.AuxLeafStore) { chanOpts = append(chanOpts, lnwallet.WithLeafStore(s)) }) + a.c.cfg.AuxSigner.WhenSome(func(s lnwallet.AuxSigner) { + chanOpts = append(chanOpts, lnwallet.WithAuxSigner(s)) + }) // Finally, we'll force close the channel completing // the force close workflow. diff --git a/contractcourt/chain_watcher.go b/contractcourt/chain_watcher.go index 89280d1bd1c..9cc56f9992b 100644 --- a/contractcourt/chain_watcher.go +++ b/contractcourt/chain_watcher.go @@ -308,7 +308,7 @@ func (c *chainWatcher) Start() error { fundingOpts := fn.MapOptionZ( chanState.TapscriptRoot, lnwallet.TapscriptRootToOpt, ) - c.fundingPkScript, _, err = input.GenTaprootFundingScript( + c.fundingPkScript, _, _, err = input.GenTaprootFundingScript( localKey, remoteKey, 0, fundingOpts..., ) if err != nil { diff --git a/contractcourt/htlc_incoming_contest_resolver.go b/contractcourt/htlc_incoming_contest_resolver.go index e73e3e45b2f..39dd1b38e11 100644 --- a/contractcourt/htlc_incoming_contest_resolver.go +++ b/contractcourt/htlc_incoming_contest_resolver.go @@ -18,6 +18,7 @@ import ( "github.com/lightningnetwork/lnd/lnwallet" "github.com/lightningnetwork/lnd/lnwire" "github.com/lightningnetwork/lnd/queue" + "github.com/lightningnetwork/lnd/tlv" ) // htlcIncomingContestResolver is a ContractResolver that's able to resolve an @@ -308,7 +309,8 @@ func (h *htlcIncomingContestResolver) Resolve( resolution, err := h.Registry.NotifyExitHopHtlc( h.htlc.RHash, h.htlc.Amt, h.htlcExpiry, currentHeight, - circuitKey, hodlQueue.ChanIn(), payload, + circuitKey, fn.None[tlv.Blob](), hodlQueue.ChanIn(), + payload, ) if err != nil { return nil, err diff --git a/contractcourt/interfaces.go b/contractcourt/interfaces.go index 0d53b07b665..57196df3aba 100644 --- a/contractcourt/interfaces.go +++ b/contractcourt/interfaces.go @@ -7,6 +7,7 @@ import ( "github.com/btcsuite/btcd/wire" "github.com/lightningnetwork/lnd/channeldb" "github.com/lightningnetwork/lnd/channeldb/models" + "github.com/lightningnetwork/lnd/fn" "github.com/lightningnetwork/lnd/htlcswitch/hop" "github.com/lightningnetwork/lnd/input" "github.com/lightningnetwork/lnd/invoices" @@ -14,6 +15,7 @@ import ( "github.com/lightningnetwork/lnd/lnwallet/chainfee" "github.com/lightningnetwork/lnd/lnwire" "github.com/lightningnetwork/lnd/sweep" + "github.com/lightningnetwork/lnd/tlv" ) // Registry is an interface which represents the invoice registry. @@ -29,7 +31,9 @@ type Registry interface { // the resolution is sent on the passed in hodlChan later. NotifyExitHopHtlc(payHash lntypes.Hash, paidAmount lnwire.MilliSatoshi, expiry uint32, currentHeight int32, - circuitKey models.CircuitKey, hodlChan chan<- interface{}, + circuitKey models.CircuitKey, + msgCustomRecords fn.Option[tlv.Blob], + hodlChan chan<- interface{}, payload invoices.Payload) (invoices.HtlcResolution, error) // HodlUnsubscribeAll unsubscribes from all htlc resolutions. diff --git a/contractcourt/mock_registry_test.go b/contractcourt/mock_registry_test.go index 7acac67ec5e..22329ee9c71 100644 --- a/contractcourt/mock_registry_test.go +++ b/contractcourt/mock_registry_test.go @@ -25,7 +25,8 @@ type mockRegistry struct { func (r *mockRegistry) NotifyExitHopHtlc(payHash lntypes.Hash, paidAmount lnwire.MilliSatoshi, expiry uint32, currentHeight int32, - circuitKey models.CircuitKey, hodlChan chan<- interface{}, + circuitKey models.CircuitKey, msgCustomRecords fn.Option[tlv.Blob], + hodlChan chan<- interface{}, payload invoices.Payload) (invoices.HtlcResolution, error) { r.notifyChan <- notifyExitHopData{ diff --git a/funding/aux_funding.go b/funding/aux_funding.go new file mode 100644 index 00000000000..9f041c4e01b --- /dev/null +++ b/funding/aux_funding.go @@ -0,0 +1,63 @@ +package funding + +import ( + "github.com/btcsuite/btcd/chaincfg/chainhash" + "github.com/lightningnetwork/lnd/channeldb" + "github.com/lightningnetwork/lnd/fn" + "github.com/lightningnetwork/lnd/lnwallet" + "github.com/lightningnetwork/lnd/protofsm" +) + +// AuxFundingController permits the implementation of the funding of custom +// channels types. The controller serves as a MsgEndpoint which allows it to +// intercept custom messages, or even the regular funding messages. The +// controller might also pass along an aux funding desc based on an existing +// pending channel ID. +type AuxFundingController interface { + // The controller is also a message endpoint. This'll allow it to + // handle custom messages specific to the funding type. + protofsm.MsgEndpoint + + // DescFromPendingChanID takes a pending channel ID, that may already be + // known due to prior custom channel messages, and maybe returns an aux + // funding desc which can be used to modify how a channel is funded. + DescFromPendingChanID(pid PendingChanID, + openChan *channeldb.OpenChannel, + localKeyRing, remoteKeyRing lnwallet.CommitmentKeyRing, + initiator bool) (fn.Option[lnwallet.AuxFundingDesc], error) + + // DeriveTapscriptRoot takes a pending channel ID and maybe returns a + // tapscript root that should be used when creating any musig2 sessions + // for a channel. + DeriveTapscriptRoot(PendingChanID) (fn.Option[chainhash.Hash], error) +} + +// descFromPendingChanID takes a pending channel ID, that may already be +// known due to prior custom channel messages, and maybe returns an aux +// funding desc which can be used to modify how a channel is funded. +func descFromPendingChanID(controller fn.Option[AuxFundingController], + chanID PendingChanID, openChan *channeldb.OpenChannel, + localKeyRing, remoteKeyRing lnwallet.CommitmentKeyRing, + initiator bool) (fn.Option[lnwallet.AuxFundingDesc], error) { + + if controller.IsNone() { + return fn.None[lnwallet.AuxFundingDesc](), nil + } + + return controller.UnsafeFromSome().DescFromPendingChanID( + chanID, openChan, localKeyRing, remoteKeyRing, initiator, + ) +} + +// deriveTapscriptRoot takes a pending channel ID and maybe returns a +// tapscript root that should be used when creating any musig2 sessions +// for a channel. +func deriveTapscriptRoot(controller fn.Option[AuxFundingController], + chanID PendingChanID) (fn.Option[chainhash.Hash], error) { + + if controller.IsNone() { + return fn.None[chainhash.Hash](), nil + } + + return controller.UnsafeFromSome().DeriveTapscriptRoot(chanID) +} diff --git a/funding/manager.go b/funding/manager.go index 4a9c0122d61..d40f545950f 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" @@ -98,7 +99,6 @@ const ( // you and limitless channel size (apart from 21 million cap). MaxBtcFundingAmountWumbo = btcutil.Amount(1000000000) - // TODO(roasbeef): tune. msgBufferSize = 50 // MaxWaitNumBlocksFundingConf is the maximum number of blocks to wait @@ -288,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 @@ -299,6 +299,8 @@ type InitFundingMsg struct { // channel that will be useful to our future selves. Memo []byte + CustomChannelData []byte + // Updates is a channel which updates to the opening status of the // channel are sent on. Updates chan *lnrpc.OpenStatusUpdate @@ -318,7 +320,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 @@ -542,6 +544,16 @@ type Config struct { // AuxLeafStore is an optional store that can be used to store auxiliary // leaves for certain custom channel types. AuxLeafStore fn.Option[lnwallet.AuxLeafStore] + + // AuxFundingController is an optional controller that can be used to + // modify the way we handle certain custom chanenl types. It's also + // able to automatically handle new custom protocol messages related to + // the funding process. + AuxFundingController fn.Option[AuxFundingController] + + // AuxSigner is an optional signer that can be used to sign auxiliary + // leaves for certain custom channel types. + AuxSigner fn.Option[lnwallet.AuxSigner] } // Manager acts as an orchestrator/bridge between the wallet's @@ -565,10 +577,11 @@ type Manager struct { // temporary channel ID's. chanIDKey [32]byte + nonceMtx sync.RWMutex + // chanIDNonce is a nonce that's incremented for each new funding // reservation created. - nonceMtx sync.RWMutex - chanIDNonce uint64 + chanIDNonce atomic.Uint64 // pendingMusigNonces is used to store the musig2 nonce we generate to // send funding locked until we receive a funding locked message from @@ -590,7 +603,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. @@ -797,24 +810,29 @@ 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 { +func (f *Manager) nextPendingChanID() PendingChanID { // 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() + 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 } @@ -1044,7 +1062,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() @@ -1064,6 +1083,9 @@ func (f *Manager) advanceFundingState(channel *channeldb.OpenChannel, f.cfg.AuxLeafStore.WhenSome(func(s lnwallet.AuxLeafStore) { chanOpts = append(chanOpts, lnwallet.WithLeafStore(s)) }) + f.cfg.AuxSigner.WhenSome(func(s lnwallet.AuxSigner) { + chanOpts = append(chanOpts, lnwallet.WithAuxSigner(s)) + }) // We create the state-machine object which wraps the database state. lnChannel, err := lnwallet.NewLightningChannel( @@ -1119,7 +1141,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 { @@ -1243,7 +1265,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. @@ -1605,6 +1627,18 @@ func (f *Manager) fundeeProcessOpenChannel(peer lnpeer.Peer, return } + // At this point, if we have an AuxFundingController active, we'll + // check to see if we have a special tapscript root to use in our + // musig2 funding output. + tapscriptRoot, err := deriveTapscriptRoot( + f.cfg.AuxFundingController, msg.PendingChannelID, + ) + if err != nil { + err = fmt.Errorf("error deriving tapscript root: %w", err) + log.Error(err) + f.failFundingFlow(peer, cid, err) + } + req := &lnwallet.InitFundingReserveMsg{ ChainHash: &msg.ChainHash, PendingChanID: msg.PendingChannelID, @@ -1621,6 +1655,7 @@ func (f *Manager) fundeeProcessOpenChannel(peer lnpeer.Peer, ZeroConf: zeroConf, OptionScidAlias: scid, ScidAliasFeature: scidFeatureVal, + TapscriptRoot: tapscriptRoot, } reservation, err := f.cfg.Wallet.InitChannelReservation(req) @@ -2217,10 +2252,27 @@ func (f *Manager) waitForPsbt(intent *chanfunding.PsbtIntent, return } + // At this point, we'll see if there's an AuxFundingDesc we + // need to deliver so the funding process can continue + // properly. + chanState := resCtx.reservation.ChanState() + localKeys, remoteKeys := resCtx.reservation.CommitmentKeyRings() + auxFundingDesc, err := descFromPendingChanID( + f.cfg.AuxFundingController, cid.tempChanID, chanState, + *localKeys, *remoteKeys, true, + ) + if err != nil { + failFlow("error continuing PSBT flow", err) + return + } + // A non-nil error means we can continue the funding flow. // Notify the wallet so it can prepare everything we need to // continue. - err = resCtx.reservation.ProcessPsbt() + // + // We'll also pass along the aux funding controller as well, + // which may be used to help process the finalized PSBT. + err = resCtx.reservation.ProcessPsbt(auxFundingDesc) if err != nil { failFlow("error continuing PSBT flow", err) return @@ -2229,8 +2281,8 @@ func (f *Manager) waitForPsbt(intent *chanfunding.PsbtIntent, // We are now ready to continue the funding flow. f.continueFundingAccept(resCtx, cid) - // Handle a server shutdown as well because the reservation won't - // survive a restart as it's in memory only. + // Handle a server shutdown as well because the reservation won't + // survive a restart as it's in memory only. case <-f.quit: log.Errorf("Unable to handle funding accept message "+ "for peer_key=%x, pending_chan_id=%x: funding manager "+ @@ -2284,6 +2336,10 @@ func (f *Manager) continueFundingAccept(resCtx *reservationWithCtx, // funding flow fails. cid.setChanID(channelID) + // Now that we're ready to resume the funding flow, we'll call into the + // aux controller with the final funding details so we can obtain the + // funding descs we need. + // Send the FundingCreated msg. fundingCreated := &lnwire.FundingCreated{ PendingChannelID: cid.tempChanID, @@ -2346,7 +2402,6 @@ func (f *Manager) fundeeProcessFundingCreated(peer lnpeer.Peer, // final funding transaction, as well as a signature for our version of // the commitment transaction. So at this point, we can validate the // initiator's commitment transaction, then send our own if it's valid. - // TODO(roasbeef): make case (p vs P) consistent throughout fundingOut := msg.FundingPoint log.Infof("completing pending_id(%x) with ChannelPoint(%v)", pendingChanID[:], fundingOut) @@ -2378,16 +2433,33 @@ func (f *Manager) fundeeProcessFundingCreated(peer lnpeer.Peer, } } + // At this point, we'll see if there's an AuxFundingDesc we need to + // deliver so the funding process can continue properly. + chanState := resCtx.reservation.ChanState() + localKeys, remoteKeys := resCtx.reservation.CommitmentKeyRings() + auxFundingDesc, err := descFromPendingChanID( + f.cfg.AuxFundingController, cid.tempChanID, chanState, + *localKeys, *remoteKeys, true, + ) + if err != nil { + log.Errorf("error continuing PSBT flow: %v", err) + f.failFundingFlow(peer, cid, err) + return + } + // With all the necessary data available, attempt to advance the // funding workflow to the next stage. If this succeeds then the // funding transaction will broadcast after our next message. // CompleteReservationSingle will also mark the channel as 'IsPending' // in the database. + // + // We'll also directly pass in the AuxFundiner controller as well, + // which may be used by the reservation system to finalize funding our + // side. completeChan, err := resCtx.reservation.CompleteReservationSingle( - &fundingOut, commitSig, + &fundingOut, commitSig, auxFundingDesc, ) if err != nil { - // TODO(roasbeef): better error logging: peerID, channelID, etc. log.Errorf("unable to complete single reservation: %v", err) f.failFundingFlow(peer, cid, err) return @@ -2698,9 +2770,6 @@ func (f *Manager) funderProcessFundingSigned(peer lnpeer.Peer, // Send an update to the upstream client that the negotiation process // is over. - // - // TODO(roasbeef): add abstraction over updates to accommodate - // long-polling, or SSE, etc. upd := &lnrpc.OpenStatusUpdate{ Update: &lnrpc.OpenStatusUpdate_ChanPending{ ChanPending: &lnrpc.PendingUpdate{ @@ -2744,7 +2813,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 @@ -2866,7 +2935,7 @@ func makeFundingScript(channel *channeldb.OpenChannel) ([]byte, error) { fundingOpts := fn.MapOptionZ( channel.TapscriptRoot, lnwallet.TapscriptRootToOpt, ) - pkScript, _, err := input.GenTaprootFundingScript( + pkScript, _, _, err := input.GenTaprootFundingScript( localKey, remoteKey, int64(channel.Capacity), fundingOpts..., ) @@ -3607,7 +3676,7 @@ func (f *Manager) annAfterSixConfs(completeChan *channeldb.OpenChannel, // a zero-conf channel. This will wait for the real confirmation, add the // confirmed SCID to the router graph, and then announce after six confs. func (f *Manager) waitForZeroConfChannel(c *channeldb.OpenChannel, - pendingID [32]byte) error { + pendingID PendingChanID) error { // First we'll check whether the channel is confirmed on-chain. If it // is already confirmed, the chainntnfs subsystem will return with the @@ -3975,7 +4044,7 @@ func (f *Manager) handleChannelReady(peer lnpeer.Peer, //nolint:funlen // channel is now active, thus we change its state to `addedToRouterGraph` 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) @@ -4405,7 +4474,6 @@ func (f *Manager) announceChannel(localIDKey, remoteIDKey *btcec.PublicKey, // InitFundingWorkflow sends a message to the funding manager instructing it // to initiate a single funder workflow with the source peer. -// TODO(roasbeef): re-visit blocking nature.. func (f *Manager) InitFundingWorkflow(msg *InitFundingMsg) { f.fundingRequests <- msg } @@ -4499,7 +4567,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 { @@ -4595,6 +4663,18 @@ func (f *Manager) handleInitFundingMsg(msg *InitFundingMsg) { scidFeatureVal = true } + // At this point, if we have an AuxFundingController active, we'll + // check to see if we have a special tapscript root to use in our + // musig2 funding output. + tapscriptRoot, err := deriveTapscriptRoot( + f.cfg.AuxFundingController, chanID, + ) + if err != nil { + err = fmt.Errorf("error deriving tapscript root: %w", err) + msg.Err <- err + return + } + req := &lnwallet.InitFundingReserveMsg{ ChainHash: &msg.ChainHash, PendingChanID: chanID, @@ -4618,6 +4698,8 @@ func (f *Manager) handleInitFundingMsg(msg *InitFundingMsg) { OptionScidAlias: scid, ScidAliasFeature: scidFeatureVal, Memo: msg.Memo, + TapscriptRoot: tapscriptRoot, + CustomChannelData: msg.CustomChannelData, } reservation, err := f.cfg.Wallet.InitChannelReservation(req) @@ -4904,7 +4986,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[:]) @@ -4952,7 +5035,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() @@ -4975,7 +5058,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() @@ -4995,7 +5078,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/htlcswitch/interfaces.go b/htlcswitch/interfaces.go index 2c27d8ab037..b73d5c1f8d9 100644 --- a/htlcswitch/interfaces.go +++ b/htlcswitch/interfaces.go @@ -6,11 +6,13 @@ import ( "github.com/btcsuite/btcd/wire" "github.com/lightningnetwork/lnd/channeldb" "github.com/lightningnetwork/lnd/channeldb/models" + "github.com/lightningnetwork/lnd/fn" "github.com/lightningnetwork/lnd/invoices" "github.com/lightningnetwork/lnd/lntypes" "github.com/lightningnetwork/lnd/lnwallet/chainfee" "github.com/lightningnetwork/lnd/lnwire" "github.com/lightningnetwork/lnd/record" + "github.com/lightningnetwork/lnd/tlv" ) // InvoiceDatabase is an interface which represents the persistent subsystem @@ -29,7 +31,9 @@ type InvoiceDatabase interface { // for decoding purposes. NotifyExitHopHtlc(payHash lntypes.Hash, paidAmount lnwire.MilliSatoshi, expiry uint32, currentHeight int32, - circuitKey models.CircuitKey, hodlChan chan<- interface{}, + circuitKey models.CircuitKey, + msgCustomRecords fn.Option[tlv.Blob], + hodlChan chan<- interface{}, payload invoices.Payload) (invoices.HtlcResolution, error) // CancelInvoice attempts to cancel the invoice corresponding to the diff --git a/htlcswitch/link.go b/htlcswitch/link.go index 778e78d7008..0d8f0ed073a 100644 --- a/htlcswitch/link.go +++ b/htlcswitch/link.go @@ -2554,6 +2554,7 @@ func (l *channelLink) updateCommitTx() error { CommitSig: newCommit.CommitSig, HtlcSigs: newCommit.HtlcSigs, PartialSig: newCommit.PartialSig, + ExtraData: newCommit.AuxSigBlob, } l.cfg.Peer.SendMessage(false, commitSig) @@ -3563,7 +3564,7 @@ func (l *channelLink) processExitHop(pd *lnwallet.PaymentDescriptor, event, err := l.cfg.Registry.NotifyExitHopHtlc( invoiceHash, pd.Amount, pd.Timeout, int32(heightNow), - circuitKey, l.hodlQueue.ChanIn(), payload, + circuitKey, pd.CustomRecords, l.hodlQueue.ChanIn(), payload, ) if err != nil { return err diff --git a/htlcswitch/mock.go b/htlcswitch/mock.go index a0f38c74fef..8b1b5765100 100644 --- a/htlcswitch/mock.go +++ b/htlcswitch/mock.go @@ -27,6 +27,7 @@ import ( "github.com/lightningnetwork/lnd/channeldb/models" "github.com/lightningnetwork/lnd/clock" "github.com/lightningnetwork/lnd/contractcourt" + "github.com/lightningnetwork/lnd/fn" "github.com/lightningnetwork/lnd/htlcswitch/hop" "github.com/lightningnetwork/lnd/invoices" "github.com/lightningnetwork/lnd/lnpeer" @@ -35,6 +36,7 @@ import ( "github.com/lightningnetwork/lnd/lnwallet/chainfee" "github.com/lightningnetwork/lnd/lnwire" "github.com/lightningnetwork/lnd/ticker" + "github.com/lightningnetwork/lnd/tlv" ) func isAlias(scid lnwire.ShortChannelID) bool { @@ -1029,12 +1031,13 @@ func (i *mockInvoiceRegistry) SettleHodlInvoice( func (i *mockInvoiceRegistry) NotifyExitHopHtlc(rhash lntypes.Hash, amt lnwire.MilliSatoshi, expiry uint32, currentHeight int32, - circuitKey models.CircuitKey, hodlChan chan<- interface{}, + circuitKey models.CircuitKey, msgCustomRecords fn.Option[tlv.Blob], + hodlChan chan<- interface{}, payload invoices.Payload) (invoices.HtlcResolution, error) { event, err := i.registry.NotifyExitHopHtlc( - rhash, amt, expiry, currentHeight, circuitKey, hodlChan, - payload, + rhash, amt, expiry, currentHeight, circuitKey, msgCustomRecords, + hodlChan, payload, ) if err != nil { return nil, err diff --git a/input/script_desc.go b/input/script_desc.go index a32ae66316f..17b58b72196 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 5ad0a0e90d9..7f719082cc7 100644 --- a/input/script_utils.go +++ b/input/script_utils.go @@ -224,9 +224,12 @@ func WithTapscriptRoot(root chainhash.Hash) FundingScriptOpt { } // GenTaprootFundingScript constructs the taproot-native funding output that -// uses MuSig2 to create a single aggregated key to anchor the channel. -func GenTaprootFundingScript(aPub, bPub *btcec.PublicKey, - amt int64, opts ...FundingScriptOpt) ([]byte, *wire.TxOut, error) { +// uses MuSig2 to create a single aggregated key to anchor the channel. This +// also returns the MuSig2 aggregated key to allow the callers to examine the +// pre tweaked key as well as the final combined key. +func GenTaprootFundingScript(aPub, bPub *btcec.PublicKey, amt int64, + opts ...FundingScriptOpt) ([]byte, *wire.TxOut, + *musig2.AggregateKey, error) { options := defaultFundingScriptOpts() for _, optFunc := range opts { @@ -247,14 +250,15 @@ func GenTaprootFundingScript(aPub, bPub *btcec.PublicKey, []*btcec.PublicKey{aPub, bPub}, true, muSig2Opt, ) if err != nil { - return nil, nil, fmt.Errorf("unable to combine keys: %w", err) + return nil, nil, nil, fmt.Errorf("unable to combine "+ + "keys: %w", err) } // Now that we have the combined key, we can create a taproot pkScript // from this, and then make the txOut given the amount. pkScript, err := PayToTaprootScript(combinedKey.FinalKey) if err != nil { - return nil, nil, fmt.Errorf("unable to make taproot "+ + return nil, nil, nil, fmt.Errorf("unable to make taproot "+ "pkscript: %w", err) } @@ -262,7 +266,7 @@ func GenTaprootFundingScript(aPub, bPub *btcec.PublicKey, // For the "witness program" we just return the raw pkScript since the // output we create can _only_ be spent with a MuSig2 signature. - return pkScript, txOut, nil + return pkScript, txOut, combinedKey, nil } // SpendMultiSig generates the witness stack required to redeem the 2-of-2 p2wsh @@ -729,8 +733,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: @@ -748,6 +752,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) @@ -1748,9 +1757,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 @@ -1758,8 +1767,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: @@ -1774,8 +1783,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: @@ -1791,6 +1800,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) @@ -2133,9 +2147,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 @@ -2144,8 +2158,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, @@ -2163,8 +2177,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: @@ -2184,6 +2198,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, selfKey, @@ -2311,7 +2330,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, @@ -2366,9 +2385,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 } @@ -2850,8 +2866,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). @@ -2887,9 +2903,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 @@ -2897,8 +2913,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: @@ -2913,8 +2929,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: @@ -2930,6 +2946,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/invoices/interface.go b/invoices/interface.go index 490db1be568..e9070034a8c 100644 --- a/invoices/interface.go +++ b/invoices/interface.go @@ -5,9 +5,11 @@ import ( "time" "github.com/lightningnetwork/lnd/channeldb/models" + "github.com/lightningnetwork/lnd/fn" "github.com/lightningnetwork/lnd/lntypes" "github.com/lightningnetwork/lnd/lnwire" "github.com/lightningnetwork/lnd/record" + "github.com/lightningnetwork/lnd/tlv" ) // InvoiceDB is the database that stores the information about invoices. @@ -198,3 +200,46 @@ type InvoiceUpdater interface { // Finalize finalizes the update before it is written to the database. Finalize(updateType UpdateType) error } + +// InterceptClientRequest is the request that is passed to the client via +// callback during an interceptor session. The request contains the invoice that +// is being intercepted and supporting information. +type InterceptClientRequest struct { + // MsgCustomRecords is the custom records that were parsed from the + // HTLC p2p message. + MsgCustomRecords fn.Option[tlv.Blob] + + // ExitHtlcCircuitKey is the circuit key that identifies the HTLC which + // is involved in the invoice settlement. + ExitHtlcCircuitKey CircuitKey + + // ExitHtlcAmt is the amount of the HTLC which is involved in the + // invoice settlement. + ExitHtlcAmt lnwire.MilliSatoshi + + // ExitHtlcExpiry is the expiry time of the HTLC which is involved in + // the invoice settlement. + ExitHtlcExpiry uint32 + + // CurrentHeight is the current block height. + CurrentHeight uint32 + + // Invoice is the invoice that is being intercepted. + Invoice Invoice +} + +// InterceptorClientCallback is a function that is called when an invoice is +// intercepted by the invoice interceptor. +type InterceptorClientCallback func(InterceptClientRequest) error + +// SettlementInterceptorInterface is an interface that allows the caller to +// intercept and specify invoice settlement outcomes. +type SettlementInterceptorInterface interface { + // SetClientCallback sets the client callback function that is called + // when an invoice is intercepted. + SetClientCallback(InterceptorClientCallback) + + // Resolve is called by the caller to settle an invoice with the + // corresponding resolution. + Resolve(lntypes.Hash, bool) error +} diff --git a/invoices/invoiceregistry.go b/invoices/invoiceregistry.go index de731b4740b..d546d9d21ae 100644 --- a/invoices/invoiceregistry.go +++ b/invoices/invoiceregistry.go @@ -9,10 +9,12 @@ import ( "time" "github.com/lightningnetwork/lnd/clock" + "github.com/lightningnetwork/lnd/fn" "github.com/lightningnetwork/lnd/lntypes" "github.com/lightningnetwork/lnd/lnwire" "github.com/lightningnetwork/lnd/queue" "github.com/lightningnetwork/lnd/record" + "github.com/lightningnetwork/lnd/tlv" ) var ( @@ -74,6 +76,11 @@ type RegistryConfig struct { // KeysendHoldTime indicates for how long we want to accept and hold // spontaneous keysend payments. KeysendHoldTime time.Duration + + // SettlementInterceptor is a service that intercepts invoices during + // the settlement phase, enabling a subscribed client to determine the + // settlement outcome. + SettlementInterceptor *SettlementInterceptor } // htlcReleaseEvent describes an htlc auto-release event. It is used to release @@ -886,8 +893,8 @@ func (i *InvoiceRegistry) processAMP(ctx invoiceUpdateCtx) error { // held htlc. func (i *InvoiceRegistry) NotifyExitHopHtlc(rHash lntypes.Hash, amtPaid lnwire.MilliSatoshi, expiry uint32, currentHeight int32, - circuitKey CircuitKey, hodlChan chan<- interface{}, - payload Payload) (HtlcResolution, error) { + circuitKey CircuitKey, msgCustomRecords fn.Option[tlv.Blob], + hodlChan chan<- interface{}, payload Payload) (HtlcResolution, error) { // Create the update context containing the relevant details of the // incoming htlc. @@ -898,6 +905,7 @@ func (i *InvoiceRegistry) NotifyExitHopHtlc(rHash lntypes.Hash, expiry: expiry, currentHeight: currentHeight, finalCltvRejectDelta: i.cfg.FinalCltvRejectDelta, + msgCustomRecords: msgCustomRecords, customRecords: payload.CustomRecords(), mpp: payload.MultiPath(), amp: payload.AMPRecord(), @@ -998,6 +1006,45 @@ func (i *InvoiceRegistry) notifyExitHopHtlcLocked( ) callback := func(inv *Invoice) (*InvoiceUpdateDesc, error) { + //setID := ctx.setID() + //htlcSet := inv.HTLCSet(setID, HtlcStateAccepted) + + // Provide the invoice to the settlement interceptor to allow + // the interceptor's client an opportunity to manipulate the + // settlement process. + var interceptSession fn.Option[InterceptSession] + if i.cfg.SettlementInterceptor != nil { + clientReq := InterceptClientRequest{ + MsgCustomRecords: ctx.msgCustomRecords, + ExitHtlcCircuitKey: ctx.circuitKey, + ExitHtlcAmt: ctx.amtPaid, + ExitHtlcExpiry: ctx.expiry, + CurrentHeight: uint32(ctx.currentHeight), + Invoice: *inv, + } + interceptSession = + i.cfg.SettlementInterceptor.Intercept(clientReq) + } + + // If the interceptor service has provided a response, we'll + // use the interceptor session to wait for the client to respond + // with a settlement resolution. + interceptSession.WhenSome(func(session InterceptSession) { + log.Debug("Waiting for client response from " + + "settlement interceptor session") + + select { + case resp := <-session.ClientResponseChannel: + log.Debugf("Received settlement interceptor "+ + "response: %v", resp) + ctx.SkipAmountCheck = resp.SkipAmountCheck + + case <-session.Quit: + // At this point, the interceptor session has + // quit. + } + }) + updateDesc, res, err := updateInvoice(ctx, inv) if err != nil { return nil, err @@ -1051,6 +1098,8 @@ func (i *InvoiceRegistry) notifyExitHopHtlcLocked( var invoiceToExpire invoiceExpiry + log.Debugf("Settlement resolution: %T %v", resolution, resolution) + switch res := resolution.(type) { case *HtlcFailResolution: // Inspect latest htlc state on the invoice. If it is found, @@ -1099,6 +1148,8 @@ func (i *InvoiceRegistry) notifyExitHopHtlcLocked( // with our peer. setID := ctx.setID() settledHtlcSet := invoice.HTLCSet(setID, HtlcStateSettled) + log.Debugf("Settled htlc set size: %d", len(settledHtlcSet)) + for key, htlc := range settledHtlcSet { preimage := res.Preimage if htlc.AMP != nil && htlc.AMP.Preimage != nil { @@ -1183,7 +1234,7 @@ func (i *InvoiceRegistry) notifyExitHopHtlcLocked( } // Now that the links have been notified of any state changes to their - // HTLCs, we'll go ahead and notify any clients wiaiting on the invoice + // HTLCs, we'll go ahead and notify any clients waiting on the invoice // state changes. if updateSubscribers { // We'll add a setID onto the notification, but only if this is diff --git a/invoices/settlement_interceptor.go b/invoices/settlement_interceptor.go new file mode 100644 index 00000000000..bbb57baefe4 --- /dev/null +++ b/invoices/settlement_interceptor.go @@ -0,0 +1,229 @@ +package invoices + +import ( + "fmt" + "sync" + + "github.com/lightningnetwork/lnd/fn" + "github.com/lightningnetwork/lnd/lntypes" + "github.com/lightningnetwork/lnd/lnutils" +) + +// SafeCallback is a thread safe wrapper around an InterceptorClientCallback. +type SafeCallback struct { + // mu is a mutex that protects the callback field. + mu sync.Mutex + + // callback is the client callback function that is called when an + // invoice is intercepted. This function gives the client the ability to + // determine how the invoice should be settled. + callback InterceptorClientCallback +} + +// Set sets the client callback function. +func (sc *SafeCallback) Set(callback InterceptorClientCallback) { + sc.mu.Lock() + defer sc.mu.Unlock() + + sc.callback = callback +} + +// Exec calls the client callback function. +func (sc *SafeCallback) Exec(req InterceptClientRequest) error { + sc.mu.Lock() + defer sc.mu.Unlock() + + if sc.callback == nil { + return fmt.Errorf("client callback not set") + } + + return sc.callback(req) +} + +// IsSet returns true if the client callback function is set. +func (sc *SafeCallback) IsSet() bool { + sc.mu.Lock() + defer sc.mu.Unlock() + + return sc.callback != nil +} + +// InterceptClientResponse is the response that is sent from the client during +// an interceptor session. The response contains modifications to the invoice +// settlement process. +type InterceptClientResponse struct { + // SkipAmountCheck is a flag that indicates whether the amount check + // should be skipped during the invoice settlement process. + SkipAmountCheck bool +} + +// InterceptSession creates a session that is returned to the caller when an +// invoice is submitted to this service. This session allows the caller to block +// until the invoice is processed. +type InterceptSession struct { + InterceptClientRequest + + // ClientResponseChannel is a channel that is populated with the + // client's interceptor response during an interceptor session. + ClientResponseChannel chan InterceptClientResponse + + // Quit is a channel that is closed when the session is no longer + // needed. + Quit chan struct{} +} + +// SettlementInterceptor is a service that intercepts invoices during the +// settlement phase, enabling a subscribed client to determine the settlement +// outcome. +type SettlementInterceptor struct { + wg sync.WaitGroup + + // callback is a client defined function that is called when an invoice + // is intercepted. This function gives the client the ability to + // determine the settlement outcome. + clientCallback SafeCallback + + // activeSessions is a map of active intercept sessions that are used to + // manage the client query/response for a given invoice payment hash. + activeSessions lnutils.SyncMap[lntypes.Hash, InterceptSession] +} + +// NewSettlementInterceptor creates a new SettlementInterceptor. +func NewSettlementInterceptor() *SettlementInterceptor { + return &SettlementInterceptor{ + activeSessions: lnutils.SyncMap[ + lntypes.Hash, InterceptSession, + ]{}, + } +} + +// Intercept generates a new intercept session for the given invoice. The +// session is returned to the caller so that they can block until the +// client resolution is received. +func (s *SettlementInterceptor) Intercept( + clientRequest InterceptClientRequest) fn.Option[InterceptSession] { + + // If there is no client callback set we will not handle the invoice + // further. + if !s.clientCallback.IsSet() { + return fn.None[InterceptSession]() + } + + // Create and store a new intercept session for the invoice. We will use + // the payment hash as the storage/retrieval key for the session. + paymentHash := clientRequest.Invoice.Terms.PaymentPreimage.Hash() + session := InterceptSession{ + InterceptClientRequest: clientRequest, + ClientResponseChannel: make(chan InterceptClientResponse, 1), + Quit: make(chan struct{}, 1), + } + s.activeSessions.Store(paymentHash, session) + + // The callback function will block at the client's discretion. We will + // therefore execute it in a separate goroutine. + s.wg.Add(1) + go func() { + defer s.wg.Done() + + // By this point, we've already checked that the client callback + // is set. However, if the client callback has been set to nil + // since that check then Exec will return an error. + err := s.clientCallback.Exec(clientRequest) + if err != nil { + log.Errorf("client callback failed: %v", err) + } + }() + + // Return the session to the caller so that they can block until the + // resolution is received. + return fn.Some(session) +} + +// Resolve passes a client specified resolution to the session resolution +// channel associated with the given invoice payment hash. +func (s *SettlementInterceptor) Resolve(invoicePaymentHash lntypes.Hash, + skipAmountCheck bool) error { + + // Retrieve the intercept session for the invoice payment hash. + session, ok := s.activeSessions.LoadAndDelete( + invoicePaymentHash, + ) + if !ok { + return fmt.Errorf("invoice intercept session not found "+ + "(payment_hash=%s)", invoicePaymentHash.String()) + } + + // Send the resolution to the session resolution channel. + resolution := InterceptClientResponse{ + SkipAmountCheck: skipAmountCheck, + } + sendSuccessful := fn.SendOrQuit( + session.ClientResponseChannel, resolution, session.Quit, + ) + if !sendSuccessful { + return fmt.Errorf("failed to send resolution to client") + } + + return nil +} + +// SetClientCallback sets the client callback function that will be called when +// an invoice is intercepted. +func (s *SettlementInterceptor) SetClientCallback( + callback InterceptorClientCallback) { + + s.clientCallback.Set(callback) +} + +// QuitSession closes the quit channel for the session associated with the +// given invoice. This signals to the client that the session has ended. +func (s *SettlementInterceptor) QuitSession(session InterceptSession) error { + // Retrieve the intercept session and delete it from the local cache. + paymentHash := session.Invoice.Terms.PaymentPreimage.Hash() + session, ok := s.activeSessions.LoadAndDelete(paymentHash) + if !ok { + // If the session is not found, no further action is necessary. + return nil + } + + // Send to the quit channel to signal the client that the session has + // ended. + session.Quit <- struct{}{} + + return nil +} + +// QuitActiveSessions quits all active sessions by sending on each session quit +// channel. +func (s *SettlementInterceptor) QuitActiveSessions() error { + s.activeSessions.Range(func(_ lntypes.Hash, session InterceptSession) bool { //nolint:lll + session.Quit <- struct{}{} + + return true + }) + + // Empty the intercept sessions map. + s.activeSessions = lnutils.SyncMap[lntypes.Hash, InterceptSession]{} + + return nil +} + +// Start starts the service. +func (s *SettlementInterceptor) Start() error { + return nil +} + +// Stop stops the service. +func (s *SettlementInterceptor) Stop() error { + // If the service is stopping, we will quit all active sessions. + err := s.QuitActiveSessions() + if err != nil { + return err + } + + return nil +} + +// Ensure that SettlementInterceptor implements the HtlcResolutionInterceptor +// interface. +var _ SettlementInterceptorInterface = (*SettlementInterceptor)(nil) diff --git a/invoices/update.go b/invoices/update.go index ed60c278ca1..438463087dc 100644 --- a/invoices/update.go +++ b/invoices/update.go @@ -5,9 +5,11 @@ import ( "errors" "github.com/lightningnetwork/lnd/amp" + "github.com/lightningnetwork/lnd/fn" "github.com/lightningnetwork/lnd/lntypes" "github.com/lightningnetwork/lnd/lnwire" "github.com/lightningnetwork/lnd/record" + "github.com/lightningnetwork/lnd/tlv" ) // invoiceUpdateCtx is an object that describes the context for the invoice @@ -19,10 +21,22 @@ type invoiceUpdateCtx struct { expiry uint32 currentHeight int32 finalCltvRejectDelta int32 - customRecords record.CustomSet - mpp *record.MPP - amp *record.AMP - metadata []byte + + // msgCustomRecords is the custom records that were included with the + // HTLC p2p message. + msgCustomRecords fn.Option[tlv.Blob] + + // customRecords is a map of custom records that were included with the + // HTLC onion payload. + customRecords record.CustomSet + + mpp *record.MPP + amp *record.AMP + metadata []byte + + // SkipAmountCheck is a flag that indicates whether the amount check + // should be skipped during the invoice settlement process. + SkipAmountCheck bool } // invoiceRef returns an identifier that can be used to lookup or update the @@ -189,13 +203,13 @@ func updateMpp(ctx *invoiceUpdateCtx, inv *Invoice) (*InvoiceUpdateDesc, } // Don't accept zero-valued sets. - if ctx.mpp.TotalMsat() == 0 { + if !ctx.SkipAmountCheck && ctx.mpp.TotalMsat() == 0 { return nil, ctx.failRes(ResultHtlcSetTotalTooLow), nil } // Check that the total amt of the htlc set is high enough. In case this // is a zero-valued invoice, it will always be enough. - if ctx.mpp.TotalMsat() < inv.Terms.Value { + if !ctx.SkipAmountCheck && ctx.mpp.TotalMsat() < inv.Terms.Value { return nil, ctx.failRes(ResultHtlcSetTotalTooLow), nil } @@ -204,7 +218,7 @@ func updateMpp(ctx *invoiceUpdateCtx, inv *Invoice) (*InvoiceUpdateDesc, // Check whether total amt matches other htlcs in the set. var newSetTotal lnwire.MilliSatoshi for _, htlc := range htlcSet { - if ctx.mpp.TotalMsat() != htlc.MppTotalAmt { + if !ctx.SkipAmountCheck && ctx.mpp.TotalMsat() != htlc.MppTotalAmt { //nolint:lll return nil, ctx.failRes(ResultHtlcSetTotalMismatch), nil } @@ -239,7 +253,7 @@ func updateMpp(ctx *invoiceUpdateCtx, inv *Invoice) (*InvoiceUpdateDesc, // If the invoice cannot be settled yet, only record the htlc. setComplete := newSetTotal >= ctx.mpp.TotalMsat() - if !setComplete { + if !ctx.SkipAmountCheck && !setComplete { return &update, ctx.acceptRes(resultPartialAccepted), nil } diff --git a/itest/list_on_test.go b/itest/list_on_test.go index b4f586fa213..4d3d58d3ec9 100644 --- a/itest/list_on_test.go +++ b/itest/list_on_test.go @@ -422,6 +422,10 @@ var allTestCases = []*lntest.TestCase{ Name: "forward interceptor", TestFunc: testForwardInterceptorBasic, }, + { + Name: "invoice acceptor basic", + TestFunc: testInvoiceAcceptorBasic, + }, { Name: "zero conf channel open", TestFunc: testZeroConfChannelOpen, diff --git a/itest/lnd_funding_test.go b/itest/lnd_funding_test.go index 97613429ebe..a376c590667 100644 --- a/itest/lnd_funding_test.go +++ b/itest/lnd_funding_test.go @@ -1164,7 +1164,7 @@ func deriveFundingShim(ht *lntest.HarnessTest, carol, dave *node.HarnessNode, daveKey, err = btcec.ParsePubKey(daveFundingKey.RawKeyBytes) require.NoError(ht, err) - _, fundingOutput, err = input.GenTaprootFundingScript( + _, fundingOutput, _, err = input.GenTaprootFundingScript( carolKey, daveKey, int64(chanSize), ) require.NoError(ht, err) diff --git a/itest/lnd_invoice_acceptor_test.go b/itest/lnd_invoice_acceptor_test.go new file mode 100644 index 00000000000..7a08caba25d --- /dev/null +++ b/itest/lnd_invoice_acceptor_test.go @@ -0,0 +1,230 @@ +package itest + +import ( + "time" + + "github.com/btcsuite/btcd/btcutil" + "github.com/lightningnetwork/lnd/chainreg" + "github.com/lightningnetwork/lnd/lnrpc" + "github.com/lightningnetwork/lnd/lnrpc/invoicesrpc" + "github.com/lightningnetwork/lnd/lnrpc/routerrpc" + "github.com/lightningnetwork/lnd/lntest" + "github.com/lightningnetwork/lnd/lntest/node" + "github.com/lightningnetwork/lnd/lntypes" + "github.com/lightningnetwork/lnd/routing/route" + "github.com/stretchr/testify/require" +) + +// testInvoiceAcceptorBasic tests the basic functionality of the invoice +// acceptor RPC server. +func testInvoiceAcceptorBasic(ht *lntest.HarnessTest) { + ts := newAcceptorTestScenario(ht) + + alice, bob, carol := ts.alice, ts.bob, ts.carol + + // Open and wait for channels. + const chanAmt = btcutil.Amount(300000) + p := lntest.OpenChannelParams{Amt: chanAmt} + reqs := []*lntest.OpenChannelRequest{ + {Local: alice, Remote: bob, Param: p}, + {Local: bob, Remote: carol, Param: p}, + } + resp := ht.OpenMultiChannelsAsync(reqs) + cpAB, cpBC := resp[0], resp[1] + + // Make sure Alice is aware of channel Bob=>Carol. + ht.AssertTopologyChannelOpen(alice, cpBC) + + // Initiate Carol's invoice acceptor. + invoiceAcceptor, cancelInvoiceAcceptor := carol.RPC.InvoiceAcceptor() + + // Prepare the test cases. + testCases := ts.prepareTestCases() + + for tcIdx, tc := range testCases { + ht.Logf("Running test case: %d", tcIdx) + + // Initiate a payment from Alice to Carol in a separate + // goroutine. We use a separate goroutine to avoid blocking the + // main goroutine where we will make use of the invoice + // acceptor. + sendPaymentDone := make(chan struct{}) + go func() { + // Signal that all the payments have been sent. + defer close(sendPaymentDone) + + _ = ts.sendPayment(tc) + }() + + acceptorRequest := ht.ReceiveInvoiceAcceptor(invoiceAcceptor) + + // Sanity check the acceptor request. + require.EqualValues(ht, tc.invoiceAmountMsat, + acceptorRequest.Invoice.ValueMsat) + require.EqualValues(ht, tc.sendAmountMsat, + acceptorRequest.ExitHtlcAmt) + + preimage, err := lntypes.MakePreimage(tc.invoice.RPreimage) + require.NoError(ht, err, "failed to parse invoice preimage") + + // For all other packets we resolve according to the test case. + err = invoiceAcceptor.Send( + &invoicesrpc.InvoiceAcceptorResponse{ + Preimage: preimage[:], + SkipAmountCheck: tc.skipAmtCheck, + }, + ) + require.NoError(ht, err, "failed to send request") + + ht.Log("Waiting for payment send to complete") + select { + case <-sendPaymentDone: + ht.Log("Payment send attempt complete") + case <-time.After(defaultTimeout): + require.Fail(ht, "timeout waiting for payment send") + } + + ht.Log("Ensure invoice status is settled") + require.Eventually(ht, func() bool { + updatedInvoice := carol.RPC.LookupInvoice( + tc.invoice.RHash, + ) + + return updatedInvoice.State == tc.finalInvoiceState + }, defaultTimeout, 1*time.Second) + } + + cancelInvoiceAcceptor() + + // Finally, close channels. + ht.CloseChannel(alice, cpAB) + ht.CloseChannel(bob, cpBC) +} + +// acceptorTestCase is a helper struct to hold test case data. +type acceptorTestCase struct { + // invoiceAmountMsat is the amount of the invoice. + invoiceAmountMsat int64 + + // sendAmountMsat is the amount that will be sent in the payment. + sendAmountMsat int64 + + // skipAmtCheck is a flag that indicates whether the amount checks + // should be skipped during the invoice settlement process. + skipAmtCheck bool + + // finalInvoiceState is the expected eventual final state of the + // invoice. + finalInvoiceState lnrpc.Invoice_InvoiceState + + // payAddr is the payment address of the invoice. + payAddr []byte + + // invoice is the invoice that will be paid. + invoice *lnrpc.Invoice +} + +// acceptorTestScenario is a helper struct to hold the test context and provides +// helpful functionality. +type acceptorTestScenario struct { + ht *lntest.HarnessTest + alice, bob, carol *node.HarnessNode +} + +// newAcceptorTestScenario initializes a new test scenario with three nodes and +// connects them to have the following topology, +// +// Alice --> Bob --> Carol +// +// Among them, Alice and Bob are standby nodes and Carol is a new node. +func newAcceptorTestScenario( + ht *lntest.HarnessTest) *acceptorTestScenario { + + alice, bob := ht.Alice, ht.Bob + carol := ht.NewNode("carol", nil) + + ht.EnsureConnected(alice, bob) + ht.EnsureConnected(bob, carol) + + return &acceptorTestScenario{ + ht: ht, + alice: alice, + bob: bob, + carol: carol, + } +} + +// prepareTestCases prepares test cases. +func (c *acceptorTestScenario) prepareTestCases() []*acceptorTestCase { + cases := []*acceptorTestCase{ + // Send a payment with amount less than the invoice amount. + // Amount checking is skipped during the invoice settlement + // process. The sent payment should eventually result in the + // invoice being settled. + { + invoiceAmountMsat: 9000, + sendAmountMsat: 1000, + skipAmtCheck: true, + finalInvoiceState: lnrpc.Invoice_SETTLED, + }, + } + + for _, t := range cases { + inv := &lnrpc.Invoice{ValueMsat: t.invoiceAmountMsat} + addResponse := c.carol.RPC.AddInvoice(inv) + invoice := c.carol.RPC.LookupInvoice(addResponse.RHash) + + // We'll need to also decode the returned invoice so we can + // grab the payment address which is now required for ALL + // payments. + payReq := c.carol.RPC.DecodePayReq(invoice.PaymentRequest) + + t.invoice = invoice + t.payAddr = payReq.PaymentAddr + } + + return cases +} + +// buildRoute is a helper function to build a route with given hops. +func (c *acceptorTestScenario) buildRoute(amtMsat int64, + hops []*node.HarnessNode, payAddr []byte) *lnrpc.Route { + + rpcHops := make([][]byte, 0, len(hops)) + for _, hop := range hops { + k := hop.PubKeyStr + pubkey, err := route.NewVertexFromStr(k) + require.NoErrorf(c.ht, err, "error parsing %v: %v", k, err) + rpcHops = append(rpcHops, pubkey[:]) + } + + req := &routerrpc.BuildRouteRequest{ + AmtMsat: amtMsat, + FinalCltvDelta: chainreg.DefaultBitcoinTimeLockDelta, + HopPubkeys: rpcHops, + PaymentAddr: payAddr, + } + + routeResp := c.alice.RPC.BuildRoute(req) + + return routeResp.Route +} + +// sendPaymentAndAssertAction sends a payment from alice to carol. +func (c *acceptorTestScenario) sendPayment( + tc *acceptorTestCase) *lnrpc.HTLCAttempt { + + // Build a route from alice to carol. + aliceBobCarolRoute := c.buildRoute( + tc.sendAmountMsat, []*node.HarnessNode{c.bob, c.carol}, + tc.payAddr, + ) + + // Send the payment. + sendReq := &routerrpc.SendToRouteRequest{ + PaymentHash: tc.invoice.RHash, + Route: aliceBobCarolRoute, + } + + return c.alice.RPC.SendToRouteV2(sendReq) +} diff --git a/itest/lnd_psbt_test.go b/itest/lnd_psbt_test.go index a3b5f757b90..fd3db1211bf 100644 --- a/itest/lnd_psbt_test.go +++ b/itest/lnd_psbt_test.go @@ -177,6 +177,17 @@ func runPsbtChanFunding(ht *lntest.HarnessTest, carol, dave *node.HarnessNode, }, ) + // If this is a taproot channel, then we'll decode the PSBT to assert + // that an internal key is included. + if commitType == lnrpc.CommitmentType_SIMPLE_TAPROOT { + decodedPSBT, err := psbt.NewFromRawBytes( + bytes.NewReader(tempPsbt), false, + ) + require.NoError(ht, err) + + require.Len(ht, decodedPSBT.Outputs[0].TaprootInternalKey, 32) + } + // Let's add a second channel to the batch. This time between Carol and // Alice. We will publish the batch TX once this channel funding is // complete. diff --git a/lnd.go b/lnd.go index 9f628ff8125..526deb61bec 100644 --- a/lnd.go +++ b/lnd.go @@ -617,7 +617,8 @@ func Main(cfg *Config, lisCfg ListenerCfg, implCfg *ImplementationCfg, // start the RPC server. err = rpcServer.addDeps( server, interceptorChain.MacaroonService(), cfg.SubRPCServers, - atplManager, server.invoices, tower, multiAcceptor, + atplManager, server.invoices, + server.invoiceSettlementInterceptor, tower, multiAcceptor, ) if err != nil { return mkErr("unable to add deps to RPC server: %v", err) diff --git a/lnrpc/invoicesrpc/config_active.go b/lnrpc/invoicesrpc/config_active.go index a5b29c32a25..3324c6f82fe 100644 --- a/lnrpc/invoicesrpc/config_active.go +++ b/lnrpc/invoicesrpc/config_active.go @@ -30,6 +30,11 @@ type Config struct { // created by the daemon. InvoiceRegistry *invoices.InvoiceRegistry + // InvoiceSettlementInterceptor is a service which intercepts invoices + // during the settlement phase, enabling a subscribed client to + // determine the settlement outcome. + InvoiceSettlementInterceptor invoices.SettlementInterceptorInterface + // IsChannelActive is used to generate valid hop hints. IsChannelActive func(chanID lnwire.ChannelID) bool diff --git a/lnrpc/invoicesrpc/invoice_acceptor.go b/lnrpc/invoicesrpc/invoice_acceptor.go new file mode 100644 index 00000000000..c1852b01a3c --- /dev/null +++ b/lnrpc/invoicesrpc/invoice_acceptor.go @@ -0,0 +1,119 @@ +package invoicesrpc + +import ( + "github.com/btcsuite/btcd/chaincfg" + "github.com/lightningnetwork/lnd/invoices" + "github.com/lightningnetwork/lnd/lntypes" + "github.com/lightningnetwork/lnd/tlv" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" +) + +// invoiceAcceptorConfig contains the configuration for an RPC invoice acceptor +// server. +type invoiceAcceptorConfig struct { + // chainParams is required to properly marshall an invoice for RPC. + chainParams *chaincfg.Params + + // rpcServer is a bidirectional RPC server to send invoices to a client + // and receive accept responses from the client. + rpcServer Invoices_InvoiceAcceptorServer + + // interceptor is the invoice interceptor that will be used to intercept + // and resolve invoices. + interceptor invoices.SettlementInterceptorInterface +} + +// invoiceAcceptor is a helper struct that handles the lifecycle of an RPC +// invoice acceptor server instance. +// +// This struct handles passing send and receive RPC messages between the client +// and the invoice service. +type invoiceAcceptor struct { + // cfg contains the configuration for the invoice acceptor. + cfg invoiceAcceptorConfig +} + +// newInvoiceAcceptor creates a new RPC invoice acceptor handler. +func newInvoiceAcceptor(cfg invoiceAcceptorConfig) *invoiceAcceptor { + return &invoiceAcceptor{ + cfg: cfg, + } +} + +// run sends the intercepted invoices to the client and receives the +// corresponding responses. +func (r *invoiceAcceptor) run() error { + // Register our invoice interceptor. + r.cfg.interceptor.SetClientCallback(r.onIntercept) + defer r.cfg.interceptor.SetClientCallback(nil) + + // Listen for a response from the client in a loop. + for { + resp, err := r.cfg.rpcServer.Recv() + if err != nil { + return err + } + + if err := r.resolveFromClient(resp); err != nil { + return err + } + } +} + +// onIntercept is called when an invoice is intercepted by the invoice +// interceptor. This method sends the invoice to the client. +func (r *invoiceAcceptor) onIntercept( + req invoices.InterceptClientRequest) error { + + // Convert the circuit key to an RPC circuit key. + rpcCircuitKey := &CircuitKey{ + ChanId: req.ExitHtlcCircuitKey.ChanID.ToUint64(), + HtlcId: req.ExitHtlcCircuitKey.HtlcID, + } + + // Convert the invoice to an RPC invoice. + rpcInvoice, err := CreateRPCInvoice(&req.Invoice, r.cfg.chainParams) + if err != nil { + return err + } + + // Unpack the message custom records from the option. + var msgCustomRecords tlv.Blob + req.MsgCustomRecords.WhenSome(func(cr tlv.Blob) { + msgCustomRecords = cr + }) + + return r.cfg.rpcServer.Send(&InvoiceAcceptorRequest{ + Invoice: rpcInvoice, + ExitHtlcCircuitKey: rpcCircuitKey, + ExitHtlcAmt: uint64(req.ExitHtlcAmt), + ExitHtlcExpiry: req.ExitHtlcExpiry, + CurrentHeight: req.CurrentHeight, + ExitHtlcMsgCustomRecords: msgCustomRecords, + }) +} + +// resolveFromClient handles an invoice resolution received from the client. +func (r *invoiceAcceptor) resolveFromClient( + in *InvoiceAcceptorResponse) error { + + log.Tracef("Resolving invoice acceptor response %v", in) + + // Parse the invoice preimage from the response. + if len(in.Preimage) != lntypes.HashSize { + return status.Errorf(codes.InvalidArgument, + "Preimage has invalid length: %d", len(in.Preimage)) + } + preimage, err := lntypes.MakePreimage(in.Preimage) + if err != nil { + return status.Errorf(codes.InvalidArgument, + "Preimage is invalid: %v", err) + } + + // Derive the payment hash from the preimage. + paymentHash := preimage.Hash() + + // Pass the resolution to the interceptor. + return r.cfg.interceptor.Resolve(paymentHash, in.SkipAmountCheck) +} diff --git a/lnrpc/invoicesrpc/invoices.pb.go b/lnrpc/invoicesrpc/invoices.pb.go index 27cf424d30d..342a2bfcb78 100644 --- a/lnrpc/invoicesrpc/invoices.pb.go +++ b/lnrpc/invoicesrpc/invoices.pb.go @@ -617,6 +617,222 @@ func (*LookupInvoiceMsg_PaymentAddr) isLookupInvoiceMsg_InvoiceRef() {} func (*LookupInvoiceMsg_SetId) isLookupInvoiceMsg_InvoiceRef() {} +// CircuitKey is a unique identifier for an HTLC. +type CircuitKey struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // / The id of the channel that the is part of this circuit. + ChanId uint64 `protobuf:"varint,1,opt,name=chan_id,json=chanId,proto3" json:"chan_id,omitempty"` + // / The index of the incoming htlc in the incoming channel. + HtlcId uint64 `protobuf:"varint,2,opt,name=htlc_id,json=htlcId,proto3" json:"htlc_id,omitempty"` +} + +func (x *CircuitKey) Reset() { + *x = CircuitKey{} + if protoimpl.UnsafeEnabled { + mi := &file_invoicesrpc_invoices_proto_msgTypes[8] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *CircuitKey) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CircuitKey) ProtoMessage() {} + +func (x *CircuitKey) ProtoReflect() protoreflect.Message { + mi := &file_invoicesrpc_invoices_proto_msgTypes[8] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use CircuitKey.ProtoReflect.Descriptor instead. +func (*CircuitKey) Descriptor() ([]byte, []int) { + return file_invoicesrpc_invoices_proto_rawDescGZIP(), []int{8} +} + +func (x *CircuitKey) GetChanId() uint64 { + if x != nil { + return x.ChanId + } + return 0 +} + +func (x *CircuitKey) GetHtlcId() uint64 { + if x != nil { + return x.HtlcId + } + return 0 +} + +// InvoiceAcceptorRequest is a message that the server sends to the client to +// request the client to accept an invoice. +type InvoiceAcceptorRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // invoice is the invoice that the client should consider accepting. + Invoice *lnrpc.Invoice `protobuf:"bytes,1,opt,name=invoice,proto3" json:"invoice,omitempty"` + // exit_htlc_circuit_key is the key of the HTLC for which we is involved in + // the invoice settlement attempt. + ExitHtlcCircuitKey *CircuitKey `protobuf:"bytes,2,opt,name=exit_htlc_circuit_key,json=exitHtlcCircuitKey,proto3" json:"exit_htlc_circuit_key,omitempty"` + // exit_htlc_amt is the amount (millisats) that the client should consider + // accepting. + ExitHtlcAmt uint64 `protobuf:"varint,3,opt,name=exit_htlc_amt,json=exitHtlcAmt,proto3" json:"exit_htlc_amt,omitempty"` + // exit_htlc_expiry is the expiry time of the exit HTLC. + ExitHtlcExpiry uint32 `protobuf:"varint,4,opt,name=exit_htlc_expiry,json=exitHtlcExpiry,proto3" json:"exit_htlc_expiry,omitempty"` + // current_height is the current block height. + CurrentHeight uint32 `protobuf:"varint,5,opt,name=current_height,json=currentHeight,proto3" json:"current_height,omitempty"` + // exit_htlc_msg_custom_records is the p2p message custom records of the + // exit HTLC. + ExitHtlcMsgCustomRecords []byte `protobuf:"bytes,6,opt,name=exit_htlc_msg_custom_records,json=exitHtlcMsgCustomRecords,proto3" json:"exit_htlc_msg_custom_records,omitempty"` +} + +func (x *InvoiceAcceptorRequest) Reset() { + *x = InvoiceAcceptorRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_invoicesrpc_invoices_proto_msgTypes[9] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *InvoiceAcceptorRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*InvoiceAcceptorRequest) ProtoMessage() {} + +func (x *InvoiceAcceptorRequest) ProtoReflect() protoreflect.Message { + mi := &file_invoicesrpc_invoices_proto_msgTypes[9] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use InvoiceAcceptorRequest.ProtoReflect.Descriptor instead. +func (*InvoiceAcceptorRequest) Descriptor() ([]byte, []int) { + return file_invoicesrpc_invoices_proto_rawDescGZIP(), []int{9} +} + +func (x *InvoiceAcceptorRequest) GetInvoice() *lnrpc.Invoice { + if x != nil { + return x.Invoice + } + return nil +} + +func (x *InvoiceAcceptorRequest) GetExitHtlcCircuitKey() *CircuitKey { + if x != nil { + return x.ExitHtlcCircuitKey + } + return nil +} + +func (x *InvoiceAcceptorRequest) GetExitHtlcAmt() uint64 { + if x != nil { + return x.ExitHtlcAmt + } + return 0 +} + +func (x *InvoiceAcceptorRequest) GetExitHtlcExpiry() uint32 { + if x != nil { + return x.ExitHtlcExpiry + } + return 0 +} + +func (x *InvoiceAcceptorRequest) GetCurrentHeight() uint32 { + if x != nil { + return x.CurrentHeight + } + return 0 +} + +func (x *InvoiceAcceptorRequest) GetExitHtlcMsgCustomRecords() []byte { + if x != nil { + return x.ExitHtlcMsgCustomRecords + } + return nil +} + +// InvoiceAcceptorResponse is a message that the client sends to the server to +// indicate whether it accepts the invoice or not. +type InvoiceAcceptorResponse struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // preimage is the preimage of the invoice. + Preimage []byte `protobuf:"bytes,1,opt,name=preimage,proto3" json:"preimage,omitempty"` + // skip_amount_check is a flag that indicates whether the client wants to + // skip the amount check during the invoice settlement process. + SkipAmountCheck bool `protobuf:"varint,2,opt,name=skip_amount_check,json=skipAmountCheck,proto3" json:"skip_amount_check,omitempty"` +} + +func (x *InvoiceAcceptorResponse) Reset() { + *x = InvoiceAcceptorResponse{} + if protoimpl.UnsafeEnabled { + mi := &file_invoicesrpc_invoices_proto_msgTypes[10] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *InvoiceAcceptorResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*InvoiceAcceptorResponse) ProtoMessage() {} + +func (x *InvoiceAcceptorResponse) ProtoReflect() protoreflect.Message { + mi := &file_invoicesrpc_invoices_proto_msgTypes[10] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use InvoiceAcceptorResponse.ProtoReflect.Descriptor instead. +func (*InvoiceAcceptorResponse) Descriptor() ([]byte, []int) { + return file_invoicesrpc_invoices_proto_rawDescGZIP(), []int{10} +} + +func (x *InvoiceAcceptorResponse) GetPreimage() []byte { + if x != nil { + return x.Preimage + } + return nil +} + +func (x *InvoiceAcceptorResponse) GetSkipAmountCheck() bool { + if x != nil { + return x.SkipAmountCheck + } + return false +} + var File_invoicesrpc_invoices_proto protoreflect.FileDescriptor var file_invoicesrpc_invoices_proto_rawDesc = []byte{ @@ -678,41 +894,77 @@ var file_invoicesrpc_invoices_proto_rawDesc = []byte{ 0x73, 0x72, 0x70, 0x63, 0x2e, 0x4c, 0x6f, 0x6f, 0x6b, 0x75, 0x70, 0x4d, 0x6f, 0x64, 0x69, 0x66, 0x69, 0x65, 0x72, 0x52, 0x0e, 0x6c, 0x6f, 0x6f, 0x6b, 0x75, 0x70, 0x4d, 0x6f, 0x64, 0x69, 0x66, 0x69, 0x65, 0x72, 0x42, 0x0d, 0x0a, 0x0b, 0x69, 0x6e, 0x76, 0x6f, 0x69, 0x63, 0x65, 0x5f, 0x72, - 0x65, 0x66, 0x2a, 0x44, 0x0a, 0x0e, 0x4c, 0x6f, 0x6f, 0x6b, 0x75, 0x70, 0x4d, 0x6f, 0x64, 0x69, - 0x66, 0x69, 0x65, 0x72, 0x12, 0x0b, 0x0a, 0x07, 0x44, 0x45, 0x46, 0x41, 0x55, 0x4c, 0x54, 0x10, - 0x00, 0x12, 0x11, 0x0a, 0x0d, 0x48, 0x54, 0x4c, 0x43, 0x5f, 0x53, 0x45, 0x54, 0x5f, 0x4f, 0x4e, - 0x4c, 0x59, 0x10, 0x01, 0x12, 0x12, 0x0a, 0x0e, 0x48, 0x54, 0x4c, 0x43, 0x5f, 0x53, 0x45, 0x54, - 0x5f, 0x42, 0x4c, 0x41, 0x4e, 0x4b, 0x10, 0x02, 0x32, 0x9b, 0x03, 0x0a, 0x08, 0x49, 0x6e, 0x76, - 0x6f, 0x69, 0x63, 0x65, 0x73, 0x12, 0x56, 0x0a, 0x16, 0x53, 0x75, 0x62, 0x73, 0x63, 0x72, 0x69, - 0x62, 0x65, 0x53, 0x69, 0x6e, 0x67, 0x6c, 0x65, 0x49, 0x6e, 0x76, 0x6f, 0x69, 0x63, 0x65, 0x12, - 0x2a, 0x2e, 0x69, 0x6e, 0x76, 0x6f, 0x69, 0x63, 0x65, 0x73, 0x72, 0x70, 0x63, 0x2e, 0x53, 0x75, - 0x62, 0x73, 0x63, 0x72, 0x69, 0x62, 0x65, 0x53, 0x69, 0x6e, 0x67, 0x6c, 0x65, 0x49, 0x6e, 0x76, - 0x6f, 0x69, 0x63, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x0e, 0x2e, 0x6c, 0x6e, - 0x72, 0x70, 0x63, 0x2e, 0x49, 0x6e, 0x76, 0x6f, 0x69, 0x63, 0x65, 0x30, 0x01, 0x12, 0x4e, 0x0a, - 0x0d, 0x43, 0x61, 0x6e, 0x63, 0x65, 0x6c, 0x49, 0x6e, 0x76, 0x6f, 0x69, 0x63, 0x65, 0x12, 0x1d, - 0x2e, 0x69, 0x6e, 0x76, 0x6f, 0x69, 0x63, 0x65, 0x73, 0x72, 0x70, 0x63, 0x2e, 0x43, 0x61, 0x6e, - 0x63, 0x65, 0x6c, 0x49, 0x6e, 0x76, 0x6f, 0x69, 0x63, 0x65, 0x4d, 0x73, 0x67, 0x1a, 0x1e, 0x2e, - 0x69, 0x6e, 0x76, 0x6f, 0x69, 0x63, 0x65, 0x73, 0x72, 0x70, 0x63, 0x2e, 0x43, 0x61, 0x6e, 0x63, - 0x65, 0x6c, 0x49, 0x6e, 0x76, 0x6f, 0x69, 0x63, 0x65, 0x52, 0x65, 0x73, 0x70, 0x12, 0x55, 0x0a, - 0x0e, 0x41, 0x64, 0x64, 0x48, 0x6f, 0x6c, 0x64, 0x49, 0x6e, 0x76, 0x6f, 0x69, 0x63, 0x65, 0x12, - 0x22, 0x2e, 0x69, 0x6e, 0x76, 0x6f, 0x69, 0x63, 0x65, 0x73, 0x72, 0x70, 0x63, 0x2e, 0x41, 0x64, - 0x64, 0x48, 0x6f, 0x6c, 0x64, 0x49, 0x6e, 0x76, 0x6f, 0x69, 0x63, 0x65, 0x52, 0x65, 0x71, 0x75, - 0x65, 0x73, 0x74, 0x1a, 0x1f, 0x2e, 0x69, 0x6e, 0x76, 0x6f, 0x69, 0x63, 0x65, 0x73, 0x72, 0x70, - 0x63, 0x2e, 0x41, 0x64, 0x64, 0x48, 0x6f, 0x6c, 0x64, 0x49, 0x6e, 0x76, 0x6f, 0x69, 0x63, 0x65, - 0x52, 0x65, 0x73, 0x70, 0x12, 0x4e, 0x0a, 0x0d, 0x53, 0x65, 0x74, 0x74, 0x6c, 0x65, 0x49, 0x6e, - 0x76, 0x6f, 0x69, 0x63, 0x65, 0x12, 0x1d, 0x2e, 0x69, 0x6e, 0x76, 0x6f, 0x69, 0x63, 0x65, 0x73, - 0x72, 0x70, 0x63, 0x2e, 0x53, 0x65, 0x74, 0x74, 0x6c, 0x65, 0x49, 0x6e, 0x76, 0x6f, 0x69, 0x63, - 0x65, 0x4d, 0x73, 0x67, 0x1a, 0x1e, 0x2e, 0x69, 0x6e, 0x76, 0x6f, 0x69, 0x63, 0x65, 0x73, 0x72, - 0x70, 0x63, 0x2e, 0x53, 0x65, 0x74, 0x74, 0x6c, 0x65, 0x49, 0x6e, 0x76, 0x6f, 0x69, 0x63, 0x65, - 0x52, 0x65, 0x73, 0x70, 0x12, 0x40, 0x0a, 0x0f, 0x4c, 0x6f, 0x6f, 0x6b, 0x75, 0x70, 0x49, 0x6e, - 0x76, 0x6f, 0x69, 0x63, 0x65, 0x56, 0x32, 0x12, 0x1d, 0x2e, 0x69, 0x6e, 0x76, 0x6f, 0x69, 0x63, - 0x65, 0x73, 0x72, 0x70, 0x63, 0x2e, 0x4c, 0x6f, 0x6f, 0x6b, 0x75, 0x70, 0x49, 0x6e, 0x76, 0x6f, - 0x69, 0x63, 0x65, 0x4d, 0x73, 0x67, 0x1a, 0x0e, 0x2e, 0x6c, 0x6e, 0x72, 0x70, 0x63, 0x2e, 0x49, - 0x6e, 0x76, 0x6f, 0x69, 0x63, 0x65, 0x42, 0x33, 0x5a, 0x31, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, - 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x6c, 0x69, 0x67, 0x68, 0x74, 0x6e, 0x69, 0x6e, 0x67, 0x6e, 0x65, - 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x2f, 0x6c, 0x6e, 0x64, 0x2f, 0x6c, 0x6e, 0x72, 0x70, 0x63, 0x2f, - 0x69, 0x6e, 0x76, 0x6f, 0x69, 0x63, 0x65, 0x73, 0x72, 0x70, 0x63, 0x62, 0x06, 0x70, 0x72, 0x6f, - 0x74, 0x6f, 0x33, + 0x65, 0x66, 0x22, 0x3e, 0x0a, 0x0a, 0x43, 0x69, 0x72, 0x63, 0x75, 0x69, 0x74, 0x4b, 0x65, 0x79, + 0x12, 0x17, 0x0a, 0x07, 0x63, 0x68, 0x61, 0x6e, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, + 0x04, 0x52, 0x06, 0x63, 0x68, 0x61, 0x6e, 0x49, 0x64, 0x12, 0x17, 0x0a, 0x07, 0x68, 0x74, 0x6c, + 0x63, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x04, 0x52, 0x06, 0x68, 0x74, 0x6c, 0x63, + 0x49, 0x64, 0x22, 0xc3, 0x02, 0x0a, 0x16, 0x49, 0x6e, 0x76, 0x6f, 0x69, 0x63, 0x65, 0x41, 0x63, + 0x63, 0x65, 0x70, 0x74, 0x6f, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x28, 0x0a, + 0x07, 0x69, 0x6e, 0x76, 0x6f, 0x69, 0x63, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0e, + 0x2e, 0x6c, 0x6e, 0x72, 0x70, 0x63, 0x2e, 0x49, 0x6e, 0x76, 0x6f, 0x69, 0x63, 0x65, 0x52, 0x07, + 0x69, 0x6e, 0x76, 0x6f, 0x69, 0x63, 0x65, 0x12, 0x4a, 0x0a, 0x15, 0x65, 0x78, 0x69, 0x74, 0x5f, + 0x68, 0x74, 0x6c, 0x63, 0x5f, 0x63, 0x69, 0x72, 0x63, 0x75, 0x69, 0x74, 0x5f, 0x6b, 0x65, 0x79, + 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x69, 0x6e, 0x76, 0x6f, 0x69, 0x63, 0x65, + 0x73, 0x72, 0x70, 0x63, 0x2e, 0x43, 0x69, 0x72, 0x63, 0x75, 0x69, 0x74, 0x4b, 0x65, 0x79, 0x52, + 0x12, 0x65, 0x78, 0x69, 0x74, 0x48, 0x74, 0x6c, 0x63, 0x43, 0x69, 0x72, 0x63, 0x75, 0x69, 0x74, + 0x4b, 0x65, 0x79, 0x12, 0x22, 0x0a, 0x0d, 0x65, 0x78, 0x69, 0x74, 0x5f, 0x68, 0x74, 0x6c, 0x63, + 0x5f, 0x61, 0x6d, 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, 0x04, 0x52, 0x0b, 0x65, 0x78, 0x69, 0x74, + 0x48, 0x74, 0x6c, 0x63, 0x41, 0x6d, 0x74, 0x12, 0x28, 0x0a, 0x10, 0x65, 0x78, 0x69, 0x74, 0x5f, + 0x68, 0x74, 0x6c, 0x63, 0x5f, 0x65, 0x78, 0x70, 0x69, 0x72, 0x79, 0x18, 0x04, 0x20, 0x01, 0x28, + 0x0d, 0x52, 0x0e, 0x65, 0x78, 0x69, 0x74, 0x48, 0x74, 0x6c, 0x63, 0x45, 0x78, 0x70, 0x69, 0x72, + 0x79, 0x12, 0x25, 0x0a, 0x0e, 0x63, 0x75, 0x72, 0x72, 0x65, 0x6e, 0x74, 0x5f, 0x68, 0x65, 0x69, + 0x67, 0x68, 0x74, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x0d, 0x63, 0x75, 0x72, 0x72, 0x65, + 0x6e, 0x74, 0x48, 0x65, 0x69, 0x67, 0x68, 0x74, 0x12, 0x3e, 0x0a, 0x1c, 0x65, 0x78, 0x69, 0x74, + 0x5f, 0x68, 0x74, 0x6c, 0x63, 0x5f, 0x6d, 0x73, 0x67, 0x5f, 0x63, 0x75, 0x73, 0x74, 0x6f, 0x6d, + 0x5f, 0x72, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x73, 0x18, 0x06, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x18, + 0x65, 0x78, 0x69, 0x74, 0x48, 0x74, 0x6c, 0x63, 0x4d, 0x73, 0x67, 0x43, 0x75, 0x73, 0x74, 0x6f, + 0x6d, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x73, 0x22, 0x61, 0x0a, 0x17, 0x49, 0x6e, 0x76, 0x6f, + 0x69, 0x63, 0x65, 0x41, 0x63, 0x63, 0x65, 0x70, 0x74, 0x6f, 0x72, 0x52, 0x65, 0x73, 0x70, 0x6f, + 0x6e, 0x73, 0x65, 0x12, 0x1a, 0x0a, 0x08, 0x70, 0x72, 0x65, 0x69, 0x6d, 0x61, 0x67, 0x65, 0x18, + 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x08, 0x70, 0x72, 0x65, 0x69, 0x6d, 0x61, 0x67, 0x65, 0x12, + 0x2a, 0x0a, 0x11, 0x73, 0x6b, 0x69, 0x70, 0x5f, 0x61, 0x6d, 0x6f, 0x75, 0x6e, 0x74, 0x5f, 0x63, + 0x68, 0x65, 0x63, 0x6b, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0f, 0x73, 0x6b, 0x69, 0x70, + 0x41, 0x6d, 0x6f, 0x75, 0x6e, 0x74, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x2a, 0x44, 0x0a, 0x0e, 0x4c, + 0x6f, 0x6f, 0x6b, 0x75, 0x70, 0x4d, 0x6f, 0x64, 0x69, 0x66, 0x69, 0x65, 0x72, 0x12, 0x0b, 0x0a, + 0x07, 0x44, 0x45, 0x46, 0x41, 0x55, 0x4c, 0x54, 0x10, 0x00, 0x12, 0x11, 0x0a, 0x0d, 0x48, 0x54, + 0x4c, 0x43, 0x5f, 0x53, 0x45, 0x54, 0x5f, 0x4f, 0x4e, 0x4c, 0x59, 0x10, 0x01, 0x12, 0x12, 0x0a, + 0x0e, 0x48, 0x54, 0x4c, 0x43, 0x5f, 0x53, 0x45, 0x54, 0x5f, 0x42, 0x4c, 0x41, 0x4e, 0x4b, 0x10, + 0x02, 0x32, 0xfd, 0x03, 0x0a, 0x08, 0x49, 0x6e, 0x76, 0x6f, 0x69, 0x63, 0x65, 0x73, 0x12, 0x56, + 0x0a, 0x16, 0x53, 0x75, 0x62, 0x73, 0x63, 0x72, 0x69, 0x62, 0x65, 0x53, 0x69, 0x6e, 0x67, 0x6c, + 0x65, 0x49, 0x6e, 0x76, 0x6f, 0x69, 0x63, 0x65, 0x12, 0x2a, 0x2e, 0x69, 0x6e, 0x76, 0x6f, 0x69, + 0x63, 0x65, 0x73, 0x72, 0x70, 0x63, 0x2e, 0x53, 0x75, 0x62, 0x73, 0x63, 0x72, 0x69, 0x62, 0x65, + 0x53, 0x69, 0x6e, 0x67, 0x6c, 0x65, 0x49, 0x6e, 0x76, 0x6f, 0x69, 0x63, 0x65, 0x52, 0x65, 0x71, + 0x75, 0x65, 0x73, 0x74, 0x1a, 0x0e, 0x2e, 0x6c, 0x6e, 0x72, 0x70, 0x63, 0x2e, 0x49, 0x6e, 0x76, + 0x6f, 0x69, 0x63, 0x65, 0x30, 0x01, 0x12, 0x4e, 0x0a, 0x0d, 0x43, 0x61, 0x6e, 0x63, 0x65, 0x6c, + 0x49, 0x6e, 0x76, 0x6f, 0x69, 0x63, 0x65, 0x12, 0x1d, 0x2e, 0x69, 0x6e, 0x76, 0x6f, 0x69, 0x63, + 0x65, 0x73, 0x72, 0x70, 0x63, 0x2e, 0x43, 0x61, 0x6e, 0x63, 0x65, 0x6c, 0x49, 0x6e, 0x76, 0x6f, + 0x69, 0x63, 0x65, 0x4d, 0x73, 0x67, 0x1a, 0x1e, 0x2e, 0x69, 0x6e, 0x76, 0x6f, 0x69, 0x63, 0x65, + 0x73, 0x72, 0x70, 0x63, 0x2e, 0x43, 0x61, 0x6e, 0x63, 0x65, 0x6c, 0x49, 0x6e, 0x76, 0x6f, 0x69, + 0x63, 0x65, 0x52, 0x65, 0x73, 0x70, 0x12, 0x55, 0x0a, 0x0e, 0x41, 0x64, 0x64, 0x48, 0x6f, 0x6c, + 0x64, 0x49, 0x6e, 0x76, 0x6f, 0x69, 0x63, 0x65, 0x12, 0x22, 0x2e, 0x69, 0x6e, 0x76, 0x6f, 0x69, + 0x63, 0x65, 0x73, 0x72, 0x70, 0x63, 0x2e, 0x41, 0x64, 0x64, 0x48, 0x6f, 0x6c, 0x64, 0x49, 0x6e, + 0x76, 0x6f, 0x69, 0x63, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1f, 0x2e, 0x69, + 0x6e, 0x76, 0x6f, 0x69, 0x63, 0x65, 0x73, 0x72, 0x70, 0x63, 0x2e, 0x41, 0x64, 0x64, 0x48, 0x6f, + 0x6c, 0x64, 0x49, 0x6e, 0x76, 0x6f, 0x69, 0x63, 0x65, 0x52, 0x65, 0x73, 0x70, 0x12, 0x4e, 0x0a, + 0x0d, 0x53, 0x65, 0x74, 0x74, 0x6c, 0x65, 0x49, 0x6e, 0x76, 0x6f, 0x69, 0x63, 0x65, 0x12, 0x1d, + 0x2e, 0x69, 0x6e, 0x76, 0x6f, 0x69, 0x63, 0x65, 0x73, 0x72, 0x70, 0x63, 0x2e, 0x53, 0x65, 0x74, + 0x74, 0x6c, 0x65, 0x49, 0x6e, 0x76, 0x6f, 0x69, 0x63, 0x65, 0x4d, 0x73, 0x67, 0x1a, 0x1e, 0x2e, + 0x69, 0x6e, 0x76, 0x6f, 0x69, 0x63, 0x65, 0x73, 0x72, 0x70, 0x63, 0x2e, 0x53, 0x65, 0x74, 0x74, + 0x6c, 0x65, 0x49, 0x6e, 0x76, 0x6f, 0x69, 0x63, 0x65, 0x52, 0x65, 0x73, 0x70, 0x12, 0x40, 0x0a, + 0x0f, 0x4c, 0x6f, 0x6f, 0x6b, 0x75, 0x70, 0x49, 0x6e, 0x76, 0x6f, 0x69, 0x63, 0x65, 0x56, 0x32, + 0x12, 0x1d, 0x2e, 0x69, 0x6e, 0x76, 0x6f, 0x69, 0x63, 0x65, 0x73, 0x72, 0x70, 0x63, 0x2e, 0x4c, + 0x6f, 0x6f, 0x6b, 0x75, 0x70, 0x49, 0x6e, 0x76, 0x6f, 0x69, 0x63, 0x65, 0x4d, 0x73, 0x67, 0x1a, + 0x0e, 0x2e, 0x6c, 0x6e, 0x72, 0x70, 0x63, 0x2e, 0x49, 0x6e, 0x76, 0x6f, 0x69, 0x63, 0x65, 0x12, + 0x60, 0x0a, 0x0f, 0x49, 0x6e, 0x76, 0x6f, 0x69, 0x63, 0x65, 0x41, 0x63, 0x63, 0x65, 0x70, 0x74, + 0x6f, 0x72, 0x12, 0x24, 0x2e, 0x69, 0x6e, 0x76, 0x6f, 0x69, 0x63, 0x65, 0x73, 0x72, 0x70, 0x63, + 0x2e, 0x49, 0x6e, 0x76, 0x6f, 0x69, 0x63, 0x65, 0x41, 0x63, 0x63, 0x65, 0x70, 0x74, 0x6f, 0x72, + 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x1a, 0x23, 0x2e, 0x69, 0x6e, 0x76, 0x6f, 0x69, + 0x63, 0x65, 0x73, 0x72, 0x70, 0x63, 0x2e, 0x49, 0x6e, 0x76, 0x6f, 0x69, 0x63, 0x65, 0x41, 0x63, + 0x63, 0x65, 0x70, 0x74, 0x6f, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x28, 0x01, 0x30, + 0x01, 0x42, 0x33, 0x5a, 0x31, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, + 0x6c, 0x69, 0x67, 0x68, 0x74, 0x6e, 0x69, 0x6e, 0x67, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, + 0x2f, 0x6c, 0x6e, 0x64, 0x2f, 0x6c, 0x6e, 0x72, 0x70, 0x63, 0x2f, 0x69, 0x6e, 0x76, 0x6f, 0x69, + 0x63, 0x65, 0x73, 0x72, 0x70, 0x63, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( @@ -728,7 +980,7 @@ func file_invoicesrpc_invoices_proto_rawDescGZIP() []byte { } var file_invoicesrpc_invoices_proto_enumTypes = make([]protoimpl.EnumInfo, 1) -var file_invoicesrpc_invoices_proto_msgTypes = make([]protoimpl.MessageInfo, 8) +var file_invoicesrpc_invoices_proto_msgTypes = make([]protoimpl.MessageInfo, 11) var file_invoicesrpc_invoices_proto_goTypes = []interface{}{ (LookupModifier)(0), // 0: invoicesrpc.LookupModifier (*CancelInvoiceMsg)(nil), // 1: invoicesrpc.CancelInvoiceMsg @@ -739,27 +991,34 @@ var file_invoicesrpc_invoices_proto_goTypes = []interface{}{ (*SettleInvoiceResp)(nil), // 6: invoicesrpc.SettleInvoiceResp (*SubscribeSingleInvoiceRequest)(nil), // 7: invoicesrpc.SubscribeSingleInvoiceRequest (*LookupInvoiceMsg)(nil), // 8: invoicesrpc.LookupInvoiceMsg - (*lnrpc.RouteHint)(nil), // 9: lnrpc.RouteHint - (*lnrpc.Invoice)(nil), // 10: lnrpc.Invoice + (*CircuitKey)(nil), // 9: invoicesrpc.CircuitKey + (*InvoiceAcceptorRequest)(nil), // 10: invoicesrpc.InvoiceAcceptorRequest + (*InvoiceAcceptorResponse)(nil), // 11: invoicesrpc.InvoiceAcceptorResponse + (*lnrpc.RouteHint)(nil), // 12: lnrpc.RouteHint + (*lnrpc.Invoice)(nil), // 13: lnrpc.Invoice } var file_invoicesrpc_invoices_proto_depIdxs = []int32{ - 9, // 0: invoicesrpc.AddHoldInvoiceRequest.route_hints:type_name -> lnrpc.RouteHint + 12, // 0: invoicesrpc.AddHoldInvoiceRequest.route_hints:type_name -> lnrpc.RouteHint 0, // 1: invoicesrpc.LookupInvoiceMsg.lookup_modifier:type_name -> invoicesrpc.LookupModifier - 7, // 2: invoicesrpc.Invoices.SubscribeSingleInvoice:input_type -> invoicesrpc.SubscribeSingleInvoiceRequest - 1, // 3: invoicesrpc.Invoices.CancelInvoice:input_type -> invoicesrpc.CancelInvoiceMsg - 3, // 4: invoicesrpc.Invoices.AddHoldInvoice:input_type -> invoicesrpc.AddHoldInvoiceRequest - 5, // 5: invoicesrpc.Invoices.SettleInvoice:input_type -> invoicesrpc.SettleInvoiceMsg - 8, // 6: invoicesrpc.Invoices.LookupInvoiceV2:input_type -> invoicesrpc.LookupInvoiceMsg - 10, // 7: invoicesrpc.Invoices.SubscribeSingleInvoice:output_type -> lnrpc.Invoice - 2, // 8: invoicesrpc.Invoices.CancelInvoice:output_type -> invoicesrpc.CancelInvoiceResp - 4, // 9: invoicesrpc.Invoices.AddHoldInvoice:output_type -> invoicesrpc.AddHoldInvoiceResp - 6, // 10: invoicesrpc.Invoices.SettleInvoice:output_type -> invoicesrpc.SettleInvoiceResp - 10, // 11: invoicesrpc.Invoices.LookupInvoiceV2:output_type -> lnrpc.Invoice - 7, // [7:12] is the sub-list for method output_type - 2, // [2:7] is the sub-list for method input_type - 2, // [2:2] is the sub-list for extension type_name - 2, // [2:2] is the sub-list for extension extendee - 0, // [0:2] is the sub-list for field type_name + 13, // 2: invoicesrpc.InvoiceAcceptorRequest.invoice:type_name -> lnrpc.Invoice + 9, // 3: invoicesrpc.InvoiceAcceptorRequest.exit_htlc_circuit_key:type_name -> invoicesrpc.CircuitKey + 7, // 4: invoicesrpc.Invoices.SubscribeSingleInvoice:input_type -> invoicesrpc.SubscribeSingleInvoiceRequest + 1, // 5: invoicesrpc.Invoices.CancelInvoice:input_type -> invoicesrpc.CancelInvoiceMsg + 3, // 6: invoicesrpc.Invoices.AddHoldInvoice:input_type -> invoicesrpc.AddHoldInvoiceRequest + 5, // 7: invoicesrpc.Invoices.SettleInvoice:input_type -> invoicesrpc.SettleInvoiceMsg + 8, // 8: invoicesrpc.Invoices.LookupInvoiceV2:input_type -> invoicesrpc.LookupInvoiceMsg + 11, // 9: invoicesrpc.Invoices.InvoiceAcceptor:input_type -> invoicesrpc.InvoiceAcceptorResponse + 13, // 10: invoicesrpc.Invoices.SubscribeSingleInvoice:output_type -> lnrpc.Invoice + 2, // 11: invoicesrpc.Invoices.CancelInvoice:output_type -> invoicesrpc.CancelInvoiceResp + 4, // 12: invoicesrpc.Invoices.AddHoldInvoice:output_type -> invoicesrpc.AddHoldInvoiceResp + 6, // 13: invoicesrpc.Invoices.SettleInvoice:output_type -> invoicesrpc.SettleInvoiceResp + 13, // 14: invoicesrpc.Invoices.LookupInvoiceV2:output_type -> lnrpc.Invoice + 10, // 15: invoicesrpc.Invoices.InvoiceAcceptor:output_type -> invoicesrpc.InvoiceAcceptorRequest + 10, // [10:16] is the sub-list for method output_type + 4, // [4:10] is the sub-list for method input_type + 4, // [4:4] is the sub-list for extension type_name + 4, // [4:4] is the sub-list for extension extendee + 0, // [0:4] is the sub-list for field type_name } func init() { file_invoicesrpc_invoices_proto_init() } @@ -864,6 +1123,42 @@ func file_invoicesrpc_invoices_proto_init() { return nil } } + file_invoicesrpc_invoices_proto_msgTypes[8].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*CircuitKey); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_invoicesrpc_invoices_proto_msgTypes[9].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*InvoiceAcceptorRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_invoicesrpc_invoices_proto_msgTypes[10].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*InvoiceAcceptorResponse); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } } file_invoicesrpc_invoices_proto_msgTypes[7].OneofWrappers = []interface{}{ (*LookupInvoiceMsg_PaymentHash)(nil), @@ -876,7 +1171,7 @@ func file_invoicesrpc_invoices_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: file_invoicesrpc_invoices_proto_rawDesc, NumEnums: 1, - NumMessages: 8, + NumMessages: 11, NumExtensions: 0, NumServices: 1, }, diff --git a/lnrpc/invoicesrpc/invoices.proto b/lnrpc/invoicesrpc/invoices.proto index 7afffba4e3a..5e5723b7a52 100644 --- a/lnrpc/invoicesrpc/invoices.proto +++ b/lnrpc/invoicesrpc/invoices.proto @@ -59,6 +59,14 @@ service Invoices { using either its payment hash, payment address, or set ID. */ rpc LookupInvoiceV2 (LookupInvoiceMsg) returns (lnrpc.Invoice); + + /* + InvoiceAcceptor is a bi-directional streaming RPC that allows a client to + accept invoices. The server will send invoices to the client and the client + can respond with whether it accepts the invoice or not. + */ + rpc InvoiceAcceptor (stream InvoiceAcceptorResponse) + returns (stream InvoiceAcceptorRequest); } message CancelInvoiceMsg { @@ -192,3 +200,48 @@ message LookupInvoiceMsg { LookupModifier lookup_modifier = 4; } + +// CircuitKey is a unique identifier for an HTLC. +message CircuitKey { + /// The id of the channel that the is part of this circuit. + uint64 chan_id = 1; + + /// The index of the incoming htlc in the incoming channel. + uint64 htlc_id = 2; +} + +// InvoiceAcceptorRequest is a message that the server sends to the client to +// request the client to accept an invoice. +message InvoiceAcceptorRequest { + // invoice is the invoice that the client should consider accepting. + lnrpc.Invoice invoice = 1; + + // exit_htlc_circuit_key is the key of the HTLC for which we is involved in + // the invoice settlement attempt. + CircuitKey exit_htlc_circuit_key = 2; + + // exit_htlc_amt is the amount (millisats) that the client should consider + // accepting. + uint64 exit_htlc_amt = 3; + + // exit_htlc_expiry is the expiry time of the exit HTLC. + uint32 exit_htlc_expiry = 4; + + // current_height is the current block height. + uint32 current_height = 5; + + // exit_htlc_msg_custom_records is the p2p message custom records of the + // exit HTLC. + bytes exit_htlc_msg_custom_records = 6; +} + +// InvoiceAcceptorResponse is a message that the client sends to the server to +// indicate whether it accepts the invoice or not. +message InvoiceAcceptorResponse { + // preimage is the preimage of the invoice. + bytes preimage = 1; + + // skip_amount_check is a flag that indicates whether the client wants to + // skip the amount check during the invoice settlement process. + bool skip_amount_check = 2; +} diff --git a/lnrpc/invoicesrpc/invoices.swagger.json b/lnrpc/invoicesrpc/invoices.swagger.json index b54d9fb38e1..136381d48b3 100644 --- a/lnrpc/invoicesrpc/invoices.swagger.json +++ b/lnrpc/invoicesrpc/invoices.swagger.json @@ -317,6 +317,56 @@ "invoicesrpcCancelInvoiceResp": { "type": "object" }, + "invoicesrpcCircuitKey": { + "type": "object", + "properties": { + "chan_id": { + "type": "string", + "format": "uint64", + "description": "/ The id of the channel that the is part of this circuit." + }, + "htlc_id": { + "type": "string", + "format": "uint64", + "description": "/ The index of the incoming htlc in the incoming channel." + } + }, + "description": "CircuitKey is a unique identifier for an HTLC." + }, + "invoicesrpcInvoiceAcceptorRequest": { + "type": "object", + "properties": { + "invoice": { + "$ref": "#/definitions/lnrpcInvoice", + "description": "invoice is the invoice that the client should consider accepting." + }, + "exit_htlc_circuit_key": { + "$ref": "#/definitions/invoicesrpcCircuitKey", + "description": "exit_htlc_circuit_key is the key of the HTLC for which we is involved in\nthe invoice settlement attempt." + }, + "exit_htlc_amt": { + "type": "string", + "format": "uint64", + "description": "exit_htlc_amt is the amount (millisats) that the client should consider\naccepting." + }, + "exit_htlc_expiry": { + "type": "integer", + "format": "int64", + "description": "exit_htlc_expiry is the expiry time of the exit HTLC." + }, + "current_height": { + "type": "integer", + "format": "int64", + "description": "current_height is the current block height." + }, + "exit_htlc_msg_custom_records": { + "type": "string", + "format": "byte", + "description": "exit_htlc_msg_custom_records is the p2p message custom records of the\nexit HTLC." + } + }, + "description": "InvoiceAcceptorRequest is a message that the server sends to the client to\nrequest the client to accept an invoice." + }, "invoicesrpcLookupModifier": { "type": "string", "enum": [ diff --git a/lnrpc/invoicesrpc/invoices_grpc.pb.go b/lnrpc/invoicesrpc/invoices_grpc.pb.go index bc2f24ac101..398a2479e50 100644 --- a/lnrpc/invoicesrpc/invoices_grpc.pb.go +++ b/lnrpc/invoicesrpc/invoices_grpc.pb.go @@ -39,6 +39,10 @@ type InvoicesClient interface { // LookupInvoiceV2 attempts to look up at invoice. An invoice can be refrenced // using either its payment hash, payment address, or set ID. LookupInvoiceV2(ctx context.Context, in *LookupInvoiceMsg, opts ...grpc.CallOption) (*lnrpc.Invoice, error) + // InvoiceAcceptor is a bi-directional streaming RPC that allows a client to + // accept invoices. The server will send invoices to the client and the client + // can respond with whether it accepts the invoice or not. + InvoiceAcceptor(ctx context.Context, opts ...grpc.CallOption) (Invoices_InvoiceAcceptorClient, error) } type invoicesClient struct { @@ -117,6 +121,37 @@ func (c *invoicesClient) LookupInvoiceV2(ctx context.Context, in *LookupInvoiceM return out, nil } +func (c *invoicesClient) InvoiceAcceptor(ctx context.Context, opts ...grpc.CallOption) (Invoices_InvoiceAcceptorClient, error) { + stream, err := c.cc.NewStream(ctx, &Invoices_ServiceDesc.Streams[1], "/invoicesrpc.Invoices/InvoiceAcceptor", opts...) + if err != nil { + return nil, err + } + x := &invoicesInvoiceAcceptorClient{stream} + return x, nil +} + +type Invoices_InvoiceAcceptorClient interface { + Send(*InvoiceAcceptorResponse) error + Recv() (*InvoiceAcceptorRequest, error) + grpc.ClientStream +} + +type invoicesInvoiceAcceptorClient struct { + grpc.ClientStream +} + +func (x *invoicesInvoiceAcceptorClient) Send(m *InvoiceAcceptorResponse) error { + return x.ClientStream.SendMsg(m) +} + +func (x *invoicesInvoiceAcceptorClient) Recv() (*InvoiceAcceptorRequest, error) { + m := new(InvoiceAcceptorRequest) + if err := x.ClientStream.RecvMsg(m); err != nil { + return nil, err + } + return m, nil +} + // InvoicesServer is the server API for Invoices service. // All implementations must embed UnimplementedInvoicesServer // for forward compatibility @@ -141,6 +176,10 @@ type InvoicesServer interface { // LookupInvoiceV2 attempts to look up at invoice. An invoice can be refrenced // using either its payment hash, payment address, or set ID. LookupInvoiceV2(context.Context, *LookupInvoiceMsg) (*lnrpc.Invoice, error) + // InvoiceAcceptor is a bi-directional streaming RPC that allows a client to + // accept invoices. The server will send invoices to the client and the client + // can respond with whether it accepts the invoice or not. + InvoiceAcceptor(Invoices_InvoiceAcceptorServer) error mustEmbedUnimplementedInvoicesServer() } @@ -163,6 +202,9 @@ func (UnimplementedInvoicesServer) SettleInvoice(context.Context, *SettleInvoice func (UnimplementedInvoicesServer) LookupInvoiceV2(context.Context, *LookupInvoiceMsg) (*lnrpc.Invoice, error) { return nil, status.Errorf(codes.Unimplemented, "method LookupInvoiceV2 not implemented") } +func (UnimplementedInvoicesServer) InvoiceAcceptor(Invoices_InvoiceAcceptorServer) error { + return status.Errorf(codes.Unimplemented, "method InvoiceAcceptor not implemented") +} func (UnimplementedInvoicesServer) mustEmbedUnimplementedInvoicesServer() {} // UnsafeInvoicesServer may be embedded to opt out of forward compatibility for this service. @@ -269,6 +311,32 @@ func _Invoices_LookupInvoiceV2_Handler(srv interface{}, ctx context.Context, dec return interceptor(ctx, in, info, handler) } +func _Invoices_InvoiceAcceptor_Handler(srv interface{}, stream grpc.ServerStream) error { + return srv.(InvoicesServer).InvoiceAcceptor(&invoicesInvoiceAcceptorServer{stream}) +} + +type Invoices_InvoiceAcceptorServer interface { + Send(*InvoiceAcceptorRequest) error + Recv() (*InvoiceAcceptorResponse, error) + grpc.ServerStream +} + +type invoicesInvoiceAcceptorServer struct { + grpc.ServerStream +} + +func (x *invoicesInvoiceAcceptorServer) Send(m *InvoiceAcceptorRequest) error { + return x.ServerStream.SendMsg(m) +} + +func (x *invoicesInvoiceAcceptorServer) Recv() (*InvoiceAcceptorResponse, error) { + m := new(InvoiceAcceptorResponse) + if err := x.ServerStream.RecvMsg(m); err != nil { + return nil, err + } + return m, nil +} + // Invoices_ServiceDesc is the grpc.ServiceDesc for Invoices service. // It's only intended for direct use with grpc.RegisterService, // and not to be introspected or modified (even as a copy) @@ -299,6 +367,12 @@ var Invoices_ServiceDesc = grpc.ServiceDesc{ Handler: _Invoices_SubscribeSingleInvoice_Handler, ServerStreams: true, }, + { + StreamName: "InvoiceAcceptor", + Handler: _Invoices_InvoiceAcceptor_Handler, + ServerStreams: true, + ClientStreams: true, + }, }, Metadata: "invoicesrpc/invoices.proto", } diff --git a/lnrpc/invoicesrpc/invoices_server.go b/lnrpc/invoicesrpc/invoices_server.go index b62538543df..7ec03930058 100644 --- a/lnrpc/invoicesrpc/invoices_server.go +++ b/lnrpc/invoicesrpc/invoices_server.go @@ -10,6 +10,7 @@ import ( "io/ioutil" "os" "path/filepath" + "sync/atomic" "github.com/grpc-ecosystem/grpc-gateway/v2/runtime" "github.com/lightningnetwork/lnd/invoices" @@ -31,6 +32,12 @@ const ( ) var ( + // ErrInvoiceAcceptorAlreadyExists signals that an invoice acceptor + // already exists. The user must disconnect an existing invoice accptor + // prior to opening another stream. + ErrInvoiceAcceptorAlreadyExists = errors.New("interceptor already " + + "exists") + // macaroonOps are the set of capabilities that our minted macaroon (if // it doesn't already exist) will have. macaroonOps = []bakery.Op{ @@ -66,6 +73,10 @@ var ( Entity: "invoices", Action: "write", }}, + "/invoicesrpc.Invoices/InvoiceAcceptor": {{ + Entity: "invoices", + Action: "write", + }}, } // DefaultInvoicesMacFilename is the default name of the invoices @@ -88,6 +99,12 @@ type Server struct { // Required by the grpc-gateway/v2 library for forward compatibility. UnimplementedInvoicesServer + // invoiceAcceptorActive is an atomic flag that indicates whether an + // invoice acceptor RPC server is currently active. It is used to ensure + // that only one invoice acceptor RPC server instance is running at a + // time. + invoiceAcceptorActive atomic.Int32 + quit chan struct{} cfg *Config @@ -447,3 +464,22 @@ func (s *Server) LookupInvoiceV2(ctx context.Context, return CreateRPCInvoice(&invoice, s.cfg.ChainParams) } + +// InvoiceAcceptor is a bidirectional streaming RPC that allows a client to +// inspect and optionally accept invoices. +func (s *Server) InvoiceAcceptor( + acceptorServer Invoices_InvoiceAcceptorServer) error { + + // Ensure that there is only one invoice acceptor RPC server instance. + if !s.invoiceAcceptorActive.CompareAndSwap(0, 1) { + return ErrInvoiceAcceptorAlreadyExists + } + defer s.invoiceAcceptorActive.CompareAndSwap(1, 0) + + // Run the invoice acceptor. + return newInvoiceAcceptor(invoiceAcceptorConfig{ + chainParams: s.cfg.ChainParams, + interceptor: s.cfg.InvoiceSettlementInterceptor, + rpcServer: acceptorServer, + }).run() +} diff --git a/lnrpc/lightning.pb.go b/lnrpc/lightning.pb.go index 46badbe84b1..92fe8c78d23 100644 --- a/lnrpc/lightning.pb.go +++ b/lnrpc/lightning.pb.go @@ -4684,7 +4684,8 @@ type Channel struct { // An optional note-to-self to go along with the channel containing some // useful information. This is only ever stored locally and in no way impacts // the channel's operation. - Memo string `protobuf:"bytes,36,opt,name=memo,proto3" json:"memo,omitempty"` + Memo string `protobuf:"bytes,36,opt,name=memo,proto3" json:"memo,omitempty"` + CustomChannelData []byte `protobuf:"bytes,37,opt,name=custom_channel_data,json=customChannelData,proto3" json:"custom_channel_data,omitempty"` } func (x *Channel) Reset() { @@ -4975,6 +4976,13 @@ func (x *Channel) GetMemo() string { return "" } +func (x *Channel) GetCustomChannelData() []byte { + if x != nil { + return x.CustomChannelData + } + return nil +} + type ListChannelsRequest struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache @@ -7660,7 +7668,8 @@ type OpenChannelRequest struct { // the channel's operation. Memo string `protobuf:"bytes,27,opt,name=memo,proto3" json:"memo,omitempty"` // A list of selected outpoints that are allocated for channel funding. - Outpoints []*OutPoint `protobuf:"bytes,28,rep,name=outpoints,proto3" json:"outpoints,omitempty"` + Outpoints []*OutPoint `protobuf:"bytes,28,rep,name=outpoints,proto3" json:"outpoints,omitempty"` + CustomChannelData []byte `protobuf:"bytes,29,opt,name=custom_channel_data,json=customChannelData,proto3" json:"custom_channel_data,omitempty"` } func (x *OpenChannelRequest) Reset() { @@ -7893,6 +7902,13 @@ func (x *OpenChannelRequest) GetOutpoints() []*OutPoint { return nil } +func (x *OpenChannelRequest) GetCustomChannelData() []byte { + if x != nil { + return x.CustomChannelData + } + return nil +} + type OpenStatusUpdate struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache @@ -18415,7 +18431,7 @@ var file_lightning_proto_rawDesc = []byte{ 0x73, 0x61, 0x74, 0x12, 0x2c, 0x0a, 0x12, 0x6d, 0x61, 0x78, 0x5f, 0x61, 0x63, 0x63, 0x65, 0x70, 0x74, 0x65, 0x64, 0x5f, 0x68, 0x74, 0x6c, 0x63, 0x73, 0x18, 0x06, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x10, 0x6d, 0x61, 0x78, 0x41, 0x63, 0x63, 0x65, 0x70, 0x74, 0x65, 0x64, 0x48, 0x74, 0x6c, 0x63, - 0x73, 0x22, 0xad, 0x0b, 0x0a, 0x07, 0x43, 0x68, 0x61, 0x6e, 0x6e, 0x65, 0x6c, 0x12, 0x16, 0x0a, + 0x73, 0x22, 0xdd, 0x0b, 0x0a, 0x07, 0x43, 0x68, 0x61, 0x6e, 0x6e, 0x65, 0x6c, 0x12, 0x16, 0x0a, 0x06, 0x61, 0x63, 0x74, 0x69, 0x76, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x06, 0x61, 0x63, 0x74, 0x69, 0x76, 0x65, 0x12, 0x23, 0x0a, 0x0d, 0x72, 0x65, 0x6d, 0x6f, 0x74, 0x65, 0x5f, 0x70, 0x75, 0x62, 0x6b, 0x65, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x72, 0x65, @@ -18506,7 +18522,10 @@ var file_lightning_proto_rawDesc = []byte{ 0x6c, 0x69, 0x61, 0x73, 0x18, 0x23, 0x20, 0x01, 0x28, 0x04, 0x42, 0x02, 0x30, 0x01, 0x52, 0x0d, 0x70, 0x65, 0x65, 0x72, 0x53, 0x63, 0x69, 0x64, 0x41, 0x6c, 0x69, 0x61, 0x73, 0x12, 0x12, 0x0a, 0x04, 0x6d, 0x65, 0x6d, 0x6f, 0x18, 0x24, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6d, 0x65, 0x6d, - 0x6f, 0x22, 0xdf, 0x01, 0x0a, 0x13, 0x4c, 0x69, 0x73, 0x74, 0x43, 0x68, 0x61, 0x6e, 0x6e, 0x65, + 0x6f, 0x12, 0x2e, 0x0a, 0x13, 0x63, 0x75, 0x73, 0x74, 0x6f, 0x6d, 0x5f, 0x63, 0x68, 0x61, 0x6e, + 0x6e, 0x65, 0x6c, 0x5f, 0x64, 0x61, 0x74, 0x61, 0x18, 0x25, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x11, + 0x63, 0x75, 0x73, 0x74, 0x6f, 0x6d, 0x43, 0x68, 0x61, 0x6e, 0x6e, 0x65, 0x6c, 0x44, 0x61, 0x74, + 0x61, 0x22, 0xdf, 0x01, 0x0a, 0x13, 0x4c, 0x69, 0x73, 0x74, 0x43, 0x68, 0x61, 0x6e, 0x6e, 0x65, 0x6c, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1f, 0x0a, 0x0b, 0x61, 0x63, 0x74, 0x69, 0x76, 0x65, 0x5f, 0x6f, 0x6e, 0x6c, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0a, 0x61, 0x63, 0x74, 0x69, 0x76, 0x65, 0x4f, 0x6e, 0x6c, 0x79, 0x12, 0x23, 0x0a, 0x0d, 0x69, 0x6e, @@ -18905,7 +18924,7 @@ var file_lightning_proto_rawDesc = []byte{ 0x6e, 0x64, 0x69, 0x6e, 0x67, 0x5f, 0x63, 0x68, 0x61, 0x6e, 0x6e, 0x65, 0x6c, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x14, 0x2e, 0x6c, 0x6e, 0x72, 0x70, 0x63, 0x2e, 0x50, 0x65, 0x6e, 0x64, 0x69, 0x6e, 0x67, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x52, 0x0f, 0x70, 0x65, 0x6e, 0x64, - 0x69, 0x6e, 0x67, 0x43, 0x68, 0x61, 0x6e, 0x6e, 0x65, 0x6c, 0x73, 0x22, 0xcb, 0x08, 0x0a, 0x12, + 0x69, 0x6e, 0x67, 0x43, 0x68, 0x61, 0x6e, 0x6e, 0x65, 0x6c, 0x73, 0x22, 0xfb, 0x08, 0x0a, 0x12, 0x4f, 0x70, 0x65, 0x6e, 0x43, 0x68, 0x61, 0x6e, 0x6e, 0x65, 0x6c, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x22, 0x0a, 0x0d, 0x73, 0x61, 0x74, 0x5f, 0x70, 0x65, 0x72, 0x5f, 0x76, 0x62, 0x79, 0x74, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x04, 0x52, 0x0b, 0x73, 0x61, 0x74, 0x50, 0x65, @@ -18974,7 +18993,10 @@ var file_lightning_proto_rawDesc = []byte{ 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6d, 0x65, 0x6d, 0x6f, 0x12, 0x2d, 0x0a, 0x09, 0x6f, 0x75, 0x74, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x73, 0x18, 0x1c, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x0f, 0x2e, 0x6c, 0x6e, 0x72, 0x70, 0x63, 0x2e, 0x4f, 0x75, 0x74, 0x50, 0x6f, 0x69, 0x6e, 0x74, 0x52, 0x09, - 0x6f, 0x75, 0x74, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x73, 0x22, 0xf3, 0x01, 0x0a, 0x10, 0x4f, 0x70, + 0x6f, 0x75, 0x74, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x73, 0x12, 0x2e, 0x0a, 0x13, 0x63, 0x75, 0x73, + 0x74, 0x6f, 0x6d, 0x5f, 0x63, 0x68, 0x61, 0x6e, 0x6e, 0x65, 0x6c, 0x5f, 0x64, 0x61, 0x74, 0x61, + 0x18, 0x1d, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x11, 0x63, 0x75, 0x73, 0x74, 0x6f, 0x6d, 0x43, 0x68, + 0x61, 0x6e, 0x6e, 0x65, 0x6c, 0x44, 0x61, 0x74, 0x61, 0x22, 0xf3, 0x01, 0x0a, 0x10, 0x4f, 0x70, 0x65, 0x6e, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x12, 0x39, 0x0a, 0x0c, 0x63, 0x68, 0x61, 0x6e, 0x5f, 0x70, 0x65, 0x6e, 0x64, 0x69, 0x6e, 0x67, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x14, 0x2e, 0x6c, 0x6e, 0x72, 0x70, 0x63, 0x2e, 0x50, 0x65, 0x6e, diff --git a/lnrpc/lightning.proto b/lnrpc/lightning.proto index 7a343b49198..d6643e29865 100644 --- a/lnrpc/lightning.proto +++ b/lnrpc/lightning.proto @@ -1590,6 +1590,8 @@ message Channel { the channel's operation. */ string memo = 36; + + bytes custom_channel_data = 37; } message ListChannelsRequest { @@ -2426,6 +2428,8 @@ message OpenChannelRequest { A list of selected outpoints that are allocated for channel funding. */ repeated OutPoint outpoints = 28; + + bytes custom_channel_data = 29; } message OpenStatusUpdate { oneof update { diff --git a/lnrpc/lightning.swagger.json b/lnrpc/lightning.swagger.json index 3c1b2a81069..f27d18ca03e 100644 --- a/lnrpc/lightning.swagger.json +++ b/lnrpc/lightning.swagger.json @@ -3805,6 +3805,10 @@ "memo": { "type": "string", "description": "An optional note-to-self to go along with the channel containing some\nuseful information. This is only ever stored locally and in no way impacts\nthe channel's operation." + }, + "custom_channel_data": { + "type": "string", + "format": "byte" } } }, @@ -6122,6 +6126,10 @@ "$ref": "#/definitions/lnrpcOutPoint" }, "description": "A list of selected outpoints that are allocated for channel funding." + }, + "custom_channel_data": { + "type": "string", + "format": "byte" } } }, diff --git a/lntest/harness.go b/lntest/harness.go index c291e7cf396..037ee657995 100644 --- a/lntest/harness.go +++ b/lntest/harness.go @@ -16,6 +16,7 @@ import ( "github.com/lightningnetwork/lnd/fn" "github.com/lightningnetwork/lnd/kvdb/etcd" "github.com/lightningnetwork/lnd/lnrpc" + "github.com/lightningnetwork/lnd/lnrpc/invoicesrpc" "github.com/lightningnetwork/lnd/lnrpc/routerrpc" "github.com/lightningnetwork/lnd/lnrpc/walletrpc" "github.com/lightningnetwork/lnd/lntest/node" @@ -2183,6 +2184,39 @@ func (h *HarnessTest) ReceiveHtlcInterceptor( return nil } +// ReceiveInvoiceAcceptor waits until a message is received on the invoice +// acceptor interceptor stream or the timeout is reached. +func (h *HarnessTest) ReceiveInvoiceAcceptor( + stream rpc.InvoiceAcceptorClient) *invoicesrpc.InvoiceAcceptorRequest { + + chanMsg := make(chan *invoicesrpc.InvoiceAcceptorRequest) + errChan := make(chan error) + go func() { + // Consume one message. This will block until the message is + // received. + resp, err := stream.Recv() + if err != nil { + errChan <- err + return + } + chanMsg <- resp + }() + + select { + case <-time.After(DefaultTimeout): + require.Fail(h, "timeout", "timeout invoice acceptor") + + case err := <-errChan: + require.Failf(h, "err from stream", + "received err from stream: %v", err) + + case updateMsg := <-chanMsg: + return updateMsg + } + + return nil +} + // ReceiveChannelEvent waits until a message is received from the // ChannelEventsClient stream or the timeout is reached. func (h *HarnessTest) ReceiveChannelEvent( diff --git a/lntest/rpc/router.go b/lntest/rpc/router.go index fbf44cb18a2..872e44bf56a 100644 --- a/lntest/rpc/router.go +++ b/lntest/rpc/router.go @@ -4,6 +4,7 @@ import ( "context" "github.com/lightningnetwork/lnd/lnrpc" + "github.com/lightningnetwork/lnd/lnrpc/invoicesrpc" "github.com/lightningnetwork/lnd/lnrpc/routerrpc" "github.com/stretchr/testify/require" ) @@ -193,6 +194,23 @@ func (h *HarnessRPC) HtlcInterceptor() (InterceptorClient, context.CancelFunc) { return resp, cancel } +type InvoiceAcceptorClient invoicesrpc.Invoices_InvoiceAcceptorClient + +// InvoiceAcceptor makes an RPC call to the node's RouterClient and asserts. +func (h *HarnessRPC) InvoiceAcceptor() (InvoiceAcceptorClient, + context.CancelFunc) { + + // InvoiceAcceptor needs to have the context alive for the entire test + // case as the returned client will be used for send and receive events + // stream. Therefore, we use cancel context here instead of a timeout + // context. + ctxt, cancel := context.WithCancel(h.runCtx) + resp, err := h.Invoice.InvoiceAcceptor(ctxt) + h.NoError(err, "InvoiceAcceptor") + + return resp, cancel +} + type TrackPaymentsClient routerrpc.Router_TrackPaymentsClient // TrackPayments makes a RPC call to the node's RouterClient and asserts. diff --git a/lnwallet/aux_signer.go b/lnwallet/aux_signer.go new file mode 100644 index 00000000000..da2d0f5151f --- /dev/null +++ b/lnwallet/aux_signer.go @@ -0,0 +1,148 @@ +package lnwallet + +import ( + "github.com/btcsuite/btcd/wire" + "github.com/lightningnetwork/lnd/channeldb" + "github.com/lightningnetwork/lnd/fn" + "github.com/lightningnetwork/lnd/input" + "github.com/lightningnetwork/lnd/tlv" +) + +// BaseAuxJob is a struct that contains the common fields that are shared among +// the aux sign/verify jobs. +type BaseAuxJob struct { + // OutputIndex is the output index of the HTLC on the commitment + // transaction being signed. + OutputIndex int32 + + // KeyRing is the commitment key ring that contains the keys needed to + // generate the second level HTLC signatures. + KeyRing CommitmentKeyRing + + // HTLC is the HTLC that is being signed or verified. + HTLC PaymentDescriptor + + // CommitBlob is the commitment transaction blob that contains the aux + // information for this channel. + CommitBlob fn.Option[tlv.Blob] + + // HtlcLeaf is the aux tap leaf that corresponds to the HTLC being + // signed/verified. + HtlcLeaf input.AuxTapLeaf +} + +// AuxSigJob is a struct that contains all the information needed to sign an +// HTLC for custom channels. +type AuxSigJob struct { + // SignDesc is the sign desc for this HTLC. + SignDesc input.SignDescriptor + + BaseAuxJob + + // Resp is a channel that will be used to send the result of the sign + // job. + Resp chan AuxSigJobResp + + // Cancel is a channel that should be closed if the caller wishes to + // abandon all pending sign jobs part of a single batch. + Cancel chan struct{} +} + +// NewAuxSigJob creates a new AuxSigJob. +func NewAuxSigJob(sigJob SignJob, keyRing CommitmentKeyRing, + htlc PaymentDescriptor, commitBlob fn.Option[tlv.Blob], + htlcLeaf input.AuxTapLeaf, cancelChan chan struct{}) AuxSigJob { + + return AuxSigJob{ + SignDesc: sigJob.SignDesc, + BaseAuxJob: BaseAuxJob{ + OutputIndex: sigJob.OutputIndex, + KeyRing: keyRing, + HTLC: htlc, + CommitBlob: commitBlob, + HtlcLeaf: htlcLeaf, + }, + Resp: make(chan AuxSigJobResp, 1), + Cancel: cancelChan, + } +} + +// AuxSigJobResp is a struct that contains the result of a sign job. +type AuxSigJobResp struct { + // SigBlob is the signature blob that was generated for the HTLC. This + // is an opauqe TLV field that may contains the signature and other + // data. + // + // TODO(roasbeef): just make sig? or new type that can map to/from a + // blob + SigBlob fn.Option[tlv.Blob] + + // HtlcIndex is the index of the HTLC that was signed. + HtlcIndex uint64 + + // Err is the error that occurred when executing the specified + // signature job. In the case that no error occurred, this value will + // be nil. + Err error +} + +// AuxVerifyJob is a struct that contains all the information needed to verify +// an HTLC for custom channels. +type AuxVerifyJob struct { + // SigBlob is the signature blob that was generated for the HTLC. This + // is an opauqe TLV field that may contains the signature and other + // data. + SigBlob fn.Option[tlv.Blob] + + BaseAuxJob + + // Cancel is a channel that should be closed if the caller wishes to + // abandon the job. + Cancel chan struct{} + + // ErrResp is a channel that will be used to send the result of the + // verify job. + ErrResp chan error +} + +// NewAuxVerifyJob creates a new AuxVerifyJob. +func NewAuxVerifyJob(sig fn.Option[tlv.Blob], keyRing CommitmentKeyRing, + htlc PaymentDescriptor, commitBlob fn.Option[tlv.Blob], + htlcLeaf input.AuxTapLeaf) AuxVerifyJob { + + return AuxVerifyJob{ + SigBlob: sig, + BaseAuxJob: BaseAuxJob{ + KeyRing: keyRing, + HTLC: htlc, + CommitBlob: commitBlob, + HtlcLeaf: htlcLeaf, + }, + } +} + +// AuxSigner is an interface that is used to sign and verify HTLCs for custom +// channels. It is similar to the existing SigPool, but uses opaque blobs to +// shuffle around signature information and other metadata. +type AuxSigner interface { + // SubmitSecondLevelSigBatch takes a batch of aux sign jobs and + // processes them asynchronously. + SubmitSecondLevelSigBatch(chanState *channeldb.OpenChannel, + commitTx *wire.MsgTx, sigJob []AuxSigJob) error + + // PackSigs takes a series of aux signatures and packs them into a + // single blob that can be sent alongside the CommitSig messages. + PackSigs(map[input.HtlcIndex]fn.Option[tlv.Blob]) (fn.Option[tlv.Blob], + error) + + // UnpackSigs takes a packed blob of signatures and returns the + // original signatures for each HTLC, keyed by HTLC index. + UnpackSigs( + fn.Option[tlv.Blob]) (map[input.HtlcIndex]fn.Option[tlv.Blob], + error) + + // VerifySecondLevelSigs attempts to synchronously verify a batch of aux + // sig jobs. + VerifySecondLevelSigs(chanState *channeldb.OpenChannel, + commitTx *wire.MsgTx, verifyJob []AuxVerifyJob) error +} diff --git a/lnwallet/chanfunding/canned_assembler.go b/lnwallet/chanfunding/canned_assembler.go index 6b95c2bd2aa..b4238ff75a9 100644 --- a/lnwallet/chanfunding/canned_assembler.go +++ b/lnwallet/chanfunding/canned_assembler.go @@ -92,10 +92,12 @@ func (s *ShimIntent) FundingOutput() ([]byte, *wire.TxOut, error) { // Similar to the existing p2wsh script, we'll always ensure // the keys are sorted before use. - return input.GenTaprootFundingScript( + pkScript, txOut, _, err := input.GenTaprootFundingScript( s.localKey.PubKey, s.remoteKey, int64(totalAmt), scriptOpts..., ) + + return pkScript, txOut, err } return input.GenFundingPkScript( @@ -105,6 +107,34 @@ func (s *ShimIntent) FundingOutput() ([]byte, *wire.TxOut, error) { ) } +// TaprootInternalKey may return the internal key for a musig2 funding output, +// but only if this is actually a musig2 channel. +func (s *ShimIntent) TaprootInternalKey() fn.Option[*btcec.PublicKey] { + if !s.musig2 { + return fn.None[*btcec.PublicKey]() + } + + // TODO(roasbeef): refactor + + var scriptOpts []input.FundingScriptOpt + s.tapscriptRoot.WhenSome(func(root chainhash.Hash) { + scriptOpts = append( + scriptOpts, input.WithTapscriptRoot(root), + ) + }) + + // Similar to the existing p2wsh script, we'll always ensure the keys + // are sorted before use. + // + // We ignore the eror here as this is only called afterr FundingOutput + // is called. + _, _, musig2Key, _ := input.GenTaprootFundingScript( + s.localKey.PubKey, s.remoteKey, 0, scriptOpts..., + ) + + return fn.Some(musig2Key.PreTweakedKey) +} + // Cancel allows the caller to cancel a funding Intent at any time. This will // return any resources such as coins back to the eligible pool to be used in // order channel fundings. diff --git a/lnwallet/chanfunding/psbt_assembler.go b/lnwallet/chanfunding/psbt_assembler.go index 10bcd70159e..8a9f41f3cf7 100644 --- a/lnwallet/chanfunding/psbt_assembler.go +++ b/lnwallet/chanfunding/psbt_assembler.go @@ -6,11 +6,14 @@ import ( "sync" "github.com/btcsuite/btcd/btcec/v2" + "github.com/btcsuite/btcd/btcec/v2/schnorr" "github.com/btcsuite/btcd/btcutil" "github.com/btcsuite/btcd/btcutil/psbt" "github.com/btcsuite/btcd/chaincfg" + "github.com/btcsuite/btcd/chaincfg/chainhash" "github.com/btcsuite/btcd/txscript" "github.com/btcsuite/btcd/wire" + "github.com/lightningnetwork/lnd/fn" "github.com/lightningnetwork/lnd/input" "github.com/lightningnetwork/lnd/keychain" ) @@ -162,6 +165,13 @@ func (i *PsbtIntent) BindKeys(localKey *keychain.KeyDescriptor, i.State = PsbtOutputKnown } +// BindTapscriptRoot takes an optional tapscript root and binds it to the +// underlying funding intent. This only applies to musig2 channels, and will be +// used to make the musig2 funding output. +func (i *PsbtIntent) BindTapscriptRoot(root fn.Option[chainhash.Hash]) { + i.tapscriptRoot = root +} + // FundingParams returns the parameters that are necessary to start funding the // channel output this intent was created for. It returns the P2WSH funding // address, the exact funding amount and a PSBT packet that contains exactly one @@ -208,7 +218,17 @@ func (i *PsbtIntent) FundingParams() (btcutil.Address, int64, *psbt.Packet, } } packet.UnsignedTx.TxOut = append(packet.UnsignedTx.TxOut, out) - packet.Outputs = append(packet.Outputs, psbt.POutput{}) + + var pOut psbt.POutput + + // If this is a musig2 channel, then we'll also return the internal key + // information as well along side the output. + pOut.TaprootInternalKey = fn.MapOptionZ( + i.TaprootInternalKey(), schnorr.SerializePubKey, + ) + + packet.Outputs = append(packet.Outputs, pOut) + return addr, out.Value, packet, nil } diff --git a/lnwallet/channel.go b/lnwallet/channel.go index b048a37b43a..fcf9b9d1cb5 100644 --- a/lnwallet/channel.go +++ b/lnwallet/channel.go @@ -385,6 +385,28 @@ type PaymentDescriptor struct { BlindingPoint lnwire.BlindingPointRecord } +// AddHeight returns a pointer to the height at which the HTLC was added to the +// commitment chain. The height is returned based on the chain the HTLC is +// being added to (local or remote chain). +func AddHeight(htlc *PaymentDescriptor, remoteChain bool) *uint64 { + if remoteChain { + return &htlc.addCommitHeightRemote + } + + return &htlc.addCommitHeightLocal +} + +// RemoveHeight returns a pointer to the height at which the HTLC was removed +// from the commitment chain. The height is returned based on the chain the HTLC +// is being removed from (local or remote chain). +func RemoveHeight(htlc *PaymentDescriptor, remoteChain bool) *uint64 { + if remoteChain { + return &htlc.removeCommitHeightRemote + } + + return &htlc.removeCommitHeightLocal +} + // PayDescsFromRemoteLogUpdates converts a slice of LogUpdates received from the // remote peer into PaymentDescriptors to inform a link's forwarding decisions. // @@ -1336,6 +1358,10 @@ type LightningChannel struct { // signatures, of which there may be hundreds. sigPool *SigPool + // auxSigner is a special signer used to obtain opaque signatures for + // custom channel variants. + auxSigner fn.Option[AuxSigner] + // Capacity is the total capacity of this channel. Capacity btcutil.Amount @@ -1400,6 +1426,7 @@ type channelOpts struct { remoteNonce *musig2.Nonces leafStore fn.Option[AuxLeafStore] + auxSigner fn.Option[AuxSigner] skipNonceInit bool } @@ -1438,6 +1465,13 @@ func WithLeafStore(store AuxLeafStore) ChannelOpt { } } +// WithAuxSigner is used to specify a custom aux signer for the channel. +func WithAuxSigner(signer AuxSigner) ChannelOpt { + return func(o *channelOpts) { + o.auxSigner = fn.Some[AuxSigner](signer) + } +} + // defaultChannelOpts returns the set of default options for a new channel. func defaultChannelOpts() *channelOpts { return &channelOpts{} @@ -1481,6 +1515,7 @@ func NewLightningChannel(signer input.Signer, lc := &LightningChannel{ Signer: signer, leafStore: opts.leafStore, + auxSigner: opts.auxSigner, sigPool: sigPool, currentHeight: localCommit.CommitHeight, remoteCommitChain: newCommitmentChain(), @@ -1554,7 +1589,7 @@ func (lc *LightningChannel) createSignDesc() error { chanState.TapscriptRoot, TapscriptRootToOpt, ) - fundingPkScript, _, err = input.GenTaprootFundingScript( + fundingPkScript, _, _, err = input.GenTaprootFundingScript( localKey, remoteKey, int64(lc.channelState.Capacity), fundingOpts..., ) @@ -3456,13 +3491,7 @@ func processAddEntry(htlc *PaymentDescriptor, ourBalance, theirBalance *lnwire.M // a new commitment), then we'll may be updating the height this entry // was added to the chain. Otherwise, we may be updating the entry's // height w.r.t the local chain. - var addHeight *uint64 - if remoteChain { - addHeight = &htlc.addCommitHeightRemote - } else { - addHeight = &htlc.addCommitHeightLocal - } - + addHeight := AddHeight(htlc, remoteChain) if *addHeight != 0 { return } @@ -3493,14 +3522,8 @@ func processRemoveEntry(htlc *PaymentDescriptor, ourBalance, theirBalance *lnwire.MilliSatoshi, nextHeight uint64, remoteChain bool, isIncoming, mutateState bool) { - var removeHeight *uint64 - if remoteChain { - removeHeight = &htlc.removeCommitHeightRemote - } else { - removeHeight = &htlc.removeCommitHeightLocal - } - // Ignore any removal entries which have already been processed. + removeHeight := RemoveHeight(htlc, remoteChain) if *removeHeight != 0 { return } @@ -3544,15 +3567,8 @@ func processFeeUpdate(feeUpdate *PaymentDescriptor, nextHeight uint64, // Fee updates are applied for all commitments after they are // sent/received, so we consider them being added and removed at the // same height. - var addHeight *uint64 - var removeHeight *uint64 - if remoteChain { - addHeight = &feeUpdate.addCommitHeightRemote - removeHeight = &feeUpdate.removeCommitHeightRemote - } else { - addHeight = &feeUpdate.addCommitHeightLocal - removeHeight = &feeUpdate.removeCommitHeightLocal - } + addHeight := AddHeight(feeUpdate, remoteChain) + removeHeight := RemoveHeight(feeUpdate, remoteChain) if *addHeight != 0 { return @@ -3577,7 +3593,7 @@ func processFeeUpdate(feeUpdate *PaymentDescriptor, nextHeight uint64, func genRemoteHtlcSigJobs(keyRing *CommitmentKeyRing, chanState *channeldb.OpenChannel, leaseExpiry uint32, remoteCommitView *commitment, - leafStore fn.Option[AuxLeafStore]) ([]SignJob, chan struct{}, error) { + leafStore fn.Option[AuxLeafStore]) ([]SignJob, []AuxSigJob, chan struct{}, error) { var ( isRemoteInitiator = !chanState.IsInitiator @@ -3594,9 +3610,10 @@ func genRemoteHtlcSigJobs(keyRing *CommitmentKeyRing, // With the keys generated, we'll make a slice with enough capacity to // hold potentially all the HTLCs. The actual slice may be a bit // smaller (than its total capacity) and some HTLCs may be dust. - numSigs := (len(remoteCommitView.incomingHTLCs) + - len(remoteCommitView.outgoingHTLCs)) + numSigs := len(remoteCommitView.incomingHTLCs) + + len(remoteCommitView.outgoingHTLCs) sigBatch := make([]SignJob, 0, numSigs) + auxSigBatch := make([]AuxSigJob, 0, numSigs) var err error cancelChan := make(chan struct{}) @@ -3606,7 +3623,7 @@ func genRemoteHtlcSigJobs(keyRing *CommitmentKeyRing, *keyRing, ) if err != nil { - return nil, nil, fmt.Errorf("unable to fetch aux leaves: "+ + return nil, nil, nil, fmt.Errorf("unable to fetch aux leaves: "+ "%w", err) } @@ -3657,11 +3674,9 @@ func genRemoteHtlcSigJobs(keyRing *CommitmentKeyRing, fn.FlattenOption(auxLeaf), ) if err != nil { - return nil, nil, err + return nil, nil, nil, err } - // TODO(roasbeef): hook up signer interface here - // Construct a full hash cache as we may be signing a segwit v1 // sighash. txOut := remoteCommitView.txn.TxOut[htlc.remoteOutputIndex] @@ -3693,6 +3708,12 @@ func genRemoteHtlcSigJobs(keyRing *CommitmentKeyRing, } sigBatch = append(sigBatch, sigJob) + + auxSigJob := NewAuxSigJob( + sigJob, *keyRing, htlc, remoteCommitView.customBlob, + fn.FlattenOption(auxLeaf), cancelChan, + ) + auxSigBatch = append(auxSigBatch, auxSigJob) } for _, htlc := range remoteCommitView.outgoingHTLCs { if HtlcIsDust( @@ -3737,7 +3758,7 @@ func genRemoteHtlcSigJobs(keyRing *CommitmentKeyRing, fn.FlattenOption(auxLeaf), ) if err != nil { - return nil, nil, err + return nil, nil, nil, err } // Construct a full hash cache as we may be signing a segwit v1 @@ -3770,9 +3791,15 @@ func genRemoteHtlcSigJobs(keyRing *CommitmentKeyRing, } sigBatch = append(sigBatch, sigJob) + + auxSigJob := NewAuxSigJob( + sigJob, *keyRing, htlc, remoteCommitView.customBlob, + fn.FlattenOption(auxLeaf), cancelChan, + ) + auxSigBatch = append(auxSigBatch, auxSigJob) } - return sigBatch, cancelChan, nil + return sigBatch, auxSigBatch, cancelChan, nil } // createCommitDiff will create a commit diff given a new pending commitment @@ -3781,7 +3808,8 @@ func genRemoteHtlcSigJobs(keyRing *CommitmentKeyRing, // new commitment to the remote party. The commit diff returned contains all // information necessary for retransmission. func (lc *LightningChannel) createCommitDiff(newCommit *commitment, - commitSig lnwire.Sig, htlcSigs []lnwire.Sig) (*channeldb.CommitDiff, + commitSig lnwire.Sig, htlcSigs []lnwire.Sig, + auxSigs map[input.HtlcIndex]fn.Option[tlv.Blob]) (*channeldb.CommitDiff, error) { // First, we need to convert the funding outpoint into the ID that's @@ -3905,6 +3933,11 @@ func (lc *LightningChannel) createCommitDiff(newCommit *commitment, // disk. diskCommit := newCommit.toDiskCommit(false) + auxSigBlob, err := packSigs(auxSigs, lc.auxSigner) + if err != nil { + return nil, fmt.Errorf("error packing aux sigs: %w", err) + } + return &channeldb.CommitDiff{ Commitment: *diskCommit, CommitSig: &lnwire.CommitSig{ @@ -3913,6 +3946,7 @@ func (lc *LightningChannel) createCommitDiff(newCommit *commitment, ), CommitSig: commitSig, HtlcSigs: htlcSigs, + ExtraData: auxSigBlob, }, LogUpdates: logUpdates, OpenedCircuitKeys: openCircuitKeys, @@ -4362,6 +4396,10 @@ type CommitSigs struct { // PartialSig is the musig2 partial signature for taproot commitment // transactions. PartialSig lnwire.OptPartialSigWithNonceTLV + + // AuxSigBlob is the blob containing all the auxiliary signatures for + // this new commitment state. + AuxSigBlob tlv.Blob } // NewCommitState wraps the various signatures needed to properly @@ -4480,7 +4518,7 @@ func (lc *LightningChannel) SignNextCommitment() (*NewCommitState, error) { if lc.channelState.ChanType.HasLeaseExpiration() { leaseExpiry = lc.channelState.ThawHeight } - sigBatch, cancelChan, err := genRemoteHtlcSigJobs( + sigBatch, auxSigBatch, cancelChan, err := genRemoteHtlcSigJobs( keyRing, lc.channelState, leaseExpiry, newCommitView, lc.leafStore, ) @@ -4489,6 +4527,16 @@ func (lc *LightningChannel) SignNextCommitment() (*NewCommitState, error) { } lc.sigPool.SubmitSignBatch(sigBatch) + err = fn.MapOptionZ(lc.auxSigner, func(a AuxSigner) error { + return a.SubmitSecondLevelSigBatch( + lc.channelState, newCommitView.txn, auxSigBatch, + ) + }) + if err != nil { + return nil, fmt.Errorf("error submitting second level sig "+ + "batch: %w", err) + } + // While the jobs are being carried out, we'll Sign their version of // the new commitment transaction while we're waiting for the rest of // the HTLC signatures to be processed. @@ -4532,11 +4580,18 @@ func (lc *LightningChannel) SignNextCommitment() (*NewCommitState, error) { sort.Slice(sigBatch, func(i, j int) bool { return sigBatch[i].OutputIndex < sigBatch[j].OutputIndex }) + sort.Slice(auxSigBatch, func(i, j int) bool { + return auxSigBatch[i].OutputIndex < auxSigBatch[j].OutputIndex + }) // With the jobs sorted, we'll now iterate through all the responses to // gather each of the signatures in order. htlcSigs = make([]lnwire.Sig, 0, len(sigBatch)) - for _, htlcSigJob := range sigBatch { + auxSigs := make( + map[input.HtlcIndex]fn.Option[tlv.Blob], len(auxSigBatch), + ) + for i := range sigBatch { + htlcSigJob := sigBatch[i] jobResp := <-htlcSigJob.Resp // If an error occurred, then we'll cancel any other active @@ -4547,12 +4602,30 @@ func (lc *LightningChannel) SignNextCommitment() (*NewCommitState, error) { } htlcSigs = append(htlcSigs, jobResp.Sig) + + if lc.auxSigner.IsNone() { + continue + } + + auxHtlcSigJob := auxSigBatch[i] + auxJobResp := <-auxHtlcSigJob.Resp + + // If an error occurred, then we'll cancel any other active + // jobs. + if auxJobResp.Err != nil { + close(cancelChan) + return nil, auxJobResp.Err + } + + auxSigs[auxJobResp.HtlcIndex] = auxJobResp.SigBlob } // As we're about to proposer a new commitment state for the remote // party, we'll write this pending state to disk before we exit, so we // can retransmit it if necessary. - commitDiff, err := lc.createCommitDiff(newCommitView, sig, htlcSigs) + commitDiff, err := lc.createCommitDiff( + newCommitView, sig, htlcSigs, auxSigs, + ) if err != nil { return nil, err } @@ -4574,6 +4647,7 @@ func (lc *LightningChannel) SignNextCommitment() (*NewCommitState, error) { CommitSig: sig, HtlcSigs: htlcSigs, PartialSig: lnwire.MaybePartialSigWithNonce(partialSig), + AuxSigBlob: commitDiff.CommitSig.ExtraData, }, PendingHTLCs: commitDiff.Commitment.Htlcs, }, nil @@ -4970,10 +5044,12 @@ func (lc *LightningChannel) computeView(view *HtlcView, remoteChain bool, // number of outstanding HTLCs has changed. if lc.channelState.IsInitiator { ourBalance += lnwire.NewMSatFromSatoshis( - commitChain.tip().fee) + commitChain.tip().fee, + ) } else if !lc.channelState.IsInitiator { theirBalance += lnwire.NewMSatFromSatoshis( - commitChain.tip().fee) + commitChain.tip().fee, + ) } nextHeight := commitChain.tip().height + 1 @@ -5053,7 +5129,8 @@ func (lc *LightningChannel) computeView(view *HtlcView, remoteChain bool, func genHtlcSigValidationJobs(chanState *channeldb.OpenChannel, localCommitmentView *commitment, keyRing *CommitmentKeyRing, htlcSigs []lnwire.Sig, leaseExpiry uint32, - leafStore fn.Option[AuxLeafStore]) ([]VerifyJob, error) { + leafStore fn.Option[AuxLeafStore], auxSigner fn.Option[AuxSigner], + sigBlob fn.Option[tlv.Blob]) ([]VerifyJob, []AuxVerifyJob, error) { var ( isLocalInitiator = chanState.IsInitiator @@ -5072,13 +5149,22 @@ func genHtlcSigValidationJobs(chanState *channeldb.OpenChannel, numHtlcs := (len(localCommitmentView.incomingHTLCs) + len(localCommitmentView.outgoingHTLCs)) verifyJobs := make([]VerifyJob, 0, numHtlcs) + auxVerifyJobs := make([]AuxVerifyJob, 0, numHtlcs) auxLeaves, err := AuxLeavesFromCommit( chanState, *localCommitmentView.toDiskCommit(true), leafStore, *keyRing, ) if err != nil { - return nil, fmt.Errorf("unable to fetch aux leaves: %w", + return nil, nil, fmt.Errorf("unable to fetch aux leaves: %w", + err) + } + + // If we have a sig blob, then we'll attempt to map that to individual + // blobs for each HTLC we might need a signature for. + auxHtlcSigs, err := unpackSigs(sigBlob, auxSigner) + if err != nil { + return nil, nil, fmt.Errorf("error unpacking aux sigs: %w", err) } @@ -5092,6 +5178,8 @@ func genHtlcSigValidationJobs(chanState *channeldb.OpenChannel, htlcIndex uint64 sigHash func() ([]byte, error) sig input.Signature + htlc *PaymentDescriptor + auxLeaf input.AuxTapLeaf err error ) @@ -5102,7 +5190,7 @@ func genHtlcSigValidationJobs(chanState *channeldb.OpenChannel, // index, then this means that we need to generate an HTLC // success transaction in order to validate the signature. case localCommitmentView.incomingHTLCIndex[outputIndex] != nil: - htlc := localCommitmentView.incomingHTLCIndex[outputIndex] + htlc = localCommitmentView.incomingHTLCIndex[outputIndex] htlcIndex = htlc.HtlcIndex @@ -5115,20 +5203,20 @@ func genHtlcSigValidationJobs(chanState *channeldb.OpenChannel, htlcFee := HtlcSuccessFee(chanType, feePerKw) outputAmt := htlc.Amount.ToSatoshis() - htlcFee - auxLeaf := fn.MapOption(func( + leaf := fn.MapOption(func( l CommitAuxLeaves) input.AuxTapLeaf { leaves := l.IncomingHtlcLeaves idx := htlc.HtlcIndex return leaves[idx].SecondLevelLeaf })(auxLeaves) + auxLeaf = fn.FlattenOption(leaf) successTx, err := CreateHtlcSuccessTx( chanType, isLocalInitiator, op, outputAmt, uint32(localChanCfg.CsvDelay), leaseExpiry, keyRing.RevocationKey, - keyRing.ToLocalKey, - fn.FlattenOption(auxLeaf), + keyRing.ToLocalKey, auxLeaf, ) if err != nil { return nil, err @@ -5171,7 +5259,7 @@ func genHtlcSigValidationJobs(chanState *channeldb.OpenChannel, // Make sure there are more signatures left. if i >= len(htlcSigs) { - return nil, fmt.Errorf("not enough HTLC " + + return nil, nil, fmt.Errorf("not enough HTLC " + "signatures") } @@ -5187,7 +5275,7 @@ func genHtlcSigValidationJobs(chanState *channeldb.OpenChannel, // is valid. sig, err = htlcSigs[i].ToSignature() if err != nil { - return nil, err + return nil, nil, err } htlc.sig = sig @@ -5195,7 +5283,7 @@ func genHtlcSigValidationJobs(chanState *channeldb.OpenChannel, // generate a timeout transaction so we can verify the // signature presented. case localCommitmentView.outgoingHTLCIndex[outputIndex] != nil: - htlc := localCommitmentView.outgoingHTLCIndex[outputIndex] + htlc = localCommitmentView.outgoingHTLCIndex[outputIndex] htlcIndex = htlc.HtlcIndex @@ -5208,21 +5296,21 @@ func genHtlcSigValidationJobs(chanState *channeldb.OpenChannel, htlcFee := HtlcTimeoutFee(chanType, feePerKw) outputAmt := htlc.Amount.ToSatoshis() - htlcFee - auxLeaf := fn.MapOption(func( + leaf := fn.MapOption(func( l CommitAuxLeaves) input.AuxTapLeaf { leaves := l.OutgoingHtlcLeaves idx := htlc.HtlcIndex return leaves[idx].SecondLevelLeaf })(auxLeaves) + auxLeaf = fn.FlattenOption(leaf) timeoutTx, err := CreateHtlcTimeoutTx( chanType, isLocalInitiator, op, outputAmt, htlc.Timeout, uint32(localChanCfg.CsvDelay), leaseExpiry, keyRing.RevocationKey, - keyRing.ToLocalKey, - fn.FlattenOption(auxLeaf), + keyRing.ToLocalKey, auxLeaf, ) if err != nil { return nil, err @@ -5267,7 +5355,7 @@ func genHtlcSigValidationJobs(chanState *channeldb.OpenChannel, // Make sure there are more signatures left. if i >= len(htlcSigs) { - return nil, fmt.Errorf("not enough HTLC " + + return nil, nil, fmt.Errorf("not enough HTLC " + "signatures") } @@ -5283,7 +5371,7 @@ func genHtlcSigValidationJobs(chanState *channeldb.OpenChannel, // is valid. sig, err = htlcSigs[i].ToSignature() if err != nil { - return nil, err + return nil, nil, err } htlc.sig = sig @@ -5300,16 +5388,26 @@ func genHtlcSigValidationJobs(chanState *channeldb.OpenChannel, }) i++ + + // TODO(roasbeef): meld aux six into tlv blob for htlc on disk? + + auxSig := auxHtlcSigs[htlcIndex] + auxVerifyJob := NewAuxVerifyJob( + auxSig, *keyRing, *htlc, + localCommitmentView.customBlob, auxLeaf, + ) + + auxVerifyJobs = append(auxVerifyJobs, auxVerifyJob) } // If we received a number of HTLC signatures that doesn't match our // commitment, we'll return an error now. if len(htlcSigs) != i { - return nil, fmt.Errorf("number of htlc sig mismatch. "+ + return nil, nil, fmt.Errorf("number of htlc sig mismatch. "+ "Expected %v sigs, got %v", i, len(htlcSigs)) } - return verifyJobs, nil + return verifyJobs, auxVerifyJobs, nil } // InvalidCommitSigError is a struct that implements the error interface to @@ -5471,6 +5569,11 @@ func (lc *LightningChannel) ReceiveNewCommitment(commitSigs *CommitSigs) error { }), ) + var auxSigBlob fn.Option[tlv.Blob] + if commitSigs.AuxSigBlob != nil { + auxSigBlob = fn.Some(commitSigs.AuxSigBlob) + } + // As an optimization, we'll generate a series of jobs for the worker // pool to verify each of the HTLC signatures presented. Once // generated, we'll submit these jobs to the worker pool. @@ -5478,9 +5581,10 @@ func (lc *LightningChannel) ReceiveNewCommitment(commitSigs *CommitSigs) error { if lc.channelState.ChanType.HasLeaseExpiration() { leaseExpiry = lc.channelState.ThawHeight } - verifyJobs, err := genHtlcSigValidationJobs( + verifyJobs, auxVerifyJobs, err := genHtlcSigValidationJobs( lc.channelState, localCommitmentView, keyRing, - commitSigs.HtlcSigs, leaseExpiry, lc.leafStore, + commitSigs.HtlcSigs, leaseExpiry, lc.leafStore, lc.auxSigner, + auxSigBlob, ) if err != nil { return err @@ -5629,6 +5733,17 @@ func (lc *LightningChannel) ReceiveNewCommitment(commitSigs *CommitSigs) error { } } + // Now that we know all the normal sigs are valid, we'll also verify + // the aux jobs, if any exist. + err = fn.MapOptionZ(lc.auxSigner, func(a AuxSigner) error { + return a.VerifySecondLevelSigs( + lc.channelState, localCommitTx, auxVerifyJobs, + ) + }) + if err != nil { + return fmt.Errorf("unable to validate aux sigs: %w", err) + } + // The signature checks out, so we can now add the new commitment to // our local commitment chain. For regular channels, we can just // serialize the ECDSA sig. For taproot channels, we'll serialize the @@ -8460,7 +8575,7 @@ func NewAnchorResolution(chanState *channeldb.OpenChannel, WitnessScript: anchorWitnessScript, Output: &wire.TxOut{ PkScript: localAnchor.PkScript(), - Value: int64(anchorSize), + Value: int64(AnchorSize), }, HashType: sweepSigHash(chanState.ChanType), } @@ -8472,7 +8587,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 @@ -8965,7 +9080,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 @@ -8985,7 +9100,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 4914fa13946..594caf9b90b 100644 --- a/lnwallet/channel_test.go +++ b/lnwallet/channel_test.go @@ -713,7 +713,7 @@ func TestCooperativeChannelClosure(t *testing.T) { testCoopClose(t, &coopCloseTestCase{ chanType: channeldb.SingleFunderTweaklessBit | channeldb.AnchorOutputsBit, - anchorAmt: anchorSize * 2, + anchorAmt: AnchorSize * 2, }) }) } @@ -823,7 +823,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) { @@ -832,7 +832,7 @@ func TestForceClose(t *testing.T) { channeldb.AnchorOutputsBit | channeldb.SimpleTaprootFeatureBit, expectedCommitWeight: input.TaprootCommitWeight, - anchorAmt: anchorSize * 2, + anchorAmt: AnchorSize * 2, }) }) t.Run("taproot with tapscript root", func(t *testing.T) { @@ -842,7 +842,7 @@ func TestForceClose(t *testing.T) { channeldb.SimpleTaprootFeatureBit | channeldb.TapscriptRootBit, expectedCommitWeight: input.TaprootCommitWeight, - anchorAmt: anchorSize * 2, + anchorAmt: AnchorSize * 2, }) }) } @@ -928,7 +928,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") } @@ -10168,9 +10168,8 @@ func TestCreateBreachRetribution(t *testing.T) { } br, our, their, err := createBreachRetribution( - tc.revocationLog, tx, - aliceChannel.channelState, keyRing, - dummyPrivate, leaseExpiry, + tc.revocationLog, tx, aliceChannel.channelState, + keyRing, dummyPrivate, leaseExpiry, fn.None[CommitAuxLeaves](), ) diff --git a/lnwallet/commitment.go b/lnwallet/commitment.go index bdd10555a10..fc7a059b301 100644 --- a/lnwallet/commitment.go +++ b/lnwallet/commitment.go @@ -18,8 +18,8 @@ import ( "github.com/lightningnetwork/lnd/tlv" ) -// 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 @@ -201,9 +201,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 @@ -211,10 +211,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 { @@ -833,6 +831,37 @@ func updateAuxBlob(chanState *channeldb.OpenChannel, ) } +// packSigs is a helper function that attempts to pack a series of aux +// signatures and packs them into a single blob that can be sent alongside the +// CommitSig messages. +func packSigs(auxSigs map[input.HtlcIndex]fn.Option[tlv.Blob], + signer fn.Option[AuxSigner]) (tlv.Blob, error) { + + if signer.IsNone() { + return nil, nil + } + + blobOption, err := signer.UnsafeFromSome().PackSigs(auxSigs) + if err != nil { + return nil, fmt.Errorf("error packing aux sigs: %w", err) + } + + return blobOption.UnwrapOr(nil), nil +} + +// unpackSigs is a helper function that takes a packed blob of signatures and +// returns the original signatures for each HTLC, keyed by HTLC index. +func unpackSigs(blob fn.Option[tlv.Blob], + signer fn.Option[AuxSigner]) (map[input.HtlcIndex]fn.Option[tlv.Blob], + error) { + + if signer.IsNone() { + return nil, nil + } + + return signer.UnsafeFromSome().UnpackSigs(blob) +} + // createUnsignedCommitmentTx generates the unsigned commitment transaction for // a commitment view and returns it as part of the unsignedCommitmentTx. The // passed in balances should be balances *before* subtracting any commitment @@ -1153,7 +1182,7 @@ func CreateCommitTx(chanType channeldb.ChannelType, if localOutput || numHTLCs > 0 { commitTx.AddTxOut(&wire.TxOut{ PkScript: localAnchor.PkScript(), - Value: int64(anchorSize), + Value: int64(AnchorSize), }) } @@ -1162,7 +1191,7 @@ func CreateCommitTx(chanType channeldb.ChannelType, if remoteOutput || numHTLCs > 0 { commitTx.AddTxOut(&wire.TxOut{ PkScript: remoteAnchor.PkScript(), - Value: int64(anchorSize), + Value: int64(AnchorSize), }) } } @@ -1187,7 +1216,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 @@ -1285,9 +1314,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, ourCommit bool, timeout uint32, +func GenTaprootHtlcScript(isIncoming, ourCommit bool, timeout uint32, rHash [32]byte, keyRing *CommitmentKeyRing, auxLeaf input.AuxTapLeaf) (*input.HtlcScriptTree, error) { @@ -1357,7 +1386,7 @@ func genHtlcScript(chanType channeldb.ChannelType, isIncoming, ourCommit bool, ) } - return genTaprootHtlcScript( + return GenTaprootHtlcScript( isIncoming, ourCommit, timeout, rHash, keyRing, auxLeaf, ) } diff --git a/lnwallet/config.go b/lnwallet/config.go index 24961f38edb..425fe15dad1 100644 --- a/lnwallet/config.go +++ b/lnwallet/config.go @@ -67,4 +67,8 @@ type Config struct { // AuxLeafStore is an optional store that can be used to store auxiliary // leaves for certain custom channel types. AuxLeafStore fn.Option[AuxLeafStore] + + // AuxSigner is an optional signer that can be used to sign auxiliary + // leaves for certain custom channel types. + AuxSigner fn.Option[AuxSigner] } diff --git a/lnwallet/reservation.go b/lnwallet/reservation.go index 4f0940fe967..dc63cafa2f6 100644 --- a/lnwallet/reservation.go +++ b/lnwallet/reservation.go @@ -11,6 +11,7 @@ import ( "github.com/btcsuite/btcd/chaincfg/chainhash" "github.com/btcsuite/btcd/wire" "github.com/lightningnetwork/lnd/channeldb" + "github.com/lightningnetwork/lnd/fn" "github.com/lightningnetwork/lnd/input" "github.com/lightningnetwork/lnd/keychain" "github.com/lightningnetwork/lnd/lnwallet/chanfunding" @@ -217,6 +218,15 @@ type ChannelReservation struct { fundingIntent chanfunding.Intent + // localInitAuxLeaves is an optional set of aux commitment leaves + // that'll modify the way we construct the commitment transaction, in + // particular the tapscript leaves. + localInitAuxLeaves fn.Option[CommitAuxLeaves] + + // remoteInitAuxLeaves is an optional set of aux commitment leaves for + // the remote party. + remoteInitAuxLeaves fn.Option[CommitAuxLeaves] + // nextRevocationKeyLoc stores the key locator information for this // channel. nextRevocationKeyLoc keychain.KeyLocator @@ -258,7 +268,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. @@ -602,12 +612,15 @@ func (r *ChannelReservation) IsCannedShim() bool { } // ProcessPsbt continues a previously paused funding flow that involves PSBT to -// construct the funding transaction. This method can be called once the PSBT is -// finalized and the signed transaction is available. -func (r *ChannelReservation) ProcessPsbt() error { +// construct the funding transaction. This method can be called once the PSBT +// is finalized and the signed transaction is available. +func (r *ChannelReservation) ProcessPsbt( + auxFundingDesc fn.Option[AuxFundingDesc]) error { + errChan := make(chan error, 1) r.wallet.msgChan <- &continueContributionMsg{ + auxFundingDesc: auxFundingDesc, pendingFundingID: r.reservationID, err: errChan, } @@ -709,8 +722,10 @@ func (r *ChannelReservation) CompleteReservation(fundingInputScripts []*input.Sc // available via the .OurSignatures() method. As this method should only be // called as a response to a single funder channel, only a commitment signature // will be populated. -func (r *ChannelReservation) CompleteReservationSingle(fundingPoint *wire.OutPoint, - commitSig input.Signature) (*channeldb.OpenChannel, error) { +func (r *ChannelReservation) CompleteReservationSingle( + fundingPoint *wire.OutPoint, commitSig input.Signature, + auxFundingDesc fn.Option[AuxFundingDesc], +) (*channeldb.OpenChannel, error) { errChan := make(chan error, 1) completeChan := make(chan *channeldb.OpenChannel, 1) @@ -720,6 +735,7 @@ func (r *ChannelReservation) CompleteReservationSingle(fundingPoint *wire.OutPoi fundingOutpoint: fundingPoint, theirCommitmentSig: commitSig, completeChan: completeChan, + auxFundingDesc: auxFundingDesc, err: errChan, } @@ -805,6 +821,36 @@ func (r *ChannelReservation) Cancel() error { return <-errChan } +// ChanState the current open channel state. +func (r *ChannelReservation) ChanState() *channeldb.OpenChannel { + r.RLock() + defer r.RUnlock() + return r.partialState +} + +// CommitmentKeyRings returns the local+remote key ring used for the very first +// commitment transaction both parties. +func (r *ChannelReservation) CommitmentKeyRings() (*CommitmentKeyRing, *CommitmentKeyRing) { + r.RLock() + defer r.RUnlock() + + chanType := r.partialState.ChanType + ourChanCfg := r.ourContribution.ChannelConfig + theirChanCfg := r.theirContribution.ChannelConfig + + localKeys := DeriveCommitmentKeys( + r.ourContribution.FirstCommitmentPoint, true, chanType, + ourChanCfg, theirChanCfg, + ) + + remoteKeys := DeriveCommitmentKeys( + r.theirContribution.FirstCommitmentPoint, false, chanType, + ourChanCfg, theirChanCfg, + ) + + return localKeys, remoteKeys +} + // VerifyConstraints is a helper function that can be used to check the sanity // of various channel constraints. func VerifyConstraints(c *channeldb.ChannelConstraints, diff --git a/lnwallet/sigpool.go b/lnwallet/sigpool.go index 2424757f937..78e66d94867 100644 --- a/lnwallet/sigpool.go +++ b/lnwallet/sigpool.go @@ -43,6 +43,8 @@ type VerifyJob struct { // HtlcIndex is the index of the HTLC from the PoV of the remote // party's update log. + // + // TODO(roasbeef): remove -- never actually used? HtlcIndex uint64 // Cancel is a channel that should be closed if the caller wishes to diff --git a/lnwallet/test/test_interface.go b/lnwallet/test/test_interface.go index 401c46683a4..0a6f20e5f2e 100644 --- a/lnwallet/test/test_interface.go +++ b/lnwallet/test/test_interface.go @@ -34,6 +34,7 @@ import ( "github.com/lightningnetwork/lnd/chainntnfs" "github.com/lightningnetwork/lnd/chainntnfs/btcdnotify" "github.com/lightningnetwork/lnd/channeldb" + "github.com/lightningnetwork/lnd/fn" "github.com/lightningnetwork/lnd/input" "github.com/lightningnetwork/lnd/keychain" "github.com/lightningnetwork/lnd/kvdb" @@ -936,6 +937,7 @@ func testSingleFunderReservationWorkflow(miner *rpctest.Harness, fundingPoint := aliceChanReservation.FundingOutpoint() _, err = bobChanReservation.CompleteReservationSingle( fundingPoint, aliceCommitSig, + fn.None[lnwallet.AuxFundingDesc](), ) require.NoError(t, err, "bob unable to consume single reservation") diff --git a/lnwallet/test_utils.go b/lnwallet/test_utils.go index 61ab72c2043..488064c42b1 100644 --- a/lnwallet/test_utils.go +++ b/lnwallet/test_utils.go @@ -254,7 +254,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 14c11b7f764..fc7ba902ecd 100644 --- a/lnwallet/transactions_test.go +++ b/lnwallet/transactions_test.go @@ -923,7 +923,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/lnwallet/wallet.go b/lnwallet/wallet.go index c579363542c..79ced58c401 100644 --- a/lnwallet/wallet.go +++ b/lnwallet/wallet.go @@ -31,6 +31,7 @@ import ( "github.com/lightningnetwork/lnd/lnwallet/chanvalidate" "github.com/lightningnetwork/lnd/lnwire" "github.com/lightningnetwork/lnd/shachain" + "github.com/lightningnetwork/lnd/tlv" ) const ( @@ -89,6 +90,33 @@ func (p *PsbtFundingRequired) Error() string { return ErrPsbtFundingRequired.Error() } +// AuxFundingDesc stores a series of attributes that may be used to modify the +// way the channel funding occurs. This struct contains information that can +// only be derived once both sides have received and sent their contributions +// to the channel (keys, etc). +type AuxFundingDesc struct { + // CustomFundingBlob is a custom blob that'll be stored in the database + // within the OpenChannel struct. This should represent information + // static to the channel lifetime. + CustomFundingBlob tlv.Blob + + // CustomLocalCommitBlob is a custom blob that'll be stored in the + // first commitment entry for the local party. + CustomLocalCommitBlob tlv.Blob + + // CustomRemoteCommitBlob is a custom blob that'll be stored in the + // first commitment entry for the remote party. + CustomRemoteCommitBlob tlv.Blob + + // LocalInitAuxLeaves is the set of aux leaves that'll be used for our + // very first commitment state. + LocalInitAuxLeaves CommitAuxLeaves + + // RemoteInitAuxLeaves is the set of aux leaves that'll be used for the + // very first commitment state for the remote party. + RemoteInitAuxLeaves CommitAuxLeaves +} + // InitFundingReserveMsg is the first message sent to initiate the workflow // required to open a payment channel with a remote peer. The initial required // parameters are configurable across channels. These parameters are to be @@ -201,9 +229,10 @@ type InitFundingReserveMsg struct { // channel that will be useful to our future selves. Memo []byte - // TapscriptRoot is the root of the tapscript tree that will be used to - // create the funding output. This is an optional field that should - // only be set for taproot channels. + CustomChannelData []byte + + // TapscriptRoot is an optional tapscript root that if provided, will + // be used to create the combined key for musig2 based channels. TapscriptRoot fn.Option[chainhash.Hash] // err is a channel in which all errors will be sent across. Will be @@ -241,7 +270,6 @@ type fundingReserveCancelMsg struct { type addContributionMsg struct { pendingFundingID uint64 - // TODO(roasbeef): Should also carry SPV proofs in we're in SPV mode contribution *ChannelContribution // NOTE: In order to avoid deadlocks, this channel MUST be buffered. @@ -254,6 +282,8 @@ type addContributionMsg struct { type continueContributionMsg struct { pendingFundingID uint64 + auxFundingDesc fn.Option[AuxFundingDesc] + // NOTE: In order to avoid deadlocks, this channel MUST be buffered. err chan error } @@ -309,6 +339,8 @@ type addCounterPartySigsMsg struct { type addSingleFunderSigsMsg struct { pendingFundingID uint64 + auxFundingDesc fn.Option[AuxFundingDesc] + // fundingOutpoint is the outpoint of the completed funding // transaction as assembled by the workflow initiator. fundingOutpoint *wire.OutPoint @@ -412,8 +444,6 @@ type LightningWallet struct { quit chan struct{} wg sync.WaitGroup - - // TODO(roasbeef): handle wallet lock/unlock } // NewLightningWallet creates/opens and initializes a LightningWallet instance. @@ -458,7 +488,6 @@ func (l *LightningWallet) Startup() error { } l.wg.Add(1) - // TODO(roasbeef): multiple request handlers? go l.requestHandler() return nil @@ -1412,7 +1441,6 @@ func (l *LightningWallet) initOurContribution(reservation *ChannelReservation, // transaction via coin selection are freed allowing future reservations to // include them. func (l *LightningWallet) handleFundingCancelRequest(req *fundingReserveCancelMsg) { - // TODO(roasbeef): holding lock too long l.limboMtx.Lock() defer l.limboMtx.Unlock() @@ -1437,11 +1465,6 @@ func (l *LightningWallet) handleFundingCancelRequest(req *fundingReserveCancelMs ) } - // TODO(roasbeef): is it even worth it to keep track of unused keys? - - // TODO(roasbeef): Is it possible to mark the unused change also as - // available? - delete(l.fundingLimbo, req.pendingFundingID) pid := pendingReservation.pendingChanID @@ -1461,7 +1484,8 @@ func (l *LightningWallet) handleFundingCancelRequest(req *fundingReserveCancelMs // createCommitOpts is a struct that holds the options for creating a new // commitment transaction. type createCommitOpts struct { - auxLeaves fn.Option[CommitAuxLeaves] + localAuxLeaves fn.Option[CommitAuxLeaves] + remoteAuxLeaves fn.Option[CommitAuxLeaves] } // defaultCommitOpts returns a new createCommitOpts with default values. @@ -1469,6 +1493,17 @@ func defaultCommitOpts() createCommitOpts { return createCommitOpts{} } +// WithAuxLeaves is a functional option that can be used to set the aux leaves +// for a new commitment transaction. +func WithAuxLeaves(localLeaves, + remoteLeaves fn.Option[CommitAuxLeaves]) CreateCommitOpt { + + return func(o *createCommitOpts) { + o.localAuxLeaves = localLeaves + o.remoteAuxLeaves = remoteLeaves + } +} + // CreateCommitOpt is a functional option that can be used to modify the way a // new commitment transaction is created. type CreateCommitOpt func(*createCommitOpts) @@ -1500,7 +1535,7 @@ func CreateCommitmentTxns(localBalance, remoteBalance btcutil.Amount, ourCommitTx, err := CreateCommitTx( chanType, fundingTxIn, localCommitmentKeys, ourChanCfg, theirChanCfg, localBalance, remoteBalance, 0, initiator, - leaseExpiry, options.auxLeaves, + leaseExpiry, options.localAuxLeaves, ) if err != nil { return nil, nil, err @@ -1514,7 +1549,7 @@ func CreateCommitmentTxns(localBalance, remoteBalance btcutil.Amount, theirCommitTx, err := CreateCommitTx( chanType, fundingTxIn, remoteCommitmentKeys, theirChanCfg, ourChanCfg, remoteBalance, localBalance, 0, !initiator, - leaseExpiry, options.auxLeaves, + leaseExpiry, options.remoteAuxLeaves, ) if err != nil { return nil, nil, err @@ -1607,16 +1642,24 @@ func (l *LightningWallet) handleContributionMsg(req *addContributionMsg) { // and remote key which will be needed to calculate the multisig // funding output in a next step. pendingChanID := pendingReservation.pendingChanID + walletLog.Debugf("Advancing PSBT funding flow for "+ "pending_id(%x), binding keys local_key=%v, "+ "remote_key=%x", pendingChanID, &ourContribution.MultiSigKey, theirContribution.MultiSigKey.PubKey.SerializeCompressed()) + fundingIntent.BindKeys( &ourContribution.MultiSigKey, theirContribution.MultiSigKey.PubKey, ) + // We might have a tapscript root, so we'll bind that now to + // ensure we make the proper funding output. + fundingIntent.BindTapscriptRoot( + pendingReservation.partialState.TapscriptRoot, + ) + // Exit early because we can't continue the funding flow yet. req.err <- &PsbtFundingRequired{ Intent: fundingIntent, @@ -1689,16 +1732,17 @@ func (l *LightningWallet) handleContributionMsg(req *addContributionMsg) { // the commitment transaction for the remote party, and verify their incoming // partial signature. func genMusigSession(ourContribution, theirContribution *ChannelContribution, - signer input.MuSig2Signer, - fundingOutput *wire.TxOut) *MusigPairSession { + signer input.MuSig2Signer, fundingOutput *wire.TxOut, + tapscriptRoot fn.Option[chainhash.Hash]) *MusigPairSession { return NewMusigPairSession(&MusigSessionCfg{ - LocalKey: ourContribution.MultiSigKey, - RemoteKey: theirContribution.MultiSigKey, - LocalNonce: *ourContribution.LocalNonce, - RemoteNonce: *theirContribution.LocalNonce, - Signer: signer, - InputTxOut: fundingOutput, + LocalKey: ourContribution.MultiSigKey, + RemoteKey: theirContribution.MultiSigKey, + LocalNonce: *ourContribution.LocalNonce, + RemoteNonce: *theirContribution.LocalNonce, + Signer: signer, + InputTxOut: fundingOutput, + TapscriptTweak: tapscriptRoot, }) } @@ -1748,6 +1792,7 @@ func (l *LightningWallet) signCommitTx(pendingReservation *ChannelReservation, musigSessions := genMusigSession( ourContribution, theirContribution, l.Cfg.Signer, fundingOutput, + pendingReservation.partialState.TapscriptRoot, ) pendingReservation.musigSessions = musigSessions } @@ -1783,6 +1828,24 @@ func (l *LightningWallet) handleChanPointReady(req *continueContributionMsg) { return } + chanState := pendingReservation.partialState + + // If we have an aux funding desc, then we can use it to populate some + // of the optional, but opaque TLV blobs we'll carry for the channel. + chanState.CustomBlob = fn.MapOption(func(desc AuxFundingDesc) tlv.Blob { + return desc.CustomFundingBlob + })(req.auxFundingDesc) + chanState.LocalCommitment.CustomBlob = fn.MapOption( + func(desc AuxFundingDesc) tlv.Blob { + return desc.CustomLocalCommitBlob + }, + )(req.auxFundingDesc) + chanState.RemoteCommitment.CustomBlob = fn.MapOption( + func(desc AuxFundingDesc) tlv.Blob { + return desc.CustomRemoteCommitBlob + }, + )(req.auxFundingDesc) + ourContribution := pendingReservation.ourContribution theirContribution := pendingReservation.theirContribution chanPoint := pendingReservation.partialState.FundingOutpoint @@ -1841,7 +1904,6 @@ func (l *LightningWallet) handleChanPointReady(req *continueContributionMsg) { // Store their current commitment point. We'll need this after the // first state transition in order to verify the authenticity of the // revocation. - chanState := pendingReservation.partialState chanState.RemoteCurrentRevocation = theirContribution.FirstCommitmentPoint // Create the txin to our commitment transaction; required to construct @@ -1857,6 +1919,14 @@ func (l *LightningWallet) handleChanPointReady(req *continueContributionMsg) { if pendingReservation.partialState.ChanType.HasLeaseExpiration() { leaseExpiry = pendingReservation.partialState.ThawHeight } + + localAuxLeaves := fn.MapOption(func(desc AuxFundingDesc) CommitAuxLeaves { + return desc.LocalInitAuxLeaves + })(req.auxFundingDesc) + remoteAuxLeaves := fn.MapOption(func(desc AuxFundingDesc) CommitAuxLeaves { + return desc.RemoteInitAuxLeaves + })(req.auxFundingDesc) + ourCommitTx, theirCommitTx, err := CreateCommitmentTxns( localBalance, remoteBalance, ourContribution.ChannelConfig, theirContribution.ChannelConfig, @@ -1864,6 +1934,7 @@ func (l *LightningWallet) handleChanPointReady(req *continueContributionMsg) { theirContribution.FirstCommitmentPoint, fundingTxIn, pendingReservation.partialState.ChanType, pendingReservation.partialState.IsInitiator, leaseExpiry, + WithAuxLeaves(localAuxLeaves, remoteAuxLeaves), ) if err != nil { req.err <- err @@ -2118,7 +2189,7 @@ func (l *LightningWallet) verifyCommitSig(res *ChannelReservation, TapscriptRootToOpt, ) - _, fundingOutput, err := input.GenTaprootFundingScript( + _, fundingOutput, _, err := input.GenTaprootFundingScript( localKey, remoteKey, channelValue, fundingOpts..., ) @@ -2129,6 +2200,7 @@ func (l *LightningWallet) verifyCommitSig(res *ChannelReservation, res.musigSessions = genMusigSession( res.ourContribution, res.theirContribution, l.Cfg.Signer, fundingOutput, + res.partialState.TapscriptRoot, ) } @@ -2219,9 +2291,6 @@ func (l *LightningWallet) handleFundingCounterPartySigs(msg *addCounterPartySigs // As we're about to broadcast the funding transaction, we'll take note // of the current height for record keeping purposes. - // - // TODO(roasbeef): this info can also be piped into light client's - // basic fee estimation? _, bestHeight, err := l.Cfg.ChainIO.GetBestBlock() if err != nil { msg.err <- err @@ -2282,6 +2351,23 @@ func (l *LightningWallet) handleSingleFunderSigs(req *addSingleFunderSigsMsg) { defer pendingReservation.Unlock() chanState := pendingReservation.partialState + + // If we have an aux funding desc, then we can use it to populate some + // of the optional, but opaque TLV blobs we'll carry for the channel. + chanState.CustomBlob = fn.MapOption(func(desc AuxFundingDesc) tlv.Blob { + return desc.CustomFundingBlob + })(req.auxFundingDesc) + chanState.LocalCommitment.CustomBlob = fn.MapOption( + func(desc AuxFundingDesc) tlv.Blob { + return desc.CustomLocalCommitBlob + }, + )(req.auxFundingDesc) + chanState.RemoteCommitment.CustomBlob = fn.MapOption( + func(desc AuxFundingDesc) tlv.Blob { + return desc.CustomRemoteCommitBlob + }, + )(req.auxFundingDesc) + chanType := pendingReservation.partialState.ChanType chanState.FundingOutpoint = *req.fundingOutpoint fundingTxIn := wire.NewTxIn(req.fundingOutpoint, nil, nil) @@ -2295,6 +2381,14 @@ func (l *LightningWallet) handleSingleFunderSigs(req *addSingleFunderSigsMsg) { if pendingReservation.partialState.ChanType.HasLeaseExpiration() { leaseExpiry = pendingReservation.partialState.ThawHeight } + + localAuxLeaves := fn.MapOption(func(desc AuxFundingDesc) CommitAuxLeaves { + return desc.LocalInitAuxLeaves + })(req.auxFundingDesc) + remoteAuxLeaves := fn.MapOption(func(desc AuxFundingDesc) CommitAuxLeaves { + return desc.RemoteInitAuxLeaves + })(req.auxFundingDesc) + ourCommitTx, theirCommitTx, err := CreateCommitmentTxns( localBalance, remoteBalance, pendingReservation.ourContribution.ChannelConfig, @@ -2303,6 +2397,7 @@ func (l *LightningWallet) handleSingleFunderSigs(req *addSingleFunderSigsMsg) { pendingReservation.theirContribution.FirstCommitmentPoint, *fundingTxIn, chanType, pendingReservation.partialState.IsInitiator, leaseExpiry, + WithAuxLeaves(localAuxLeaves, remoteAuxLeaves), ) if err != nil { req.err <- err @@ -2365,7 +2460,7 @@ func (l *LightningWallet) handleSingleFunderSigs(req *addSingleFunderSigsMsg) { TapscriptRootToOpt, ) //nolint:lll - fundingWitnessScript, fundingTxOut, err = input.GenTaprootFundingScript( + fundingWitnessScript, fundingTxOut, _, err = input.GenTaprootFundingScript( ourKey.PubKey, theirKey.PubKey, channelValue, fundingOpts..., ) @@ -2509,6 +2604,9 @@ func (l *LightningWallet) ValidateChannel(channelState *channeldb.OpenChannel, l.Cfg.AuxLeafStore.WhenSome(func(s AuxLeafStore) { chanOpts = append(chanOpts, WithLeafStore(s)) }) + l.Cfg.AuxSigner.WhenSome(func(s AuxSigner) { + chanOpts = append(chanOpts, WithAuxSigner(s)) + }) // First, we'll obtain a fully signed commitment transaction so we can // pass into it on the chanvalidate package for verification. @@ -2531,7 +2629,7 @@ func (l *LightningWallet) ValidateChannel(channelState *channeldb.OpenChannel, channelState.TapscriptRoot, TapscriptRootToOpt, ) - fundingScript, _, err = input.GenTaprootFundingScript( + fundingScript, _, _, err = input.GenTaprootFundingScript( localKey, remoteKey, int64(channel.Capacity), fundingOpts..., ) diff --git a/lnwire/extra_bytes.go b/lnwire/extra_bytes.go index 0ebf48f57d0..c8e18c182fb 100644 --- a/lnwire/extra_bytes.go +++ b/lnwire/extra_bytes.go @@ -71,7 +71,7 @@ func (e *ExtraOpaqueData) PackRecords(recordProducers ...tlv.RecordProducer) err return err } - *e = ExtraOpaqueData(extraBytesWriter.Bytes()) + *e = append(extraBytesWriter.Bytes(), *e...) return nil } diff --git a/lnwire/extra_bytes_test.go b/lnwire/extra_bytes_test.go index fd9f28841d4..bc1de8c5768 100644 --- a/lnwire/extra_bytes_test.go +++ b/lnwire/extra_bytes_test.go @@ -151,3 +151,25 @@ func TestExtraOpaqueDataPackUnpackRecords(t *testing.T) { t.Fatalf("type2 not found in typeMap") } } + +// TestPackRecordsPrepend tests that if an ExtraOpaqueData instance already a +// set of opaque bytes, then any records passed in are prepended to the +// existing bytes. +func TestPackRecordsPrepend(t *testing.T) { + t.Parallel() + + chanTypeRecord := tlv.NewPrimitiveRecord[tlv.TlvType1](uint8(2)) + + // Create some opaque data that is already pre-populated with some + // bytes. + existingBytes := bytes.Repeat([]byte{1}, 10) + extraBytes := ExtraOpaqueData(existingBytes) + + // Now we'll attempt to pack the records into the existing bytes. + err := extraBytes.PackRecords(&chanTypeRecord) + require.NoError(t, err) + + // After we've packed the records, the existing bytes should be at the + // very end. + require.True(t, bytes.HasSuffix(extraBytes[:], existingBytes)) +} diff --git a/peer/brontide.go b/peer/brontide.go index b9a9f68ca15..61a12f3404e 100644 --- a/peer/brontide.go +++ b/peer/brontide.go @@ -42,6 +42,7 @@ import ( "github.com/lightningnetwork/lnd/lnwire" "github.com/lightningnetwork/lnd/netann" "github.com/lightningnetwork/lnd/pool" + "github.com/lightningnetwork/lnd/protofsm" "github.com/lightningnetwork/lnd/queue" "github.com/lightningnetwork/lnd/subscribe" "github.com/lightningnetwork/lnd/ticker" @@ -363,6 +364,10 @@ type Config struct { // leaves for certain custom channel types. AuxLeafStore fn.Option[lnwallet.AuxLeafStore] + // AuxSigner is an optional signer that can be used to sign auxiliary + // leaves for certain custom channel types. + AuxSigner fn.Option[lnwallet.AuxSigner] + // PongBuf is a slice we'll reuse instead of allocating memory on the // heap. Since only reads will occur and no writes, there is no need // for any synchronization primitives. As a result, it's safe to share @@ -374,6 +379,11 @@ type Config struct { // invalid. DisallowRouteBlinding bool + // MsgRouter is an optional instance of the main message router that + // the peer will use. If None, then a new default version will be used + // in place. + MsgRouter fn.Option[protofsm.MsgRouter] + // Quit is the server's quit channel. If this is closed, we halt operation. Quit chan struct{} } @@ -493,6 +503,10 @@ type Brontide struct { // potentially holding lots of un-consumed events. channelEventClient *subscribe.Client + // msgRouter is an instance of the MsgRouter which is used to send off + // new wire messages for handing. + msgRouter fn.Option[protofsm.MsgRouter] + startReady chan struct{} quit chan struct{} wg sync.WaitGroup @@ -508,6 +522,14 @@ var _ lnpeer.Peer = (*Brontide)(nil) func NewBrontide(cfg Config) *Brontide { logPrefix := fmt.Sprintf("Peer(%x):", cfg.PubKeyBytes) + // We'll either use the msg router instance passed in, or create a new + // blank instance. + // + // TODO(roasbeef): extend w/ source peer info? + msgRouter := cfg.MsgRouter.Alt( + fn.Some[protofsm.MsgRouter](protofsm.NewMultiMsgRouter()), + ) + p := &Brontide{ cfg: cfg, activeSignal: make(chan struct{}), @@ -530,6 +552,7 @@ func NewBrontide(cfg Config) *Brontide { startReady: make(chan struct{}), quit: make(chan struct{}), log: build.NewPrefixLog(logPrefix, peerLog), + msgRouter: msgRouter, } var ( @@ -704,6 +727,12 @@ func (p *Brontide) Start() error { return err } + // Register the message router now as we may need to register some + // endpoints while loading the channels below. + p.msgRouter.WhenSome(func(router protofsm.MsgRouter) { + router.Start() + }) + msgs, err := p.loadActiveChannels(activeChans) if err != nil { return fmt.Errorf("unable to load channels: %w", err) @@ -877,6 +906,9 @@ func (p *Brontide) loadActiveChannels(chans []*channeldb.OpenChannel) ( p.cfg.AuxLeafStore.WhenSome(func(s lnwallet.AuxLeafStore) { chanOpts = append(chanOpts, lnwallet.WithLeafStore(s)) }) + p.cfg.AuxSigner.WhenSome(func(s lnwallet.AuxSigner) { + chanOpts = append(chanOpts, lnwallet.WithAuxSigner(s)) + }) lnChan, err := lnwallet.NewLightningChannel( p.cfg.Signer, dbChan, p.cfg.SigPool, chanOpts..., ) @@ -1269,6 +1301,10 @@ func (p *Brontide) Disconnect(reason error) { p.log.Errorf("couldn't stop pingManager during disconnect: %v", err) } + + p.msgRouter.WhenSome(func(router protofsm.MsgRouter) { + router.Stop() + }) } // String returns the string representation of this peer. @@ -1708,6 +1744,19 @@ out: } } + // If a message router is active, then we'll try to have it + // handle this message. If it can, then we're able to skip the + // rest of the message handling logic. + ok := fn.MapOptionZ(p.msgRouter, func(r protofsm.MsgRouter) error { + return r.RouteMsg(nextMsg) + }) + + // No error occurred, and the message was handled by the + // router. + if ok == nil { + continue + } + var ( targetChan lnwire.ChannelID isLinkUpdate bool @@ -3988,6 +4037,9 @@ func (p *Brontide) addActiveChannel(c *lnpeer.NewChannel) error { p.cfg.AuxLeafStore.WhenSome(func(s lnwallet.AuxLeafStore) { chanOpts = append(chanOpts, lnwallet.WithLeafStore(s)) }) + p.cfg.AuxSigner.WhenSome(func(s lnwallet.AuxSigner) { + chanOpts = append(chanOpts, lnwallet.WithAuxSigner(s)) + }) // If not already active, we'll add this channel to the set of active // channels, so we can look it up later easily according to its channel diff --git a/protofsm/log.go b/protofsm/log.go new file mode 100644 index 00000000000..368abfab20b --- /dev/null +++ b/protofsm/log.go @@ -0,0 +1,45 @@ +package protofsm + +import ( + "github.com/btcsuite/btclog" + "github.com/lightningnetwork/lnd/build" +) + +// log is a logger that is initialized with no output filters. This +// means the package will not perform any logging by default until the caller +// requests it. +var log btclog.Logger + +// The default amount of logging is none. +func init() { + UseLogger(build.NewSubLogger("PFSM", nil)) +} + +// DisableLog disables all library log output. Logging output is disabled +// by default until UseLogger is called. +func DisableLog() { + UseLogger(btclog.Disabled) +} + +// UseLogger uses a specified Logger to output package logging info. +// This should be used in preference to SetLogWriter if the caller is also +// using btclog. +func UseLogger(logger btclog.Logger) { + log = logger +} + +// logClosure is used to provide a closure over expensive logging operations +// so they aren't performed when the logging level doesn't warrant it. +type logClosure func() string + +// String invokes the underlying function and returns the result. +func (c logClosure) String() string { + return c() +} + +// newLogClosure returns a new closure over a function that returns a string +// which itself provides a Stringer interface so that it can be used with the +// logging system. +func newLogClosure(c func() string) logClosure { + return logClosure(c) +} diff --git a/protofsm/msg_router.go b/protofsm/msg_router.go new file mode 100644 index 00000000000..176cda5b475 --- /dev/null +++ b/protofsm/msg_router.go @@ -0,0 +1,301 @@ +package protofsm + +import ( + "fmt" + "maps" + "sync" + + "github.com/lightningnetwork/lnd/fn" + "github.com/lightningnetwork/lnd/lnwire" +) + +var ( + // ErrDuplicateEndpoint is returned when an endpoint is registered with + // a name that already exists. + ErrDuplicateEndpoint = fmt.Errorf("endpoint already registered") + + // ErrUnableToRouteMsg is returned when a message is unable to be + // routed to any endpoints. + ErrUnableToRouteMsg = fmt.Errorf("unable to route message") +) + +// EndPointName is the name of a given endpoint. This MUST be unique across all +// registered endpoints. +type EndPointName = string + +// MsgEndpoint is an interface that represents a message endpoint, or the +// sub-system that will handle processing an incoming wire message. +type MsgEndpoint interface { + // Name returns the name of this endpoint. This MUST be unique across + // all registered endpoints. + Name() EndPointName + + // CanHandle returns true if the target message can be routed to this + // endpoint. + CanHandle(msg lnwire.Message) bool + + // SendMessage handles the target message, and returns true if the + // message was able to be processed. + SendMessage(msg lnwire.Message) bool +} + +// MsgRouter is an interface that represents a message router, which is generic +// sub-system capable of routing any incoming wire message to a set of +// registered endpoints. +// +// TODO(roasbeef): move to diff sub-system? +type MsgRouter interface { + // RegisterEndpoint registers a new endpoint with the router. If a + // duplicate endpoint exists, an error is returned. + RegisterEndpoint(MsgEndpoint) error + + // UnregisterEndpoint unregisters the target endpoint from the router. + UnregisterEndpoint(EndPointName) error + + // RouteMsg attempts to route the target message to a registered + // endpoint. If ANY endpoint could handle the message, then nil is + // returned. Otherwise, ErrUnableToRouteMsg is returned. + RouteMsg(lnwire.Message) error + + // Start starts the peer message router. + Start() + + // Stop stops the peer message router. + Stop() +} + +// queryMsg is a message sent into the main event loop to query or modify the +// internal state. +type queryMsg[Q any, R any] struct { + query Q + + respChan chan fn.Either[R, error] +} + +// sendQuery sends a query to the main event loop, and returns the response. +func sendQuery[Q any, R any](sendChan chan queryMsg[Q, R], queryArg Q, + quit chan struct{}) fn.Either[R, error] { + + query := queryMsg[Q, R]{ + query: queryArg, + respChan: make(chan fn.Either[R, error], 1), + } + + if !fn.SendOrQuit(sendChan, query, quit) { + return fn.NewRight[R](fmt.Errorf("router shutting down")) + } + + resp, err := fn.RecvResp(query.respChan, nil, quit) + if err != nil { + return fn.NewRight[R](err) + } + + return resp +} + +// sendQueryErr is a helper function based on sendQuery that can be used when +// the query only needs an error response. +func sendQueryErr[Q any](sendChan chan queryMsg[Q, error], queryArg Q, + quitChan chan struct{}) error { + + var err error + resp := sendQuery(sendChan, queryArg, quitChan) + resp.WhenRight(func(e error) { + err = e + }) + resp.WhenLeft(func(e error) { + err = e + }) + + return err +} + +// EndpointsMap is a map of all registered endpoints. +type EndpointsMap map[EndPointName]MsgEndpoint + +// MultiMsgRouter is a type of message router that is capable of routing new +// incoming messages, permitting a message to be routed to multiple registered +// endpoints. +type MultiMsgRouter struct { + startOnce sync.Once + stopOnce sync.Once + + // registerChan is the channel that all new endpoints will be sent to. + registerChan chan queryMsg[MsgEndpoint, error] + + // unregisterChan is the channel that all endpoints that are to be + // removed are sent to. + unregisterChan chan queryMsg[EndPointName, error] + + // msgChan is the channel that all messages will be sent to for + // processing. + msgChan chan queryMsg[lnwire.Message, error] + + // endpointsQueries is a channel that all queries to the endpoints map + // will be sent to. + endpointQueries chan queryMsg[MsgEndpoint, EndpointsMap] + + wg sync.WaitGroup + quit chan struct{} +} + +// NewMultiMsgRouter creates a new instance of a peer message router. +func NewMultiMsgRouter() *MultiMsgRouter { + return &MultiMsgRouter{ + registerChan: make(chan queryMsg[MsgEndpoint, error]), + unregisterChan: make(chan queryMsg[EndPointName, error]), + msgChan: make(chan queryMsg[lnwire.Message, error]), + endpointQueries: make(chan queryMsg[MsgEndpoint, EndpointsMap]), + quit: make(chan struct{}), + } +} + +// Start starts the peer message router. +func (p *MultiMsgRouter) Start() { + log.Infof("Starting MsgRouter") + + p.startOnce.Do(func() { + p.wg.Add(1) + go p.msgRouter() + }) +} + +// Stop stops the peer message router. +func (p *MultiMsgRouter) Stop() { + log.Infof("Stopping MsgRouter") + + p.stopOnce.Do(func() { + close(p.quit) + p.wg.Wait() + }) +} + +// RegisterEndpoint registers a new endpoint with the router. If a duplicate +// endpoint exists, an error is returned. +func (p *MultiMsgRouter) RegisterEndpoint(endpoint MsgEndpoint) error { + return sendQueryErr(p.registerChan, endpoint, p.quit) +} + +// UnregisterEndpoint unregisters the target endpoint from the router. +func (p *MultiMsgRouter) UnregisterEndpoint(name EndPointName) error { + return sendQueryErr(p.unregisterChan, name, p.quit) +} + +// RouteMsg attempts to route the target message to a registered endpoint. If +// ANY endpoint could handle the message, then true is +// returned. +func (p *MultiMsgRouter) RouteMsg(msg lnwire.Message) error { + return sendQueryErr(p.msgChan, msg, p.quit) +} + +// Endpoints returns a list of all registered endpoints. +func (p *MultiMsgRouter) Endpoints() EndpointsMap { + resp := sendQuery(p.endpointQueries, nil, p.quit) + + var endpoints EndpointsMap + resp.WhenLeft(func(e EndpointsMap) { + endpoints = e + }) + + return endpoints +} + +// msgRouter is the main goroutine that handles all incoming messages. +func (p *MultiMsgRouter) msgRouter() { + defer p.wg.Done() + + // endpoints is a map of all registered endpoints. + endpoints := make(map[EndPointName]MsgEndpoint) + + for { + select { + // A new endpoint was just sent in, so we'll add it to our set + // of registered endpoints. + case newEndpointMsg := <-p.registerChan: + endpoint := newEndpointMsg.query + + log.Infof("MsgRouter: registering new "+ + "MsgEndpoint(%s)", endpoint.Name()) + + // If this endpoint already exists, then we'll return + // an error as we require unique names. + if _, ok := endpoints[endpoint.Name()]; ok { + log.Errorf("MsgRouter: rejecting "+ + "duplicate endpoint: %v", + endpoint.Name()) + + newEndpointMsg.respChan <- fn.NewRight[error]( + ErrDuplicateEndpoint, + ) + + continue + } + + endpoints[endpoint.Name()] = endpoint + + // TODO(roasbeef): put in method? + newEndpointMsg.respChan <- fn.NewRight[error, error]( + nil, + ) + + // A request to unregister an endpoint was just sent in, so + // we'll attempt to remove it. + case endpointName := <-p.unregisterChan: + delete(endpoints, endpointName.query) + + log.Infof("MsgRouter: unregistering "+ + "MsgEndpoint(%s)", endpointName.query) + + endpointName.respChan <- fn.NewRight[error, error]( + nil, + ) + + // A new message was just sent in. We'll attempt to route it to + // all the endpoints that can handle it. + case msgQuery := <-p.msgChan: + msg := msgQuery.query + + // Loop through all the endpoints and send the message + // to those that can handle it the message. + var couldSend bool + for _, endpoint := range endpoints { + if endpoint.CanHandle(msg) { + log.Tracef("MsgRouter: sending "+ + "msg %T to endpoint %s", msg, + endpoint.Name()) + + sent := endpoint.SendMessage(msg) + couldSend = couldSend || sent + } + } + + var err error + if !couldSend { + log.Tracef("MsgRouter: unable to route "+ + "msg %T", msg) + + err = ErrUnableToRouteMsg + } + + msgQuery.respChan <- fn.NewRight[error](err) + + // A query for the endpoint state just came in, we'll send back + // a copy of our current state. + case endpointQuery := <-p.endpointQueries: + endpointsCopy := make(EndpointsMap, len(endpoints)) + maps.Copy(endpointsCopy, endpoints) + + //nolint:lll + endpointQuery.respChan <- fn.NewLeft[EndpointsMap, error]( + endpointsCopy, + ) + + case <-p.quit: + return + } + } +} + +// A compile time check to ensure MultiMsgRouter implements the MsgRouter +// interface. +var _ MsgRouter = (*MultiMsgRouter)(nil) diff --git a/protofsm/msg_router_test.go b/protofsm/msg_router_test.go new file mode 100644 index 00000000000..b5e56e1e371 --- /dev/null +++ b/protofsm/msg_router_test.go @@ -0,0 +1,146 @@ +package protofsm + +import ( + "testing" + + "github.com/lightningnetwork/lnd/lnwire" + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/require" +) + +type mockEndpoint struct { + mock.Mock +} + +func (m *mockEndpoint) Name() string { + args := m.Called() + + return args.String(0) +} + +func (m *mockEndpoint) CanHandle(msg lnwire.Message) bool { + args := m.Called(msg) + + return args.Bool(0) +} + +func (m *mockEndpoint) SendMessage(msg lnwire.Message) bool { + args := m.Called(msg) + + return args.Bool(0) +} + +// TestMessageRouterOperation tests the basic operation of the message router: +// add new endpoints, route to them, remove, them, etc. +func TestMessageRouterOperation(t *testing.T) { + msgRouter := NewMultiMsgRouter() + msgRouter.Start() + defer msgRouter.Stop() + + openChanMsg := &lnwire.OpenChannel{} + commitSigMsg := &lnwire.CommitSig{} + + errorMsg := &lnwire.Error{} + + // For this test, we'll have two endpoints, each with distinct names. + // One endpoint will only handle OpenChannel, while the other will + // handle the CommitSig message. + fundingEndpoint := &mockEndpoint{} + fundingEndpointName := "funding" + fundingEndpoint.On("Name").Return(fundingEndpointName) + fundingEndpoint.On("CanHandle", openChanMsg).Return(true) + fundingEndpoint.On("CanHandle", errorMsg).Return(false) + fundingEndpoint.On("CanHandle", commitSigMsg).Return(false) + fundingEndpoint.On("SendMessage", openChanMsg).Return(true) + + commitEndpoint := &mockEndpoint{} + commitEndpointName := "commit" + commitEndpoint.On("Name").Return(commitEndpointName) + commitEndpoint.On("CanHandle", commitSigMsg).Return(true) + commitEndpoint.On("CanHandle", openChanMsg).Return(false) + commitEndpoint.On("CanHandle", errorMsg).Return(false) + commitEndpoint.On("SendMessage", commitSigMsg).Return(true) + + t.Run("add endpoints", func(t *testing.T) { + // First, we'll add the funding endpoint to the router. + require.NoError(t, msgRouter.RegisterEndpoint(fundingEndpoint)) + + // There should be a single endpoint registered. + require.Len(t, msgRouter.Endpoints(), 1) + + // The name of the registered endpoint should be "funding". + require.Equal( + t, "funding", + msgRouter.Endpoints()[fundingEndpointName].Name(), + ) + }) + + t.Run("duplicate endpoint reject", func(t *testing.T) { + // Next, we'll attempt to add the funding endpoint again. This + // should return an ErrDuplicateEndpoint error. + require.ErrorIs( + t, msgRouter.RegisterEndpoint(fundingEndpoint), + ErrDuplicateEndpoint, + ) + }) + + t.Run("route to endpoint", func(t *testing.T) { + // Next, we'll add our other endpoint, then attempt to route a + // message. + require.NoError(t, msgRouter.RegisterEndpoint(commitEndpoint)) + + // If we try to route a message none of the endpoints know of, + // we should get an error. + require.ErrorIs( + t, msgRouter.RouteMsg(errorMsg), ErrUnableToRouteMsg, + ) + + fundingEndpoint.AssertCalled(t, "CanHandle", errorMsg) + commitEndpoint.AssertCalled(t, "CanHandle", errorMsg) + + // Next, we'll route the open channel message. Only the + // fundingEndpoint should be used. + require.NoError(t, msgRouter.RouteMsg(openChanMsg)) + + fundingEndpoint.AssertCalled(t, "CanHandle", openChanMsg) + commitEndpoint.AssertCalled(t, "CanHandle", openChanMsg) + + fundingEndpoint.AssertCalled(t, "SendMessage", openChanMsg) + commitEndpoint.AssertNotCalled(t, "SendMessage", openChanMsg) + + // We'll do the same for the commit sig message. + require.NoError(t, msgRouter.RouteMsg(commitSigMsg)) + + fundingEndpoint.AssertCalled(t, "CanHandle", commitSigMsg) + commitEndpoint.AssertCalled(t, "CanHandle", commitSigMsg) + + commitEndpoint.AssertCalled(t, "SendMessage", commitSigMsg) + fundingEndpoint.AssertNotCalled(t, "SendMessage", commitSigMsg) + }) + + t.Run("remove endpoints", func(t *testing.T) { + // Finally, we'll remove both endpoints. + require.NoError( + t, msgRouter.UnregisterEndpoint(fundingEndpointName), + ) + require.NoError( + t, msgRouter.UnregisterEndpoint(commitEndpointName), + ) + + // There should be no endpoints registered. + require.Len(t, msgRouter.Endpoints(), 0) + + // Trying to route a message should fail. + require.ErrorIs( + t, msgRouter.RouteMsg(openChanMsg), + ErrUnableToRouteMsg, + ) + require.ErrorIs( + t, msgRouter.RouteMsg(commitSigMsg), + ErrUnableToRouteMsg, + ) + }) + + commitEndpoint.AssertExpectations(t) + fundingEndpoint.AssertExpectations(t) +} diff --git a/routing/router.go b/routing/router.go index aa3201f8853..5bc1bca2c56 100644 --- a/routing/router.go +++ b/routing/router.go @@ -1550,7 +1550,7 @@ func makeFundingScript(bitcoinKey1, bitcoinKey2 []byte, return nil, err } - fundingScript, _, err := input.GenTaprootFundingScript( + fundingScript, _, _, err := input.GenTaprootFundingScript( pubKey1, pubKey2, 0, ) if err != nil { diff --git a/rpcserver.go b/rpcserver.go index 7f2ca16da95..83ca271ff80 100644 --- a/rpcserver.go +++ b/rpcserver.go @@ -659,7 +659,9 @@ func newRPCServer(cfg *Config, interceptorChain *rpcperms.InterceptorChain, // be started, and start accepting RPC calls. func (r *rpcServer) addDeps(s *server, macService *macaroons.Service, subServerCgs *subRPCServerConfigs, atpl *autopilot.Manager, - invoiceRegistry *invoices.InvoiceRegistry, tower *watchtower.Standalone, + invoiceRegistry *invoices.InvoiceRegistry, + invoiceSettlementInterceptor *invoices.SettlementInterceptor, + tower *watchtower.Standalone, chanPredicate chanacceptor.MultiplexAcceptor) error { // Set up router rpc backend. @@ -758,12 +760,13 @@ func (r *rpcServer) addDeps(s *server, macService *macaroons.Service, // TODO(roasbeef): extend sub-sever config to have both (local vs remote) DB err = subServerCgs.PopulateDependencies( r.cfg, s.cc, r.cfg.networkDir, macService, atpl, invoiceRegistry, - s.htlcSwitch, r.cfg.ActiveNetParams.Params, s.chanRouter, - routerBackend, s.nodeSigner, s.graphDB, s.chanStateDB, - s.sweeper, tower, s.towerClientMgr, r.cfg.net.ResolveTCPAddr, - genInvoiceFeatures, genAmpInvoiceFeatures, - s.getNodeAnnouncement, s.updateAndBrodcastSelfNode, parseAddr, - rpcsLog, s.aliasMgr.GetPeerAlias, + invoiceSettlementInterceptor, s.htlcSwitch, + r.cfg.ActiveNetParams.Params, s.chanRouter, routerBackend, + s.nodeSigner, s.graphDB, s.chanStateDB, s.sweeper, tower, + s.towerClientMgr, r.cfg.net.ResolveTCPAddr, genInvoiceFeatures, + genAmpInvoiceFeatures, s.getNodeAnnouncement, + s.updateAndBrodcastSelfNode, parseAddr, rpcsLog, + s.aliasMgr.GetPeerAlias, ) if err != nil { return err @@ -2282,6 +2285,7 @@ func (r *rpcServer) parseOpenChannelReq(in *lnrpc.OpenChannelRequest, FundUpToMaxAmt: fundUpToMaxAmt, MinFundAmt: minFundAmt, Memo: []byte(in.Memo), + CustomChannelData: in.CustomChannelData, Outpoints: outpoints, }, nil } @@ -4399,6 +4403,11 @@ func createRPCOpenChannel(r *rpcServer, dbChannel *channeldb.OpenChannel, // is returned and peerScidAlias will be an empty ShortChannelID. peerScidAlias, _ := r.server.aliasMgr.GetPeerAlias(chanID) + var customChannelData []byte + dbChannel.CustomBlob.WhenSome(func(blob tlv.Blob) { + customChannelData = blob + }) + channel := &lnrpc.Channel{ Active: isActive, Private: isPrivate(dbChannel), @@ -4431,6 +4440,7 @@ func createRPCOpenChannel(r *rpcServer, dbChannel *channeldb.OpenChannel, ZeroConf: dbChannel.IsZeroConf(), ZeroConfConfirmedScid: dbChannel.ZeroConfRealScid().ToUint64(), Memo: string(dbChannel.Memo), + CustomChannelData: customChannelData, // TODO: remove the following deprecated fields CsvDelay: uint32(dbChannel.LocalChanCfg.CsvDelay), LocalChanReserveSat: int64(dbChannel.LocalChanCfg.ChanReserve), diff --git a/server.go b/server.go index 650cdbced40..36b59a5af8f 100644 --- a/server.go +++ b/server.go @@ -260,6 +260,8 @@ type server struct { invoices *invoices.InvoiceRegistry + invoiceSettlementInterceptor *invoices.SettlementInterceptor + channelNotifier *channelnotifier.ChannelNotifier peerNotifier *peernotifier.PeerNotifier @@ -556,6 +558,8 @@ func newServer(cfg *Config, listenAddrs []net.Addr, return nil, err } + invoiceSettlementInterceptor := invoices.NewSettlementInterceptor() + registryConfig := invoices.RegistryConfig{ FinalCltvRejectDelta: lncfg.DefaultFinalCltvRejectDelta, HtlcHoldDuration: invoices.DefaultHtlcHoldDuration, @@ -565,6 +569,7 @@ func newServer(cfg *Config, listenAddrs []net.Addr, GcCanceledInvoicesOnStartup: cfg.GcCanceledInvoicesOnStartup, GcCanceledInvoicesOnTheFly: cfg.GcCanceledInvoicesOnTheFly, KeysendHoldTime: cfg.KeysendHoldTime, + SettlementInterceptor: invoiceSettlementInterceptor, } s := &server{ @@ -613,6 +618,8 @@ func newServer(cfg *Config, listenAddrs []net.Addr, peerConnectedListeners: make(map[string][]chan<- lnpeer.Peer), peerDisconnectedListeners: make(map[string][]chan<- struct{}), + invoiceSettlementInterceptor: invoiceSettlementInterceptor, + customMessageServer: subscribe.NewServer(), tlsManager: tlsManager, @@ -1249,6 +1256,7 @@ func newServer(cfg *Config, listenAddrs []net.Addr, return &pc.Incoming }, AuxLeafStore: implCfg.AuxLeafStore, + AuxSigner: implCfg.AuxSigner, }, dbs.ChanStateDB) // Select the configuration and funding parameters for Bitcoin. @@ -1493,8 +1501,9 @@ func newServer(cfg *Config, listenAddrs []net.Addr, EnableUpfrontShutdown: cfg.EnableUpfrontShutdown, MaxAnchorsCommitFeeRate: chainfee.SatPerKVByte( s.cfg.MaxCommitFeeRateAnchors * 1000).FeePerKWeight(), - DeleteAliasEdge: deleteAliasEdge, - AliasManager: s.aliasMgr, + DeleteAliasEdge: deleteAliasEdge, + AliasManager: s.aliasMgr, + AuxFundingController: implCfg.AuxFundingController, }) if err != nil { return nil, err @@ -2004,6 +2013,12 @@ func (s *server) Start() error { } cleanup = cleanup.add(s.interceptableSwitch.Stop) + if err := s.invoiceSettlementInterceptor.Start(); err != nil { + startErr = err + return + } + cleanup = cleanup.add(s.invoiceSettlementInterceptor.Stop) + if err := s.chainArb.Start(); err != nil { startErr = err return @@ -3912,6 +3927,8 @@ func (s *server) peerConnected(conn net.Conn, connReq *connmgr.ConnReq, DisallowRouteBlinding: s.cfg.ProtocolOptions.NoRouteBlinding(), Quit: s.quit, AuxLeafStore: s.implCfg.AuxLeafStore, + AuxSigner: s.implCfg.AuxSigner, + MsgRouter: s.implCfg.MsgRouter, } copy(pCfg.PubKeyBytes[:], peerAddr.IdentityKey.SerializeCompressed()) diff --git a/subrpcserver_config.go b/subrpcserver_config.go index 6687f71a7d4..d1584661068 100644 --- a/subrpcserver_config.go +++ b/subrpcserver_config.go @@ -104,6 +104,7 @@ func (s *subRPCServerConfigs) PopulateDependencies(cfg *Config, networkDir string, macService *macaroons.Service, atpl *autopilot.Manager, invoiceRegistry *invoices.InvoiceRegistry, + invoiceSettlementInterceptor *invoices.SettlementInterceptor, htlcSwitch *htlcswitch.Switch, activeNetParams *chaincfg.Params, chanRouter *routing.ChannelRouter, @@ -238,6 +239,10 @@ func (s *subRPCServerConfigs) PopulateDependencies(cfg *Config, subCfgValue.FieldByName("InvoiceRegistry").Set( reflect.ValueOf(invoiceRegistry), ) + //nolint:lll + subCfgValue.FieldByName("InvoiceSettlementInterceptor").Set( + reflect.ValueOf(invoiceSettlementInterceptor), + ) subCfgValue.FieldByName("IsChannelActive").Set( reflect.ValueOf(htlcSwitch.HasActiveLink), )