{
 "number": 35294,
 "repo": "bitcoin/bitcoin",
 "url": "https://github.com/bitcoin/bitcoin/pull/35294",
 "title": "wallet: Update tx chain state during loading during AttachChain instead of before",
 "author": "achow101",
 "author_association": "MEMBER",
 "created_at": "2026-05-15T00:15:10Z",
 "updated_at": "2026-09-17T01:51:25Z",
 "age_days": 125,
 "draft": false,
 "labels": [
  "Wallet"
 ],
 "milestone": null,
 "base": "master",
 "head_sha": "fcfec4b7a95aab934002104cb8789652a8172a88",
 "head_ref": "wallet-chain-load-inconsistency",
 "head_repo": "achow101/bitcoin",
 "head_history": [
  {
   "t": "2026-05-18T20:00:34Z",
   "sha": "18bc686c6e97fb5522a93412240ddca99057f1b7"
  },
  {
   "t": "2026-05-19T19:00:57Z",
   "sha": "4de5c3b692b6958b36451a0bd6ac368f8f605b4f"
  },
  {
   "t": "2026-05-26T19:52:57Z",
   "sha": "ff48c8c1039c9e3f25ff464d277e0f4bd724e86e"
  },
  {
   "t": "2026-05-26T23:08:52Z",
   "sha": "33a0ce85ec904f821066aedacea6a08f034ff8ee"
  },
  {
   "t": "2026-05-28T18:34:25Z",
   "sha": "d33afaf84e54c0262f3b6f1e93ac2edf877747de"
  },
  {
   "t": "2026-06-05T18:47:14Z",
   "sha": "3556f80f0e12957064b8eb44a8db7299a9b190fc"
  },
  {
   "t": "2026-06-10T22:14:20Z",
   "sha": "cce42a0db67c100f786ae5b8b03c6f6336bed945"
  },
  {
   "t": "2026-08-05T22:54:34Z",
   "sha": "fcfec4b7a95aab934002104cb8789652a8172a88"
  }
 ],
 "additions": 94,
 "deletions": 13,
 "changed_files": 4,
 "commit_count": 2,
 "size_bucket": "M",
 "mergeable_state": "clean",
 "bot": {
  "drahtbot": {
   "present": true,
   "reviews": {
    "concept_ack": [
     {
      "login": "rkrux",
      "url": "https://github.com/bitcoin/bitcoin/pull/35294#pullrequestreview-4319186215"
     },
     {
      "login": "pablomartin4btc",
      "url": "https://github.com/bitcoin/bitcoin/pull/35294#pullrequestreview-4876826921"
     }
    ],
    "stale_ack": [
     {
      "login": "shuv-amp",
      "url": "https://github.com/bitcoin/bitcoin/pull/35294#issuecomment-4485703303"
     },
     {
      "login": "dergoegge",
      "url": "https://github.com/bitcoin/bitcoin/pull/35294#pullrequestreview-4772211961"
     }
    ]
   },
   "conflicts": [
    {
     "number": 35716,
     "title": "wallet: Replace mapWallet and wtxOrdered with a boost::multi_index",
     "author": "achow101"
    },
    {
     "number": 27865,
     "title": "wallet: Track no-longer-spendable TXOs separately",
     "author": "achow101"
    }
   ]
  }
 },
 "acks_parsed": {
  "shuv-amp": {
   "kind": "ack",
   "hash": "18bc686c6e97fb5522a93412240ddca99057f1b7",
   "t": "2026-05-19T08:08:59Z",
   "stale": true
  },
  "rkrux": {
   "kind": "concept_ack",
   "hash": "18bc686c6e97fb5522a93412240ddca99057f1b7",
   "t": "2026-05-19T13:12:43Z",
   "stale": false
  },
  "dergoegge": {
   "kind": "ack",
   "hash": "cce42a0db67c100f786ae5b8b03c6f6336bed945",
   "t": "2026-07-24T10:06:00Z",
   "stale": true
  },
  "pablomartin4btc": {
   "kind": "concept_ack",
   "hash": null,
   "t": "2026-08-06T19:32:11Z",
   "stale": false
  }
 },
 "acks_tally": {
  "ack": 0,
  "stale_ack": 2,
  "concept_ack": 2,
  "approach_ack": 0,
  "nack": 0,
  "concept_nack": 0,
  "approach_nack": 0
 },
 "reviews": {
  "approved": 3,
  "changes_requested": 0,
  "distinct_reviewers": [
   "dergoegge",
   "fanquake",
   "l0rinc",
   "maflcko",
   "pablomartin4btc",
   "rkrux",
   "shuv-amp"
  ]
 },
 "signals": {
  "needs_rebase": false,
  "ci_failed": false,
  "mergeable_state": "clean",
  "last_author_activity": "2026-09-08T20:20:54Z",
  "last_reviewer_activity": "2026-08-06T19:32:11Z",
  "last_reviewer": "pablomartin4btc",
  "author_silent_days": 8,
  "waiting_on_author_days": 0,
  "days_since_update": 0
 },
 "refs": {
  "mentioned": [
   34599
  ],
  "depends_on": [],
  "fixes": [
   34599
  ],
  "linked_issues": [
   {
    "number": 34599,
    "type": "issue",
    "state": "open",
    "merged": false,
    "merged_at": null,
    "title": "wallet: Assertion `!wtx.isConfirmed()` failed"
   }
  ],
  "references": [
   {
    "number": 34599,
    "type": "issue",
    "state": "open",
    "merged": false,
    "merged_at": null,
    "title": "wallet: Assertion `!wtx.isConfirmed()` failed"
   }
  ],
  "conflicts": [
   35716,
   27865
  ]
 },
 "stack": {
  "shares_commits_with": [],
  "based_on": [],
  "base_for": []
 },
 "review_paths": [
  "src/wallet/wallet.cpp",
  "test/functional/wallet_chain_reprocess.py"
 ],
 "body": "When loading a wallet, updating the state based on the chain data prior to `AttachChain` results in some blocks being missed, which leads to inconsistent state information that can cause assertion failures. Moving the state updating inside of `AttachChain` after the chain notifications handler has been attached ensures that the state will always be up to date with the tip that the wallet is tracking.\n\nFixes #34599",
 "commits": [
  {
   "sha": "8bd3196e6d3d8299ce12ca51dfc68b7cf721eb30",
   "date": "2026-08-05T22:35:47Z",
   "message": "wallet: Update tx chain state after chain notifications are attached\n\nUpdating transactions against the current chain state can only be done\nin AttachChain after chain notifications have been attached. AttachChain\nwill guarantee that any blocks missing between the stored best block and\nthe current tip are rescanned, and any blocks connected during\nAttachChain will be processed after loading completes.\n\nThis fixes an issue where tx states were updated with no guarantee that\nany blocks connected prior to AttachChain are scanned, resulting in\nincorrect tx states."
  },
  {
   "sha": "fcfec4b7a95aab934002104cb8789652a8172a88",
   "date": "2026-08-05T22:35:48Z",
   "message": "test: Check all wallet tx states are correct after unclean shutdown"
  }
 ],
 "timeline": [
  {
   "t": "2026-05-18T20:00:34Z",
   "kind": "force_push",
   "who": "achow101",
   "commit": "18bc686c6e97fb5522a93412240ddca99057f1b7"
  },
  {
   "t": "2026-05-19T08:08:59Z",
   "kind": "comment",
   "who": "shuv-amp",
   "assoc": "CONTRIBUTOR",
   "text": "ACK 18bc686c6e97fb5522a93412240ddca99057f1b7\n\nReviewed the change and tested locally.\n\nMoving the tx state update out of `LoadToWallet()` makes sense to me. During DB\nload, the wallet may already have a chain pointer, but it is not yet registered\nfor chain notifications. In `AttachChain()`, notifications are registered before\nthe state update and any needed rescan, so the wallet no longer updates tx\nstates against a chain tip that can move without the wallet seeing the\nintervening block connections.\n\nThe new test fails on the pre-fix parent (`ddb94fd3e1`) with a wallet transaction\nstill reporting 0 confirmations after restart, and passes on this PR head:\n\n```bash\ntest/functional/wallet_chain_reprocess.py --configfile=$PWD/build/test/config.ini --timeout-factor=4"
  },
  {
   "t": "2026-05-19T13:06:32Z",
   "kind": "review_comment",
   "who": "rkrux",
   "assoc": "MEMBER",
   "path": "test/functional/wallet_chain_reprocess.py",
   "commit": "18bc686c6e97fb5522a93412240ddca99057f1b7",
   "in_reply_to": null,
   "text": "```diff\n-        #self.extra_args = [[\"-checkblockindex=0\"]]\n```"
  },
  {
   "t": "2026-05-19T13:08:25Z",
   "kind": "review_comment",
   "who": "rkrux",
   "assoc": "MEMBER",
   "path": "test/functional/wallet_chain_reprocess.py",
   "commit": "18bc686c6e97fb5522a93412240ddca99057f1b7",
   "in_reply_to": null,
   "text": "```diff\n-        self.nodes[0].createwallet(\"target\", load_on_startup=False)\n+        self.nodes[0].createwallet(\"target\")\n```\n\nThis argument doesn't seem necessary."
  },
  {
   "t": "2026-05-19T13:08:42Z",
   "kind": "review_comment",
   "who": "rkrux",
   "assoc": "MEMBER",
   "path": "test/functional/wallet_chain_reprocess.py",
   "commit": "18bc686c6e97fb5522a93412240ddca99057f1b7",
   "in_reply_to": null,
   "text": "Is there a particular reason to mine 1000 blocks?"
  },
  {
   "t": "2026-05-19T13:12:43Z",
   "kind": "review",
   "who": "rkrux",
   "assoc": "MEMBER",
   "state": "COMMENTED",
   "commit": "18bc686c6e97fb5522a93412240ddca99057f1b7",
   "text": "Concept ACK 18bc686c6e97fb5522a93412240ddca99057f1b7\n\nThis is indeed quite a subtle and peculiar issue!"
  },
  {
   "t": "2026-05-19T14:11:30Z",
   "kind": "review_comment",
   "who": "rkrux",
   "assoc": "MEMBER",
   "path": "src/wallet/wallet.cpp",
   "commit": "03a92323af9f554a069439949cb4615ebc1ce23d",
   "in_reply_to": null,
   "text": "[quoted text omitted]\n\nThis doesn't seem entirely correct now because the state of all transactions are not updated due to the following abandon call being present in the same loop that also updates the transaction state."
  },
  {
   "t": "2026-05-19T14:31:44Z",
   "kind": "review_comment",
   "who": "rkrux",
   "assoc": "MEMBER",
   "path": "test/functional/wallet_chain_reprocess.py",
   "commit": "18bc686c6e97fb5522a93412240ddca99057f1b7",
   "in_reply_to": 3266512716,
   "text": "[quoted text omitted]\n\nI believe the broad reason is to make the processing of few blocks by the wallet to be missed (on master without the fix here). Ultimately, this can happen as long as there are enough blocks for the node to process (post an unclean shutdown) so that block processing keeps on happening even after the loading of wallet (for the sync_all to come into effect).\n\nFor which I think a lower number can also be sufficient - 100 works on my system. I checked it by comparing the node's chain height right before the sync_all call on line 56 and the node's chain height before the unclean shutdown - it showed a more than 50 block difference.\n\nEither way, it would be very helpful to mention the reason as a comment in the test here for review and future reference."
  },
  {
   "t": "2026-05-19T19:00:57Z",
   "kind": "force_push",
   "who": "achow101",
   "commit": "4de5c3b692b6958b36451a0bd6ac368f8f605b4f"
  },
  {
   "t": "2026-05-19T19:01:01Z",
   "kind": "review_comment",
   "who": "achow101",
   "assoc": "MEMBER",
   "path": "test/functional/wallet_chain_reprocess.py",
   "commit": "18bc686c6e97fb5522a93412240ddca99057f1b7",
   "in_reply_to": 3266498611,
   "text": "Removed"
  },
  {
   "t": "2026-05-19T19:01:06Z",
   "kind": "review_comment",
   "who": "achow101",
   "assoc": "MEMBER",
   "path": "test/functional/wallet_chain_reprocess.py",
   "commit": "18bc686c6e97fb5522a93412240ddca99057f1b7",
   "in_reply_to": 3266510915,
   "text": "Removed"
  },
  {
   "t": "2026-05-19T19:02:22Z",
   "kind": "review_comment",
   "who": "achow101",
   "assoc": "MEMBER",
   "path": "test/functional/wallet_chain_reprocess.py",
   "commit": "18bc686c6e97fb5522a93412240ddca99057f1b7",
   "in_reply_to": 3266512716,
   "text": "Yes, it's so that there's enough blocks that on the restart, the node is still processing blocks while and after the wallet is loading. I chose 1000 because that was a number very likely to trigger the problem.\n\nAdded a comment and reduced to 100."
  },
  {
   "t": "2026-05-19T19:02:29Z",
   "kind": "review_comment",
   "who": "achow101",
   "assoc": "MEMBER",
   "path": "src/wallet/wallet.cpp",
   "commit": "03a92323af9f554a069439949cb4615ebc1ce23d",
   "in_reply_to": 3266959099,
   "text": "Updated"
  },
  {
   "t": "2026-05-21T10:10:17Z",
   "kind": "comment",
   "who": "fanquake",
   "assoc": "MEMBER",
   "text": "@dergoegge CI is passing now, so can run through Antithesis?"
  },
  {
   "t": "2026-05-21T10:27:04Z",
   "kind": "comment",
   "who": "dergoegge",
   "assoc": "MEMBER",
   "text": "[quoted text omitted]\n\nRunning"
  },
  {
   "t": "2026-05-22T12:40:26Z",
   "kind": "review",
   "who": "dergoegge",
   "assoc": "MEMBER",
   "state": "APPROVED",
   "commit": "4de5c3b692b6958b36451a0bd6ac368f8f605b4f",
   "text": "tACK 4de5c3b692b6958b36451a0bd6ac368f8f605b4f\n\nThis fixes the issue."
  },
  {
   "t": "2026-05-25T11:23:51Z",
   "kind": "review_comment",
   "who": "rkrux",
   "assoc": "MEMBER",
   "path": "test/functional/wallet_chain_reprocess.py",
   "commit": "4de5c3b692b6958b36451a0bd6ac368f8f605b4f",
   "in_reply_to": null,
   "text": "In 4de5c3b692b6958b36451a0bd6ac368f8f605b4f \"test: Check all wallet tx states are correct after unclean shutdown\"\n\ns/1000/100\ns/sufficiednt/sufficient"
  },
  {
   "t": "2026-05-25T12:34:44Z",
   "kind": "review_comment",
   "who": "rkrux",
   "assoc": "MEMBER",
   "path": "test/functional/wallet_chain_reprocess.py",
   "commit": "fcfec4b7a95aab934002104cb8789652a8172a88",
   "in_reply_to": null,
   "text": "In 4de5c3b \"test: Check all wallet tx states are correct after unclean shutdown\"\n\nNit: I don't believe unloading the wallet is necessary because all the blocks contain wallet transaction so the best block will be written to disk on every block connected notification just like unloading the wallet would do.\n```diff\n-        wallet.unloadwallet()\n```"
  },
  {
   "t": "2026-05-25T12:37:25Z",
   "kind": "review_comment",
   "who": "rkrux",
   "assoc": "MEMBER",
   "path": "test/functional/wallet_chain_reprocess.py",
   "commit": "33a0ce85ec904f821066aedacea6a08f034ff8ee",
   "in_reply_to": null,
   "text": "Considering the subtleness of the issue, can we add few assertions here to make it explicit for the reader regarding node/wallet height states? Maybe the following diff:\n\n```diff\ndiff --git a/test/functional/wallet_chain_reprocess.py b/test/functional/wallet_chain_reprocess.py\nindex be4c748fca..1158fa964c 100755\n--- a/test/functional/wallet_chain_reprocess.py\n+++ b/test/functional/wallet_chain_reprocess.py\n@@ -45,13 +45,20 @@ class WalletChainReprocess(BitcoinTestFramework):\n\n         # Sync nodes, kill node 0, and restart\n         self.sync_all()\n+        node_chain_height_before_kill = self.nodes[0].getblockchaininfo()[\"blocks\"]\n+        wallet_scan_height_before_kill = wallet.getwalletinfo()[\"lastprocessedblock\"][\"height\"]\n+        assert_equal(wallet_scan_height_before_kill, node_chain_height_before_kill)\n         self.nodes[0].kill_process()\n         self.start_node(0)\n         self.connect_nodes(0, 1)\n+        node_chain_height_after_kill = self.nodes[0].getblockchaininfo()[\"blocks\"]\n+        assert_greater_than(node_chain_height_before_kill, node_chain_height_after_kill)\n+        assert_greater_than(wallet_scan_height_before_kill, node_chain_height_after_kill)\n\n         self.nodes[0].loadwallet(\"target\")\n         wallet = self.nodes[0].get_wallet_rpc(\"target\")\n+        wallet_scan_height_after_kill = wallet.getwalletinfo()[\"lastprocessedblock\"][\"height\"]\n+        assert_greater_than(wallet_scan_height_before_kill, wallet_scan_height_after_kill)\n\n         # Wait for node0 to be synced\n         self.sync_all()\n```"
  },
  {
   "t": "2026-05-25T12:43:35Z",
   "kind": "review",
   "who": "rkrux",
   "assoc": "MEMBER",
   "state": "COMMENTED",
   "commit": "4de5c3b692b6958b36451a0bd6ac368f8f605b4f",
   "text": "LGTM broadly 4de5c3b692b6958b36451a0bd6ac368f8f605b4f.\n\nFew minor suggestions."
  },
  {
   "t": "2026-05-26T19:35:20Z",
   "kind": "review_comment",
   "who": "achow101",
   "assoc": "MEMBER",
   "path": "test/functional/wallet_chain_reprocess.py",
   "commit": "fcfec4b7a95aab934002104cb8789652a8172a88",
   "in_reply_to": 3298164136,
   "text": "Since the tests run with `-unsafesqlitesync`, not all data will be flushed at any given time. While we don't currently `fsync` everything on a wallet close (we probably should though), unloading the wallet at least calls `sqlite3_close` which should help to guarantee that all of the wallet data is persisted. My main concern is that killing the process while the wallet is loaded might result in data loss which could cause the test to fail intermittently or behave unexepctedly. Unloading the wallet should help mitigate such issues."
  },
  {
   "t": "2026-05-26T19:52:57Z",
   "kind": "force_push",
   "who": "achow101",
   "commit": "ff48c8c1039c9e3f25ff464d277e0f4bd724e86e"
  },
  {
   "t": "2026-05-26T19:52:59Z",
   "kind": "review_comment",
   "who": "achow101",
   "assoc": "MEMBER",
   "path": "test/functional/wallet_chain_reprocess.py",
   "commit": "4de5c3b692b6958b36451a0bd6ac368f8f605b4f",
   "in_reply_to": 3297848062,
   "text": "Done"
  },
  {
   "t": "2026-05-26T19:53:04Z",
   "kind": "review_comment",
   "who": "achow101",
   "assoc": "MEMBER",
   "path": "test/functional/wallet_chain_reprocess.py",
   "commit": "33a0ce85ec904f821066aedacea6a08f034ff8ee",
   "in_reply_to": 3298177014,
   "text": "Done"
  },
  {
   "t": "2026-05-26T23:08:52Z",
   "kind": "force_push",
   "who": "achow101",
   "commit": "33a0ce85ec904f821066aedacea6a08f034ff8ee"
  },
  {
   "t": "2026-05-28T08:30:30Z",
   "kind": "review_comment",
   "who": "maflcko",
   "assoc": "MEMBER",
   "path": "test/functional/wallet_chain_reprocess.py",
   "commit": "fcfec4b7a95aab934002104cb8789652a8172a88",
   "in_reply_to": null,
   "text": "https://github.com/bitcoin/bitcoin/actions/runs/26480361555/job/77976026440?pr=35294#step:9:3403:\n\n```\n216/469 - wallet_chain_reprocess.py failed, Duration: 3 s\n\nstdout:\n2026-05-26T23:34:49.429029Z TestFramework (INFO): PRNG seed is: 2150906247185331291\n2026-05-26T23:34:49.429675Z TestFramework (INFO): Initializing test directory /Users/runner/work/bitcoin/bitcoin/repo_archive/ci/scratch_ \u20bf\ud83e\uddea_/test_runner/test_runner_\u20bf_\ud83c\udfc3_20260526_232554/wallet_chain_reprocess_251\n2026-05-26T23:34:49.962081Z TestFramework (INFO): Testing that all blocks are reprocessed by the wallet after an unclean shutdown\n2026-05-26T23:34:52.293951Z TestFramework (ERROR): Unexpected exception:\nTraceback (most recent call last):\n  File \"/Users/runner/work/bitcoin/bitcoin/repo_archive/test/functional/test_framework/test_framework.py\", line 143, in main\n    self.run_test()\n    ~~~~~~~~~~~~~^^\n  File \"/Users/runner/work/bitcoin/bitcoin/repo_archive/ci/scratch_ \u20bf\ud83e\uddea_/build-aarch64-apple-darwin24.6.0/test/functional/wallet_chain_reprocess.py\", line 76, in run_test\n    self.test_process_all_blocks_after_unclean()\n    ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^^\n  File \"/Users/runner/work/bitcoin/bitcoin/repo_archive/ci/scratch_ \u20bf\ud83e\uddea_/build-aarch64-apple-darwin24.6.0/test/functional/wallet_chain_reprocess.py\", line 56, in test_process_all_blocks_after_unclean\n    assert_greater_than(tip_height, restart_tip_height)\n    ~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n  File \"/Users/runner/work/bitcoin/bitcoin/repo_archive/test/functional/test_framework/util.py\", line 96, in assert_greater_than\n    raise AssertionError(\"%s <= %s\" % (str(thing1), str(thing2)))\nAssertionError: 302 <= 302"
  },
  {
   "t": "2026-05-28T18:34:25Z",
   "kind": "force_push",
   "who": "achow101",
   "commit": "d33afaf84e54c0262f3b6f1e93ac2edf877747de"
  },
  {
   "t": "2026-05-28T18:43:31Z",
   "kind": "review_comment",
   "who": "achow101",
   "assoc": "MEMBER",
   "path": "test/functional/wallet_chain_reprocess.py",
   "commit": "fcfec4b7a95aab934002104cb8789652a8172a88",
   "in_reply_to": 3316394753,
   "text": "Hopefully fixed"
  },
  {
   "t": "2026-06-05T15:48:18Z",
   "kind": "review_comment",
   "who": "l0rinc",
   "assoc": "MEMBER",
   "path": "src/wallet/wallet.cpp",
   "commit": "fcfec4b7a95aab934002104cb8789652a8172a88",
   "in_reply_to": null,
   "text": "[quoted text omitted]\n\nDo we need the reverse traversal here, or could we do the state updates and abandonments in separate passes instead? It seems simpler to first update every transaction state, then run the inactive coinbase abandonment pass afterward.\n```patch\ndiff --git a/src/wallet/wallet.cpp b/src/wallet/wallet.cpp\n--- a/src/wallet/wallet.cpp\t(revision d33afaf84e54c0262f3b6f1e93ac2edf877747de)\n+++ b/src/wallet/wallet.cpp\t(revision 866e54d2e3f0851256950ad6c737c6032f67527d)\n@@ -76,7 +76,6 @@\n #include <condition_variable>\n #include <exception>\n #include <optional>\n-#include <ranges>\n #include <stdexcept>\n #include <thread>\n #include <tuple>\n@@ -3254,17 +3253,18 @@\n         walletInstance->SetLastBlockProcessedInMem(-1, uint256());\n     }\n\n-    // Update the state of every transaction now that we are connected to the chain\n-    // Do this in reverse order to ensure that any abandoning will work recursively\n-    for (auto& [_, wtx] : std::ranges::reverse_view(walletInstance->wtxOrdered)) {\n-        wtx->updateState(chain);\n-        // After updating the state of the tx, abandon if it is a coinbase that is no longer in the active chain.\n-        // This could happen during an external wallet load, or if the user replaced the chain data.\n-        if (wtx->IsCoinBase() && wtx->isInactive()) {\n-            walletInstance->AbandonTransaction(*wtx);\n+    // Update the state of every transaction now that we are connected to the chain.\n+    for (auto& [_, wtx] : walletInstance->mapWallet) {\n+        wtx.updateState(chain);\n+    }\n+\n+    // After updating all tx records, abandon any coinbase that is no longer in the active chain.\n+    // This could happen during an external wallet load, or if the user replaced the chain data.\n+    for (auto& [_, wtx] : walletInstance->mapWallet) {\n+        if (wtx.IsCoinBase() && wtx.isInactive()) {\n+            walletInstance->AbandonTransaction(wtx);\n         }\n     }\n-\n\n     if (tip_height && *tip_height != rescan_height)\n     {\n```"
  },
  {
   "t": "2026-06-05T15:58:31Z",
   "kind": "review_comment",
   "who": "l0rinc",
   "assoc": "MEMBER",
   "path": "test/functional/wallet_chain_reprocess.py",
   "commit": "fcfec4b7a95aab934002104cb8789652a8172a88",
   "in_reply_to": null,
   "text": "Can we also check that the wallet's processed block has caught back up to the node tip after the restart catch-up path?\n```patch\ndiff --git a/test/functional/wallet_chain_reprocess.py b/test/functional/wallet_chain_reprocess.py\n--- a/test/functional/wallet_chain_reprocess.py\t(revision 866e54d2e3f0851256950ad6c737c6032f67527d)\n+++ b/test/functional/wallet_chain_reprocess.py\t(revision 6c6e9e004a4c698a4a1a691d1fb09863271d173f)\n@@ -62,6 +62,7 @@\n\n         # Wait for node0 to be synced\n         self.sync_all()\n+        assert_equal(wallet.getwalletinfo()[\"lastprocessedblock\"][\"height\"], self.nodes[0].getblockchaininfo()[\"blocks\"])\n\n         # Check every transaction is confirmed\n         for txid in txids:\n```"
  },
  {
   "t": "2026-06-05T16:07:41Z",
   "kind": "review",
   "who": "l0rinc",
   "assoc": "MEMBER",
   "state": "APPROVED",
   "commit": "d33afaf84e54c0262f3b6f1e93ac2edf877747de",
   "text": "I just jumped here for a different reason, the change makes high-level sense, but I'm not well-versed enough with the details to provide a more meaningful review, left two nits."
  },
  {
   "t": "2026-06-05T18:45:36Z",
   "kind": "review_comment",
   "who": "achow101",
   "assoc": "MEMBER",
   "path": "src/wallet/wallet.cpp",
   "commit": "fcfec4b7a95aab934002104cb8789652a8172a88",
   "in_reply_to": 3363763255,
   "text": "We want to minimize iterating the entire set of transactions since there can be a lot of them, and be very slow. Iterating in reverse order allows us to do both at the same time."
  },
  {
   "t": "2026-06-05T18:47:14Z",
   "kind": "force_push",
   "who": "achow101",
   "commit": "3556f80f0e12957064b8eb44a8db7299a9b190fc"
  },
  {
   "t": "2026-06-05T18:47:16Z",
   "kind": "review_comment",
   "who": "achow101",
   "assoc": "MEMBER",
   "path": "test/functional/wallet_chain_reprocess.py",
   "commit": "fcfec4b7a95aab934002104cb8789652a8172a88",
   "in_reply_to": 3363818735,
   "text": "Done"
  },
  {
   "t": "2026-06-10T22:14:20Z",
   "kind": "force_push",
   "who": "achow101",
   "commit": "cce42a0db67c100f786ae5b8b03c6f6336bed945"
  },
  {
   "t": "2026-07-24T10:06:00Z",
   "kind": "review",
   "who": "dergoegge",
   "assoc": "MEMBER",
   "state": "APPROVED",
   "commit": "cce42a0db67c100f786ae5b8b03c6f6336bed945",
   "text": "utACK cce42a0db67c100f786ae5b8b03c6f6336bed945"
  },
  {
   "t": "2026-08-05T22:54:34Z",
   "kind": "force_push",
   "who": "achow101",
   "commit": "fcfec4b7a95aab934002104cb8789652a8172a88"
  },
  {
   "t": "2026-08-06T16:47:37Z",
   "kind": "review_comment",
   "who": "pablomartin4btc",
   "assoc": "MEMBER",
   "path": "src/wallet/wallet.cpp",
   "commit": "fcfec4b7a95aab934002104cb8789652a8172a88",
   "in_reply_to": null,
   "text": "minor nit: accidental extra line here?\n```suggestion\n\n```"
  },
  {
   "t": "2026-08-06T16:56:53Z",
   "kind": "review_comment",
   "who": "pablomartin4btc",
   "assoc": "MEMBER",
   "path": "src/wallet/wallet.cpp",
   "commit": "fcfec4b7a95aab934002104cb8789652a8172a88",
   "in_reply_to": null,
   "text": "just noticed that there's no `AssertLockHeld` in `AbandonTransaction(CWalletTx&)`: this new call site in `AttachChain` relies on `cs_wallet` being held (which it is), but the overload doesn't assert it; this is pre-existing to this PR but I think worth noting..."
  },
  {
   "t": "2026-08-06T16:59:28Z",
   "kind": "review_comment",
   "who": "pablomartin4btc",
   "assoc": "MEMBER",
   "path": "src/wallet/wallet.cpp",
   "commit": "fcfec4b7a95aab934002104cb8789652a8172a88",
   "in_reply_to": null,
   "text": "`updateState` runs before the rescan: after an unclean shutdown, txs in blocks above the restart tip get temporarily demoted to Inactive, then the rescan/blockConnected re-confirms them. I think a comment explaining this ordering dependency perhaps would help future readers (?)."
  },
  {
   "t": "2026-08-06T17:44:14Z",
   "kind": "review_comment",
   "who": "pablomartin4btc",
   "assoc": "MEMBER",
   "path": "src/wallet/wallet.cpp",
   "commit": "fcfec4b7a95aab934002104cb8789652a8172a88",
   "in_reply_to": null,
   "text": "iterating children before parents ensures that descendants have `updateState` called before the parent coinbase's `AbandonTransaction` cascade reaches them \u2014 if it didn't, the chain could have advanced between processing parent and child, leaving the child Confirmed and triggering the [assert](https://github.com/bitcoin/bitcoin/blob/c36ffd870e8b0b9933f991dc6eda661a943f3819/src/wallet/wallet.cpp#L1317)."
  },
  {
   "t": "2026-08-06T19:14:08Z",
   "kind": "review_comment",
   "who": "pablomartin4btc",
   "assoc": "MEMBER",
   "path": "src/wallet/wallet.cpp",
   "commit": "fcfec4b7a95aab934002104cb8789652a8172a88",
   "in_reply_to": null,
   "text": "this might be obvious... change is safe for `bitcoin-wallet` tool, since it won't call `AttachChain` either (where `updateState`'s call is being moved)..."
  },
  {
   "t": "2026-08-06T19:32:11Z",
   "kind": "review",
   "who": "pablomartin4btc",
   "assoc": "MEMBER",
   "state": "COMMENTED",
   "commit": "fcfec4b7a95aab934002104cb8789652a8172a88",
   "text": "Concept ACK\n\nMoving `updateState` and coinbase abandonment from inside `LoadTxRecords`/`LoadToWallet` (during `PopulateWalletFromDB`, before the chain is attached) to `AttachChain` (after `handleNotifications` is registered) closes a real race: previously, as each tx was loaded, `updateState` queried the live chainstate \u2014 which could advance between loading tx A and tx B. This left a parent coinbase as `Inactive` while a descendant appeared `Confirmed` (its block just arrived), causing `AbandonTransaction`'s internal `try_updating_state` to hit `assert(!wtx.isConfirmed())`.\n\nFWIW, I made some plain ASCII draw to show the code move and help me understand the flow.\n\nBEFORE change\n\n---\n```\nLoadExisting()\n  \u251c\u2500 PopulateWalletFromDB()              [cs_wallet held]\n  \u2502    \u2514\u2500 WalletBatch::LoadWallet()\n  \u2502         \u2514\u2500 LoadTxRecords()\n  \u2502              \u251c\u2500 Loop 1: LoadToWallet() \u00d7 N txs\n  \u2502              \u2502           \u2514\u2500 updateState() per tx   \u2190 \u26a0 chain can move between txs\n  \u2502              \u2514\u2500 Loop 2: AbandonTransaction()       \u2190 \u26a0 assert crash window for inactive coinbases\n  \u251c\u2500 TopUpKeyPool()\n  \u2514\u2500 AttachChain()\n       \u251c\u2500 handleNotifications()          \u2190 register (too late)\n       \u251c\u2500 SetLastBlockProcessedInMem\n       \u2514\u2500 ScanForWalletTransactions\n```\n\nAFTER change\n\n---\n```\nLoadExisting()\n  \u251c\u2500 PopulateWalletFromDB()              [cs_wallet held]\n  \u2502    \u2514\u2500 WalletBatch::LoadWallet()\n  \u2502         \u2514\u2500 LoadTxRecords()\n  \u2502              \u2514\u2500 Loop 1: LoadToWallet() \u00d7 N txs   (no updateState)\n  \u251c\u2500 TopUpKeyPool()\n  \u2514\u2500 AttachChain()                       [cs_wallet held]\n       \u251c\u2500 handleNotifications()          \u2190 register FIRST\n       \u251c\u2500 SetLastBlockProcessedInMem\n       \u251c\u2500 Loop (reverse): updateState() + AbandonTransaction()  \u2190 consistent snapshot\n       \u2514\u2500 ScanForWalletTransactions\n```"
  },
  {
   "t": "2026-08-26T18:17:51Z",
   "kind": "review_comment",
   "who": "achow101",
   "assoc": "MEMBER",
   "path": "src/wallet/wallet.cpp",
   "commit": "fcfec4b7a95aab934002104cb8789652a8172a88",
   "in_reply_to": 3730591669,
   "text": "If I retouch"
  },
  {
   "t": "2026-08-26T18:18:08Z",
   "kind": "review_comment",
   "who": "achow101",
   "assoc": "MEMBER",
   "path": "src/wallet/wallet.cpp",
   "commit": "fcfec4b7a95aab934002104cb8789652a8172a88",
   "in_reply_to": 3730640463,
   "text": "Leaving this as is. There is also an `EXCLUSIVE_LOCKS_HELD` annotation."
  },
  {
   "t": "2026-09-08T20:20:54Z",
   "kind": "review_comment",
   "who": "achow101",
   "assoc": "MEMBER",
   "path": "src/wallet/wallet.cpp",
   "commit": "fcfec4b7a95aab934002104cb8789652a8172a88",
   "in_reply_to": 3730653984,
   "text": "If I retouch"
  }
 ],
 "labels_log": [
  {
   "t": "2026-05-15T00:15:14Z",
   "action": "labeled",
   "label": "Wallet",
   "who": "DrahtBot"
  },
  {
   "t": "2026-05-15T01:12:54Z",
   "action": "labeled",
   "label": "CI failed",
   "who": "DrahtBot"
  },
  {
   "t": "2026-05-18T21:22:45Z",
   "action": "unlabeled",
   "label": "CI failed",
   "who": "DrahtBot"
  },
  {
   "t": "2026-05-26T21:12:54Z",
   "action": "labeled",
   "label": "CI failed",
   "who": "DrahtBot"
  },
  {
   "t": "2026-05-28T20:02:06Z",
   "action": "unlabeled",
   "label": "CI failed",
   "who": "DrahtBot"
  },
  {
   "t": "2026-06-05T19:54:24Z",
   "action": "labeled",
   "label": "CI failed",
   "who": "DrahtBot"
  },
  {
   "t": "2026-06-10T23:19:13Z",
   "action": "unlabeled",
   "label": "CI failed",
   "who": "DrahtBot"
  },
  {
   "t": "2026-08-04T22:56:45Z",
   "action": "labeled",
   "label": "Needs rebase",
   "who": "DrahtBot"
  },
  {
   "t": "2026-08-06T00:49:47Z",
   "action": "unlabeled",
   "label": "Needs rebase",
   "who": "DrahtBot"
  }
 ],
 "state_log": [],
 "text_chars": 14263,
 "text_tokens_estimate": 3565,
 "changed_paths": [
  "src/wallet/wallet.cpp",
  "src/wallet/walletdb.cpp",
  "test/functional/test_runner.py",
  "test/functional/wallet_chain_reprocess.py"
 ],
 "files": [
  {
   "path": "src/wallet/wallet.cpp",
   "add": 13,
   "del": 5
  },
  {
   "path": "src/wallet/walletdb.cpp",
   "add": 0,
   "del": 8
  },
  {
   "path": "test/functional/test_runner.py",
   "add": 1,
   "del": 0
  },
  {
   "path": "test/functional/wallet_chain_reprocess.py",
   "add": 80,
   "del": 0
  }
 ],
 "test_lines": 81,
 "git": {
  "head": "fcfec4b7a95aab934002104cb8789652a8172a88",
  "head_matches_backup": true,
  "base": "c4fbd3c72114e3466329ec6f4b0f0a4b7ebce3b1",
  "commits": [
   {
    "sha": "8bd3196e6d",
    "subject": "wallet: Update tx chain state after chain notifications are attached",
    "files": 2,
    "add": 13,
    "del": 13
   },
   {
    "sha": "fcfec4b7a9",
    "subject": "test: Check all wallet tx states are correct after unclean shutdown",
    "files": 2,
    "add": 81,
    "del": 0
   }
  ],
  "patch_truncated": false
 },
 "input_hash": "1265a5c80dc65867",
 "extracted_at": "2026-09-17T16:15:31+00:00"
}