Skip to content

test(7.15): consolidated Python harness, bindings, and device coverage - #197

Open
BitHighlander wants to merge 511 commits into
masterfrom
reconcile/upstream-sync
Open

BitHighlander wants to merge 511 commits into
masterfrom
reconcile/upstream-sync

Conversation

@BitHighlander

@BitHighlander BitHighlander commented Jul 16, 2026 •

Copy link
Copy Markdown
Contributor

Canonical multi-release companion

This is the single canonical python-keepkey branch used by firmware 7.14.x and 7.15. It does not encode a 7.15-only fork: behavior is selected by the device protocol, firmware version, product variant, and explicit partial-stack capability declarations.

Immutable dependencies and targets

  • python-keepkey head: 3e1689d3ec6ffb68c8aefbbb091e0fca2b78cd4b
  • Docker-tested runtime/test parent: 49d537ce953b7524599bfc87f56b7a50a51a13a1
  • device-protocol gitlink: dd9c85dc747cf965fb0e7bf49615dc9e7568ee65
  • ethereum-lists gitlink: 89a64f717e1690bb31adb3e4c38e23640357333c
  • firmware 7.14.3 target: 4125e1c7409b1cb7b08ba595bc408e3128fc24ca
  • firmware 7.15 Docker-validated target: 9b0583a41b758af7fb4c08280ab2a2bd8bb5d503

The head differs from its Docker-tested parent only by two immutable workflow refs. Both regular and bitcoin-only hosted 7.15 jobs now target the exact Docker-validated firmware runtime head. The bitcoin-only matrix consumes each row its own immutable firmware_ref.

Release behavior

The branch carries generated bindings, chain clients, offline fixtures, ERC-7730/Solana helpers, full and bitcoin-only matrices, exact OLED assertions, owned-emulator storage reboot gates, and release-policy controls. Runtime clear-sign providers remain AdvancedMode-only and session-scoped; persistent providers and the KeepKey trusted root are not part of the 7.15 contract. Older firmware retains safe behavior through version and feature gating.

Docker evidence

  • 7.14.3 regular compatibility: 558 passed, 269 skipped, zero failures
  • 7.15 regular integration: 755 passed, 75 deliberate skips, zero failures
  • 7.15 regular OLED: 167 passed, 21 skipped, 642 deselected, 1,128 PNGs
  • 7.15 bitcoin-only integration: 348 passed, 482 deliberate skips, zero failures
  • 7.15 bitcoin-only OLED: 40 passed, 148 skipped, 642 deselected, 216 PNGs
  • all five real reboot/storage cases passed for both 7.15 product variants

Full release policy, image manifests, skip disposition, and human audit instructions are in firmware PR BitHighlander/keepkey-firmware#843.

Merge gates

  • one canonical multi-release branch; no release-only companion
  • exact local Docker compatibility for 7.14.3 and 7.15
  • protocol and registry gitlinks are immutable
  • hosted 7.15 jobs pin the Docker-audited firmware head
  • current exact-head hosted checks
  • human review before merge

Merging this host dependency does not authorize firmware merge, hardware testing, or release.

@BitHighlander BitHighlander changed the title test(7.15): hive signing + clearsign v2 harness + regenerated bindings test(7.15): consolidated Python harness, bindings, and device coverage Jul 25, 2026
@BitHighlander
BitHighlander marked this pull request as ready for review July 25, 2026 15:42
@BitHighlander
BitHighlander requested a review from pastaghost July 26, 2026 18:32
BitHighlander added a commit to BitHighlander/keepkey-firmware that referenced this pull request Aug 22, 2026
The previous pin carried a storage-version gate that asserted
STORAGE_VERSION == 20 and the literal "case StorageVersion_18:". Both are
true on the 7.16 passkeys branch and both are FALSE here, where
STORAGE_VERSION is 17 and nothing is burned -- so develop was pinning a
test suite guaranteed to fail against its own firmware.

006142da70e4 derives the ladder, the burned set and LAST_SHIPPED from the
tree under test instead. Verified green on both lines from one file: 10
passed / 5 skipped here, 15 passed on the 7.16 tree.

It also carries the integration-CI repair, which matters for a release
branch: that job had been ending "cancelled" at exactly 30 minutes with
zero assertions run, behind a check that reported success. It now
finishes in 2m46s with 636 passed, 32 skipped, 0 failed, against an
emulator built from current firmware rather than a five-month-old
published image.

Still the head of the open upstream PR keepkey/python-keepkey#197, which
was fast-forwarded to this commit first, so the pin stays resolvable for
an upstream reviewer.
BitHighlander added a commit to BitHighlander/keepkey-firmware that referenced this pull request Aug 22, 2026
The previous pin carried a storage-version gate that asserted
STORAGE_VERSION == 20 and the literal "case StorageVersion_18:". Both are
true on the 7.16 passkeys branch and both are FALSE here, where
STORAGE_VERSION is 17 and nothing is burned -- so develop was pinning a
test suite guaranteed to fail against its own firmware.

