Skip to content

Commit f861506

Browse files
committed
Put an address beside its own QR code, on one screen
Opening an address showed its text, and the code was a step further in. Scanning is what a reader almost always came for, so it is the screen they land on now - with the derivation path and the address itself in the margin the right-aligned code leaves, in the same groups of four the paged view uses. Two facts about one address were on two screens; they are on one. The grid is fixed at two groups to a line rather than as many as the margin holds, so the shape is the same every time and only the last line varies. Each column is as wide as the widest group in the value: the face is proportional, so groups of equal length are not of equal width, and centring each line would start every one at its own x. A last line that does not fill its columns is centred on the block instead of left in the first one. Not every address fits. A taproot address is 62 characters against the 48 two columns hold there, so seedtool_render_qr_address measures before it draws anything and refuses what will not fit - those fall back to the code alone and then the paged text, which has the width for them. An address cut off at the margin's end looks exactly like one that ended there, which is the whole reason this returns a bool rather than drawing as far as it reaches. That refusal is checked rather than argued: the new self-test alters an address's last group and requires the margin to change with it, so a tail that was never drawn fails, and it requires the taproot address to be refused outright. Both halves were confirmed against a deliberately broken build - removing the height guard fails it - because the existing grouped-paging check walks only the paged path at the body face and says nothing about groups drawn by any other route. page_grouped loses its only caller and goes; the paged grouped view it named is reached through page_text_impl now, for the values that need it. Verified with the host self-test, the 52 Python tests, the QR smoke test and the self-test under ASan and UBSan. Every address shape the firmware derives was rendered off the framebuffer and read.
1 parent 4e98211 commit f861506

7 files changed

Lines changed: 281 additions & 24 deletions

File tree

‎host/origo_sdl.c‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -169,6 +169,15 @@ bool seedtool_display_qr(const char* title, const char* text)
169169
return ok;
170170
}
171171

172+
bool seedtool_display_qr_address(const char* title, const char* text)
173+
{
174+
const bool ok = seedtool_render_qr_address(title, text);
175+
if (ok) {
176+
present();
177+
}
178+
return ok;
179+
}
180+
172181
bool seedtool_display_qr_bytes(const char* title, const uint8_t* data, const size_t len)
173182
{
174183
const bool ok = seedtool_render_qr_bytes(title, data, len);

‎host/origo_simulator.c‎

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1714,6 +1714,92 @@ static bool grouped_paging_preserves_the_value(void)
17141714
return true;
17151715
}
17161716

