mirror of
https://github.com/lightningnetwork/lnd.git
synced 2025-06-03 03:29:50 +02:00
Merge pull request #8188 from guggero/debug-rpcs
rpcserver+lncli: add ability to create encrypted debug information package
This commit is contained in:
commit
9afe1b72dc
450
cmd/lncli/cmd_debug.go
Normal file
450
cmd/lncli/cmd_debug.go
Normal file
@ -0,0 +1,450 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"math"
|
||||
"os"
|
||||
|
||||
"github.com/andybalholm/brotli"
|
||||
"github.com/btcsuite/btcd/btcec/v2"
|
||||
"github.com/lightningnetwork/lnd"
|
||||
"github.com/lightningnetwork/lnd/lnencrypt"
|
||||
"github.com/lightningnetwork/lnd/lnrpc"
|
||||
"github.com/urfave/cli"
|
||||
"google.golang.org/protobuf/proto"
|
||||
)
|
||||
|
||||
var getDebugInfoCommand = cli.Command{
|
||||
Name: "getdebuginfo",
|
||||
Category: "Debug",
|
||||
Usage: "Returns debug information related to the active daemon.",
|
||||
Action: actionDecorator(getDebugInfo),
|
||||
}
|
||||
|
||||
func getDebugInfo(ctx *cli.Context) error {
|
||||
ctxc := getContext()
|
||||
client, cleanUp := getClient(ctx)
|
||||
defer cleanUp()
|
||||
|
||||
req := &lnrpc.GetDebugInfoRequest{}
|
||||
resp, err := client.GetDebugInfo(ctxc, req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
printRespJSON(resp)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type DebugPackage struct {
|
||||
EphemeralPubKey string `json:"ephemeral_public_key"`
|
||||
EncryptedPayload string `json:"encrypted_payload"`
|
||||
}
|
||||
|
||||
var encryptDebugPackageCommand = cli.Command{
|
||||
Name: "encryptdebugpackage",
|
||||
Category: "Debug",
|
||||
Usage: "Collects a package of debug information and encrypts it.",
|
||||
Description: `
|
||||
When requesting support with lnd, it's often required to submit a lot of
|
||||
debug information to the developer in order to track down a problem.
|
||||
This command will collect all the relevant information and encrypt it
|
||||
using the provided public key. The resulting file can then be sent to
|
||||
the developer for further analysis.
|
||||
Because the file is encrypted, it is safe to send it over insecure
|
||||
channels or upload it to a GitHub issue.
|
||||
|
||||
The file by default contains the output of the following commands:
|
||||
- lncli getinfo
|
||||
- lncli getdebuginfo
|
||||
- lncli getnetworkinfo
|
||||
|
||||
By specifying the following flags, additional information can be added
|
||||
to the file (usually this will be requested by the developer depending
|
||||
on the issue at hand):
|
||||
--peers:
|
||||
- lncli listpeers
|
||||
--onchain:
|
||||
- lncli listunspent
|
||||
- lncli listchaintxns
|
||||
--channels:
|
||||
- lncli listchannels
|
||||
- lncli pendingchannels
|
||||
- lncli closedchannels
|
||||
|
||||
Use 'lncli encryptdebugpackage 0xxxxxx... > package.txt' to write the
|
||||
encrypted package to a file called package.txt.
|
||||
`,
|
||||
ArgsUsage: "pubkey [--output_file F]",
|
||||
Flags: []cli.Flag{
|
||||
cli.StringFlag{
|
||||
Name: "pubkey",
|
||||
Usage: "the public key to encrypt the information " +
|
||||
"for (hex-encoded, e.g. 02aabb..), this " +
|
||||
"should be provided to you by the issue " +
|
||||
"tracker or developer you're requesting " +
|
||||
"support from",
|
||||
},
|
||||
cli.StringFlag{
|
||||
Name: "output_file",
|
||||
Usage: "(optional) the file to write the encrypted " +
|
||||
"package to; if not specified, the debug " +
|
||||
"package is printed to stdout",
|
||||
},
|
||||
cli.BoolFlag{
|
||||
Name: "peers",
|
||||
Usage: "include information about connected peers " +
|
||||
"(lncli listpeers)",
|
||||
},
|
||||
cli.BoolFlag{
|
||||
Name: "onchain",
|
||||
Usage: "include information about on-chain " +
|
||||
"transactions (lncli listunspent, " +
|
||||
"lncli listchaintxns)",
|
||||
},
|
||||
cli.BoolFlag{
|
||||
Name: "channels",
|
||||
Usage: "include information about channels " +
|
||||
"(lncli listchannels, lncli pendingchannels, " +
|
||||
"lncli closedchannels)",
|
||||
},
|
||||
},
|
||||
Action: actionDecorator(encryptDebugPackage),
|
||||
}
|
||||
|
||||
func encryptDebugPackage(ctx *cli.Context) error {
|
||||
if ctx.NArg() == 0 && ctx.NumFlags() == 0 {
|
||||
return cli.ShowCommandHelp(ctx, "encryptdebugpackage")
|
||||
}
|
||||
|
||||
var (
|
||||
args = ctx.Args()
|
||||
pubKeyBytes []byte
|
||||
err error
|
||||
)
|
||||
switch {
|
||||
case ctx.IsSet("pubkey"):
|
||||
pubKeyBytes, err = hex.DecodeString(ctx.String("pubkey"))
|
||||
case args.Present():
|
||||
pubKeyBytes, err = hex.DecodeString(args.First())
|
||||
}
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to decode pubkey argument: %w", err)
|
||||
}
|
||||
|
||||
pubKey, err := btcec.ParsePubKey(pubKeyBytes)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to parse pubkey: %w", err)
|
||||
}
|
||||
|
||||
// Collect the information we want to send from the daemon.
|
||||
payload, err := collectDebugPackageInfo(ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to collect debug package "+
|
||||
"information: %w", err)
|
||||
}
|
||||
|
||||
// We've collected the information we want to send, but before
|
||||
// encrypting it, we want to compress it as much as possible to reduce
|
||||
// the size of the final payload.
|
||||
var (
|
||||
compressBuf bytes.Buffer
|
||||
options = brotli.WriterOptions{
|
||||
Quality: brotli.BestCompression,
|
||||
}
|
||||
writer = brotli.NewWriterOptions(&compressBuf, options)
|
||||
)
|
||||
_, err = writer.Write(payload)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to compress payload: %w", err)
|
||||
}
|
||||
if err := writer.Close(); err != nil {
|
||||
return fmt.Errorf("unable to compress payload: %w", err)
|
||||
}
|
||||
|
||||
// Now we have the full payload that we want to encrypt, so we'll create
|
||||
// an ephemeral keypair to encrypt the payload with.
|
||||
localKey, err := btcec.NewPrivateKey()
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to generate local key: %w", err)
|
||||
}
|
||||
|
||||
enc, err := lnencrypt.ECDHEncrypter(localKey, pubKey)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to create encrypter: %w", err)
|
||||
}
|
||||
|
||||
var cipherBuf bytes.Buffer
|
||||
err = enc.EncryptPayloadToWriter(compressBuf.Bytes(), &cipherBuf)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to encrypt payload: %w", err)
|
||||
}
|
||||
|
||||
response := DebugPackage{
|
||||
EphemeralPubKey: hex.EncodeToString(
|
||||
localKey.PubKey().SerializeCompressed(),
|
||||
),
|
||||
EncryptedPayload: hex.EncodeToString(
|
||||
cipherBuf.Bytes(),
|
||||
),
|
||||
}
|
||||
|
||||
// If the user specified an output file, we'll write the encrypted
|
||||
// payload to that file.
|
||||
if ctx.IsSet("output_file") {
|
||||
fileName := lnd.CleanAndExpandPath(ctx.String("output_file"))
|
||||
jsonBytes, err := json.Marshal(response)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to encode JSON: %w", err)
|
||||
}
|
||||
|
||||
return os.WriteFile(fileName, jsonBytes, 0644)
|
||||
}
|
||||
|
||||
// Finally, we'll print out the final payload as a JSON if no output
|
||||
// file was specified.
|
||||
printJSON(response)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// collectDebugPackageInfo collects the information we want to send to the
|
||||
// developer(s) from the daemon.
|
||||
func collectDebugPackageInfo(ctx *cli.Context) ([]byte, error) {
|
||||
ctxc := getContext()
|
||||
client, cleanUp := getClient(ctx)
|
||||
defer cleanUp()
|
||||
|
||||
info, err := client.GetInfo(ctxc, &lnrpc.GetInfoRequest{})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error getting info: %w", err)
|
||||
}
|
||||
|
||||
debugInfo, err := client.GetDebugInfo(
|
||||
ctxc, &lnrpc.GetDebugInfoRequest{},
|
||||
)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error getting debug info: %w", err)
|
||||
}
|
||||
|
||||
networkInfo, err := client.GetNetworkInfo(
|
||||
ctxc, &lnrpc.NetworkInfoRequest{},
|
||||
)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error getting network info: %w", err)
|
||||
}
|
||||
|
||||
var payloadBuf bytes.Buffer
|
||||
addToBuf := func(msgs ...proto.Message) error {
|
||||
for _, msg := range msgs {
|
||||
jsonBytes, err := lnrpc.ProtoJSONMarshalOpts.Marshal(
|
||||
msg,
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error encoding response: %w",
|
||||
err)
|
||||
}
|
||||
|
||||
payloadBuf.Write(jsonBytes)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := addToBuf(info); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := addToBuf(debugInfo); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := addToBuf(info, debugInfo, networkInfo); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Add optional information to the payload.
|
||||
if ctx.Bool("peers") {
|
||||
peers, err := client.ListPeers(ctxc, &lnrpc.ListPeersRequest{
|
||||
LatestError: true,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error getting peers: %w", err)
|
||||
}
|
||||
if err := addToBuf(peers); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
if ctx.Bool("onchain") {
|
||||
unspent, err := client.ListUnspent(
|
||||
ctxc, &lnrpc.ListUnspentRequest{
|
||||
MaxConfs: math.MaxInt32,
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error getting unspent: %w", err)
|
||||
}
|
||||
chainTxns, err := client.GetTransactions(
|
||||
ctxc, &lnrpc.GetTransactionsRequest{},
|
||||
)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error getting chain txns: %w",
|
||||
err)
|
||||
}
|
||||
if err := addToBuf(unspent, chainTxns); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
if ctx.Bool("channels") {
|
||||
channels, err := client.ListChannels(
|
||||
ctxc, &lnrpc.ListChannelsRequest{},
|
||||
)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error getting channels: %w",
|
||||
err)
|
||||
}
|
||||
pendingChannels, err := client.PendingChannels(
|
||||
ctxc, &lnrpc.PendingChannelsRequest{},
|
||||
)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error getting pending "+
|
||||
"channels: %w", err)
|
||||
}
|
||||
closedChannels, err := client.ClosedChannels(
|
||||
ctxc, &lnrpc.ClosedChannelsRequest{},
|
||||
)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error getting closed "+
|
||||
"channels: %w", err)
|
||||
}
|
||||
if err := addToBuf(
|
||||
channels, pendingChannels, closedChannels,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return payloadBuf.Bytes(), nil
|
||||
}
|
||||
|
||||
var decryptDebugPackageCommand = cli.Command{
|
||||
Name: "decryptdebugpackage",
|
||||
Category: "Debug",
|
||||
Usage: "Decrypts a package of debug information.",
|
||||
Description: `
|
||||
Decrypt a debug package that was created with the encryptdebugpackage
|
||||
command. Decryption requires the private key that corresponds to the
|
||||
public key the package was encrypted to.
|
||||
The command expects the encrypted package JSON to be provided on stdin.
|
||||
If decryption is successful, the information will be printed to stdout.
|
||||
|
||||
Use 'lncli decryptdebugpackage 0xxxxxx... < package.txt > decrypted.txt'
|
||||
to read the encrypted package from a file called package.txt and to
|
||||
write the decrypted content to a file called decrypted.txt.
|
||||
`,
|
||||
ArgsUsage: "privkey [--input_file F]",
|
||||
Flags: []cli.Flag{
|
||||
cli.StringFlag{
|
||||
Name: "privkey",
|
||||
Usage: "the hex encoded private key to decrypt the " +
|
||||
"debug package",
|
||||
},
|
||||
cli.StringFlag{
|
||||
Name: "input_file",
|
||||
Usage: "(optional) the file to read the encrypted " +
|
||||
"package from; if not specified, the debug " +
|
||||
"package is read from stdin",
|
||||
},
|
||||
},
|
||||
Action: actionDecorator(decryptDebugPackage),
|
||||
}
|
||||
|
||||
func decryptDebugPackage(ctx *cli.Context) error {
|
||||
if ctx.NArg() == 0 && ctx.NumFlags() == 0 {
|
||||
return cli.ShowCommandHelp(ctx, "decryptdebugpackage")
|
||||
}
|
||||
|
||||
var (
|
||||
args = ctx.Args()
|
||||
privKeyBytes []byte
|
||||
err error
|
||||
)
|
||||
switch {
|
||||
case ctx.IsSet("pubkey"):
|
||||
privKeyBytes, err = hex.DecodeString(ctx.String("pubkey"))
|
||||
case args.Present():
|
||||
privKeyBytes, err = hex.DecodeString(args.First())
|
||||
}
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to decode privkey argument: %w", err)
|
||||
}
|
||||
|
||||
privKey, _ := btcec.PrivKeyFromBytes(privKeyBytes)
|
||||
|
||||
// Read the file from stdin and decode the JSON into a DebugPackage.
|
||||
var pkg DebugPackage
|
||||
if ctx.IsSet("input_file") {
|
||||
fileName := lnd.CleanAndExpandPath(ctx.String("input_file"))
|
||||
jsonBytes, err := os.ReadFile(fileName)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to read file '%s': %w",
|
||||
fileName, err)
|
||||
}
|
||||
|
||||
err = json.Unmarshal(jsonBytes, &pkg)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to decode JSON: %w", err)
|
||||
}
|
||||
} else {
|
||||
err = json.NewDecoder(os.Stdin).Decode(&pkg)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to decode JSON: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Decode the ephemeral public key and encrypted payload.
|
||||
ephemeralPubKeyBytes, err := hex.DecodeString(pkg.EphemeralPubKey)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to decode ephemeral public key: %w",
|
||||
err)
|
||||
}
|
||||
encryptedPayloadBytes, err := hex.DecodeString(pkg.EncryptedPayload)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to decode encrypted payload: %w", err)
|
||||
}
|
||||
|
||||
// Parse the ephemeral public key and create an encrypter.
|
||||
ephemeralPubKey, err := btcec.ParsePubKey(ephemeralPubKeyBytes)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to parse ephemeral public key: %w",
|
||||
err)
|
||||
}
|
||||
enc, err := lnencrypt.ECDHEncrypter(privKey, ephemeralPubKey)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to create encrypter: %w", err)
|
||||
}
|
||||
|
||||
// Decrypt the payload.
|
||||
decryptedPayload, err := enc.DecryptPayloadFromReader(
|
||||
bytes.NewReader(encryptedPayloadBytes),
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to decrypt payload: %w", err)
|
||||
}
|
||||
|
||||
// Decompress the payload.
|
||||
reader := brotli.NewReader(bytes.NewBuffer(decryptedPayload))
|
||||
decompressedPayload, err := io.ReadAll(reader)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to decompress payload: %w", err)
|
||||
}
|
||||
|
||||
fmt.Println(string(decompressedPayload))
|
||||
|
||||
return nil
|
||||
}
|
@ -459,6 +459,9 @@ func main() {
|
||||
walletBalanceCommand,
|
||||
channelBalanceCommand,
|
||||
getInfoCommand,
|
||||
getDebugInfoCommand,
|
||||
encryptDebugPackageCommand,
|
||||
decryptDebugPackageCommand,
|
||||
getRecoveryInfoCommand,
|
||||
pendingChannelsCommand,
|
||||
sendPaymentCommand,
|
||||
|
93
config.go
93
config.go
@ -2111,3 +2111,96 @@ func checkEstimateMode(estimateMode string) error {
|
||||
return fmt.Errorf("estimatemode must be one of the following: %v",
|
||||
bitcoindEstimateModes[:])
|
||||
}
|
||||
|
||||
// configToFlatMap converts the given config struct into a flat map of key/value
|
||||
// pairs using the dot notation we are used to from the config file or command
|
||||
// line flags.
|
||||
func configToFlatMap(cfg Config) (map[string]string, error) {
|
||||
result := make(map[string]string)
|
||||
|
||||
// redact is the helper function that redacts sensitive values like
|
||||
// passwords.
|
||||
redact := func(key, value string) string {
|
||||
sensitiveKeySuffixes := []string{
|
||||
"pass",
|
||||
"password",
|
||||
"dsn",
|
||||
}
|
||||
for _, suffix := range sensitiveKeySuffixes {
|
||||
if strings.HasSuffix(key, suffix) {
|
||||
return "[redacted]"
|
||||
}
|
||||
}
|
||||
|
||||
return value
|
||||
}
|
||||
|
||||
// printConfig is the helper function that goes into nested structs
|
||||
// recursively. Because we call it recursively, we need to declare it
|
||||
// before we define it.
|
||||
var printConfig func(reflect.Value, string)
|
||||
printConfig = func(obj reflect.Value, prefix string) {
|
||||
// Turn struct pointers into the actual struct, so we can
|
||||
// iterate over the fields as we would with a struct value.
|
||||
if obj.Kind() == reflect.Ptr {
|
||||
obj = obj.Elem()
|
||||
}
|
||||
|
||||
// Abort on nil values.
|
||||
if !obj.IsValid() {
|
||||
return
|
||||
}
|
||||
|
||||
// Loop over all fields of the struct and inspect the type.
|
||||
for i := 0; i < obj.NumField(); i++ {
|
||||
field := obj.Field(i)
|
||||
fieldType := obj.Type().Field(i)
|
||||
|
||||
longName := fieldType.Tag.Get("long")
|
||||
namespace := fieldType.Tag.Get("namespace")
|
||||
group := fieldType.Tag.Get("group")
|
||||
switch {
|
||||
// We have a long name defined, this is a config value.
|
||||
case longName != "":
|
||||
key := longName
|
||||
if prefix != "" {
|
||||
key = prefix + "." + key
|
||||
}
|
||||
|
||||
// Add the value directly to the flattened map.
|
||||
result[key] = redact(key, fmt.Sprintf(
|
||||
"%v", field.Interface(),
|
||||
))
|
||||
|
||||
// We have no long name but a namespace, this is a
|
||||
// nested struct.
|
||||
case longName == "" && namespace != "":
|
||||
key := namespace
|
||||
if prefix != "" {
|
||||
key = prefix + "." + key
|
||||
}
|
||||
|
||||
printConfig(field, key)
|
||||
|
||||
// Just a group means this is a dummy struct to house
|
||||
// multiple config values, the group name doesn't go
|
||||
// into the final field name.
|
||||
case longName == "" && group != "":
|
||||
printConfig(field, prefix)
|
||||
|
||||
// Anonymous means embedded struct. We need to recurse
|
||||
// into it but without adding anything to the prefix.
|
||||
case fieldType.Anonymous:
|
||||
printConfig(field, prefix)
|
||||
|
||||
default:
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Turn the whole config struct into a flat map.
|
||||
printConfig(reflect.ValueOf(cfg), "")
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
48
config_test.go
Normal file
48
config_test.go
Normal file
@ -0,0 +1,48 @@
|
||||
package lnd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/lightningnetwork/lnd/chainreg"
|
||||
"github.com/lightningnetwork/lnd/routing"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
var (
|
||||
testPassword = "testpassword"
|
||||
redactedPassword = "[redacted]"
|
||||
)
|
||||
|
||||
// TestConfigToFlatMap tests that the configToFlatMap function works as
|
||||
// expected on the default configuration.
|
||||
func TestConfigToFlatMap(t *testing.T) {
|
||||
cfg := DefaultConfig()
|
||||
cfg.BitcoindMode.RPCPass = testPassword
|
||||
cfg.BtcdMode.RPCPass = testPassword
|
||||
cfg.Tor.Password = testPassword
|
||||
cfg.DB.Etcd.Pass = testPassword
|
||||
cfg.DB.Postgres.Dsn = testPassword
|
||||
|
||||
result, err := configToFlatMap(cfg)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Pick a couple of random values to check.
|
||||
require.Equal(t, DefaultLndDir, result["lnddir"])
|
||||
require.Equal(
|
||||
t, fmt.Sprintf("%v", chainreg.DefaultBitcoinTimeLockDelta),
|
||||
result["bitcoin.timelockdelta"],
|
||||
)
|
||||
require.Equal(
|
||||
t, fmt.Sprintf("%v", routing.DefaultAprioriWeight),
|
||||
result["routerrpc.apriori.weight"],
|
||||
)
|
||||
require.Contains(t, result, "routerrpc.routermacaroonpath")
|
||||
|
||||
// Check that sensitive values are not included.
|
||||
require.Equal(t, redactedPassword, result["bitcoind.rpcpass"])
|
||||
require.Equal(t, redactedPassword, result["btcd.rpcpass"])
|
||||
require.Equal(t, redactedPassword, result["tor.password"])
|
||||
require.Equal(t, redactedPassword, result["db.etcd.pass"])
|
||||
require.Equal(t, redactedPassword, result["db.postgres.dsn"])
|
||||
}
|
@ -89,6 +89,15 @@
|
||||
the new payment status `Payment_INITIATED` should be used for payment-related
|
||||
RPCs. It's recommended to use it to provide granular controls over payments.
|
||||
|
||||
* A [helper command (`lncli encryptdebugpackage`) for collecting and encrypting
|
||||
useful debug information](https://github.com/lightningnetwork/lnd/pull/8188)
|
||||
was added. This allows a user to collect the most relevant information about
|
||||
their node with a single command and securely encrypt it to the public key of
|
||||
a developer or support person. That way the person supporting the user with
|
||||
their issue has an eas way to get all the information they usually require
|
||||
without the user needing to publicly give away a lot of privacy-sensitive
|
||||
data.
|
||||
|
||||
## RPC Additions
|
||||
|
||||
* [Deprecated](https://github.com/lightningnetwork/lnd/pull/7175)
|
||||
@ -100,6 +109,11 @@
|
||||
* Adds a new rpc endpoint gettx to the walletrpc sub-server to [fetch
|
||||
transaction details](https://github.com/lightningnetwork/lnd/pull/7654).
|
||||
|
||||
* [The new `GetDebugInfo` RPC method was added that returns the full runtime
|
||||
configuration of the node as well as the complete log
|
||||
file](https://github.com/lightningnetwork/lnd/pull/8188). The corresponding
|
||||
`lncli getdebuginfo` command was also added.
|
||||
|
||||
## lncli Additions
|
||||
|
||||
# Improvements
|
||||
|
2
go.mod
2
go.mod
@ -3,6 +3,7 @@ module github.com/lightningnetwork/lnd
|
||||
require (
|
||||
github.com/NebulousLabs/go-upnp v0.0.0-20180202185039-29b680b06c82
|
||||
github.com/Yawning/aez v0.0.0-20211027044916-e49e68abd344
|
||||
github.com/andybalholm/brotli v1.0.3
|
||||
github.com/btcsuite/btcd v0.23.5-0.20230905170901-80f5a0ffdf36
|
||||
github.com/btcsuite/btcd/btcec/v2 v2.3.2
|
||||
github.com/btcsuite/btcd/btcutil v1.1.4-0.20230904040416-d4f519f5dc05
|
||||
@ -75,7 +76,6 @@ require (
|
||||
github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5 // indirect
|
||||
github.com/aead/chacha20 v0.0.0-20180709150244-8b13a72661da // indirect
|
||||
github.com/aead/siphash v1.0.1 // indirect
|
||||
github.com/andybalholm/brotli v1.0.3 // indirect
|
||||
github.com/beorn7/perks v1.0.1 // indirect
|
||||
github.com/btcsuite/btcwallet/wallet/txsizes v1.2.3 // indirect
|
||||
github.com/btcsuite/go-socks v0.0.0-20170105172521-4720035b7bfd // indirect
|
||||
|
@ -5,8 +5,8 @@ import (
|
||||
"crypto/sha256"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
|
||||
"github.com/btcsuite/btcd/btcec/v2"
|
||||
"github.com/lightningnetwork/lnd/keychain"
|
||||
"golang.org/x/crypto/chacha20poly1305"
|
||||
)
|
||||
@ -69,6 +69,25 @@ func KeyRingEncrypter(keyRing keychain.KeyRing) (*Encrypter, error) {
|
||||
}, nil
|
||||
}
|
||||
|
||||
// ECDHEncrypter derives an encryption key by performing an ECDH operation on
|
||||
// the passed keys. The resulting key is used to encrypt or decrypt files with
|
||||
// sensitive content.
|
||||
func ECDHEncrypter(localKey *btcec.PrivateKey,
|
||||
remoteKey *btcec.PublicKey) (*Encrypter, error) {
|
||||
|
||||
ecdh := keychain.PrivKeyECDH{
|
||||
PrivKey: localKey,
|
||||
}
|
||||
encryptionKey, err := ecdh.ECDH(remoteKey)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error deriving encryption key: %w", err)
|
||||
}
|
||||
|
||||
return &Encrypter{
|
||||
encryptionKey: encryptionKey[:],
|
||||
}, nil
|
||||
}
|
||||
|
||||
// EncryptPayloadToWriter attempts to write the set of provided bytes into the
|
||||
// passed io.Writer in an encrypted form. We use a 24-byte chachapoly AEAD
|
||||
// instance with a randomized nonce that's pre-pended to the final payload and
|
||||
@ -112,7 +131,7 @@ func (e Encrypter) DecryptPayloadFromReader(payload io.Reader) ([]byte,
|
||||
|
||||
// Next, we'll read out the entire blob as we need to isolate the nonce
|
||||
// from the rest of the ciphertext.
|
||||
packedPayload, err := ioutil.ReadAll(payload)
|
||||
packedPayload, err := io.ReadAll(payload)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -4,6 +4,7 @@ import (
|
||||
"bytes"
|
||||
"testing"
|
||||
|
||||
"github.com/btcsuite/btcd/btcec/v2"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
@ -35,7 +36,8 @@ func TestEncryptDecryptPayload(t *testing.T) {
|
||||
{
|
||||
plaintext: []byte("payload test plain text"),
|
||||
mutator: func(p *[]byte) {
|
||||
// Flip a byte in the payload to render it invalid.
|
||||
// Flip a byte in the payload to render it
|
||||
// invalid.
|
||||
(*p)[0] ^= 1
|
||||
},
|
||||
valid: false,
|
||||
@ -53,54 +55,55 @@ func TestEncryptDecryptPayload(t *testing.T) {
|
||||
}
|
||||
|
||||
keyRing := &MockKeyRing{}
|
||||
keyRingEnc, err := KeyRingEncrypter(keyRing)
|
||||
require.NoError(t, err)
|
||||
|
||||
for i, payloadCase := range payloadCases {
|
||||
var cipherBuffer bytes.Buffer
|
||||
encrypter, err := KeyRingEncrypter(keyRing)
|
||||
require.NoError(t, err)
|
||||
_, pubKey := btcec.PrivKeyFromBytes([]byte{0x01, 0x02, 0x03, 0x04})
|
||||
|
||||
// First, we'll encrypt the passed payload with our scheme.
|
||||
err = encrypter.EncryptPayloadToWriter(
|
||||
payloadCase.plaintext, &cipherBuffer,
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("unable encrypt paylaod: %v", err)
|
||||
}
|
||||
privKey, err := btcec.NewPrivateKey()
|
||||
require.NoError(t, err)
|
||||
privKeyEnc, err := ECDHEncrypter(privKey, pubKey)
|
||||
require.NoError(t, err)
|
||||
|
||||
// If we have a mutator, then we'll wrong the mutator over the
|
||||
// cipher text, then reset the main buffer and re-write the new
|
||||
// cipher text.
|
||||
if payloadCase.mutator != nil {
|
||||
cipherText := cipherBuffer.Bytes()
|
||||
for _, payloadCase := range payloadCases {
|
||||
payloadCase := payloadCase
|
||||
for _, enc := range []*Encrypter{keyRingEnc, privKeyEnc} {
|
||||
enc := enc
|
||||
|
||||
payloadCase.mutator(&cipherText)
|
||||
// First, we'll encrypt the passed payload with our
|
||||
// scheme.
|
||||
var cipherBuffer bytes.Buffer
|
||||
err = enc.EncryptPayloadToWriter(
|
||||
payloadCase.plaintext, &cipherBuffer,
|
||||
)
|
||||
require.NoError(t, err)
|
||||
|
||||
cipherBuffer.Reset()
|
||||
cipherBuffer.Write(cipherText)
|
||||
}
|
||||
// If we have a mutator, then we'll wrong the mutator
|
||||
// over the cipher text, then reset the main buffer and
|
||||
// re-write the new cipher text.
|
||||
if payloadCase.mutator != nil {
|
||||
cipherText := cipherBuffer.Bytes()
|
||||
|
||||
plaintext, err := encrypter.DecryptPayloadFromReader(
|
||||
&cipherBuffer,
|
||||
)
|
||||
payloadCase.mutator(&cipherText)
|
||||
|
||||
switch {
|
||||
// If this was meant to be a valid decryption, but we failed,
|
||||
// then we'll return an error.
|
||||
case err != nil && payloadCase.valid:
|
||||
t.Fatalf("unable to decrypt valid payload case %v", i)
|
||||
cipherBuffer.Reset()
|
||||
cipherBuffer.Write(cipherText)
|
||||
}
|
||||
|
||||
// If this was meant to be an invalid decryption, and we didn't
|
||||
// fail, then we'll return an error.
|
||||
case err == nil && !payloadCase.valid:
|
||||
t.Fatalf("payload was invalid yet was able to decrypt")
|
||||
}
|
||||
plaintext, err := enc.DecryptPayloadFromReader(
|
||||
&cipherBuffer,
|
||||
)
|
||||
|
||||
// Only if this case was mean to be valid will we ensure the
|
||||
// resulting decrypted plaintext matches the original input.
|
||||
if payloadCase.valid &&
|
||||
!bytes.Equal(plaintext, payloadCase.plaintext) {
|
||||
t.Fatalf("#%v: expected %v, got %v: ", i,
|
||||
payloadCase.plaintext, plaintext)
|
||||
if !payloadCase.valid {
|
||||
require.Error(t, err)
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
require.NoError(t, err)
|
||||
require.Equal(
|
||||
t, plaintext, payloadCase.plaintext,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -111,7 +114,5 @@ func TestInvalidKeyGeneration(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
_, err := KeyRingEncrypter(&MockKeyRing{true})
|
||||
if err == nil {
|
||||
t.Fatal("expected error due to fail key gen")
|
||||
}
|
||||
require.Error(t, err)
|
||||
}
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -550,6 +550,24 @@ func local_request_Lightning_GetInfo_0(ctx context.Context, marshaler runtime.Ma
|
||||
|
||||
}
|
||||
|
||||
func request_Lightning_GetDebugInfo_0(ctx context.Context, marshaler runtime.Marshaler, client LightningClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
|
||||
var protoReq GetDebugInfoRequest
|
||||
var metadata runtime.ServerMetadata
|
||||
|
||||
msg, err := client.GetDebugInfo(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))
|
||||
return msg, metadata, err
|
||||
|
||||
}
|
||||
|
||||
func local_request_Lightning_GetDebugInfo_0(ctx context.Context, marshaler runtime.Marshaler, server LightningServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
|
||||
var protoReq GetDebugInfoRequest
|
||||
var metadata runtime.ServerMetadata
|
||||
|
||||
msg, err := server.GetDebugInfo(ctx, &protoReq)
|
||||
return msg, metadata, err
|
||||
|
||||
}
|
||||
|
||||
func request_Lightning_GetRecoveryInfo_0(ctx context.Context, marshaler runtime.Marshaler, client LightningClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
|
||||
var protoReq GetRecoveryInfoRequest
|
||||
var metadata runtime.ServerMetadata
|
||||
@ -2892,6 +2910,29 @@ func RegisterLightningHandlerServer(ctx context.Context, mux *runtime.ServeMux,
|
||||
|
||||
})
|
||||
|
||||
mux.Handle("GET", pattern_Lightning_GetDebugInfo_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
|
||||
ctx, cancel := context.WithCancel(req.Context())
|
||||
defer cancel()
|
||||
var stream runtime.ServerTransportStream
|
||||
ctx = grpc.NewContextWithServerTransportStream(ctx, &stream)
|
||||
inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
|
||||
rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/lnrpc.Lightning/GetDebugInfo", runtime.WithHTTPPathPattern("/v1/getdebuginfo"))
|
||||
if err != nil {
|
||||
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
|
||||
return
|
||||
}
|
||||
resp, md, err := local_request_Lightning_GetDebugInfo_0(rctx, inboundMarshaler, server, req, pathParams)
|
||||
md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer())
|
||||
ctx = runtime.NewServerMetadataContext(ctx, md)
|
||||
if err != nil {
|
||||
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
|
||||
return
|
||||
}
|
||||
|
||||
forward_Lightning_GetDebugInfo_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
|
||||
|
||||
})
|
||||
|
||||
mux.Handle("GET", pattern_Lightning_GetRecoveryInfo_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
|
||||
ctx, cancel := context.WithCancel(req.Context())
|
||||
defer cancel()
|
||||
@ -4266,6 +4307,26 @@ func RegisterLightningHandlerClient(ctx context.Context, mux *runtime.ServeMux,
|
||||
|
||||
})
|
||||
|
||||
mux.Handle("GET", pattern_Lightning_GetDebugInfo_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
|
||||
ctx, cancel := context.WithCancel(req.Context())
|
||||
defer cancel()
|
||||
inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
|
||||
rctx, err := runtime.AnnotateContext(ctx, mux, req, "/lnrpc.Lightning/GetDebugInfo", runtime.WithHTTPPathPattern("/v1/getdebuginfo"))
|
||||
if err != nil {
|
||||
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
|
||||
return
|
||||
}
|
||||
resp, md, err := request_Lightning_GetDebugInfo_0(rctx, inboundMarshaler, client, req, pathParams)
|
||||
ctx = runtime.NewServerMetadataContext(ctx, md)
|
||||
if err != nil {
|
||||
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
|
||||
return
|
||||
}
|
||||
|
||||
forward_Lightning_GetDebugInfo_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
|
||||
|
||||
})
|
||||
|
||||
mux.Handle("GET", pattern_Lightning_GetRecoveryInfo_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
|
||||
ctx, cancel := context.WithCancel(req.Context())
|
||||
defer cancel()
|
||||
@ -5322,6 +5383,8 @@ var (
|
||||
|
||||
pattern_Lightning_GetInfo_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1}, []string{"v1", "getinfo"}, ""))
|
||||
|
||||
pattern_Lightning_GetDebugInfo_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1}, []string{"v1", "getdebuginfo"}, ""))
|
||||
|
||||
pattern_Lightning_GetRecoveryInfo_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1}, []string{"v1", "getrecoveryinfo"}, ""))
|
||||
|
||||
pattern_Lightning_PendingChannels_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2}, []string{"v1", "channels", "pending"}, ""))
|
||||
@ -5458,6 +5521,8 @@ var (
|
||||
|
||||
forward_Lightning_GetInfo_0 = runtime.ForwardResponseMessage
|
||||
|
||||
forward_Lightning_GetDebugInfo_0 = runtime.ForwardResponseMessage
|
||||
|
||||
forward_Lightning_GetRecoveryInfo_0 = runtime.ForwardResponseMessage
|
||||
|
||||
forward_Lightning_PendingChannels_0 = runtime.ForwardResponseMessage
|
||||
|
@ -455,6 +455,31 @@ func RegisterLightningJSONCallbacks(registry map[string]func(ctx context.Context
|
||||
callback(string(respBytes), nil)
|
||||
}
|
||||
|
||||
registry["lnrpc.Lightning.GetDebugInfo"] = func(ctx context.Context,
|
||||
conn *grpc.ClientConn, reqJSON string, callback func(string, error)) {
|
||||
|
||||
req := &GetDebugInfoRequest{}
|
||||
err := marshaler.Unmarshal([]byte(reqJSON), req)
|
||||
if err != nil {
|
||||
callback("", err)
|
||||
return
|
||||
}
|
||||
|
||||
client := NewLightningClient(conn)
|
||||
resp, err := client.GetDebugInfo(ctx, req)
|
||||
if err != nil {
|
||||
callback("", err)
|
||||
return
|
||||
}
|
||||
|
||||
respBytes, err := marshaler.Marshal(resp)
|
||||
if err != nil {
|
||||
callback("", err)
|
||||
return
|
||||
}
|
||||
callback(string(respBytes), nil)
|
||||
}
|
||||
|
||||
registry["lnrpc.Lightning.GetRecoveryInfo"] = func(ctx context.Context,
|
||||
conn *grpc.ClientConn, reqJSON string, callback func(string, error)) {
|
||||
|
||||
|
@ -143,6 +143,13 @@ service Lightning {
|
||||
*/
|
||||
rpc GetInfo (GetInfoRequest) returns (GetInfoResponse);
|
||||
|
||||
/* lncli: 'getdebuginfo'
|
||||
GetDebugInfo returns debug information concerning the state of the daemon
|
||||
and its subsystems. This includes the full configuration and the latest log
|
||||
entries from the log file.
|
||||
*/
|
||||
rpc GetDebugInfo (GetDebugInfoRequest) returns (GetDebugInfoResponse);
|
||||
|
||||
/** lncli: `getrecoveryinfo`
|
||||
GetRecoveryInfo returns information concerning the recovery mode including
|
||||
whether it's in a recovery mode, whether the recovery is finished, and the
|
||||
@ -1955,6 +1962,14 @@ message GetInfoResponse {
|
||||
bool store_final_htlc_resolutions = 22;
|
||||
}
|
||||
|
||||
message GetDebugInfoRequest {
|
||||
}
|
||||
|
||||
message GetDebugInfoResponse {
|
||||
map<string, string> config = 1;
|
||||
repeated string log = 2;
|
||||
}
|
||||
|
||||
message GetRecoveryInfoRequest {
|
||||
}
|
||||
message GetRecoveryInfoResponse {
|
||||
|
@ -1061,6 +1061,29 @@
|
||||
]
|
||||
}
|
||||
},
|
||||
"/v1/getdebuginfo": {
|
||||
"get": {
|
||||
"summary": "lncli: 'getdebuginfo'\nGetDebugInfo returns debug information concerning the state of the daemon\nand its subsystems. This includes the full configuration and the latest log\nentries from the log file.",
|
||||
"operationId": "Lightning_GetDebugInfo",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "A successful response.",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/lnrpcGetDebugInfoResponse"
|
||||
}
|
||||
},
|
||||
"default": {
|
||||
"description": "An unexpected error response.",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/rpcStatus"
|
||||
}
|
||||
}
|
||||
},
|
||||
"tags": [
|
||||
"Lightning"
|
||||
]
|
||||
}
|
||||
},
|
||||
"/v1/getinfo": {
|
||||
"get": {
|
||||
"summary": "lncli: `getinfo`\nGetInfo returns general information concerning the lightning node including\nit's identity pubkey, alias, the chains it is connected to, and information\nconcerning the number of open+pending channels.",
|
||||
@ -4866,6 +4889,23 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"lnrpcGetDebugInfoResponse": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"config": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"log": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"lnrpcGetInfoResponse": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
|
@ -44,6 +44,8 @@ http:
|
||||
get: "/v1/peers/subscribe"
|
||||
- selector: lnrpc.Lightning.GetInfo
|
||||
get: "/v1/getinfo"
|
||||
- selector: lnrpc.Lightning.GetDebugInfo
|
||||
get: "/v1/getdebuginfo"
|
||||
- selector: lnrpc.Lightning.GetRecoveryInfo
|
||||
get: "/v1/getrecoveryinfo"
|
||||
- selector: lnrpc.Lightning.PendingChannels
|
||||
|
@ -102,6 +102,11 @@ type LightningClient interface {
|
||||
// it's identity pubkey, alias, the chains it is connected to, and information
|
||||
// concerning the number of open+pending channels.
|
||||
GetInfo(ctx context.Context, in *GetInfoRequest, opts ...grpc.CallOption) (*GetInfoResponse, error)
|
||||
// lncli: 'getdebuginfo'
|
||||
// GetDebugInfo returns debug information concerning the state of the daemon
|
||||
// and its subsystems. This includes the full configuration and the latest log
|
||||
// entries from the log file.
|
||||
GetDebugInfo(ctx context.Context, in *GetDebugInfoRequest, opts ...grpc.CallOption) (*GetDebugInfoResponse, error)
|
||||
// * lncli: `getrecoveryinfo`
|
||||
// GetRecoveryInfo returns information concerning the recovery mode including
|
||||
// whether it's in a recovery mode, whether the recovery is finished, and the
|
||||
@ -611,6 +616,15 @@ func (c *lightningClient) GetInfo(ctx context.Context, in *GetInfoRequest, opts
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (c *lightningClient) GetDebugInfo(ctx context.Context, in *GetDebugInfoRequest, opts ...grpc.CallOption) (*GetDebugInfoResponse, error) {
|
||||
out := new(GetDebugInfoResponse)
|
||||
err := c.cc.Invoke(ctx, "/lnrpc.Lightning/GetDebugInfo", in, out, opts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (c *lightningClient) GetRecoveryInfo(ctx context.Context, in *GetRecoveryInfoRequest, opts ...grpc.CallOption) (*GetRecoveryInfoResponse, error) {
|
||||
out := new(GetRecoveryInfoResponse)
|
||||
err := c.cc.Invoke(ctx, "/lnrpc.Lightning/GetRecoveryInfo", in, out, opts...)
|
||||
@ -1409,6 +1423,11 @@ type LightningServer interface {
|
||||
// it's identity pubkey, alias, the chains it is connected to, and information
|
||||
// concerning the number of open+pending channels.
|
||||
GetInfo(context.Context, *GetInfoRequest) (*GetInfoResponse, error)
|
||||
// lncli: 'getdebuginfo'
|
||||
// GetDebugInfo returns debug information concerning the state of the daemon
|
||||
// and its subsystems. This includes the full configuration and the latest log
|
||||
// entries from the log file.
|
||||
GetDebugInfo(context.Context, *GetDebugInfoRequest) (*GetDebugInfoResponse, error)
|
||||
// * lncli: `getrecoveryinfo`
|
||||
// GetRecoveryInfo returns information concerning the recovery mode including
|
||||
// whether it's in a recovery mode, whether the recovery is finished, and the
|
||||
@ -1773,6 +1792,9 @@ func (UnimplementedLightningServer) SubscribePeerEvents(*PeerEventSubscription,
|
||||
func (UnimplementedLightningServer) GetInfo(context.Context, *GetInfoRequest) (*GetInfoResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method GetInfo not implemented")
|
||||
}
|
||||
func (UnimplementedLightningServer) GetDebugInfo(context.Context, *GetDebugInfoRequest) (*GetDebugInfoResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method GetDebugInfo not implemented")
|
||||
}
|
||||
func (UnimplementedLightningServer) GetRecoveryInfo(context.Context, *GetRecoveryInfoRequest) (*GetRecoveryInfoResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method GetRecoveryInfo not implemented")
|
||||
}
|
||||
@ -2233,6 +2255,24 @@ func _Lightning_GetInfo_Handler(srv interface{}, ctx context.Context, dec func(i
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
func _Lightning_GetDebugInfo_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||
in := new(GetDebugInfoRequest)
|
||||
if err := dec(in); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if interceptor == nil {
|
||||
return srv.(LightningServer).GetDebugInfo(ctx, in)
|
||||
}
|
||||
info := &grpc.UnaryServerInfo{
|
||||
Server: srv,
|
||||
FullMethod: "/lnrpc.Lightning/GetDebugInfo",
|
||||
}
|
||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.(LightningServer).GetDebugInfo(ctx, req.(*GetDebugInfoRequest))
|
||||
}
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
func _Lightning_GetRecoveryInfo_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||
in := new(GetRecoveryInfoRequest)
|
||||
if err := dec(in); err != nil {
|
||||
@ -3267,6 +3307,10 @@ var Lightning_ServiceDesc = grpc.ServiceDesc{
|
||||
MethodName: "GetInfo",
|
||||
Handler: _Lightning_GetInfo_Handler,
|
||||
},
|
||||
{
|
||||
MethodName: "GetDebugInfo",
|
||||
Handler: _Lightning_GetDebugInfo_Handler,
|
||||
},
|
||||
{
|
||||
MethodName: "GetRecoveryInfo",
|
||||
Handler: _Lightning_GetRecoveryInfo_Handler,
|
||||
|
40
rpcserver.go
40
rpcserver.go
@ -10,6 +10,8 @@ import (
|
||||
"math"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"sort"
|
||||
"strconv"
|
||||
@ -330,6 +332,19 @@ func MainRPCServerPermissions() map[string][]bakery.Op {
|
||||
Entity: "info",
|
||||
Action: "read",
|
||||
}},
|
||||
"/lnrpc.Lightning/GetDebugInfo": {{
|
||||
Entity: "info",
|
||||
Action: "read",
|
||||
}, {
|
||||
Entity: "offchain",
|
||||
Action: "read",
|
||||
}, {
|
||||
Entity: "onchain",
|
||||
Action: "read",
|
||||
}, {
|
||||
Entity: "peers",
|
||||
Action: "read",
|
||||
}},
|
||||
"/lnrpc.Lightning/GetRecoveryInfo": {{
|
||||
Entity: "info",
|
||||
Action: "read",
|
||||
@ -3020,6 +3035,31 @@ func (r *rpcServer) GetInfo(_ context.Context,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// GetDebugInfo returns debug information concerning the state of the daemon
|
||||
// and its subsystems. This includes the full configuration and the latest log
|
||||
// entries from the log file.
|
||||
func (r *rpcServer) GetDebugInfo(_ context.Context,
|
||||
_ *lnrpc.GetDebugInfoRequest) (*lnrpc.GetDebugInfoResponse, error) {
|
||||
|
||||
flatConfig, err := configToFlatMap(*r.cfg)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error converting config to flat map: "+
|
||||
"%w", err)
|
||||
}
|
||||
|
||||
logFileName := filepath.Join(r.cfg.LogDir, defaultLogFilename)
|
||||
logContent, err := os.ReadFile(logFileName)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error reading log file '%s': %w",
|
||||
logFileName, err)
|
||||
}
|
||||
|
||||
return &lnrpc.GetDebugInfoResponse{
|
||||
Config: flatConfig,
|
||||
Log: strings.Split(string(logContent), "\n"),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// GetRecoveryInfo returns a boolean indicating whether the wallet is started
|
||||
// in recovery mode, whether the recovery is finished, and the progress made
|
||||
// so far.
|
||||
|
Loading…
x
Reference in New Issue
Block a user