Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
4271f95
Add codex32 application to bip85.py
BenWestgate Aug 19, 2025
37e4c9c
fix imports, ValueError, don't assign unused data, remove junk code b…
BenWestgate Aug 19, 2025
0c1bcc7
BIP93: fix t-n = 1 bug, fixup imports, sanity checks
BenWestgate Aug 19, 2025
a505d0d
Create bip93.py
BenWestgate Aug 19, 2025
fbdb997
bip85 vectors: add BIP93 test vectors
BenWestgate Aug 19, 2025
275bf26
WIP: placeholder for the full BIP93 test vectors
BenWestgate Aug 19, 2025
e9d4c11
test_BIP85: add test_codex32 to validate vectors
BenWestgate Aug 19, 2025
535d47b
WIP: Create test_bip93.py need to find test vector
BenWestgate Aug 19, 2025
3abca70
Add INDEX_TO_HRP dict
BenWestgate Oct 6, 2025
276f0e5
Remove electrum, add entropy_to_codex32
BenWestgate Oct 6, 2025
66ca66d
Create settings.json
BenWestgate Oct 6, 2025
bfa13bd
Add cli tool "codex32" for generating codex32
BenWestgate Oct 6, 2025
b1873d5
Update bip93_vectors.py
BenWestgate Oct 6, 2025
193aae3
fix import typo
BenWestgate Oct 6, 2025
33777e0
Update test_bip93.py
BenWestgate Oct 6, 2025
d93858d
fix codex32 help string as it uses token_bytes
BenWestgate Oct 12, 2025
965f020
Add codex32 & recover features & bip85 codex32 app
BenWestgate Oct 13, 2025
9a4ae77
Add codex32 dependency for bip93 encoding.
BenWestgate Oct 13, 2025
65bd0b1
bip85: Import things from codex32 we'll need
BenWestgate Oct 13, 2025
94b15df
add: xprv --codex32 option and `recover` command
BenWestgate Oct 13, 2025
2c63697
Update README.md
BenWestgate Oct 14, 2025
3b34cc7
Update README.md
BenWestgate Oct 14, 2025
86308ba
Update pyproject.toml
BenWestgate Oct 14, 2025
676b864
Merge pull request #3 from BenWestgate/patch-2
BenWestgate Oct 14, 2025
5b2f1e3
Delete test_bip93.py
BenWestgate Oct 14, 2025
f24e6ac
Delete bip93_vectors.py
BenWestgate Oct 14, 2025
9c26d8b
import codex32, delete non-bip85 codex32 commands
BenWestgate Oct 14, 2025
143e81e
Update pyproject.toml to import codex32
BenWestgate Oct 14, 2025
df2a599
doc: add example of the proposed bip85 codex32 app
BenWestgate Oct 14, 2025
be05e2e
Delete bip93.py
BenWestgate Oct 14, 2025
ddb119e
Merge branch 'patch-1' of https://github.com/BenWestgate/bipsea into …
BenWestgate Oct 14, 2025
14fd2d8
Delete settings.json
BenWestgate Oct 14, 2025
38814cb
Update .gitignore
BenWestgate Oct 14, 2025
0a9708c
Update bipsea.py
BenWestgate Oct 14, 2025
a1b860b
new bip85 bip93 derive proposal
BenWestgate Oct 14, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,4 @@ __pycache__
build
dist
.coverage
.vscode
69 changes: 64 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,8 @@ bipsea offers four commands that work together:

1. `mnemonic` creates BIP-39 seed mnemonics in 9 languages
1. `validate` validates BIP-39 in 9 languages
1. `codex32` creates BIP-93 codex32 backups
1. `recover` validates BIP-93 strings and recovers seed
1. `xprv` derives a BIP-32 extended private key
1. `derive` applies BIP-85 to an xprv to derive child secrets

Expand Down Expand Up @@ -122,20 +124,51 @@ bipsea mnemonic -t spa -n 12 | bipsea validate -f spa
relleno peón exilio vara grave hora boda terapia dinero vulgar vulgar goloso


## `codex32`

Suppose you want a 3-of-5 codex32 backup.

```sh
bipsea codex32
```
ms13casha320zyxwvutsrqpnmlkjhgfedca2a8d0zehn8a0t ms13cashcacdefghjklmnpqrstuvwxyz023949xq35my48dr ms13cashd0wsedstcdcts64cd7wvy4m90lm28w4ffupqs7rm ms13casheekgpemxzshcrmqhaydlp6yhms3ws7320xyxsar9 ms13cashf8jh6sdrkpyrsp5ut94pj8ktehhw2hfvyrj48704

