Skip to content

python-codex32 reference impl - #74

Open
BenWestgate wants to merge 4 commits into
BlockstreamResearch:masterfrom
BenWestgate:python-codex32
Open

BenWestgate wants to merge 4 commits into
BlockstreamResearch:masterfrom
BenWestgate:python-codex32

Conversation

@BenWestgate

Copy link
Copy Markdown
Contributor

apoelstra/rust-codex32#2 (comment):

@BenWestgate if you have a rewrite feel free to open a PR -- if it's not too hard to review I'm happy to take it in.

But I think the correct direction to rewrite in is one where we add a rust-bech32 dependency, use that for all the checksumming and encoding stuff, and then here we add (a) utility methods, and (b) constructors and accessors for the id/threshold/share index.

I have wrote this python implementation that covers more tests than the rust one does. It also does the approach above and relies on general checksum and bech32 modules. So it also passes Bech32/Bech32m tests.

It includes new test vectors for bitcoin/bips#2040 and bitcoin/bips#2077.

From: https://github.com/BenWestgate/python-codex32

Published on PyPI: https://pypi.org/project/codex32/

@apoelstra

Copy link
Copy Markdown
Contributor

I vote to merge this with a cursory review; later when I improve rust-codex32, I will steal all the test vectors from this implementation, and presumably if they pass on both implementations, they're both correct :P.

cursory review ACK 4857e18 -- checked that this is shaped like it does codex32 stuff, that there's nothing obviously malicious, no filesystem or network access, etc.

cc @roconnor-blockstream can you let me know if I should hold off on merging this? Fine to ping me out-of-band.

@BenWestgate

BenWestgate commented Aug 28, 2026 •

Copy link
Copy Markdown
Contributor Author

I nACK 4857e18 because it allows encoding shares from padded bytes. When the padding is not uniform, independent random it can leak up to (k-1) * pad_len bits of secret to attackers with k-1 derived shares.

Comment explaining the problem: 4857e18#r198130738

The rust impl allows this too, and an issue was opened where a user of our libraries was confused he could not recover the same secret bytes from derived share bytes.

image

The best solution is refuse to encode or decode shares from/to bytes. Especially now that bitcoin/bips#2258 defines "S" decoding to be application specific.

I have some BIP0173 style test vectors in the bitcoin/bips#2040 (comment) that are worth stealing as well.

@BenWestgate

This comment was marked as off-topic.

BenWestgate added a commit to BenWestgate/python-codex32 that referenced this pull request Oct 8, 2026
Rewrite the BIP93 encoding, checksum, sharing and profile layers of the
v0.6.1 code (byte-identical to 4857e18, ACKed in
BlockstreamResearch/codex32#74) as frozen, typed values with no
third-party dependency.

- gf32/checksums/bech32: immutable checksum specs, GF(32) tables and
  strict single-case Bech32 parsing; drop the unused segwit_addr module.
- bip93: Header/Share/Secret values, parse_codex32, derive_share and
  recover_secret.  There is no public encoder from raw (padded) bytes to
  a share; shares come only from interpolation.
- profiles: ms (BIP32 master seed), cl (Core Lightning hsm_secret, unshared
  only) and bip39 entropy.
- Tooling: drop mypy.ini, requirements.txt and the pylint job; add
  ruff, strict mypy and a hash-pinned setuptools build dependency.

Claude-Session: https://claude.ai/code/session_01MDsbGqZmvVThoJrs4vzc4K
BenWestgate added a commit to BenWestgate/python-codex32 that referenced this pull request Oct 9, 2026
Rewrite the BIP93 encoding, checksum, sharing and profile layers of the
v0.6.1 code (byte-identical to 4857e18, ACKed in
BlockstreamResearch/codex32#74) as frozen, typed values with no
third-party dependency.

- gf32/checksums/bech32: immutable checksum specs, GF(32) tables and
  strict single-case Bech32 parsing; drop the unused segwit_addr module.
- bip93: Header/Share/Secret values, parse_codex32, derive_share and
  recover_secret.  There is no public encoder from raw (padded) bytes to
  a share; shares come only from interpolation.
- profiles: ms (BIP32 master seed), cl (Core Lightning hsm_secret, unshared
  only) and bip39 entropy.
- Tooling: drop mypy.ini, requirements.txt and the pylint job; add
  ruff, strict mypy and a hash-pinned setuptools build dependency.

Claude-Session: https://claude.ai/code/session_01MDsbGqZmvVThoJrs4vzc4K
BenWestgate added a commit to BenWestgate/python-codex32 that referenced this pull request Oct 9, 2026
Rewrite the BIP93 encoding, checksum, sharing and profile layers of the
v0.6.1 code (byte-identical to 4857e18, ACKed in
BlockstreamResearch/codex32#74) as frozen, typed values with no
third-party dependency.

- gf32/checksums/bech32: immutable checksum specs, GF(32) tables and
  strict single-case Bech32 parsing; drop the unused segwit_addr module.
- bip93: Header/Share/Secret values, parse_codex32, derive_share and
  recover_secret.  There is no public encoder from raw (padded) bytes to
  a share; shares come only from interpolation.
- profiles: ms (BIP32 master seed), cl (Core Lightning hsm_secret, unshared
  only) and bip39 entropy.
- Tooling: drop mypy.ini, requirements.txt and the pylint job; add
  ruff, strict mypy and a hash-pinned setuptools build dependency.

Claude-Session: https://claude.ai/code/session_01MDsbGqZmvVThoJrs4vzc4K
@apoelstra

Copy link
Copy Markdown
Contributor

I have done a cursory review of 4857e18. Want me to merge it?

Shares are now made only by interpolation. encode() and from_seed()
reject share indices other than 's', and decode() returns no seed
bytes or padding for shares. Codex32String keeps the string it was
given instead of re-encoding from bytes, so attributes are no longer
writable aliases.

Drop code a reference implementation should not carry:
- the bip32 dependency and the fingerprint default identifier
  (BIP-93 leaves the identifier to the user; coincurve also fails to
  build on Python 3.13)
- segwit_addr.py, Bech32/Bech32m constants and their tests
- unused HRP_CODES, IDX_ORDER and IdNotLength4

Claude-Session: https://claude.ai/code/session_01PYQtm2qPkBYqZ9WuNvhv51
- "ms" strings must have an encoded length of 48, 54, 61, 67, 74 or
  127, and master seeds must be 16, 20, 24, 28, 32 or 64 bytes.
- Select the checksum by expanded codeword length (HRP expansion
  plus data part): regular up to 93, long from 96 to 1023; 94 and 95
  are invalid.
- Limit the HRP to 83 characters.
- Add Test vectors 6-8 from BIP-93 master and 9-11 plus the new
  invalid vectors from bips#2320. Drop the long-HRP boundary vectors
  that encoded the old data-length checksum selection.

Claude-Session: https://claude.ai/code/session_01PYQtm2qPkBYqZ9WuNvhv51
@BenWestgate

BenWestgate commented Oct 17, 2026 •

Copy link
Copy Markdown
Contributor Author

I updated this PR to resolve my NACK and keep it matching BIP93 master and bitcoin/bips#2320.

  1. 5db9e9b removes the bip32 dependency, "fingerprint identifiers", Bech32/segwit_addr constants/tests, and confusing ability to encode/decode bytes <--> shares.

  2. 4619cb4 enforces the new "ms" lengths, "expanded codewords" checksum selection and max HRP length 83, and new test vectors from PR #3020 and master.

Less than 45 lines added so this is a quick cursory re-review.

BenWestgate

This comment was marked as outdated.

codex32_decode now checks only what every codex32 string has: the
checksum and the header (digit threshold, share index "s" when k=0).
ms32_verify adds the BIP-93 "ms" length rule, and Codex32String uses it
for "ms" strings, so generic strings with other HRPs no longer need
byte-aligned payloads.

encode() and decode() only handle codex32-encoded master seeds, so they
drop their hrp argument. Codex32String stores the HRP, header and
payload characters and gets bytes from data(), which only works for
"ms" secrets. The seed-size check and InvalidSeedLength are gone since
the "ms" lengths already imply the seed sizes.

Claude-Session: https://claude.ai/code/session_01PYQtm2qPkBYqZ9WuNvhv51
@BenWestgate

BenWestgate commented Oct 19, 2026 •

Copy link
Copy Markdown
Contributor Author

Generalizing HRP needed one more change:
3. 2ecd191 non-"ms" strings no longer need byte-aligned payloads; "ms" rules live in ms32_verify, and encode/decode are "ms"-only.

It is now correct and up-to-date with master and open BIP93 PRs.

BenWestgate added a commit to BenWestgate/python-codex32 that referenced this pull request Oct 20, 2026
Rewrite the BIP93 encoding, checksum, sharing and profile layers of the
v0.6.1 code (byte-identical to 4857e18, ACKed in
BlockstreamResearch/codex32#74) as frozen, typed values with no
third-party dependency.

- gf32/checksums/bech32: immutable checksum specs, GF(32) tables and
  strict single-case Bech32 parsing; drop the unused segwit_addr module.
- bip93: Header/Share/Secret values, parse_codex32, derive_share and
  recover_secret.  There is no public encoder from raw (padded) bytes to
  a share; shares come only from interpolation.
- profiles: ms (BIP32 master seed), cl (Core Lightning hsm_secret, unshared
  only) and bip39 entropy.
- Tooling: drop mypy.ini, requirements.txt and the pylint job; add
  ruff, strict mypy and a hash-pinned setuptools build dependency.

Claude-Session: https://claude.ai/code/session_01MDsbGqZmvVThoJrs4vzc4K
@BenWestgate

Copy link
Copy Markdown
Contributor Author

I have done a cursory review of 4857e18. Want me to merge it?

It's stale vs BIP93 but it's the version on PyPI so better than no merge.

The best reference for bip93 master is 4619cb4 and bip39_12w won't parse without 2ecd191.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants