Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 2 additions & 18 deletions src/codex32/_cli_input.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,11 +29,6 @@
ExistingTargetIndex,
InvalidChecksum,
InvalidLength,
MismatchedIdentifier,
MismatchedPayloadLength,
MismatchedProfile,
MismatchedThreshold,
SecretInRecoverySet,
)
from codex32.profiles import Profile, _optional_profile_rules, _profile_rules
from codex32.profiles.ms32 import (
Expand Down Expand Up @@ -318,7 +313,7 @@ def _parse(value: str, profiles: tuple[Profile, ...] | None) -> Artifact:
try:
artifact = parse_codex32(value)
except CodexError as error:
message = _FRIENDLY_SET_ERRORS.get(type(error), str(error))
message = str(error)
if isinstance(error, InvalidChecksum):
# Parsing has already checked the container and generic length.
# Explain an unsupported profile length without validating input
Expand Down Expand Up @@ -688,17 +683,6 @@ def allowed(candidate: CorrectionCandidate) -> bool:
return accepted


_FRIENDLY_SET_ERRORS: dict[type[Exception], str] = {
InvalidChecksum: "The checksum does not match.",
MismatchedProfile: "These strings are for different applications.",
MismatchedThreshold: "These strings require different numbers of shares.",
MismatchedIdentifier: "These strings have different identifiers.",
MismatchedPayloadLength: "These strings have different lengths.",
DuplicateShareIndex: "That share index was already entered.",
SecretInRecoverySet: "Enter ordinary shares rather than the shared secret.",
}


def _interactive(
*,
basis: bool,
Expand Down Expand Up @@ -774,7 +758,7 @@ def allowed(candidate: CorrectionCandidate) -> bool:
try:
validate(artifact)
except CodexError as error:
_stderr(f"Rejected: {_FRIENDLY_SET_ERRORS.get(type(error), str(error))}\n")
_stderr(f"Rejected: {error}\n")
duplicate = isinstance(error, (DuplicateShareIndex, ExistingTargetIndex))
prefill = "" if duplicate else _retry_text(entered, prefix)
continue
Expand Down
18 changes: 9 additions & 9 deletions src/codex32/bip93.py
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@ def _decode_codex32(text: str) -> tuple[str, _ProfileRules | None, tuple[int, ..
body = tuple(encoded[: -checksum.length])
Header._from_symbols(body[:6])
if not bech32_verify_checksum(hrp, encoded, checksum):
raise InvalidChecksum(f"invalid {checksum.kind} checksum")
raise InvalidChecksum("The checksum does not match.")

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should we keep {checksum.kind} or not?

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Drop it. Which checksum applies follows from the string's length, and naming it ("invalid Long codex32 checksum") gives the user nothing to act on. #161 drops it too, so both codex32 and bech32 checksum failures read "The checksum does not match." The kind field stays because tests/test_crc.py reads it.


Generated by Claude Code

profile_rules = _optional_profile_rules(hrp)
if profile_rules is not None:
profile_rules.validate_text_length(len(text))
Expand Down Expand Up @@ -278,31 +278,31 @@ def _validate_share_set(artifacts: Sequence[Share | Secret], *, require_exact: b
first = copied[0]
threshold = first.header.threshold
if threshold not in range(2, 10):
raise MismatchedThreshold("linear sharing requires threshold 2 through 9")
raise MismatchedThreshold("Only strings with a threshold of 2 through 9 can be combined.")
if len(copied) > threshold or (require_exact and len(copied) != threshold):
raise WrongShareCount(f"threshold is {threshold}, but {len(copied)} artifacts were supplied")
first_tail, checksum_length, encoded_length = _artifact_tail(first)
tails = [first_tail]
indices = [first.header.index]
for item in copied[1:]:
if item.hrp != first.hrp:
raise MismatchedHrp(f"{first.hrp} and {item.hrp} cannot be combined")
raise MismatchedHrp("These strings are for different applications.")

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

did you like showing the mismatch or is the new string best?

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this diff uses strings repeatedly but sometimes a more user friendly message may say shares when the provided data was all non-S

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Showing the mismatch is better. #161 keeps the library's "ms and bip39_12w cannot be combined", which you said seemed fine earlier today. On shares versus strings: #161 (10c8f9b) now says "shares" when every input is an ordinary share and "strings" when a secret is among them.


Generated by Claude Code

if item.header.threshold != threshold:
raise MismatchedThreshold("share thresholds do not match")
raise MismatchedThreshold("These strings require different numbers of shares.")
if item.header.identifier != first.header.identifier:
raise MismatchedIdentifier("share identifiers do not match")
raise MismatchedIdentifier("These strings have different identifiers.")

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is a situation where strings or shares could be correct. i nit slightly prefer to use the one our set is.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in #161 (10c8f9b). The threshold, identifier and length mismatches all pick the noun from the set itself. "These strings require different numbers of shares" also became "These shares have different thresholds" (or "These strings have…") so that it reads well with either noun.


Generated by Claude Code

tail, item_checksum_length, item_encoded_length = _artifact_tail(item)
item_shape = (
len(item.payload_symbols),
item_checksum_length,
item_encoded_length,
)
if item_shape != (len(first.payload_symbols), checksum_length, encoded_length):
raise MismatchedPayloadLength("share payload/checksum lengths do not match")
raise MismatchedPayloadLength("These strings have different lengths.")

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same shares sometimes, strings other times.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same fix: the length mismatch uses the set's noun in #161 (10c8f9b).


Generated by Claude Code

tails.append(tail)
indices.append(item.header.index)
if len(set(indices)) != len(indices):
raise DuplicateShareIndex("share indices must be distinct")
raise DuplicateShareIndex("That share index was already entered.")
return _ShareSet(
copied,
first.hrp,
Expand All @@ -317,7 +317,7 @@ def _validate_share_set(artifacts: Sequence[Share | Secret], *, require_exact: b
def _validate_recovery_prefix(shares: Sequence[Share | Secret]) -> None:
share_set = _validate_share_set(shares, require_exact=False)
if any(isinstance(item, Secret) for item in share_set.artifacts):
raise SecretInRecoverySet("recovery accepts ordinary shares only")
raise SecretInRecoverySet("Enter ordinary shares rather than the shared secret.")


def _validate_basis_prefix(basis: Sequence[Share | Secret]) -> None:
Expand All @@ -342,7 +342,7 @@ def recover_secret(shares: Sequence[Share]) -> Secret:
"""Recover S from exactly k compatible, distinct ordinary shares."""
share_set = _validate_share_set(shares)
if any(isinstance(item, Secret) for item in share_set.artifacts):
raise SecretInRecoverySet("recovery accepts ordinary shares only")
raise SecretInRecoverySet("Enter ordinary shares rather than the shared secret.")
Comment on lines 319 to +345

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why are we duplicating this logic?

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We shouldn't. In #161 (10c8f9b), _validate_recovery_prefix takes require_exact and returns the share set, and recover_secret calls it, so the secret-in-recovery check lives in one place. The order of checks is unchanged. Full suite passes (910, normal and -O).


Generated by Claude Code

recovered = _interpolate_tail(share_set, "s")
if not isinstance(recovered, Secret):
raise InvalidShareSet("interpolation did not produce a secret")
Expand Down
3 changes: 2 additions & 1 deletion tests/test_bip93.py
Original file line number Diff line number Diff line change
Expand Up @@ -106,8 +106,9 @@ def test_vector_6_core_lightning_examples_are_immutable() -> None:

@pytest.mark.parametrize("value", BAD_CHECKSUMS)
def test_bad_checksums_are_rejected(value: str) -> None:
with pytest.raises(InvalidChecksum):
with pytest.raises(InvalidChecksum) as error:
parse_codex32(value)
assert str(error.value) == "The checksum does not match."


@pytest.mark.parametrize("value", BAD_CASES)
Expand Down
21 changes: 14 additions & 7 deletions tests/test_sharing.py
Original file line number Diff line number Diff line change
Expand Up @@ -112,28 +112,35 @@ def test_every_threshold_recovers_exactly_k_shares(threshold: int) -> None:
def test_share_set_mismatches_have_distinct_errors() -> None:
ms_secret, ms_masks = _ms_basis()
opaque = parse_codex32(oracle_encode("xy", "2testa" + "q" * len(ms_secret.payload_symbols)))
with pytest.raises(MismatchedProfile):
with pytest.raises(MismatchedProfile) as error:
derive_share([ms_secret, opaque], "c") # type: ignore[list-item]
assert str(error.value) == "These strings are for different applications."

threshold_three = parse_codex32(oracle_encode("ms", "3testc" + "q" * len(ms_secret.payload_symbols)))
with pytest.raises(MismatchedThreshold):
with pytest.raises(MismatchedThreshold) as error:
derive_share([ms_secret, threshold_three], "d") # type: ignore[list-item]
assert str(error.value) == "These strings require different numbers of shares."

other_id = parse_codex32(oracle_encode("ms", "2namec" + "q" * len(ms_secret.payload_symbols)))
with pytest.raises(MismatchedIdentifier):
with pytest.raises(MismatchedIdentifier) as error:
derive_share([ms_secret, other_id], "d") # type: ignore[list-item]
assert str(error.value) == "These strings have different identifiers."

longer = MasterSeed.from_seed(bytes(20), identifier="test", threshold=2)
longer_mask = parse_codex32(oracle_encode("ms", "2testc" + "q" * len(longer.payload_symbols)))
with pytest.raises(MismatchedPayloadLength):
with pytest.raises(MismatchedPayloadLength) as error:
derive_share([ms_secret, longer_mask], "d") # type: ignore[list-item]
assert str(error.value) == "These strings have different lengths."

with pytest.raises(DuplicateShareIndex):
with pytest.raises(DuplicateShareIndex) as error:
recover_secret([ms_masks[0], ms_masks[0]]) # type: ignore[list-item]
with pytest.raises(SecretInRecoverySet):
assert str(error.value) == "That share index was already entered."
with pytest.raises(SecretInRecoverySet) as error:
recover_secret([ms_secret, ms_masks[0]]) # type: ignore[list-item]
with pytest.raises(MismatchedThreshold):
assert str(error.value) == "Enter ordinary shares rather than the shared secret."
with pytest.raises(MismatchedThreshold) as error:
recover_secret([MasterSeed.from_seed(bytes(16), identifier="test")]) # type: ignore[list-item]
assert str(error.value) == "Only strings with a threshold of 2 through 9 can be combined."
with pytest.raises(TypeError):
recover_secret([VECTOR_2["share_A"], VECTOR_2["share_C"]]) # type: ignore[list-item]
with pytest.raises(TypeError):
Expand Down
Loading