Or a 2-of-3 with identifier 'NAME'.

```sh
bipsea codex32 -t2 -n3 -i'NAME' --pretty
```
MS12NAMEA320ZYXWVUTSRQPNMLKJHGFEDCAXRPP870HKKQRM
MS12NAMECACDEFGHJKLMNPQRSTUVWXYZ023FTR2GDZMPY6PN
MS12NAMEDLL4F8JLH4E5VDVULDLFXU2JHDNLSM97XVENRXEG


## `bipsea recover`

BIP-93 codex32 backups contain a 48-124 characters, and include a checksum.
`recover` checks the integrity of a codex32 string or set of shares, recovers the codex32 secret,
then echoes the result so that you can pipe it to `bipsea xprv`.

```sh
echo "MS12NAMEDLL4F8JLH4E5VDVULDLFXU2JHDNLSM97XVENRXEG MS12NAMEA320ZYXWVUTSRQPNMLKJHGFEDCAXRPP870HKKQRM" | bipsea recover
```
MS12NAMES6XQGUZTTXKEQNJSJZV4JV3NZ5K3KWGSPHUH6EVW


## `bipsea xprv`

```sh
bipsea mnemonic | bipsea validate | bipsea xprv
```
xprv9s21ZrQH143K41bKPQ9XHbPoqfdCDmZLBorYHay5E273HTu5yAFm27sSWRoCpisgQNH9vfrL9yVvVg5rBEbMCk2UwQ8K7qCFnZAY7aXhuqV