1717+
/* The QR screen draws the address beside the code, and the margin it has is
1718+
* not always enough: a taproot address is 62 characters against the 48 two
1719+
* columns of four hold there. seedtool_render_qr_address must refuse those
1720+
* outright rather than draw as far as it reaches, because an address cut off
1721+
* mid-value looks exactly like one that ended there and nothing on screen
1722+
* says otherwise. This is the check grouped_paging_preserves_the_value does
1723+
* not make: that one walks seedtool_render_fit_grouped, the paged path at the
1724+
* body face, and says nothing about groups drawn by any other route. The first
1725+
* version of this screen truncated a taproot address to 48 of its characters
1726+
* with every other check still passing.
1727+
*
1728+
* Proving the tail is drawn without restating the renderer's own geometry:
1729+
* change only the value's last group and the margin must change with it. The
1730+
* margin is everything left of the code, found by looking for the first column
1731+
* carrying a run of white tall enough to be the code itself rather than a line
1732+
* of text. */
1733+
static int qr_code_left_edge(void)
1734+
{
1735+
const uint16_t* const pixels = seedtool_render_pixels();
1736+
for (int x = 0; x < SEEDTOOL_DISPLAY_WIDTH; ++x) {
1737+
int run = 0;
1738+
for (int y = 0; y < SEEDTOOL_DISPLAY_HEIGHT; ++y) {
1739+
run = pixels[y * SEEDTOOL_DISPLAY_WIDTH + x] == 0xffff ? run + 1 : 0;
1740+
if (run > 40) {
1741+
return x;
1742+
}
1743+
}
1744+
}
1745+
return -1;
1746+
}
1747+
1748+
static bool qr_address_draws_every_group(void)
1749+
{
1750+
static const char* const fits[] = {
1751+
"bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4",
1752+
"1BvBMSEYstWetqTFn5Au4m4GFg7xJaNVN2",
1753+
"3J98t1WpEZ73CNmQviecrnyiWrnqRhWNLy",
1754+
};
1755+
static uint16_t before[SEEDTOOL_DISPLAY_WIDTH * SEEDTOOL_DISPLAY_HEIGHT];
1756+
1757+
for (size_t v = 0; v < sizeof(fits) / sizeof(fits[0]); ++v) {
1758+
const char* const value = fits[v];
1759+
if (!seedtool_render_qr_address("m/84'/0'/0'/0/0", value)) {
1760+
return false; /* these do fit; refusing them is its own failure */
1761+
}
1762+
const int edge = qr_code_left_edge();
1763+
if (edge <= 0) {
1764+
return false;
1765+
}
1766+
memcpy(before, seedtool_render_pixels(), sizeof(before));
1767+
1768+
/* The same value with its last group altered, same length so the code
1769+
* keeps its version and the margin keeps its width. */
1770+
char altered[80] = { 0 };
1771+
const size_t total = strlen(value);
1772+
if (total + 1 > sizeof(altered) || total < SEEDTOOL_GROUP_LEN) {
1773+
return false;
1774+
}
1775+
memcpy(altered, value, total);
1776+
for (size_t i = total - SEEDTOOL_GROUP_LEN; i < total; ++i) {
1777+
altered[i] = altered[i] == 'q' ? 'p' : 'q';
1778+
}
1779+
if (!seedtool_render_qr_address("m/84'/0'/0'/0/0", altered)) {
1780+
return false;
1781+
}
1782+
const uint16_t* const after = seedtool_render_pixels();
1783+
1784+
bool margin_changed = false;
1785+
for (int y = 0; y < SEEDTOOL_DISPLAY_HEIGHT && !margin_changed; ++y) {
1786+
for (int x = 0; x < edge; ++x) {
1787+
if (before[y * SEEDTOOL_DISPLAY_WIDTH + x] != after[y * SEEDTOOL_DISPLAY_WIDTH + x]) {
1788+
margin_changed = true;
1789+
break;
1790+
}
1791+
}
1792+
}
1793+
if (!margin_changed) {
1794+
return false; /* the tail was never drawn */
1795+
}
1796+
}
1797+
1798+
/* And the one the margin cannot hold, which must say so rather than clip. */
1799+
return !seedtool_render_qr_address(
1800+
"m/86'/0'/0'/0/0", "bc1p5cyxnuxmeuwuvkwfem96lqzszd02n6xdcjrs20cac6yqjjwudpxqkedrcr");
1801+
}
1802+
17171803
/* The grouped line is centred with its gaps counted in, so it can run off an
17181804
* edge in a way the plain centred line cannot. Measured, not assumed. */
17191805
static bool grouped_lines_clear_the_edges(void)
@@ -2347,6 +2433,10 @@ static int self_test(void)
23472433
fputs("Origo grouped address geometry self-test failed\n", stderr);
23482434
return 1;
23492435
}
2436+
if (!qr_address_draws_every_group()) {
2437+
fputs("Origo address-beside-QR self-test failed\n", stderr);
2438+
return 1;
2439+
}
23502440
if (!nav_chrome_bands_do_not_collide()) {
23512441
fputs("Origo nav chrome geometry self-test failed\n", stderr);
23522442
return 1;

‎main/seedtool_app.c‎

Lines changed: 26 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -833,13 +833,6 @@ static bool page_text(const char* title, const char* text)
833833
return page_text_impl(title, text, true, false);
834834
}
835835

836-
/* page_text for a value with no word shapes to read by - an address. Drawn in
837-
* groups of four so the reader transcribing it has somewhere to keep their
838-
* place; the string itself is untouched. */
839-
static bool page_grouped(const char* title, const char* text)
840-
{
841-
return page_text_impl(title, text, true, true);
842-
}
843836