006142da70e4 derives the ladder, the burned set and LAST_SHIPPED from the
tree under test instead. Verified green on both lines from one file: 10
passed / 5 skipped here, 15 passed on the 7.16 tree.

It also carries the integration-CI repair, which matters for a release
branch: that job had been ending "cancelled" at exactly 30 minutes with
zero assertions run, behind a check that reported success. It now
finishes in 2m46s with 636 passed, 32 skipped, 0 failed, against an
emulator built from current firmware rather than a five-month-old
published image.

Still the head of the open upstream PR keepkey/python-keepkey#197, which
was fast-forwarded to this commit first, so the pin stays resolvable for
an upstream reviewer.
BitHighlander added a commit to BitHighlander/keepkey-firmware that referenced this pull request Aug 22, 2026
The previous pin carried a storage-version gate that asserted
STORAGE_VERSION == 20 and the literal "case StorageVersion_18:". Both are
true on the 7.16 passkeys branch and both are FALSE here, where
STORAGE_VERSION is 17 and nothing is burned -- so develop was pinning a
test suite guaranteed to fail against its own firmware.

006142da70e4 derives the ladder, the burned set and LAST_SHIPPED from the
tree under test instead. Verified green on both lines from one file: 10
passed / 5 skipped here, 15 passed on the 7.16 tree.

It also carries the integration-CI repair, which matters for a release
branch: that job had been ending "cancelled" at exactly 30 minutes with
zero assertions run, behind a check that reported success. It now
finishes in 2m46s with 636 passed, 32 skipped, 0 failed, against an
emulator built from current firmware rather than a five-month-old
published image.

Still the head of the open upstream PR keepkey/python-keepkey#197, which
was fast-forwarded to this commit first, so the pin stays resolvable for
an upstream reviewer.
@BitHighlander

Copy link
Copy Markdown
Contributor Author

Review guide

Scope: the 7.15 Python harness, protobuf bindings and device coverage. Like device-protocol, this is a dependency merge — it ships nothing to a device, it makes firmware submodule pins resolvable.

⚠️ The red CircleCI check is expected and cannot be made green here

ci/circleci: emulator-build-test   FAILURE
integration                        SUCCESS
lint                               SUCCESS

That job clones firmware at -b master, and upstream firmware master is 7.14.0 — which contains none of what this PR tests:

lib/firmware/eip712_stream.c     ABSENT
lib/firmware/signed_metadata.c   ABSENT
lib/firmware/zcash.c             ABSENT
lib/firmware/hive.c              ABSENT

Meanwhile this PR carries 80 tests gated on requires_firmware("7.15.0"). It is a chicken-and-egg: the job can only pass once 7.15 firmware reaches upstream master. Please do not chase it green — it resolves when the firmware lands, not before.

The two GitHub Actions checks (lint, integration) are the meaningful signal and both pass.

#216 is subsumed by this PR

