BIP93: Generalize codex32 format for any hrp and fix typos #2040

pull BenWestgate wants to merge 5 commits into bitcoin:master from BenWestgate:bip93-fix-threshold changing 1 files +209 −95
  1. BenWestgate commented at 6:36 am on November 22, 2025: none

    Summary of Changes: Describe codex32 format for arbitrary human-readable parts not just “ms”, specify master seed encoding standard, add new test vectors and enhance readability. This makes the document more like BIP-0173: proposing an encoding “codex32”, then defining a standard for something using it.

    See discussion on #2023 (comment).

    Spec:

    • fixed the threshold mistake in the abstract
    • replaced “master seed” with “secret”, prior to the “Master seed format” section and made descriptions hrp general
    • updated the checksum reference code to produce valid checksums for any hrp
    • change t to k to match the test vectors and book
    • defined “ms” codex32 secrets:
      • using terms “secret seed” (as the book does) and “codex32-encoded master seed” to refer to “ms” codex32 secrets
      • recommended using first 4 characters of the bech32-encoded fingerprint as the identifier
      • recommended the padding bits be set with a CRC code for extra error detection. Provided reference code for this checksum.

    Test Vectors:

    • Fixed the cornucopia of naming conventions in the Test vectors
      • used mostly “secret seed”, “codex32 secret”, and “codex32-encoded X”.
    • Fixed test vector 5 which did not actually append a long checksum to “random” data as the text said it would.
    • Added vector 6 encoding a “cl” prefix codex32-encoded HSM secret, then relabels the identifier (producing a new checksum and codex32-encoded HSM secret)
    • Added vector 7 which parses a “cl” prefix codex32 secret and decodes the HSM secret
    • Clarified why invalid prefix test vectors were bad (their checksum is for “ms” but their prefix is not “ms”)
    • We might want to add one that uses “cl” with the old “ms” checksum code as that will now fail with the updated ms32_verify_checksum function
  2. Generalize codex32 format for any hrp and fix typos
    Clarify codex32 format for different hrp values, specify master seed encoding standard, add new test vectors and enhance readability.
    c6f8bd07a6
  3. Revert title for BIP93 document aedb912bd1
  4. jonatack added the label Proposed BIP modification on Nov 22, 2025
  5. jonatack added the label Pending acceptance on Nov 22, 2025
  6. in bip-0093.mediawiki:140 in aedb912bd1
    142+guarantees detection of '''any error affecting at most 8 characters'''
    143+and has less than a 3 in 10<sup>19</sup> chance of failing to detect more
    144+errors. The human-readable part is processed by first
    145+feeding the higher bits of each character's US-ASCII value into the
    146+checksum calculation followed by a zero and then the lower bits of each<ref>'''Why are the high bits of the human-readable part processed first?'''
    147+This results in the actually checksummed data being ''[high hrp] 0 [low hrp] [data]''. This means that under the assumption that errors to the
    


    roconnor commented at 5:18 pm on November 24, 2025:

    The lengths limitations of the codex32 strings are working under the assumption that the HRP is not subject to error correction. We more or less cannot do that anyways as all sorts of various bech32 formats have appeared all with different checksums and characteristics. In order to run the checksum algorithm you have to know the prefix first in order to know which checksum algorithm to try.

    This isn’t really a problem in practice since there are only a small finite number of prefixes, and from context only a few are going to be applicable anyways.


    BenWestgate commented at 1:03 am on November 25, 2025:

    This was copied over from BIP-00173. Delete it?

    Bech32 attempts to decode two checksums, a universal bech32 decoder could try decoding the string with the bech32, bech32m and codex32 checksums to discover the format. Unless covering the HRP exceeds the max length at HD=9, 2 subsitutions in the HRP will always be detected by every format.

    If HRP is swapped between formats the chances of false verification is:

    • 1 in 2^65 for a “codex32 checksum” validating when the encoding was Bech32/Bech32m

    • ~1 in 2^30 for “Bech32 checksum” validating when the encoding was Codex32.


    roconnor commented at 4:35 pm on November 25, 2025:

    This text was certainly the design goal of BIP-173, but we are not using their checksum, and we haven’t realized this part of their design in codex32 in part because our 13 character checksum unfortunately works only on relatively short strings.

    Instead we process this HRP in this way because that is what BIP-173 does, and we still want the HRP to change the residue to catch random errors, so me might as well do it in the standard way.

    Unless covering the HRP exceeds the max length at HD=9, 2 subsitutions in the HRP will always be detected by every format.

    The problem is that our particular 13 character checksum’s max length for its error detection and correction properties is limited to 93 bech32 characters. That’s why our payload is limited to 74 characters add in 13 character checksum and 6 characters for the header and we get 93 bech32 characters, with nothing left over to detect or correct errors in the HRP. Yes, in cases where the payload is 72 characters or less, our error correction / detection properties extend to the low 5 bits of the ascii characters of a 2 character prefix, but that doesn’t apply to 73 or 74 character payloads.

    I don’t know if we really want to get into these subtleties. I’m not even sure correcting and detecting errors in the HRP is useful to begin. If you are a hardware wallet expecting a master seed and someone gives you a “cl” codex32 string, you don’t need a fancy error correction algorithm to detect the “cl” prefix is wrong; if it is a expecting a master seed then the “cl” prefix must be wrong.


    BenWestgate commented at 5:18 pm on November 25, 2025:

    I will delete the footmark and say: “The human-readable part is processed as per BIP-0173.”

    I updated the rationale accordingly:

    At this length, the human-readable part is not covered by the checksum. This is acceptable because the checksum scheme itself requires you to know that a valid human-readable part is being used in the first place. If the prefix is damaged and a user is guessing that the data might be using this scheme, then the user can enter the available data explicitly using the suspected prefix.

    I’m not even sure correcting and detecting errors in the HRP is useful to begin

    wallets.md import guidance prefills the prefix in applications expecting only one to prevent mistakes.

    A future application for extended keys (Is long codex32 HD=9 for 74 bytes?) has a situation where decoders need to accept both “xprv” and “xpub” HRPs in the same descriptor. So here it is absolutely useful to detect and correct errors in the HRP.

    However we should not go into details about that until such an application needing to disambiguate different HRP actually exists.


    roconnor commented at 5:59 pm on November 25, 2025:
    What does HD=9 mean?

    BenWestgate commented at 6:05 pm on November 25, 2025:
    Hamming distance 9, able to detect 8 substitution errors. (and correct 4)

    roconnor commented at 6:32 pm on November 25, 2025:
    Yeah the 15 character checksum has HD=9 up to a maximum length 1023, supports data up to 1008 = (1023 - 15) characters and a payload of 1002 (=1008 - 6 header characters) bech32 characters. If you want to cover the HRP you lose payload capacity.

    BenWestgate commented at 6:54 pm on November 25, 2025:
    Plenty of room to long codex32 encode extended keys with covered human-readable parts “xpub”, “xprv”, “tprv” and “tpub” someday. They’re horrible to handle and type in base58. Is this something we should think about in a separate PR?

    BenWestgate commented at 9:42 am on November 26, 2025:

    I ran the numbers here and we must further restrict HRP lengths to 72 or 74 (same maximum as the payload). At len(hrp) = 74 two header characters two header characters aren’t checked but what does a threshold or share index even mean for a 0 byte payload?

    I also explicitly stated the maximum length for a codex32 string is 96. Although perhaps at length = 96 the HRP we assume “ms” while the max length for all other prefixes is 94?


    roconnor commented at 4:24 pm on November 26, 2025:

    Since we are excluding the HRP from the error-correction and detection routine, the HRP can be as long as folks want. The HRP just adds a “random” factor to the checksum as a function of the HRP text and the number of characters in the data. And by “random” I really mean deterministic.

    The way the error correction algorithm works is that it finds locations of errors starting from the end of the string (i.e. the end of the checksum) going back as far as but no further than 93 characters. So when the hrp-expanded segement + the data segment is longer than 93 characters, the algorithm simply fails to locate errors at any location beyond the last 93 characters.

    When the hrp-expanded segement + the data segment is longer than 93 characters the hamming distance property still holds on the last 93 characters. That is if you only change the last 93 characters you must change at least 9 characters in order to get another valid string.


    BenWestgate commented at 7:03 pm on November 26, 2025:

    The HRP is included in the error detection routine. That’s why I’ve restricted the length to 94 characters if hrp != “ms”. In the 96 length case we assume it’s “ms”

    Would you like to not have any length restriction and not promise to detect errors in the HRP?

    That sounds strictly worse, since we don’t have to support any length of HRP, we can restrict it to ones that do get covered by the 13 character checksum, and that is better if a lot of them proliferate and some future application needs to disambiguate two short checksums.


    roconnor commented at 8:59 pm on November 26, 2025:

    Would you like to not have any length restriction and not promise to detect errors in the HRP?

    That’s what I was thinking. There would just be a 93 data character limit and no correction of the HRP. This is how the limits of BIP-93 was designed.

    That sounds strictly worse

    I’m struggling to see how error correction is useful for the HRP. You need to already know the HRP in order to know select the codex32 error correction algorithm to begin with. CL wallets only take codex32 strings with the “cl” prefix. Hardware wallets only take codex32 strings beginning with “ms”. If you cannot read the prefix you won’t even know if it is a bip-173 address or a codex32 string or someone else’s algorithm. If you find a piece of paper laying around with “xd10lueasd35kw6r5de5kueedxyesqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqanvrktzhlhus” what are you expecting folks to do with it? Is it a bitcoin address, is it a private key, is it some other coin’s something?

    There is a simple error correction algorithm: just try replacing the prefix all the prefixes that you care about.


    BenWestgate commented at 9:15 pm on November 26, 2025:

    Let’s use a descriptor for an example. Say these prefixes get registered “xpub”, “xprv”, “privkey” or whatever.

    Now we have an import field that should be able to correct hrp errors.

    In case we typed or wrote “xpub” for “xprv” or vice versa.

    It would be best to have error detection guarantees (and corrections) for the HRP instead of the 1/(2^65) sort. And to get that, the checksum needs to be covered.

    In this case the app can try “xpub”, “xprv” or “privkey” and detect or repair transcription errors in these strings.


    BenWestgate commented at 9:53 pm on November 26, 2025:

    Why don’t we keep it as is: a maximum length of 96 and then say that HRP’s may further restrict length. For example how “ms” restricts to 16-64 bytes or “cl” 32.

    Because the two current applications do not need to disambiguate HRP’s, but future ones may need to, in which case they should shorten the max length to 94 to cover HRP.


    roconnor commented at 10:12 pm on November 26, 2025:

    Let’s use a descriptor for an example. Say these prefixes get registered “xpub”, “xprv”, “privkey” or whatever.

    Now we have an import field that should be able to correct hrp errors.

    In case we typed or wrote “xpub” for “xprv” or vice versa.

    I agree that for applications that can accept more than one different HRPs in the same context, it makes sense to make sure error detection can cover them and those applications will need their own BIPs to cover their own length restrictions. My point is that no one is mixing up master seeds with addresses.

    It would be best to have error detection guarantees (and corrections) for the HRP instead of the 1/(2^65) sort. And to get that, the checksum needs to be covered.

    The situation is actually much worse than the 1/(2^65) sort.

    ms1aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaady97ykdtray0m

    has two corrections, either

    ms1aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaady97ykdtraypy

    or

    cl1aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaady97ykdtray0m

    (Edit: yes ‘a’ is an invalid threshold, but you know what I mean. The point is that for the 93 character data part, mistaking the ms prefix for a cl prefix is the same as making a two character error in the checksum.)

    The math is that x^93 == 1 modulo our (short) codex32 generator, which means errors at location n are indistinguishable from errors at location n+93.

    Ugh, I’m starting to think you are right. The total length of strings must be 94 characters or less, with an exception that an ms string can be 96 characters. Even a length restriction of 94 characters isn’t going to be enough unless we also restrict the HRP to use only letters and disallow numbers (we could also accept underscores and perhaps a very small handful of carefully selected special ASCII characters).

    And maybe (in a separate PR) we should outlaw unusual sized master seed strings too to get rid of even that “ms” exception if we can agree on a definition of unusual sized.


    BenWestgate commented at 10:25 pm on November 26, 2025:
    Speaking of those multi-hrp applications, my reviewer on the bip85 codex app is working on one such application (BIP32 encodings) #1958 (comment)

    roconnor commented at 11:10 pm on November 26, 2025:

    Okay we have a choice of making a complicated rule like “the string length plus the length of the hrp (i.e double counting the hrp) must be less than or equal to 93” or we can have simple rule like “the length of the string must be less than or equal to 94” but also restricting the hrp character set to at most 32 values where the lower 5 bits of the ASCII character are all distinct.

    Given how length constrained our 13 character checksum is, I think we should restrict the HRP character set. I’d go for “a-z” and special characters @_[|]~, but the choice of special characters is debatable (but our options are limited). However, we will have to pick some.

    We have a choice between @ and `, of which I think @ is preferable

    We have a choice between ? and _ , of which I think _ is preferable. In particular I don’t think we want ? as a legal HRP character.

    We have a choice between ;, [, and {. We have a choice between <, , and |. We have a choice between =, ] and }. We have a choice between >, ^, and ~. I don’t have very strong preferences within these options.


    roconnor commented at 11:35 pm on November 26, 2025:
    Actually I’m not sure even this restriction is enough. We might need to require hrps only consist of ascii characters between 96 and 126 (or their upper case variants) so that all their upper bits are always 011 in order to be safe.

    roconnor commented at 11:41 pm on November 26, 2025:
    @apoelstra already has a prefix, “BIP32_24W” which mixes up letter and numbers in violation of my proposal.

    roconnor commented at 11:55 pm on November 26, 2025:

    @apoelstra ’s BIP32_24W has 82 length strings with at 9 character hrp. 82 + 9 = 91 which is just barely less than 93.

    Okay I guess the total length of a string + the length of the hrp (.i.e. double counting the hrp) must be less than or equal to 93 should be the rule for using the 13 character checksum.


    roconnor commented at 0:16 am on November 27, 2025:
    Perhaps a better way of phrasing this is that the maximum length of string (including the HRP) for a given codex scheme is 93 - the length of the HRP.

    BenWestgate commented at 0:24 am on November 27, 2025:

    The situation is actually much worse than the 1/(2^65) sort.

    Ah yes, I forgot how fragile ECC is when pushed beyond it’s capacity.

    (…The point is that for the 93 character data part, mistaking the ms prefix for a cl prefix is the same as making a two character error in the checksum.)

    Well “cl” fortunately has a 32-byte length requirement (and we should probably start talking about length limits as payload bytes unless we want to support pure u5 secrets that do not need conversion to bytes) but we do need to plan for the eventual case of detecting HRP errors at the maximum 13 character checksum length.

    Ugh, I’m starting to think you are right. The total length of strings must be 94 characters or less, with an exception that an ms string can be 96 characters.

    95 characters has an incomplete group decoding to bytes so I can remove the “95 or” 96.

    Even a length restriction of 94 characters isn’t going to be enough unless we also restrict the HRP to use only letters and disallow numbers (we could also accept underscores and perhaps a very small handful of carefully selected special ASCII characters).

    What do we need to restrict it to in order to allow every US-ASCII character? Keeping in mind it expands to two 5 bit values but only the upper 2 bits can change so we should have better detection ability than 10 bits per character.

    ie 5 US-ASCII characters should be similar to 7 data part characters.

    Does that mean 52 US-ASCII character limit on the hrp for short codex32 strings? That sounds fine. Although now we get a maximum length formula like strings can’t exceed (len(hrp) + 4) * 7 // 5 + len(payload) <= 74

    We also have restrictions within our own codex32 header that can be exploited to lend slightly more correction power to the human-readable part: 9 out of 32 values for threshold. Threshold 0 only has 1/32 share indices.

    That might get it to 53 US-ASCII, I didn’t run the numbers.

    And maybe (in a separate PR) we should outlaw unusual sized master seed strings too to get rid of even that “ms” exception if we can agree on a definition of unusual sized.

    I gave a very good argument to justify outlawing lengths besides 128, 192, 256, 512, or put another looser way, lengths not divisible by 64 bits. Any closer has input lengths within the insert/delete correctable distance of two valid lengths and causes ambiguity.

    This can be an “ms” specific recommendation (or requirement) I don’t think every codex32 application needs to be as strict on lengths if they have a good reason to support variable length encodings. Although it would be worth recommending this 64 bit spacing to all applications in case their developers are unaware of the harm it causes insert/delete correction.

    The final concern on this topic is that the maximum hrp length is really a derivative of the maximum checksummed payload length, which is 370 bits. If we spend 70 bits on a 10 US-ASCII HRP, there’s only 300 bits, 60 bech32 characters left for the secret. or a 50 character HRP supports a payload length of 4 bech32 characters (2 bytes).

    This is making me want to roll the HRP into the CRC, then we get an extra 1-4 bits of error detection on it, which in some cases will extend the permitted HRP length by 1. OTOH for pure secret data recovery purposes, it’s stronger if it just checksums the seed, although it barely matters since it can’t correct any bit errors at the 128 or 256 bitlengths


    BenWestgate commented at 0:32 am on November 27, 2025:

    Given how length constrained our 13 character checksum is, I think we should restrict the HRP character set. I’d go for “a-z” and special characters @_[|]~, but the choice of special characters is debatable (but our options are limited). However, we will have to pick some.

    This is too breaking of BIP-0173 spec, just allow the entire range and accept a shorter string. If an application needs a super long HRP then either we don’t allow it (83 maximum in BIP-0173, although must be lower here for short codex32) or they use the long codex32 checksum which is good for 730 HRP characters.

    Do we want to avoid a situation where strings of the same length and different HRP may be using different checksums, or if we don’t care (i don’t care) we can choose the checksum according to combined length:

    We pack the bech32_hrp_expand(hrp) + data and then use that to determine checksum_len, that would eliminate our HRP length requirement being smaller than BIP-0173’s, do you agree with this solution?

    I’m sure there’s some “hrp insert/delete” correction assumption nightmares for error correcting decoders but applications that need to disambiguate SHOULD at least make all the HRP’s they must consider the same length to avoid this mess.

    I’m also fine with treating each HRP character as two bech32 characters for maximum length purposes, even though we can probably do better than that (even using bech32_hrp_expand()) due to each character only being a 7-bit value.


    BenWestgate commented at 0:41 am on November 27, 2025:

    Okay I guess the total length of a string + the length of the hrp (.i.e. double counting the hrp) must be less than or equal to 93 should be the rule for using the 13 character checksum.

    I like this. Can we do better than double counting the HRP since each US-ASCII is 7- not 10 bits? Or will our detection guarantees begin to break down (not able to correct any 2 HRP substitutions or detect any 4 HRP errors if its length exceed 37s, with an empty data part)?

    I guess the pure 7-bit character translation of our guarantees is: correct any 2 hrp errors, detect any 5 errors, 9 if contiguous for short codex32 (10 if contiguous on long codex32)

    Can we achieve this property simply? We should if we can.


    roconnor commented at 0:41 am on November 27, 2025:

    What do we need to restrict it to in order to allow every US-ASCII character? Keeping in mind it expands to two 5 bit values but only the upper 2 bits can change so we should have better detection ability than 10 bits per character.

    The issue is that for BCH codes the data really needs to fit within their length restriction, so we cannot just count entropy. Even if we know some some bits are fixed, if the polynomial we extract has degree more that 93, everything falls apart because it can no longer distinguish errors on one side of the polynomial from errors on the other side of the polynomial. The rules that the the string length plus counting the hrp again must be less than 93 is the only thing that makes the HRP expanded polynomial concatenated with the data part fit in degree at most 93.


    BenWestgate commented at 10:15 pm on November 27, 2025:

    If we REQUIRE every registered HRP be unique in the lower 5 bits then:

    1. we don’t have to ever distinguish errors in the high bits they’re a lookup table, VALID_HRP.
    2. the expanded data we covered by the checksum will be < 93 single counting hrp characters.
    3. With the valid HRP table, Correcting errors in the low bits, corrects any errors in the high bits.
    4. We know which checksum is being used by the length of the string, which is far simpler than a per hrp (impossible) design or one that double weights hrp characters towards max_length.

    roconnor commented at 10:26 pm on November 27, 2025:

    With the valid HRP table, Correcting errors in the low bits, corrects any errors in the high bits.

    It’s not that simple. For maximum length codex32 string, errors in the high bits appear as errors in the checksum at the end of the string because for BCH codes, any polynomial longer than the maximum length (93 in our case) effectively wraps around.

    Edit: And errors in the high bits are not going to be uncommon. Let me tell you the number of times I’ve mistaken a 5 for an S.


    BenWestgate commented at 10:41 pm on November 27, 2025:
    Then let’s guarantee to detect 8 errors, 13/15 if contiguous in the low bits, BIP-0173 style but not error correct the HRP. Trying different suspected HRPs will have to be the way to correct a damaged prefix, like our rationale suggests doing.

    roconnor commented at 11:18 pm on November 27, 2025:
    mistaking a 5 for an S in the HRP counts as two errors.

    roconnor commented at 11:34 pm on November 27, 2025:
    BIP-0173 error detection doesn’t tell you where the errors are. In particular it doesn’t tell whether the HRP is correct or not. You need to invoke the error correction to find locations.
  7. in bip-0093.mediawiki:341 in aedb912bd1
    337+** A conversion of the 16-to-64-byte BIP-0032 HD master seed to bech32:
    338+*** Start with the bits of the master seed, most significant bit per byte first.
    339+*** Re-arrange those bits into groups of 5, and pad with arbitrary bits at the end if needed.
    340+*** Translate those bits to characters using the bech32 character table from BIP-0173.
    341+
    342+When padding bits are needed they should be generated using CRC polynomial <code>(1 << pad_len) | 3</code> with an initial value of <code>0</code> and appended to the master seed bits. Note that unlike the codex32 checksums, we do NOT include the header data.
    


    roconnor commented at 5:25 pm on November 24, 2025:

    I don’t really want this CRC stuff in the standard. At most it is a recommendation that folks MAY use to select padding bits. but I doubt it will be useful in practice since you cannot know if any given codex32 master seed was generated with this CRC or with random padding as the codex32 book does. If one’s seed is so corrupted that the codex32 error correction wasn’t able to fix it, I’m skeptical a few more bits will help.

    If it is include than padding MAY be random should also be stated. Perhaps it is better to move this to a separate PR if we want to further discuss it.


    BenWestgate commented at 4:08 am on November 25, 2025:

    I’ll replace it with one sentence for now:

    Encoders MAY select padding using a CRC-w where: w = pad_bits_needed, poly = 1 << w | 3, init = 1, const = 1 << w - 1, refIn = false, and refOut = false.

    CRC-4 helps 32-byte seeds detect 94% of 1 character errors/deletions and narrows 1 erasure down to 2 candidates. The most common substitutions in CHARSET are 1 bit apart so actual performance will exceed that.

    since you cannot know if any given codex32 master seed was generated with this CRC or with random padding

    Finding the book is reasonable suspicion for random padding IF all electronically encoded seeds use CRC. The person who made the backup certainly knows if they encoded with the book or not.

    as the codex32 book does

    A book insert could compute CRCs by hand, but I have not divided a large enough number to benchmark time for 128-bits, Andrew and I determined the space requirement is two pieces of 11 x 8.5 graph paper.

    If it is include than padding MAY be random should also be stated.

    Decoders, MUST accept random padding, although they may someday warn on it.

    For electronic encoders, trusting RNGs introduces risk: it can leak up to 32 bits to an attacker with 8 shares and breaks decode->encode round-trip. Using zero padding round-trips but leaks the final payload character to an attacker with (5-pad_len) / pad_len interpolated shares.

    CRC pad minimizes RNG trust by using entropy already present in the payload bytes.

    Perhaps it is better to move this to a separate PR if we want to further discuss it.

    Lets discuss on my deterministic codex32 BIP85 PR where it’s required since reviewers asked me to directly encode bytes not u5 ints even for share payloads.


    roconnor commented at 4:42 pm on November 25, 2025:
    I’d still rather this be in a separate PR and in a separate section on recommendations for determinisitic generation.

    BenWestgate commented at 4:53 pm on November 25, 2025:
    Should it be a PR to this BIP93 document or to BlockstreamResearch/codex32 wallets.md?

    roconnor commented at 6:35 pm on November 25, 2025:
    Maybe hammering out the details at codex32_wallets.md makes the most sense. Then we can decide if we want to include it in BIP-93, or maybe just make a reference to the codex32_wallets.md.
  8. in bip-0093.mediawiki:119 in aedb912bd1
    116 
    117-def ms32_verify_checksum(data):
    118+def bech32_hrp_expand(s):
    119+  return [ord(x) >> 5 for x in s] + [0] + [ord(x) & 31 for x in s]
    120+
    121+def ms32_verify_checksum(hrp, data):
    


    roconnor commented at 5:26 pm on November 24, 2025:
    If you want an hrp parameter, you have to rename this function to something like codex32_verify_checksum.

    BenWestgate commented at 4:29 am on November 25, 2025:
    Will do
  9. in bip-0093.mediawiki:85 in aedb912bd1
    84 ** A checksum which consists of 13 bech32 characters as described below.
    85 
    86 As with bech32 strings, a codex32 string MUST be entirely uppercase or entirely lowercase.
    87 For presentation, lowercase is usually preferable, but uppercase SHOULD be used for handwritten codex32 strings.
    88 If a codex32 string is encoded in a QR code, it SHOULD use the uppercase form, as this is encoded more compactly.
    89+The lowercase form is used when determining a character's value for checksum purposes.
    


    roconnor commented at 5:27 pm on November 24, 2025:
    This doesn’t make sense. The lowercase form and uppercase form of Bech32 characters have the same value.

    BenWestgate commented at 4:13 am on November 25, 2025:

    Not for HRP which needs to be lower cased during decoding or bech32_hrp_expand(hrp) would return a different result.

    This line is repeated from the test vectors, why explain the rules about case in the vectors instead of up here?


    roconnor commented at 4:38 pm on November 25, 2025:
    I guess we should reword this to make it more clear that the relevance is for the HRP.

    BenWestgate commented at 8:41 pm on November 25, 2025:
    “When constructing or verifying a checksum, the human-readable part MUST be interpreted in lowercase, as specified in BIP-0173.”

    roconnor commented at 11:19 pm on November 25, 2025:
    I might say “MUST be converted to lowercase” instead.

    BenWestgate commented at 5:06 am on November 26, 2025:

    That seems to imply mutating the string when verifying a checksum.

    Something BIP-93 omitted was that encoders should always emit lower-case strings. Did we relax that requirement?

    Currently I have the sentence as: “Encoders MUST emit lowercase; decoders MUST reject mixed-case and MUST lowercase the human-readable part during checksum verification.”

    And I am adding a section with a codex32_encode and codex32_decode definitions as I think it’s easier to see these rules in code than english.

    Uppercase/lowercase

    The lowercase form is used when determining a character’s value for checksum purposes.

    Encoders MUST always output an all lowercase Bech32 string. If an uppercase version of the encoding result is desired, (e.g.- for presentation purposes, or QR code use), then an uppercasing procedure can be performed external to the encoding process.

    Decoders MUST NOT accept strings where some characters are uppercase and some are lowercase (such strings are referred to as mixed case strings).

    For presentation, lowercase is usually preferable, but inside QR codes uppercase SHOULD be used, as those permit the use of alphanumeric mode, which is 45% more compact than the normal byte mode.


    roconnor commented at 3:52 pm on November 26, 2025:

    As far as I’m concerned both all lowercase and all uppercase strings are valid, so encoders can produce either format with lowercase is generally preferred. I’m not really sure what BIP-173 thinks it is achieving by talking about encoders being somewhat different from a post-processing step. Maybe they are just trying to say that when creating a checksum, of course, a lowercase HRP must be used.

    That seems to imply mutating the string when verifying a checksum.

    This is exactly what the BIP-173 reference python decoder does:

    https://github.com/sipa/bech32/blob/master/ref/python/segwit_addr.py#L78

    However, “The lowercase form is used …” is also fine wording though.


    BenWestgate commented at 7:08 pm on November 26, 2025:
    “Decoders MUST use the lowercase form of the human-readable part during checksum verification.”
  10. in bip-0093.mediawiki:551 in aedb912bd1
    547@@ -481,10 +548,49 @@ The payload contains 103 bech32 characters, which corresponds to 515 bits. The l
    548 
    549 This is an example of a '''Long codex32 String'''.
    550 
    551-* Secret share with index <code>S</code>: <code>MS100C8VSM32ZXFGUHPCHTLUPZRY9X8GF2TVDW0S3JN54KHCE6MUA7LQPZYGSFJD6AN074RXVCEMLH8WU3TK925ACDEFGHJKLMNPQRSTUVWXY06FHPV80UNDVARHRAK</code>
    552-* Master secret (hex): <code>dc5423251cb87175ff8110c8531d0952d8d73e1194e95b5f19d6f9df7c01111104c9baecdfea8cccc677fb9ddc8aec5553b86e528bcadfdcc201c17c638c47e9</code>
    553+unchecksummed string (bech32): <code>MS10C8VSM32ZXFGUHPCHTLUPZRY9X8GF2TVDW0S3JN54KHCE6MUA7LQPZYGSFJD6AN074RXVCEMLH8WU3TK925ACDEFGHJKLMNPQRSTUVWXY06F</code>
    


    roconnor commented at 5:36 pm on November 24, 2025:

    I’d be included to remove this uncheckedsummed string. I’m really nervous displaying strings without a checksum anywhere. They are very problematic.

    If you insist on going into this much detail in this test vector I’d say use the following bullets

    • Master seed (hex):
    • master node xprv
    • Payload
    • HRP
    • Identifier
    • Checksum
    • Secret seed

    That’s the order I’d use, but maybe some other permutations are also good.


    BenWestgate commented at 4:27 am on November 25, 2025:

    How about since the text said:

    This example shows generating a new 512-bit master seed using “random” codex32 characters and appending a checksum.

    human-readable part: MS k value: 0 identifier: 0C8V share index: S payload: M32ZXFGUHPCHTLUPZRY9X8GF2TVDW0S3JN54KHCE6MUA7LQPZYGSFJD6AN074RXVCEMLH8WU3TK925ACDEFGHJKLMNPQRSTUVWXY06F

    • checksum: HPV80UNDVARHRAK
    • secret seed: MS100C8VSM32ZXFGUHPCHTLUPZRY9X8GF2TVDW0S3JN54KHCE6MUA7LQPZYGSFJD6AN074RXVCEMLH8WU3TK925ACDEFGHJKLMNPQRSTUVWXY06FHPV80UNDVARHRAK
    • Master seed (hex): dc5423251cb87175ff8110c8531d0952d8d73e1194e95b5f19d6f9df7c01111104c9baecdfea8cccc677fb9ddc8aec5553b86e528bcadfdcc201c17c638c47e9
    • master node xprv: xprv9s21ZrQH143K4UYT4rP3TZVKKbmRVmfRqTx9mG2xCy2JYipZbkLV8rwvBXsUbEv9KQiUD7oED1Wyi9evZzUn2rqK9skRgPkNaAzyw3YrpJN

    No information is displayed we did not already in Vector 1.


    roconnor commented at 4:40 pm on November 25, 2025:
    That would be better.

    BenWestgate commented at 8:46 pm on November 25, 2025:

    Would you rather just change V5 text to match master’s vectors?

    From:

    This example shows generating a new 512-bit master seed using “random” codex32 characters and appending a checksum.

    To:

    This example shows the long codex32 format, when used without splitting the secret into any shares.


    roconnor commented at 9:21 pm on November 25, 2025:
    I somewhat prefer the current text.

    BenWestgate commented at 7:19 pm on November 26, 2025:

    I reverted it

    We start given

    k value = 0 identifier = 0C8V payload =

    then compute

    • checksum
    • secret seed
    • Master seed
    • master node xprv

    We are able to infer share index = “s” and hrp = “MS” from the text.

    FWIW I have been using the term “secret seed” when a codex32 secret is arrived at from bech characters (interpolation or randomly selected) and I have been using the term codex32-encoded master seed when it’s produced from bytes.

    This is slightly more precise but we need not bother readers with the distinction since both are valid.

  11. in bip-0093.mediawiki:334 in aedb912bd1
    330+
    331+* The human-readable part "ms" for master seed.
    332+* The data-part values:
    333+** A threshold parameter, which MUST be a single digit between "2" and "9", or the digit "0".
    334+** An identifier consisting of 4 bech32 characters.
    335+*** We recommend the first 4 characters of the bech32-encoded BIP-0032 key fingerprint.
    


    roconnor commented at 5:53 pm on November 24, 2025:

    When some shares of a master seed are compromised, a user may wish to simply dispose of remaining shares and rederive a new set of secret shares without the cost of sweeping their wallet. In such a case a user very much should use a fresh identifier so that they do not get mix up their obsolete share data with their fresh shares.

    At best a hardware wallet may suggest such an identifier, but only when the hardware wallet is generating a fresh master seed and thus knows that there are no other secret shares for the same secret floating around.

    Again, maybe put this recommendation into a separate PR, perhaps including more details and test vectors.


    BenWestgate commented at 6:19 am on November 25, 2025:

    This is a good case for BIP85 derive codex32 application to avoid trusting or generating randomness for this.

    If we want a default identifier for “reshares” too:

    0identifier = fingerprint(master_seed)[:2] + fingerprint(false_seed)[2:4]
    

    Where false_seed is recovered from fresh initial shares (reducing k if needed).

    During the first generation with k fresh shares; the two slices together produce the full fingerprint. If the identifier is unspecified, recommend this default for master seeds. Or if that collides, the next higher.

    Again, maybe put this recommendation into a separate PR, perhaps including more details and test vectors.

    Already done here. Agreed wrt details and vectors.

    https://github.com/BenWestgate/bips/blob/a8f8e98d05a2183aba395f8f8ff479b4fb764f95/bip-0085.mediawiki#unshared-secret

    #1958

    I can move this recommendation (and the padding rec) into a separate BIP93 PR pending the final BIP85 codex32 design (needs finishing touches feedback). I’ll remove it from this PR so it remains focused on typos and general HRP support.

  12. in bip-0093.mediawiki:484 in aedb912bd1
    482-* Master secret (hex): <code>d1808e096b35b209ca12132b264662a5</code>
    483+* Recovered secret seed with index <code>S</code>: <code>MS12NAMES6XQGUZTTXKEQNJSJZV4JV3NZ5K3KWGSPHUH6EVW</code>
    484+* Master seed (hex): <code>d1808e096b35b209ca12132b264662a5</code>
    485 * master node xprv: <code>xprv9s21ZrQH143K2NkobdHxXeyFDqE44nJYvzLFtsriatJNWMNKznGoGgW5UMTL4fyWtajnMYb5gEc2CgaKhmsKeskoi9eTimpRv2N11THhPTU</code>
    486 
    487 Note that per BIP-0173, the lowercase form is used when determining a character's value for checksum purposes.
    


    BenWestgate commented at 6:24 pm on November 25, 2025:

    @roconnor why do we have spec notes in the test vectors?

    This was what I was trying to make unnecessary with the earlier sentence about case and checksum.


    roconnor commented at 6:44 pm on November 25, 2025:
    I don’t know. Before we were using a fixed constant for the “ms” prefix, so this text wasn’t really necessary. Now that we want to support more general HRPs, I agree that we need some wording like this somewhere.

    BenWestgate commented at 8:49 pm on November 25, 2025:
    I removed it and added wording like this to the end of the codex32 Spec section.
  13. Rename ms32 functions to codex32, remove recommendations, clarify HRP case in checksum a4f1e91ad9
  14. in bip-0093.mediawiki:485 in aedb912bd1
    483+* Recovered secret seed with index <code>S</code>: <code>MS12NAMES6XQGUZTTXKEQNJSJZV4JV3NZ5K3KWGSPHUH6EVW</code>
    484+* Master seed (hex): <code>d1808e096b35b209ca12132b264662a5</code>
    485 * master node xprv: <code>xprv9s21ZrQH143K2NkobdHxXeyFDqE44nJYvzLFtsriatJNWMNKznGoGgW5UMTL4fyWtajnMYb5gEc2CgaKhmsKeskoi9eTimpRv2N11THhPTU</code>
    486 
    487 Note that per BIP-0173, the lowercase form is used when determining a character's value for checksum purposes.
    488 In particular, given an all uppercase codex32 string, we still use lowercase <code>ms</code> as the human-readable part during checksum construction.
    


    BenWestgate commented at 6:42 pm on November 25, 2025:

    If I add this to spec: “When constructing or verifying a checksum, the human-readable part MUST be interpreted in lowercase, as specified in BIP-0173.”

    Then we can remove this.

  15. in bip-0093.mediawiki:63 in a4f1e91ad9
    57@@ -59,67 +58,84 @@ However, BIP-0039 has no error-correcting ability, cannot sensibly be extended t
    58 
    59 ==Specification==
    60 
    61+We first describe the general checksummed base32<ref>'''Why use base32 at all?''' The lack of mixed case makes it more
    62+efficient to read out loud, write, type or to put into QR codes.</ref> format called
    63+''codex32'' and then define the BIP-0032 master seed encoding using it.
    


    BenWestgate commented at 10:10 pm on November 25, 2025:

    Should it be:

    and then define a BIP-0032 master seed encoding using it.

    ? Because this is only one encoding of master seeds, SLIP-39 and WIF are others, we should use “a” not “the”.

  16. in bip-0093.mediawiki:70 in a4f1e91ad9
    68 It reuses the base-32 character set from BIP-0173, and consists of:
    69-
    70-* A human-readable part, which is the string "ms" (or "MS").
    71-* A separator, which is always "1".
    72+* A human-readable part, which is intended to convey the type of data, or anything else that is relevant to the reader. This part MUST contain 1 to 83 US-ASCII characters, with each character having a value in the range [33-126]. HRP validity may be further restricted by specific applications.
    73+* A separator, which is always "1". In case "1" is allowed inside the human-readable part, the last one in the string is the separator<ref>'''Why include a separator in codex32 strings?''' That way the human-readable
    


    BenWestgate commented at 10:16 pm on November 25, 2025:

    Do we need to be this wordy or should we say:

    • A human-readable part, as specified in BIP-0173, which is intended to convey the type of data, or anything else that is relevant to the reader.
    • A separator, as specified in BIP-0173, which is always “1”.
  17. in bip-0093.mediawiki:225 in a4f1e91ad9
    225+If we already have ''k'' valid codex32 strings such that:
    226 
    227-* All strings have the same threshold value ''t'', the same identifier, and the same length
    228-* All of the share index values are distinct
    229+* All strings have the same human-readable part, the same threshold value ''k'', the same identifier, and the same length.
    230+* All of the share index values are distinct.
    


    BenWestgate commented at 10:19 pm on November 25, 2025:
    remove the periods I inadvertently added to these bullets?
  18. in bip-0093.mediawiki:262 in a4f1e91ad9
    278-# Choose a threshold value ''t'' between 2 and 9, inclusive
    279+# Choose a human-readable part according to application (Use "ms" for BIP-0032 master seeds)
    280+# Choose a threshold value ''k'' between 2 and 9, inclusive
    281 # Choose a 4 bech32 character identifier
    282-#* We do not define how to choose the identifier, beyond noting that it SHOULD be distinct for every master seed the user may need to disambiguate.
    283+#* We do not define how to choose the identifier, beyond noting that it SHOULD be distinct for every secret the user may need to disambiguate
    


    BenWestgate commented at 10:25 pm on November 25, 2025:
    Should we say “secret or set of shares” because this is the reshare case you mentioned that SHOULD have a unique identifier? Here we make it sound like it’s OK to reuse an identifier if the secret is the same which is false.

    roconnor commented at 10:47 pm on November 25, 2025:
    Yes, set of shares.

    BenWestgate commented at 7:26 pm on November 26, 2025:

    I used “set of shares” in for an existing secret and “secret” in for a fresh secret.

    This is technically correct, no need to say both “secret and set of shares” in existing secret, if you follow that process you always get a fresh set of shares and that is what needs to be uniquely identified not the secret per se.

  19. Fix Test vector 5, add encode/decode ref, add length limit, add clairity
    Clarify codex32 specification and examples for encoding and decoding processes, including detailed explanations of parameters and checksum handling.
    f74527ed4f
  20. Revert deleted new line 3123cead1d
  21. in bip-0093.mediawiki:264 in a4f1e91ad9
    282-#* We do not define how to choose the identifier, beyond noting that it SHOULD be distinct for every master seed the user may need to disambiguate.
    283+#* We do not define how to choose the identifier, beyond noting that it SHOULD be distinct for every secret the user may need to disambiguate
    284 # Set the share index to <code>s</code>
    285-# Set the payload to a bech32 encoding of the master seed, padded with arbitrary bits
    286-# Generating a valid checksum in accordance with the Checksum section
    287+# Set the payload to a bech32 encoding of the secret, padded with arbitrary bits
    


    BenWestgate commented at 10:30 pm on November 25, 2025:
    Change this to “secret data” or “secret bytes” to specify its encoding bytes or leave it? There may be some confusion between “secret” meaning the string with share index “s” and the decoded payload bytes of that [codex32] secret.

    roconnor commented at 10:50 pm on November 25, 2025:
    “secret data” sounds good.
  22. in bip-0093.mediawiki:401 in 3123cead1d
    397@@ -323,10 +398,10 @@ While we could use the 15 character checksum for both cases, we prefer to keep t
    398 We only guarantee to correct 4 characters no matter how long the string is.
    399 Longer strings mean more chances for transcription errors, so shorter strings are better.
    400 
    401-The longest data part using the regular 13 character checksum is 93 characters and corresponds to a 400-bit secret.
    402+The longest data part using the regular 13 character checksum is 93 characters and corresponds to a 368-bit secret.
    


    BenWestgate commented at 7:44 pm on November 26, 2025:

    The original forgot to subtract the 6 character header from the secret payload bits.

    I used 368 instead of 370 because 2 of them are padding and not secret data.

    Is this another one of those places we should say “secret data” to avoid ambiguity between the “s” string and bytes?


    roconnor commented at 9:05 pm on November 26, 2025:
    I think it is fine as is, since the “368-bit” makes it clear it is data, but if you want to change the wording, that would also be fine.
  23. in bip-0093.mediawiki:402 in 3123cead1d
    397@@ -323,10 +398,10 @@ While we could use the 15 character checksum for both cases, we prefer to keep t
    398 We only guarantee to correct 4 characters no matter how long the string is.
    399 Longer strings mean more chances for transcription errors, so shorter strings are better.
    400 
    401-The longest data part using the regular 13 character checksum is 93 characters and corresponds to a 400-bit secret.
    402+The longest data part using the regular 13 character checksum is 93 characters and corresponds to a 368-bit secret.
    403 At this length, the prefix <code>MS1</code> is not covered by the checksum.
    


    BenWestgate commented at 7:48 pm on November 26, 2025:

    This is implicit from the spec definition:

    Strings of length 95 and 96 MUST use HRP “ms” (or “MS”)

    If it needs to be explained here also. Or a sentence why the maximum length is 94 for other HRP, let me know and I’ll try.

    We could reduce maximum length to 94 characters and remove the special HRP vs length rule, but that breaks existing “46-byte codex32-encoded master seeds” and these are absolutely critical to support given the wide-spread deployment of both codex32 and 46-byte master seeds.

  24. in bip-0093.mediawiki:403 in 3123cead1d
    397@@ -323,10 +398,10 @@ While we could use the 15 character checksum for both cases, we prefer to keep t
    398 We only guarantee to correct 4 characters no matter how long the string is.
    399 Longer strings mean more chances for transcription errors, so shorter strings are better.
    400 
    401-The longest data part using the regular 13 character checksum is 93 characters and corresponds to a 400-bit secret.
    402+The longest data part using the regular 13 character checksum is 93 characters and corresponds to a 368-bit secret.
    403 At this length, the prefix <code>MS1</code> is not covered by the checksum.
    404-This is acceptable because the checksum scheme itself requires you to know that the <code>MS1</code> prefix is being used in the first place.
    405-If the prefix is damaged and a user is guessing that the data might be using this scheme, then the user can enter the available data explicitly using the suspected <code>MS1</code> prefix.
    406+This is acceptable because the checksum scheme itself requires you to know that a codex32 human-readable part is being used in the first place.
    


    BenWestgate commented at 7:50 pm on November 26, 2025:

    At this point we should link to the registry somewhere in our document so people know what a “codex32 human-readable part” might be:

    Where’s the best place to put this hyperlink? https://github.com/satoshilabs/slips/blob/master/slip-0173.md#uses-of-codex32


    roconnor commented at 9:06 pm on November 26, 2025:
    I don’t really want to be seen as endorsing a particular registry. But I also see how a link could be useful, so I’m torn.

    roconnor commented at 9:08 pm on November 26, 2025:
    Maybe best to leave the registry out since they may or may not be Bitcoin related.

    BenWestgate commented at 9:59 pm on November 26, 2025:

    https://github.com/bitcoin/bips/blob/master/bip-0173.mediawiki#user-content-Registered_Humanreadable_Prefixes

    BIP-0173 which is a prerequisite for implementing this format links to that registry.

    We don’t have to link to it as they can find it in BIP-0173 but it the concept of registering codex32 HRP should be mentioned to avoid chaos and disaster of using anything for everything.


    roconnor commented at 10:57 pm on November 26, 2025:
    Oh. Well if there is precedent then I guess it is okay.
  25. BenWestgate commented at 0:48 am on November 27, 2025: none

    Fair enough, double counting it is. That’s easy to remember and implement.

    Packing the 7-bit values together wouldn’t have helped much anyway because single ASCII errors would affect multiple data symbols especially when isolated.

    I’ll be adding your length of the string is 93 - length of the HRP.

    And we’re keeping the special rule for 96 where the HRP MUST equal “MS”?

    Sent with Proton Mail secure email.

    On Wednesday, November 26th, 2025 at 6:41 PM, roconnor @.***> wrote:

    @roconnor commented on this pull request.


    In bip-0093.mediawiki:

     polymod = ms32_polymod(values + [0] * 13) ^ MS32_CONST
    
     return [(polymod >> 5 * (12 - i)) & 31 for i in range(13)]
    

    +This implements a [https://en.wikipedia.org/wiki/BCH_code BCH code] that +guarantees detection of ‘‘‘any error affecting at most 8 characters’’’ +and has less than a 3 in 1019 chance of failing to detect more +errors. The human-readable part is processed by first +feeding the higher bits of each character’s US-ASCII value into the +checksum calculation followed by a zero and then the lower bits of each‘‘‘Why are the high bits of the human-readable part processed first?’’’ +This results in the actually checksummed data being ‘’[high hrp] 0 [low hrp] [data]’’. This means that under the assumption that errors to the

    What do we need to restrict it to in order to allow every US-ASCII character? Keeping in mind it expands to two 5 bit values but only the upper 2 bits can change so we should have better detection ability than 10 bits per character.

    The issue is that for BCH codes the data really needs to fit within their length restriction, so we cannot just count entropy. Even if we know some some bits are fixed, if the polynomial we extract has degree more that 93, everything falls apart because it can no longer distinguish errors on one side of the polynomial from errors on the other side of the polynomial. The rules that the the string length plus counting the hrp again must be less than 93 is the only thing that makes the HRP expanded polynomial concatenated with the data part fit in degree at most 93.

    — Reply to this email directly, view it on GitHub, or unsubscribe. You are receiving this because you authored the thread.Message ID: @.***>

  26. roconnor commented at 0:52 am on November 27, 2025: none

    Yes let’s keep the silly exception for now for the sake of getting a agreeable PR. We should hammer out the master seed bit-size restrictions in a separate PR.

    If you want to say that length 96 ms seeds are deprecated that’s okay too. But I still want to argue for the merits of 160 bit master seeds.

  27. in bip-0093.mediawiki:124 in 3123cead1d
    137-        return ms32_verify_long_checksum(data)
    138+        return codex32_verify_long_checksum(bech32_hrp_expand(hrp) + data)
    139     if len(data) <= 93:
    140-        return ms32_polymod(data) == MS32_CONST
    141+        return codex32_polymod(bech32_hrp_expand(hrp) + data) == CODEX32_CONST
    142     return False
    


    BenWestgate commented at 7:18 am on November 27, 2025:

    I dislike a situation where valid “long codex32” strings can be shorter overall (and in data part characters) than regular codex32.

    “long” codex32 format: 10 hrp characters + 1 + 6 header characters + 54 payload characters + 15 checksum characters = 86 codex32 format: “ms” hrp characters + 1 + 6 header characters + 74 payload characters + 13 checksum characters = 96

    So I’m now restricting the HRP length and leaving codex32_verify_checksum() alone. The checksum will remain selected as it currently is: based on the length of the data.

    The maximum HRP length will be restricted (going forward) so that any HRP is always covered by our 4 error correction guarantees if its errors only affect low (or high bits). 96 character short “ms” strings are deprecated, they decode properly but the same hrp & data will now encode with the long checksum.

    It has to be this way, if we needed the HRP to know which checksum to use, we can’t protect the HRP. If we change verify rules, we break backwards compatibility.

    0def bech32_decode(bech):
    1    """Validate a Bech32/Bech32m string, and determine HRP and data."""
    

    We must do the equivalent:

    0def codex32_decode(codex):
    1    """Validate a codex32/Long codex32 string, and determine HRP and data."""
    

    The decoder must be ignorant of HRP, because that’s the point of it, to determine it.


    roconnor commented at 4:55 pm on November 27, 2025:
    I can probably write this code out if you want, but my thoughts are we should have codex32_decode, an independent long_codex32_decode and an ms_decode that can call both of them.

    BenWestgate commented at 5:29 pm on November 27, 2025:

    That makes sense! Bech32 has an encode/decode for the format and then a separate encode/decode function for segwit addresses.

    However they do encode/decode both Bech32/Bech32m checksums at once.

    We need codex32_encode and codex32_decode function to handle both checksums. That has to be format level, not application level in order to detect/correct HRP errors.


    roconnor commented at 5:55 pm on November 27, 2025:

    Below is untested code that is approximately what I’m thinking

     0def bech32_hrp_expand(s):
     1  return [ord(x) >> 5 for x in s] + [0] + [ord(x) & 31 for x in s]
     2
     3CODEX32_CONST = 0x10ce0795c2fd1e62a
     4  
     5def codex32_polymod(residue, values):
     6    if len(values) > 93:
     7        return False
     8    GEN = [
     9        0x19dc500ce73fde210,
    10        0x1bfae00def77fe529,
    11        0x1fbd920fffe7bee52,
    12        0x1739640bdeee3fdad,
    13        0x07729a039cfc75f5a,
    14    ]
    15    for v in values:
    16        b = (residue >> 60)
    17        residue = (residue & 0x0fffffffffffffff) << 5 ^ v
    18        for i in range(5):
    19            residue ^= GEN[i] if ((b >> i) & 1) else 0
    20    return residue
    21
    22CODEX32_LONG_CONST = 0x43381e570bf4798ab26
    23    
    24def codex32_long_polymod(residue, values):
    25    if len(values) > 1023:
    26        return False
    27    GEN = [
    28        0x3d59d273535ea62d897,
    29        0x7a9becb6361c6c51507,
    30        0x543f9b7e6c38d8a2a0e,
    31        0x0c577eaeccf1990d13c,
    32        0x1887f74f8dc71b10651,
    33    ]
    34    for v in values:
    35        b = (residue >> 70)
    36        residue = (residue & 0x3fffffffffffffffff) << 5 ^ v
    37        for i in range(5):
    38            residue ^= GEN[i] if ((b >> i) & 1) else 0
    39    return residue
    40
    41def codex32_verify_checksum(hrp, data):
    42    return codex32_polymod(1, bech32_hrp_expand(hrp) + data) == CODEX32_CONST
    43    
    44def codex32_verify_long_checksum(hrp, data):
    45    return codex32_long_polymod(1, bech32_hrp_expand(hrp) + data) == CODEX32_LONG_CONST
    46
    47def codex32_create_checksum(hrp, data):
    48    polymod = codex32_polymod(1, bech32_hrp_expand(hrp) + data + [0] * 13)
    49    if polymod:
    50        polymod = polymod ^ MS32_CONST
    51        return [(polymod >> 5 * (12 - i)) & 31 for i in range(13)]
    52    return False
    53
    54def codex32_create_long_checksum(hrp, data):
    55    polymod = codex32_long_polymod(1, bech32_hrp_expand(hrp) + data + [0] * 15)
    56    if polymod:
    57        polymod = polymod ^ MS32_LONG_CONST
    58        return [(polymod >> 5 * (14 - i)) & 31 for i in range(15)]
    59    return False
    60
    61def ms32_verify_checksum(data):
    62    if len(data) >= 96:
    63        return codex32_verify_long_checksum("ms", data)
    64    return codex32_polymod(codex32_polymod(1, bech32_hrp_expand("ms")), data) == CODEX32_CONST
    65
    66def ms32_create_checksum(data):
    67    if len(data) > 80:
    68        return codex32_create_long_checksum("ms", data)
    69    polymod = codex32_polymod(codex32_polymod(1, bech32_hrp_expand("ms")), data + [0] * 13)
    70    polymod = polymod ^ CODEX32_CONST
    71    return [(polymod >> 5 * (12 - i)) & 31 for i in range(13)]
    

    As you can see, I think it is up to the particular application to handle switching between the long codex32 format and the regular codex32 format.


    BenWestgate commented at 8:41 pm on November 27, 2025:

    We can’t assume “ms” to know which checksum to verify.

    If there’s an hrp substitution error, we need to know which checksum is being used to detect/correct it, but if which checksum depends on which hrp application, instead of just codex32 string length, then we’re stuck.

    That made our length 96 exception a bug, as how can a decoder know this rule applies if it can’t detect the integrity of what determines its applicability?

    We need a codex32_decode function that if it validates it has the correct HRP or more than 8 errors, so applications can’t choose their checksum, the format defines which to use.

    Like BIP-0173’s HRP detection assumption, our error correction guarantee only applies to lower (or upper) 5 bits of HRP characters. As the swaps that produce an upper bit change are very unlikely. But we can guarantee to correct 2 “double” errors.

  28. bosshaas13131313 commented at 7:23 am on November 27, 2025: none

    No.

    On Thu, Nov 27, 2025, 2:20 AM Ben Westgate @.***> wrote:

    @.**** commented on this pull request.

    In bip-0093.mediawiki https://github.com/bitcoin/bips/pull/2040#discussion_r2567413699:

    +def codex32_verify_checksum(hrp, data): if len(data) >= 96: # See Long codex32 Strings

    •    return ms32_verify_long_checksum(data)
      
    •    return codex32_verify_long_checksum(bech32_hrp_expand(hrp) + data)
      
      if len(data) <= 93:
    •    return ms32_polymod(data) == MS32_CONST
      
    •    return codex32_polymod(bech32_hrp_expand(hrp) + data) == CODEX32_CONST
      
      return False

    Needs to become now to:

    def codex32_verify_checksum(hrp, data): combined = bech32_hrp_expand(hrp) + data if len(combined) >= 96: return codex32_verify_long_checksum(combined) if len(combined) <= 93: return codex32_polymod(combined) == CODEX32_CONST return False

    Missing:

    • the thorny zero length “ms” rule.
    • the check in codex32_decode() for the upper long codex32 length limit.

    Because of this new max length rule rule we have the curious situation where valid “long codex32” strings can actually be shorter overall (and in data part characters) than regular codex32.

    May want to rename that format any thoughts?

    Ex: “long” codex32 format: 10 hrp characters + 1 + 6 header characters + 54 payload characters + 15 checksum characters = 86 codex32 format: “ms” hrp characters + 1 + 6 header characters + 74 payload characters + 13 checksum characters = 96

    — Reply to this email directly, view it on GitHub https://github.com/bitcoin/bips/pull/2040#pullrequestreview-3513818681, or unsubscribe https://github.com/notifications/unsubscribe-auth/BNFR33IQPDGIR4ICII5KOVT362Q2BAVCNFSM6AAAAACM4BCWD6VHI2DSMVQWIX3LMV43YUDVNRWFEZLROVSXG5CSMV3GSZLXHMZTKMJTHAYTQNRYGE . You are receiving this because you are subscribed to this thread.Message ID: @.***>

  29. in bip-0093.mediawiki:347 in 3123cead1d
    377     values = data
    378-    polymod = ms32_long_polymod(values + [0] * 15) ^ MS32_LONG_CONST
    379+    polymod = codex32_long_polymod(values + [0] * 15) ^ CODEX32_LONG_CONST
    380     return [(polymod >> 5 * (14 - i)) & 31 for i in range(15)]
    381 </source>
    382 
    


    BenWestgate commented at 7:57 am on November 27, 2025:
    Probably should mention its maximum length for the correction guarantees in a sentence here, similar to what is in the checksum section. Otherwise 1024 is a magic number in the reference snippets.
  30. scgbckbone commented at 3:35 pm on November 27, 2025: contributor

    This is what I have:

    • compatibility encoding for BIP-39, allowing to Shamir split mnemonic & extended private key wallets
      • HRP: cc
      • encodes chaincode + private key of BIP-32 master extended key (64 bytes)
      • cc10zcvjs5klr60nyt8usd553sge7r5glcy2ztwfv2d2smmcs7m3mq6dduwavccnjzjchlkffjfx8p3cjjx64q9vkxdt8q9qzuu3s8jfgjysa5pc5nezf2qkfhqpfwf

    I only have one HRP, I do not differentiate between testnet/mainnet, even tho I use extended key data, I’m also using it for menmonics, where I first generate master BIP-32 key and then use those values for the codex32 secret share. DO you consider the lack of testnet/mainnet separation an issue?

    Your addition of HRP into checksum definitely broke my tests wrt checksum for cc hrp secrets (not an issue, I haven’t released yet - but I’m planning to in few weeks)

    My HWW implementation is pretty much in accordance with https://github.com/BlockstreamResearch/codex32/blob/master/docs/wallets.md . My implementation is not ECW. I even provide generate support for secret share S. I only allow to generate 128 & 256 bit MS secrets (but allow to import also 512 bit). In short:

    1. TRNG 256 entropy bits
    2. r = sha256(sha265(entropy))
    3. x = r[:byte_len]
    4. x is new master secret, and default ID is 20 MSB from master XFP (but user can change if he wishes to)

    What are the chances of this patch-set to be accepted? Is this spec stable enough to start releasing it ?

  31. scgbckbone commented at 3:46 pm on November 27, 2025: contributor

    How I generate non-secret shares:

    1. From current loaded secret (whether it is mnemonic, xprv, or codex32)
      • codex32: secret = master_seed (secret share with hrp MS)
      • others: secret = chaincode + privkey (64bytes) (secret share with hrp CC)
    2. BIP-85 derive from above secret –> master secret for share ‘a’
    3. interpolate secret share with share ‘a’ while changing only index (c,d,d,e,f,g,h…) to generate new shares
  32. BenWestgate commented at 5:02 pm on November 27, 2025: none
    * HRP: `cc`
    

    “bc” and “tb” for Bech32 addresses were an upgrade in human-readable prefix from the base58 encoding.

    I consider it a regression if you use less characters to encode a human-readable prefix than the base58 extended key format did. “xpriv” is an option here.

    * encodes chaincode + private key of BIP-32 master extended key (64 bytes)
    

    Does your format need a 65th byte for the public key that is zero when encoding private keys?

    There are many advantages to the strings needing disambiguation having the same byte length.

    I only have one HRP, I do not differentiate between testnet/mainnet, … DO you consider the lack of testnet/mainnet separation an issue?

    Yes, this is a huge regression from the current bip32 extended key format we want to upgrade. Mostly that I can’t tell by looking at the descriptor if it’s for real funds or not.

    Your addition of HRP into checksum definitely broke my tests wrt checksum for cc hrp secrets (not an issue, I haven’t released yet - but I’m planning to in few weeks)

    HRP was always in the checksum, it just was pre-computed for “ms” so the checksums for other HRP were wrong. I noticed when I tried to validate the CLN HSM secret examples in my python-codex32 package.

    My implementation is not ECW. @roconnor has a PR in codex32 that does ECC you could test.

    I even provide generate support for secret share S. I only allow to generate 128 & 256 bit MS secrets (but allow to import also 512 bit).

    I have a codex32 PR to update wallets.md guidance for generation, you may see something useful, especially in the HWW case.

    In short:

    1. TRNG 256 entropy bits

    2. r = sha256(sha265(entropy))

    3. x = r[:byte_len]

    4. x is new master secret, and default ID is 20 MSB from master XFP (but user can change if he wishes to)

    You can and probably should use the entropy bits directly. If they lack entropy, sha256d is an illusion of security.

    What are the chances of this patch-set to be accepted? Is this spec stable enough to start releasing it ?

    It will need wider community review than us. But there’s comments by P. Wuille as far back as 2020 stating a 4 error correcting bech32 encoding of extended keys is needed. So high acceptance changes once it’s correct and shiney.

    This spec PR will not change anything that affects your encoding of ~78 bytes or whatever an extended key has.

    We’re mostly debating behavior at the limit between short and long checksums. Yours unambiguously use long codex32.

  33. BenWestgate commented at 5:10 pm on November 27, 2025: none

    How I generate non-secret shares:

    1. BIP-85 derive from above secret –> master secret for share ‘a’

    2. interpolate secret share with share ‘a’ while changing only index (c,d,d,e,f,g,h…) to generate new shares

    It is unsafe to child derive shares from the secret they recover. They should be independently random.

    When part of the secret is compromised and an attacker tries to brute force the rest: the dependent relation between the secret and share A allows an attacker with k-1 shares or share A to check his guesses against this. This is far faster than checking an address.

  34. scgbckbone commented at 5:54 pm on November 27, 2025: contributor

    HRP was always in the checksum, it just was pre-computed for “ms” so the checksums for other HRP were wrong. I noticed when I tried to validate the CLN HSM secret examples in my python-codex32 package.

    I see now…

    It is unsafe to child derive shares from the secret they recover. They should be independently random.

    I do not want to use randomness here, as I want to split existing secret, and I require the “split” to be deterministic, so that if user is splitting the exact same secret, uses same hrp, same threshold, same id, and same number of shares - application always produces the exact same shares. I could add an option to to choose, if random, or deterministic split, but deterministic is a hard requirement.

    …also it is 5 hardened derivation steps plus hmac_sha512

    When part of the secret is compromised and an attacker tries to brute force the rest: the dependent relation between the secret and share A allows an attacker with k-1 shares or share A to check his guesses against this. This is far faster than checking an address.

    there are plenty other brute-force options if attacker has part of secret, I do not consider this scenario of yours to be something I should optimize for

    Yes, this is a huge regression from the current bip32 extended key format we want to upgrade. Mostly that I can’t tell by looking at the descriptor if it’s for real funds or not.

    I do not encode extended key (or full extended key), I only encode chaincode + privkey, without any other data as I just want to be able to restore naked xpriv from it, without any more meta extended keys carry. As I use it for both mnemonics and extended keys.

    That is why I dismissed the idea of doing testnet/mainnet differentiation as I consider my 64bytes to be the “secret”

  35. BenWestgate commented at 7:45 pm on November 27, 2025: none

    It is unsafe to child derive shares from the secret they recover. They should be independently random.

    I do not want to use randomness here, as I want to split existing secret, and I require the “split” to be deterministic, so that if user is splitting the exact same secret, uses same hrp, same threshold, same id, and same number of shares - application always produces the exact same shares. I could add an option to to choose, if random, or deterministic split, but deterministic is a hard requirement.

    The best you could do here if you insist, is perform a KDF on the secret data to harden it before deriving child shares from that derived key. But it still reduces security from information theoretic to computational.

    …also it is 5 hardened derivation steps plus hmac_sha512

    Still significantly faster than address checking. The EC mult is the bottleneck for address checking is what Andrew told me.

    When part of the secret is compromised and an attacker tries to brute force the rest: the dependent relation between the secret and share A allows an attacker with k-1 shares or share A to check his guesses against this. This is far faster than checking an address.

    there are plenty other brute-force options if attacker has part of secret, I do not consider this scenario of yours to be something I should optimize for

    My point is your standard should be harder to exploit than all other options or we lose security for nothing. Simply deriving child shares from an argon2id or scrypt derived key is probably enough protection.

    That is why I dismissed the idea of doing testnet/mainnet differentiation as I consider my 64bytes to be the “secret”

    It seems better to encode the recovery words and wordlist with a bip39_12w or bip39_24w human-readable part encoding standard than encode the resulting private key and chaincode bytes. A full bip32 codex32 encoding standard would be more useful than a neutered master xprv only edition.

  36. BenWestgate commented at 9:30 pm on November 27, 2025: none

    This table shows the undetectable errors, each row has 2-3 characters which cannot be distinguished since they differ only in the upper bits.

    I found an 83 character Bech32 HRP with 3 substitutions that validates. In theory, some long HRP won’t detect even 1-2 errors affecting high bits. We inherit this problem if we copy Bech32 max length rules.

    The worse case is: a secret is transcribed wrong or damaged, user or heirs, application is forgotten, it validates or corrects to a different application and then is transmitted.

    This is worse than a wrong HRP address validating.

    We should guarantee to correct 2 HRP errors by covering the expanded characters. Now any wrong 2 character HRP for every seed length reveals it is “ms” secret data. For the more common errors affecting only the low (or high) bits two errors from the data can also be corrected.

    So the correct 4 errors guarantee holds under the assumption the HRP errors affect only low (or high) bits. Same assumption as Bech32’s detection guarantee, and it’s a detection only standard. We store secrets so we need correction guarantees and this is how we get them.

  37. roconnor commented at 10:08 pm on November 27, 2025: none
    I’m this close to throwing in the towel. BIP-93’s design was never intended to be generalized to arbitrary HRP, and it shows. If people want to reuse our polynomial for their own schemes, then more power to them. They can make their own BIP.
  38. apoelstra commented at 5:37 pm on December 5, 2025: contributor

    Sorry for being late to the party. I have read through this whole discussion except for the digression about deterministic share derivation and except for Russell’s detailed code. As I understand it there are a few issues at play:

    • Ben wants the HRP to be covered by the checksum, which has multiple problems
      • if you don’t know the HRP you arguably don’t know the checksum so how can you correct it?
      • but conversely Ben points out that we likely want xpub/xprv HRPs which are easy to mess up and would share a checksum
      • each HRP character contributes two characters to the checksum plus an extra “separator” character so length n takes away 2n+1 from your total length, which is surprising and weird
      • (There was a long discussion about restricting the character set of HRPs. As you have observed, I’ve already violated this with my bip32_24w HRPs. I’m skeptical this matters. If there is anybody except me doing this, they would have needed to do a comparable amount of insane off-spec work to accomplish it and “protecting them” by extending the spec to include them should not be a priority.)
    • How do we determine the threshold at which to switch to “long codex32” which is a totally different checksum
      • Ben would like there to only be a few allowable lengths of long codex32 strings, which I directionally agree with, but I also note that I have violated this (I have 264-bit strings which are converted directly from BIP39 seed words).
    • Some discussion on what the allowable HRPs should be. BIP-173 allows any ASCII string up to 83 characters.
      • …but if you look at the registered list of BIP-173 prefixes, despite there being some pretty crazy crap in there, every single prefix is less than 12 characters, and except for one using : and one using @ every single one is alphanumeric
      • Ben initially proposed restricting the set of characters to ones that all have distinct low bits so that we could “ignore the high bits”. But as seen in his above table, this is impossible if we allow both numbers and letters.

    In the interest of moving forward I would kinda like Ben to make a new PR with the non-HRP changes, which it seems like everyone agrees with and would reduce the size of the diff of this one.

    Then my opinions on the above:

    I agree with Russell that in general we should not attempt to correct the HRP. This was outside of the design space for our codex32 SSSS application and among other things we (ab)used this fact to distinguish codex32 from long codex32 on length alone and not HRP. Having said this, if Ben wants to try to error-correct HRPs all the power to him and we should take some effort to avoid undermining that goal.

    So for this BIP we should say:

    • Users can register their own HRPs at [link] but they are only allowed to use ASCII 96 to 126, and their length can be at most 8, say. (These are the tightest restrictions I would support, and I’d also accept any looser ones up to the “83 ascii characters free for all” of BIP 173.) This gives us { | } and ~ as well as letters. People who want a separator should be happy to use ~.
    • The HRP defines the checksum and SHOULD NOT be error-corrected, unless there is a separate specification describing how to do this. xpub/xprv I think needs to have its own BIP for this. Maybe there could be a general-purpose “bip93 with HRP correction” BIP that covers questions like “what if the user has a character outside of the allowable set” or “should we preferentially try to correct _ and - to ~ or just try random things” or “should we have a fixed set of supported HRPs and just try all of these”. It seems that different answers make sense in different contexts.
    • I’m happy with whatever length threshold we want for switching between codex32 and long codex32. I think “93 - length of HRP” is fine, along with an exception for ms. We should specify the maximum length in the table of registered HRPs so people don’t have to know the formula if they don’t want to.

    I think this should make everyone happy, except that it leaves HRP correction underspecified and delegated to another future BIP. (I would also be open to bringing more text into BIP 93 itself, but let’s try to accomplish the above before we do that.)


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: 2025-12-14 08:10 UTC

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