`bipsea xprv` converts a mnemonic into a master node (the root of your wallet
`bipsea xprv` converts a mnemonic or codex32 secret into a master node (the root of your wallet
chain) that serializes as an xprv or _extended private key_.


### xprv from dice rolls (or any string)

```
```sh
bipsea validate -f free -m "123456123456123456" | bipsea xprv
```
Warning: Relative entropy of input seems low (0.42). Consider a more complex --mnemonic.
Expand Down Expand Up @@ -174,7 +207,7 @@ Below are several applications.


### base85 passwords
```
```sh
bipsea validate -m $MNEMONIC | bipsea xprv | bipsea derive -a base85
```
iu?42{I|2Ct{39IpEP5zBn=0
Expand All @@ -185,14 +218,38 @@ we get `-n 20` characters of a base85 password.

### mnemonic phrases

```
```sh
bipsea validate -m "$MNEMONIC" | bipsea xprv | bipsea derive -a mnemonic -t jpn -n 12
```
ちこく へいおん ふくざつ ゆらい あたりまえ けんか らくがき ずほう みじかい たんご いそうろう えいきょう

As with all applications, you can change the child index from it's default of zero
to get a fresh, repeatable secret.

### codex32 strings
```
bipsea validate -m "$MNEMONIC" | bipsea xprv | bipsea derive -a codex32 -t 3 -id cash
```
ms13casha320zyxwvutsrqpnmlkjhgfedca2a8d0zehn8a0t ms13cashcacdefghjklmnpqrstuvwxyz023949xq35my48dr ms13cashd0wsedstcdcts64cd7wvy4m90lm28w4ffupqs7rm

As with all applications, you can change the child index from it's default of zero
to get a fresh, repeatable secret. Note this increments the `--identifier` as it
should be unique for each secret.

### codex32 strings

```sh
bipsea validate -m "$MNEMONIC" | bipsea xprv | bipsea derive -a codex32 -t 3
```
ms13casha320zyxwvutsrqpnmlkjhgfedca2a8d0zehn8a0t ms13cashcacdefghjklmnpqrstuvwxyz023949xq35my48dr ms13cashd0wsedstcdcts64cd7wvy4m90lm28w4ffupqs7rm

The output will always be the first threshold t initial shares, or a codex32 secret if `-t 0` these may be passed to `codex32` to generate a backup of -n shares.

```sh
bipsea validate -m "$MNEMONIC" | bipsea xprv | bipsea derive -a codex32 -t 3 | bipsea codex32 -n 5
```
ms13casha320zyxwvutsrqpnmlkjhgfedca2a8d0zehn8a0t ms13cashcacdefghjklmnpqrstuvwxyz023949xq35my48dr ms13cashd0wsedstcdcts64cd7wvy4m90lm28w4ffupqs7rm ms13casheekgpemxzshcrmqhaydlp6yhms3ws7320xyxsar9 ms13cashf8jh6sdrkpyrsp5ut94pj8ktehhw2hfvyrj48704


### DRNG, enter the matrix

Expand Down Expand Up @@ -347,7 +404,7 @@ See [Makefile](./Makefile) for more commands.

## Is the bipsea implementation correct?

bipsea passes all BIP-32, BIP-39, and BIP-85 test vectors in all BIP-39 languages
bipsea passes all BIP-32, BIP-39, BIP-85 and BIP-93 test vectors in all BIP-39 languages
plus its own unit tests.

There is a single BIP-85 vector, which we believe to be incorrect in the spec,
Expand All @@ -364,6 +421,8 @@ mnemonic seed words
generalized BIP-32 paths
1. [BIP-85](https://github.com/bitcoin/bips/blob/master/bip-0085.mediawiki)
generalized cryptographic entropy
1. [BIP-93](https://github.com/bitcoin/bips/blob/master/bip-0094.mediawiki)
checksummed SSSS-aware BIP-32 seeds


# TODO
Expand Down
3 changes: 2 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ authors = ["Aneesh Karve <[email protected]>"]
license = "Apache-2.0"
homepage = "https://github.com/akarve/bipsea"
repository = "https://github.com/akarve/bipsea"
keywords = ["Bitcoin", "BIP-32", "BIP-39", "BIP-85", "cryptography", "secrets", "ECDSA", "entropy"]
keywords = ["Bitcoin", "BIP-32", "BIP-39", "BIP-85", "BIP-93", "cryptography", "secrets", "ECDSA", "entropy"]
classifiers = [
"Development Status :: 4 - Beta",
"Intended Audience :: Developers",
Expand All @@ -32,6 +32,7 @@ click = "~8.1.3"
base58 = "~2.1.1"
build = "~1.2.1"
ecdsa = "~0.19.0"
codex32 = "~0.1.0"

[tool.poetry.group.dev.dependencies]
black = "~24.4.2"
Expand Down
42 changes: 42 additions & 0 deletions src/bipsea/bip85.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,12 @@
from typing import Dict, Union

import base58
from codex32.codex32 import (
CHARSET,
bech32_decode,
Codex32String,
entropy_to_strings,
)

from .bip32 import VERSIONS, ExtendedKey
from .bip32 import derive_key as derive_key_bip32
Expand All @@ -23,10 +29,13 @@
"drng": "0'",
"hex": "128169'",
"mnemonic": "39'",
"codex32": "93'",
"wif": "2'",
"xprv": "32'",
}

# TODO lots to copy in from patch-1!

RANGES = {
"base64": (20, 86),
"base85": (10, 80),
Expand All @@ -51,6 +60,11 @@
"9'": "portuguese", # not in BIP-85 but in BIP-39 test vectors
}

INDEX_TO_HRP = {
"0'": "ms",
"1'": "cl",
}

assert set(INDEX_TO_LANGUAGE.values()) == set(LANGUAGES.keys())


Expand Down Expand Up @@ -83,6 +97,34 @@ def apply_85(derived_key: ExtendedKey, path: str) -> Dict[str, Union[bytes, str]
"entropy": trimmed_entropy,
"application": " ".join(words),
}
elif app == APPLICATIONS["codex32"]:
header, n_bytes = indexes[:2]
hrp, data = bech32_decode(header)
if hrp not in INDEX_TO_HRP:
raise ValueError(f"Unsupported human-readable prefix: {hrp}.")
k = int(CHARSET[data[0]])
if k == 1:
raise ValueError(
f"Threshold '{k}' is not an allowed value (2 through 9, or 0)."
)
ident = header[1:5]
byte_length = int(n_bytes.rstrip("'"))
if not 16 <= byte_length <= 64:
raise ValueError(
f"Byte length '{byte_length}' is not an allowed value (16 through 64)."
)
drng = DRNG(entropy)
alphabetized_charset = "sacdefghjk" # threshold above 9 is invalid
shares = []
for share_idx in alphabetized_charset[bool(k) : k + 1]:
shares += Codex32String.from_seed(
drng.read(byte_length), ident, hrp, k, share_idx
).s

return {
"entropy": entropy,
"application": " ".join(shares),
}
Comment on lines +100 to +127
Copy link
Author

Choose a reason for hiding this comment

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

I think this new proposed path and codex32 derivation address both yours and scgbckbone feedback:

Fewer derivation levels: {header}, {n_bytes}, {index}
Fewer parameters: {share_idx} and {num_shares} are dropped

Indices output are deterministic based on k. No derived shares, just the initial k

{header} is the first 8 characters of a codex32 string, it would be some serialization of {hrp}|{threshold}{identifier} and fits if converted from bech32 to an int.

I also want to feed the bip85 app {index} into the ident as it should be unique for different seeds

elif app == APPLICATIONS["wif"]:
trimmed_entropy = entropy[: 256 // 8]
prefix = b"\x80" if derived_key.get_network() == "mainnet" else b"\xef"
Expand Down
63 changes: 61 additions & 2 deletions src/bipsea/bipsea.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@
import sys

import click
from codex32.codex32 import (
CHARSET,
entropy_to_codex32,
)

from .bip32 import to_master_key
from .bip32types import parse_ext_key, validate_prv_str
Expand All @@ -21,6 +25,7 @@
APPLICATIONS,
DRNG,
INDEX_TO_LANGUAGE,
INDEX_TO_HRP,
PURPOSE_CODES,
RANGES,
apply_85,
Expand Down Expand Up @@ -137,16 +142,49 @@ def validate(from_, mnemonic):
click.echo(" ".join(words))


# TODO: paste codex32 cli here


@click.command(
name="recover",
help="Validate a set of codex32 strings and recover the secret or share at target share index",
)
@click.option(
"-t", "--target", help="Share index to recover at. Default is the secret `s`."
)
def recover(strings, target="s"):
click.echo(Codex32String.interpolate_at(strings, target))


@click.command(
name="xprv",
help="Derive a BIP-32 XPRV from arbitrary string. Use bipsea validate` to validate!",
)
@click.option("-m", "--mnemonic", help="Mnemonic. Pipe from `bipsea validate`.")
@click.option("-c", "--codex32", help="Codex32 secret. Pipe from `bipsea recover`.")
@click.option("-p", "--passphrase", default="", help="BIP-39 passphrase.")
@click.option("--mainnet/--testnet", is_flag=True, default=True)
def xprv(mnemonic, passphrase, mainnet):
def xprv(mnemonic, codex32, passphrase, mainnet):
if mnemonic:
mnemonic = mnemonic.strip()
elif codex32:
codex32 = codex32.strip()
no_empty_param("--codex32", codex32)
if len(codex32) < 48:
raise click.BadOptionUsage(
option_name="--codex32",
message="Suspiciously short codex32 secret. Try `bipsea recover`.",
)
if passphrase:
raise click.BadOptionUsage(
option_name="--passphrase",
message="No passphrase support for codex32 secrets. Try `bipsea recover`.",
)
seed = Codex32String.from_string(codex32).parts().data()
prv = to_master_key(seed, mainnet=mainnet, private=True)

click.echo(prv)
return
else:
mnemonic = try_for_pipe_input()
no_empty_param("--mnemonic", mnemonic)
Expand Down Expand Up @@ -206,6 +244,8 @@ def xprv(mnemonic, passphrase, mainnet):
help="Output language for `--application mnemonic`.",
)
def derive_cli(application, number, index, special, xprv, to):
# TODO Write the "glue" between the bip85 codex32 logic and CLI last as it's the hardest step.
# TODO Copy unfinished work from patch-1
if xprv:
xprv = xprv.strip()
else:
Expand Down Expand Up @@ -241,8 +281,25 @@ def derive_cli(application, number, index, special, xprv, to):

if application == "mnemonic":
language = ISO_TO_LANGUAGE[to]
code_85 = next(i for i, l in INDEX_TO_LANGUAGE.items() if l == language)
code_85 = next(
i for i, l in INDEX_TO_LANGUAGE.items() if l == language
) # noqa: E741
path += f"/{code_85}/{number}'/{index}'"
if application == "codex32":
code_85 = next(i for i, l in INDEX_TO_HRP.items() if l == "ms") # noqa: E741
path += f"/{code_85}/{number}'/{index}'"
elif application == "codex32":
hrp, data = bech32_decode("ms1" + str(0) + "test")
code_85 = ISO_TO_HRP[hrp] + int.from_bytes(
convertbits(data, 5, 8)
) # serialization must fit in 32-bits
# 3.2 for threshold, 20 for identifier, 1 for hrp if just 0,1 dict + 4 more for default IDs
# add the {index} to the encoded indentifier so that it increments as the index is. That gives 2^20 seeds before we reuse the original identifier
# this gives the amazing property for fingerprint IDs when you know the master fingerprint
# that you can subtract them and see what index was used to generate that codex32 backup.
# be sure it rolls over at 2^20 so that the threshold doesn't increment as well which will
# break things.
path += f"/{code_85}'/{number}'/{index}'"
elif application in ("wif", "xprv"):
path += f"/{index}'"
elif application in ("base64", "base85", "hex"):
Expand Down Expand Up @@ -271,6 +328,8 @@ def cli():

cli.add_command(mnemonic)
cli.add_command(validate)
cli.add_command(codex32)
cli.add_command(recover)
cli.add_command(xprv)
cli.add_command(derive_cli)

Expand Down
Loading