release/7.14.2-tests (#216, now merged) and this branch touch the same 15 files. git log shows this branch 1 behind #216, which is a squash artifact — #216 was one squashed commit, and its content is already here:

tests/test_msg_display_disclosure.py        PRESENT
tests/vectors/eip155_oracle.py              PRESENT
tests/vectors/regenerate_eip155_vectors.py  PRESENT

On those shared files this branch carries ~2,883 more lines. Nothing from #216 is lost.

Where to focus

+16,257/−1,091 across 73 files is a lot, but it is test harness and bindings, not device code. The parts worth real attention:

  • tests/common.py — the capability gates. requires_firmware, requires_message, and the newer requires_structured_eip712, which probes the device rather than asking whether the local bindings define a message.
  • tests/test_storage_version_gate.py — reworked to derive the ladder, burned set and LAST_SHIPPED from whichever firmware tree it runs against, so it is meaningful on both the 7.15 (V17) and 7.16 (V20) lines from one file. Verified green on both; proven non-vacuous by injecting 10 mutations, 9 of which it catches.
  • scripts/generate-test-report.py — the atlas catalog. Every entry was verified to resolve to a test that exists (280 references checked), because a renamed test silently orphans its requirement section.

Known and deliberately not fixed

scripts/generate-test-report.py cites "SRS-7.16 R-4.1, R-4.2". That document lives on the firmware side and is not part of the 7.15 release cut, so the reference dangles until 7.16.

BitHighlander added a commit that referenced this pull request Aug 22, 2026
#216 landed as one squashed commit whose content had already reached #197 by
another route, so most of the 31 conflicts are the two branches doing the same
thing in different words. Resolved by hand, hunk by hunk; no --ours/--theirs.
Every #216 addition is verified present in the merged tree, including the
files that never conflicted (test_msg_display_disclosure.py, the eip155
vectors, the ripple/ping/tron/eth-signtx disclosure tests, and the
`chain_id is not None` fix in keepkeylib/client.py -- that one matters, it is
what lets an explicit chain_id=0 reach the wire to be refused).

tests/test_sign_typed_data.py (1 hunk) -- #197
  Same policy call on both sides; the only difference is `with open(...)` vs
  open/close and one extra clause of comment. Nothing asserted differs.

tests/test_msg_thorchain_signtx.py (2 hunks) -- #197, and this one is a real
divergence, not a restatement.
  #197 repointed test_sign_eth_add_liquidity at the firmware-pinned THORChain
  router (to=0xd37bbe57...); that line auto-merged, so it IS the transaction
  the merged test signs. #216 regenerated exact r/s against the OLD
  to=0x41e55600.... `to` is an RLP field of the legacy sighash, so #216's
  vectors do not describe the transaction under test and would fail. #197's
  structural assertions win on those grounds -- NOT because structural is
  preferable. They are genuinely weaker than what #216 had, and the superseded
  vectors are recorded in a comment at the assertion so the gap is visible and
  regenerable on-device. Same treatment for the memo-length hunk: both sides
  independently corrected 0x3b -> 0x3a (the memo is 58 bytes; the 59th was ABI
  padding), so the comments were combined rather than one dropped.

tests/test_msg_mayachain_signtx.py (2 hunks) -- #197
  Same router story (to=0xe3985e6b..., Maya router v4), so #216's exact r/s --
  byte-identical to the thorchain pair, same base fixture -- are stale here too.
  #197 does not merely relax: it reconstructs the sighash host-side and recovers
  the signer, so a wrong digest, wrong calldata or wrong key fails. Taking #216
  would also have re-added a stray `)` and restored @unittest.skip on
  test_mayachain_remove_liquidity, which #197 unskipped. Raw assertion count in
  this file drops 26 -> 13 because seven frozen-vector memo cases became one
  loop over the same seven memos and three skipped tests now actually run; memo
  coverage is unchanged at nine cases and none of them are skipped any more.

tests/test_msg_ton_signtx.py (9 hunks) -- union
  Both sides added the identical AdvancedMode opt-in to nine tests; #216 also
  carried the four-line reason. Kept #216's comment and call on all nine.
  #197 additionally gated three tests #216 did not -- the reject-path tests --
  and those auto-merged; annotated them, because opening the gate is what stops
  them passing for the wrong reason (with the gate shut the firmware refuses
  every TonSignTx and the raises-assertion is satisfied without the parser ever
  validating a field).

scripts/generate-test-report.py (17 hunks) -- #197
  Fifteen hunks are section-letter collisions: #216 filed Display Binding under
  'S' and Display Disclosure under 'D', but 'S' is already Solana and 'D' is
  already BIP-85 Child Derivation, so #216's own catalog carries each letter
  twice. #197 assigns 'J' and 'Q' and does not collide; _audit_catalog() asserts
  letter and id uniqueness and would have tripped on #216's scheme. Entries are
  otherwise identical -- same module, same test, same prose -- so nothing #216
  catalogued is lost, only relettered. Fixed one cross-reference #197's rename
  left dangling: J13's prose said "the control for S12", which now points at a
  Solana test; it means J12. The other two hunks are #197-only additions (the F,
  I, L, U, TD sections and MUST_RUN_MODULES) against an empty #216 side.

tests/test_msg_ethereum_erc20_0x_signtx.py -- union, and git got this one wrong
  Not one of the 31: it merged clean and merged badly. Both sides inserted an
  AdvancedMode opt-in into test__sign_transformERC20 at different offsets, so
  the merged file called apply_policy twice in a row. Deduped to one call
  carrying both rationales (#216's 1480-bytes-past-the-1024-chunk mechanism,
  which is what atlas J1 actually claims, plus #197's no-recognised-handler
  note). Folded in the comment #197 left three lines above it saying "No
  AdvancedMode policy is set here on purpose", which contradicted its own
  apply_policy call and both sides' findings; kept as an explicitly superseded
  note rather than deleted, so the behaviour change stays legible.

Verified: 0 conflict markers; every .py under tests/, scripts/ and keepkeylib/
parses; _audit_catalog() passes and the report renders 27 sections / 381
distinct tests; all 383 catalog rows resolve to a test that exists (V17-V67
against CLEARSIGN_FLOWS, which generates those methods by the same transform;
K3-K11 are native gtest suites the JUnit parser handles by bare classname).
Not verified: no test was executed against firmware. The one suite that runs
offline, test_storage_version_gate.py, needs the firmware sources checked out
above this tree and errors in setUp without them -- unchanged by this merge.
BitHighlander added a commit that referenced this pull request Aug 22, 2026
Squashed from 194 commits. Everything below is against master, which this
branch already contains -- the 7.14.2 disclosure tests from #216 are merged
in, not replaced.

