lnd_recovery_test.go 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445
  1. package itest
  2. import (
  3. "bytes"
  4. "fmt"
  5. "math"
  6. "github.com/btcsuite/btcd/btcutil"
  7. "github.com/btcsuite/btcd/btcutil/hdkeychain"
  8. "github.com/btcsuite/btcd/chaincfg/chainhash"
  9. "github.com/btcsuite/btcd/txscript"
  10. "github.com/btcsuite/btcd/wire"
  11. "github.com/lightningnetwork/lnd/aezeed"
  12. "github.com/lightningnetwork/lnd/input"
  13. "github.com/lightningnetwork/lnd/lnrpc"
  14. "github.com/lightningnetwork/lnd/lnrpc/signrpc"
  15. "github.com/lightningnetwork/lnd/lnrpc/walletrpc"
  16. "github.com/lightningnetwork/lnd/lntest"
  17. "github.com/lightningnetwork/lnd/lntest/node"
  18. "github.com/lightningnetwork/lnd/lntest/wait"
  19. "github.com/lightningnetwork/lnd/lnwallet/chainfee"
  20. "github.com/stretchr/testify/require"
  21. )
  22. // testGetRecoveryInfo checks whether lnd gives the right information about
  23. // the wallet recovery process.
  24. func testGetRecoveryInfo(ht *lntest.HarnessTest) {
  25. // First, create a new node with strong passphrase and grab the mnemonic
  26. // used for key derivation. This will bring up Carol with an empty
  27. // wallet, and such that she is synced up.
  28. password := []byte("The Magic Words are Squeamish Ossifrage")
  29. carol, mnemonic, _ := ht.NewNodeWithSeed("Carol", nil, password, false)
  30. checkInfo := func(expectedRecoveryMode, expectedRecoveryFinished bool,
  31. expectedProgress float64, recoveryWindow int32) {
  32. // Restore Carol, passing in the password, mnemonic, and
  33. // desired recovery window.
  34. node := ht.RestoreNodeWithSeed(
  35. carol.Name(), nil, password, mnemonic, "",
  36. recoveryWindow, nil,
  37. )
  38. // Query carol for her current wallet recovery progress.
  39. err := wait.NoError(func() error {
  40. // Verify that recovery info gives the right response.
  41. resp := node.RPC.GetRecoveryInfo(nil)
  42. mode := resp.RecoveryMode
  43. finished := resp.RecoveryFinished
  44. progress := resp.Progress
  45. if mode != expectedRecoveryMode {
  46. return fmt.Errorf("expected recovery mode %v "+
  47. "got %v", expectedRecoveryMode, mode)
  48. }
  49. if finished != expectedRecoveryFinished {
  50. return fmt.Errorf("expected finished %v "+
  51. "got %v", expectedRecoveryFinished,
  52. finished)
  53. }
  54. if progress != expectedProgress {
  55. return fmt.Errorf("expected progress %v"+
  56. "got %v", expectedProgress, progress)
  57. }
  58. return nil
  59. }, defaultTimeout)
  60. require.NoError(ht, err)
  61. // Lastly, shutdown this Carol so we can move on to the next
  62. // restoration.
  63. ht.Shutdown(node)
  64. }
  65. // Restore Carol with a recovery window of 0. Since it's not in recovery
  66. // mode, the recovery info will give a response with recoveryMode=false,
  67. // recoveryFinished=false, and progress=0
  68. checkInfo(false, false, 0, 0)
  69. // Change the recovery windown to be 1 to turn on recovery mode. Since
  70. // the current chain height is the same as the birthday height, it
  71. // should indicate the recovery process is finished.
  72. checkInfo(true, true, 1, 1)
  73. // We now go ahead 5 blocks. Because the wallet's syncing process is
  74. // controlled by a goroutine in the background, it will catch up
  75. // quickly. This makes the recovery progress back to 1.
  76. ht.MineBlocks(5)
  77. checkInfo(true, true, 1, 1)
  78. }
  79. // testOnchainFundRecovery checks lnd's ability to rescan for onchain outputs
  80. // when providing a valid aezeed that owns outputs on the chain. This test
  81. // performs multiple restorations using the same seed and various recovery
  82. // windows to ensure we detect funds properly.
  83. func testOnchainFundRecovery(ht *lntest.HarnessTest) {
  84. // First, create a new node with strong passphrase and grab the mnemonic
  85. // used for key derivation. This will bring up Carol with an empty
  86. // wallet, and such that she is synced up.
  87. password := []byte("The Magic Words are Squeamish Ossifrage")
  88. carol, mnemonic, _ := ht.NewNodeWithSeed("Carol", nil, password, false)
  89. // As long as the mnemonic is non-nil and the extended key is empty, the
  90. // closure below will always restore the node from the seed. The tests
  91. // need to manually overwrite this value to change that behavior.
  92. rootKey := ""
  93. // Create a closure for testing the recovery of Carol's wallet. This
  94. // method takes the expected value of Carol's balance when using the
  95. // given recovery window. Additionally, the caller can specify an action
  96. // to perform on the restored node before the node is shutdown.
  97. restoreCheckBalance := func(expAmount int64, expectedNumUTXOs uint32,
  98. recoveryWindow int32, fn func(*node.HarnessNode)) {
  99. ht.Helper()
  100. // Restore Carol, passing in the password, mnemonic, and
  101. // desired recovery window.
  102. node := ht.RestoreNodeWithSeed(
  103. carol.Name(), nil, password, mnemonic, rootKey,
  104. recoveryWindow, nil,
  105. )
  106. // Query carol for her current wallet balance, and also that we
  107. // gain the expected number of UTXOs.
  108. var (
  109. currBalance int64
  110. currNumUTXOs uint32
  111. )
  112. err := wait.NoError(func() error {
  113. resp := node.RPC.WalletBalance()
  114. currBalance = resp.ConfirmedBalance
  115. req := &walletrpc.ListUnspentRequest{
  116. Account: "",
  117. MaxConfs: math.MaxInt32,
  118. MinConfs: 0,
  119. }
  120. utxoResp := node.RPC.ListUnspent(req)
  121. currNumUTXOs = uint32(len(utxoResp.Utxos))
  122. // Verify that Carol's balance and number of UTXOs
  123. // matches what's expected.
  124. if expAmount != currBalance {
  125. return fmt.Errorf("balance not matched, want "+
  126. "%d, got %d", expAmount, currBalance)
  127. }
  128. if currNumUTXOs != expectedNumUTXOs {
  129. return fmt.Errorf("num of UTXOs not matched, "+
  130. "want %d, got %d", expectedNumUTXOs,
  131. currNumUTXOs)
  132. }
  133. return nil
  134. }, defaultTimeout)
  135. require.NoError(ht, err, "timeout checking Carol")
  136. // If the user provided a callback, execute the commands against
  137. // the restored Carol.
  138. if fn != nil {
  139. fn(node)
  140. }
  141. // Lastly, shutdown this Carol so we can move on to the next
  142. // restoration.
  143. ht.Shutdown(node)
  144. }
  145. // Create a closure-factory for building closures that can generate and
  146. // skip a configurable number of addresses, before finally sending coins
  147. // to a next generated address. The returned closure will apply the same
  148. // behavior to both default P2WKH and NP2WKH scopes.
  149. skipAndSend := func(nskip int) func(*node.HarnessNode) {
  150. return func(node *node.HarnessNode) {
  151. ht.Helper()
  152. // Generate and skip the number of addresses requested.
  153. for i := 0; i < nskip; i++ {
  154. req := &lnrpc.NewAddressRequest{}
  155. req.Type = AddrTypeWitnessPubkeyHash
  156. node.RPC.NewAddress(req)
  157. req.Type = AddrTypeNestedPubkeyHash
  158. node.RPC.NewAddress(req)
  159. req.Type = AddrTypeTaprootPubkey
  160. node.RPC.NewAddress(req)
  161. }
  162. // Send one BTC to the next P2WKH address.
  163. ht.FundCoins(btcutil.SatoshiPerBitcoin, node)
  164. // And another to the next NP2WKH address.
  165. ht.FundCoinsNP2WKH(btcutil.SatoshiPerBitcoin, node)
  166. // Add another whole coin to the P2TR address.
  167. ht.FundCoinsP2TR(btcutil.SatoshiPerBitcoin, node)
  168. }
  169. }
  170. // Restore Carol with a recovery window of 0. Since no coins have been
  171. // sent, her balance should be zero.
  172. //
  173. // After, one BTC is sent to both her first external P2WKH and NP2WKH
  174. // addresses.
  175. restoreCheckBalance(0, 0, 0, skipAndSend(0))
  176. // Check that restoring without a look-ahead results in having no funds
  177. // in the wallet, even though they exist on-chain.
  178. restoreCheckBalance(0, 0, 0, nil)
  179. // Now, check that using a look-ahead of 1 recovers the balance from
  180. // the two transactions above. We should also now have 2 UTXOs in the
  181. // wallet at the end of the recovery attempt.
  182. //
  183. // After, we will generate and skip 9 P2WKH, NP2WKH and P2TR addresses,
  184. // and send another BTC to the subsequent 10th address in each
  185. // derivation path.
  186. restoreCheckBalance(3*btcutil.SatoshiPerBitcoin, 3, 1, skipAndSend(9))
  187. // Check that using a recovery window of 9 does not find the two most
  188. // recent txns.
  189. restoreCheckBalance(3*btcutil.SatoshiPerBitcoin, 3, 9, nil)
  190. // Extending our recovery window to 10 should find the most recent
  191. // transactions, leaving the wallet with 6 BTC total. We should also
  192. // learn of the two additional UTXOs created above.
  193. //
  194. // After, we will skip 19 more addrs, sending to the 20th address past
  195. // our last found address, and repeat the same checks.
  196. restoreCheckBalance(6*btcutil.SatoshiPerBitcoin, 6, 10, skipAndSend(19))
  197. // Check that recovering with a recovery window of 19 fails to find the
  198. // most recent transactions.
  199. restoreCheckBalance(6*btcutil.SatoshiPerBitcoin, 6, 19, nil)
  200. // Ensure that using a recovery window of 20 succeeds with all UTXOs
  201. // found and the final balance reflected.
  202. // After these checks are done, we'll want to make sure we can also
  203. // recover change address outputs. This is mainly motivated by a now
  204. // fixed bug in the wallet in which change addresses could at times be
  205. // created outside of the default key scopes. Recovery only used to be
  206. // performed on the default key scopes, so ideally this test case
  207. // would've caught the bug earlier. Carol has received 9 BTC so far from
  208. // the miner, we'll send 8 back to ensure all of her UTXOs get spent to
  209. // avoid fee discrepancies and a change output is formed.
  210. const minerAmt = 8 * btcutil.SatoshiPerBitcoin
  211. const finalBalance = 9 * btcutil.SatoshiPerBitcoin
  212. promptChangeAddr := func(node *node.HarnessNode) {
  213. ht.Helper()
  214. minerAddr := ht.Miner.NewMinerAddress()
  215. req := &lnrpc.SendCoinsRequest{
  216. Addr: minerAddr.String(),
  217. Amount: minerAmt,
  218. TargetConf: 6,
  219. }
  220. resp := node.RPC.SendCoins(req)
  221. txid := ht.Miner.AssertNumTxsInMempool(1)[0]
  222. require.Equal(ht, txid.String(), resp.Txid)
  223. block := ht.MineBlocks(1)[0]
  224. ht.Miner.AssertTxInBlock(block, txid)
  225. }
  226. restoreCheckBalance(finalBalance, 9, 20, promptChangeAddr)
  227. // We should expect a static fee of 36400 satoshis for spending 9
  228. // inputs (3 P2WPKH, 3 NP2WPKH, 3 P2TR) to two P2TR outputs. Carol
  229. // should therefore only have one UTXO present (the change output) of
  230. // 9 - 8 - fee BTC.
  231. const fee = 37000
  232. restoreCheckBalance(finalBalance-minerAmt-fee, 1, 21, nil)
  233. // Last of all, make sure we can also restore a node from the extended
  234. // master root key directly instead of the seed.
  235. var seedMnemonic aezeed.Mnemonic
  236. copy(seedMnemonic[:], mnemonic)
  237. cipherSeed, err := seedMnemonic.ToCipherSeed(password)
  238. require.NoError(ht, err)
  239. extendedRootKey, err := hdkeychain.NewMaster(
  240. cipherSeed.Entropy[:], harnessNetParams,
  241. )
  242. require.NoError(ht, err)
  243. rootKey = extendedRootKey.String()
  244. mnemonic = nil
  245. restoreCheckBalance(finalBalance-minerAmt-fee, 1, 21, nil)
  246. }
  247. // testRescanAddressDetection makes sure that addresses created from internal
  248. // (m/1017' scope) keys aren't detected as UTXOs when re-scanning the wallet
  249. // with --reset-wallet-transactions to avoid showing them as un-spent ghost
  250. // UTXOs even if they are being spent. This is to test a fix in the wallet that
  251. // addresses the following scenario:
  252. // 1. A key is derived from the internal 1017' scope with a custom key family
  253. // and a p2wkh address is derived from that key.
  254. // 2. Funds are sent to the address created above in a way that also creates a
  255. // change output. The change output is recognized as belonging to the
  256. // wallet, which is correct.
  257. // 3. The funds on the address created in step 1 are fully spent (without
  258. // creating a change output) into an output that doesn't belong to the
  259. // wallet (e.g. a channel funding output).
  260. // 4. At some point the user re-scans their wallet by using the
  261. // --reset-wallet-transactions flag.
  262. // 5. The wallet re-scan detects the change output created in step 2 and flags
  263. // the transaction as relevant.
  264. // 6. While adding the relevant TX to the wallet DB, the wallet also detects
  265. // the address from step 1 as belonging to the wallet (because the internal
  266. // key scope is defined as having the address type p2wkh) and adds that
  267. // output as an UTXO as well (<- this is the bug). The wallet now has two
  268. // UTXOs in its database.
  269. // 7. The transaction that spends the UTXO of the address from step 1 is not
  270. // detected by the wallet as belonging to it (because the output is a
  271. // channel output and the input (correctly) isn't recognized as belonging to
  272. // the wallet in that part of the code, it is never marked as spent and
  273. // stays in the wallet as a ghost UTXO forever.
  274. //
  275. // The fix in the wallet is simple: In step 6, don't detect addresses from
  276. // internal scopes while re-scanning to be in line with the logic in other areas
  277. // of the wallet code.
  278. func testRescanAddressDetection(ht *lntest.HarnessTest) {
  279. // We start off by creating a new node with the wallet re-scan flag
  280. // enabled. This won't have any effect on the first startup but will
  281. // come into effect after we re-start the node.
  282. walletPassword := []byte("some-password")
  283. carol, _, _ := ht.NewNodeWithSeed(
  284. "carol", []string{"--reset-wallet-transactions"},
  285. walletPassword, false,
  286. )
  287. ht.FundCoins(btcutil.SatoshiPerBitcoin, carol)
  288. // Create an address generated from internal keys.
  289. keyDesc := carol.RPC.DeriveNextKey(&walletrpc.KeyReq{KeyFamily: 123})
  290. pubKeyHash := btcutil.Hash160(keyDesc.RawKeyBytes)
  291. ghostUtxoAddr, err := btcutil.NewAddressWitnessPubKeyHash(
  292. pubKeyHash, harnessNetParams,
  293. )
  294. require.NoError(ht, err)
  295. // Send funds to the (p2wkh!) address generated from the internal
  296. // (m/1017') key scope. Because the internal key scope is defined as
  297. // p2wkh address type, this might be incorrectly detected by the wallet
  298. // in some situations (which this test makes sure is fixed).
  299. const ghostUtxoAmount = 456_000
  300. carol.RPC.SendCoins(&lnrpc.SendCoinsRequest{
  301. Addr: ghostUtxoAddr.String(),
  302. Amount: ghostUtxoAmount,
  303. SatPerVbyte: 1,
  304. })
  305. ht.MineBlocksAndAssertNumTxes(1, 1)
  306. // Make sure we see the change output in our list of unspent outputs.
  307. // We _don't_ expect to see the ghost UTXO here as in this step it's
  308. // ignored as an internal address correctly.
  309. ht.AssertNumUTXOsConfirmed(carol, 1)
  310. unspent := carol.RPC.ListUnspent(&walletrpc.ListUnspentRequest{
  311. MinConfs: 1,
  312. })
  313. // Which one was the change output and which one the ghost UTXO output?
  314. var ghostUtxoIndex uint32
  315. if unspent.Utxos[0].Outpoint.OutputIndex == 0 {
  316. ghostUtxoIndex = 1
  317. }
  318. ghostUtxoHash, err := chainhash.NewHash(
  319. unspent.Utxos[0].Outpoint.TxidBytes,
  320. )
  321. require.NoError(ht, err)
  322. burnScript, _ := ht.CreateBurnAddr(AddrTypeWitnessPubkeyHash)
  323. // Create fee estimation for a p2wkh input and p2wkh output.
  324. feeRate := chainfee.SatPerKWeight(12500)
  325. estimator := input.TxWeightEstimator{}
  326. estimator.AddP2WKHInput()
  327. estimator.AddP2WKHOutput()
  328. estimatedWeight := int64(estimator.Weight())
  329. requiredFee := feeRate.FeeForWeight(estimatedWeight)
  330. tx := wire.NewMsgTx(2)
  331. tx.TxIn = []*wire.TxIn{{
  332. PreviousOutPoint: wire.OutPoint{
  333. Hash: *ghostUtxoHash,
  334. Index: ghostUtxoIndex,
  335. },
  336. }}
  337. value := int64(ghostUtxoAmount - requiredFee)
  338. tx.TxOut = []*wire.TxOut{{
  339. PkScript: burnScript,
  340. Value: value,
  341. }}
  342. var buf bytes.Buffer
  343. require.NoError(ht, tx.Serialize(&buf))
  344. ghostUtxoScript := ht.PayToAddrScript(ghostUtxoAddr)
  345. utxoInfo := []*signrpc.TxOut{{
  346. PkScript: ghostUtxoScript,
  347. Value: ghostUtxoAmount,
  348. }}
  349. // Let's sign the input now.
  350. signResp := carol.RPC.SignOutputRaw(&signrpc.SignReq{
  351. RawTxBytes: buf.Bytes(),
  352. SignDescs: []*signrpc.SignDescriptor{{
  353. Output: utxoInfo[0],
  354. InputIndex: 0,
  355. KeyDesc: keyDesc,
  356. Sighash: uint32(txscript.SigHashAll),
  357. WitnessScript: utxoInfo[0].PkScript,
  358. }},
  359. })
  360. // Add the witness to the input and publish the tx.
  361. tx.TxIn[0].Witness = wire.TxWitness{
  362. append(signResp.RawSigs[0], byte(txscript.SigHashAll)),
  363. keyDesc.RawKeyBytes,
  364. }
  365. buf.Reset()
  366. require.NoError(ht, tx.Serialize(&buf))
  367. carol.RPC.PublishTransaction(&walletrpc.Transaction{
  368. TxHex: buf.Bytes(),
  369. })
  370. // Wait until the spending tx is found and mine a block to confirm it.
  371. ht.Miner.AssertNumTxsInMempool(1)
  372. ht.MineBlocks(1)
  373. // The wallet should still just see a single UTXO of the change output
  374. // created earlier.
  375. ht.AssertNumUTXOsConfirmed(carol, 1)
  376. // Let's now re-start the node, causing it to do the wallet re-scan.
  377. ht.RestartNode(carol)
  378. // There should now still only be a single UTXO from the change output
  379. // instead of two (the ghost UTXO should be missing if the fix works).
  380. ht.AssertNumUTXOsConfirmed(carol, 1)
  381. }