Skip to content

7.15 stack tests and report fixes - #230

Merged
BitHighlander merged 37 commits into
keepkey:reconcile/upstream-syncfrom
BitHighlander:release/715-pyk2-b2
Oct 9, 2026
Merged

BitHighlander merged 37 commits into
keepkey:reconcile/upstream-syncfrom
BitHighlander:release/715-pyk2-b2

Conversation

@BitHighlander

Copy link
Copy Markdown
Contributor

Moves #197 forward 37 commits for the 7.14.3 and 7.15 firmware stack.

  • Real-world EIP-712 corpus (UniswapX, Across, Hyperliquid, Safe MultiSend, Ethermint)
  • Zcash TEX and unified-address tests; EOS K1 authority keys
  • THORChain/Maya fee and Solana one-page screen checks
  • Report matches 7.15 behaviour; flaky emulator port fixed

Fast-forward only. All seven firmware blocks pin 8d62850 and are green on CI.

Firmware now decrypts each Orchard output, so the fixture is the
zcash-test-vectors note 0 with its real ciphertext (Ironwood V3 too).
Bindings regenerated with protoc 3.5.1, as before; the descriptor differs
only by Features.Capability CAPABILITY_TRON_TRC20_REVIEW = 25.
Gated on 7.15.0 and the tron-trc20-review capability, so 7.15 builds
without the trusted token table skip. Checks the screen text and verifies
the signature over sha256(raw_data) against the account's address.
Address screens wrap at their own widths and pitches beside a QR code,
so shows() takes a pitch, ignores the blank top row the QR border may
cover, and stops the edge check at the wrap limit. shows_address() tries
each layout in app_layout.c; assert_shows_address() runs a show_display
call and requires one screen to show the whole value.
A Hive STM key lost its tail on the device and no test noticed. Each
show_display test now checks the screen against a known vector or the
show_display=False response. Adds Hive, THORChain, Maya, Osmosis and EOS
show tests, and stops the TON and Tron show tests swallowing errors.
7.14 and the 7.15 blocks before the fix drop the last characters of a
52-character cosmosvaloper1 address. Covers delegate, undelegate,
redelegate (both validators) and rewards; nested display checks now
restore the enclosing capture.
Before firmware 88ae1c3be DebugLinkState was built in the shared
response arena, so reading the screen during a confirm wiped a response
the handler had already built; on a 7.14.3 emulator the cosmos, thor,
maya, osmosis, solana, ripple, eos, tron and ton responses came back
empty and an xpub confirm wedged. Older firmware now runs the call
unwatched, and the TON and Tron show tests gate on 7.15.0, not 7.14.2.
The fixture attested two accounts for a message that loads one, which
firmware now refuses to show. Add the partial/padded-list case.
7.14.3 now builds DebugLinkState outside the response arena, so the
bitcoin-only report checks every shown address as 7.15 does.
The Groestlcoin checks made the whole test full-firmware only, so the
bitcoin-only report never displayed a bech32 address.
A real multi-receiver Unified Address holding the output's Orchard
receiver is shown verbatim, in both pools. A valid address with another
Orchard receiver is refused before any output screen. Without one the
device shows the Orchard-only address as "Orchard address".

Repins device-protocol for ZcashPCZTAction.user_address.
eip712_corpus.json is the firmware repository's corpus (42 documents,
one per protocol and shape, written from each protocol's source). Each
must sign on the device with the expected domain separator and message
hash, recover to the device's address, and end on a screen that names
the whole primary type. Gated on 7.15.0 and structured EIP-712.

The host encoder now accepts struct type names with ':' up to 47
characters, as the firmware does, so Hyperliquid's
HyperliquidTransaction:* actions can be sent.
From erc20-unlimited-approve-review and erc20-unlimited-permit-review,
7.15 signs an unlimited approve after an UNLIMITED approval screen with
the full spender, and shows an unlimited permit value as UNLIMITED on
its own screen. The refusal tests now run only on firmware that lacks
those capabilities (requires_capability_absent).

The corpus gains three max approvals: the Uniswap default Permit2
PermitSingle (uint160 max), an EIP-2612 USDC permit of 2^256-1 and a DAI
permit with allowed=true. They run where the firmware reports the
capability, and before 7.16 their value screen must read UNLIMITED.
Each corpus document is now its own test (test_doc_<id>), so one slow
document no longer exhausts a single test's timeout and a refusal names
the document. The 7.15.0 gate is unchanged.

New documents: a Seaport bundle of 10 offer and 6 consideration items,
an order with 12 consideration items, a 32-item bundle, Permit2 batches
of 20 and 32, a Snapshot vote whose reason holds a newline, a tab and a
CR, and a Safe transaction whose data is an unlimited USDC approve,
which must show the UNLIMITED warning first.
An execTransaction that embeds approve(spender, 2^256-1) shows the
UNLIMITED approval screen, with the full spender, before the inner
review or the blind screen. Declining it signs nothing, a finite approve
shows no warning, and a dirty spender word is refused.
From NU6.3 every Orchard spend is paired with a zero-valued output whose
ciphertext is random bytes (ZIP 326). This bundle is built by the orchard
0.16.0 crate for the "all" seed, so its rk values are the device's own. The
device must sign both wallet spends, show only the change and the fee, and
show no 0 ZEC output.
Wallets pad an Orchard bundle with a dummy zero-valued output to a random
address whose ciphertext decrypts. The device showed it as a 0 ZEC send.
Like Keystone and Ledger it now shows only the change and the fee.