844837
/* Paged text there is nothing to take. Returns nothing because there is
845838
* nothing to return: the reader pages down and leaves by the arrow, and a
@@ -1945,19 +1938,35 @@ static void show_descriptor(const char* mnemonic, const char* passphrase, const
19451938
seedtool_zero(value, sizeof(value));
19461939
}
19471940

1948-
/* A single address's own QR, opened from the address list: no account key in
1949-
* this view to warn about or to carousel over to, since a photo of one
1950-
* address on its own reveals nothing the address itself did not already. */
1941+
/* A single address's own QR, and the first thing opening an address shows: no
1942+
* account key in this view to warn about or to carousel over to, since a photo
1943+
* of one address on its own reveals nothing the address itself did not
1944+
* already. Scanning is what a reader almost always came for, so the code, its
1945+
* path and the address itself share one screen rather than two.
1946+
*
1947+
* Not every address fits beside its code: a taproot address is 62 characters
1948+
* against the 48 the margin holds. seedtool_display_qr_address refuses those
1949+
* rather than drawing as far as it reaches - an address cut off mid-value
1950+
* looks exactly like one that ended there - and they fall back to the code
1951+
* alone, then the paged text, which has the width for them. */
19511952
static void show_address_qr(const char* title, const char* address)
19521953
{
19531954
for (;;) {
1955+
if (seedtool_display_qr_address(title, address)) {
1956+
if (wait_key() == KEY_REDRAW) {
1957+
continue;
1958+
}
1959+
return;
1960+
}
19541961
if (!seedtool_display_qr(title, address)) {
19551962
notice("Too long for a QR", title, "Read it as text instead");
19561963
return;
19571964
}
1958-
if (wait_key() != KEY_REDRAW) {
1959-
return;
1965+
if (wait_key() == KEY_REDRAW) {
1966+
continue;
19601967
}
1968+
(void)page_text_impl(title, address, false, true);
1969+
return;
19611970
}
19621971
}
19631972

@@ -2238,9 +2247,11 @@ static void show_addresses(
22382247
* make. */
22392248
char path[sizeof(prefix) + 12];
22402249
(void)snprintf(path, sizeof(path), "%s/%u", prefix, (unsigned)index);
2241-
if (page_grouped(path, address)) {
2242-
show_address_qr(path, address);
2243-
}
2250+
/* One screen: the code, its path, and the address itself beside it.
2251+
* Opening an address used to land on the text and reach the QR through
2252+
* it, which cost a step to the reader who came to scan - and left the
2253+
* two halves of the same fact on separate screens. */
2254+
show_address_qr(path, address);
22442255
seedtool_zero(address, sizeof(address));
22452256
}
22462257
seedtool_zero(addresses, sizeof(addresses));

‎main/seedtool_display.c‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -265,6 +265,15 @@ bool seedtool_display_qr(const char* title, const char* text)
265265
return ok;
266266
}
267267

268+
bool seedtool_display_qr_address(const char* title, const char* text)
269+
{
270+
const bool ok = seedtool_render_qr_address(title, text);
271+
if (ok) {
272+
flush();
273+
}
274+
return ok;
275+
}
276+
268277
bool seedtool_display_qr_bytes(const char* title, const uint8_t* data, const size_t len)
269278
{
270279
const bool ok = seedtool_render_qr_bytes(title, data, len);

‎main/seedtool_display.h‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ void seedtool_display_value_box(
4747
void seedtool_display_keyboard(const char* title, const char* text, const char* layout, const bool* enabled,
4848
size_t selected, size_t position, size_t total);
4949
bool seedtool_display_qr(const char* title, const char* text);
50+
bool seedtool_display_qr_address(const char* title, const char* text);
5051
bool seedtool_display_qr_bytes(const char* title, const uint8_t* data, size_t len);
5152
bool seedtool_display_qr_bytes_region(const char* title, const uint8_t* data, size_t len, size_t region_index);
5253
bool seedtool_display_qr_bytes_map(const char* title, const uint8_t* data, size_t len);

0 commit comments

Comments
 (0)