diff --git a/reference/python-codex32/LICENSE b/reference/python-codex32/LICENSE new file mode 100644 index 0000000..c707f7b --- /dev/null +++ b/reference/python-codex32/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 Ben Westgate + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/reference/python-codex32/LICENSES/BSD-3-Clause-Curr-Sneed.txt b/reference/python-codex32/LICENSES/BSD-3-Clause-Curr-Sneed.txt new file mode 100644 index 0000000..7cf010a --- /dev/null +++ b/reference/python-codex32/LICENSES/BSD-3-Clause-Curr-Sneed.txt @@ -0,0 +1,28 @@ +BSD 3-Clause License + +Copyright 2023 Leon Olsson Curr and Pearlwort Sneed + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/reference/python-codex32/README.md b/reference/python-codex32/README.md new file mode 100644 index 0000000..1dd22a4 --- /dev/null +++ b/reference/python-codex32/README.md @@ -0,0 +1,95 @@ +# python-codex32 + +Reference implementation of BIP-0093 (codex32): checksummed, SSSS-aware BIP32 seed strings. + +This repository implements the codex32 string format described by BIP-0093. +It provides encoding/decoding, regular/long codex32 checksums, CRC padding for base conversions, +Shamir secret sharing scheme (SSSS) interpolation helpers and helpers to build codex32 strings from seed bytes. + +## Features +- Encode/decode codex32 data via `from_string` and `from_unchecksummed_string`. +- Regular and long codex32 checksum support. +- Construct codex32 strings from raw seed bytes via `from_seed`. +- `from_seed` uses default bech32-encoded BIP32 fingerprint identifier and CRC padding. +- Interpolate shares recover secrets via `interpolate_at`. +- Parse codex32 strings and access parts via properties. +- Mutate codex32 strings by reassigning `is_upper`, `hrp`, `k`, `ident`, `share_idx`, `data`, and `pad_val`. +- Supports Bech32/Bech32m and segwit address format aswell. + +## Security +Caution: This is reference code. Verify carefully before using with real funds. + +## Installation +**Compatibility:** Python 3.10–3.14 + +**Recommended:** use a virtual environment +### Linux / macOS +```bash +python -m venv .venv +source .venv/bin/activate +pip install codex32 +``` +### Windows +```powershell +python -m venv .venv +.venv\Scripts\Activate.ps1 +pip install codex32 +``` + + +## Quick usage +```python +from codex32 import Codex32String + +# Create from seed bytes +s = Codex32String.from_seed( + bytes.fromhex('ffeeddccbbaa99887766554433221100'), + "ms13cashs", # prefix string, (HRP + '1' + header) + 0 # padding value (default "CRC", otherwise integer) +) +print(s.s) # codex32 string + +# Parse an existing codex32 string and inspect parts +a = Codex32String("ms13casha320zyxwvutsrqpnmlkjhgfedca2a8d0zehn8a0t") +print(a.hrp) # human-readable part +print(a.k) # threshold parameter +print(a.ident) # 4 character identifier +print(a.share_idx) # share index character +print(a.payload) # payload part +print(a.checksum) # checksum part +print(len(a)) # length of the codex32 string +print(a.is_upper) # case is upper True/False +print(s.data.hex()) # raw seed bytes as hex +print(a.pad_val) # padding value integer, (MSB first) + + + +# Create from unchecksummed string (will append checksum) +c = Codex32String.from_unchecksummed_string("ms13cashcacdefghjklmnpqrstuvwxyz023") +print(str(c)) # equivalent to print(c.s) + +# Interpolate shares to recover or derive target share index +shares = [s, a, c] +derived_share_d = Codex32String.interpolate_at(shares, target='d') +print(derived_share_d.s) + +# Create Codex32String object from existing codex32 string and validate any HRP +e = Codex32String.from_string("cl", "cl10lueasd35kw6r5de5kueedxyesqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqanvrktzhlhusz") +print(e.ident) +print(e.s) + +# Relabel a Codex32String object +e.ident = "cln2" +print(e.ident) +print(e.s) + +# Uppercase a Codex32String object (for encoding in QR codes or handwriting) +e.is_upper = True +print(e.s) +``` + +## Tests +``` bash +pip install -e .[dev] +pytest +``` diff --git a/reference/python-codex32/pyproject.toml b/reference/python-codex32/pyproject.toml new file mode 100644 index 0000000..2be4bd9 --- /dev/null +++ b/reference/python-codex32/pyproject.toml @@ -0,0 +1,49 @@ +[build-system] +requires = ["setuptools>=80", "setuptools-scm>=8"] +build-backend = "setuptools.build_meta" + +[project] +name = "codex32" +version = "0.6.0" +authors = [ + { name = "Ben Westgate", email = "benwestgate@protonmail.com" }, +] +description = "Python reference implementation for codex32 (BIP93) and codex32-encoded master seeds." +readme = "README.md" +requires-python = ">=3.10" +license = "MIT" +license-files = ["LICENSE*"] +maintainers = [ + { name = "Ben Westgate", email = "benwestgate@protonmail.com" }, +] +keywords = ["bitcoin", "HD wallet", "BIP32", "BIP93", "BIP173", "BIP350", "codex32", "cryptography", "secret sharing", "secrets", "entropy", "bech32"] +classifiers = [ + "Development Status :: 4 - Beta", + "Intended Audience :: Developers", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3 :: Only", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", + "Programming Language :: Python :: 3.14", + "Operating System :: OS Independent", + "Topic :: Security :: Cryptography", + "Topic :: Software Development :: Libraries", +] +dependencies = ["bip32>=5.0.0"] + +[project.optional-dependencies] +dev = ["pytest"] + +[tool.setuptools.packages.find] +where = ["src"] + +[tool.pytest.ini_options] +testpaths = ["tests"] + +[project.urls] +Homepage = "https://github.com/benwestgate/python-codex32" +Repository = "https://github.com/benwestgate/python-codex32" +Issues = "https://github.com/benwestgate/python-codex32/issues" +Changelog = "https://github.com/benwestgate/python-codex32/tree/master/CHANGELOG.md" diff --git a/reference/python-codex32/src/codex32/__init__.py b/reference/python-codex32/src/codex32/__init__.py new file mode 100644 index 0000000..b0fe0f5 --- /dev/null +++ b/reference/python-codex32/src/codex32/__init__.py @@ -0,0 +1,27 @@ +# Copyright (c) 2026 Ben Westgate +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +"""codex32 package: bech32/codex32 helpers and encoders/decoders.""" + +from .bip93 import Codex32String, encode, decode +from .errors import CodexError + + +__all__ = ["CodexError", "Codex32String", "encode", "decode"] diff --git a/reference/python-codex32/src/codex32/bech32.py b/reference/python-codex32/src/codex32/bech32.py new file mode 100644 index 0000000..3e734e9 --- /dev/null +++ b/reference/python-codex32/src/codex32/bech32.py @@ -0,0 +1,159 @@ +# Portions of this file are derived from work by: +# Copyright (c) 2017, 2020 Pieter Wuille +# +# Additional code and modifications: +# Copyright (c) 2026 Ben Westgate +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +"""Internal bech32 and u5 helpers for Bech32/codex32 encoding and decoding.""" + +from codex32.errors import CodexError +from codex32.checksums import Checksum, crc_pad + + +# pylint: disable=missing-class-docstring +class InvalidDataValue(CodexError): ... + + +class IncompleteGroup(CodexError): ... + + +class InvalidLength(CodexError): ... + + +class InvalidChar(CodexError): ... + + +class InvalidCase(CodexError): ... + + +class InvalidChecksum(CodexError): ... + + +class InvalidPadding(CodexError): ... + + +class MissingHrp(CodexError): ... + + +class SeparatorNotFound(CodexError): ... + + +class MissingChecksum(CodexError): ... + + +class MissingEncoding(CodexError): ... + + +CHARSET = "qpzry9x8gf2tvdw0s3jn54khce6mua7l" + + +def bech32_hrp_expand(hrp: str) -> list[int]: + """Expand the HRP into values for checksum computation.""" + return [ord(x) >> 5 for x in hrp] + [0] + [ord(x) & 31 for x in hrp] + + +def u5_to_chars(data: list[int]) -> str: + """Map list of 5-bit integers (0-31) -> Bech32 data-part string.""" + for i, x in enumerate(data): + if not 0 <= x < 32: + raise InvalidDataValue(f"from 0 to 31 index={i} value={x}") + return "".join(CHARSET[d] for d in data) + + +def u5_encode(hrp: str, data: list[int], spec: Checksum) -> str: + """Compute a Bech32 string given HRP and data values.""" + combined = data + spec.create(bech32_hrp_expand(hrp) + data) + return hrp + "1" + u5_to_chars(combined) + + +def chars_to_u5(bech: str) -> list[int]: + """Map Bech32 data-part string -> list of 5-bit integers (0-31).""" + for i, ch in enumerate(bech): + if ch not in CHARSET: + raise InvalidChar(f"'{ch!r}' at pos={i} in data part") + return [CHARSET.find(x) for x in bech] + + +def u5_parse(bech: str) -> tuple[str, list[int]]: + """Parse a Bech32/Codex32 string, and return HRP and 5-bit data.""" + for i, ch in enumerate(bech): + if ord(ch) < 33 or ord(ch) > 126: + raise InvalidChar(f"non-printable U+{ord(ch):04X} at pos={i}") + if bech.upper() != bech and bech.lower() != bech: + raise InvalidCase("mixed upper/lower case bech32 string") + if (pos := (bech := bech.lower()).rfind("1")) < 1: + raise MissingHrp("empty HRP") if not pos else SeparatorNotFound("'1' not found") + hrp = bech[:pos] + data = chars_to_u5(bech[pos + 1 :]) + return hrp, data + + +def u5_decode(bech: str, encodings: list[Checksum]) -> tuple[str, list[int], Checksum]: + """Validate a Bech32/Codex32 string, and determine HRP and data.""" + hrp, data = u5_parse(bech) + e = MissingEncoding("no encoding or encodings were passed") + for spec in encodings: + if len(hrp) <= (datlen := len(bech) - 1 - spec.cs_len): + if datlen in (c := spec.coverage): + if spec.verify(bech32_hrp_expand(hrp) + data): + return hrp, data[: -spec.cs_len], spec + e = InvalidChecksum(f"{spec.kind} checksum invalid for hrp and data") + if not isinstance(e, InvalidChecksum): + e = InvalidLength(f"{datlen} chars {spec.kind} reqs {min(c)}..{max(c)}") + if not isinstance(e, (InvalidLength, InvalidChecksum)): + e = MissingChecksum(f"{spec.kind}: {len(data)} data chars < {spec.cs_len}") + raise e + + +def convertbits( + data: list[int] | bytes, + frombits: int, + tobits: int, + pad: bool = True, + pad_val: int | str = 0, +) -> list[int]: + """General power-of-2 base conversion.""" + acc = 0 + bits = 0 + ret = [] + maxv = (1 << tobits) - 1 + max_acc = (1 << (frombits + tobits - 1)) - 1 + for value in data: + if value < 0 or (value >> frombits): + raise InvalidDataValue(f"{value} is not in 0 to {(1 << frombits) - 1}") + acc = ((acc << frombits) | value) & max_acc + bits += frombits + while bits >= tobits: + bits -= tobits + ret.append((acc >> bits) & maxv) + if not pad and bits >= frombits: + raise IncompleteGroup(f" {bits} bits remaining, must be {frombits - 1} or less") + pad_len = (tobits - bits) if pad and bits else bits + pv = crc_pad(convertbits(data, frombits, 1)) if pad_val == "CRC" else pad_val + if isinstance(pad_val, int) and not 0 <= pad_val < (1 << pad_len): + raise InvalidDataValue(f"padding int {pad_val} must be 0 to {(1< +# License: BSD-3-Clause +# Derived work: BECH32_INV, bech32_mul, bech32_lagrange, codex32_interpolate +# +# Modifications and additional code: +# Copyright (c) 2026 Ben Westgate , MIT License +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +"""Reference implementation for codex32/Long codex32 and codex32-encoded master seeds.""" + + +from bip32 import BIP32 + +from codex32.bech32 import ( + CHARSET, + chars_to_u5, + convertbits, + u5_to_chars, + u5_decode, + u5_encode, + u5_parse, +) +from codex32.checksums import CODEX32, CODEX32_LONG +from codex32.errors import CodexError + +HRP_CODES = { + "ms": 0, # BIP-0032 master seed + "cl": 1, # CLN HSM secret +} # Registry: https://github.com/satoshilabs/slips/blob/master/slip-0173.md#uses-of-codex32 +IDX_ORDER = "sacdefghjklmnpqrstuvwxyz023456789" # Canonical BIP93 share indices alphabetical order +BECH32_INV = [ + 0, + 1, + 20, + 24, + 10, + 8, + 12, + 29, + 5, + 11, + 4, + 9, + 6, + 28, + 26, + 31, + 22, + 18, + 17, + 23, + 2, + 25, + 16, + 19, + 3, + 21, + 14, + 30, + 13, + 7, + 27, + 15, +] + + +# pylint: disable=missing-class-docstring + + +class IdNotLength4(CodexError): ... + + +class InvalidThreshold(CodexError): ... + + +class InvalidShareIndex(CodexError): ... + + +class MismatchedLength(CodexError): ... + + +class MismatchedHrp(CodexError): ... + + +class MismatchedThreshold(CodexError): ... + + +class MismatchedId(CodexError): ... + + +class RepeatedIndex(CodexError): ... + + +class ThresholdNotPassed(CodexError): ... + + +class InvalidSeedLength(CodexError): ... + + +def bech32_mul(a, b): + """Multiply two Bech32 values.""" + res = 0 + for i in range(5): + res ^= a if ((b >> i) & 1) else 0 + a *= 2 + a ^= 41 if (32 <= a) else 0 + return res + + +def bech32_lagrange(pts, x): + """Compute Bech32 lagrange.""" + n = 1 + c = [] + for i in pts: + n = bech32_mul(n, i ^ x) + m = 1 + for j in pts: + m = bech32_mul(m, (x if i == j else i) ^ j) + c.append(m) + return [bech32_mul(n, BECH32_INV[i]) for i in c] + + +def codex32_decode(codex): + """Validate a codex32 string, and determine HRP and data.""" + return u5_decode(codex, [CODEX32_LONG, CODEX32]) + + +def codex32_interpolate(strings, x): + """Interpolate a set of codex32 data values given target index.""" + w = bech32_lagrange([s[5] for s in strings], x) + res = [] + for i in range(len(strings[0])): + n = 0 + for j, val in enumerate(strings): + n ^= bech32_mul(w[j], val[i]) + res.append(n) + return res + + +def codex32_encode(hrp: str, data): + """Compute a codex32 string given HRP and data values.""" + spec = CODEX32_LONG if len(hrp) + len(data) > 80 else CODEX32 + return u5_encode(hrp, data, spec) + + +def decode(hrp: str, s: str, pad_val: int | str = "any"): + """Decode a codex32 string, and determine header, seed, and padding.""" + hrpgot, data, _ = codex32_decode(s) + if hrpgot != hrp: + raise MismatchedHrp(f"{hrpgot} != {hrp}") + if len(header := u5_to_chars(data[:6])) < 6: + raise MismatchedLength(f"'{header}' header too short: {len(data)} < 6") + if not (k := header[0]).isdigit(): + raise InvalidThreshold(f"threshold parameter '{k}' must be a digit") + if k == "0" and (idx := header[5]) != "s": + raise InvalidShareIndex(f"share index '{idx}' must be 's' when k='0'") + decoded = convertbits(data[6:], 5, 8, False, pad_val) + if hrp == "ms" and (not 16 <= (msl := len(decoded)) <= 64 or msl % 4): + raise InvalidSeedLength(f"Master seeds must be in 16..20..64 bytes, got {msl}") + pad = data[-1] % (1 << ((len(data[6:]) * 5) % 8)) + return header, bytes(decoded), pad if pad_val == "any" else pad_val + + +def encode(hrp: str, header: str, seed: bytes, pad_val: int | str = "CRC"): + """Encode a codex32 string given HRP, header, seed, and padding.""" + u5_payload = convertbits(seed, 8, 5, True, pad_val) + ret = codex32_encode(hrp, chars_to_u5(header) + u5_payload) + if len(header) != 6 or (header, seed, pad_val) != decode(hrp, ret, pad_val): + raise MismatchedLength(f"'{header}' header must be 6 chars, got {len(header)}") + return ret + + +class Codex32String: + """Class representing a codex32 string.""" + + def __init__(self, s: str) -> None: + """Initialize Codex32String from a codex32 string.""" + self.is_upper = s.isupper() + self.hrp = codex32_decode(s)[0] + header, self.data, self.pad_val = decode(self.hrp, s) + self.k, self.ident, self.share_idx = header[0], header[1:5], header[5] + + @property + def payload(self) -> str: + """Return the payload part of the codex32 string.""" + return u5_to_chars(convertbits(self.data, 8, 5, True, self.pad_val)) + + @property + def s(self) -> str: + """Return the full codex32 string.""" + header = self.k + self.ident + self.share_idx + ret = encode(self.hrp, header, self.data, self.pad_val) + return ret.upper() if ret and self.is_upper else ret + + def __str__(self) -> str: + return self.s + + def __len__(self) -> int: + return len(self.s) + + @property + def checksum(self) -> str: + """Return the checksum part of the codex32 string.""" + return self.s[-codex32_decode(self.s)[2].cs_len :] + + @classmethod + def from_unchecksummed_string(cls, s: str) -> "Codex32String": + """Create Codex32String from unchecksummed string.""" + ret = codex32_encode(*u5_parse(s)) + return cls(ret.upper() if s.isupper() else ret) + + @classmethod + def from_string(cls, hrp: str, s: str) -> "Codex32String": + """Create Codex32String from a given codex32 string and HRP.""" + if (hrpgot := u5_parse(s)[0]) != hrp: + raise MismatchedHrp(f"{hrpgot} != {hrp}") + return cls(s) + + @classmethod + def interpolate_at( + cls, shares: list["Codex32String"], target: str = "s" + ) -> "Codex32String": + """Interpolate a set of Codex32String objects to a specific target index.""" + if not all(isinstance(share, Codex32String) for share in shares): + raise TypeError("All shares must be Codex32String instances") + if (threshold := int(shares[0].k) if shares else 1) > len(shares): + raise ThresholdNotPassed(f"threshold={threshold}, n_shares={len(shares)}") + for share in shares: + if len(shares[0]) != len(share): + raise MismatchedLength(f"{len(shares[0])}, {len(share)}") + if shares[0].hrp != share.hrp: + raise MismatchedHrp(f"{shares[0].hrp}, {share.hrp}") + if shares[0].k != share.k: + raise MismatchedThreshold(f"{shares[0].k}, {share.k}") + if shares[0].ident != share.ident: + raise MismatchedId(f"{shares[0].ident}, {share.ident}") + if [share.share_idx for share in shares].count(share.share_idx) > 1: + raise RepeatedIndex(share.share_idx) + if ret := [share for share in shares if share.share_idx == target.lower()]: + return ret.pop() + u5_shares = [codex32_decode(share.s)[1] for share in shares] + data = codex32_interpolate(u5_shares, CHARSET.find(target.lower())) + ret = codex32_encode(shares[0].hrp, data) + return cls(ret.upper() if all(s.s.upper() == s.s for s in shares) else ret) + + @classmethod + def from_seed( + cls, data: bytes, prefix: str = "ms10", pad_val: int | str = "CRC" + ) -> "Codex32String": + """Create Codex32String given prefix and bare seed data.""" + hrp, data_part = u5_parse(prefix) + header = u5_to_chars(data_part) + k = "0" if not header else header[:1] + if not (ident := header[1 : max(5, len(header) - 1)]): + bip32_fingerprint = BIP32.from_seed(data).get_fingerprint() + ident = u5_to_chars(convertbits(bip32_fingerprint, 8, 5)[:4]) + elif len(ident) != 4: + raise IdNotLength4(f"identifier had wrong length {len(ident)}") + share_idx = "s" if not header[5:] else header[5:6] + return cls(encode(hrp, k + ident + share_idx, data, pad_val)) diff --git a/reference/python-codex32/src/codex32/checksums.py b/reference/python-codex32/src/codex32/checksums.py new file mode 100644 index 0000000..a133fd6 --- /dev/null +++ b/reference/python-codex32/src/codex32/checksums.py @@ -0,0 +1,99 @@ +# Bech32/Bech32m constants in this file are derived from work by: +# Copyright (c) 2017, 2020 Pieter Wuille, MIT License +# codex32 constants in this file are derived from work by: +# Author: Leon Olsson Curr and Pearlwort Sneed +# License: BSD-3-Clause +# +# Additional code: +# Copyright (c) 2026 Ben Westgate , MIT License +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +"""Checksum specs and utilities for Bech32, codex32, and CRC variants.""" + + +# Generators are the reduction polynomials for polymod computations +BECH32_GEN = [0x3B6A57B2, 0x26508E6D, 0x1EA119FA, 0x3D4233DD, 0x2A1462B3] +CODEX32_GEN = [ + 0x19DC500CE73FDE210, + 0x1BFAE00DEF77FE529, + 0x1FBD920FFFE7BEE52, + 0x1739640BDEEE3FDAD, + 0x07729A039CFC75F5A, +] +CODEX32_LONG_GEN = [ + 0x3D59D273535EA62D897, + 0x7A9BECB6361C6C51507, + 0x543F9B7E6C38D8A2A0E, + 0x0C577EAECCF1990D13C, + 0x1887F74F8DC71B10651, +] +BECH32_CONST = 1 +BECH32M_CONST = 0x2BC830A3 +CODEX32_CONST = 0x10CE0795C2FD1E62A +CODEX32_LONG_CONST = 0x43381E570BF4798AB26 + + +class Checksum: + """Checksum spec (polynomial gens, length, constant, coverage, create and verify).""" + + def __init__(self, kind: str, specs, coverage): + self.kind = kind + self.gen, self.cs_len, self.const = specs + self.coverage = range(coverage[0], coverage[1] + 1) # valid lengths + self.shift = len(self.gen) * (self.cs_len - 1) + self.mask = (1 << self.shift) - 1 + + def polymod(self, values, residue=1): + """Internal function that computes the Bech32/Codex32/CRC checksums.""" + for value in values: + top = residue >> self.shift + residue = (residue & self.mask) << len(self.gen) ^ value + for i, g in enumerate(self.gen): + residue ^= g if ((top >> i) & 1) else 0 + return residue + + def verify(self, values): + """Verify a checksum given values.""" + return self.polymod(values) == self.const + + def create(self, values): + """Compute the checksum values given values.""" + polymod = self.polymod(values + [0] * self.cs_len) ^ self.const + mask = (1 << (w := len(self.gen))) - 1 + cs_len = self.cs_len + return [(polymod >> (w * (cs_len - 1 - i))) & mask for i in range(cs_len)] + + +codex32_long_spec = (CODEX32_LONG_GEN, 15, CODEX32_LONG_CONST) # detects 8 errors +CODEX32_LONG = Checksum("Long codex32", codex32_long_spec, (81, 1008)) +CODEX32 = Checksum("codex32", (CODEX32_GEN, 13, CODEX32_CONST), (0, 80)) +BECH32 = Checksum("Bech32", (BECH32_GEN, 6, BECH32_CONST), (0, 83)) # detects 4 errors +BECH32M = Checksum("Bech32m", (BECH32_GEN, 6, BECH32M_CONST), (0, 83)) +CRC1 = Checksum("CRC1", ([1], 1, 0), (0, 0)) +CRC2 = Checksum("CRC2", ([3], 2, 0), (0, 1)) # detects 2 errors +CRC3 = Checksum("CRC3", ([3], 3, 0), (0, 4)) +CRC4 = Checksum("CRC4", ([3], 4, 0), (0, 11)) + + +def crc_pad(bits: list[int]) -> int: + """Compute the CRC padding value given payload bits list.""" + crc = [None, CRC1, CRC2, CRC3, CRC4][k := ((-len(bits) % 5) or (len(bits) % 8))] + crc_bits = crc.create(bits[: len(bits) // 8 * 8]) if crc else [] + return sum(b << (k - 1 - i) for i, b in enumerate(crc_bits)) if k else 0 diff --git a/reference/python-codex32/src/codex32/errors.py b/reference/python-codex32/src/codex32/errors.py new file mode 100644 index 0000000..03c44b1 --- /dev/null +++ b/reference/python-codex32/src/codex32/errors.py @@ -0,0 +1,12 @@ +"""codex32 / Bech32 encoding and usage errors.""" + + +class CodexError(Exception): + """Base class for all codex32 / Bech32 errors.""" + + def __init__(self, extra: str | None = None) -> None: + self.extra = extra + super().__init__(extra) + + def __str__(self) -> str: + return str(self.extra) if self.extra else "" diff --git a/reference/python-codex32/src/codex32/segwit_addr.py b/reference/python-codex32/src/codex32/segwit_addr.py new file mode 100644 index 0000000..f970b00 --- /dev/null +++ b/reference/python-codex32/src/codex32/segwit_addr.py @@ -0,0 +1,85 @@ +# Copyright (c) 2017, 2020 Pieter Wuille +# Copyright (c) 2026 Ben Westgate +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +"""Reference implementation for Bech32/Bech32m and segwit addresses.""" + +from enum import Enum + +from codex32.errors import CodexError +from codex32.bech32 import u5_encode, u5_decode, convertbits +from codex32.checksums import BECH32, BECH32M + + +class Encoding(Enum): + """Enumeration type to list the various supported encodings.""" + + BECH32 = 1 + BECH32M = 2 + + +def bech32_encode(hrp, data, spec): + """Compute a Bech32 string given HRP and data values.""" + return u5_encode(hrp, data, BECH32 if spec == Encoding.BECH32 else BECH32M) + + +def bech32_decode(bech: str): + """Validate a Bech32/Bech32m string, and determine HRP and data.""" + try: + hrp, data, spec = u5_decode(bech, [BECH32, BECH32M]) + if spec is None: + return (None, None, None) + return hrp, data, Encoding.BECH32 if spec == BECH32 else Encoding.BECH32M + except CodexError: + return (None, None, None) + + +def decode(hrp: str, addr): + """Decode a segwit address.""" + hrpgot, data, spec = bech32_decode(addr) + if hrpgot != hrp or not data: + return (None, None) + try: + decoded = convertbits(data[1:], 5, 8, False) + except CodexError: + decoded = None + if decoded is None or len(decoded) < 2 or len(decoded) > 40: + return (None, None) + if data[0] > 16: + return (None, None) + if data[0] == 0 and len(decoded) != 20 and len(decoded) != 32: + return (None, None) + if ( + data[0] == 0 + and spec != Encoding.BECH32 + or data[0] != 0 + and spec != Encoding.BECH32M + ): + return (None, None) + return (data[0], decoded) + + +def encode(hrp, witver, witprog): + """Encode a segwit address.""" + spec = Encoding.BECH32 if witver == 0 else Encoding.BECH32M + ret = bech32_encode(hrp, [witver] + convertbits(witprog, 8, 5, True, 0), spec) + if decode(hrp, ret) == (None, None): + return None + return ret diff --git a/reference/python-codex32/tests/data/bip93_vectors.py b/reference/python-codex32/tests/data/bip93_vectors.py new file mode 100644 index 0000000..2f03743 --- /dev/null +++ b/reference/python-codex32/tests/data/bip93_vectors.py @@ -0,0 +1,255 @@ +# tests/data/bip93_vectors.py +# pylint: disable=line-too-long +"""BIP-93 / codex32 canonical test vectors.""" +VECTOR_1 = { + "secret_s": "ms10testsxxxxxxxxxxxxxxxxxxxxxxxxxx4nzvca9cmczlw", + "secret_hex": "318c6318c6318c6318c6318c6318c631", + "hrp": "ms", + "k": "0", + "identifier": "test", + "share_index": "s", + "payload": "xxxxxxxxxxxxxxxxxxxxxxxxxx", + "checksum": "4nzvca9cmczlw", +} + +VECTOR_2 = { + "share_A": "MS12NAMEA320ZYXWVUTSRQPNMLKJHGFEDCAXRPP870HKKQRM", + "share_C": "MS12NAMECACDEFGHJKLMNPQRSTUVWXYZ023FTR2GDZMPY6PN", + "derived_D": "MS12NAMEDLL4F8JLH4E5VDVULDLFXU2JHDNLSM97XVENRXEG", + "secret_S": "MS12NAMES6XQGUZTTXKEQNJSJZV4JV3NZ5K3KWGSPHUH6EVW", + "secret_hex": "d1808e096b35b209ca12132b264662a5", +} + +VECTOR_3 = { + "secret_hex": "ffeeddccbbaa99887766554433221100", + "secret_s": "ms13cashsllhdmn9m42vcsamx24zrxgs3qqjzqud4m0d6nln", + "share_a": "ms13casha320zyxwvutsrqpnmlkjhgfedca2a8d0zehn8a0t", + "share_c": "ms13cashcacdefghjklmnpqrstuvwxyz023949xq35my48dr", + "derived_d": "ms13cashd0wsedstcdcts64cd7wvy4m90lm28w4ffupqs7rm", + "derived_e": "ms13casheekgpemxzshcrmqhaydlp6yhms3ws7320xyxsar9", + "derived_f": "ms13cashf8jh6sdrkpyrsp5ut94pj8ktehhw2hfvyrj48704", + "secret_s_alternate_0": "ms13cashsllhdmn9m42vcsamx24zrxgs3qqjzqud4m0d6nln", + "secret_s_alternate_1": "ms13cashsllhdmn9m42vcsamx24zrxgs3qpte35dvzkjpt0r", + "secret_s_alternate_2": "ms13cashsllhdmn9m42vcsamx24zrxgs3qzfatvdwq5692k6", + "secret_s_alternate_3": "ms13cashsllhdmn9m42vcsamx24zrxgs3qrsx6ydhed97jx2", +} + +VECTOR_4 = { + "secret_hex": "ffeeddccbbaa99887766554433221100ffeeddccbbaa99887766554433221100", + "secret_s": "ms10leetsllhdmn9m42vcsamx24zrxgs3qrl7ahwvhw4fnzrhve25gvezzyqqtum9pgv99ycma", + "secret_s_alternate_0": "ms10leetsllhdmn9m42vcsamx24zrxgs3qrl7ahwvhw4fnzrhve25gvezzyqqtum9pgv99ycma", + "secret_s_alternate_1": "ms10leetsllhdmn9m42vcsamx24zrxgs3qrl7ahwvhw4fnzrhve25gvezzyqpj82dp34u6lqtd", + "secret_s_alternate_2": "ms10leetsllhdmn9m42vcsamx24zrxgs3qrl7ahwvhw4fnzrhve25gvezzyqzsrs4pnh7jmpj5", + "secret_s_alternate_3": "ms10leetsllhdmn9m42vcsamx24zrxgs3qrl7ahwvhw4fnzrhve25gvezzyqrfcpap2w8dqezy", + "secret_s_alternate_4": "ms10leetsllhdmn9m42vcsamx24zrxgs3qrl7ahwvhw4fnzrhve25gvezzyqy5tdvphn6znrf0", + "secret_s_alternate_5": "ms10leetsllhdmn9m42vcsamx24zrxgs3qrl7ahwvhw4fnzrhve25gvezzyq9dsuypw2ragmel", + "secret_s_alternate_6": "ms10leetsllhdmn9m42vcsamx24zrxgs3qrl7ahwvhw4fnzrhve25gvezzyqx05xupvgp4v6qx", + "secret_s_alternate_7": "ms10leetsllhdmn9m42vcsamx24zrxgs3qrl7ahwvhw4fnzrhve25gvezzyq8k0h5p43c2hzsk", + "secret_s_alternate_8": "ms10leetsllhdmn9m42vcsamx24zrxgs3qrl7ahwvhw4fnzrhve25gvezzyqgum7hplmjtr8ks", + "secret_s_alternate_9": "ms10leetsllhdmn9m42vcsamx24zrxgs3qrl7ahwvhw4fnzrhve25gvezzyqf9q0lpxzt5clxq", + "secret_s_alternate_10": "ms10leetsllhdmn9m42vcsamx24zrxgs3qrl7ahwvhw4fnzrhve25gvezzyq28y48pyqfuu7le", + "secret_s_alternate_11": "ms10leetsllhdmn9m42vcsamx24zrxgs3qrl7ahwvhw4fnzrhve25gvezzyqt7ly0paesr8x0f", + "secret_s_alternate_12": "ms10leetsllhdmn9m42vcsamx24zrxgs3qrl7ahwvhw4fnzrhve25gvezzyqvrvg7pqydv5uyz", + "secret_s_alternate_13": "ms10leetsllhdmn9m42vcsamx24zrxgs3qrl7ahwvhw4fnzrhve25gvezzyqd6hekpea5n0y5j", + "secret_s_alternate_14": "ms10leetsllhdmn9m42vcsamx24zrxgs3qrl7ahwvhw4fnzrhve25gvezzyqwcnrwpmlkmt9dt", + "secret_s_alternate_15": "ms10leetsllhdmn9m42vcsamx24zrxgs3qrl7ahwvhw4fnzrhve25gvezzyq0pgjxpzx0ysaam", +} + +VECTOR_5 = { + "hrp": "MS", + "k": "0", + "identifier": "0C8V", + "share_idx": "S", + "payload": "M32ZXFGUHPCHTLUPZRY9X8GF2TVDW0S3JN54KHCE6MUA7LQPZYGSFJD6AN074RXVCEMLH8WU3TK925ACDEFGHJKLMNPQRSTUVWXY06F", + "checksum": "HPV80UNDVARHRAK", + "secret_s": "MS100C8VSM32ZXFGUHPCHTLUPZRY9X8GF2TVDW0S3JN54KHCE6MUA7LQPZYGSFJD6AN074RXVCEMLH8WU3TK925ACDEFGHJKLMNPQRSTUVWXY06FHPV80UNDVARHRAK", + "secret_hex": "dc5423251cb87175ff8110c8531d0952d8d73e1194e95b5f19d6f9df7c01111104c9baecdfea8cccc677fb9ddc8aec5553b86e528bcadfdcc201c17c638c47e9", +} + +VECTOR_6 = { + "codex32_luea": "cl10lueasd35kw6r5de5kueedxyesqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqanvrktzhlhusz", + "ident_cln2": "cln2", + "codex32_cln2": "cl10cln2sd35kw6r5de5kueedxyesqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqn9lcvcu7cez4s", + "codex32_peev": "cl10peevst6cqh0wu7p5ssjyf4z4ez42ks9jlt3zneju9uuypr2hddak6tlqsjhsks4laxts8q", +} + + +VALID_CODEX32 = [ + "A12UEL5LLGCHJ4UJCQVHG", + "a12uel5llgchj4ujcqvhg", + "a74characterlonghumanreadablepartcontainingnumber1andexcludedcharactersbio15tttgsdupy3h58nvmja", + "abcdef13qpzry9x8gf2tvdw0s3jn54khce6mua7lclc606q3t75r4", + "1199999999999999999999999999999999999999999999999999999999999999999999999999999997f7ekwq8dq7tm", + "split12checkupstagehandshakeupstreamerranterredcaperred75pe8uz2kh9ey", + "?13zyfclf624rkvjcl35t", +] + +VALID_CODEX32_LONG = [ + "A12UEL5LQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQV3RR8ZLCK96GTC3", + "a12uel5lqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqv3rr8zlck96gtc3", + "a1002characterlonghumanreadablepartthatcontainsthenumber1,theexcludedcharactersbio,andeveryus-asciicharacterin[33-126]!\"#$%&'()*+,-./0123456789:;<=>?@[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~!\"#$%&'()*+,-./0123456789:;<=>?@[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~!\"#$%&'()*+,-./0123456789:;<=>?@[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~!\"#$%&'()*+,-./0123456789:;<=>?@[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~!\"#$%&'()*+,-./0123456789:;<=>?@[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~!\"#$%&'()*+,-./0123456789:;<=>?@[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~!\"#$%&'()*+,-./0123456789:;<=>?@[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~!\"#$%&'()*+,-./0123456789:;<=>?@[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~!\"#$%&'()*+,-./0123456789:;<=>?@[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~!\"#$%&'()*+,-./0123456789:;<=>?@[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~!\"#$%&'()*+,-./0123456789:;<=>?@[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~!\"#$%&'()*+,-./0123456789:;<=>?@[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~!\"#$%&'()*+,-./0123456789:;<=>?@[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~15ttgtscr3gvktxamm8mzt", + "abcdef12l7aum6echk45nj3s0wdvt2fg8x9yrzpql7aum6echk45nj3s0wdvt2fg8x9yrzpql7aum6echk45nj3s0wdvt2fg8x9yrzpqp9evrmhc52umqew", + "1177777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777fn0jxg9gc35xwa8", + "split13checkupstagehandshakeupstreamerranterredcaperredscatteredsusurrantplunderedqsp5ws8r2klm66l", + "?17v59aaqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq2uwygewx4t4gps0", +] +INVALID_CODEX32 = { + " 12fauxxpgjxu9gyqhql4", # HRP character out of range + "\x7f" + "12fauxxk7kd7xqlns9mj", # HRP character out of range + "\x80" + "12fauxxgqp5ecwf5kzg3", # HRP character out of range + # overall max length exceeded + "a75characterslonghumanreadablepartcontainingnumber1andexcludedcharactersbio12fauxxau7wnkdhzp90r", + "x12fauxbhf2k7v7ay7ua5", # Invalid data character + "li12fauxxz4pdg55uwav3", # Too short checksum + "de12fauxxrmt7mj886swl" + "\xff", # Invalid character in checksum + "A12FAUXXMRQDLRATCD0WJ", # Checksum calculated with uppercase form of HRP +} + +INVALID_CODEX32_LONG = { + " 12fauxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx8qt67zg4n9sqylv", # HRP character out of range + "\x7f" + + "12fauxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx836hdd09mhkhkhx", # HRP character out of range + "\x80" + + "12fauxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxlhf5ywnkmk4r3tc", # HRP character out of range + # overall max length exceeded + "a1003characterslonghumanreadablepartthatcontainsthenumber1,theexcludedcharactersbio,andeveryus-asciicharacterin[33-126]!\"#$%&'()*+,-./0123456789:;<=>?@[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~!\"#$%&'()*+,-./0123456789:;<=>?@[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~!\"#$%&'()*+,-./0123456789:;<=>?@[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~!\"#$%&'()*+,-./0123456789:;<=>?@[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~!\"#$%&'()*+,-./0123456789:;<=>?@[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~!\"#$%&'()*+,-./0123456789:;<=>?@[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~!\"#$%&'()*+,-./0123456789:;<=>?@[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~!\"#$%&'()*+,-./0123456789:;<=>?@[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~!\"#$%&'()*+,-./0123456789:;<=>?@[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~!\"#$%&'()*+,-./0123456789:;<=>?@[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~!\"#$%&'()*+,-./0123456789:;<=>?@[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~!\"#$%&'()*+,-./0123456789:;<=>?@[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~!\"#$%&'()*+,-./0123456789:;<=>?@[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~12fauxxru38cppmlpu0t6l", + "y12bfauxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxt3y5fewy4gnw2hs", # Invalid data character + "lt12ifauxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxjxd0ehq868vm3zl", # Invalid data character + "in12fauxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxvgegljrsvs5w9q", # Too short checksum + "mm12fauxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxbz9tqm7y53swfaw", # Invalid character in checksum + "au12fauxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxoqz9gl44za2owxc", # Invalid character in checksum + "M12FAUXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX4DZ47P062DVJUNM", # Checksum calculated with uppercase form of HRP +} + +INVALID_MASTER_SEED = [ + # Invalid HRP + "cl10fauxs0xlxvlhemja6c4dqv22uapctqupfhlxm9h8z3k2e72q4k9hcz7vq3yaqk7cywvn0h", + # Invalid checksum algorithm (long codex32 instead of codex32) + "ms10fauxs0xlxvlhemja6c4dqv22uapctqupfhlxm9h8z3k2e72q4k9hcz7vq7rvjegpr4vhajgq", + # Invalid checksum algorithm (bech32m instead of codex32) + "ms10fauxs0xlxvlhemja6c4dqv22uapctqupfhlxm9h8z3k2e72q4k9hcz7vqu8ld7l", + # Invalid checksum algorithm (bech32 instead of codex32) + "MS10FAUXS0XLXVLHEMJA6C4DQV22UAPCTQUPFHLXM9H8Z3K2E72Q4K9HCZ7VQFM0PMA", + # Invalid checksum algorithm (bech32m instead of codex32) + "bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kemeawh", + # Invalid checksum algorithm (bech32 instead of long codex32) + "ms10fauxsxlxvlhemja6c4dqv22uapctqupfhlxm9h8z3k2e72q4k9hcz7vq0xlxvlhemja6c4dqv22uapctqupfhlxm9h8z3k2e72q4k9hcz7vq7dmzyl", + # Invalid checksum algorithm (codex32 instead of long codex32) + "ms10fauxsxlxvlhemja6c4dqv22uapctqupfhlxm9h8z3k2e72q4k9hcz7vq0xlxvlhemja6c4dqv22uapctqupfhlxm9h8z3k2e72q4k9hcz7vq3vy27qhysq096", + # Invalid character in checksum + "ms10fauxs0xlxvlhemja6c4dqv22uapctqupfhlxm9h8z3k2e72q4k9hcz7vqg4khy5dvhao86", + # Invalid seed length (15 byte) + "MS10FAUXS508D6QEJXTDG4Y5R3ZARVARYEQ3F4VK4PNLXD", + # Invalid seed length (65 bytes) + "ms10fauxsxlxvlhemja6c4dqv22uapctqupfhlxm9h8z3k2e72q4k9hcz7vq0xlxvlhemja6c4dqv22uapctqupfhlxm9h8z3k2e72q4k9hcz7vqqqetj40aljmajm22", + # Mixed case + "ms10fauxs0xlxvlhemja6c4dqv22uapctqupfhlxm9h8z3k2e72q4k9hcz7vqg4khy5dvhA686", + # More than 4 padding bits + "ms10fauxs508d6qejxtdg4y5r3zarvary234567890z2jnc3fj9mqt9", + # Empty data section + "ms1pg7d74n5v8xrz", + # Empty payload section + "ms12fauxxcel69nm8tntnn", +] + +INVALID_MASTER_SEED_ENC = [ + ("MS", "0FAUXS", bytes(16)), # Invalid uppercase + ("MS", "0fauxs", bytes(16)), # Invalid mixed case + ("ms", "0FAUXS", bytes(16)), # Invalid mixed case + ("", "0fauxs", bytes(16)), # Invalid empty HRP + ("ms", "", b"\xd0" * 32), # Invalid empty header + ("ms", "0faux", b"\x80" * 32), # Invalid missing share idx + ("ms", "0", b"\x80" * 32), # Invalid missing identifier and share idx + ("ms", "0fauxxxs", bytes(16)), # Invalid identifier length + ("ms", "fauxxs", bytes(32)), # Invalid threshold + ("ms", "0fauxx", bytes(64)), # Invalid share idx + ("ms", "0fauxs", bytes(15)), # Invalid seed length (<16 bytes) + ("ms", "0fauxs", bytes(65)), # Invalid seed length (>64 bytes) + ("ms", "0fauxs", bytes(17)), # Invalid seed length (non-multiple of 4 bytes) + ("ms", "0fauxs", bytes(18)), # Invalid seed length (non-multiple of 4 bytes) + ("ms", "0fauxs", bytes(19)), # Invalid seed length (non-multiple of 4 bytes) +] + + +BAD_CHECKSUMS = [ + "ms10fauxsxxxxxxxxxxxxxxxxxxxxxxxxxxve740yyge2ghq", + "ms10fauxsxxxxxxxxxxxxxxxxxxxxxxxxxxve740yyge2ghp", + "ms10fauxsxxxxxxxxxxxxxxxxxxxxxxxxxxxxlk3yepcstwr", + "ms10fauxsxxxxxxxxxxxxxxxxxxxxxxxxxxx6pgnv7jnpcsp", + "ms10fauxsxxxxxxxxxxxxxxxxxxxxxxxxxxxx0cpvr7n4geq", + "ms10fauxsxxxxxxxxxxxxxxxxxxxxxxxxxxxxm5252y7d3lr", + "ms10fauxsxxxxxxxxxxxxxxxxxxxxxxxxxxxrd9sukzl05ej", + "ms10fauxsxxxxxxxxxxxxxxxxxxxxxxxxxxxxc55srw5jrm0", + "ms10fauxsxxxxxxxxxxxxxxxxxxxxxxxxxxxxgc7rwhtudwc", + "ms10fauxsxxxxxxxxxxxxxxxxxxxxxxxxxxx4gy22afwghvs", + "ms10fauxsxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxme084q0vpht7pe0", + "ms10fauxsxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxme084q0vpht7pew", + "ms10fauxsxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxqyadsp3nywm8a", + "ms10fauxsxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxzvg7ar4hgaejk", + "ms10fauxsxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxcznau0advgxqe", + "ms10fauxsxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxch3jrc6j5040j", + "ms10fauxsxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx52gxl6ppv40mcv", + "ms10fauxsxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx7g4g2nhhle8fk", + "ms10fauxsxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx63m45uj8ss4x8", + "ms10fauxsxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxy4r708q7kg65x", +] + +WRONG_CHECKSUMS = [ + "ms10fauxsxxxxxxxxxxxxxxxxxxxxxxxxurfvwmdcmymdufv", + "ms10fauxsxxxxxxxxxxxxxxxxxxxxxxxxxxcsyppjkd8lz4hx3", + "ms10fauxsxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxu6hwvl5p0l9xf3c", + "ms10fauxsxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxwqey9rfs6smenxa", + "ms10fauxsxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxv70wkzrjr4ntqet", + "ms10fauxsxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx3hmlrmpa4zl0v", + "ms10fauxsxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxrfggf88znkaup", + "ms10fauxsxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxpt7l4aycv9qzj", + "ms10fauxsxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxus27z9xtyxyw3", + "ms10fauxsxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxcwm4re8fs78vn", +] + +INVALID_LENGTHS = [ + "ms10fauxsxxxxxxxxxxxxxxxxxxxxxxxxw0a4c70rfefn4", + "ms10fauxsxxxxxxxxxxxxxxxxxxxxxxxxxk4pavy5n46nea", + "ms10fauxsxxxxxxxxxxxxxxxxxxxxxxxxxxx9lrwar5zwng4w", + "ms10fauxsxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxr335l5tv88js3", + "ms10fauxsxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxvu7q9nz8p7dj68v", + "ms10fauxsxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxpq6k542scdxndq3", + "ms10fauxsxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxkmfw6jm270mz6ej", + "ms12fauxxxxxxxxxxxxxxxxxxxxxxxxxxzhddxw99w7xws", + "ms12fauxxxxxxxxxxxxxxxxxxxxxxxxxxxx42cux6um92rz", + "ms12fauxxxxxxxxxxxxxxxxxxxxxxxxxxxxxarja5kqukdhy9", + "ms12fauxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxky0ua3ha84qk8", + "ms12fauxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx9eheesxadh2n2n9", + "ms12fauxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx9llwmgesfulcj2z", + "ms12fauxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx02ev7caq6n9fgkf", +] + +INVALID_SHARE_INDEX = [ + "ms10fauxxxxxxxxxxxxxxxxxxxxxxxxxxxx0z26tfn0ulw3p", +] + +INVALID_THRESHOLD = [ + "ms1fauxxxxxxxxxxxxxxxxxxxxxxxxxxxxxda3kr3s0s2swg", +] + +INVALID_PREFIX_OR_SEPARATOR = [ + "0fauxsxxxxxxxxxxxxxxxxxxxxxxxxxxuqxkk05lyf3x2", + "10fauxsxxxxxxxxxxxxxxxxxxxxxxxxxxuqxkk05lyf3x2", + "ms0fauxsxxxxxxxxxxxxxxxxxxxxxxxxxxuqxkk05lyf3x2", + "m10fauxsxxxxxxxxxxxxxxxxxxxxxxxxxxuqxkk05lyf3x2", + "s10fauxsxxxxxxxxxxxxxxxxxxxxxxxxxxuqxkk05lyf3x2", + "0fauxsxxxxxxxxxxxxxxxxxxxxxxxxxxhkd4f70m8lgws", + "10fauxsxxxxxxxxxxxxxxxxxxxxxxxxxxhkd4f70m8lgws", + "m10fauxsxxxxxxxxxxxxxxxxxxxxxxxxxx8t28z74x8hs4l", + "s10fauxsxxxxxxxxxxxxxxxxxxxxxxxxxxh9d0fhnvfyx3x", +] + +BAD_CASES = [ + "Ms10fauxsxxxxxxxxxxxxxxxxxxxxxxxxxxuqxkk05lyf3x2", + "mS10fauxsxxxxxxxxxxxxxxxxxxxxxxxxxxuqxkk05lyf3x2", + "MS10fauxsxxxxxxxxxxxxxxxxxxxxxxxxxxuqxkk05lyf3x2", + "ms10FAUXsxxxxxxxxxxxxxxxxxxxxxxxxxxuqxkk05lyf3x2", + "ms10fauxSxxxxxxxxxxxxxxxxxxxxxxxxxxuqxkk05lyf3x2", + "ms10fauxsXXXXXXXXXXXXXXXXXXXXXXXXXXuqxkk05lyf3x2", + "ms10fauxsxxxxxxxxxxxxxxxxxxxxxxxxxxUQXKK05LYF3X2", +] diff --git a/reference/python-codex32/tests/test_bech32.py b/reference/python-codex32/tests/test_bech32.py new file mode 100644 index 0000000..7c9bc76 --- /dev/null +++ b/reference/python-codex32/tests/test_bech32.py @@ -0,0 +1,220 @@ +#!/usr/bin/python3 + +# Copyright (c) 2017 Pieter Wuille +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + + +"""Reference tests for segwit adresses""" + +import binascii +import unittest +from codex32 import segwit_addr + + +def segwit_scriptpubkey(witver, witprog): + """Construct a Segwit scriptPubKey for a given witness program.""" + return bytes([witver + 0x50 if witver else 0, len(witprog)] + witprog) + + +VALID_BECH32 = [ + "A12UEL5L", + "a12uel5l", + "an83characterlonghumanreadablepartthatcontainsthenumber1andtheexcludedcharactersbio1tt5tgs", + "abcdef1qpzry9x8gf2tvdw0s3jn54khce6mua7lmqqqxw", + "11qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqc8247j", + "split1checkupstagehandshakeupstreamerranterredcaperred2y9e3w", + "?1ezyfcl", +] + +VALID_BECH32M = [ + "A1LQFN3A", + "a1lqfn3a", + "an83characterlonghumanreadablepartthatcontainsthetheexcludedcharactersbioandnumber11sg7hg6", + "abcdef1l7aum6echk45nj3s0wdvt2fg8x9yrzpqzd3ryx", + "11llllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllludsr8", + "split1checkupstagehandshakeupstreamerranterredcaperredlc445v", + "?1v759aa", +] + +INVALID_BECH32 = [ + " 1nwldj5", # HRP character out of range + "\x7f" + "1axkwrx", # HRP character out of range + "\x80" + "1eym55h", # HRP character out of range + # overall max length exceeded + "an84characterslonghumanreadablepartthatcontainsthenumber1andtheexcludedcharactersbio1569pvx", + "pzry9x0s0muk", # No separator character + "1pzry9x0s0muk", # Empty HRP + "x1b4n0q5v", # Invalid data character + "li1dgmt3", # Too short checksum + "de1lg7wt" + "\xff", # Invalid character in checksum + "A1G7SGD8", # checksum calculated with uppercase form of HRP + "10a06t8", # empty HRP + "1qzzfhee", # empty HRP +] + +INVALID_BECH32M = [ + " 1xj0phk", # HRP character out of range + "\x7f" + "1g6xzxy", # HRP character out of range + "\x80" + "1vctc34", # HRP character out of range + # overall max length exceeded + "an84characterslonghumanreadablepartthatcontainsthetheexcludedcharactersbioandnumber11d6pts4", + "qyrz8wqd2c9m", # No separator character + "1qyrz8wqd2c9m", # Empty HRP + "y1b0jsk6g", # Invalid data character + "lt1igcx5c0", # Invalid data character + "in1muywd", # Too short checksum + "mm1crxm3i", # Invalid character in checksum + "au1s5cgom", # Invalid character in checksum + "M1VUXWEZ", # Checksum calculated with uppercase form of HRP + "16plkw9", # Empty HRP + "1p2gdwpf", # Empty HRP +] + +VALID_ADDRESS = [ + [ + "BC1QW508D6QEJXTDG4Y5R3ZARVARY0C5XW7KV8F3T4", + "0014751e76e8199196d454941c45d1b3a323f1433bd6", + ], + [ + "tb1qrp33g0q5c5txsp9arysrx4k6zdkfs4nce4xj0gdcccefvpysxf3q0sl5k7", + "00201863143c14c5166804bd19203356da136c985678cd4d27a1b8c6329604903262", + ], + [ + "bc1pw508d6qejxtdg4y5r3zarvary0c5xw7kw508d6qejxtdg4y5r3zarvary0c5xw7kt5nd6y", + "5128751e76e8199196d454941c45d1b3a323f1433bd6751e76e8199196d454941c45d1b3a323f1433bd6", + ], + ["BC1SW50QGDZ25J", "6002751e"], + ["bc1zw508d6qejxtdg4y5r3zarvaryvaxxpcs", "5210751e76e8199196d454941c45d1b3a323"], + [ + "tb1qqqqqp399et2xygdj5xreqhjjvcmzhxw4aywxecjdzew6hylgvsesrxh6hy", + "0020000000c4a5cad46221b2a187905e5266362b99d5e91c6ce24d165dab93e86433", + ], + [ + "tb1pqqqqp399et2xygdj5xreqhjjvcmzhxw4aywxecjdzew6hylgvsesf3hn0c", + "5120000000c4a5cad46221b2a187905e5266362b99d5e91c6ce24d165dab93e86433", + ], + [ + "bc1p0xlxvlhemja6c4dqv22uapctqupfhlxm9h8z3k2e72q4k9hcz7vqzk5jj0", + "512079be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798", + ], +] + +INVALID_ADDRESS = [ + # Invalid HRP + "tc1p0xlxvlhemja6c4dqv22uapctqupfhlxm9h8z3k2e72q4k9hcz7vq5zuyut", + # Invalid checksum algorithm (bech32 instead of bech32m) + "bc1p0xlxvlhemja6c4dqv22uapctqupfhlxm9h8z3k2e72q4k9hcz7vqh2y7hd", + # Invalid checksum algorithm (bech32 instead of bech32m) + "tb1z0xlxvlhemja6c4dqv22uapctqupfhlxm9h8z3k2e72q4k9hcz7vqglt7rf", + # Invalid checksum algorithm (bech32 instead of bech32m) + "BC1S0XLXVLHEMJA6C4DQV22UAPCTQUPFHLXM9H8Z3K2E72Q4K9HCZ7VQ54WELL", + # Invalid checksum algorithm (bech32m instead of bech32) + "bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kemeawh", + # Invalid checksum algorithm (bech32m instead of bech32) + "tb1q0xlxvlhemja6c4dqv22uapctqupfhlxm9h8z3k2e72q4k9hcz7vq24jc47", + # Invalid character in checksum + "bc1p38j9r5y49hruaue7wxjce0updqjuyyx0kh56v8s25huc6995vvpql3jow4", + # Invalid witness version + "BC130XLXVLHEMJA6C4DQV22UAPCTQUPFHLXM9H8Z3K2E72Q4K9HCZ7VQ7ZWS8R", + # Invalid program length (1 byte) + "bc1pw5dgrnzv", + # Invalid program length (41 bytes) + "bc1p0xlxvlhemja6c4dqv22uapctqupfhlxm9h8z3k2e72q4k9hcz7v8n0nx0muaewav253zgeav", + # Invalid program length for witness version 0 (per BIP141) + "BC1QR508D6QEJXTDG4Y5R3ZARVARYV98GJ9P", + # Mixed case + "tb1p0xlxvlhemja6c4dqv22uapctqupfhlxm9h8z3k2e72q4k9hcz7vq47Zagq", + # More than 4 padding bits + "bc1p0xlxvlhemja6c4dqv22uapctqupfhlxm9h8z3k2e72q4k9hcz7v07qwwzcrf", + # Non-zero padding in 8-to-5 conversion + "tb1p0xlxvlhemja6c4dqv22uapctqupfhlxm9h8z3k2e72q4k9hcz7vpggkg4j", + # Empty data section + "bc1gmk9yu", +] + +INVALID_ADDRESS_ENC = [ + ("BC", 0, 20), + ("bc", 0, 21), + ("bc", 17, 32), + ("bc", 1, 1), + ("bc", 16, 41), +] + + +class TestSegwitAddress(unittest.TestCase): + """Unit test class for segwit addressess.""" + + def test_valid_checksum(self): + """Test checksum creation and validation.""" + for spec in segwit_addr.Encoding: + tests = ( + VALID_BECH32 if spec == segwit_addr.Encoding.BECH32 else VALID_BECH32M + ) + for test in tests: + hrp, _, dspec = segwit_addr.bech32_decode(test) + self.assertTrue(hrp is not None and dspec == spec) + pos = test.rfind("1") + test = test[: pos + 1] + chr(ord(test[pos + 1]) ^ 1) + test[pos + 2 :] + hrp, _, dspec = segwit_addr.bech32_decode(test) + self.assertIsNone(hrp) + + def test_invalid_checksum(self): + """Test validation of invalid checksums.""" + for spec in segwit_addr.Encoding: + tests = ( + INVALID_BECH32 + if spec == segwit_addr.Encoding.BECH32 + else INVALID_BECH32M + ) + for test in tests: + hrp, _, dspec = segwit_addr.bech32_decode(test) + self.assertTrue(hrp is None or dspec != spec) + + def test_valid_address(self): + """Test whether valid addresses decode to the correct output.""" + for address, hexscript in VALID_ADDRESS: + hrp = "bc" + witver, witprog = segwit_addr.decode(hrp, address) + if witver is None: + hrp = "tb" + witver, witprog = segwit_addr.decode(hrp, address) + self.assertIsNotNone(witver, address) + scriptpubkey = segwit_scriptpubkey(witver, witprog) + self.assertEqual(scriptpubkey, binascii.unhexlify(hexscript)) + addr = segwit_addr.encode(hrp, witver, witprog) + self.assertEqual(address.lower(), addr) + + def test_invalid_address(self): + """Test whether invalid addresses fail to decode.""" + for test in INVALID_ADDRESS: + witver, _ = segwit_addr.decode("bc", test) + self.assertIsNone(witver) + witver, _ = segwit_addr.decode("tb", test) + self.assertIsNone(witver) + + def test_invalid_address_enc(self): + """Test whether address encoding fails on invalid input.""" + for hrp, version, length in INVALID_ADDRESS_ENC: + code = segwit_addr.encode(hrp, version, [0] * length) + self.assertIsNone(code) + + +if __name__ == "__main__": + unittest.main() diff --git a/reference/python-codex32/tests/test_bip93.py b/reference/python-codex32/tests/test_bip93.py new file mode 100644 index 0000000..8a5d15f --- /dev/null +++ b/reference/python-codex32/tests/test_bip93.py @@ -0,0 +1,243 @@ +# tests/test_bip93.py +"""Tests for BIP-93 codex32 implementation.""" +import pytest +from data.bip93_vectors import ( + VECTOR_1, + VECTOR_2, + VECTOR_3, + VECTOR_4, + VECTOR_5, + VECTOR_6, + VALID_CODEX32, + VALID_CODEX32_LONG, + INVALID_CODEX32, + INVALID_CODEX32_LONG, + INVALID_MASTER_SEED, + INVALID_MASTER_SEED_ENC, + BAD_CHECKSUMS, + WRONG_CHECKSUMS, + INVALID_LENGTHS, + INVALID_SHARE_INDEX, + INVALID_THRESHOLD, + INVALID_PREFIX_OR_SEPARATOR, + BAD_CASES, +) +from codex32.bip93 import ( + Codex32String, + InvalidSeedLength, + MismatchedHrp, + MismatchedLength, + codex32_decode, + encode, + decode, + InvalidThreshold, + InvalidShareIndex, +) +from codex32.bech32 import ( + InvalidCase, + InvalidChecksum, + InvalidDataValue, + IncompleteGroup, + InvalidLength, + InvalidChar, + MissingHrp, + SeparatorNotFound, +) +from codex32.checksums import CODEX32, CODEX32_LONG + + +def test_parts(): + """Test Vector 1: parse a codex32 string into parts""" + s = Codex32String(VECTOR_1["secret_s"]) + assert str(s) == VECTOR_1["secret_s"] + assert s.hrp == VECTOR_1["hrp"] + assert s.k == VECTOR_1["k"] + assert s.share_idx == VECTOR_1["share_index"] + assert s.ident == VECTOR_1["identifier"] + assert s.payload == VECTOR_1["payload"] + assert s.checksum == VECTOR_1["checksum"] + assert s.data.hex() == VECTOR_1["secret_hex"] + + +def test_derive_and_recover(): + """Test Vector 2: derive new share and recover the secret""" + a = Codex32String(VECTOR_2["share_A"]) + c = Codex32String(VECTOR_2["share_C"]) + # interpolation target is 'D' (uppercase as inputs are uppercase) + d = Codex32String.interpolate_at([a, c], "D") + assert str(d) == VECTOR_2["derived_D"] + s = Codex32String.interpolate_at([a, c], "S") + assert str(s) == VECTOR_2["secret_S"] + assert s.data.hex() == VECTOR_2["secret_hex"] + + +def test_from_seed_and_interpolate_3_of_5(): + """Test Vector 3: encode secret share from seed and split 3-of-5""" + seed = bytes.fromhex(VECTOR_3["secret_hex"]) + a = Codex32String(VECTOR_3["share_a"]) + c = Codex32String(VECTOR_3["share_c"]) + s = Codex32String.from_seed(seed, f"{a.hrp}1{a.k}{a.ident}", 0) + assert str(s) == VECTOR_3["secret_s"] + d = Codex32String.interpolate_at([s, a, c], "d") + e = Codex32String.interpolate_at([s, a, c], "e") + f = Codex32String.interpolate_at([s, a, c], "f") + assert str(d) == VECTOR_3["derived_d"] + assert str(e) == VECTOR_3["derived_e"] + assert str(f) == VECTOR_3["derived_f"] + for pad_val in range(0b11 + 1): + s = Codex32String.from_seed(seed, f"{a.hrp}1{a.k}{a.ident}", pad_val) + assert str(s) == VECTOR_3[f"secret_s_alternate_{pad_val}"] + + +def test_from_seed_and_alternates(): + """Test Vector 4: encode secret share from seed""" + seed = bytes.fromhex(VECTOR_4["secret_hex"]) + for pad_val in range(0b1111 + 1): + # confirm all 16 encodings decode to same master data + s = Codex32String.from_seed(seed, "ms10leet", pad_val) + assert str(s) == VECTOR_4[f"secret_s_alternate_{pad_val}"] + assert s.data.hex() == VECTOR_4["secret_hex"] + + +def test_long_string(): + """Test Vector 5: decode long codex32 secret and confirm secret bytes.""" + s = Codex32String.from_unchecksummed_string( + VECTOR_5["hrp"] + + "1" + + VECTOR_5["k"] + + VECTOR_5["identifier"] + + VECTOR_5["share_idx"] + + VECTOR_5["payload"] + ) + assert s.checksum == VECTOR_5["checksum"] + assert str(s) == VECTOR_5["secret_s"] + assert s.data.hex() == VECTOR_5["secret_hex"] + long_str = VECTOR_5["secret_s"] + long_seed = Codex32String(long_str) + assert long_seed.data.hex() == VECTOR_5["secret_hex"] + + +def test_alternate_hrp(): + """Test Vector 6: codex32 strings with "cl" HRP.""" + c0 = Codex32String(VECTOR_6["codex32_luea"]) + assert str(c0) == VECTOR_6["codex32_luea"] + c0.ident = VECTOR_6["ident_cln2"] + assert str(c0) == VECTOR_6["codex32_cln2"] + c1 = Codex32String(VECTOR_6["codex32_cln2"]) + assert str(c1) == VECTOR_6["codex32_cln2"] + c2 = Codex32String.from_string("cl", VECTOR_6["codex32_peev"]) + assert str(c2) == VECTOR_6["codex32_peev"] + + +def test_valid_codex32(): + """Test checksum creation and validation.""" + for spec in CODEX32, CODEX32_LONG: + tests = VALID_CODEX32 if spec == CODEX32 else VALID_CODEX32_LONG + for test in tests: + hrp, _, dspec = codex32_decode(test) + assert hrp is not None and dspec == spec + pos = test.rfind("1") + test = test[: pos + 1] + chr(ord(test[pos + 1]) ^ 1) + test[pos + 2 :] + with pytest.raises(InvalidChecksum): + codex32_decode(test) + + +def test_invalid_checksum(): + """Test validation of invalid checksums.""" + for spec in CODEX32, CODEX32_LONG: + tests = INVALID_CODEX32 if spec == CODEX32 else INVALID_CODEX32_LONG + for test in tests: + with pytest.raises( + (InvalidChecksum, InvalidLength, InvalidChar, AssertionError) + ): + _, _, dspec = codex32_decode(test) + assert dspec != spec + + +def test_invalid_master_seed(): + """Test whether invalid addresses fail to decode.""" + for test in INVALID_MASTER_SEED: + with pytest.raises( + ( + MismatchedHrp, + MismatchedLength, + InvalidChecksum, + InvalidSeedLength, + InvalidLength, + InvalidChar, + InvalidThreshold, + InvalidCase, + IncompleteGroup, + ) + ): + decode("ms", test) + + +def test_invalid_master_seed_enc(): + """Test whether master seed encoding fails on invalid input.""" + for hrp, header, data in INVALID_MASTER_SEED_ENC: + with pytest.raises( + ( + MissingHrp, + MismatchedLength, + InvalidSeedLength, + InvalidChar, + InvalidCase, + InvalidDataValue, + InvalidThreshold, + InvalidShareIndex, + AssertionError, + ) + ): + encode(hrp, header, data) + + +def test_bad_checksums(): + """Test strings with bad checksums.""" + for chk in BAD_CHECKSUMS: + with pytest.raises((InvalidChecksum)): + Codex32String(chk) + + +def test_wrong_checksums_or_length(): + """Test strings with wrong checksums or lengths.""" + for chk in WRONG_CHECKSUMS: + with pytest.raises( + (InvalidLength, InvalidSeedLength, IncompleteGroup, InvalidChecksum) + ): + Codex32String(chk) + + +def test_invalid_length(): + """Test strings with invalid lengths.""" + for chk in INVALID_LENGTHS: + with pytest.raises((InvalidLength, InvalidSeedLength, IncompleteGroup)): + Codex32String(chk) + + +def test_invalid_index(): + """Test strings with invalid share indices.""" + for chk in INVALID_SHARE_INDEX: + with pytest.raises(InvalidShareIndex): + Codex32String(chk) + + +def test_invalid_threshold(): + """Test strings with invalid threshold characters.""" + for chk in INVALID_THRESHOLD: + with pytest.raises(InvalidThreshold): + Codex32String(chk) + + +def test_invalid_prefix_or_separator(): + """Test strings with invalid prefixes or separators.""" + for chk in INVALID_PREFIX_OR_SEPARATOR: + with pytest.raises((MismatchedHrp, SeparatorNotFound, MissingHrp)): + Codex32String.from_string("ms", chk) + + +def test_invalid_case_examples(): + """Test strings with invalid casing.""" + for chk in BAD_CASES: + with pytest.raises(InvalidCase): + Codex32String(chk) diff --git a/reference/python-codex32/tests/test_roundtrip_interpolated.py b/reference/python-codex32/tests/test_roundtrip_interpolated.py new file mode 100644 index 0000000..6c6dfbe --- /dev/null +++ b/reference/python-codex32/tests/test_roundtrip_interpolated.py @@ -0,0 +1,40 @@ +"""Test for round-trip encoding/decoding and recovery via interpolation.""" + +from codex32 import Codex32String, decode + + +def test_round_trip_recovery(): + """Test round-trip encoding/decoding and recovery via interpolation.""" + # secret share from seed + s = Codex32String.from_seed( + bytes.fromhex("68f14219957131d21b615271058437e8"), "ms13k00ls" + ) + assert s.s == "ms13k00lsdrc5yxv4wycayxmp2fcstpphaq55a60p9hfds9t" + a = Codex32String.from_seed( + bytes.fromhex("641be1cb12c97ede1c6bad8edf067760"), "ms13k00la" + ) + assert a.s == "ms13k00lavsd7rjcje9ldu8rt4k8d7pnhvppyrt5gpff9wwl" + c = Codex32String.from_seed( + bytes.fromhex("61b3c4052f7a31dc2b425c843a13c9b4"), "ms13k00lc" + ) + assert c.s == "ms13k00lcvxeugpf00gcac26ztjzr5y7fknx9cw72l8md0xn" + # derive next share via interpolation + d = Codex32String.interpolate_at([s, a, c], "d") + assert d.s == "ms13k00ldp4v5nw8lph96x47mjxzgwjexehw766s4dmj6qx8" + + # now round-trip d share ('d' is derived via interpolation, NOT via 'from_seed') + dd = Codex32String.from_seed(d.data, "ms13k00ld", d.pad_val) + assert dd.s == d.s + + e = Codex32String.interpolate_at([s, a, c], "e") + f = Codex32String.interpolate_at([s, a, c], "f") + assert e.s == "ms13k00lezuknydaaygk5u20zs4fm736vj6zlcrjhtduanyk" + assert f.s == "ms13k00lf0ehe53zsu6vrxcjjh9v7wzsa8vd9pjk28l8zavw" + + # recover from shares, use 'd' without round-trip + rec_s = Codex32String.interpolate_at([a, c, d], "s") + # recover from shares, use 'd' after round-trip + rec_ss = Codex32String.interpolate_at([a, c, dd], "s") + # confirm recovered secrets and padding match original + assert decode(s.hrp, rec_s.s, "CRC") == ("3k00ls", s.data, "CRC") + assert decode(s.hrp, rec_ss.s, "CRC") == ("3k00ls", s.data, "CRC")