{
 "number": 33043,
 "repo": "bitcoin/bitcoin",
 "url": "https://github.com/bitcoin/bitcoin/pull/33043",
 "title": "[POC] wallet: Enable non-electronic (paper-based) wallet backup with codex32",
 "author": "w0xlt",
 "author_association": "CONTRIBUTOR",
 "created_at": "2025-07-23T07:36:29Z",
 "updated_at": "2026-07-06T00:34:30Z",
 "age_days": 421,
 "draft": true,
 "labels": [
  "Needs rebase"
 ],
 "milestone": null,
 "base": "master",
 "head_sha": "14f3342262ae7667f389bfc50c787e4689f25a1b",
 "head_ref": "codex32",
 "head_repo": "w0xlt/bitcoin",
 "head_history": [
  {
   "t": "2025-07-23T17:34:31Z",
   "sha": "c61039482788365a09bdd18732024d7e6ccac758"
  },
  {
   "t": "2025-07-23T18:17:37Z",
   "sha": "14f3342262ae7667f389bfc50c787e4689f25a1b"
  }
 ],
 "additions": 1437,
 "deletions": 46,
 "changed_files": 31,
 "commit_count": 5,
 "size_bucket": "XL",
 "mergeable_state": "dirty",
 "bot": {
  "drahtbot": {
   "present": true,
   "reviews": {},
   "conflicts": [
    {
     "number": 33112,
     "title": "wallet: relax external_signer flag constraints",
     "author": "Sjors"
    },
    {
     "number": 33034,
     "title": "wallet: Store transactions in a separate sqlite table",
     "author": "achow101"
    },
    {
     "number": 32977,
     "title": "wallet: Remove wallet version and several legacy related functions",
     "author": "w0xlt"
    },
    {
     "number": 32895,
     "title": "wallet: Prepare for future upgrades by recording versions of last client to open and decrypt",
     "author": "achow101"
    },
    {
     "number": 32652,
     "title": "wallet: add codex32 argument to addhdkey",
     "author": "roconnor-blockstream"
    },
    {
     "number": 32489,
     "title": "wallet: Add `exportwatchonlywallet` RPC to export a watchonly version of a wallet",
     "author": "achow101"
    },
    {
     "number": 28333,
     "title": "wallet: Construct ScriptPubKeyMans with all data rather than loaded progressively",
     "author": "achow101"
    },
    {
     "number": 27865,
     "title": "wallet: Track no-longer-spendable TXOs separately",
     "author": "achow101"
    },
    {
     "number": 26022,
     "title": "Add util::ResultPtr class",
     "author": "ryanofsky"
    }
   ]
  }
 },
 "acks_parsed": {},
 "acks_tally": {
  "ack": 0,
  "stale_ack": 0,
  "concept_ack": 0,
  "approach_ack": 0,
  "nack": 0,
  "concept_nack": 0,
  "approach_nack": 0
 },
 "reviews": {
  "approved": 0,
  "changes_requested": 0,
  "distinct_reviewers": [
   "BenWestgate",
   "Sjors",
   "apoelstra",
   "rkrux"
  ]
 },
 "signals": {
  "needs_rebase": true,
  "ci_failed": false,
  "mergeable_state": "dirty",
  "last_author_activity": "2025-07-24T20:12:39Z",
  "last_reviewer_activity": "2026-04-08T14:26:24Z",
  "last_reviewer": "rkrux",
  "author_silent_days": 419,
  "waiting_on_author_days": 162,
  "days_since_update": 73
 },
 "refs": {
  "mentioned": [
   32652
  ],
  "depends_on": [],
  "fixes": [],
  "linked_issues": [],
  "references": [
   {
    "number": 32652,
    "type": "pull",
    "state": "closed",
    "merged": false,
    "merged_at": null,
    "title": "wallet: add codex32 argument to addhdkey"
   }
  ],
  "conflicts": [
   33112,
   33034,
   32977,
   32895,
   32652,
   32489,
   28333,
   27865,
   26022
  ]
 },
 "stack": {
  "shares_commits_with": [],
  "based_on": [],
  "base_for": []
 },
 "review_paths": [],
 "body": "This PR introduces support for exporting and restoring wallet seeds using the [codex32](https://github.com/BlockstreamResearch/codex32) format, enabling non-electronic (paper-based) wallet backups.\n\nTo accomplish this, the patch ports the `codex32.{c,h}` implementation from Core Lightning to C++, integrating it with Bitcoin Core's libraries. Corresponding unit tests for codex32 encoding and decoding are also included.\n\nBecause Bitcoin Core wallets currently do not store the seed material by default, this PR adds support for doing so, along with a new wallet flag to explicitly indicate when this feature is enabled.\n\nTwo new RPCs are introduced:\n\n`exposesecret`: exports the wallet seed in codex32 format.\n\n`recoverwalletfromseed`: restores a wallet from a codex32-encoded seed.\n\nA functional test included in the last commit demonstrates the full backup and restore flow. Currently, only the default derivation path is supported.\n\nThis PR is intended as a proposal for feedback\u2014to assess whether this functionality is desirable, and to explore how it might evolve further.",
 "commits": [
  {
   "sha": "8b80a5989346a25140ba8b98b10dfade58a59ae7",
   "date": "2025-07-23T17:34:21Z",
   "message": "wallet: Add codex32 files"
  },
  {
   "sha": "2d6b910820eb6740afa8c2e0b4da9774dc08aa8f",
   "date": "2025-07-23T17:34:21Z",
   "message": "wallet: Store seeds in database"
  },
  {
   "sha": "25b0878cff5948d6d164962e26df8750bb9a01be",
   "date": "2025-07-23T17:34:21Z",
   "message": "wallet: Add exposesecret RPC"
  },
  {
   "sha": "27bef167f0458a6042ecefcf8c0d2894cbcfe940",
   "date": "2025-07-23T17:34:21Z",
   "message": "Add recoverwalletfromseed RPC"
  },
  {
   "sha": "14f3342262ae7667f389bfc50c787e4689f25a1b",
   "date": "2025-07-23T18:17:22Z",
   "message": "Add codex32 functional test"
  }
 ],
 "timeline": [
  {
   "t": "2025-07-23T14:35:12Z",
   "kind": "comment",
   "who": "apoelstra",
   "assoc": "CONTRIBUTOR",
   "text": "[quoted text omitted]\n\nIf I understand you correctly, the intent is that an attacker needs the *wallet passphrase* in addition to the seed. But the seed is the seed -- once an attacker has it, it's game over. He doesn't need the passphrase or any other part of the wallet to sweep coins.\n\ncodex32 itself does not have any notion of passphrases or encryption."
  },
  {
   "t": "2025-07-23T15:57:33Z",
   "kind": "comment",
   "who": "w0xlt",
   "assoc": "CONTRIBUTOR",
   "text": "Yes, you're right. I thought the passphrase deterministically modified the seed to create the BIP 32 Extended Key, but that's not the case.\nhttps://github.com/bitcoin/bitcoin/blob/73e754bd01b0653d1fda2d947fcaed0742da81c3/src/wallet/wallet.cpp#L3542\n\nSo the passphrase doesn't provide any additional security in this case."
  },
  {
   "t": "2025-07-23T16:25:43Z",
   "kind": "comment",
   "who": "w0xlt",
   "assoc": "CONTRIBUTOR",
   "text": "This could still be implemented\u2014even as part of this PR.\n\nThe wallet generates the seed, stores it, and then derives the BIP32 master key by deterministically combining the seed with the user-provided passphrase. This approach allows users to back up the raw seed while still requiring the passphrase for wallet recovery.\n\nOr am I missing something ?"
  },
  {
   "t": "2025-07-23T17:34:31Z",
   "kind": "force_push",
   "who": "w0xlt",
   "commit": "c61039482788365a09bdd18732024d7e6ccac758"
  },
  {
   "t": "2025-07-23T18:17:37Z",
   "kind": "force_push",
   "who": "w0xlt",
   "commit": "14f3342262ae7667f389bfc50c787e4689f25a1b"
  },
  {
   "t": "2025-07-24T19:20:05Z",
   "kind": "comment",
   "who": "Sjors",
   "assoc": "MEMBER",
   "text": "How does this relate to https://github.com/bitcoin/bitcoin/pull/27351 / #32652? It's probably easier to support import before export."
  },
  {
   "t": "2025-07-24T20:12:39Z",
   "kind": "comment",
   "who": "w0xlt",
   "assoc": "CONTRIBUTOR",
   "text": "The emphasis of this PR is on backup functionality: a backup feature isn\u2019t very useful unless you can also restore the wallet reliably.\n\nBy contrast, the other PRs aim only to import a Codex32 secret\u2014presumably from Core Lightning into Bitcoin Core\u2014and each uses a different method.\nThe approach here creates a fresh wallet and rescans the chain, whereas PR \u202f#32652 appears to convert a Codex32 secret into an xpub via the proposed `addhdkey` RPC."
  },
  {
   "t": "2025-07-25T06:39:29Z",
   "kind": "comment",
   "who": "Sjors",
   "assoc": "MEMBER",
   "text": "It might be useful to open an issue to discuss paper backups in general.\n\nI think conceptually the are three different things to backup, and each has different requirements and frequencies:\n\n1. Private key material: backed up once at wallet creation time  (or never, in some multisig setups where recovery consists of using the other keys and moving to a fresh wallet). Needs to be kept secure against theft.\n2. Output descriptors: for anything more complicated than a [BIP48 multisig](https://github.com/bitcoin/bips/blob/master/bip-0048.mediawiki) setup you have to keep track of this, or you'll never find your coins. New descriptors may be added later in the life of a wallet (e.g. for our wallet it happened when taproot support was added, albeit in a deterministic way). Backup happens at wallet creation time and each time a new descriptor is added. Needs to be kept secure for privacy reasons, but not for theft\n3. Transaction meta data (labels, etc), see https://github.com/bitcoin/bips/blob/master/bip-0329.mediawiki. Backups happens frequently. Needs to be kept secure for privacy reasons, but also may need to be imported into bookkeeping software. Printing it may be useful so you can still look things up 20 years from now when who knows what the world looks like and what software people use.\n\nThis division borrows heavily from the motivation section of: https://delvingbitcoin.org/t/a-simple-backup-scheme-for-wallet-accounts/1607\n\nIt seems to me that codex32 only plays a role in (1). And so importing it should be done with `addhdkey`. Exporting could be done with `gethdkeys`.\n\nIt may be nice to have a more user friendly way to do all or some of (1), (2) and (3). But that I would probably write a markdown document or a Python script. Outside of this project I could imagine a nice tool that calls various RPC methods and generates a pretty PDF, though I would not recommend that for (1)."
  },
  {
   "t": "2025-08-21T15:54:28Z",
   "kind": "comment",
   "who": "BenWestgate",
   "assoc": "CONTRIBUTOR",
   "text": "[quoted text omitted]\n\nNot part of the BIP-0093 spec.\n\nIf a passphrase is used it becomes a nested 2 of 2(passphrase, codex32 secret) causing funds loss if the passphrase is forgotten. Which is more likely than lost shares if the passphrase offers enough entropy to be secure (4+ `EFF_large` words w/ 4 rounds of 1GB argon2id KDF.\n\nIn my project [BAILS](https://github.com/BenWestgate/Bails): I derive one codex32 share from a memorized passphrase using Argon2id to create a hybrid brain wallet shamir secret sharing scheme that is resilient against memory loss if n > t + 1.\n\nA passphrase on the secret is how SLIP-39 does it, similar to what you proposed."
  },
  {
   "t": "2025-08-21T16:05:22Z",
   "kind": "comment",
   "who": "BenWestgate",
   "assoc": "CONTRIBUTOR",
   "text": "[quoted text omitted]\n\nIt would be a usability and compatibility improvement to begin exporting `hdseed` in the codex32 secret format; as:\n\n1. Easier to type, write and speak than Base58.\n2. Error correcting, hand-verifiable checksum\n3. Easier to extract the payload seed bytes without needing base58 conversions.\n\nThe 4 character identifier when exporting `hdseed` as a codex32 secret should be the BIP-0032 fingerprint of `hdseed`.\n\nRationale: This default assists users in locating the correct codex32 backup for their wallets, should be distinct for every master seed users may need to disambiguate and, as it is widely stored, improves overall error correction."
  },
  {
   "t": "2025-10-13T08:12:10Z",
   "kind": "comment",
   "who": "BenWestgate",
   "assoc": "CONTRIBUTOR",
   "text": "[quoted text omitted]\n\nWallets would need to begin storing the seed by default for `exposesecret` to function.\n\nThis feature also should allow specifying the identifier for disambiguation and the threshold if secret sharing, possibly the number of shares as well, but that requires new entropy.\n\nIt may be simpler to initially support only `recoverwalletfromseed` for an empty wallet and not store it after.\n\nLastly, this should support recovering from valid sets of codex32 shares not just codex32 secrets.\n\n[quoted text omitted]\nSorry I haven't reviewed the code but we need the timestamp no? Other approaches made the import/restore command require a corresponding descriptor to function and that seems more robust for existing wallets.\n\n[quoted text omitted]\nThe functionality is highly desirable. And the sooner the most basic support is included the ROI on other features like `exposesecret`, recovering private keys for existing wallets or generating sets of shares is greater."
  },
  {
   "t": "2026-04-08T14:26:24Z",
   "kind": "comment",
   "who": "rkrux",
   "assoc": "CONTRIBUTOR",
   "text": "[quoted text omitted]\n\nI have not looked at the PR in detail yet but leaving some quick initial points because feedback is asked for.\n\nConcept a-c-k for the overall idea of allowing non-electronic (paper-based) wallet backups.\n\nIt appears that a paper based backup is highly desirable given other wallets in the space have been providing one since many years that has led to a standard that's become ubiquitous. Now that legacy wallets are out of the way completely and there is only one kind of wallet to focus on, it can be considered to add paper based backups that don't need to replace the electronic backup that the wallet already has (at least not initially) but can instead complement it one way or the other to start with."
  }
 ],
 "labels_log": [
  {
   "t": "2025-07-23T09:36:19Z",
   "action": "labeled",
   "label": "CI failed",
   "who": "DrahtBot"
  },
  {
   "t": "2025-08-16T00:36:20Z",
   "action": "labeled",
   "label": "Needs rebase",
   "who": "DrahtBot"
  },
  {
   "t": "2025-09-26T12:00:01Z",
   "action": "unlabeled",
   "label": "CI failed",
   "who": "maflcko"
  }
 ],
 "state_log": [
  {
   "t": "2025-07-23T16:00:06Z",
   "kind": "convert_to_draft",
   "who": "w0xlt"
  }
 ],
 "text_chars": 7869,
 "text_tokens_estimate": 1967,
 "changed_paths": [
  "src/bech32.cpp",
  "src/bech32.h",
  "src/bench/wallet_balance.cpp",
  "src/bench/wallet_create.cpp",
  "src/bench/wallet_create_tx.cpp",
  "src/interfaces/wallet.h",
  "src/qt/test/addressbooktests.cpp",
  "src/qt/test/wallettests.cpp",
  "src/qt/walletcontroller.cpp",
  "src/rpc/client.cpp",
  "src/wallet/CMakeLists.txt",
  "src/wallet/codex32.cpp",
  "src/wallet/codex32.h",
  "src/wallet/interfaces.cpp",
  "src/wallet/load.cpp",
  "src/wallet/rpc/backup.cpp",
  "src/wallet/rpc/wallet.cpp",
  "src/wallet/test/CMakeLists.txt",
  "src/wallet/test/codex32_tests.cpp",
  "src/wallet/test/coinselector_tests.cpp",
  "src/wallet/test/group_outputs_tests.cpp",
  "src/wallet/test/util.cpp",
  "src/wallet/wallet.cpp",
  "src/wallet/wallet.h",
  "src/wallet/walletdb.cpp",
  "src/wallet/walletdb.h",
  "src/wallet/wallettool.cpp",
  "src/wallet/walletutil.h",
  "test/functional/test_runner.py",
  "test/functional/wallet_avoidreuse.py",
  "test/functional/wallet_codex32.py"
 ],
 "files": [
  {
   "path": "src/bech32.cpp",
   "add": 5,
   "del": 5
  },
  {
   "path": "src/bech32.h",
   "add": 3,
   "del": 0
  },
  {
   "path": "src/bench/wallet_balance.cpp",
   "add": 1,
   "del": 1
  },
  {
   "path": "src/bench/wallet_create.cpp",
   "add": 1,
   "del": 1
  },
  {
   "path": "src/bench/wallet_create_tx.cpp",
   "add": 2,
   "del": 2
  },
  {
   "path": "src/interfaces/wallet.h",
   "add": 1,
   "del": 1
  },
  {
   "path": "src/qt/test/addressbooktests.cpp",
   "add": 1,
   "del": 1
  },
  {
   "path": "src/qt/test/wallettests.cpp",
   "add": 1,
   "del": 1
  },
  {
   "path": "src/qt/walletcontroller.cpp",
   "add": 1,
   "del": 1
  },
  {
   "path": "src/rpc/client.cpp",
   "add": 3,
   "del": 0
  },
  {
   "path": "src/wallet/CMakeLists.txt",
   "add": 1,
   "del": 0
  },
  {
   "path": "src/wallet/codex32.cpp",
   "add": 420,
   "del": 0
  },
  {
   "path": "src/wallet/codex32.h",
   "add": 63,
   "del": 0
  },
  {
   "path": "src/wallet/interfaces.cpp",
   "add": 2,
   "del": 2
  },
  {
   "path": "src/wallet/load.cpp",
   "add": 1,
   "del": 1
  },
  {
   "path": "src/wallet/rpc/backup.cpp",
   "add": 263,
   "del": 0
  },
  {
   "path": "src/wallet/rpc/wallet.cpp",
   "add": 7,
   "del": 1
  },
  {
   "path": "src/wallet/test/CMakeLists.txt",
   "add": 1,
   "del": 0
  },
  {
   "path": "src/wallet/test/codex32_tests.cpp",
   "add": 366,
   "del": 0
  },
  {
   "path": "src/wallet/test/coinselector_tests.cpp",
   "add": 1,
   "del": 1
  },
  {
   "path": "src/wallet/test/group_outputs_tests.cpp",
   "add": 1,
   "del": 1
  },
  {
   "path": "src/wallet/test/util.cpp",
   "add": 2,
   "del": 2
  },
  {
   "path": "src/wallet/wallet.cpp",
   "add": 82,
   "del": 15
  },
  {
   "path": "src/wallet/wallet.h",
   "add": 20,
   "del": 7
  },
  {
   "path": "src/wallet/walletdb.cpp",
   "add": 85,
   "del": 0
  },
  {
   "path": "src/wallet/walletdb.h",
   "add": 4,
   "del": 0
  },
  {
   "path": "src/wallet/wallettool.cpp",
   "add": 1,
   "del": 1
  },
  {
   "path": "src/wallet/walletutil.h",
   "add": 3,
   "del": 0
  },
  {
   "path": "test/functional/test_runner.py",
   "add": 1,
   "del": 0
  },
  {
   "path": "test/functional/wallet_avoidreuse.py",
   "add": 2,
   "del": 2
  },
  {
   "path": "test/functional/wallet_codex32.py",
   "add": 92,
   "del": 0
  }
 ],
 "test_lines": 484,
 "git": {
  "head": "14f3342262ae7667f389bfc50c787e4689f25a1b",
  "head_matches_backup": true,
  "base": "b53fab1467fde73c40402e2022b25edfff1e4668",
  "commits": [
   {
    "sha": "8b80a59893",
    "subject": "wallet: Add codex32 files",
    "files": 7,
    "add": 859,
    "del": 5
   },
   {
    "sha": "2d6b910820",
    "subject": "wallet: Store seeds in database",
    "files": 7,
    "add": 173,
    "del": 5
   },
   {
    "sha": "25b0878cff",
    "subject": "wallet: Add exposesecret RPC",
    "files": 2,
    "add": 147,
    "del": 0
   },
   {
    "sha": "27bef167f0",
    "subject": "Add recoverwalletfromseed RPC",
    "files": 18,
    "add": 174,
    "del": 45
   },
   {
    "sha": "14f3342262",
    "subject": "Add codex32 functional test",
    "files": 2,
    "add": 93,
    "del": 0
   }
  ],
  "patch_truncated": true
 },
 "input_hash": "596f4d2bea232fe7",
 "extracted_at": "2026-09-17T16:15:31+00:00"
}