mirror of
https://github.com/lightningnetwork/lnd.git
synced 2025-03-26 01:33:02 +01:00
multi: add a rpc endpoint to track the recovery process
This commit is contained in:
parent
c1ef5bb908
commit
987edc9d81
@ -1729,6 +1729,27 @@ func getInfo(ctx *cli.Context) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
var getRecoveryInfoCommand = cli.Command{
|
||||
Name: "getrecoveryinfo",
|
||||
Usage: "Display information about an ongoing recovery attempt.",
|
||||
Action: actionDecorator(getRecoveryInfo),
|
||||
}
|
||||
|
||||
func getRecoveryInfo(ctx *cli.Context) error {
|
||||
ctxb := context.Background()
|
||||
client, cleanUp := getClient(ctx)
|
||||
defer cleanUp()
|
||||
|
||||
req := &lnrpc.GetRecoveryInfoRequest{}
|
||||
resp, err := client.GetRecoveryInfo(ctxb, req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
printRespJSON(resp)
|
||||
return nil
|
||||
}
|
||||
|
||||
var pendingChannelsCommand = cli.Command{
|
||||
Name: "pendingchannels",
|
||||
Category: "Channels",
|
||||
|
@ -271,6 +271,7 @@ func main() {
|
||||
walletBalanceCommand,
|
||||
channelBalanceCommand,
|
||||
getInfoCommand,
|
||||
getRecoveryInfoCommand,
|
||||
pendingChannelsCommand,
|
||||
sendPaymentCommand,
|
||||
payInvoiceCommand,
|
||||
|
@ -164,7 +164,15 @@ That final line indicates the rescan is complete! If not all funds have
|
||||
appeared, then the user may need to _repeat_ the process with a higher recovery
|
||||
window. Depending on how old the wallet is (the cipher seed stores the wallet's
|
||||
birthday!) and how many addresses were used, the rescan may take anywhere from
|
||||
a few minutes to a few hours.
|
||||
a few minutes to a few hours. To track the recovery progress, one can use the
|
||||
command `lncli getrecoveryinfo`. When finished, the following is returned,
|
||||
```
|
||||
{
|
||||
"recovery_mode": true,
|
||||
"recovery_finished": true,
|
||||
"progress": 1
|
||||
}
|
||||
```
|
||||
|
||||
If the rescan wasn't able to complete fully (`lnd` was shutdown for example),
|
||||
then from `lncli unlock`, it's possible to _restart_ the rescan from where it
|
||||
|
@ -55,6 +55,8 @@ description):
|
||||
* Lists all available connected peers.
|
||||
* GetInfo
|
||||
* Returns basic data concerning the daemon.
|
||||
* GetRecoveryInfo
|
||||
* Returns information about recovery process.
|
||||
* PendingChannels
|
||||
* List the number of pending (not fully confirmed) channels.
|
||||
* ListChannels
|
||||
|
@ -41,6 +41,8 @@ http:
|
||||
get: "/v1/peers/subscribe"
|
||||
- selector: lnrpc.Lightning.GetInfo
|
||||
get: "/v1/getinfo"
|
||||
- selector: lnrpc.Lightning.GetRecoveryInfo
|
||||
get: "/v1/getrecoveryinfo"
|
||||
- selector: lnrpc.Lightning.PendingChannels
|
||||
get: "/v1/channels/pending"
|
||||
- selector: lnrpc.Lightning.ListChannels
|
||||
|
@ -1876,8 +1876,8 @@ func (m *ForwardHtlcInterceptRequest) GetExpiry() uint32 {
|
||||
}
|
||||
|
||||
//*
|
||||
//ForwardHtlcInterceptResponse enables the caller to resolve a previously hold forward.
|
||||
//The caller can choose either to:
|
||||
//ForwardHtlcInterceptResponse enables the caller to resolve a previously hold
|
||||
//forward. The caller can choose either to:
|
||||
//- `Resume`: Execute the default behavior (usually forward).
|
||||
//- `Reject`: Fail the htlc backwards.
|
||||
//- `Settle`: Settle this htlc with a given preimage.
|
||||
|
@ -98,7 +98,7 @@ service Router {
|
||||
rpc TrackPayment (TrackPaymentRequest) returns (stream PaymentStatus) {
|
||||
option deprecated = true;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
HtlcInterceptor dispatches a bi-directional streaming RPC in which
|
||||
Forwarded HTLC requests are sent to the client and the client responds with
|
||||
@ -106,7 +106,8 @@ service Router {
|
||||
In case of interception, the htlc can be either settled, cancelled or
|
||||
resumed later by using the ResolveHoldForward endpoint.
|
||||
*/
|
||||
rpc HtlcInterceptor (stream ForwardHtlcInterceptResponse) returns (stream ForwardHtlcInterceptRequest);
|
||||
rpc HtlcInterceptor (stream ForwardHtlcInterceptResponse)
|
||||
returns (stream ForwardHtlcInterceptRequest);
|
||||
}
|
||||
|
||||
message SendPaymentRequest {
|
||||
@ -589,7 +590,7 @@ message PaymentStatus {
|
||||
repeated lnrpc.HTLCAttempt htlcs = 4;
|
||||
}
|
||||
|
||||
message CircuitKey {
|
||||
message CircuitKey {
|
||||
/// The id of the channel that the is part of this circuit.
|
||||
uint64 chan_id = 1;
|
||||
|
||||
@ -618,14 +619,14 @@ message ForwardHtlcInterceptRequest {
|
||||
}
|
||||
|
||||
/**
|
||||
ForwardHtlcInterceptResponse enables the caller to resolve a previously hold forward.
|
||||
The caller can choose either to:
|
||||
ForwardHtlcInterceptResponse enables the caller to resolve a previously hold
|
||||
forward. The caller can choose either to:
|
||||
- `Resume`: Execute the default behavior (usually forward).
|
||||
- `Reject`: Fail the htlc backwards.
|
||||
- `Settle`: Settle this htlc with a given preimage.
|
||||
*/
|
||||
message ForwardHtlcInterceptResponse {
|
||||
/**
|
||||
/**
|
||||
The key of this forwarded htlc. It defines the incoming channel id and
|
||||
the index in this channel.
|
||||
*/
|
||||
@ -638,9 +639,8 @@ message ForwardHtlcInterceptResponse {
|
||||
bytes preimage = 3;
|
||||
}
|
||||
|
||||
enum ResolveHoldForwardAction {
|
||||
enum ResolveHoldForwardAction {
|
||||
SETTLE = 0;
|
||||
FAIL = 1;
|
||||
RESUME = 2;
|
||||
RESUME = 2;
|
||||
}
|
||||
|
||||
|
1341
lnrpc/rpc.pb.go
1341
lnrpc/rpc.pb.go
File diff suppressed because it is too large
Load Diff
@ -519,6 +519,24 @@ func local_request_Lightning_GetInfo_0(ctx context.Context, marshaler runtime.Ma
|
||||
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
msg, err := client.GetRecoveryInfo(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))
|
||||
return msg, metadata, err
|
||||
|
||||
}
|
||||
|
||||
func local_request_Lightning_GetRecoveryInfo_0(ctx context.Context, marshaler runtime.Marshaler, server LightningServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
|
||||
var protoReq GetRecoveryInfoRequest
|
||||
var metadata runtime.ServerMetadata
|
||||
|
||||
msg, err := server.GetRecoveryInfo(ctx, &protoReq)
|
||||
return msg, metadata, err
|
||||
|
||||
}
|
||||
|
||||
func request_Lightning_PendingChannels_0(ctx context.Context, marshaler runtime.Marshaler, client LightningClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
|
||||
var protoReq PendingChannelsRequest
|
||||
var metadata runtime.ServerMetadata
|
||||
@ -2194,6 +2212,26 @@ func RegisterLightningHandlerServer(ctx context.Context, mux *runtime.ServeMux,
|
||||
|
||||
})
|
||||
|
||||
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()
|
||||
inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
|
||||
rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req)
|
||||
if err != nil {
|
||||
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
|
||||
return
|
||||
}
|
||||
resp, md, err := local_request_Lightning_GetRecoveryInfo_0(rctx, inboundMarshaler, server, req, pathParams)
|
||||
ctx = runtime.NewServerMetadataContext(ctx, md)
|
||||
if err != nil {
|
||||
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
|
||||
return
|
||||
}
|
||||
|
||||
forward_Lightning_GetRecoveryInfo_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
|
||||
|
||||
})
|
||||
|
||||
mux.Handle("GET", pattern_Lightning_PendingChannels_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
|
||||
ctx, cancel := context.WithCancel(req.Context())
|
||||
defer cancel()
|
||||
@ -3197,6 +3235,26 @@ func RegisterLightningHandlerClient(ctx context.Context, mux *runtime.ServeMux,
|
||||
|
||||
})
|
||||
|
||||
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()
|
||||
inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
|
||||
rctx, err := runtime.AnnotateContext(ctx, mux, req)
|
||||
if err != nil {
|
||||
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
|
||||
return
|
||||
}
|
||||
resp, md, err := request_Lightning_GetRecoveryInfo_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_GetRecoveryInfo_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
|
||||
|
||||
})
|
||||
|
||||
mux.Handle("GET", pattern_Lightning_PendingChannels_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
|
||||
ctx, cancel := context.WithCancel(req.Context())
|
||||
defer cancel()
|
||||
@ -3953,6 +4011,8 @@ var (
|
||||
|
||||
pattern_Lightning_GetInfo_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1}, []string{"v1", "getinfo"}, "", runtime.AssumeColonVerbOpt(true)))
|
||||
|
||||
pattern_Lightning_GetRecoveryInfo_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1}, []string{"v1", "getrecoveryinfo"}, "", runtime.AssumeColonVerbOpt(true)))
|
||||
|
||||
pattern_Lightning_PendingChannels_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2}, []string{"v1", "channels", "pending"}, "", runtime.AssumeColonVerbOpt(true)))
|
||||
|
||||
pattern_Lightning_ListChannels_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1}, []string{"v1", "channels"}, "", runtime.AssumeColonVerbOpt(true)))
|
||||
@ -4059,6 +4119,8 @@ var (
|
||||
|
||||
forward_Lightning_GetInfo_0 = runtime.ForwardResponseMessage
|
||||
|
||||
forward_Lightning_GetRecoveryInfo_0 = runtime.ForwardResponseMessage
|
||||
|
||||
forward_Lightning_PendingChannels_0 = runtime.ForwardResponseMessage
|
||||
|
||||
forward_Lightning_ListChannels_0 = runtime.ForwardResponseMessage
|
||||
|
@ -140,6 +140,14 @@ service Lightning {
|
||||
*/
|
||||
rpc GetInfo (GetInfoRequest) returns (GetInfoResponse);
|
||||
|
||||
/** lncli: `getrecoveryinfo`
|
||||
GetRecoveryInfo returns information concerning the recovery mode including
|
||||
whether it's in a recovery mode, whether the recovery is finished, and the
|
||||
progress made so far.
|
||||
*/
|
||||
rpc GetRecoveryInfo (GetRecoveryInfoRequest)
|
||||
returns (GetRecoveryInfoResponse);
|
||||
|
||||
// TODO(roasbeef): merge with below with bool?
|
||||
/* lncli: `pendingchannels`
|
||||
PendingChannels returns a list of all the channels that are currently
|
||||
@ -1381,6 +1389,19 @@ message GetInfoResponse {
|
||||
map<uint32, Feature> features = 19;
|
||||
}
|
||||
|
||||
message GetRecoveryInfoRequest {
|
||||
}
|
||||
message GetRecoveryInfoResponse {
|
||||
// Whether the wallet is in recovery mode
|
||||
bool recovery_mode = 1;
|
||||
|
||||
// Whether the wallet recovery progress is finished
|
||||
bool recovery_finished = 2;
|
||||
|
||||
// The recovery progress, ranging from 0 to 1.
|
||||
double progress = 3;
|
||||
}
|
||||
|
||||
message Chain {
|
||||
// The blockchain the node is on (eg bitcoin, litecoin)
|
||||
string chain = 1;
|
||||
|
@ -829,6 +829,29 @@
|
||||
]
|
||||
}
|
||||
},
|
||||
"/v1/getrecoveryinfo": {
|
||||
"get": {
|
||||
"summary": "* lncli: `getrecoveryinfo`\nGetRecoveryInfo returns information concerning the recovery mode including\nwhether it's in a recovery mode, whether the recovery is finished, and the\nprogress made so far.",
|
||||
"operationId": "GetRecoveryInfo",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "A successful response.",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/lnrpcGetRecoveryInfoResponse"
|
||||
}
|
||||
},
|
||||
"default": {
|
||||
"description": "An unexpected error response",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/runtimeError"
|
||||
}
|
||||
}
|
||||
},
|
||||
"tags": [
|
||||
"Lightning"
|
||||
]
|
||||
}
|
||||
},
|
||||
"/v1/graph": {
|
||||
"get": {
|
||||
"summary": "lncli: `describegraph`\nDescribeGraph returns a description of the latest graph state from the\npoint of view of the node. The graph information is partitioned into two\ncomponents: all the nodes/vertexes, and all the edges that connect the\nvertexes themselves. As this is a directed graph, the edges also contain\nthe node directional specific routing policy which includes: the time lock\ndelta, fee information, etc.",
|
||||
@ -3530,6 +3553,26 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"lnrpcGetRecoveryInfoResponse": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"recovery_mode": {
|
||||
"type": "boolean",
|
||||
"format": "boolean",
|
||||
"title": "Whether the wallet is in recovery mode"
|
||||
},
|
||||
"recovery_finished": {
|
||||
"type": "boolean",
|
||||
"format": "boolean",
|
||||
"title": "Whether the wallet recovery progress is finished"
|
||||
},
|
||||
"progress": {
|
||||
"type": "number",
|
||||
"format": "double",
|
||||
"description": "The recovery progress, ranging from 0 to 1."
|
||||
}
|
||||
}
|
||||
},
|
||||
"lnrpcGraphTopologyUpdate": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
|
@ -817,6 +817,105 @@ const (
|
||||
AddrTypeNestedPubkeyHash = lnrpc.AddressType_NESTED_PUBKEY_HASH
|
||||
)
|
||||
|
||||
// testGetRecoveryInfo checks whether lnd gives the right information about
|
||||
// the wallet recovery process.
|
||||
func testGetRecoveryInfo(net *lntest.NetworkHarness, t *harnessTest) {
|
||||
ctxb := context.Background()
|
||||
|
||||
// First, create a new node with strong passphrase and grab the mnemonic
|
||||
// used for key derivation. This will bring up Carol with an empty
|
||||
// wallet, and such that she is synced up.
|
||||
password := []byte("The Magic Words are Squeamish Ossifrage")
|
||||
carol, mnemonic, err := net.NewNodeWithSeed("Carol", nil, password)
|
||||
if err != nil {
|
||||
t.Fatalf("unable to create node with seed; %v", err)
|
||||
}
|
||||
|
||||
shutdownAndAssert(net, t, carol)
|
||||
|
||||
checkInfo := func(expectedRecoveryMode, expectedRecoveryFinished bool,
|
||||
expectedProgress float64, recoveryWindow int32) {
|
||||
|
||||
// Restore Carol, passing in the password, mnemonic, and
|
||||
// desired recovery window.
|
||||
node, err := net.RestoreNodeWithSeed(
|
||||
"Carol", nil, password, mnemonic, recoveryWindow, nil,
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("unable to restore node: %v", err)
|
||||
}
|
||||
|
||||
// Wait for Carol to sync to the chain.
|
||||
_, minerHeight, err := net.Miner.Node.GetBestBlock()
|
||||
if err != nil {
|
||||
t.Fatalf("unable to get current blockheight %v", err)
|
||||
}
|
||||
ctxt, _ := context.WithTimeout(ctxb, defaultTimeout)
|
||||
err = waitForNodeBlockHeight(ctxt, node, minerHeight)
|
||||
if err != nil {
|
||||
t.Fatalf("unable to sync to chain: %v", err)
|
||||
}
|
||||
|
||||
// Query carol for her current wallet recovery progress.
|
||||
var (
|
||||
recoveryMode bool
|
||||
recoveryFinished bool
|
||||
progress float64
|
||||
)
|
||||
|
||||
err = wait.Predicate(func() bool {
|
||||
// Verify that recovery info gives the right response.
|
||||
req := &lnrpc.GetRecoveryInfoRequest{}
|
||||
ctxt, _ := context.WithTimeout(ctxb, defaultTimeout)
|
||||
resp, err := node.GetRecoveryInfo(ctxt, req)
|
||||
if err != nil {
|
||||
t.Fatalf("unable to query recovery info: %v", err)
|
||||
}
|
||||
|
||||
recoveryMode = resp.RecoveryMode
|
||||
recoveryFinished = resp.RecoveryFinished
|
||||
progress = resp.Progress
|
||||
|
||||
if recoveryMode != expectedRecoveryMode ||
|
||||
recoveryFinished != expectedRecoveryFinished ||
|
||||
progress != expectedProgress {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}, 15*time.Second)
|
||||
if err != nil {
|
||||
t.Fatalf("expected recovery mode to be %v, got %v, "+
|
||||
"expected recovery finished to be %v, got %v, "+
|
||||
"expected progress %v, got %v",
|
||||
expectedRecoveryMode, recoveryMode,
|
||||
expectedRecoveryFinished, recoveryFinished,
|
||||
expectedProgress, progress,
|
||||
)
|
||||
}
|
||||
|
||||
// Lastly, shutdown this Carol so we can move on to the next
|
||||
// restoration.
|
||||
shutdownAndAssert(net, t, node)
|
||||
}
|
||||
|
||||
// Restore Carol with a recovery window of 0. Since it's not in recovery
|
||||
// mode, the recovery info will give a response with recoveryMode=false,
|
||||
// recoveryFinished=false, and progress=0
|
||||
checkInfo(false, false, 0, 0)
|
||||
|
||||
// Change the recovery windown to be 1 to turn on recovery mode. Since the
|
||||
// current chain height is the same as the birthday height, it should
|
||||
// indicate the recovery process is finished.
|
||||
checkInfo(true, true, 1, 1)
|
||||
|
||||
// We now go ahead 5 blocks. Because the wallet's syncing process is
|
||||
// controlled by a goroutine in the background, it will catch up quickly.
|
||||
// This makes the recovery progress back to 1.
|
||||
mineBlocks(t, net, 5, 0)
|
||||
checkInfo(true, true, 1, 1)
|
||||
}
|
||||
|
||||
// testOnchainFundRecovery checks lnd's ability to rescan for onchain outputs
|
||||
// when providing a valid aezeed that owns outputs on the chain. This test
|
||||
// performs multiple restorations using the same seed and various recovery
|
||||
@ -14430,6 +14529,10 @@ var testsCases = []*testCase{
|
||||
name: "sweep coins",
|
||||
test: testSweepAllCoins,
|
||||
},
|
||||
{
|
||||
name: "recovery info",
|
||||
test: testGetRecoveryInfo,
|
||||
},
|
||||
{
|
||||
name: "onchain fund recovery",
|
||||
test: testOnchainFundRecovery,
|
||||
|
@ -844,3 +844,83 @@ func (b *BtcWallet) IsSynced() (bool, int64, error) {
|
||||
|
||||
return true, bestTimestamp, nil
|
||||
}
|
||||
|
||||
// GetRecoveryInfo returns a boolean indicating whether the wallet is started
|
||||
// in recovery mode. It also returns a float64, ranging from 0 to 1,
|
||||
// representing the recovery progress made so far.
|
||||
//
|
||||
// This is a part of the WalletController interface.
|
||||
func (b *BtcWallet) GetRecoveryInfo() (bool, float64, error) {
|
||||
isRecoveryMode := true
|
||||
progress := float64(0)
|
||||
|
||||
// A zero value in RecoveryWindow indicates there is no trigger of
|
||||
// recovery mode.
|
||||
if b.cfg.RecoveryWindow == 0 {
|
||||
isRecoveryMode = false
|
||||
return isRecoveryMode, progress, nil
|
||||
}
|
||||
|
||||
// Query the wallet's birthday block height from db.
|
||||
var birthdayBlock waddrmgr.BlockStamp
|
||||
err := walletdb.View(b.db, func(tx walletdb.ReadTx) error {
|
||||
var err error
|
||||
addrmgrNs := tx.ReadBucket(waddrmgrNamespaceKey)
|
||||
birthdayBlock, _, err = b.wallet.Manager.BirthdayBlock(addrmgrNs)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
// The wallet won't start until the backend is synced, thus the birthday
|
||||
// block won't be set and this particular error will be returned. We'll
|
||||
// catch this error and return a progress of 0 instead.
|
||||
if waddrmgr.IsError(err, waddrmgr.ErrBirthdayBlockNotSet) {
|
||||
return isRecoveryMode, progress, nil
|
||||
}
|
||||
|
||||
return isRecoveryMode, progress, err
|
||||
}
|
||||
|
||||
// Grab the best chain state the wallet is currently aware of.
|
||||
syncState := b.wallet.Manager.SyncedTo()
|
||||
|
||||
// Next, query the chain backend to grab the info about the tip of the
|
||||
// main chain.
|
||||
//
|
||||
// NOTE: The actual recovery process is handled by the btcsuite/btcwallet.
|
||||
// The process purposefully doesn't update the best height. It might create
|
||||
// a small difference between the height queried here and the height used
|
||||
// in the recovery process, ie, the bestHeight used here might be greater,
|
||||
// showing the recovery being unfinished while it's actually done. However,
|
||||
// during a wallet rescan after the recovery, the wallet's synced height
|
||||
// will catch up and this won't be an issue.
|
||||
_, bestHeight, err := b.cfg.ChainSource.GetBestBlock()
|
||||
if err != nil {
|
||||
return isRecoveryMode, progress, err
|
||||
}
|
||||
|
||||
// The birthday block height might be greater than the current synced height
|
||||
// in a newly restored wallet, and might be greater than the chain tip if a
|
||||
// rollback happens. In that case, we will return zero progress here.
|
||||
if syncState.Height < birthdayBlock.Height ||
|
||||
bestHeight < birthdayBlock.Height {
|
||||
return isRecoveryMode, progress, nil
|
||||
}
|
||||
|
||||
// progress is the ratio of the [number of blocks processed] over the [total
|
||||
// number of blocks] needed in a recovery mode, ranging from 0 to 1, in
|
||||
// which,
|
||||
// - total number of blocks is the current chain's best height minus the
|
||||
// wallet's birthday height plus 1.
|
||||
// - number of blocks processed is the wallet's synced height minus its
|
||||
// birthday height plus 1.
|
||||
// - If the wallet is born very recently, the bestHeight can be equal to
|
||||
// the birthdayBlock.Height, and it will recovery instantly.
|
||||
progress = float64(syncState.Height-birthdayBlock.Height+1) /
|
||||
float64(bestHeight-birthdayBlock.Height+1)
|
||||
|
||||
return isRecoveryMode, progress, nil
|
||||
}
|
||||
|
@ -285,6 +285,11 @@ type WalletController interface {
|
||||
// known to the wallet, expressed in Unix epoch time
|
||||
IsSynced() (bool, int64, error)
|
||||
|
||||
// GetRecoveryInfo returns a boolean indicating whether the wallet is
|
||||
// started in recovery mode. It also returns a float64 indicating the
|
||||
// recovery progress made so far.
|
||||
GetRecoveryInfo() (bool, float64, error)
|
||||
|
||||
// Start initializes the wallet, making any necessary connections,
|
||||
// starting up required goroutines etc.
|
||||
Start() error
|
||||
|
@ -45,6 +45,7 @@ import (
|
||||
"github.com/lightningnetwork/lnd/lnwallet/chainfee"
|
||||
"github.com/lightningnetwork/lnd/lnwallet/chanfunding"
|
||||
"github.com/lightningnetwork/lnd/lnwire"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
var (
|
||||
@ -364,6 +365,51 @@ func createTestWallet(tempTestDir string, miningNode *rpctest.Harness,
|
||||
return wallet, nil
|
||||
}
|
||||
|
||||
func testGetRecoveryInfo(miner *rpctest.Harness,
|
||||
alice, bob *lnwallet.LightningWallet, t *testing.T) {
|
||||
|
||||
// alice's wallet is in recovery mode
|
||||
expectedRecoveryMode := true
|
||||
expectedProgress := float64(1)
|
||||
|
||||
isRecoveryMode, progress, err := alice.GetRecoveryInfo()
|
||||
require.NoError(t, err, "unable to get alice's recovery info")
|
||||
|
||||
require.Equal(t,
|
||||
expectedRecoveryMode, isRecoveryMode, "recovery mode incorrect",
|
||||
)
|
||||
require.Equal(t, expectedProgress, progress, "progress incorrect")
|
||||
|
||||
// Generate 5 blocks and check the recovery process again.
|
||||
const numBlocksMined = 5
|
||||
_, err = miner.Node.Generate(numBlocksMined)
|
||||
require.NoError(t, err, "unable to mine blocks")
|
||||
|
||||
// Check the recovery process. Once synced, the progress should be 1.
|
||||
err = waitForWalletSync(miner, alice)
|
||||
require.NoError(t, err, "Couldn't sync Alice's wallet")
|
||||
|
||||
isRecoveryMode, progress, err = alice.GetRecoveryInfo()
|
||||
require.NoError(t, err, "unable to get alice's recovery info")
|
||||
|
||||
require.Equal(t,
|
||||
expectedRecoveryMode, isRecoveryMode, "recovery mode incorrect",
|
||||
)
|
||||
require.Equal(t, expectedProgress, progress, "progress incorrect")
|
||||
|
||||
// bob's wallet is not in recovery mode
|
||||
expectedRecoveryMode = false
|
||||
expectedProgress = float64(0)
|
||||
|
||||
isRecoveryMode, progress, err = bob.GetRecoveryInfo()
|
||||
require.NoError(t, err, "unable to get bob's recovery info")
|
||||
|
||||
require.Equal(t,
|
||||
expectedRecoveryMode, isRecoveryMode, "recovery mode incorrect",
|
||||
)
|
||||
require.Equal(t, expectedProgress, progress, "progress incorrect")
|
||||
}
|
||||
|
||||
func testDualFundingReservationWorkflow(miner *rpctest.Harness,
|
||||
alice, bob *lnwallet.LightningWallet, t *testing.T) {
|
||||
|
||||
@ -2712,6 +2758,10 @@ var walletTests = []walletTestCase{
|
||||
name: "test sign create account",
|
||||
test: testSignOutputCreateAccount,
|
||||
},
|
||||
{
|
||||
name: "test get recovery info",
|
||||
test: testGetRecoveryInfo,
|
||||
},
|
||||
}
|
||||
|
||||
func clearWalletStates(a, b *lnwallet.LightningWallet) error {
|
||||
@ -3177,6 +3227,8 @@ func runTests(t *testing.T, walletDriver *lnwallet.WalletDriver,
|
||||
NetParams: netParams,
|
||||
ChainSource: aliceClient,
|
||||
CoinType: keychain.CoinTypeTestnet,
|
||||
// wallet starts in recovery mode
|
||||
RecoveryWindow: 2,
|
||||
}
|
||||
aliceWalletController, err = walletDriver.New(aliceWalletConfig)
|
||||
if err != nil {
|
||||
@ -3200,6 +3252,8 @@ func runTests(t *testing.T, walletDriver *lnwallet.WalletDriver,
|
||||
NetParams: netParams,
|
||||
ChainSource: bobClient,
|
||||
CoinType: keychain.CoinTypeTestnet,
|
||||
// wallet starts without recovery mode
|
||||
RecoveryWindow: 0,
|
||||
}
|
||||
bobWalletController, err = walletDriver.New(bobWalletConfig)
|
||||
if err != nil {
|
||||
|
3
mock.go
3
mock.go
@ -349,6 +349,9 @@ func (*mockWalletController) SubscribeTransactions() (lnwallet.TransactionSubscr
|
||||
func (*mockWalletController) IsSynced() (bool, int64, error) {
|
||||
return true, int64(0), nil
|
||||
}
|
||||
func (*mockWalletController) GetRecoveryInfo() (bool, float64, error) {
|
||||
return true, float64(1), nil
|
||||
}
|
||||
func (*mockWalletController) Start() error {
|
||||
return nil
|
||||
}
|
||||
|
25
rpcserver.go
25
rpcserver.go
@ -261,6 +261,10 @@ func mainRPCServerPermissions() map[string][]bakery.Op {
|
||||
Entity: "info",
|
||||
Action: "read",
|
||||
}},
|
||||
"/lnrpc.Lightning/GetRecoveryInfo": {{
|
||||
Entity: "info",
|
||||
Action: "read",
|
||||
}},
|
||||
"/lnrpc.Lightning/ListPeers": {{
|
||||
Entity: "peers",
|
||||
Action: "read",
|
||||
@ -2481,6 +2485,27 @@ func (r *rpcServer) GetInfo(ctx context.Context,
|
||||
}, 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.
|
||||
func (r *rpcServer) GetRecoveryInfo(ctx context.Context,
|
||||
in *lnrpc.GetRecoveryInfoRequest) (*lnrpc.GetRecoveryInfoResponse, error) {
|
||||
|
||||
isRecoveryMode, progress, err := r.server.cc.wallet.GetRecoveryInfo()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to get wallet recovery info: %v", err)
|
||||
}
|
||||
|
||||
rpcsLog.Debugf("[getrecoveryinfo] is recovery mode=%v, progress=%v",
|
||||
isRecoveryMode, progress)
|
||||
|
||||
return &lnrpc.GetRecoveryInfoResponse{
|
||||
RecoveryMode: isRecoveryMode,
|
||||
RecoveryFinished: progress == 1,
|
||||
Progress: progress,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// ListPeers returns a verbose listing of all currently active peers.
|
||||
func (r *rpcServer) ListPeers(ctx context.Context,
|
||||
in *lnrpc.ListPeersRequest) (*lnrpc.ListPeersResponse, error) {
|
||||
|
Loading…
x
Reference in New Issue
Block a user