Skip to content

Commit 8bf06fa

Browse files
committed
remove Parts class, make parts properties
1 parent 9db6166 commit 8bf06fa

File tree

3 files changed

+57
-113
lines changed

3 files changed

+57
-113
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ Shamir-share interpolation helpers and helpers to build codex32 strings from see
1111
- Short checksum (13 chars) and long checksum (15 chars) support.
1212
- Construct codex32 strings from raw seed bytes via `from_seed`.
1313
- CRC-based default padding scheme for `from_seed`.
14-
- Parse codex32 strings and access parts via `Parts`.
14+
- Parse codex32 strings and access parts via properties.
1515
- Interpolate/recover shares via `interpolate_at`.
1616
- Default identifier is the bech32-encoded BIP32 fingerprint.
1717

src/codex32/codex32.py

Lines changed: 41 additions & 93 deletions
Original file line numberDiff line numberDiff line change
@@ -321,6 +321,31 @@ class Codex32String:
321321

322322
def __init__(self, s=""):
323323
self.s = s
324+
self.hrp, data = bech32_decode(self.s)
325+
_, s = self.s.rsplit("1", 1)
326+
if len(s) < 94 and len(s) > 44:
327+
checksum_len = 13
328+
elif len(s) >= 96 and len(s) < 125:
329+
checksum_len = 15
330+
else:
331+
raise InvalidLength(f"{len(s)} must be 45-93 or 96 to 124")
332+
threshold_char = s[0]
333+
if threshold_char.isdigit() and threshold_char != "1":
334+
k = int(threshold_char)
335+
else:
336+
raise InvalidThreshold(threshold_char)
337+
self.k = k
338+
self.ident = s[1:5]
339+
self.share_index = s[5]
340+
self.payload = s[6 : len(s) - checksum_len]
341+
self.checksum = s[-checksum_len:]
342+
if self.k == 0 and self.share_index.lower() != "s":
343+
raise InvalidShareIndex(self.share_index + "must be 's' when k=0")
344+
if not ms32_verify_checksum(data):
345+
raise InvalidChecksum(f"string={s}")
346+
incomplete_group = (len(self.payload) * 5) % 8
347+
if incomplete_group > 4:
348+
raise IncompleteGroup(str(incomplete_group))
324349

325350
def __str__(self):
326351
return self.s
@@ -333,79 +358,44 @@ def __eq__(self, other):
333358
def __hash__(self):
334359
return hash(self.s)
335360

336-
def sanity_check(self):
337-
"""Perform sanity check on the codex32 string."""
338-
parts = self.parts
339-
incomplete_group = (len(parts.payload) * 5) % 8
340-
if incomplete_group > 4:
341-
raise IncompleteGroup(str(incomplete_group))
361+
@property
362+
def data(self):
363+
"""Return the payload data bytes."""
364+
return bytes(convertbits(bech32_to_u5(self.payload), 5, 8, False))
342365

343366
@classmethod
344367
def from_unchecksummed_string(cls, s, hrp="ms"):
345368
"""Create Codex32String from unchecksummed string."""
346-
hrp, data = bech32_decode(s, hrp=hrp)
369+
_, data = bech32_decode(s, hrp=hrp)
347370
ret = cls(bech32_encode(data + ms32_create_checksum(data), hrp))
348-
ret.sanity_check()
349371
return ret
350372

351373
@classmethod
352374
def from_string(cls, s, hrp="ms"):
353375
"""Create Codex32String from a codex32 string."""
354-
_, data = bech32_decode(s, hrp=hrp)
355-
if not ms32_verify_checksum(data):
356-
raise InvalidChecksum(f"string={s}")
357376
ret = cls(s)
358-
ret.sanity_check()
359-
return ret
360-
361-
@property
362-
def parts(self):
363-
"""Get parts of the codex32 string."""
364-
hrp, s = self.s.rsplit("1", 1) if "1" in self.s else ("", self.s)
365-
if len(s) < 94 and len(s) > 44:
366-
checksum_len = 13
367-
elif len(s) >= 96 and len(s) < 125:
368-
checksum_len = 15
369-
else:
370-
raise InvalidLength(f"{len(s)} must be 45-93 or 96 to 124")
371-
threshold_char = s[0]
372-
if threshold_char.isdigit() and threshold_char != "1":
373-
k = int(threshold_char)
374-
else:
375-
raise InvalidThreshold(threshold_char)
376-
ret = Parts(
377-
hrp=hrp,
378-
k=k,
379-
ident=s[1:5],
380-
share_index=s[5],
381-
payload=s[6 : len(s) - checksum_len],
382-
checksum=s[-checksum_len:],
383-
)
384-
if ret.k == 0 and ret.share_index.lower() != "s":
385-
raise InvalidShareIndex(ret.share_index + "must be 's' when k=0")
386377
return ret
387378

