BIP draft: Unspendable Internal Keys for Wallet Policies #2340

pull 2099999997690000 wants to merge 8 commits into bitcoin:master from bitryonix:bip-unspendable-internal-keys changing 17 files +5671 −0
  1. 2099999997690000 commented at 6:43 AM on November 1, 2026: none

    This proposes _ as an unspendable Taproot internal-key placeholder in wallet policies. Wallets and signers can reproduce the internal key from the policy and derivation path, without keeping a separate random tweak.

    The construction uses the BIP 341 NUMS point as the public key of a synthetic BIP 32 root. Its chain code commits to the normalized policy, including both the keys and the policy structure. Child keys are then derived using unhardened BIP 32 derivation.

    This builds on the Delving discussion and the earlier work in #1746. I shared an earlier draft on the Bitcoin Development Mailing List.

    I've included a Python reference implementation, fixed JSON test vectors, and instructions for running the checks. All 21 offline tests pass, and the export and derivation cross-check passes all 20 cases against Bitcoin Core 31.0.

  2. first commit version 0.1.0 567848cfa4
  3. added bitcoindev link b636b8ec09
  4. duplicated bitcoindev post to correct the bad title 2c8a3ba22d
  5. Merge branch 'bitcoin:master' into bip-xxxx-unspendable-internal-keys 72c8d77ead
  6. reference implementation added and some editorial changes on the bip file 0f4bab28f8
  7. corrected bip name e18f81a2ac
  8. jonatack added the label New BIP on Nov 1, 2026
  9. jonatack renamed this:
    Add BIP: Unspendable Internal Keys for Wallet Policies
    BIP draft: Unspendable Internal Keys for Wallet Policies
    on Nov 1, 2026
  10. in bip-unspendable-internal-keys.mediawiki:30 in e18f81a2ac outdated
      25 | +
      26 | +The vendored secp256k1lab files are licensed under the [[bip-unspendable-internal-keys/secp256k1lab/COPYING|MIT license]].
      27 | +
      28 | +== Motivation ==
      29 | +
      30 | +A Taproot policy that uses a fixed unspendable internal key produces outputs that are recognizable when script-path spends reveal that key. [[bip-0341.mediawiki#constructing-and-spending-taproot-outputs|BIP 341]] recommends ''H + rG'' with a fresh ''r''. Current implementations use various approaches, such as taking entropy from the keys or choosing and retaining a random ''r''. Deriving the public tweaks from a policy commitment lets independent implementations reproduce the internal key from a policy backup, without additional secret material. Committing to the policy's structure also distinguishes descriptors that use the same keys under different spending conditions.
    


    jonatack commented at 9:07 PM on November 1, 2026:

    Perhaps worth a line on the post-quantum verification property described in https://groups.google.com/g/bitcoindev/c/se3TkNnbno4/m/NC5TeBFrBAAJ (anyone with the policy can re-derive the internal key and prove the key path was unspendable).

    A Taproot policy that uses a fixed unspendable internal key produces outputs that are recognizable when script-path spends reveal that key. [[bip-0341.mediawiki#constructing-and-spending-taproot-outputs|BIP 341]] recommends ''H + rG'' with a fresh ''r''. Current implementations use various approaches, such as taking entropy from the keys or choosing and retaining a random ''r''. Deriving the public tweaks from a policy commitment lets independent implementations reproduce the internal key from a policy backup, without additional secret material. Committing to the policy's structure also distinguishes descriptors that use the same keys under different spending conditions.
    +
    +Anyone holding the policy can also re-derive the internal key and thereby prove the key path was unspendable. This can be useful in post-quantum settings or for exposure classification.
    

  11. in bip-unspendable-internal-keys.mediawiki:131 in e18f81a2ac outdated
     126 | +
     127 | +== Reference implementation and test vectors ==
     128 | +
     129 | +A [[bip-unspendable-internal-keys/reference.py|reference implementation for Python 3.10 or later]] and standalone [[bip-unspendable-internal-keys/vectors.json|JSON test vectors]] are provided. The implementation is for demonstration and interoperability testing, not production use.
     130 | +
     131 | +The recursive parser and normalizer may exceed Python's recursion limit and raise <code>RecursionError</code> on deeply nested inputs. This is a resource limitation of the reference implementation.
    


    jonatack commented at 9:07 PM on November 1, 2026:
     The recursive parser and normalizer may exceed Python's recursion limit and raise <code>RecursionError</code> on deeply nested inputs. This is a resource limitation of the reference implementation.
    +
    +Production implementations should use iterative parsing (or raise the limit) if deep nesting is expected.
    

  12. in bip-unspendable-internal-keys.mediawiki:121 in e18f81a2ac
     116 | +
     117 | +The policy and selected derivation are enough to reproduce the internal key. Seed backups alone do not reconstruct the policy.
     118 | +
     119 | +== Backward compatibility ==
     120 | +
     121 | +Descriptor consumers that support the exported expressions can use the exported form without recognizing <code>_</code>. Export does not guarantee acceptance by a BIP 388 wallet-policy consumer: for example, this construction permits records with equal public keys and different chain codes, while BIP 388 requires pairwise distinct public keys in its key-information vector.
    


    jonatack commented at 9:08 PM on November 1, 2026:

    Maybe an example would make the BIP 388 rejection clearer.

     Descriptor consumers that support the exported expressions can use the exported form without recognizing <code>_</code>. Export does not guarantee acceptance by a BIP 388 wallet-policy consumer: for example, this construction permits records with equal public keys and different chain codes, while BIP 388 requires pairwise distinct public keys in its key-information vector.
    +
    +An export that lists the same public key under two different chain codes will be rejected by a strict BIP 388 consumer.
    

    2099999997690000 commented at 5:11 AM on November 2, 2026:

    Expanded this paragraph to cover duplicate public keys, repeated MuSig indexes, and overlapping derivation paths, with a link to the relevant BIP 388 rules.

    https://github.com/bitcoin/bips/blob/a2175a1e336eddb80562173bc8863bbaf2269932/bip-unspendable-internal-keys.mediawiki?plain=1#L123

    Also added tests covering them:

    https://github.com/bitcoin/bips/blob/a2175a1e336eddb80562173bc8863bbaf2269932/bip-unspendable-internal-keys/tests.py#L140-L193

    Thank you.

  13. jonatack commented at 9:21 PM on November 1, 2026: member

    Looks good and complete. Ran the tests and check_core.py.

    <details><summary>Test run and check_core run</summary><p>

    (e18f81a2...):~/bitcoin/bips$ python3 -B bip-unspendable-internal-keys/tests.py
    test_all_key_permutations (__main__.ConstructionTests.test_all_key_permutations) ... ok
    test_compressed_parity_is_preserved_between_steps (__main__.ConstructionTests.test_compressed_parity_is_preserved_between_steps) ... ok
    test_equivalences_and_distinctions (__main__.ConstructionTests.test_equivalences_and_distinctions) ... ok
    test_export_preserves_input_and_indexes (__main__.ConstructionTests.test_export_preserves_input_and_indexes) ... ok
    test_fixed_vectors (__main__.ConstructionTests.test_fixed_vectors) ... ok
    test_full_records_control_musig_sorting (__main__.ConstructionTests.test_full_records_control_musig_sorting) ... ok
    test_invalid_vectors (__main__.ConstructionTests.test_invalid_vectors) ... ok
    test_manual_preimages (__main__.ConstructionTests.test_manual_preimages) ... ok
    test_network_only_changes_root_version_and_checksum (__main__.ConstructionTests.test_network_only_changes_root_version_and_checksum) ... ok
    test_no_multipath_has_one_expansion (__main__.ConstructionTests.test_no_multipath_has_one_expansion) ... ok
    test_normalization_idempotence (__main__.ConstructionTests.test_normalization_idempotence) ... ok
    test_record_count_compactsize_boundary (__main__.ConstructionTests.test_record_count_compactsize_boundary) ... ok
    test_recorded_core_results_match_vectors (__main__.ConstructionTests.test_recorded_core_results_match_vectors) ... ok
    test_stops_at_first_invalid_child (__main__.ConstructionTests.test_stops_at_first_invalid_child) ... ok
    test_template_length_compactsize_boundary (__main__.ConstructionTests.test_template_length_compactsize_boundary) ... ok
    test_base58check (__main__.PrimitiveTests.test_base58check) ... ok
    test_bip32_public_derivation (__main__.PrimitiveTests.test_bip32_public_derivation) ... ok
    test_compactsize (__main__.PrimitiveTests.test_compactsize) ... ok
    test_invalid_children_and_zero_tweak (__main__.PrimitiveTests.test_invalid_children_and_zero_tweak) ... ok
    test_invalid_points (__main__.PrimitiveTests.test_invalid_points) ... ok
    test_nums_point_and_fingerprint (__main__.PrimitiveTests.test_nums_point_and_fingerprint) ... ok
    
    ----------------------------------------------------------------------
    Ran 21 tests in 0.758s
    
    OK
    jon|(e18f81a2...):~/bitcoin/bips$ python3 -B bip-unspendable-internal-keys/check_core.py
    {
      "core_version": "/Satoshi:32.1.0/",
      "network": "regtest",
      "checks": [
        {
          "name": "single",
          "expansion": 0,
          "index": 0,
          "internal_key": "c3fdf3b6a04fafa134e5d0c8f9926bf45e3377ae4f27d1d26a880f357bdeb904",
          "addresses": [
            "bcrt1pcf34kqg3n4v0zev84rswat52ysz0a3kgyvlw83pqnz84lcuqq20sek65cu"
          ]
        },
        {
          "name": "single",
          "expansion": 1,
          "index": 0,
          "internal_key": "bf113123683375fd195a4f0df956937fdc5a393d1091900aacba81b3cf77a97b",
          "addresses": [
            "bcrt1paj5g4g8qczcqpnautjwsrmdujud5ylzc3vlweunzxwn69pgq5sjqqrqvgz"
          ]
        },
        {
          "name": "single",
          "expansion": 0,
          "index": 2147483647,
          "internal_key": "a51f4adcf9999994ea499a81d6ff3b2390ee567d57486c81ecc0aed2d140f5b9",
          "addresses": [
            "bcrt1pv5lxmsgnz2855t0c4fje370pl9spdj2q83gptq4sxtaltd5fauqsmnph6w"
          ]
        },
        {
          "name": "single-expanded",
          "expansion": 0,
          "index": 0,
          "internal_key": "c3fdf3b6a04fafa134e5d0c8f9926bf45e3377ae4f27d1d26a880f357bdeb904",
          "addresses": [
            "bcrt1pcf34kqg3n4v0zev84rswat52ysz0a3kgyvlw83pqnz84lcuqq20sek65cu"
          ]
        },
        {
          "name": "single-expanded",
          "expansion": 0,
          "index": 1,
          "internal_key": "66aa19a3b73ca4058d910da7c95cd89e96b7f116a7608b0207d034a03da2a16d",
          "addresses": [
            "bcrt1pgph6mhru3w8s56y5h4lnxeszy4ax0pn7hxfzxl3yvj69fmcetgcqxv0lnv"
          ]
        },
        {
          "name": "four-expansions",
          "expansion": 0,
          "index": 0,
          "internal_key": "82c15b1dd33d5dc1b491bc16a38c5a9c70fbfd00534101d79404c9a5972cca9e",
          "addresses": [
            "bcrt1pcr9frk0qn4vjxmgescfsn3my8t2s0fnjpsnvdjqcfz50qgf6y24qx5gsaa"
          ]
        },
        {
          "name": "four-expansions",
          "expansion": 1,
          "index": 0,
          "internal_key": "65dba4f2e2e730df7cbe8b1d8b8435ca9ee4dfc36d933deb4a9564c392446c97",
          "addresses": [
            "bcrt1pp64wha9y4eeh4kfq3sc8wkfysck690l5g5n6ra3py8h0nt0gyvaqttk5ps"
          ]
        },
        {
          "name": "four-expansions",
          "expansion": 3,
          "index": 17,
          "internal_key": "0f7611b6c84637620caa3f1d7e6db6837187362f45fad0acab86430e82249dff",
          "addresses": [
            "bcrt1p98zgq9q9wtukvtcupyzxzd7f62e9edxf3jklj8ddgrs9n3rzjxzsld0dvs"
          ]
        },
        {
          "name": "sorted",
          "expansion": 0,
          "index": 0,
          "internal_key": "fcea15adb7040910c1dc9c4339045e2647578a216b7ad3cea7fa8a2a86f70ad0",
          "addresses": [
            "bcrt1pl3upn3aw0c0td9y0c78q8qz8px04f7ydqvnpxjgqutefm90rxats9wqpy8"
          ]
        },
        {
          "name": "sorted",
          "expansion": 0,
          "index": 1,
          "internal_key": "87ffa4a79a969cc397fdafcf2dbd7b17040ba22dd6f1cf42fb35830630471fe6",
          "addresses": [
            "bcrt1pmz0aq2ylxmzlcg0ht042ng6a456qkjtrahvjkuwmxd2z97j2mm8qhv40v8"
          ]
        },
        {
          "name": "sorted-permuted",
          "expansion": 0,
          "index": 0,
          "internal_key": "fcea15adb7040910c1dc9c4339045e2647578a216b7ad3cea7fa8a2a86f70ad0",
          "addresses": [
            "bcrt1pl3upn3aw0c0td9y0c78q8qz8px04f7ydqvnpxjgqutefm90rxats9wqpy8"
          ]
        },
        {
          "name": "sorted-permuted",
          "expansion": 0,
          "index": 1,
          "internal_key": "87ffa4a79a969cc397fdafcf2dbd7b17040ba22dd6f1cf42fb35830630471fe6",
          "addresses": [
            "bcrt1pmz0aq2ylxmzlcg0ht042ng6a456qkjtrahvjkuwmxd2z97j2mm8qhv40v8"
          ]
        },
        {
          "name": "musig",
          "expansion": 0,
          "index": 0,
          "internal_key": "be56170918b1939bdd676aab8750ed7ce5183fc8216276c361c3d6335eca0f57",
          "addresses": [
            "bcrt1pr8yxfqf0kadcu43at4svt7rckc7wcwz6drcvgeud2q6t0lppt4fqjcy0fr"
          ]
        },
        {
          "name": "musig",
          "expansion": 0,
          "index": 1,
          "internal_key": "1f4b7a9af7e7e11263c91aa5b68a22b32139020729e7cdea0bc0bd49cebbc0a1",
          "addresses": [
            "bcrt1pr9f55lxma77y5m5pjvc6z7t0c8zezjsk4y0qaufsxtfj24u46pls08w0cy"
          ]
        },
        {
          "name": "tree",
          "expansion": 0,
          "index": 0,
          "internal_key": "01a9ed75464a9fdc15dd77ac565d39e48a4e30179da2a65fbe08c7823837fdd5",
          "addresses": [
            "bcrt1plp0rw0egy7a086nynmq8nwajm5f9acsyzcyqj4h8ks9tyuf8ga4qsgkvsw"
          ]
        },
        {
          "name": "tree",
          "expansion": 0,
          "index": 1,
          "internal_key": "63209dc77a6cfedcc6a2f983d689f3e1331d449db8b827f0786cff1f27cf6a8c",
          "addresses": [
            "bcrt1pzmr5fdal78aq5w6rmfpgtg5en3l78xek8dg4yhvc63ln7h5jpuzslpxk3d"
          ]
        },
        {
          "name": "tree-swapped",
          "expansion": 0,
          "index": 0,
          "internal_key": "01a9ed75464a9fdc15dd77ac565d39e48a4e30179da2a65fbe08c7823837fdd5",
          "addresses": [
            "bcrt1plp0rw0egy7a086nynmq8nwajm5f9acsyzcyqj4h8ks9tyuf8ga4qsgkvsw"
          ]
        },
        {
          "name": "tree-swapped",
          "expansion": 0,
          "index": 1,
          "internal_key": "63209dc77a6cfedcc6a2f983d689f3e1331d449db8b827f0786cff1f27cf6a8c",
          "addresses": [
            "bcrt1pzmr5fdal78aq5w6rmfpgtg5en3l78xek8dg4yhvc63ln7h5jpuzslpxk3d"
          ]
        },
        {
          "name": "timelock",
          "expansion": 0,
          "index": 0,
          "internal_key": "ed2c0c0f14e63b15f9302782b74654b8a7808085e210906fb663d3c242883436",
          "addresses": [
            "bcrt1pz0f3cnsend3832wjc29l8g0kfxgfl05nlezhhvyf9m5jcngv6fuskqmwd5"
          ]
        },
        {
          "name": "timelock",
          "expansion": 0,
          "index": 1,
          "internal_key": "432bffa5c9ea820bd654662756d052d2d19153610b99ce445249e45714c56356",
          "addresses": [
            "bcrt1pxduf9wpjquelxsp6yuru9g6sdtvzp0fct5ph4lm9wmyqqgaynqksdppysq"
          ]
        }
      ]
    }
    

    </p></details>

  14. jonatack added the label Needs number assignment on Nov 1, 2026
  15. fametrano commented at 1:12 AM on November 2, 2026: none

    ACK e18f81a2ac192f8c59da7fe9cc2795309b8dd343

    I implemented normalization, the commitment and the export from the BIP text, without the reference code, on btclib-org's BIP 32 and hashing code (btclib-wallet, btclib, ecc), which I maintain. This adds to the runs of tests.py and check_core.py above an implementation that shares no code with the reference. It reproduces every intermediate value of all 51 valid vectors, the 105 internal keys and the 51 exports, agrees with the 11 equivalent groups and 13 distinct pairs, and rejects the 50 invalid vectors at their stated stage. btclib-wallet's descriptor parser reads back the exports of 42 valid vectors and derives the same 86 internal keys; it refuses the leaves of the other 9 as not sane or not satisfiable. Its addresses match the 20 Bitcoin Core results in core-checks.json, and gen_vectors.py prints vectors.json byte for byte. Scripts and output: https://gist.github.com/fametrano/d8966a1925a4f8d1800c05f2422d365a/d26bc4f8de24a38d6389cdfc3e7c2e4c7e4a5f40

  16. added @jonatack suggestions to bip file and added tests demonstrating this bip may process descriptors that bip388 rejects a2175a1e33
  17. fametrano commented at 9:25 AM on November 2, 2026: none

    ACK a2175a1e336eddb80562173bc8863bbaf2269932

    Since e18f81a2 only the BIP text and tests.py changed; reference.py, vectors.json and core-checks.json are byte-identical, so the btclib-org reproduction in my earlier review still holds: https://gist.github.com/fametrano/d8966a1925a4f8d1800c05f2422d365a/d26bc4f8de24a38d6389cdfc3e7c2e4c7e4a5f40. On this head tests.py passes its 24 tests, gen_vectors.py still prints vectors.json byte for byte, and the three cases the new Backward compatibility paragraph lists are the ones BIP 388's Additional rules forbid.

    Not blocking: when the policy has keys, the export gives the root the last index but places it first, as in tr(@2/**,{pk(@0/**),pk(@1/**)}), while the same Additional rules say the key vector should be ordered so that @0 appears first; the paragraph could say so. And "Production implementations should use iterative parsing (or raise the limit)" now comes before the RecursionError paragraph, so "the limit" is not yet defined there; it belongs after it.

  18. fixed an editorial misplacement and an implementation discrepancy raised at https://github.com/bitcoin/bips/pull/2340#pullrequestreview-5734401612 9a4073fcd0
  19. 2099999997690000 commented at 4:06 AM on November 3, 2026: none

    ACK a2175a1

    Since e18f81a only the BIP text and tests.py changed; reference.py, vectors.json and core-checks.json are byte-identical, so the btclib-org reproduction in my earlier review still holds: https://gist.github.com/fametrano/d8966a1925a4f8d1800c05f2422d365a/d26bc4f8de24a38d6389cdfc3e7c2e4c7e4a5f40. On this head tests.py passes its 24 tests, gen_vectors.py still prints vectors.json byte for byte, and the three cases the new Backward compatibility paragraph lists are the ones BIP 388's Additional rules forbid.

    Not blocking: when the policy has keys, the export gives the root the last index but places it first, as in tr(@2/**,{pk(@0/**),pk(@1/**)}), while the same Additional rules say the key vector should be ordered so that @0 appears first; the paragraph could say so. And "Production implementations should use iterative parsing (or raise the limit)" now comes before the RecursionError paragraph, so "the limit" is not yet defined there; it belongs after it.

    Thanks, addressed both points in 9a4073f.

    The exporter now assigns the synthetic root to @0 and numbers the other placeholders by first appearance. Your example now exports as tr(@0/**,{pk(@1/**),pk(@2/**)}):

    https://github.com/bitcoin/bips/blob/9a4073fcd0a8edcdd25af8378d7d1630bd3313a5/bip-unspendable-internal-keys/reference.py#L175-L192

    I added a regression test covering the ordering across all 51 valid vectors and updated the expected exports. The resolved descriptors and derived internal keys are unchanged. All 25 tests pass.

    https://github.com/bitcoin/bips/blob/9a4073fcd0a8edcdd25af8378d7d1630bd3313a5/bip-unspendable-internal-keys/tests.py#L142-L153

    Also moved the production guidance after the explanation of the recursion limit:

    https://github.com/bitcoin/bips/blob/9a4073fcd0a8edcdd25af8378d7d1630bd3313a5/bip-unspendable-internal-keys.mediawiki?plain=1#L134


github-metadata-mirror

This is a metadata mirror of the GitHub repository bitcoin/bips. This site is not affiliated with GitHub. Content is generated from a GitHub metadata backup.
generated: 2026-11-03 19:10 UTC

This site is hosted by @0xB10C
More mirrored repositories can be found on mirror.b10c.me