github.com/decred/dcrlnd@v0.7.6/lntest/itest/lnd_recovery_test.go (about)

     1  package itest
     2  
     3  import (
     4  	"context"
     5  	"math"
     6  
     7  	"github.com/decred/dcrd/dcrutil/v4"
     8  	"github.com/decred/dcrlnd/lnrpc"
     9  	"github.com/decred/dcrlnd/lntest"
    10  	"github.com/decred/dcrlnd/lntest/wait"
    11  	"github.com/stretchr/testify/require"
    12  )
    13  
    14  // testGetRecoveryInfo checks whether lnd gives the right information about
    15  // the wallet recovery process.
    16  func testGetRecoveryInfo(net *lntest.NetworkHarness, t *harnessTest) {
    17  	// TODO: reenable after the wallet impls support this.
    18  	t.Skipf("Re-enable after wallet impls support this")
    19  
    20  	ctxb := context.Background()
    21  
    22  	// First, create a new node with strong passphrase and grab the mnemonic
    23  	// used for key derivation. This will bring up Carol with an empty
    24  	// wallet, and such that she is synced up.
    25  	password := []byte("The Magic Words are Squeamish Ossifrage")
    26  	carol, mnemonic, _, err := net.NewNodeWithSeed(
    27  		"Carol", nil, password, false,
    28  	)
    29  	if err != nil {
    30  		t.Fatalf("unable to create node with seed; %v", err)
    31  	}
    32  
    33  	shutdownAndAssert(net, t, carol)
    34  
    35  	checkInfo := func(expectedRecoveryMode, expectedRecoveryFinished bool,
    36  		expectedProgress float64, recoveryWindow int32) {
    37  
    38  		// Restore Carol, passing in the password, mnemonic, and
    39  		// desired recovery window.
    40  		node, err := net.RestoreNodeWithSeed(
    41  			"Carol", nil, password, mnemonic, "", recoveryWindow,
    42  			nil,
    43  		)
    44  		if err != nil {
    45  			t.Fatalf("unable to restore node: %v", err)
    46  		}
    47  
    48  		// Wait for Carol to sync to the chain.
    49  		ctxt, _ := context.WithTimeout(ctxb, defaultTimeout)
    50  		_, minerHeight, err := net.Miner.Node.GetBestBlock(ctxt)
    51  		if err != nil {
    52  			t.Fatalf("unable to get current blockheight %v", err)
    53  		}
    54  		err = waitForNodeBlockHeight(node, minerHeight)
    55  		if err != nil {
    56  			t.Fatalf("unable to sync to chain: %v", err)
    57  		}
    58  
    59  		// Query carol for her current wallet recovery progress.
    60  		var (
    61  			recoveryMode     bool
    62  			recoveryFinished bool
    63  			progress         float64
    64  		)
    65  
    66  		err = wait.Predicate(func() bool {
    67  			// Verify that recovery info gives the right response.
    68  			req := &lnrpc.GetRecoveryInfoRequest{}
    69  			ctxt, _ := context.WithTimeout(ctxb, defaultTimeout)
    70  			resp, err := node.GetRecoveryInfo(ctxt, req)
    71  			if err != nil {
    72  				t.Fatalf("unable to query recovery info: %v", err)
    73  			}
    74  
    75  			recoveryMode = resp.RecoveryMode
    76  			recoveryFinished = resp.RecoveryFinished
    77  			progress = resp.Progress
    78  
    79  			if recoveryMode != expectedRecoveryMode ||
    80  				recoveryFinished != expectedRecoveryFinished ||
    81  				progress != expectedProgress {
    82  				return false
    83  			}
    84  
    85  			return true
    86  		}, defaultTimeout)
    87  		if err != nil {
    88  			t.Fatalf("expected recovery mode to be %v, got %v, "+
    89  				"expected recovery finished to be %v, got %v, "+
    90  				"expected progress %v, got %v",
    91  				expectedRecoveryMode, recoveryMode,
    92  				expectedRecoveryFinished, recoveryFinished,
    93  				expectedProgress, progress,
    94  			)
    95  		}
    96  
    97  		// Lastly, shutdown this Carol so we can move on to the next
    98  		// restoration.
    99  		shutdownAndAssert(net, t, node)
   100  	}
   101  
   102  	// Restore Carol with a recovery window of 0. Since it's not in recovery
   103  	// mode, the recovery info will give a response with recoveryMode=false,
   104  	// recoveryFinished=false, and progress=0
   105  	checkInfo(false, false, 0, 0)
   106  
   107  	// Change the recovery windown to be 1 to turn on recovery mode. Since the
   108  	// current chain height is the same as the birthday height, it should
   109  	// indicate the recovery process is finished.
   110  	checkInfo(true, true, 1, 1)
   111  
   112  	// We now go ahead 5 blocks. Because the wallet's syncing process is
   113  	// controlled by a goroutine in the background, it will catch up quickly.
   114  	// This makes the recovery progress back to 1.
   115  	mineBlocks(t, net, 5, 0)
   116  	checkInfo(true, true, 1, 1)
   117  }
   118  
   119  // testOnchainFundRecovery checks lnd's ability to rescan for onchain outputs
   120  // when providing a valid aezeed that owns outputs on the chain. This test
   121  // performs multiple restorations using the same seed and various recovery
   122  // windows to ensure we detect funds properly.
   123  func testOnchainFundRecovery(net *lntest.NetworkHarness, t *harnessTest) {
   124  	ctxb := context.Background()
   125  
   126  	// First, create a new node with strong passphrase and grab the mnemonic
   127  	// used for key derivation. This will bring up Carol with an empty
   128  	// wallet, and such that she is synced up.
   129  	password := []byte("The Magic Words are Squeamish Ossifrage")
   130  	carol, mnemonic, _, err := net.NewNodeWithSeed(
   131  		"Carol", nil, password, false,
   132  	)
   133  	require.NoError(t.t, err)
   134  	shutdownAndAssert(net, t, carol)
   135  
   136  	// As long as the mnemonic is non-nil and the extended key is empty, the
   137  	// closure below will always restore the node from the seed. The tests
   138  	// need to manually overwrite this value to change that behavior.
   139  	rootKey := ""
   140  
   141  	// Create a closure for testing the recovery of Carol's wallet. This
   142  	// method takes the expected value of Carol's balance when using the
   143  	// given recovery window. Additionally, the caller can specify an action
   144  	// to perform on the restored node before the node is shutdown.
   145  	restoreCheckBalance := func(expAmount int64, expectedNumUTXOs uint32,
   146  		recoveryWindow int32, fn func(*lntest.HarnessNode)) {
   147  
   148  		t.t.Helper()
   149  
   150  		// Restore Carol, passing in the password, mnemonic, and
   151  		// desired recovery window.
   152  		node, err := net.RestoreNodeWithSeed(
   153  			"Carol", nil, password, mnemonic, rootKey,
   154  			recoveryWindow, nil,
   155  		)
   156  		require.NoError(t.t, err)
   157  
   158  		// Query carol for her current wallet balance, and also that we
   159  		// gain the expected number of UTXOs.
   160  		var (
   161  			currBalance  int64
   162  			currNumUTXOs uint32
   163  		)
   164  		err = wait.Predicate(func() bool {
   165  			req := &lnrpc.WalletBalanceRequest{}
   166  			ctxt, _ := context.WithTimeout(ctxb, defaultTimeout)
   167  			resp, err := node.WalletBalance(ctxt, req)
   168  			require.NoError(t.t, err)
   169  			currBalance = resp.ConfirmedBalance
   170  
   171  			utxoReq := &lnrpc.ListUnspentRequest{
   172  				MaxConfs: math.MaxInt32,
   173  			}
   174  			ctxt, _ = context.WithTimeout(ctxb, defaultTimeout)
   175  			utxoResp, err := node.ListUnspent(ctxt, utxoReq)
   176  			require.NoError(t.t, err)
   177  			currNumUTXOs = uint32(len(utxoResp.Utxos))
   178  
   179  			// Verify that Carol's balance and number of UTXOs
   180  			// matches what's expected.
   181  			if expAmount != currBalance {
   182  				return false
   183  			}
   184  			if currNumUTXOs != expectedNumUTXOs {
   185  				return false
   186  			}
   187  
   188  			return true
   189  		}, defaultTimeout)
   190  		if err != nil {
   191  			t.Fatalf("expected restored node to have %d atoms, "+
   192  				"instead has %d atoms, expected %d utxos "+
   193  				"instead has %d", expAmount, currBalance,
   194  				expectedNumUTXOs, currNumUTXOs)
   195  		}
   196  
   197  		// If the user provided a callback, execute the commands against
   198  		// the restored Carol.
   199  		if fn != nil {
   200  			fn(node)
   201  		}
   202  
   203  		// Lastly, shutdown this Carol so we can move on to the next
   204  		// restoration.
   205  		shutdownAndAssert(net, t, node)
   206  	}
   207  
   208  	// Create a closure-factory for building closures that can generate and
   209  	// skip a configurable number of addresses, before finally sending coins
   210  	// to a next generated address. The returned closure will apply the same
   211  	// behavior to both default P2WKH and NP2WKH scopes.
   212  	skipAndSend := func(nskip int) func(*lntest.HarnessNode) {
   213  		return func(node *lntest.HarnessNode) {
   214  			newP2PKHAddrReq := &lnrpc.NewAddressRequest{
   215  				Type: AddrTypePubkeyHash,
   216  			}
   217  
   218  			// Generate and skip the number of addresses requested.
   219  			for i := 0; i < nskip; i++ {
   220  				ctxt, _ := context.WithTimeout(ctxb, defaultTimeout)
   221  				_, err = node.NewAddress(ctxt, newP2PKHAddrReq)
   222  				require.NoError(t.t, err)
   223  			}
   224  
   225  			// Send one DCR to the next P2PKH address.
   226  			net.SendCoins(
   227  				t.t, dcrutil.AtomsPerCoin, node,
   228  			)
   229  		}
   230  	}
   231  
   232  	// Restore Carol with a recovery window of 0. Since no coins have been
   233  	// sent, her balance should be zero.
   234  	//
   235  	// After, one DCR is sent to both her first external P2WKH and NP2WKH
   236  	// addresses.
   237  	restoreCheckBalance(0, 0, 0, skipAndSend(0))
   238  
   239  	// Check that restoring without a look-ahead results in having no funds
   240  	// in the wallet, even though they exist on-chain.
   241  	restoreCheckBalance(0, 0, 0, nil)
   242  
   243  	// Now, check that using a look-ahead of 1 recovers the balance from
   244  	// the two transactions above. We should also now have 2 UTXOs in the
   245  	// wallet at the end of the recovery attempt.
   246  	//
   247  	// After, we will generate and skip 9 P2WKH and NP2WKH addresses, and
   248  	// send another DCR to the subsequent 10th address in each derivation
   249  	// path.
   250  	restoreCheckBalance(2*dcrutil.AtomsPerCoin, 2, 1, skipAndSend(9))
   251  
   252  	// Check that using a recovery window of 9 does not find the two most
   253  	// recent txns.
   254  	restoreCheckBalance(2*dcrutil.AtomsPerCoin, 2, 9, nil)
   255  
   256  	// Extending our recovery window to 10 should find the most recent
   257  	// transactions, leaving the wallet with 4 BTC total. We should also
   258  	// learn of the two additional UTXOs created above.
   259  	//
   260  	// After, we will skip 19 more addrs, sending to the 20th address past
   261  	// our last found address, and repeat the same checks.
   262  	restoreCheckBalance(4*dcrutil.AtomsPerCoin, 4, 10, skipAndSend(19))
   263  
   264  	// Check that recovering with a recovery window of 19 fails to find the
   265  	// most recent transactions.
   266  	restoreCheckBalance(4*dcrutil.AtomsPerCoin, 4, 19, nil)
   267  
   268  	// Ensure that using a recovery window of 20 succeeds with all UTXOs
   269  	// found and the final balance reflected.
   270  
   271  	// After these checks are done, we'll want to make sure we can also
   272  	// recover change address outputs.  This is mainly motivated by a now
   273  	// fixed bug in the wallet in which change addresses could at times be
   274  	// created outside of the default key scopes. Recovery only used to be
   275  	// performed on the default key scopes, so ideally this test case
   276  	// would've caught the bug earlier. Carol has received 6 BTC so far from
   277  	// the miner, we'll send 5 back to ensure all of her UTXOs get spent to
   278  	// avoid fee discrepancies and a change output is formed.
   279  	const minerAmt = 5 * dcrutil.AtomsPerCoin
   280  	const finalBalance = 6 * dcrutil.AtomsPerCoin
   281  	promptChangeAddr := func(node *lntest.HarnessNode) {
   282  		t.t.Helper()
   283  		minerAddr, err := net.Miner.NewAddress(ctxb)
   284  		require.NoError(t.t, err)
   285  
   286  		ctxt, _ := context.WithTimeout(ctxb, defaultTimeout)
   287  		resp, err := node.SendCoins(ctxt, &lnrpc.SendCoinsRequest{
   288  			Addr:   minerAddr.String(),
   289  			Amount: minerAmt,
   290  		})
   291  		require.NoError(t.t, err)
   292  		txid, err := waitForTxInMempool(
   293  			net.Miner.Node, minerMempoolTimeout,
   294  		)
   295  		require.NoError(t.t, err)
   296  		require.Equal(t.t, txid.String(), resp.Txid)
   297  
   298  		block := mineBlocks(t, net, 1, 1)[0]
   299  		assertTxInBlock(t, block, txid)
   300  	}
   301  	restoreCheckBalance(finalBalance, 6, 20, promptChangeAddr)
   302  
   303  	// We should expect a static fee of 27750 satoshis for spending 6 inputs
   304  	// (3 P2WPKH, 3 NP2WPKH) to two P2WPKH outputs. Carol should therefore
   305  	// only have one UTXO present (the change output) of 6 - 5 - fee BTC.
   306  	const fee = 27750
   307  	restoreCheckBalance(finalBalance-minerAmt-fee, 1, 21, nil)
   308  
   309  	// Last of all, make sure we can also restore a node from the extended
   310  	// master root key directly instead of the seed.
   311  	//
   312  	// Note(decred): this is disabled as we don't support restoring from
   313  	// extended priv keys yet.
   314  	/*
   315  		var seedMnemonic aezeed.Mnemonic
   316  		copy(seedMnemonic[:], mnemonic)
   317  		cipherSeed, err := seedMnemonic.ToCipherSeed(password)
   318  		require.NoError(t.t, err)
   319  		extendedRootKey, err := hdkeychain.NewMaster(
   320  			cipherSeed.Entropy[:], harnessNetParams,
   321  		)
   322  		require.NoError(t.t, err)
   323  		rootKey = extendedRootKey.String()
   324  		mnemonic = nil
   325  
   326  		restoreCheckBalance(finalBalance-minerAmt-fee, 1, 21, nil)
   327  	*/
   328  }