Skip to content

Conversation

@BenWestgate
Copy link

@BenWestgate BenWestgate commented Sep 7, 2025

This allows wallets to derive codex32 secrets and codex32 shares from BIP-0032 master keys.

Summary of changes

Rationale

  • Mirrors the existing BIP-85 application for BIP-39.
  • Codex32 offers error correction, hand verification, identifiers, and secret sharing improvements vs BIP-39.
  • Deterministic generation produces auditable backups by avoiding reliance on local RNG, helping users who distrust device entropy.

Specification

  • Adds Application 93' to BIP-0085 using derivation path:
m/83696968'/93'/{hrp || threshold}'/{byte_length}'/{index}'
  • Uses the BIP-85 DRNG
  • Unspecified identifiers default to child's BIP-32 master seed fingerprint

Tests
Reference tests and new vectors will be included in the reference bipsea implementation:
BenWestgate/bipsea@master...BenWestgate:bipsea:master

Mailing List
Discussion: https://groups.google.com/g/bitcoindev/c/--lHTAtq0Qc

Status
Ready for conceptual and approach review. This change is additive and does not modify existing BIP-85 behavior.

@jonatack jonatack added Proposed BIP modification Pending acceptance This BIP modification requires sign-off by the champion of the BIP being modified labels Sep 8, 2025
@BenWestgate BenWestgate marked this pull request as draft September 8, 2025 00:17
@BenWestgate BenWestgate marked this pull request as ready for review September 8, 2025 00:26
@BenWestgate BenWestgate changed the title Add Codex32 (BIP-0093) as application 93' to BIP-0085 BIP85: Add Codex32 application 93' Sep 9, 2025
@BenWestgate BenWestgate changed the title BIP85: Add Codex32 application 93' BIP85: Add Codex32 as application 93' Sep 9, 2025
@akarve
Copy link
Contributor

akarve commented Sep 10, 2025

Documenting recent discussions:
@BenWestgate Please see my mailing list comments to your thread with suggestions and simplifications (path, byte extraction, idx, etc.). Regarding 1.4.0 the main thing is we want to warrant full compatibility (all features) up to the prior version and (just saw you reopened 68) a PR to the 1.3.0 client is probably the easiest way to achieve that. Lmk if anything is unclear.

@BenWestgate
Copy link
Author

BenWestgate commented Sep 12, 2025

Documenting recent discussions: @BenWestgate Please see my mailing list comments to your thread with suggestions and simplifications (path, byte extraction, idx, etc.). Regarding 1.4.0 the main thing is we want to warrant full compatibility (all features) up to the prior version and (just saw you reopened 68) a PR to the 1.3.0 client is probably the easiest way to achieve that. Lmk if anything is unclear.

It seems you'd like to consolidate some of the paths. There's a few ways to do this, if you have a favorite or one that immediately stands out as obvious let me know.

