From df30474d612bb2a47337c397f9f77eb782fcd078 Mon Sep 17 00:00:00 2001 From: highlander Date: Sat, 15 Aug 2026 00:31:09 -0600 Subject: [PATCH] test(eth): pin the multi-byte chain_id EIP-1559 regression MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Firmware fix ed6db167 ("EIP-1559 chainId hashing wrong for multi-byte chain IDs") shipped with no test at a chain id that reproduces it. Every EIP-1559 case in this file uses chain_id 1 or 3 — both single-byte — so the bug had zero coverage in the file that tests the feature, and can silently return. The bug: hash_rlp_field((uint8_t*)&chain_id, 1) fed only the least-significant byte into keccak on little-endian ARM. Base (8453 = 0x2105) hashed 0x05, so the signature recovered to an unrelated address with no funds. The RLP length was computed correctly from the full value and the legacy EIP-155 path was always right — only the EIP-1559 hash step was wrong. A golden r/s would need a device run to produce, so this is a differential instead. Sign one identical tx under two chain ids the BUGGY firmware cannot distinguish: 8453 = 0x2105 low byte 0x05, two-byte value 4357 = 0x1105 low byte 0x05, two-byte value Same low byte and same RLP length header, so the broken code hashes a byte-identical pre-image for both. Signing is deterministic (RFC 6979), so buggy firmware returns the same signature twice and the assertion fails. Correct firmware hashes 0x21 0x05 vs 0x11 0x05 and they differ. No golden value, no crypto deps — the repo has ecdsa but no keccak, so recovering the signer address was not an option. Version-gated to 7.15.0 so it SKIPs rather than fails on firmware predating the fix. Worth recording for whoever tests this by hand: Vault cannot reach this path at all. It forces legacy transactions for chainId >= 256 (eip1559Ok = chainId < 256 in swap.ts), which is why ordinary Vault sends on Base were never broken. The BEX builds type-2 with no such guard, so a dApp transaction through the extension is the only manual route to it. --- tests/test_msg_ethereum_signtx.py | 54 +++++++++++++++++++++++++++++++ 1 file changed, 54 insertions(+) diff --git a/tests/test_msg_ethereum_signtx.py b/tests/test_msg_ethereum_signtx.py index 15a61d3f..f7534375 100644 --- a/tests/test_msg_ethereum_signtx.py +++ b/tests/test_msg_ethereum_signtx.py @@ -381,6 +381,60 @@ def test_ethereum_eip_1559(self): "67297089e0ba53c29dda1aafc23fce64a772c5433e127e5885edc03ece4670c9", ) + def test_ethereum_eip_1559_multibyte_chain_id(self): + """EIP-1559 must hash the WHOLE chain_id, not just its low byte. + + Regression for the multi-byte chain_id bug: the EIP-1559 hash step used + hash_rlp_field((uint8_t*)&chain_id, 1), which on little-endian ARM fed + only the least-significant byte into keccak. For Base (8453 = 0x2105) + that hashed 0x05, so the signature recovered to an unrelated address + with no funds. The RLP *length* was computed correctly from the full + value, and the legacy EIP-155 path was always correct — only the + EIP-1559 hash was wrong. Affected: Base (8453), Arbitrum (42161), + Avalanche (43114). Unaffected: ETH (1), OP (10), BSC (56), Polygon (137). + + Asserting a golden r/s would need a device run to produce it. Instead + sign one identical transaction under two chain ids chosen so the BUGGY + firmware cannot tell them apart: + + 8453 = 0x2105 low byte 0x05, two-byte value + 4357 = 0x1105 low byte 0x05, two-byte value + + Same low byte AND same RLP length header, so the broken code hashes a + byte-identical pre-image for both. Signing is deterministic (RFC 6979), + so buggy firmware returns the SAME signature twice and this fails. + Correct firmware hashes 0x21 0x05 vs 0x11 0x05, which must differ. + + Vault cannot reach this path: it forces legacy txs for chainId >= 256 + (eip1559Ok = chainId < 256). The BEX builds type-2 with no such guard, + which is why the failures only ever showed up there. + """ + self.requires_fullFeature() + self.requires_firmware("7.15.0") + self.setup_mnemonic_nopin_nopassphrase() + + def sign(chain_id): + return self.client.ethereum_sign_tx( + n=[0x80000000 | 44, 0x80000000 | 60, 0x80000000, 0, 0], + nonce=0, + gas_limit=0x5ac3, + max_fee_per_gas=0x16854be509, + max_priority_fee_per_gas=0x540ae480, + to=binascii.unhexlify("fc0cc6e85dff3d75e3985e0cb83b090cfd498dd1"), + value=0x1550f7dca70000, + chain_id=chain_id, + ) + + _, base_r, base_s = sign(8453) + _, twin_r, twin_s = sign(4357) + + self.assertNotEqual( + (binascii.hexlify(base_r), binascii.hexlify(base_s)), + (binascii.hexlify(twin_r), binascii.hexlify(twin_s)), + "chain_id 8453 and 4357 produced the same signature — only the low " + "byte of chain_id reached the EIP-1559 hash", + ) + def test_ethereum_signtx_nodata_eip_1559(self): self.requires_fullFeature() self.requires_firmware("7.2.1")