{
 "number": 36031,
 "repo": "bitcoin/bitcoin",
 "url": "https://github.com/bitcoin/bitcoin/pull/36031",
 "title": "wallet: Remove mapMasterKeys and enforce that only one encryption key can exist",
 "author": "achow101",
 "author_association": "MEMBER",
 "created_at": "2026-08-19T23:41:55Z",
 "updated_at": "2026-09-16T14:18:34Z",
 "age_days": 28,
 "draft": false,
 "labels": [
  "Wallet"
 ],
 "milestone": null,
 "base": "master",
 "head_sha": "94af5c903604486dbcb8bc1fb4c7cb0ce9044cb9",
 "head_ref": "single-encryption-key",
 "head_repo": "achow101/bitcoin",
 "head_history": [
  {
   "t": "2026-08-24T22:56:17Z",
   "sha": "94af5c903604486dbcb8bc1fb4c7cb0ce9044cb9"
  }
 ],
 "additions": 38,
 "deletions": 48,
 "changed_files": 6,
 "commit_count": 3,
 "size_bucket": "S",
 "mergeable_state": "clean",
 "bot": {
  "drahtbot": {
   "present": true,
   "reviews": {
    "ack": [
     {
      "login": "davidgumberg",
      "url": "https://github.com/bitcoin/bitcoin/pull/36031#issuecomment-5593564117"
     }
    ],
    "concept_ack": [
     {
      "login": "jeanpablojp",
      "url": "https://github.com/bitcoin/bitcoin/pull/36031#pullrequestreview-5000816963"
     },
     {
      "login": "theStack",
      "url": "https://github.com/bitcoin/bitcoin/pull/36031#issuecomment-5398232107"
     },
     {
      "login": "rkrux",
      "url": "https://github.com/bitcoin/bitcoin/pull/36031#issuecomment-5423077372"
     },
     {
      "login": "w0xlt",
      "url": "https://github.com/bitcoin/bitcoin/pull/36031#issuecomment-5589853356"
     }
    ],
    "approach_ack": [
     {
      "login": "vicjuma",
      "url": "https://github.com/bitcoin/bitcoin/pull/36031#pullrequestreview-5012575616"
     }
    ]
   },
   "conflicts": [
    {
     "number": 35998,
     "title": "wallet: Handle or explicitly ignore `WalletBatch` write failures",
     "author": "achow101"
    },
    {
     "number": 35752,
     "title": "wallet: make encryption state updates atomic",
     "author": "l0rinc"
    },
    {
     "number": 33034,
     "title": "wallet: Store transactions in a separate sqlite table",
     "author": "achow101"
    },
    {
     "number": 32895,
     "title": "wallet: Prepare for future upgrades by recording versions of last client to open and decrypt",
     "author": "achow101"
    }
   ]
  }
 },
 "acks_parsed": {
  "jeanpablojp": {
   "kind": "concept_ack",
   "hash": null,
   "t": "2026-08-22T18:45:08Z",
   "stale": false
  },
  "theStack": {
   "kind": "concept_ack",
   "hash": null,
   "t": "2026-08-24T16:30:30Z",
   "stale": false
  },
  "vicjuma": {
   "kind": "approach_ack",
   "hash": null,
   "t": "2026-08-24T21:24:30Z",
   "stale": false
  },
  "w0xlt": {
   "kind": "concept_ack",
   "hash": null,
   "t": "2026-09-08T18:23:04Z",
   "stale": false
  },
  "davidgumberg": {
   "kind": "ack",
   "hash": null,
   "t": "2026-09-08T23:52:47Z",
   "stale": false
  }
 },
 "acks_tally": {
  "ack": 1,
  "stale_ack": 0,
  "concept_ack": 3,
  "approach_ack": 1,
  "nack": 0,
  "concept_nack": 0,
  "approach_nack": 0
 },
 "reviews": {
  "approved": 0,
  "changes_requested": 0,
  "distinct_reviewers": [
   "davidgumberg",
   "jeanpablojp",
   "rkrux",
   "theStack",
   "vicjuma",
   "w0xlt"
  ]
 },
 "signals": {
  "needs_rebase": false,
  "ci_failed": false,
  "mergeable_state": "clean",
  "last_author_activity": "2026-08-24T23:00:44Z",
  "last_reviewer_activity": "2026-09-08T23:52:47Z",
  "last_reviewer": "davidgumberg",
  "author_silent_days": 23,
  "waiting_on_author_days": 8,
  "days_since_update": 1
 },
 "refs": {
  "mentioned": [
   352
  ],
  "depends_on": [],
  "fixes": [],
  "linked_issues": [],
  "references": [
   {
    "number": 352,
    "type": "pull",
    "state": "closed",
    "merged": true,
    "merged_at": "2011-07-13",
    "title": "Wallet Private Key Encryption (on CWallet)"
   }
  ],
  "conflicts": [
   35998,
   35752,
   33034,
   32895
  ]
 },
 "stack": {
  "shares_commits_with": [],
  "based_on": [],
  "base_for": []
 },
 "review_paths": [
  "src/wallet/crypter.h",
  "src/wallet/wallet.cpp",
  "src/wallet/walletdb.cpp"
 ],
 "body": "`mapMasterKeys` was introduced in #352 without a clear rationale. Likely it was to allow the user to have multiple passphrases by encrypting the wallet's encryption key with different passphrases. However, this functionality was never implemented, and current code enforces in a few places (inconsistently) that there is only one encryption key.\n\nBy removing `mapMasterKeys` and replacing it with a single `m_encryption_key`, we can remove this confusion and simplify encryption key handling. No one should have a wallet that has more than one encryption key, and the ID of that key should be `1`.\n\nThe format of the database record remains unchanged. If a wallet somehow has an encryption key with an ID other than `1`, the record will stay the same and the `id` is stored. Otherwise, new encryption keys always have an ID of `1`.\n\nIf a wallet has more than one encryption key, this becomes a corruption error because it should never happen outside of someone doing something weird with their wallet.\n\n***\n\nI asked Matt for his rationale for adding `mapMasterKeys` and his response was\n\n[quoted text omitted]",
 "commits": [
  {
   "sha": "eea102293c2494c110efe46b974f6623b93bbfdc",
   "date": "2026-08-19T23:19:45Z",
   "message": "crypter: Store the CMasterKey ID in the CMasterKey itself"
  },
  {
   "sha": "a66f77f625e67e793c1fc090ea6230e53c847ce5",
   "date": "2026-08-19T23:32:41Z",
   "message": "walletdb: Make WriteMasterKey take only a CMasterKey"
  },
  {
   "sha": "94af5c903604486dbcb8bc1fb4c7cb0ce9044cb9",
   "date": "2026-08-24T22:56:04Z",
   "message": "wallet: Hold only one encryption key in memory\n\nmapMasterKeys was introduced in #352 without a clear rationale. Likely\nit was to allow the user to have multiple passphrases by encrypting the\nwallet's encryption key with different passphrases. However, this\nfunctionality was never implemented, and current code enforces in a few\nplaces that there is only one encryption key.\n\nBy removing mapMasterKeys and replacing it with a single\nm_encryption_key, we can remove this confusion and simplify encryption\nkey handling. No one should have a wallet that has more than one\nencrypion key."
  }
 ],
 "timeline": [
  {
   "t": "2026-08-22T18:45:08Z",
   "kind": "review",
   "who": "jeanpablojp",
   "assoc": "CONTRIBUTOR",
   "state": "COMMENTED",
   "commit": "dc1d640c5d74d3967f751aa663a335ba0af9739a",
   "text": "Concept ACK, keeping a single key makes sense. One thing inline."
  },
  {
   "t": "2026-08-22T18:45:09Z",
   "kind": "review_comment",
   "who": "jeanpablojp",
   "assoc": "CONTRIBUTOR",
   "path": "src/wallet/walletdb.cpp",
   "commit": "94af5c903604486dbcb8bc1fb4c7cb0ce9044cb9",
   "in_reply_to": null,
   "text": "Tried this out. With v0.19.1, I created a wallet with `blank=true` and ran `encryptwallet`, `unloadwallet`, `loadwallet`, and `encryptwallet` again, ending up with two `mkey` records. The second `encryptwallet` succeeded because `IsCrypted()` back then read an in-memory flag that loading an `mkey` never set. v28.2 opens that wallet and leaves both `mkey` records in place. On master, `migratewallet` converts it to a descriptor wallet but carries both records over. On this branch, `migratewallet` rejects the legacy wallet and `loadwallet` rejects the already migrated descriptor wallet with `Wallet corrupted`. Neither failure writes to the file. I don't know how common these wallets are."
  },
  {
   "t": "2026-08-24T16:30:30Z",
   "kind": "comment",
   "who": "theStack",
   "assoc": "MEMBER",
   "text": "Concept ACK"
  },
  {
   "t": "2026-08-24T21:08:46Z",
   "kind": "review_comment",
   "who": "vicjuma",
   "assoc": "CONTRIBUTOR",
   "path": "src/wallet/crypter.h",
   "commit": "94af5c903604486dbcb8bc1fb4c7cb0ce9044cb9",
   "in_reply_to": null,
   "text": "Clearly `backward compatible` as its value is independent of the encryption operations\n\n**Tweak**\n```cpp\nsrc/wallet/crypter.h:45  uint32_t m_id{10000};\n```\n\n**Results**"
  },
  {
   "t": "2026-08-24T21:23:38Z",
   "kind": "review_comment",
   "who": "vicjuma",
   "assoc": "CONTRIBUTOR",
   "path": "src/wallet/wallet.cpp",
   "commit": "94af5c903604486dbcb8bc1fb4c7cb0ce9044cb9",
   "in_reply_to": null,
   "text": "Confirmed the assertion via a functional test\n\n```python\ndef test_nonstandard_master_key_id(self, passphrase, do_wallet_tool):\n        self.log.info(\"Test that a master key with a non-standard ID remains unchanged\")\n        current_node = self.nodes[0]\n        wallet_name = \"pr36031_non_standard_id\"\n        current_node.createwallet(wallet_name=wallet_name)\n        pr36031_non_standard_id = current_node.get_wallet_rpc(wallet_name)\n        pr36031_non_standard_id.encryptwallet(passphrase)\n        pr36031_non_standard_id.unloadwallet()\n        dumpfile_path = current_node.datadir_path / f\"{pr36031_non_standard_id}.dump\"\n        do_wallet_tool(f\"-wallet={wallet_name}\", f\"-dumpfile={dumpfile_path}\", \"dump\")\n        mkey_prefix = \"046d6b6579\"\n        ''' Patch the mkey record's ID from 1 (01000000, 4-byte LE) to 42 (2a000000)'''\n        with open(dumpfile_path, \"r\") as f:\n            dump_content = f.readlines()\n        dump_content = dump_content[:-1]\n        mkey_idx = next(i for i, l in enumerate(dump_content) if l.startswith(F\"{mkey_prefix}\"))\n        key_hex, value_hex = dump_content[mkey_idx].strip().split(\",\")\n\n        ''''asserts that the value is not 1 as changed in `src/wallet/crypter.h:45  uint32_t m_id{10000};` '''\n        assert_not_equal(key_hex[-8:], \"01000000\")\n        dump_content[mkey_idx] = f\"{key_hex[:-8]}2a000000,{value_hex}\\n\"\n        with open(dumpfile_path, \"w\") as f:\n            contents = \"\".join(dump_content)\n            f.write(contents)\n            checksum = hash256(contents.encode())\n            f.write(f\"checksum,{checksum.hex()}\\n\")\n        wallet_name_new = \"pr36031_non_standard_id_new\"\n        do_wallet_tool(f\"-wallet={wallet_name_new}\", f\"-dumpfile={dumpfile_path}\", \"createfromdump\")\n        current_node.loadwallet(wallet_name_new)\n        pr36031_non_standard_id_new = current_node.get_wallet_rpc(wallet_name_new)\n        receive = pr36031_non_standard_id_new.getnewaddress()\n        current_node.generatetoaddress(101, receive, called_by_framework=True)\n\n        with WalletUnlock(pr36031_non_standard_id_new, passphrase):\n            destination = pr36031_non_standard_id_new.getnewaddress()\n\n            utxo = pr36031_non_standard_id_new.listunspent()[0]\n\n            raw = pr36031_non_standard_id_new.createrawtransaction(\n                [{\"txid\": utxo[\"txid\"], \"vout\": utxo[\"vout\"]}],\n                {destination: 1.0},\n            )\n\n            signed = pr36031_non_standard_id_new.signrawtransactionwithwallet(raw)\n\n            assert_equal(signed[\"complete\"], True)\n        pr36031_non_standard_id_new.unloadwallet()\n\n        redump_path = current_node.datadir_path / \"pr36031_non_standard_id_new.dump\"\n        do_wallet_tool(f\"-wallet={wallet_name_new}\", f\"-dumpfile={redump_path}\", \"dump\")\n        with open(redump_path, \"r\") as f:\n            mkey_line = next(l for l in f if l.startswith(f\"{mkey_prefix}\"))\n\n        ''''asserts that the above operations were actually done using the tweaked mkey '''\n        assert mkey_line.split(\",\")[0].endswith(\"2a000000\")\n```\n\nThe test passes with the above change\n**output**"
  },
  {
   "t": "2026-08-24T21:24:30Z",
   "kind": "review",
   "who": "vicjuma",
   "assoc": "CONTRIBUTOR",
   "state": "COMMENTED",
   "commit": "dc1d640c5d74d3967f751aa663a335ba0af9739a",
   "text": "Approach ACK"
  },
  {
   "t": "2026-08-24T22:56:17Z",
   "kind": "force_push",
   "who": "achow101",
   "commit": "94af5c903604486dbcb8bc1fb4c7cb0ce9044cb9"
  },
  {
   "t": "2026-08-24T23:00:44Z",
   "kind": "review_comment",
   "who": "achow101",
   "assoc": "MEMBER",
   "path": "src/wallet/walletdb.cpp",
   "commit": "94af5c903604486dbcb8bc1fb4c7cb0ce9044cb9",
   "in_reply_to": 3836748386,
   "text": "That's interesting.\n\nI think this scenario is pretty unlikely. It requires you to create a blank wallet, encrypt it, and then unload it, then reload, then encrypt again. The expected usage of blank wallets was that you would immediately put stuff into that wallet and it would no longer become blank. There's really no reason to have a blank wallet that you would reload. This was a also a fairly niche feature when introduced (and kinda still is) and it looks like this behavior was present for 0.18 and 0.19, so I expect that the number of affected users is approximately 0.\n\nI should also note that once the wallet becomes not blank, even with the multiple encryption keys, only one of those keys would actually be used and be the correct one for that wallet. If we had some way to figure out which key that was, this change would still mostly hold.\n\nBut I don't think that this is worth addressing as I expect no one to actually be affected by it. If it is problem, I have a half-baked solution that should work, but I feel like it's a bit complicated and premature to be adding that in when we don't know if anyone is affected by this."
  },
  {
   "t": "2026-08-26T09:07:56Z",
   "kind": "comment",
   "who": "rkrux",
   "assoc": "MEMBER",
   "text": "Strong Concept ACK 94af5c903604486dbcb8bc1fb4c7cb0ce9044cb9\n\nI don't see a good reason to have multiple keys or passphrases to encrypt the wallet going forward.\nAnd the corresponding code cleanup is necessary for long term wallet maintainability and usability."
  },
  {
   "t": "2026-09-08T18:23:04Z",
   "kind": "comment",
   "who": "w0xlt",
   "assoc": "CONTRIBUTOR",
   "text": "Concept ACK"
  },
  {
   "t": "2026-09-08T23:51:47Z",
   "kind": "review_comment",
   "who": "davidgumberg",
   "assoc": "MEMBER",
   "path": "src/wallet/walletdb.cpp",
   "commit": "94af5c903604486dbcb8bc1fb4c7cb0ce9044cb9",
   "in_reply_to": null,
   "text": "High-level out-of-scope remark:\n\nIt would be wise to just rename these records instead of deleting them."
  },
  {
   "t": "2026-09-08T23:52:47Z",
   "kind": "comment",
   "who": "davidgumberg",
   "assoc": "MEMBER",
   "text": "crACK https://github.com/bitcoin/bitcoin/commit/94af5c903604486dbcb8bc1fb4c7cb0ce9044cb9\n\nFrom a high-level this change is safe:\n\nthe first two commits are basically ~refactors, but there is a slight behavior change in eea102293c2494c110efe46b974f6623b93bbfdc which is that multiple encryption keys in a wallet file will result in failing to load after only this commit.\n\nIn the main commit (94af5c903604486dbcb8bc1fb4c7cb0ce9044cb9) it seems to me that the worst that can happen is that some wallet edge case has not been taken into account and will fail to load on startup, which is fine."
  }
 ],
 "labels_log": [
  {
   "t": "2026-08-19T23:41:59Z",
   "action": "labeled",
   "label": "Wallet",
   "who": "DrahtBot"
  },
  {
   "t": "2026-08-20T00:59:25Z",
   "action": "labeled",
   "label": "CI failed",
   "who": "DrahtBot"
  },
  {
   "t": "2026-08-25T00:13:58Z",
   "action": "unlabeled",
   "label": "CI failed",
   "who": "DrahtBot"
  }
 ],
 "state_log": [],
 "text_chars": 8039,
 "text_tokens_estimate": 2009,
 "changed_paths": [
  "src/bench/wallet_encrypt.cpp",
  "src/wallet/crypter.h",
  "src/wallet/wallet.cpp",
  "src/wallet/wallet.h",
  "src/wallet/walletdb.cpp",
  "src/wallet/walletdb.h"
 ],
 "files": [
  {
   "path": "src/bench/wallet_encrypt.cpp",
   "add": 2,
   "del": 3
  },
  {
   "path": "src/wallet/crypter.h",
   "add": 2,
   "del": 0
  },
  {
   "path": "src/wallet/wallet.cpp",
   "add": 20,
   "del": 26
  },
  {
   "path": "src/wallet/wallet.h",
   "add": 1,
   "del": 3
  },
  {
   "path": "src/wallet/walletdb.cpp",
   "add": 12,
   "del": 15
  },
  {
   "path": "src/wallet/walletdb.h",
   "add": 1,
   "del": 1
  }
 ],
 "test_lines": 5,
 "git": {
  "head": "94af5c903604486dbcb8bc1fb4c7cb0ce9044cb9",
  "head_matches_backup": true,
  "base": "367b2202a496c866c485d940864171ca97c514f2",
  "commits": [
   {
    "sha": "eea102293c",
    "subject": "crypter: Store the CMasterKey ID in the CMasterKey itself",
    "files": 2,
    "add": 3,
    "del": 0
   },
   {
    "sha": "a66f77f625",
    "subject": "walletdb: Make WriteMasterKey take only a CMasterKey",
    "files": 3,
    "add": 5,
    "del": 5
   },
   {
    "sha": "94af5c9036",
    "subject": "wallet: Hold only one encryption key in memory",
    "files": 4,
    "add": 31,
    "del": 44
   }
  ],
  "patch_truncated": false
 },
 "input_hash": "4eda52a06e271ea7",
 "extracted_at": "2026-09-17T16:15:31+00:00"
}