388379
@classmethod
389-
def interpolate_at(cls, shares, target):
380+
def interpolate_at(cls, shares, target="s"):
390381
"""Interpolate to a specific target share index."""
391382
indices = []
392383
ms32_shares = []
393-
s0_parts = shares[0].parts
384+
s0_parts = shares[0]
394385
if s0_parts.k > len(shares):
395386
raise ThresholdNotPassed(f"threshold={s0_parts.k}, n_shares={len(shares)}")
396387
for share in shares:
397-
parts = share.parts
398388
if len(shares[0].s) != len(share.s):
399389
raise MismatchedLength(f"{len(shares[0].s)}, {len(share.s)}")
400-
if s0_parts.hrp != parts.hrp:
401-
raise MismatchedHrp(f"{s0_parts.hrp}, {parts.hrp}")
402-
if s0_parts.k != parts.k:
403-
raise MismatchedThreshold(f"{s0_parts.k}, {parts.k}")
404-
if s0_parts.ident != parts.ident:
405-
raise MismatchedId(f"{s0_parts.ident}, {parts.ident}")
406-
if parts.share_index in indices:
407-
raise RepeatedIndex(parts.share_index)
408-
indices.append(parts.share_index)
390+
if s0_parts.hrp != share.hrp:
391+
raise MismatchedHrp(f"{s0_parts.hrp}, {share.hrp}")
392+
if s0_parts.k != share.k:
393+
raise MismatchedThreshold(f"{s0_parts.k}, {share.k}")
394+
if s0_parts.ident != share.ident:
395+
raise MismatchedId(f"{s0_parts.ident}, {share.ident}")
396+
if share.share_index in indices:
397+
raise RepeatedIndex(share.share_index)
398+
indices.append(share.share_index)
409399
ms32_shares.append(bech32_decode(share.s)[1])
410400
for i, share in enumerate(shares):
411401
if indices[i] == target:
@@ -432,45 +422,3 @@ def from_seed(cls, data, ident="", hrp="ms", k=0, share_idx="s", pad_val=None):
432422
combined = header + payload
433423
ret = bech32_encode(combined + ms32_create_checksum(combined), hrp)
434424
return cls(ret)
435-
436-
437-
class Parts:
438-
"""Class representing parts of a Codex32 string."""
439-
440-
# pylint: disable=too-many-arguments,too-many-positional-arguments
441-
def __init__(self, hrp, k, ident, share_index, payload, checksum):
442-
self.hrp = hrp
443-
self.k = k
444-
self.ident = ident
445-
self.share_index = share_index
446-
self.payload = payload
447-
self.checksum = checksum
448-
449-
@property
450-
def data(self):
451-
"""Get data from payload."""
452-
return bytes(convertbits(bech32_to_u5(self.payload), 5, 8, False))
453-
454-
def __eq__(self, other):
455-
if not isinstance(other, Parts):
456-
return False
457-
return (
458-
self.hrp == other.hrp
459-
and self.k == other.k
460-
and self.ident == other.ident
461-
and self.share_index == other.share_index
462-
and self.payload == other.payload
463-
and self.checksum == other.checksum
464-
)
465-
466-
def __hash__(self):
467-
return hash(
468-
(
469-
self.hrp,
470-
self.k,
471-
self.ident,
472-
self.share_index,
473-
self.payload,
474-
self.checksum,
475-
)
476-
)

tests/test_bip93.py

Lines changed: 15 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -32,15 +32,14 @@
3232

