Skip to content

Commit 4ef0ca1

Browse files
committed
Added codec: bech32
1 parent 0b48da2 commit 4ef0ca1

4 files changed

Lines changed: 134 additions & 0 deletions

File tree

‎README.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -286,6 +286,7 @@ CACTCGGTCGGCCATATGTTCGGCCATATGTTCGTCTGTTCACTCGCCCATACACT
286286
- [X] `base2048`: [Base2048](https://github.com/qntm/base2048) custom conversion (11 bits per character, optimised for Twitter)
287287
- [X] `base32768`: [Base32768](https://github.com/qntm/base32768) custom conversion (15 bits per character, optimised for UTF-16)
288288
- [X] `base65536`: [Base65536](https://github.com/qntm/base65536) custom conversion (16 bits per character, optimised for UTF-32)
289+
- [X] `bech32`: [Bech32](https://github.com/bitcoin/bips/blob/master/bip-0173.mediawiki) encoding with a human-readable part ; `bech32m` for its [Bech32m](https://github.com/bitcoin/bips/blob/master/bip-0350.mediawiki) variant
289290
- [X] `base-genericN`: see [base encodings](https://python-codext.readthedocs.io/en/latest/enc/base.html) ; supports any possible base
290291

291292
This category also contains `ascii85`, `adobe`, `[x]btoa`, `zeromq` with the `base85` codec.

‎docs/pages/enc/base.md‎

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,8 @@ This encoding implements various different versions of Base85.
156156
`base122` | text <-> Base122 encoded text | `base[-_]?122` | Python 3 only
157157
`base128` | text <-> Base128 encoded text | `base[-_]?128` | Relies on the ASCII charset
158158
`base1024` | data <-> Ecoji encoded text | `base[-_]?1024`, `ecoji`, `ecoji-v1`, `ecoji-v2` | 10 bits per emoji ; encodes with Ecoji version 2 by default
159+
`bech32` | data <-> Bech32 encoded text | `bech32-<hrp>` | human-readable part required for encoding
160+
`bech32m` | data <-> Bech32m encoded text | `bech32m-<hrp>` | human-readable part required for encoding
159161
`base2048` | data <-> Base2048 encoded text | `base[-_]?2048` | 11 bits per character, optimised for Twitter
160162
`base32768` | data <-> Base32768 encoded text | `base[-_]?32768` | 15 bits per character, optimised for UTF-16
161163
`base65536` | data <-> Base65536 encoded text | `base[-_]?65536` | 16 bits per character, optimised for UTF-32
@@ -197,3 +199,14 @@ Base1024, also known as [Ecoji](https://github.com/keith-turner/ecoji), encodes
197199
>>> codext.decode("🏯🔩🚗🌷🍌☕☕☕", "base1024")
198200
'Hello!'
199201
```
202+
203+
Bech32 ([BIP-173](https://github.com/bitcoin/bips/blob/master/bip-0173.mediawiki)) and Bech32m ([BIP-350](https://github.com/bitcoin/bips/blob/master/bip-0350.mediawiki)) write the data with a human-readable part (HRP), the separator "`1`", then the data regrouped by 5 bits in the alphabet "`qpzry9x8gf2tvdw0s3jn54khce6mua7l`", followed by a 6-character checksum computed over the HRP and the data. The HRP is required for encoding. When decoding, it is read from the text and only checked if given with the codec name ; the data is returned without it. The length limit of 90 characters of BIP-173 is not enforced. SegWit addresses, whose first data character is the witness version, are not decoded as such.
204+
205+
```python
206+
>>> codext.encode("Hello, World!", "bech32-test")
207+
'test1fpjkcmr09ss9wmmjd3jzzw2fmd3'
208+
>>> codext.decode("test1fpjkcmr09ss9wmmjd3jzzw2fmd3", "bech32")
209+
'Hello, World!'
210+
>>> codext.encode("Hello, World!", "bech32m-test")
211+
'test1fpjkcmr09ss9wmmjd3jzzmkehgn'
212+
```

‎src/codext/base/__init__.py‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
from .base100 import *
99
from .base122 import *
1010
from .base1024 import *
11+
from .bech32 import *
1112
from .base65536 import *
1213
from .baseN import *
1314
from ..__common__ import *

‎src/codext/base/bech32.py‎

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
# -*- coding: UTF-8 -*-
2+
"""Bech32/Bech32m Codecs - bech32 and bech32m content encodings.
3+
4+
Bech32 (BIP-173) and Bech32m (BIP-350) write the data with a human-readable part (HRP), the separator "1", then the
5+
data regrouped by 5 bits (the last group being padded with zero bits) in the alphabet
6+
"qpzry9x8gf2tvdw0s3jn54khce6mua7l", followed by a 6-character BCH checksum computed over the HRP and the data ; both
7+
only differ by the constant of their checksum.
8+
9+
The HRP is required for encoding (e.g. "bech32-bc") ; without it, a ValueError is raised. When decoding, it is read
10+
from the text and only checked if given with the codec name ; the data is returned without it. The length limit of 90
11+
characters of BIP-173 is not enforced.
12+
13+
NB: this does not decode SegWit addresses, whose first data character is the witness version.
14+
15+
This codec:
16+
- en/decodes strings from str to str
17+
- en/decodes strings from bytes to bytes
18+
- decodes file content to str (read)
19+
- encodes file content from str to bytes (write)
20+
21+
Reference: https://github.com/bitcoin/bips/blob/master/bip-0173.mediawiki
22+
https://github.com/bitcoin/bips/blob/master/bip-0350.mediawiki
23+
"""
24+
from ..__common__ import *
25+
26+
27+
_DATA = "\x00\x44\x32\x14\xc7\x42\x54\xb6\x35\xcf\x84\x65\x3a\x56\xd7\xc6\x75\xbe\x77\xdf"
28+
# data whose 5-bit values are 0, 1, ..., 31 (Bech32 test vector) and 31, 30, ..., 0 (Bech32m test vector)
29+
_DATAM = "\xff\xbb\xcd\xeb\x38\xbd\xab\x49\xca\x30\x7b\x9a\xc5\xa9\x28\x39\x8a\x41\x88\x20"
30+
__examples1__ = {
31+
'enc(bech32)': {'test': None},
32+
'enc(bech32-abcdef)': {_DATA: "abcdef1qpzry9x8gf2tvdw0s3jn54khce6mua7lmqqqxw", '': "abcdef1hu3qdg"},
33+
'dec(bech32)': {'abcdef1qpzry9x8gf2tvdw0s3jn54khce6mua7lmqqqxw': _DATA, 'A12UEL5L': "", 'a12uel5l': "",
34+
'?1ezyfcl': "", 'split1checkupstagehandshakeupstreamerranterredcaperred2y9e3w':
35+
"\xc5\xf3\x8b\x70\x30\x5f\x51\x9b\xf6\x6d\x85\xfb\x6c\xf0\x30\x58\xf3\xdd\xe4\x63\xec\xd7"
36+
"\x91\x8f\x2d\xc7\x43\x91\x8f\x2d",
37+
"an83characterlonghumanreadablepartthatcontainsthenumber1andtheexcludedcharactersbio"
38+
"1tt5tgs": "", 'pzry9x0s0muk': None, '1pzry9x0s0muk': None, 'x1b4n0q5v': None,
39+
'li1dgmt3': None, 'A1G7SGD8': None, '10a06t8': None, '1qzzfhee': None,
40+
'aBcDef1qpzry9x8gf2tvdw0s3jn54khce6mua7lmqqqxw': None, 'a1lqfn3a': None, 'a1qq': None,
41+
"11" + "q" * 82 + "c8247j": "\x00" * 51},
42+
'dec(bech32-abcdef)': {'abcdef1qpzry9x8gf2tvdw0s3jn54khce6mua7lmqqqxw': _DATA, 'a12uel5l': None},
43+
'enc-dec(bech32-a|bech32_bc|bech32-cosmos)': ["", "Hello, World!", "@random{512,1024}"],
44+
}
45+
__examples2__ = {
46+
'enc(bech32m)': {'test': None},
47+
'enc(bech32m-abcdef)': {_DATAM: "abcdef1l7aum6echk45nj3s0wdvt2fg8x9yrzpqzd3ryx"},
48+
'dec(bech32m)': {'abcdef1l7aum6echk45nj3s0wdvt2fg8x9yrzpqzd3ryx': _DATAM, 'a1lqfn3a': "",
49+
'A1LQFN3A': "", '?1v759aa': "", 'a12uel5l': None, 'M1VUXWEZ': None, 'abc1rzg': None,
50+
# valid Bech32m string, but its padding bits ("11") are not zero, hence no valid bytes
51+
"11" + "l" * 82 + "ludsr8": None},
52+
'enc-dec(bech32m-a|bech32m_bc|bech32m-cosmos)': ["", "Hello, World!", "@random{512,1024}"],
53+
}
54+
__guess1__, __guess2__ = ["bech32"], ["bech32m"]
55+
56+
57+
CHARSET, CONSTANTS = "qpzry9x8gf2tvdw0s3jn54khce6mua7l", {'bech32': 1, 'bech32m': 0x2bc830a3}
58+
59+
60+
def __polymod(values):
61+
""" BCH checksum computation, as defined in BIP-173. """
62+
chk = 1
63+
for v in values:
64+
b = chk >> 25
65+
chk = (chk & 0x1ffffff) << 5 ^ v
66+
for i, g in enumerate((0x3b6a57b2, 0x26508e6d, 0x1ea119fa, 0x3d4233dd, 0x2a1462b3)):
67+
chk ^= g if b >> i & 1 else 0
68+
return chk
69+
70+
71+
def __hrp_expand(hrp):
72+
return [ord(c) >> 5 for c in hrp] + [0] + [ord(c) & 31 for c in hrp]
73+
74+
75+
def __bech32(name, decode=False):
76+
def _wrapper(hrp=None):
77+
# an HRP made of digits is given as an integer by the codec name pattern
78+
hrp = str(hrp or "")
79+
# a missing HRP is only reported when encoding, as the bare codec name is used for decoding (e.g. when guessing)
80+
def _code(text, errors="strict"):
81+
_h, text = handle_error(name, errors, decode=decode, kind="token"), ensure_str(text)
82+
if not decode:
83+
if not hrp:
84+
raise ValueError(f"Bad parameter for encoding '{name}': a human-readable part is required")
85+
bits = "".join(f"{x:08b}" for x in b(text))
86+
bits += "0" * (-len(bits) % 5)
87+
values = [int(bits[i:i+5], 2) for i in range(0, len(bits), 5)]
88+
chk = __polymod(__hrp_expand(hrp) + values + [0] * 6) ^ CONSTANTS[name]
89+
values += [chk >> 5 * (5 - i) & 31 for i in range(6)]
90+
return (s := hrp + "1" + "".join(CHARSET[v] for v in values)), len(s)
91+
# strict mode: mixed case, missing separator or HRP, too short checksum, wrong characters or checksum,
92+
# wrong HRP and non-zero padding are errors ; otherwise, the data is decoded as far as possible
93+
if text.lower() != text and text.upper() != text:
94+
_h(text, 0)
95+
text = text.lower()
96+
if (pos := text.rfind("1")) < 1 or pos + 7 > len(text) or any(not 33 <= ord(c) <= 126 for c in text[:pos]):
97+
return (r := b(_h(text, 0))), len(r)
98+
h, values = text[:pos], []
99+
if hrp and h != hrp:
100+
_h(h, 0)
101+
for i, c in enumerate(text[pos+1:], pos + 1):
102+
if c in CHARSET:
103+
values.append(CHARSET.index(c))
104+
else:
105+
_h(c, i)
106+
if __polymod(__hrp_expand(h) + values) != CONSTANTS[name]:
107+
_h(text[-6:], len(text) - 6)
108+
bits = "".join(f"{v:05b}" for v in values[:-6])
109+
if len(bits) % 8 >= 5 or "1" in bits[len(bits) - len(bits) % 8:]:
110+
_h(text[-7], len(text) - 7)
111+
r = bytes(int(bits[i:i+8], 2) for i in range(0, len(bits) - 7, 8))
112+
return r, len(r)
113+
return _code
114+
return _wrapper
115+
116+
117+
for _n, _ex, _gs in [("bech32", __examples1__, __guess1__), ("bech32m", __examples2__, __guess2__)]:
118+
add(_n, __bech32(_n), __bech32(_n, True), rf"^{_n}(?:[-_]([a-z0-9]+))?$", examples=_ex, guess=_gs,
119+
printables_rate=1., expansion_factor=1.6)

0 commit comments

Comments
 (0)