@@ -1235,7 +1235,7 @@ static bool compact_seedqr_is_sound(void)
12351235 if (qrcode_versionForBytes (ECC_LOW , (uint16_t )len , 6 ) != 1 ) {
12361236 return false;
12371237 }
1238- if (!seedtool_render_qr_bytes ("Compact SeedQR" , entropy , len )) {
1238+ if (!seedtool_render_qr_bytes ("Compact SeedQR" , entropy , len , SEEDTOOL_NAV_BACK )) {
12391239 return false;
12401240 }
12411241 if (seedtool_mnemonic_entropy (mnemonic24 , entropy , sizeof (entropy ), & len ) != SEEDTOOL_OK || len != 32 ) {
@@ -1249,7 +1249,7 @@ static bool compact_seedqr_is_sound(void)
12491249 if (qrcode_versionForBytes (ECC_LOW , (uint16_t )len , 6 ) != 2 ) {
12501250 return false;
12511251 }
1252- if (!seedtool_render_qr_bytes ("Compact SeedQR" , entropy , len )) {
1252+ if (!seedtool_render_qr_bytes ("Compact SeedQR" , entropy , len , SEEDTOOL_NAV_BACK )) {
12531253 return false;
12541254 }
12551255 return seedtool_mnemonic_entropy (bad_checksum_mnemonic , entropy , sizeof (entropy ), & len ) != SEEDTOOL_OK ;
@@ -1275,11 +1275,11 @@ static bool zoomed_qr_regions_are_sound(void)
12751275 return false;
12761276 }
12771277 for (size_t i = 0 ; i < 9 ; ++ i ) {
1278- if (!seedtool_render_qr_bytes_region ("Compact SeedQR" , entropy , len , i )) {
1278+ if (!seedtool_render_qr_bytes_region ("Compact SeedQR" , entropy , len , i , SEEDTOOL_NAV_BACK )) {
12791279 return false;
12801280 }
12811281 }
1282- if (seedtool_render_qr_bytes_region ("Compact SeedQR" , entropy , len , 9 )) {
1282+ if (seedtool_render_qr_bytes_region ("Compact SeedQR" , entropy , len , 9 , SEEDTOOL_NAV_BACK )) {
12831283 return false;
12841284 }
12851285
@@ -1290,11 +1290,11 @@ static bool zoomed_qr_regions_are_sound(void)
12901290 return false;
12911291 }
12921292 for (size_t i = 0 ; i < 25 ; ++ i ) {
1293- if (!seedtool_render_qr_bytes_region ("Compact SeedQR" , entropy , len , i )) {
1293+ if (!seedtool_render_qr_bytes_region ("Compact SeedQR" , entropy , len , i , SEEDTOOL_NAV_BACK )) {
12941294 return false;
12951295 }
12961296 }
1297- return !seedtool_render_qr_bytes_region ("Compact SeedQR" , entropy , len , 25 );
1297+ return !seedtool_render_qr_bytes_region ("Compact SeedQR" , entropy , len , 25 , SEEDTOOL_NAV_BACK );
12981298}
12991299
13001300/* The bar's warn (red) and complete (green) colors appear nowhere else on a
@@ -1756,7 +1756,7 @@ static bool qr_address_draws_every_group(void)
17561756
17571757 for (size_t v = 0 ; v < sizeof (fits ) / sizeof (fits [0 ]); ++ v ) {
17581758 const char * const value = fits [v ];
1759- if (!seedtool_render_qr_address ("m/84'/0'/0'/0/0" , value )) {
1759+ if (!seedtool_render_qr_address ("m/84'/0'/0'/0/0" , value , SEEDTOOL_NAV_BACK )) {
17601760 return false; /* these do fit; refusing them is its own failure */
17611761 }
17621762 const int edge = qr_code_left_edge ();
@@ -1776,7 +1776,7 @@ static bool qr_address_draws_every_group(void)
17761776 for (size_t i = total - SEEDTOOL_GROUP_LEN ; i < total ; ++ i ) {
17771777 altered [i ] = altered [i ] == 'q' ? 'p' : 'q' ;
17781778 }
1779- if (!seedtool_render_qr_address ("m/84'/0'/0'/0/0" , altered )) {
1779+ if (!seedtool_render_qr_address ("m/84'/0'/0'/0/0" , altered , SEEDTOOL_NAV_BACK )) {
17801780 return false;
17811781 }
17821782 const uint16_t * const after = seedtool_render_pixels ();
@@ -1796,8 +1796,143 @@ static bool qr_address_draws_every_group(void)
17961796 }
17971797
17981798 /* 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" );
1799+ return !seedtool_render_qr_address ("m/86'/0'/0'/0/0" ,
1800+ "bc1p5cyxnuxmeuwuvkwfem96lqzszd02n6xdcjrs20cac6yqjjwudpxqkedrcr" , SEEDTOOL_NAV_BACK );
1801+ }
1802+
1803+ /* How far right the theme's orange reaches in the strip the two QR controls
1804+ * share - NAV_BACK_Y + NAV_BACK_HEIGHT tall, above the first line a margin
1805+ * draws. Which control the cursor is on is read off this rather than off a
1806+ * box's coordinates, so the proofs below survive the chrome being moved. */
1807+ static int qr_highlight_reach (void )
1808+ {
1809+ const uint16_t highlight = 0xfd20 ;
1810+ const uint16_t * const pixels = seedtool_render_pixels ();
1811+ int reach = -1 ;
1812+ for (int y = 0 ; y < 20 ; ++ y ) {
1813+ for (int x = 0 ; x < SEEDTOOL_DISPLAY_WIDTH ; ++ x ) {
1814+ if (pixels [y * SEEDTOOL_DISPLAY_WIDTH + x ] == highlight && x > reach ) {
1815+ reach = x ;
1816+ }
1817+ }
1818+ }
1819+ return reach ;
1820+ }
1821+
1822+ /* Every QR screen wears the same two controls, and the cursor reaches the sun
1823+ * on all of them: the account key animates and the Compact SeedQR carousels,
1824+ * so a shade that only the address screen could reach would be missing from
1825+ * exactly the codes hardest to scan. */
1826+ static bool qr_chrome_is_the_same_everywhere (void )
1827+ {
1828+ static const char * const text = "bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4" ;
1829+ static const uint8_t entropy [16 ] = { 1 , 2 , 3 , 4 , 5 , 6 , 7 , 8 , 9 , 10 , 11 , 12 , 13 , 14 , 15 , 16 };
1830+ for (int screen = 0 ; screen < 5 ; ++ screen ) {
1831+ int reach [2 ];
1832+ for (int i = 0 ; i < 2 ; ++ i ) {
1833+ const size_t selected = i ? SEEDTOOL_NAV_SHADE : SEEDTOOL_NAV_BACK ;
1834+ const bool ok = screen == 0 ? seedtool_render_qr ("Account key" , text , selected )
1835+ : screen == 1 ? seedtool_render_qr_address ("m/84'/0'/0'/0/0" , text , selected )
1836+ : screen == 2 ? seedtool_render_qr_bytes ("Compact SeedQR" , entropy , sizeof (entropy ), selected )
1837+ : screen == 3 ? seedtool_render_qr_bytes_map ("Compact SeedQR" , entropy , sizeof (entropy ), selected )
1838+ : seedtool_render_qr_bytes_region ("Compact SeedQR" , entropy , sizeof (entropy ), 0 , selected );
1839+ if (!ok ) {
1840+ return false;
1841+ }
1842+ reach [i ] = qr_highlight_reach ();
1843+ }
1844+ if (reach [0 ] < 0 || reach [1 ] <= reach [0 ]) {
1845+ return false; /* no cursor, or one that cannot reach the sun */
1846+ }
1847+ }
1848+ return true;
1849+ }
1850+
1851+ /* Cycling the QR's shade has to move the light half of the code and nothing
1852+ * else: a step that also touched the dark modules would be lowering contrast
1853+ * from both sides, which is the opposite of what a camera that cannot lock on
1854+ * needs. Measured off the panel rather than read off the ramp, so the test
1855+ * says nothing about which greys were chosen - only that one press lands the
1856+ * light field on one new colour, that the ring closes on itself, and that the
1857+ * cursor can be seen to be on the control that does it. */
1858+ static bool qr_shade_moves_only_the_light_field (void )
1859+ {
1860+ static const char * const value = "bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4" ;
1861+ static const char * const path = "m/84'/0'/0'/0/0" ;
1862+ static uint16_t white [SEEDTOOL_DISPLAY_WIDTH * SEEDTOOL_DISPLAY_HEIGHT ];
1863+ static uint16_t cycled [SEEDTOOL_DISPLAY_WIDTH * SEEDTOOL_DISPLAY_HEIGHT ];
1864+ const size_t count = sizeof (white ) / sizeof (white [0 ]);
1865+
1866+ /* Nothing has touched the shade yet, so this is also the proof that a QR
1867+ * left alone is drawn exactly as it was before the ramp existed. */
1868+ if (!seedtool_render_qr_address (path , value , SEEDTOOL_NAV_BACK )) {
1869+ return false;
1870+ }
1871+ memcpy (white , seedtool_render_pixels (), sizeof (white ));
1872+
1873+ /* The cursor has to be visible ON the control, not merely somewhere on the
1874+ * screen: moving to the second control has to carry the highlight to it.
1875+ * A check that "something changed" passes even when the second control
1876+ * ignores the cursor entirely, because the first one still gives its
1877+ * highlight up. */
1878+ const int back_reach = qr_highlight_reach ();
1879+ if (back_reach < 0 ) {
1880+ return false; /* the cursor was never on the way out either */
1881+ }
1882+ if (!seedtool_render_qr_address (path , value , SEEDTOOL_NAV_SHADE )) {
1883+ return false;
1884+ }
1885+ if (qr_highlight_reach () <= back_reach ) {
1886+ return false; /* the highlight did not move to the second control */
1887+ }
1888+
1889+ seedtool_render_qr_cycle_shade ();
1890+ if (!seedtool_render_qr_address (path , value , SEEDTOOL_NAV_BACK )) {
1891+ return false;
1892+ }
1893+ memcpy (cycled , seedtool_render_pixels (), sizeof (cycled ));
1894+
1895+ /* Every pixel that moved was white and is now one single other colour -
1896+ * the code's light field and the swatch on the button that reports it. */
1897+ uint16_t shade = 0xffff ;
1898+ size_t moved = 0 ;
1899+ for (size_t i = 0 ; i < count ; ++ i ) {
1900+ if (white [i ] == cycled [i ]) {
1901+ continue ;
1902+ }
1903+ if (white [i ] != 0xffff ) {
1904+ return false; /* something other than the light field moved */
1905+ }
1906+ if (!moved ) {
1907+ shade = cycled [i ];
1908+ } else if (cycled [i ] != shade ) {
1909+ return false; /* the light field is no longer one colour */
1910+ }
1911+ ++ moved ;
1912+ }
1913+ if (!moved || shade == 0xffff ) {
1914+ return false;
1915+ }
1916+
1917+ /* The ring closes: pressing on round the ramp lands back on white, in the
1918+ * same number of presses every time. Each one has to move something, or a
1919+ * ramp with a repeated entry would still come home and look correct. */
1920+ for (int i = 0 ; i < 32 ; ++ i ) {
1921+ seedtool_render_qr_cycle_shade ();
1922+ if (!seedtool_render_qr_address (path , value , SEEDTOOL_NAV_BACK )) {
1923+ return false;
1924+ }
1925+ if (memcmp (cycled , seedtool_render_pixels (), sizeof (cycled )) == 0 ) {
1926+ return false; /* a press that changed nothing */
1927+ }
1928+ memcpy (cycled , seedtool_render_pixels (), sizeof (cycled ));
1929+ if (memcmp (white , cycled , sizeof (white )) == 0 ) {
1930+ /* Home again, and the ramp is left where every other test expects
1931+ * to find it. */
1932+ return true;
1933+ }
1934+ }
1935+ return false; /* the ring never came back to where it started */
18011936}
18021937
18031938/* The grouped line is centred with its gaps counted in, so it can run off an
@@ -2437,6 +2572,14 @@ static int self_test(void)
24372572 fputs ("Origo address-beside-QR self-test failed\n" , stderr );
24382573 return 1 ;
24392574 }
2575+ if (!qr_chrome_is_the_same_everywhere ()) {
2576+ fputs ("Origo QR chrome self-test failed\n" , stderr );
2577+ return 1 ;
2578+ }
2579+ if (!qr_shade_moves_only_the_light_field ()) {
2580+ fputs ("Origo QR shade self-test failed\n" , stderr );
2581+ return 1 ;
2582+ }
24402583 if (!nav_chrome_bands_do_not_collide ()) {
24412584 fputs ("Origo nav chrome geometry self-test failed\n" , stderr );
24422585 return 1 ;
@@ -2452,7 +2595,7 @@ static int self_test(void)
24522595 const unsigned purpose = i ? 86 : 84 ;
24532596 (void )snprintf (payload , sizeof (payload ), "[73c5da0a/%u'/0'/%u']%s" , purpose , SEEDTOOL_MAX_ACCOUNT_INDEX ,
24542597 i ? expected_xpub86 : expected_xpub84 );
2455- if (strlen (payload ) > 134 || !seedtool_render_qr ("BIP84 account key" , payload )) {
2598+ if (strlen (payload ) > 134 || !seedtool_render_qr ("BIP84 account key" , payload , SEEDTOOL_NAV_BACK )) {
24562599 fputs ("Origo account key QR self-test failed\n" , stderr );
24572600 return 1 ;
24582601 }
@@ -2465,7 +2608,7 @@ static int self_test(void)
24652608 }
24662609 seedtool_render_screen ("ORIGO" , "HOST SELF-TEST" , address , "OK" );
24672610 seedtool_render_splash ();
2468- if (!seedtool_render_qr ("BIP84 address 0" , address )) {
2611+ if (!seedtool_render_qr ("BIP84 address 0" , address , SEEDTOOL_NAV_BACK )) {
24692612 fputs ("Origo QR self-test failed\n" , stderr );
24702613 return 1 ;
24712614 }
0 commit comments