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
8 changes: 8 additions & 0 deletions docs/developer/gui.md
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,14 @@ library's.
- The account number is fixed at 0, which is the command line's default. The
restore screen says so, and points anyone whose wallet record shows another
number at `ms32 wallet --account N`.
- The window offers no checksum completer, and does not tell the operator that
13 or 15 trailing `?`, depending on card length, would be one. It is aimed at
someone whose seed comes from the operating system, for whom a Book worksheet
never arises, so advertising the route there would be all cost. The route is
reachable anyway, so `_guess_gate_page` speaks to a person completing new data
as well as to a person recovering a damaged card, and forbids replacing a
checksum outright. `docs/user/guide.md` documents the route for the command
line, which is where the worksheet audience already is.
- Restoring asks the operator to *check* the wallet-identity fields against
their record rather than copy them onto it, and shows no creation date. A
restore is the only moment the program can show that the cards entered belong
Expand Down
15 changes: 13 additions & 2 deletions docs/user/gui.md
Original file line number Diff line number Diff line change
Expand Up @@ -93,8 +93,19 @@ compare it character by character before you accept it.

If too much is missing, the window stops and asks you to type `YES` in capitals
first. That is not a formality: with that little checksum left, a repair can look
correct without being correct, and any earlier mistake gets locked in with
nothing left to detect it. If the funds matter, stop there and get help.
correct without being correct. The screen says so twice over, because two
different people reach it. If you are filling in the last squares of a backup you
are making by hand, it tells you to check every character against what you wrote,
since completing the squares locks any earlier mistake in for good. If you are
recovering a damaged card, it tells you the answer may simply be wrong, and that
you may have to try likely misreadings one at a time. If the funds matter, stop
there and get help.

**Never erase a card's last characters to make it check out.** A card that fails
its check is telling you something is wrong. Replacing the ending hides that
mistake inside a result that now looks valid, and you lose the one signal that
would have found it. Type what the card actually says, `?` included, and let the
window work from that.

If more than one repair fits, the window shows none of them. Check the card again.

Expand Down
22 changes: 18 additions & 4 deletions src/codex32_gui/pages.py
Original file line number Diff line number Diff line change
Expand Up @@ -1132,17 +1132,31 @@ def following() -> Adw.NavigationPage:
def _guess_gate_page(
view: Adw.NavigationView, following: Callable[[], Adw.NavigationPage]
) -> Adw.NavigationPage:
"""Invariant 5: disclose nothing about the candidate until literal YES is typed."""
"""Invariant 5: disclose nothing until YES; whole-checksum completion reaches this gate too."""
field = Gtk.Entry(placeholder_text="YES")
show = _button("Show the guess", lambda: view.push(following()), style="destructive-action")
show.set_sensitive(False)
field.connect("changed", lambda _entry: show.set_sensitive(field.get_text().strip() == "YES"))
content = _column(
_title("This repair would be a guess"),
_note(
"So much of this card is unreadable that codex32 can fill in the blanks in a way that looks "
"correct without being correct. If any earlier character is also wrong, that mistake gets "
"locked in and the card becomes wrong forever, with nothing left to detect it.",
"So much of this card is unreadable that codex32 can fill in the blanks in a way that "
"looks correct without being correct.",
"warning",
),
_note(
"If you are filling in the last squares of a backup you are making by hand, check every "
"character you have typed against what you wrote down before you go on. Nothing can "
"detect a mistake made earlier: filling in the squares locks it in for good."
),
_note(
"If you are recovering a damaged card, the answer may simply be wrong. If it does not "
"restore your wallet, you may have to try likely misreadings one at a time."
),
_note(
"Never erase a card's last characters to make it check out. A card that fails its check "
"is telling you something is wrong, and replacing the ending hides that mistake instead "
"of finding it.",
"warning",
),
_note("If the funds matter, stop here and get help instead."),
Expand Down
7 changes: 4 additions & 3 deletions tests/test_gui_reading.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
"""What the graphical entry field makes of typed text, without a display."""

import pytest
from data.bip93_vectors import VECTOR_2, VECTOR_3
from data.bip93_vectors import VECTOR_2, VECTOR_3, VECTOR_5

from codex32_gui.reading import (
PREFIX,
Expand Down Expand Up @@ -127,10 +127,11 @@ def test_a_repair_that_repeats_an_accepted_card_is_not_offered() -> None:
assert isinstance(repair(SHARE_A[:-1] + "Q", 48, ("a",)), str)


def test_a_card_with_too_little_checksum_left_demands_the_warning() -> None:
@pytest.mark.parametrize(("card", "checksum_length"), ((SHARE_C, 13), (VECTOR_5["secret_s"], 15)))
def test_a_card_with_too_little_checksum_left_demands_the_warning(card: str, checksum_length: int) -> None:
from codex32 import CorrectionCandidate

found = repair(SHARE_C[:-13] + "?" * 13, None)
found = repair(card[:-checksum_length] + "?" * checksum_length, None)
assert isinstance(found, CorrectionCandidate)
assert found.low_checksum_discrimination

Expand Down
53 changes: 53 additions & 0 deletions tools/gui_walkthrough.py
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,8 @@ def do_activate(self) -> None:
self.candidate,
self.back_to_entry,
self.accepted_repair,
self.completion_entry,
self.completion_gate,
self.preflight,
self.network,
self.letters,
Expand Down Expand Up @@ -278,6 +280,57 @@ def accepted_repair(self) -> bool:
press(page, "Done")
return True

def completion_entry(self) -> bool:
"""Thirteen unreadable characters at the end are a whole checksum."""
page = self.page()
if page.get_title() != "codex32":
return False
rows(page)[3].emit("activated")
page = self.page()
field = field_of(page)
field.set_text(SHARE_C[:-13] + "?" * 13)
settle()
fix = button(page, "Suggest a repair")
check("completing a checksum is offered as a repair", fix.get_sensitive())
fix.emit("clicked")
return True

def completion_gate(self) -> bool:
page = self.page()
if page.get_title() != "Warning" or button(page, "Show the guess") is None:
return False
shown = labels(page)
check("nothing about the guess is disclosed yet", card(page) == "", card(page))
check(
"the gate addresses someone completing new data",
any("making by hand" in text and "locks it in" in text for text in shown),
[text for text in shown if "hand" in text],
)
check(
"and someone recovering a damaged card",
any("may simply be wrong" in text for text in shown),
[text for text in shown if "wrong" in text],
)
check(
"and forbids replacing a checksum outright",
any("Never erase" in text for text in shown),
[text for text in shown if "Never" in text],
)
show = button(page, "Show the guess")
check("the guess stays hidden until YES is typed", not show.get_sensitive())
entry = next(item for item in walk(page) if isinstance(item, Gtk.Entry))
for typed in ("yes", "Yes", "YES please", "Y"):
entry.set_text(typed)
settle()
check(f"{typed!r} does not open the gate", not show.get_sensitive())
entry.set_text("YES")
settle()
check("literal YES opens it", show.get_sensitive())
press(page, "Cancel")
settle()
self.view.replace([pages.home(self.view)])
return True

def preflight(self) -> bool:
page = self.page()
if page.get_title() != "codex32":
Expand Down
Loading