multi: add a rpc endpoint to track the recovery process

This commit is contained in:
yyforyongyu 2020-06-09 14:47:17 +08:00
parent c1ef5bb908
commit 987edc9d81
17 changed files with 1182 additions and 613 deletions

View File

@ -1729,6 +1729,27 @@ func getInfo(ctx *cli.Context) error {
return nil 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{ var pendingChannelsCommand = cli.Command{
Name: "pendingchannels", Name: "pendingchannels",
Category: "Channels", Category: "Channels",

View File

@ -271,6 +271,7 @@ func main() {
walletBalanceCommand, walletBalanceCommand,
channelBalanceCommand, channelBalanceCommand,
getInfoCommand, getInfoCommand,
getRecoveryInfoCommand,
pendingChannelsCommand, pendingChannelsCommand,
sendPaymentCommand, sendPaymentCommand,
payInvoiceCommand, payInvoiceCommand,

View File

@ -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 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 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 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), 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 then from `lncli unlock`, it's possible to _restart_ the rescan from where it

View File

@ -55,6 +55,8 @@ description):
* Lists all available connected peers. * Lists all available connected peers.
* GetInfo * GetInfo
* Returns basic data concerning the daemon. * Returns basic data concerning the daemon.
* GetRecoveryInfo
* Returns information about recovery process.
* PendingChannels * PendingChannels
* List the number of pending (not fully confirmed) channels. * List the number of pending (not fully confirmed) channels.
* ListChannels * ListChannels

View File

@ -41,6 +41,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.GetRecoveryInfo
get: "/v1/getrecoveryinfo"
- selector: lnrpc.Lightning.PendingChannels - selector: lnrpc.Lightning.PendingChannels
get: "/v1/channels/pending" get: "/v1/channels/pending"
- selector: lnrpc.Lightning.ListChannels - selector: lnrpc.Lightning.ListChannels

View File

@ -1876,8 +1876,8 @@ func (m *ForwardHtlcInterceptRequest) GetExpiry() uint32 {
} }
//* //*
//ForwardHtlcInterceptResponse enables the caller to resolve a previously hold forward. //ForwardHtlcInterceptResponse enables the caller to resolve a previously hold
//The caller can choose either to: //forward. The caller can choose either to:
//- `Resume`: Execute the default behavior (usually forward). //- `Resume`: Execute the default behavior (usually forward).
//- `Reject`: Fail the htlc backwards. //- `Reject`: Fail the htlc backwards.
//- `Settle`: Settle this htlc with a given preimage. //- `Settle`: Settle this htlc with a given preimage.

View File

@ -98,7 +98,7 @@ service Router {
rpc TrackPayment (TrackPaymentRequest) returns (stream PaymentStatus) { rpc TrackPayment (TrackPaymentRequest) returns (stream PaymentStatus) {
option deprecated = true; option deprecated = true;
} }
/** /**
HtlcInterceptor dispatches a bi-directional streaming RPC in which HtlcInterceptor dispatches a bi-directional streaming RPC in which
Forwarded HTLC requests are sent to the client and the client responds with 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 In case of interception, the htlc can be either settled, cancelled or
resumed later by using the ResolveHoldForward endpoint. resumed later by using the ResolveHoldForward endpoint.
*/ */
rpc HtlcInterceptor (stream ForwardHtlcInterceptResponse) returns (stream ForwardHtlcInterceptRequest); rpc HtlcInterceptor (stream ForwardHtlcInterceptResponse)
returns (stream ForwardHtlcInterceptRequest);
} }
message SendPaymentRequest { message SendPaymentRequest {
@ -589,7 +590,7 @@ message PaymentStatus {
repeated lnrpc.HTLCAttempt htlcs = 4; repeated lnrpc.HTLCAttempt htlcs = 4;
} }
message CircuitKey { message CircuitKey {
/// The id of the channel that the is part of this circuit. /// The id of the channel that the is part of this circuit.
uint64 chan_id = 1; uint64 chan_id = 1;
@ -618,14 +619,14 @@ message ForwardHtlcInterceptRequest {
} }
/** /**
ForwardHtlcInterceptResponse enables the caller to resolve a previously hold forward. ForwardHtlcInterceptResponse enables the caller to resolve a previously hold
The caller can choose either to: forward. The caller can choose either to:
- `Resume`: Execute the default behavior (usually forward). - `Resume`: Execute the default behavior (usually forward).
- `Reject`: Fail the htlc backwards. - `Reject`: Fail the htlc backwards.
- `Settle`: Settle this htlc with a given preimage. - `Settle`: Settle this htlc with a given preimage.
*/ */
message ForwardHtlcInterceptResponse { message ForwardHtlcInterceptResponse {
/** /**
The key of this forwarded htlc. It defines the incoming channel id and The key of this forwarded htlc. It defines the incoming channel id and
the index in this channel. the index in this channel.
*/ */
@ -638,9 +639,8 @@ message ForwardHtlcInterceptResponse {
bytes preimage = 3; bytes preimage = 3;
} }
enum ResolveHoldForwardAction { enum ResolveHoldForwardAction {
SETTLE = 0; SETTLE = 0;
FAIL = 1; FAIL = 1;
RESUME = 2; RESUME = 2;
} }

File diff suppressed because it is too large Load Diff

View File

@ -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) { 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 protoReq PendingChannelsRequest
var metadata runtime.ServerMetadata 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) { mux.Handle("GET", pattern_Lightning_PendingChannels_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()
@ -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) { mux.Handle("GET", pattern_Lightning_PendingChannels_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()
@ -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_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_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))) 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_GetInfo_0 = runtime.ForwardResponseMessage
forward_Lightning_GetRecoveryInfo_0 = runtime.ForwardResponseMessage
forward_Lightning_PendingChannels_0 = runtime.ForwardResponseMessage forward_Lightning_PendingChannels_0 = runtime.ForwardResponseMessage
forward_Lightning_ListChannels_0 = runtime.ForwardResponseMessage forward_Lightning_ListChannels_0 = runtime.ForwardResponseMessage

View File

@ -140,6 +140,14 @@ service Lightning {
*/ */
rpc GetInfo (GetInfoRequest) returns (GetInfoResponse); 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? // TODO(roasbeef): merge with below with bool?
/* lncli: `pendingchannels` /* lncli: `pendingchannels`
PendingChannels returns a list of all the channels that are currently PendingChannels returns a list of all the channels that are currently
@ -1381,6 +1389,19 @@ message GetInfoResponse {
map<uint32, Feature> features = 19; 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 { message Chain {
// The blockchain the node is on (eg bitcoin, litecoin) // The blockchain the node is on (eg bitcoin, litecoin)
string chain = 1; string chain = 1;

View File

@ -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": { "/v1/graph": {
"get": { "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.", "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": { "lnrpcGraphTopologyUpdate": {
"type": "object", "type": "object",
"properties": { "properties": {

View File

@ -817,6 +817,105 @@ const (
AddrTypeNestedPubkeyHash = lnrpc.AddressType_NESTED_PUBKEY_HASH 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 // testOnchainFundRecovery checks lnd's ability to rescan for onchain outputs
// when providing a valid aezeed that owns outputs on the chain. This test // when providing a valid aezeed that owns outputs on the chain. This test
// performs multiple restorations using the same seed and various recovery // performs multiple restorations using the same seed and various recovery
@ -14430,6 +14529,10 @@ var testsCases = []*testCase{
name: "sweep coins", name: "sweep coins",
test: testSweepAllCoins, test: testSweepAllCoins,
}, },
{
name: "recovery info",
test: testGetRecoveryInfo,
},
{ {
name: "onchain fund recovery", name: "onchain fund recovery",
test: testOnchainFundRecovery, test: testOnchainFundRecovery,

View File

@ -844,3 +844,83 @@ func (b *BtcWallet) IsSynced() (bool, int64, error) {
return true, bestTimestamp, nil 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
}

View File

@ -285,6 +285,11 @@ type WalletController interface {
// known to the wallet, expressed in Unix epoch time // known to the wallet, expressed in Unix epoch time
IsSynced() (bool, int64, error) 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, // Start initializes the wallet, making any necessary connections,
// starting up required goroutines etc. // starting up required goroutines etc.
Start() error Start() error

View File

@ -45,6 +45,7 @@ import (
"github.com/lightningnetwork/lnd/lnwallet/chainfee" "github.com/lightningnetwork/lnd/lnwallet/chainfee"
"github.com/lightningnetwork/lnd/lnwallet/chanfunding" "github.com/lightningnetwork/lnd/lnwallet/chanfunding"
"github.com/lightningnetwork/lnd/lnwire" "github.com/lightningnetwork/lnd/lnwire"
"github.com/stretchr/testify/require"
) )
var ( var (
@ -364,6 +365,51 @@ func createTestWallet(tempTestDir string, miningNode *rpctest.Harness,
return wallet, nil 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, func testDualFundingReservationWorkflow(miner *rpctest.Harness,
alice, bob *lnwallet.LightningWallet, t *testing.T) { alice, bob *lnwallet.LightningWallet, t *testing.T) {
@ -2712,6 +2758,10 @@ var walletTests = []walletTestCase{
name: "test sign create account", name: "test sign create account",
test: testSignOutputCreateAccount, test: testSignOutputCreateAccount,
}, },
{
name: "test get recovery info",
test: testGetRecoveryInfo,
},
} }
func clearWalletStates(a, b *lnwallet.LightningWallet) error { func clearWalletStates(a, b *lnwallet.LightningWallet) error {
@ -3177,6 +3227,8 @@ func runTests(t *testing.T, walletDriver *lnwallet.WalletDriver,
NetParams: netParams, NetParams: netParams,
ChainSource: aliceClient, ChainSource: aliceClient,
CoinType: keychain.CoinTypeTestnet, CoinType: keychain.CoinTypeTestnet,
// wallet starts in recovery mode
RecoveryWindow: 2,
} }
aliceWalletController, err = walletDriver.New(aliceWalletConfig) aliceWalletController, err = walletDriver.New(aliceWalletConfig)
if err != nil { if err != nil {
@ -3200,6 +3252,8 @@ func runTests(t *testing.T, walletDriver *lnwallet.WalletDriver,
NetParams: netParams, NetParams: netParams,
ChainSource: bobClient, ChainSource: bobClient,
CoinType: keychain.CoinTypeTestnet, CoinType: keychain.CoinTypeTestnet,
// wallet starts without recovery mode
RecoveryWindow: 0,
} }
bobWalletController, err = walletDriver.New(bobWalletConfig) bobWalletController, err = walletDriver.New(bobWalletConfig)
if err != nil { if err != nil {

View File

@ -349,6 +349,9 @@ func (*mockWalletController) SubscribeTransactions() (lnwallet.TransactionSubscr
func (*mockWalletController) IsSynced() (bool, int64, error) { func (*mockWalletController) IsSynced() (bool, int64, error) {
return true, int64(0), nil return true, int64(0), nil
} }
func (*mockWalletController) GetRecoveryInfo() (bool, float64, error) {
return true, float64(1), nil
}
func (*mockWalletController) Start() error { func (*mockWalletController) Start() error {
return nil return nil
} }

View File

@ -261,6 +261,10 @@ func mainRPCServerPermissions() map[string][]bakery.Op {
Entity: "info", Entity: "info",
Action: "read", Action: "read",
}}, }},
"/lnrpc.Lightning/GetRecoveryInfo": {{
Entity: "info",
Action: "read",
}},
"/lnrpc.Lightning/ListPeers": {{ "/lnrpc.Lightning/ListPeers": {{
Entity: "peers", Entity: "peers",
Action: "read", Action: "read",
@ -2481,6 +2485,27 @@ func (r *rpcServer) GetInfo(ctx context.Context,
}, nil }, 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. // ListPeers returns a verbose listing of all currently active peers.
func (r *rpcServer) ListPeers(ctx context.Context, func (r *rpcServer) ListPeers(ctx context.Context,
in *lnrpc.ListPeersRequest) (*lnrpc.ListPeersResponse, error) { in *lnrpc.ListPeersRequest) (*lnrpc.ListPeersResponse, error) {