BINDINGS (keepkeylib)
  Protobuf regenerated for the 7.15 surface: ethereum, hive, ripple, solana,
  thorchain, zcash, types. New modules for the features those messages carry --
  clearsign_abi, clearsign_catalog, eip712_stream, signed_metadata, hive, zcash.

  transport_udp gained a socket timeout. It had none, so a crashed emulator
  blocked in recv() until the CI job was killed, which is reported as a
  cancelled job rather than a failing test and throws away every result.

  eth/token_policy.py bounds the built-in token table. It is the largest
  read-only symbol in the ARM image (31KB for 1,945 mostly-2018 entries) and
  the vetted source it comes from is a stale snapshot -- no UNI, no AAVE, no
  modern stables. The policy keeps what users hold plus what coins[] requires,
  and takes a priority symbol only when the source gives it exactly one address,
  so a scam token cannot inherit a real one's label.

COVERAGE (17 new suites)
  taproot: address derivation, signing, and on-screen verification
  zcash: PCZT device signing and seed fingerprint
  clear-signing: additive tier, signing guards, EIP-712 streaming, Solana LUT
    attestation, thorchain deposit
  session trust lifetime, hive, osmosis, bitcoin-only variant, storage gate

NOTES ON TWO DELIBERATE CHOICES
  The thorchain and mayachain suites assert structure rather than frozen
  (r,s) vectors. #197 repoints those transactions at current routers, and `to`
  is an RLP field of the EIP-155 sighash, so the old vectors describe a
  different transaction. The superseded vectors are kept in comments at the
  assertion site so the gap stays visible and regenerable on hardware.

  The storage version gate skips on trees that predate it, but still FAILS if
  storage.c references the floor while storage.h no longer defines it -- the
  skip cannot swallow the regression it guards.
@BitHighlander
BitHighlander force-pushed the reconcile/upstream-sync branch from 1f6c240 to a08dbd9 Compare August 22, 2026 22:57
@pastaghost
pastaghost requested a balanced review from Copilot August 22, 2026 23:07

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Consolidates Python bindings, client helpers, CI, and device coverage for firmware 7.15/RC18.

Changes:

  • Regenerates protocol bindings and adds Hive, Zcash, Solana, EIP-712, and clear-signing support.
  • Expands device and regression coverage across multiple chains.
  • Reworks CI to build current firmware and run deterministic contract tests.

Reviewed changes

Copilot reviewed 58 out of 67 changed files in this pull request and generated 8 comments.

Show a summary per file
File Description
.github/workflows/ci.yml Builds firmware emulator and expands CI gates.
.github/workflows/copilot-review.yml Requests reviews for non-draft PRs.
.gitmodules Targets the protocol release branch.
build_pb.sh Generates Hive bindings.
keepkeylib/clearsign_abi.py Adds static ABI encoding helpers.
keepkeylib/debuglink.py Adds dice-input debug support.
keepkeylib/eip712_stream.py Implements streamed EIP-712 encoding.
keepkeylib/eth/ethereum_tokens.py Applies token-table budgets.
keepkeylib/eth/token_policy.py Defines token selection policy.
keepkeylib/eth/uniswap_tokens.py Budgets Uniswap token entries.
keepkeylib/hive.py Adds Hive client helpers.
keepkeylib/mapping.py Registers clearsign and Hive messages.
keepkeylib/messages_ripple_pb2.py Adds Ripple memo binding.
keepkeylib/messages_solana_pb2.py Adds Solana attestation fields.
keepkeylib/messages_thorchain_pb2.py Adds THORChain denomination field.
keepkeylib/signed_metadata.py Extends clear-sign metadata formats.
keepkeylib/transport_udp.py Adds emulator response timeouts.
keepkeylib/zcash.py Adds ZIP-32 fingerprint calculation.
tests/common.py Adds capability gates and screenshot cleanup.
tests/config.py Adds forced UDP selection.
tests/probe.py Adds import-path diagnostics.
tests/test_message_signing_protocol_bindings.py Checks Solana binding compatibility.
tests/test_msg_bip85.py Updates BIP-85 firmware gating.
tests/test_msg_cosmos_signtx.py Replaces an unprofessional memo fixture.
tests/test_msg_eip712_streaming.py Covers streamed structured EIP-712.
tests/test_msg_ethereum_clearsign_additive.py Tests additive clear-sign reviews.
tests/test_msg_ethereum_erc20_0x_signtx.py Documents blind-sign policy behavior.
tests/test_msg_ethereum_erc20_uniswap_liquidity.py Centralizes emulator skipping.
tests/test_msg_ethereum_signing_guards.py Covers Ethereum signing guards.
tests/test_msg_ethereum_signtx.py Adds policy and chain-ID regressions.
tests/test_msg_ethereum_thorchain_deposit.py Covers THORChain EVM deposits.
tests/test_msg_getaddress_taproot.py Adds BIP-86 address vectors.
tests/test_msg_getentropy.py Tests entropy budget behavior.
tests/test_msg_osmosis_signtx.py Adds Osmosis signing coverage.
tests/test_msg_recoverydevice_cipher.py Updates recovery-version documentation.
tests/test_msg_resetdevice.py Adds dice-reset and ceremony tests.
tests/test_msg_signtx_taproot.py Adds Taproot transaction coverage.
tests/test_msg_solana_lut_attestation.py Tests Solana LUT attestations.
tests/test_msg_thorchain_signtx.py Updates THORChain router fixtures.
tests/test_msg_ton_signtx.py Enables parser-specific rejection tests.
tests/test_msg_tron_signtx.py Applies blind-sign policy handling.
tests/test_msg_zcash_display_address.py Updates device-derived address tests.
tests/test_msg_zcash_seed_fingerprint.py Tests device fingerprint binding.
tests/test_msg_zcash_sign_pczt.py Adds offline PCZT contract tests.
tests/test_msg_zcash_sign_pczt_device.py Adds shielded device coverage.
tests/test_protection_levels.py Clears sessions before entropy tests.
tests/test_sign_typed_data.py Covers typed-data policy gates.
tests/test_taproot_screens.py Adds long-address OLED coverage.
tests/test_zcash_seed_fingerprint_helper.py Tests fingerprint helper vectors.
tests/txcache/insight_bitcoin_tx_3e1fdf….json Adds mixed-input transaction fixture.
tests/txcache/insight_bitcoin_tx_6e3203….json Adds Taproot prevout fixture.
Files not reviewed (7)
  • keepkeylib/messages_ethereum_pb2.py: Generated file
  • keepkeylib/messages_hive_pb2.py: Generated file
  • keepkeylib/messages_ripple_pb2.py: Generated file
  • keepkeylib/messages_solana_pb2.py: Generated file
  • keepkeylib/messages_thorchain_pb2.py: Generated file
  • keepkeylib/messages_zcash_pb2.py: Generated file
  • keepkeylib/types_pb2.py: Generated file
