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 }