mirror of
https://github.com/lightningnetwork/lnd.git
synced 2025-06-11 09:21:38 +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,
|
walletBalanceCommand,
|
||||||
channelBalanceCommand,
|
channelBalanceCommand,
|
||||||
getInfoCommand,
|
getInfoCommand,
|
||||||
|
getDebugInfoCommand,
|
||||||
|
encryptDebugPackageCommand,
|
||||||
|
decryptDebugPackageCommand,
|
||||||
getRecoveryInfoCommand,
|
getRecoveryInfoCommand,
|
||||||
pendingChannelsCommand,
|
pendingChannelsCommand,
|
||||||
sendPaymentCommand,
|
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",
|
return fmt.Errorf("estimatemode must be one of the following: %v",
|
||||||
bitcoindEstimateModes[:])
|
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
|
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.
|
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
|
## RPC Additions
|
||||||
|
|
||||||
* [Deprecated](https://github.com/lightningnetwork/lnd/pull/7175)
|
* [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
|
* Adds a new rpc endpoint gettx to the walletrpc sub-server to [fetch
|
||||||
transaction details](https://github.com/lightningnetwork/lnd/pull/7654).
|
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
|
## lncli Additions
|
||||||
|
|
||||||
# Improvements
|
# Improvements
|
||||||
|
2
go.mod
2
go.mod
@ -3,6 +3,7 @@ module github.com/lightningnetwork/lnd
|
|||||||
require (
|
require (
|
||||||
github.com/NebulousLabs/go-upnp v0.0.0-20180202185039-29b680b06c82
|
github.com/NebulousLabs/go-upnp v0.0.0-20180202185039-29b680b06c82
|
||||||
github.com/Yawning/aez v0.0.0-20211027044916-e49e68abd344
|
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 v0.23.5-0.20230905170901-80f5a0ffdf36
|
||||||
github.com/btcsuite/btcd/btcec/v2 v2.3.2
|
github.com/btcsuite/btcd/btcec/v2 v2.3.2
|
||||||
github.com/btcsuite/btcd/btcutil v1.1.4-0.20230904040416-d4f519f5dc05
|
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/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5 // indirect
|
||||||
github.com/aead/chacha20 v0.0.0-20180709150244-8b13a72661da // indirect
|
github.com/aead/chacha20 v0.0.0-20180709150244-8b13a72661da // indirect
|
||||||
github.com/aead/siphash v1.0.1 // 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/beorn7/perks v1.0.1 // indirect
|
||||||
github.com/btcsuite/btcwallet/wallet/txsizes v1.2.3 // indirect
|
github.com/btcsuite/btcwallet/wallet/txsizes v1.2.3 // indirect
|
||||||
github.com/btcsuite/go-socks v0.0.0-20170105172521-4720035b7bfd // indirect
|
github.com/btcsuite/go-socks v0.0.0-20170105172521-4720035b7bfd // indirect
|
||||||
|
@ -5,8 +5,8 @@ import (
|
|||||||
"crypto/sha256"
|
"crypto/sha256"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"io/ioutil"
|
|
||||||
|
|
||||||
|
"github.com/btcsuite/btcd/btcec/v2"
|
||||||
"github.com/lightningnetwork/lnd/keychain"
|
"github.com/lightningnetwork/lnd/keychain"
|
||||||
"golang.org/x/crypto/chacha20poly1305"
|
"golang.org/x/crypto/chacha20poly1305"
|
||||||
)
|
)
|
||||||
@ -69,6 +69,25 @@ func KeyRingEncrypter(keyRing keychain.KeyRing) (*Encrypter, error) {
|
|||||||
}, nil
|
}, 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
|
// 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
|
// 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
|
// 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
|
// Next, we'll read out the entire blob as we need to isolate the nonce
|
||||||
// from the rest of the ciphertext.
|
// from the rest of the ciphertext.
|
||||||
packedPayload, err := ioutil.ReadAll(payload)
|
packedPayload, err := io.ReadAll(payload)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@ -4,6 +4,7 @@ import (
|
|||||||
"bytes"
|
"bytes"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/btcsuite/btcd/btcec/v2"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -35,7 +36,8 @@ func TestEncryptDecryptPayload(t *testing.T) {
|
|||||||
{
|
{
|
||||||
plaintext: []byte("payload test plain text"),
|
plaintext: []byte("payload test plain text"),
|
||||||
mutator: func(p *[]byte) {
|
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
|
(*p)[0] ^= 1
|
||||||
},
|
},
|
||||||
valid: false,
|
valid: false,
|
||||||
@ -53,54 +55,55 @@ func TestEncryptDecryptPayload(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
keyRing := &MockKeyRing{}
|
keyRing := &MockKeyRing{}
|
||||||
|
keyRingEnc, err := KeyRingEncrypter(keyRing)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
for i, payloadCase := range payloadCases {
|
_, pubKey := btcec.PrivKeyFromBytes([]byte{0x01, 0x02, 0x03, 0x04})
|
||||||
var cipherBuffer bytes.Buffer
|
|
||||||
encrypter, err := KeyRingEncrypter(keyRing)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
// First, we'll encrypt the passed payload with our scheme.
|
privKey, err := btcec.NewPrivateKey()
|
||||||
err = encrypter.EncryptPayloadToWriter(
|
require.NoError(t, err)
|
||||||
payloadCase.plaintext, &cipherBuffer,
|
privKeyEnc, err := ECDHEncrypter(privKey, pubKey)
|
||||||
)
|
require.NoError(t, err)
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("unable encrypt paylaod: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// If we have a mutator, then we'll wrong the mutator over the
|
for _, payloadCase := range payloadCases {
|
||||||
// cipher text, then reset the main buffer and re-write the new
|
payloadCase := payloadCase
|
||||||
// cipher text.
|
for _, enc := range []*Encrypter{keyRingEnc, privKeyEnc} {
|
||||||
if payloadCase.mutator != nil {
|
enc := enc
|
||||||
cipherText := cipherBuffer.Bytes()
|
|
||||||
|
|
||||||
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()
|
// If we have a mutator, then we'll wrong the mutator
|
||||||
cipherBuffer.Write(cipherText)
|
// 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(
|
payloadCase.mutator(&cipherText)
|
||||||
&cipherBuffer,
|
|
||||||
)
|
|
||||||
|
|
||||||
switch {
|
cipherBuffer.Reset()
|
||||||
// If this was meant to be a valid decryption, but we failed,
|
cipherBuffer.Write(cipherText)
|
||||||
// then we'll return an error.
|
}
|
||||||
case err != nil && payloadCase.valid:
|
|
||||||
t.Fatalf("unable to decrypt valid payload case %v", i)
|
|
||||||
|
|
||||||
// If this was meant to be an invalid decryption, and we didn't
|
plaintext, err := enc.DecryptPayloadFromReader(
|
||||||
// fail, then we'll return an error.
|
&cipherBuffer,
|
||||||
case err == nil && !payloadCase.valid:
|
)
|
||||||
t.Fatalf("payload was invalid yet was able to decrypt")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Only if this case was mean to be valid will we ensure the
|
if !payloadCase.valid {
|
||||||
// resulting decrypted plaintext matches the original input.
|
require.Error(t, err)
|
||||||
if payloadCase.valid &&
|
|
||||||
!bytes.Equal(plaintext, payloadCase.plaintext) {
|
continue
|
||||||
t.Fatalf("#%v: expected %v, got %v: ", i,
|
}
|
||||||
payloadCase.plaintext, plaintext)
|
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(
|
||||||
|
t, plaintext, payloadCase.plaintext,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -111,7 +114,5 @@ func TestInvalidKeyGeneration(t *testing.T) {
|
|||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
_, err := KeyRingEncrypter(&MockKeyRing{true})
|
_, err := KeyRingEncrypter(&MockKeyRing{true})
|
||||||
if err == nil {
|
require.Error(t, err)
|
||||||
t.Fatal("expected error due to fail key gen")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
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) {
|
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 protoReq GetRecoveryInfoRequest
|
||||||
var metadata runtime.ServerMetadata
|
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) {
|
mux.Handle("GET", pattern_Lightning_GetRecoveryInfo_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
|
||||||
ctx, cancel := context.WithCancel(req.Context())
|
ctx, cancel := context.WithCancel(req.Context())
|
||||||
defer cancel()
|
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) {
|
mux.Handle("GET", pattern_Lightning_GetRecoveryInfo_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
|
||||||
ctx, cancel := context.WithCancel(req.Context())
|
ctx, cancel := context.WithCancel(req.Context())
|
||||||
defer cancel()
|
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_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_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"}, ""))
|
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_GetInfo_0 = runtime.ForwardResponseMessage
|
||||||
|
|
||||||
|
forward_Lightning_GetDebugInfo_0 = runtime.ForwardResponseMessage
|
||||||
|
|
||||||
forward_Lightning_GetRecoveryInfo_0 = runtime.ForwardResponseMessage
|
forward_Lightning_GetRecoveryInfo_0 = runtime.ForwardResponseMessage
|
||||||
|
|
||||||
forward_Lightning_PendingChannels_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)
|
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,
|
registry["lnrpc.Lightning.GetRecoveryInfo"] = func(ctx context.Context,
|
||||||
conn *grpc.ClientConn, reqJSON string, callback func(string, error)) {
|
conn *grpc.ClientConn, reqJSON string, callback func(string, error)) {
|
||||||
|
|
||||||
|
@ -143,6 +143,13 @@ service Lightning {
|
|||||||
*/
|
*/
|
||||||
rpc GetInfo (GetInfoRequest) returns (GetInfoResponse);
|
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`
|
/** lncli: `getrecoveryinfo`
|
||||||
GetRecoveryInfo returns information concerning the recovery mode including
|
GetRecoveryInfo returns information concerning the recovery mode including
|
||||||
whether it's in a recovery mode, whether the recovery is finished, and the
|
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;
|
bool store_final_htlc_resolutions = 22;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
message GetDebugInfoRequest {
|
||||||
|
}
|
||||||
|
|
||||||
|
message GetDebugInfoResponse {
|
||||||
|
map<string, string> config = 1;
|
||||||
|
repeated string log = 2;
|
||||||
|
}
|
||||||
|
|
||||||
message GetRecoveryInfoRequest {
|
message GetRecoveryInfoRequest {
|
||||||
}
|
}
|
||||||
message GetRecoveryInfoResponse {
|
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": {
|
"/v1/getinfo": {
|
||||||
"get": {
|
"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.",
|
"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": {
|
"lnrpcGetInfoResponse": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
|
@ -44,6 +44,8 @@ http:
|
|||||||
get: "/v1/peers/subscribe"
|
get: "/v1/peers/subscribe"
|
||||||
- selector: lnrpc.Lightning.GetInfo
|
- selector: lnrpc.Lightning.GetInfo
|
||||||
get: "/v1/getinfo"
|
get: "/v1/getinfo"
|
||||||
|
- selector: lnrpc.Lightning.GetDebugInfo
|
||||||
|
get: "/v1/getdebuginfo"
|
||||||
- selector: lnrpc.Lightning.GetRecoveryInfo
|
- selector: lnrpc.Lightning.GetRecoveryInfo
|
||||||
get: "/v1/getrecoveryinfo"
|
get: "/v1/getrecoveryinfo"
|
||||||
- selector: lnrpc.Lightning.PendingChannels
|
- selector: lnrpc.Lightning.PendingChannels
|
||||||
|
@ -102,6 +102,11 @@ type LightningClient interface {
|
|||||||
// it's identity pubkey, alias, the chains it is connected to, and information
|
// it's identity pubkey, alias, the chains it is connected to, and information
|
||||||
// concerning the number of open+pending channels.
|
// concerning the number of open+pending channels.
|
||||||
GetInfo(ctx context.Context, in *GetInfoRequest, opts ...grpc.CallOption) (*GetInfoResponse, error)
|
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`
|
// * lncli: `getrecoveryinfo`
|
||||||
// GetRecoveryInfo returns information concerning the recovery mode including
|
// GetRecoveryInfo returns information concerning the recovery mode including
|
||||||
// whether it's in a recovery mode, whether the recovery is finished, and the
|
// 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
|
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) {
|
func (c *lightningClient) GetRecoveryInfo(ctx context.Context, in *GetRecoveryInfoRequest, opts ...grpc.CallOption) (*GetRecoveryInfoResponse, error) {
|
||||||
out := new(GetRecoveryInfoResponse)
|
out := new(GetRecoveryInfoResponse)
|
||||||
err := c.cc.Invoke(ctx, "/lnrpc.Lightning/GetRecoveryInfo", in, out, opts...)
|
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
|
// it's identity pubkey, alias, the chains it is connected to, and information
|
||||||
// concerning the number of open+pending channels.
|
// concerning the number of open+pending channels.
|
||||||
GetInfo(context.Context, *GetInfoRequest) (*GetInfoResponse, error)
|
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`
|
// * lncli: `getrecoveryinfo`
|
||||||
// GetRecoveryInfo returns information concerning the recovery mode including
|
// GetRecoveryInfo returns information concerning the recovery mode including
|
||||||
// whether it's in a recovery mode, whether the recovery is finished, and the
|
// 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) {
|
func (UnimplementedLightningServer) GetInfo(context.Context, *GetInfoRequest) (*GetInfoResponse, error) {
|
||||||
return nil, status.Errorf(codes.Unimplemented, "method GetInfo not implemented")
|
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) {
|
func (UnimplementedLightningServer) GetRecoveryInfo(context.Context, *GetRecoveryInfoRequest) (*GetRecoveryInfoResponse, error) {
|
||||||
return nil, status.Errorf(codes.Unimplemented, "method GetRecoveryInfo not implemented")
|
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)
|
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) {
|
func _Lightning_GetRecoveryInfo_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||||
in := new(GetRecoveryInfoRequest)
|
in := new(GetRecoveryInfoRequest)
|
||||||
if err := dec(in); err != nil {
|
if err := dec(in); err != nil {
|
||||||
@ -3267,6 +3307,10 @@ var Lightning_ServiceDesc = grpc.ServiceDesc{
|
|||||||
MethodName: "GetInfo",
|
MethodName: "GetInfo",
|
||||||
Handler: _Lightning_GetInfo_Handler,
|
Handler: _Lightning_GetInfo_Handler,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
MethodName: "GetDebugInfo",
|
||||||
|
Handler: _Lightning_GetDebugInfo_Handler,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
MethodName: "GetRecoveryInfo",
|
MethodName: "GetRecoveryInfo",
|
||||||
Handler: _Lightning_GetRecoveryInfo_Handler,
|
Handler: _Lightning_GetRecoveryInfo_Handler,
|
||||||
|
40
rpcserver.go
40
rpcserver.go
@ -10,6 +10,8 @@ import (
|
|||||||
"math"
|
"math"
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
"runtime"
|
"runtime"
|
||||||
"sort"
|
"sort"
|
||||||
"strconv"
|
"strconv"
|
||||||
@ -330,6 +332,19 @@ func MainRPCServerPermissions() map[string][]bakery.Op {
|
|||||||
Entity: "info",
|
Entity: "info",
|
||||||
Action: "read",
|
Action: "read",
|
||||||
}},
|
}},
|
||||||
|
"/lnrpc.Lightning/GetDebugInfo": {{
|
||||||
|
Entity: "info",
|
||||||
|
Action: "read",
|
||||||
|
}, {
|
||||||
|
Entity: "offchain",
|
||||||
|
Action: "read",
|
||||||
|
}, {
|
||||||
|
Entity: "onchain",
|
||||||
|
Action: "read",
|
||||||
|
}, {
|
||||||
|
Entity: "peers",
|
||||||
|
Action: "read",
|
||||||
|
}},
|
||||||
"/lnrpc.Lightning/GetRecoveryInfo": {{
|
"/lnrpc.Lightning/GetRecoveryInfo": {{
|
||||||
Entity: "info",
|
Entity: "info",
|
||||||
Action: "read",
|
Action: "read",
|
||||||
@ -3020,6 +3035,31 @@ func (r *rpcServer) GetInfo(_ context.Context,
|
|||||||
}, nil
|
}, 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
|
// GetRecoveryInfo returns a boolean indicating whether the wallet is started
|
||||||
// in recovery mode, whether the recovery is finished, and the progress made
|
// in recovery mode, whether the recovery is finished, and the progress made
|
||||||
// so far.
|
// so far.
|
||||||
|
Loading…
x
Reference in New Issue
Block a user