Suppressed comments (3)

tests/test_msg_solana_lut_attestation.py:193

  • As in the first test, the baseline signing clears the RAM-only signer. The replay attempt therefore runs with no verification key and proves only the no-signer fallback, not that transaction binding rejects a replay. Reload the signer after collecting the baseline.
    tests/test_msg_ethereum_thorchain_deposit.py:198
  • This tuple is equivalent to catching all Exception, so any unrelated failure satisfies the unpinned-chain regression test. Catch only CallException and assert the blind-sign policy message to ensure the intended guard is what rejected the transaction.
    tests/test_msg_thorchain_signtx.py:159
  • These structural assertions no longer prove that the changed router/calldata was committed to the signature. Replace the superseded oracle with a regenerated golden signature or an independent digest/recovery check.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread keepkeylib/eip712_stream.py Outdated
Comment thread tests/test_msg_recoverydevice_cipher.py
Comment thread tests/test_msg_solana_lut_attestation.py
Comment thread tests/test_msg_ethereum_thorchain_deposit.py Outdated
Comment thread .gitmodules Outdated
Comment thread keepkeylib/clearsign_abi.py Outdated
Comment thread keepkeylib/messages_thorchain_pb2.py
Comment thread tests/test_msg_thorchain_signtx.py

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 59 out of 68 changed files in this pull request and generated no new comments.

Files not reviewed (7)
  • keepkeylib/messages_ethereum_pb2.py: Generated file
  • keepkeylib/messages_hive_pb2.py: Generated file
  • keepkeylib/messages_ripple_pb2.py: Generated file
  • keepkeylib/messages_solana_pb2.py: Generated file
  • keepkeylib/messages_thorchain_pb2.py: Generated file
  • keepkeylib/messages_zcash_pb2.py: Generated file
  • keepkeylib/types_pb2.py: Generated file
Suppressed comments (4)

Previously missed (4) — in code that hasn't changed since the last review.

keepkeylib/client.py:2109

  • The deferred transparent response is accepted without checking that it contains one signature per transparent input. A device response that omits ZcashTransparentSigned or returns a short list therefore succeeds and can hand the caller an incomplete transaction, while Orchard signatures are correctly count-checked below. Validate this count before returning the final response.
        if isinstance(resp, zcash_proto.ZcashTransparentSigned):
            transparent_sigs = list(resp.signatures)
            resp = self.transport.read_blocking()

tests/test_msg_osmosis_signtx.py:163

  • This test still expects raw-wire uatom to sign, contradicting the new uosmo-only invariant documented and enforced by keepkeylib/client.py:1057-1066. Against the intended 7.15 firmware, sign_denom('uatom') should raise instead of returning a second signature. Replace this with a raw-wire rejection assertion and update the P4 report entry accordingly.
    tests/test_msg_bitcoin_only_variant.py:16
  • This module-level contract is now false: setUp() unconditionally calls requires_bitcoinOnly(), so every test skips on the regular image and none provides dual-build evidence. Describe the actual bitcoin-only-only scope instead.
    tests/test_msg_bitcoin_only_variant.py:107
  • This unconditional gate skips all eleven new product-boundary tests in the only Python integration job: .github/workflows/ci.yml:120-129 builds and starts only the regular emulator, with no bitcoin-only matrix/job. Consequently the advertised bitcoin-only coverage is never exercised by this repository's required CI. Add a bitcoin-only emulator integration target that runs this suite.

@pastaghost

Copy link
Copy Markdown
Contributor

Addressing the four suppressed comments ("Previously missed") from the round-2 Copilot review. They were not posted as inline threads, so the responses are collected here. Pushed as be1975c, 70f3055, 44d82ef.


1. keepkeylib/client.py:2109 — transparent signature count not validated — fixed (70f3055)

Confirmed. The Orchard signatures were count-checked and length-checked; the deferred ZcashTransparentSigned response was accepted unconditionally, so a device that omitted the message or returned a short list still returned success with unspendable transparent inputs.

zcash_sign_pczt now requires one signature per transparent input and rejects a present-but-empty entry. Transparent signatures are DER ECDSA, so there is no fixed length to assert the way the 64-byte RedPallas signatures are asserted. Both checks were placed after the Failure and response-type arms so a device-reported error still surfaces its own message rather than being masked by a count mismatch.

Four scripted-flow tests added — short list, omitted message, empty entry, matching count. The three negative tests fail against the previous client and pass against the new one.

2. tests/test_msg_osmosis_signtx.py:163 — raw-wire uatom — pushing back; the comment was the wrong artifact (be1975c)

The finding reads the contradiction correctly but inverts which side is wrong. The test is right and the firmware invariant it is measured against does not exist.

Firmware 7.15 does not reject a non-uosmo denom on the OsmosisMsgAck path:

  • fsm_msgOsmosisMsgAck checks only has_denom presence and forwards msg->send.denom into osmosis_signTxUpdateMsgSend — there is no rejection arm.
  • Since 7.14.2 (firmware c9dccf68) that serializer takes a denom parameter and escapes it straight into the signed Amino document, with an explicit comment about the IBC and factory denom paths that exceed the 64-byte scratch buffer.
  • The only remaining strcmp(d, "uosmo") in firmware is in osmosis_format_amount, and it selects the display exponent — non-native denoms are shown as the exact integer because their exponent is unknown.

So sign_denom('uatom') returning a second, different signature is the correct and intended 7.15 behavior, and P4 in the report catalog describes it accurately. What was false was the comment at keepkeylib/client.py:1057-1066 claiming "Firmware now enforces the same rule on direct OsmosisMsgAck traffic". That has been corrected to state the real situation: the uosmo-only check is a host policy, not a firmware invariant. It stays because osmosis_sign_tx is not version-gated and firmware older than 7.14.2 hardcoded uosmo in the serializer, so forwarding another denom there would silently sign a uosmo transfer the caller never asked for — fail closed rather than mis-sign.

No test or report change; the contradiction is resolved in the direction the firmware actually behaves.

3. tests/test_msg_bitcoin_only_variant.py:16 — module contract is false — fixed (44d82ef)

Confirmed. setUp() calls requires_bitcoinOnly() unconditionally, so "NOTHING HERE SKIPS ... evidence on both builds" was untrue the moment that gate was added. The docstring now states the actual scope: this module runs on the bitcoin-only image only, deliberately, because several tests assert screen sequences that legitimately differ on the multi-chain build. It also names the CI job the file's value now depends on.

4. tests/test_msg_bitcoin_only_variant.py:107 — coverage never exercised by CI — fixed (44d82ef)

Confirmed. .github/workflows/ci.yml built and started only the regular emulator, so all eleven product-boundary tests ran as skips in the one required integration job.

Added an integration-btc job that:

  • builds the emulator with -DKK_BITCOIN_ONLY=ON, via the coinsupport build arg scripts/emulator/Dockerfile already forwards into its cmake invocation;
  • asserts features.firmware_variant is KeepKeyBTC/EmulatorBTC before pytest runs, so a build arg that silently failed to take effect is named rather than showing up as eleven skips;
  • fails when any test in the module skips. This is the point of the job: pytest exits 0 on a fully skipped module, so a green run proves nothing unless the skip count is zero. The gate was exercised against synthetic JUnit XML for the all-skipped, none-skipped, and missing-report cases.