3333
def test_parts():
3434
"""Test Vector 1: parse a codex32 string into parts"""
35-
c32 = Codex32String.from_string(VECTOR_1["secret_s"])
36-
parts = c32.parts
37-
assert parts.hrp == VECTOR_1["hrp"]
38-
assert parts.k == VECTOR_1["k"]
39-
assert parts.share_index == VECTOR_1["share_index"]
40-
assert parts.ident == VECTOR_1["identifier"]
41-
assert parts.payload == VECTOR_1["payload"]
42-
assert parts.checksum == VECTOR_1["checksum"]
43-
assert parts.data.hex() == VECTOR_1["secret_hex"]
35+
c32 = Codex32String(VECTOR_1["secret_s"])
36+
assert c32.hrp == VECTOR_1["hrp"]
37+
assert c32.k == VECTOR_1["k"]
38+
assert c32.share_index == VECTOR_1["share_index"]
39+
assert c32.ident == VECTOR_1["identifier"]
40+
assert c32.payload == VECTOR_1["payload"]
41+
assert c32.checksum == VECTOR_1["checksum"]
42+
assert c32.data.hex() == VECTOR_1["secret_hex"]
4443

4544

4645
def test_derive_and_recover():
@@ -52,16 +51,15 @@ def test_derive_and_recover():
5251
assert str(d) == VECTOR_2["derived_D"]
5352
s = Codex32String.interpolate_at([a, c], "S")
5453
assert str(s) == VECTOR_2["secret_S"]
55-
assert s.parts.data.hex() == VECTOR_2["secret_hex"]
54+
assert s.data.hex() == VECTOR_2["secret_hex"]
5655

5756

5857
def test_from_seed_and_interpolate_3_of_5():
5958
"""Test Vector 3: encode secret share from seed and split 3-of-5"""
6059
seed = bytes.fromhex(VECTOR_3["secret_hex"])
6160
a = Codex32String.from_string(VECTOR_3["share_a"])
6261
c = Codex32String.from_string(VECTOR_3["share_c"])
63-
parts = a.parts
64-
s = Codex32String.from_seed(seed, parts.ident, parts.hrp, parts.k, pad_val=0)
62+
s = Codex32String.from_seed(seed, a.ident, a.hrp, a.k, pad_val=0)
6563
assert str(s) == VECTOR_3["secret_s"]
6664
d = Codex32String.interpolate_at([s, a, c], "d")
6765
e = Codex32String.interpolate_at([s, a, c], "e")
@@ -70,9 +68,7 @@ def test_from_seed_and_interpolate_3_of_5():
7068
assert str(e) == VECTOR_3["derived_e"]
7169
assert str(f) == VECTOR_3["derived_f"]
7270
for pad_val in range(4):
73-
s = Codex32String.from_seed(
74-
seed, parts.ident, parts.hrp, parts.k, pad_val=pad_val
75-
)
71+
s = Codex32String.from_seed(seed, a.ident, a.hrp, a.k, pad_val=pad_val)
7672
assert str(s) == VECTOR_3["secret_s_alternates"][pad_val]
7773

7874

@@ -84,15 +80,15 @@ def test_from_seed_and_alternates():
8480
seed, hrp="ms", k=0, ident="leet", share_idx="s", pad_val=pad_v
8581
)
8682
assert str(s) == VECTOR_4["secret_s_alternates"][pad_v]
87-
assert s.parts.data == list(seed) or s.parts.data == seed
83+
assert s.data == list(seed) or s.data == seed
8884
# confirm all 16 encodings decode to same master data
8985

9086

9187
def test_long_string():
9288
"""Test Vector 5: decode long codex32 secret and confirm secret bytes."""
9389
long_str = VECTOR_5["secret_s"]
9490
long_seed = Codex32String.from_string(long_str)
95-
assert long_seed.parts.data.hex() == VECTOR_5["secret_hex"]
91+
assert long_seed.data.hex() == VECTOR_5["secret_hex"]
9692

9793

9894
# pylint: disable=missing-function-docstring
@@ -105,7 +101,7 @@ def test_invalid_bad_checksums():
105101
def test_wrong_checksums_or_length():
106102
for chk in WRONG_CHECKSUMS:
107103
with pytest.raises((InvalidChecksum, InvalidLength)):
108-
Codex32String.from_string(chk)
104+
Codex32String(chk)
109105

110106

111107
def test_invalid_improper_length():
@@ -129,7 +125,7 @@ def test_invalid_threshold():
129125
def test_invalid_prefix_or_separator():
130126
for chk in INVALID_PREFIX_OR_SEPARATOR:
131127
try:
132-
Codex32String.from_string(chk)
128+
Codex32String(chk)
133129
assert False, f"Accepted invalid HRP/separator in: {chk}"
134130
except (MismatchedHrp, SeparatorNotFound):
135131
pass

0 commit comments

Comments
 (0)