I think `m/83696968'/93'/{hrp}'/{cat({n} {threshold} {byte_length}}'/{index}'

hrp is alone as it's unknown how many future human-readable prefixes there may be.

n will always be ''1'' through ''31'', t ''0'', or ''2'' through ''9'', byte_length ''16'' through ''64''. So we can decimal concatenate them with the max value being: 31 9 64 -> 31964'

I'm thinking the identifier could be the bech32 encoding of the bip85 index, as the purpose of incrementing the index is to get new seeds, and BIP93 says "...the identifier SHOULD be distinct for all master seeds the user may need to disambiguate."

index = 0 -> identifier = qqqq, index = 1 -> identifier qqqp, and so on. A particular identifier can be selected by converting it to an integer {index} once index reaches 32^4, it can fall back to the default BIP-0032 fingerprint.

On byte extraction: I agree we should draw byte_length bytes and pad to a multiple of 5 bits with a CRC. The polynomials (1 << crc_len) | 3 is optimal for 1-4 bits. Output share indices still can use the current read one byte at a time method.

Copy link
Member

@jonatack jonatack left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pinging @scgbckbone (who has been active on BIP85 review) for feedback.

@scgbckbone
Copy link
Contributor

Seems to me this is well over the BIP-85 application scope. As I understand it, BIP85 generates "a thing" from "a thing". Your application is generating "multiple things" from "a thing".

Why are you generating multiple initial shares via BIP85 ?

What I imagined BIP85 application should looks like after reading BIP93:

  1. way to generate secret share s from BIP-32 root seed (so that you can load other wallets with derived entropy). Something like this: m/83696968'/93'/{b93_index mapped to int -> s in this case}'/{byte_length}'/{index}'
  2. way to generate any non-secret share from BIP32 root seed. This, as per rationale, would allow users to generate 2nd (and only 2nd) share deterministically via BIP85, and not via RNG. All other shares should be derived according to BIP93 via interpolation. m/83696968'/93'/{b93_index mapped to int -> not s in this case}'/{byte_length}'/{index}'

** maybe even threshold should be part of the BIP32 derivation path, BUT I think not as it has no effect to the actual secret generated (it only affects checksum)

Assuming I'm not wrong in my "specualtion", why not just use m/83696968'/128169'/{num_bytes}'/{index}' to generate deterministic bytes from BIP-32 root seed for any share ?

@jonatack jonatack added the PR Author action required Needs updates, has unaddressed review comments, or is otherwise waiting for PR author label Oct 14, 2025
@BenWestgate
Copy link
Author

BenWestgate commented Oct 19, 2025

Thank you for great feedback @scgbckbone. I'll explain the rationale behind your questions first.

...BIP85 generates "a thing" from "a thing".

True, but that thing can be structured.
For example, BIP39 derives an entire mnemonic, not one word at a time.
Here, the "thing" is a complete codex32 backup as there's no recoverable seed without at least {threshold} shares.

Why are you generating multiple initial shares via BIP85 ?

Determinism.
We want to eliminate ambiguity about which initial share indices were derived by BIP85 to make BIP85 child seed recovery easier. Example:

bip85 = Bip85(master_root_xprv)
# 1. generate secret share "s" from root seed
secret1 = bip85.derive_codex32(t=3, share_idx='s')
# 2. generate `k` any non-"s" shares from root seed, interpolate according to BIP93
secret2 = Codex32String.interpolate_at(
    [
      bip85.derive_codex32(k=3, share_idx='a'),
      bip85.derive_codex32(k=3, share_idx='c),
      bip85.derive_codex32(k=3, share_idx='d'),
    ],
    target="s"
)
secret3 = Codex32String.recover(
    [
        bip85.derive_codex32(k=3, share_idx='x'),
        bip85.derive_codex32(k=3, share_idx='y),
        bip85.derive_codex32(k=3, share_idx='z'),
    ],
    target="s"
)
derived_secrets = [secret1, secret2, secret3]
identifiers = set()
master_seeds = set()
for secret in derived_secrets:
    identifiers.add(secret.identifier)
    master_seeds.add(secret.data)

if len(identifiers) < len(master_seeds):
    raise Bip93Quote("Identifier SHOULD be distinct for every master seed the user may need to disambiguate")

For the same BIP85 root key, each {threshold} set of initial {share_idx} BIP85 derived shares recovers a different secret; and the 's' derivation yet another. That's a bad property: these codex32 sets share the same header ms13<identifier> making them hard to disambiguate. Mismatched sets recover wrong seeds.

** maybe even threshold should be part of the BIP32 derivation path, BUT I think not as it has no effect to the actual secret generated (it only affects checksum)

For secret sharing, the {threshold} must be in the derivation path.
Otherwise ms12testa... and ms13testa... share entropy payloads even though they're distinct backup sets, a security vulnerability if both are used.

For unshared secrets, the threshold has no effect, so it'd be ideal to ignore it when not secret sharing. That way, knowing the BIP85 index and root key uniquely identifies the seed, regardless of threshold, consistent with other BIP85 applications.

...why not just use m/83696968'/128169'/{num_bytes}'/{index}' to generate deterministic bytes from BIP-32 root seed for any share ?

Then why not use that for BIP39 or any other application too?
Let users convert deterministic bytes into mnemonics or codex32 strings as they wish.
The point of a BIP85 application is to standardize how that entropy is consumed into a specific deterministic format.

Based on feedback from you and @akarve
Simplified proposal:
Derivation:
matlab m/83696968'/93'/{header}'/{byte_length}'/{index}'

  • where {header} is an int encoding of <hrp>, <hrp>1<k>, or <hrp>1<k><identifier> (TBD).

Simplifications:

  • {share_idx} and {num_shares} can be eliminated
  • {identifier} can be implicit, but if user-defined, {index} should feed into it to keep output identifiers distinct per master seed
  • "Existing master seed" derivation rule is removed, we only generate fresh seeds.
    • Users can discard an initial share and interpolate if they have an existing master seed they wish to share.
  • BIP93 interpolation and relabeling identifiers left to users.
  • Default identifier = BIP32 fingerprint of derived seed.

Example of the simplified form:

bip85 = Bip85(master_root_xprv)
# 1. generate k=0 secret share "s" from root seed
secret1 = bip85.derive_codex32(k=0)
# 2. generate `k` fixed non-"s" shares from root seed, interpolate according to BIP93
shares = bip85.derive_codex32(k=2)
secret2 = Codex32String.interpolate_at(shares, target="s")
shares = bip85.derive_codex32(k=3)
secret3 = Codex32String.interpolate_at(shares, target="s")
derived_secrets = [secret1, secret2, secret3]
identifiers = set()
master_seeds = set()
for secret in derived_secrets:
    identifiers.add(secret.identifier)
    master_seeds.add(secret.data)

assert len(identifiers) == len(master_seeds)
# header is distinct for each master seed the user may need to disambiguate

assert len(master_seeds) == 1
# same master seed for same {index}, regardless of k

Seems over the BIP-85 scope.

The version brings it back within scope:

  • k=0: derive 1 codex32 secret.
  • k=2: derive 2 codex32 shares ('a' and 'c') → recover same secret.
  • k=3: generate 3 codex32 shares ('a', 'c' and, 'd') → recover same secret.
    Identifier defaults to derived seed’s BIP32 fingerprint.
    Incrementing {index} yields new seeds, with new identifiers automatically.

@jonatack jonatack removed the PR Author action required Needs updates, has unaddressed review comments, or is otherwise waiting for PR author label Oct 19, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Pending acceptance This BIP modification requires sign-off by the champion of the BIP being modified Proposed BIP modification

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants