Merge pull request #8188 from guggero/debug-rpcs

rpcserver+lncli: add ability to create encrypted debug information package
This commit is contained in:
Oliver Gugger 2024-01-09 16:28:03 +01:00 committed by GitHub
commit 9afe1b72dc
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 4283 additions and 3284 deletions

450
cmd/lncli/cmd_debug.go Normal file
View 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
}

View File

@ -459,6 +459,9 @@ func main() {
walletBalanceCommand,
channelBalanceCommand,
getInfoCommand,
getDebugInfoCommand,
encryptDebugPackageCommand,
decryptDebugPackageCommand,
getRecoveryInfoCommand,
pendingChannelsCommand,
sendPaymentCommand,

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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