The bundle is a real NU6.2 one built by the orchard 0.16.0 crate.
From NU6.3 Orchard funds can only leave the Orchard pool. ZIP 318 moves
them to Ironwood with a v6 transfer that has both bundles, after
16-action note-preparation transactions.

zcash_sign_pczt takes the Ironwood actions of a crossing transaction
and streams them after the Orchard ones. The tests sign a transfer and
a preparation transaction built by librustzcash's zcash_pool_migration.

Repins device-protocol for the crossing fields.
Outputs proven to pay the account's internal address are shown as one
"Change back to your wallet" total. The fabricated, padding and
migration transactions now show that total instead of per-output
screens, and the 16-action preparation transaction takes three
confirmations instead of 32.

A new real NU6.2 bundle (orchard 0.16.0) checks that a self-send to the
account's external address and a payment to another account's change
address are still shown, and that a zero-valued memo-only send with the
address the user entered is shown at 0 ZEC.
Native and token deposits to THORChain's BSC and Base routers and Maya's
Arbitrum router clear-sign on their own chain, and each signature recovers
to the device's address. The same router address on another chain is
refused.
A V3DutchOrder witness references seven struct types and nests six
frames deep (witness.baseOutputs[i].curve.relativeAmounts). Add three
filled Arbitrum orders: one output, one with a fee output, and one
with a negative curve amount. Each document was rebuilt from the
order the UniswapX API returned, and the swapper's own signature
recovers from its digest.
A wallet pays a TEX address from shielded funds in two transactions:
one to its own one-time address m/44'/133'/account'/2/index, then a
transparent-only one from there to the TEX recipient. Both are built
by librustzcash for the "all" seed.

Step 1 shows the output as a transfer to your one-time address when
its path is sent, and as a plain t1 send without it. Step 2 has no
shielded action; it shows the recipient as tex1... and its signature
verifies under librustzcash's sighash, in v6 and v5. Shielded bundle
claims, a non-empty Sapling digest, another account's path and change
3 are refused.

zcash_sign_pczt accepts no actions when there is a transparent input.
Repins device-protocol for is_tex and the one-time output path.
EOSIO key types are K1=0, R1=1, WA=2. parse_authorization sent type 1
for every address_n key, so an updateauth or newaccount built here put
the user's K1 key bytes on chain as an R1 key nobody holds, which can
freeze the account. Send type 0; a raw PUB_R1_ key still sends 1.

Every firmware accepts type 0 for a derived key, and firmware
that checks the type refuses anything else. The updateauth, newaccount
and JSON newaccount digests are re-derived with an independent EOSIO
serializer; the old digests come out of the same serializer with type 1.
A Safe MultiSend's data or a Snapshot proposal's body is often several
KB, past the 1024-byte EthereumTypedDataValueAck. eip712_stream.value_ack()
answers a ValueRequest whole up to 1 KB, and past that in 1 KB chunks:
the first names the total length, each later one echoes the offset the
device asked for. It chunks only for firmware that reports
CAPABILITY_EIP712_CHUNKED_VALUES; older firmware would hash the first
chunk alone, so without it a long value is refused on the host.

Pin device-protocol at feat/eip712-chunked-values and regenerate the
bindings. Add a real Safe MultiSend (4,900 B of data, digest equals its
safeTxHash) and a real Snapshot proposal (3,929 B body, digest equals
its proposal id) to the corpus, and device tests for boundary lengths,
UTF-8 split across chunks, misframed chunks, a part declined mid-value
and an unlimited approve heading chunked SafeTx data.
Olympus DAO's Safe batched approve(WETH, max) to the CoW vault relayer
with a setPreSignature swap order (nonce 1802). The device names the
spender and WETH on the UNLIMITED screen; the digest is its safeTxHash.
Gated on eip712-chunked-values, the block that brings the warning.
Evmos and Injective sign EIP-712 with a string verifyingContract and salt;
the firmware refused them. Adds an Evmos MsgSend (evmosjs eip712 helpers)
and Injective's EIP-712 v2 MsgSend fixture (injective-ts eip712.spec.ts),
plus the Arbitrum V3 Dutch order with 16-point curves, the most the decay
library accepts. Hashes match eth-sig-util. Gated on eip712-chunked-values,
the block that brings the fixes.
The port pair is only known to be free when it is picked; another
process can bind it before kkemu does, and the reboot test then failed
at random. Pick a fresh pair and boot again when kkemu reports it could
not bind.
A 0.02 RUNE fee was printed as "Fee: 2000000 rune". Fails on the old
firmware with exactly that text.
A forced line break before "to" paged the last characters of a 44-char
address onto a screen of their own.
- drop K8, K8b and K9: no such tests exist in this firmware, so they
  rendered as pending inside a passing section
- unlimited approvals sign after a warning from 7.15; say so, and
  capture the warning screen instead of withholding it
- read section L from the bitcoin-only JUnit beside the full one, where
  those tests actually run, and update the L3 and L11 text now that both
  pass
K8, K8b and K9 are staged controls: a complete release requires them,
and validate_junit exempts them only while the firmware reports
storage-v19-kdf missing. Restore the rows, and in the PDF show a staged
control as withheld when its capability is missing instead of pending.
Hash-only signing is no longer the only EIP-712 path: the streamed
protocol in section TD reads the document. E16 is the blind path for
hash-only hosts; E16b proves the old JSON endpoint stays refused.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant