WO-04 — Bech32m codec

Local checks passed; stopped after step 7. Claude’s independent check is pending.

Branch: wo-04-bech32m, from wo-02-shake at 148c7fd. New golden reference/vectors frozen in 3af5f22, before C++ implementation. No other work order’s golden files changed. No installations, pushes or merges.

Built

  • e46/include/e46/bech32m.hpp, e46/src/bech32m.cpp: static target E46::bech32m, generic raw encode/decode and E46 version/payload framing.

  • All ten mainnet/testnet prefixes from §2a, version symbols q/p/z.

  • Mandatory expected prefix on E46 decoding, rejecting wrong network/object prefixes; strict checksum, ASCII, case, length and padding validation.

  • Independent Python reference, frozen vectors, batch CLI, unit/differential tests, parser fuzzer and benchmark. CMake changes only integrate this WO.

  • Owner’s UNC formatting; warnings are errors. Library uses no SHA-2, floating point or external crypto library.

Standards: BIP-350 and BIP-173. Official vectors here are the generic Bech32m vectors. Bitcoin SegWit witness-version/program rules are not E46’s address rules.

Measured

Evidence: summary and source fingerprints. Command outputs and fuzz/benchmark logs are alongside it.

Check

Result

Generic BIP-350 vectors

7 valid and 14 invalid cases passed

Chain §2a examples

All 10 reproduce exactly

Python/C++ differential

7,233 comparisons, zero mismatches, including 1,000 seeded random inputs

Edge checks

Empty/max-size frames, uppercase/mixed case, unknown/wrong prefixes, unsupported versions, old Bech32 checksums, tampering, excess/nonzero padding

Exhaustive padding unit check

All 1,024 two-word combinations checked

Full release suite, including WO-01/02

8/8 passed

Full ASan/UBSan suite

8/8 passed, recovery disabled

Golden regeneration

Byte-for-byte identical; frozen file not overwritten

Formatting

New C++ files pass clang-format --dry-run --Werror

Fuzzing

1,030,037 executions in 31 seconds, one worker, no errors; seeded with official vectors and E46 examples

Benchmark

Median 658,711 encode+decode round trips/second, 32-byte payload, one worker, five ~0.25-second samples

Fuzzer seed: 2026100804, input cap 256 bytes. Benchmark: macOS arm64, Apple Clang 21 release build; fuzzing uses the already-installed Homebrew LLVM 22. Throughput includes both operations and is host/load-specific.

API boundary for review

encode_raw(hrp, five_bit_words) and decode_raw(text) implement generic Bech32m. encode_e46(hrp, version, bytes) and decode_e46(text, expected_hrp) additionally handle E46 prefixes, versions and canonical byte conversion. Encoding emits lowercase; decoding accepts uniform uppercase and rejects mixed case. Invalid inputs throw std::invalid_argument.

This is a framing codec, not address derivation or a consensus validator. Payload meaning/length and whether reserved p is activated belong to WO-05. Empty payloads and p envelopes are representable without declaring them valid spendable addresses. Every E46 version uses Bech32m, including q; Bitcoin’s version-zero Bech32 exception does not apply. No automatic error correction.

Re-run

cmake --preset release && cmake --build --preset release && ctest --preset release
cmake --preset sanitize && cmake --build --preset sanitize && ctest --preset sanitize

The CLI reads one operation per line and returns OK ... or ERR:

encode e46 0 00010203
decode e46 <encoded-string-as-hex>
raw_encode <hrp-as-hex> <five-bit-words-as-hex>
raw_decode <encoded-string-as-hex>

Use - for an empty byte field. CLI: build/release/bech32m_cli. Python reference for fresh-input review: e46/golden/bech32m_reference.py. The builder’s public seed is not Claude’s unpublished-seed surprise exam. No later work order was started.