Skip to content

test(eth): regression for EIP-1559 chunked-data signing bug (firmware ≤ 7.14.0)#191

Closed
BitHighlander wants to merge 4 commits into
keepkey:masterfrom
BitHighlander:test/eip1559-chunked-data-regression
Closed

test(eth): regression for EIP-1559 chunked-data signing bug (firmware ≤ 7.14.0)#191
BitHighlander wants to merge 4 commits into
keepkey:masterfrom
BitHighlander:test/eip1559-chunked-data-regression

Conversation

@BitHighlander
Copy link
Copy Markdown
Contributor

Summary

Adds a single regression test that catches a firmware bug in firmware/ethereum.c where the empty access-list byte (0xC0) — which closes the EIP-1559 RLP body and must be the last byte fed to keccak before signing — was being hashed inside ethereum_signing_init() immediately after the initial 1024-byte data chunk, before the host had a chance to send the remaining EthereumTxAck frames.

For any EIP-1559 transaction whose data exceeded the single-USB-chunk threshold (1024 B), the firmware produced a non-canonical pre-image:

keccak( ...header...
        || data_len_prefix
        || data[0..1024]
        || 0xC0           ← bug: should be after ALL data
        || data[1024..end] )

The signature was mathematically valid for that mangled hash so RPCs accepted the broadcast (signature checks pass), but the recovered signer was a wrong-but-deterministic address. The mempool dropped the transaction because the recovered from had no balance / wrong nonce.

Production symptom: every Uniswap Universal Router swap, Permit2 batch, and large multicall on affected firmware hung at "Confirm in wallet."

Single-chunk transactions (≤ 1024 B) escaped the bug only by accident — the misplaced 0xC0 happened to land at the end anyway.

Affected firmware

7.x.0 through 7.14.0 inclusive. Fix landing in 7.14.1.

Test design

Recovery-based, seed-agnostic. The test:

  1. Pairs the device with the all-all-all test mnemonic.
  2. Calls ethereum_get_address(m/44'/60'/0'/0/0) to capture the device's own EOA.
  3. Signs a 1550-byte EIP-1559 transaction (same size class as the captured production failure — Uniswap Universal Router calldata).
  4. Builds the canonical type-2 unsigned-envelope keccak hash from the same inputs.
  5. Performs ECDSA recovery against (canonical_hash, sig_v, sig_r, sig_s).
  6. Asserts the recovered address equals the device's address.

No golden vectors to capture, no seed-specific hard-coded sigs — the test asserts the actual invariant ("signature recovers to the signer"). Will fail on every firmware lacking the 0xC0-ordering fix, pass on 7.14.1+.

Expected CI behavior

🔴 This PR is expected to go RED in CI when run against any emulator built from a firmware revision ≤ 7.14.0. That's the intended signal — the test captures the broken state. CI will go green automatically once the matching firmware fix lands and the emulator pin is bumped.

Files

  • tests/test_msg_ethereum_signtx_chunked_data_eip1559.py — new file, single test, ~150 lines including the bug write-up and helpers.
  • .github/workflows/ci.yml — adds eth-keys to the existing pip install line. Ships a pure-Python keccak via eth-utils, no native deps.
  • scripts/generate-test-report.py — adds an E5b row so the test surfaces in the test report.

Test plan

  • CI runs and goes RED on the current emulator firmware (7.14.0). That's the proof the test catches the bug.
  • After the firmware fix is merged and the emulator pin is bumped (separate PR against the firmware repo), re-run CI here → expect ✅.

🤖 Generated with Claude Code

BitHighlander and others added 2 commits April 28, 2026 18:33
… ≤ 7.14.0)

Pairs the device, signs a 1550-byte EIP-1559 transaction with the
all-all-all test mnemonic, and asserts that ECDSA recovery against the
canonical type-2 pre-image yields the device's own address.

Catches a firmware/ethereum.c ordering bug present in 7.x.0 .. 7.14.0
where the empty access-list byte (0xC0) — which closes the EIP-1559 RLP
body and must be the last byte fed to keccak before signing — was being
hashed inside ethereum_signing_init() right after the initial 1024-byte
data chunk, BEFORE the host had a chance to send the remaining
EthereumTxAck frames. For any tx whose data exceeded the single-chunk
threshold, the resulting pre-image was:

  keccak( ...header...
          || data_len_prefix
          || data[0..1024]
          || 0xC0           (bug: should be after ALL data)
          || data[1024..end] )

The signature was mathematically valid for that mangled hash so RPCs
accepted the broadcast, but the recovered signer was a wrong-but-
deterministic address. The mempool dropped the tx because the recovered
"from" had no balance / wrong nonce. Production symptom: every Uniswap
Universal Router swap, Permit2 batch, and large multicall hung at
"Confirm in wallet."

Single-chunk transactions (<= 1024 bytes) escaped the bug only by
accident — the misplaced 0xC0 happened to land at the end anyway.

Recovery-based assertion (eth-keys, eth-utils.keccak) — works on any
seed, no golden vectors to capture, the test asserts the actual
invariant: "signature recovers to the signer." Fails on broken
firmware, passes on 7.14.1+.

CI: eth-keys added to the existing pip install line; ships a pure-Python
keccak via eth-utils so no native deps are required.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
requires_message("EthereumTxAck") sends an empty EthereumTxAck as a
discovery probe. The firmware (correctly) rejects that with
Failure_UnexpectedMessage because we're not mid-sign, which skips the
test before the actual assertion runs.

requires_firmware("7.2.1") is sufficient — EthereumTxAck has been part
of the protocol since EIP-1559 support landed in 7.2.1.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
BitHighlander and others added 2 commits April 28, 2026 18:42
eth-utils ships keccak via the eth-hash adapter, which auto-selects
between pycryptodome and pysha3 at import time. Without either backend
installed, importing keccak raises:

  ImportError: None of these hashing backends are installed:
  ['pycryptodome', 'pysha3'].

The new EIP-1559 chunked-data regression test imports keccak from
eth_utils to build the canonical type-2 pre-image, so it failed at
import rather than at the recovery assertion. Adding pycryptodome to
the existing pip-install line fixes it.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
KeepKeyTest overrides unittest's assertEqual with a 2-arg version
(common.py:104) that doesn't accept the optional msg parameter — passing
one raises:

  TypeError: KeepKeyTest.assertEqual() takes 3 positional arguments
  but 4 were given

Print the regression diagnostic before asserting instead. Pytest captures
stdout on failure, so the divergence (expected vs recovered, canonical
hash, sig values) still surfaces in the failure report.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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