Verification: the lint-gate suite (test_msg_zcash_sign_pczt.py, test_zcash_seed_fingerprint_helper.py) passes 16/16 locally under protobuf 3.20.3 with PROTOCOL_BUFFERS_PYTHON_IMPLEMENTATION=python, and py_compile is clean across keepkeylib/*.py and the touched tests. The integration-btc job itself has not run yet — GitHub CI on this push is the first execution.

The .gitmodules thread stays open: device-protocol #112 is still open, so the canonical master repin and binding regeneration it asks for remain blocked.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 59 out of 68 changed files in this pull request and generated 1 comment.

Files not reviewed (7)
  • keepkeylib/messages_ethereum_pb2.py: Generated file
  • keepkeylib/messages_hive_pb2.py: Generated file
  • keepkeylib/messages_ripple_pb2.py: Generated file
  • keepkeylib/messages_solana_pb2.py: Generated file
  • keepkeylib/messages_thorchain_pb2.py: Generated file
  • keepkeylib/messages_zcash_pb2.py: Generated file
  • keepkeylib/types_pb2.py: Generated file
Suppressed comments (6)

Previously missed (4) — in code that hasn't changed since the last review.

.github/workflows/ci.yml:100

  • Pin the firmware revision used by this release CI instead of building the mutable alpha branch. The current green run can therefore validate post-RC18 behavior rather than the RC18 dependency graph; for example, it passes the new Ironwood acceptance test even though the linked RC18 protocol contract marks Ironwood fields as schema-only and requires rejection. A later alpha push can also change this PR's result without any Python commit.
      - name: Checkout firmware
        uses: actions/checkout@v4
        with:
          repository: BitHighlander/keepkey-firmware
          ref: alpha
          path: keepkey-firmware

tests/test_msg_zcash_sign_pczt_device.py:305

  • This asserts Ironwood succeeds on firmware 7.15, but the linked device-protocol #112 RC18 contract explicitly says shielded_pool/ironwood_digest are schema-only and RC18 rejects a non-Orchard pool. That also conflicts with this PR's stated Orchard-only RC18 contract/report. Remove this from RC18 coverage or gate it to the later firmware version that actually supports Ironwood; RC18 should test refusal instead.
    tests/test_msg_ethereum_signing_guards.py:59
  • These assertions only prove that the device returned a correctly shaped signature. The malformed-RLP regression described by this test can still return 32-byte r/s values, so the test would pass while signing the wrong preimage. Reconstruct the EIP-1559 digest with an empty priority-fee field and recover/verify the signer against the device address.

This issue also appears on line 131 of the same file.
keepkeylib/clearsign_abi.py:77

  • The encoder accepts non-existent Solidity integer widths such as uint7, uint0, or int264 and can emit plausible 32-byte calldata for them. Since this module promises canonical Solidity ABI encoding, validate both signed and unsigned widths as multiples of 8 in the 8..256 range before encoding.

This issue also appears on line 78 of the same file.

        elif typ.startswith('uint'):
            digits = typ[4:]
            bits = int(digits) if digits else 256
            n = int(val)

tests/test_msg_ethereum_signing_guards.py:135

  • This does not test either property in its name/docstring: any 32-byte r/s passes even if only the first calldata chunk was hashed, and no DebugLink screen is inspected to ensure the Sablier clear-sign summary was suppressed. Recover the signer against a digest containing the full data and record/assert the fallback raw-data review screen.
    keepkeylib/clearsign_abi.py:82
  • Static type validation is also permissive here: arbitrary truthy values are silently rewritten to ABI true, and bytes0/bytes33 are accepted (the latter emits more than one word and shifts every following argument). Require an actual boolean and restrict fixed bytes to bytes1 through bytes32.
        elif typ == 'bool':
            out += (1 if val else 0).to_bytes(32, 'big')
        elif typ.startswith('bytes') and typ != 'bytes' and not typ.endswith('[]'):
            n = int(typ[5:])
            b = val if isinstance(val, (bytes, bytearray)) else bytes.fromhex(

Comment thread .circleci/config.yml Outdated
@pastaghost

Copy link
Copy Markdown
Contributor

Round-3 review: the one inline comment is answered in its own thread. The 6 suppressed comments have no threads, so they are answered here. Pushed as 73f96be, d469ea6, 9d64a07, f01c36d.


.github/workflows/ci.yml:100 — build the mutable alpha branch — fixed (f01c36d)

Confirmed, with direct evidence from this PR's own CI: the integration-btc run logged emulator firmware 7.16.0, variant 'EmulatorBTC'. alpha is 7.16.0 (a710bb57, "feat/passkeys-7.16"), so the green run was validating post-RC18 firmware, exactly as you said. Both integration jobs now pin ref: a710bb5777f3ad888bb489b383dbafab800d55c6, so a firmware push can no longer change this PR's result without a Python commit.

Pinned to that SHA rather than to RC18, deliberately, and the comment says so. Pinning to audit/7.15.0-rc18-final (fd1012c5, 7.15.0) does not work today, for two concrete reasons:

  • lib/board/variant.c on RC18 has no EmulatorBTC arm — variant_getName() returns "Emulator" for the bitcoin-only build. requires_bitcoinOnly() keys on that name, so every test in the integration-btc job would skip, and the job's variant assert would fail closed.
  • unittests/firmware/zcash.cpp on RC18 has no IronwoodNoteCommitment_V3KnownVector, which is where the Ironwood note fixtures come from.

So the RC18-vs-7.16 gap is real and is not closed by this change — CI is now reproducible, not RC18-validating. Flagging that as an open release question rather than papering over it.

tests/test_msg_zcash_sign_pczt_device.py:305 — Ironwood on RC18 — fixed, but the cited rationale does not hold (f01c36d)

The conclusion is right and the reason given is not. device-protocol #112 does not mark shielded_pool/ironwood_digest schema-only. In messages-zcash.proto at the PR head, the only field marked reserved-and-rejected is sapling_digest ("Reserved for future Sapling support; currently rejected"); shielded_pool and ironwood_digest are ordinary transaction-v6 fields, documented as "the fifth v6 transaction component". Nothing in that file or the breaking-change header says RC18 rejects a non-Orchard pool.

What is true is the firmware side: the class gate is requires_firmware("7.15.0"), a floor, so these tests were running on 7.16 and would run on RC18, whose zcash.cpp has no Ironwood vectors. Gated test_pool_selection_is_honoured and test_ironwood_note_is_accepted to 7.16.0 so RC18 skips them rather than failing. That is a firmware-support gate, not a wire-contract one, and the comment says which.

tests/test_msg_ethereum_signing_guards.py:59 and :135 — shape-only assertions — fixed (d469ea6)

Confirmed on both. Each test asserted only len(r) == 32 and len(s) == 32, and both regressions they describe still produce a well-formed 32-byte r/s, so neither test could fail for the reason it existed.

  • test_eip1559_no_priority_fee_signs now rebuilds the digest with eth_sighash_eip1559(..., max_priority_fee_per_gas=0, ...) — the absent field must encode as the empty integer — and recovers the signer against ethereum_get_address.
  • test_contract_handler_streamed_calldata_signs_full_data now recovers the signer against eth_sighash_legacy over the complete 1104-byte calldata. Verified offline that a prefix-only digest does not recover to the same address, so the recovery genuinely discriminates rather than passing by construction.

tests/test_msg_ethereum_signing_guards.py:135 — neither property in its name — fixed (d469ea6)

The second half of your point — no screen was inspected — was also correct, and the old docstring admitted it while claiming it was covered elsewhere. Added test_streamed_handler_calldata_is_not_clear_signed, which records the framebuffer at every ButtonRequest (same ScreenRecorder approach as test_msg_ethereum_clearsign_additive.py) and compares the handler run against a no-handler baseline of identical shape: same to, same calldata length, same streaming path, only the selector differs. A clear-sign summary would be extra confirm frames the baseline does not have. Compared against a baseline rather than a hardcoded screen count because the raw-data screen paginates with the calldata.

keepkeylib/clearsign_abi.py:77 and :82 — permissive type validation — fixed (73f96be)

Both confirmed. uint7, uint0 and int264 were accepted and emitted plausible 32-byte words for types no compiler emits. bool coerced truthiness, so 1, 'false' and 2 all became ABI booleans. bytes0 was accepted, and bytes33 was worse than invalid: ljust(32, ...) does not truncate, so a 33-byte value emitted a 33-byte "word" and shifted every following argument one byte right — silently corrupt calldata, which is the one I would least want in a signing-test fixture generator.

intN/uintN widths are now validated as 8..256 in steps of 8, bool requires an actual bool, and fixed bytes are restricted to bytes1..bytes32. Arrays now reach the existing dynamic-type error instead of being parsed as a width. All 51 catalog flows still build unchanged, and tests/test_clearsign_abi.py covers each rejection.


Verification: the lint gate is 31/31 locally (protobuf 3.20.3, PROTOCOL_BUFFERS_PYTHON_IMPLEMENTATION=python) and py_compile is clean. The new token-table gate was fault-injected on four arms. The two strengthened Ethereum tests are device tests and run on the emulator in CI, not locally — GitHub CI on this push is their first execution.

.gitmodules remains open and blocked: device-protocol #112 is still open.

BitHighlander and others added 25 commits September 20, 2026 18:04
test(solana): gate plain text at its 7.15 capability
Copilot AI review requested due to automatic review settings September 22, 2026 23:44

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot review overview

🟡 Changes recommended

One critical transport hang and multiple moderate correctness, compatibility, coverage, dependency, and CI issues remain unresolved.

Get a fresh assessment by requesting another Copilot review.

Review effort: Balanced
Findings: 2 High severity · 4 Medium severity

Open (6)
Resolved since last review (2)

data = self.socket.recv(64)
try:
data = self.socket.recv(64)
except socket.timeout:
Comment thread .gitmodules
[submodule "device-protocol"]
path = device-protocol
url = https://github.com/keepkey/device-protocol.git
url = https://github.com/BitHighlander/device-protocol.git
seen.add(key)
chosen.append(r)

for r in sorted(records, key=address_of):
Comment on lines +330 to +331
if not mnemonic or mnemonic[-1] != words:
mnemonic.append(words)
tx.append(1) # 1 instruction
tx.extend(bytes([1])) # program index -> SYSTEM_PROGRAM
tx.append(1) # 1 account index
tx.append(3) # index 3: BEYOND the static table -> external
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.

4 participants