Skip to content

Commit 4d5719c

Browse files
committed
test: regression coverage for 7.15.0 firmware fixes
EIP-1559 (fix/eip1559): - test_eip1559_base_chain_id: Base mainnet chain_id=8453 (overflows uint8_t) - test_eip1559_arbitrum_chain_id: Arbitrum One chain_id=42161 - test_eip1559_avalanche_chain_id: Avalanche C-Chain chain_id=43114 Token chain-id (fix/token-chain-id): - test_erc20_transfer_arbitrum: ERC-20 transfer on Arbitrum (chain_id=42161) - test_erc20_transfer_base: ERC-20 transfer on Base (chain_id=8453) EIP-712 security (fix/eip712-security): - TestEIP712Security.test_eip712_normal_hash_signing_still_works - TestEIP712Security.test_eip712_hash_signing_no_message_hash Ripple memo (feat/ripple-memo): - test_ripple_sign_no_memo_backward_compat: omitting memo must not change serialized bytes THORChain any-denom (feat/thorchain-any-denom): - test_thorchain_rune_denom_backward_compat: rune path unchanged after firmware fix - test_thorchain_msgsend_direct_with_denom: direct proto call with denom (skips if pb2 not updated) Solana token decimals (fix/solana-token-decimals): - test_solana_token_transfer_checked_decimals_6: TokenTransferChecked with decimals=6 - test_solana_token_transfer_checked_decimals_9: TokenTransferChecked with decimals=9 TRON blind-sign (fix/tron-blind-sign): - test_tron_blind_sign_still_completes_after_ux_fix TON blind-sign (fix/ton-blind-sign): - test_ton_blind_sign_still_completes_after_ux_fix
1 parent bf870e6 commit 4d5719c

8 files changed

Lines changed: 495 additions & 0 deletions

tests/test_msg_ethereum_signtx.py

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -532,6 +532,91 @@ def test_ethereum_signtx_nodata(self):
532532
"6e33c4230b1ecf96a8dbb514b4aec0a6d6ba53f8991c8143f77812aa6daa993f",
533533
)
534534

535+
# ------------------------------------------------------------------ #
536+
# Regression: fix/eip1559 — chain_id >= 256 RLP encoding bug #
537+
# Before the fix, large chain IDs were stored as uint8_t which #
538+
# silently truncated to the low byte, producing a wrong signer. #
539+
# ------------------------------------------------------------------ #
540+
541+
def test_eip1559_base_chain_id(self):
542+
"""Regression for fix/eip1559 — EIP-1559 signing on Base (chain_id=8453).
543+
544+
chain_id=8453 overflows uint8_t (max 255). Before the fix the device
545+
signed with chain_id & 0xFF = 5 instead of 8453, recovering the wrong
546+
signer address. This test verifies the signature is produced without
547+
error and that sig_v is 0 or 1 (EIP-1559 parity, not the legacy value).
548+
"""
549+
self.requires_fullFeature()
550+
self.requires_firmware("7.15.0")
551+
self.setup_mnemonic_allallall()
552+
553+
sig_v, sig_r, sig_s = self.client.ethereum_sign_tx(
554+
n=[0x80000000 | 44, 0x80000000 | 60, 0x80000000, 0, 0],
555+
nonce=0,
556+
max_fee_per_gas=100000000000, # 100 gwei
557+
max_priority_fee_per_gas=1000000000, # 1 gwei
558+
gas_limit=21000,
559+
to=binascii.unhexlify("1d1c328764a41bda0492b66baa30c4a339ff85ef"),
560+
value=1000000000000000, # 0.001 ETH
561+
chain_id=8453, # Base mainnet
562+
)
563+
564+
# EIP-1559 parity must be 0 or 1 (never 0x21 etc. from wrong chain_id)
565+
self.assertIn(sig_v, (0, 1), "EIP-1559 sig_v must be 0 or 1 for Base chain_id=8453")
566+
self.assertEqual(len(sig_r), 32)
567+
self.assertEqual(len(sig_s), 32)
568+
569+
def test_eip1559_arbitrum_chain_id(self):
570+
"""Regression for fix/eip1559 — EIP-1559 signing on Arbitrum One (chain_id=42161).
571+
572+
chain_id=42161 (0xA4B1) was affected by the same uint8_t truncation.
573+
Verifies the device signs successfully and max_priority_fee_per_gas
574+
is honoured (non-zero tip distinguishes this from a legacy tx).
575+
"""
576+
self.requires_fullFeature()
577+
self.requires_firmware("7.15.0")
578+
self.setup_mnemonic_allallall()
579+
580+
sig_v, sig_r, sig_s = self.client.ethereum_sign_tx(
581+
n=[0x80000000 | 44, 0x80000000 | 60, 0x80000000, 0, 0],
582+
nonce=1,
583+
max_fee_per_gas=200000000000, # 200 gwei
584+
max_priority_fee_per_gas=2000000000, # 2 gwei priority tip
585+
gas_limit=21000,
586+
to=binascii.unhexlify("1d1c328764a41bda0492b66baa30c4a339ff85ef"),
587+
value=5000000000000000, # 0.005 ETH
588+
chain_id=42161, # Arbitrum One
589+
)
590+
591+
self.assertIn(sig_v, (0, 1), "EIP-1559 sig_v must be 0 or 1 for Arbitrum chain_id=42161")
592+
self.assertEqual(len(sig_r), 32)
593+
self.assertEqual(len(sig_s), 32)
594+
595+
def test_eip1559_avalanche_chain_id(self):
596+
"""Regression for fix/eip1559 — EIP-1559 signing on Avalanche C-Chain (chain_id=43114).
597+
598+
chain_id=43114 rounds to 0x2A when truncated to uint8_t. Verify
599+
signing works and parity is valid EIP-1559 (0 or 1).
600+
"""
601+
self.requires_fullFeature()
602+
self.requires_firmware("7.15.0")
603+
self.setup_mnemonic_allallall()
604+
605+
sig_v, sig_r, sig_s = self.client.ethereum_sign_tx(
606+
n=[0x80000000 | 44, 0x80000000 | 60, 0x80000000, 0, 0],
607+
nonce=2,
608+
max_fee_per_gas=30000000000, # 30 gwei
609+
max_priority_fee_per_gas=500000000, # 0.5 gwei
610+
gas_limit=21000,
611+
to=binascii.unhexlify("1d1c328764a41bda0492b66baa30c4a339ff85ef"),
612+
value=100000000000000000, # 0.1 AVAX
613+
chain_id=43114, # Avalanche C-Chain
614+
)
615+
616+
self.assertIn(sig_v, (0, 1), "EIP-1559 sig_v must be 0 or 1 for Avalanche chain_id=43114")
617+
self.assertEqual(len(sig_r), 32)
618+
self.assertEqual(len(sig_s), 32)
619+
535620

536621
if __name__ == "__main__":
537622
unittest.main()

tests/test_msg_ripple_sign_tx.py

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,48 @@ def test_sign_with_thorchain_memo(self):
151151
"plain send must not contain Memos array (0xF9 marker)"
152152
)
153153

154+
# ------------------------------------------------------------------ #
155+
# Regression: feat/ripple-memo — memo field, backward compat #
156+
# Firmware 7.14.2 added optional memo (field 7) to RippleSignTx. #
157+
# A tx without memo must still produce the same serialized bytes as #
158+
# before — adding an optional field must not change existing encodings.#
159+
# ------------------------------------------------------------------ #
160+
161+
def test_ripple_sign_no_memo_backward_compat(self):
162+
"""Regression for feat/ripple-memo — signing without memo must be byte-identical to pre-7.14.2.
163+
164+
The Memos field is optional. Omitting it must not change the
165+
serialized transaction encoding (no trailing 0xF9 marker).
166+
The expected serialized_tx is pinned from the original test_sign test.
167+
"""
168+
self.requires_fullFeature()
169+
self.requires_firmware("7.14.2")
170+
self.setup_mnemonic_allallall()
171+
172+
msg = messages.RippleSignTx(
173+
address_n=parse_path("m/44'/144'/0'/0/0"),
174+
payment=messages.RipplePayment(
175+
amount=100000000,
176+
destination="rBKz5MC2iXdoS3XgnNSYmF69K1Yo4NS3Ws",
177+
),
178+
flags=0x80000000,
179+
fee=100000,
180+
sequence=25,
181+
# memo field intentionally absent
182+
)
183+
resp = self.client.call(msg)
184+
185+
# Must match the serialized bytes from the pre-memo firmware exactly
186+
self.assertEqual(
187+
binascii.hexlify(resp.serialized_tx),
188+
"12000022800000002400000019614000000005f5e1006840000000000186a0732102131facd1eab748d6cddc492f54b04e8c35658894f4add2232ebc5afe7521dbe474473045022100e243ef623675eeeb95965c35c3e06d63a9fc68bb37e17dc87af9c0af83ec057e02206ca8aa5eaab8396397aef6d38d25710441faf7c79d292ee1d627df15ad9346c081148fb40e1ffa5d557ce9851a535af94965e0dd098883147148ebebf7304ccdf1676fefcf9734cf1e780826",
189+
)
190+
# No Memos STArray marker (0xF9) should appear in the output
191+
self.assertFalse(
192+
b'\xf9' in resp.serialized_tx,
193+
"Plain send without memo must not contain XRPL Memos array (0xF9 marker)",
194+
)
195+
154196
def test_ripple_sign_invalid_fee(self):
155197
self.requires_fullFeature()
156198
self.requires_firmware("6.4.0")

tests/test_msg_signtx_ethereum_erc20.py

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,77 @@ def test_approve_all(self):
8686
self.assertEqual(binascii.hexlify(sig_r), '3671acb6aed5241948de56635ef64554d5e834355e99d806c4ae30bf463eae57')
8787
self.assertEqual(binascii.hexlify(sig_s), '2b0aa2fdfabefb4ae687f3418b13cddf1111e62338bc8fd3ca4e0196352bb6f8')
8888

89+
# ------------------------------------------------------------------ #
90+
# Regression: fix/token-chain-id — uint8_t overflow for chain_id>255 #
91+
# ERC-20 token lookup used a uint8_t for chain_id, silently wrapping #
92+
# for Arbitrum (42161), Base (8453), and Avalanche (43114). #
93+
# ------------------------------------------------------------------ #
94+
95+
def test_erc20_transfer_arbitrum(self):
96+
"""Regression for fix/token-chain-id — ERC-20 transfer on Arbitrum One.
97+
98+
chain_id=42161 previously overflowed uint8_t in the token lookup table,
99+
causing the display to show the wrong (or no) token name. This test
100+
verifies signing completes without error on Arbitrum.
101+
"""
102+
self.requires_fullFeature()
103+
self.requires_firmware("7.15.0")
104+
self.setup_mnemonic_allallall()
105+
106+
# USDT on Arbitrum One (0xfd086bc7cd5c481dcc9c85ebe478a1c0b69fcbb9)
107+
# transfer(address,uint256): 0xa9059cbb + padded recipient + amount
108+
recipient = '0000000000000000000000001d1c328764a41bda0492b66baa30c4a339ff85ef'
109+
amount = '000000000000000000000000000000000000000000000000000000003b9aca00' # 1000 USDT (6 dec)
110+
transfer_data = binascii.unhexlify('a9059cbb' + recipient + amount)
111+
112+
sig_v, sig_r, sig_s = self.client.ethereum_sign_tx(
113+
n=[0x80000000 | 44, 0x80000000 | 60, 0x80000000, 0, 0],
114+
nonce=0,
115+
gas_price=100000000, # 0.1 gwei (Arbitrum is cheap)
116+
gas_limit=65000,
117+
value=0,
118+
to=binascii.unhexlify('fd086bc7cd5c481dcc9c85ebe478a1c0b69fcbb9'),
119+
chain_id=42161, # Arbitrum One
120+
data=transfer_data,
121+
)
122+
123+
# EIP-155 replay protection: v = 2*chain_id + 35 or 36 = 84357 or 84358
124+
self.assertIn(sig_v, (84357, 84358),
125+
"Expected EIP-155 v for Arbitrum chain_id=42161, got %d" % sig_v)
126+
self.assertEqual(len(sig_r), 32)
127+
self.assertEqual(len(sig_s), 32)
128+
129+
def test_erc20_transfer_base(self):
130+
"""Regression for fix/token-chain-id — ERC-20 transfer on Base (chain_id=8453).
131+
132+
chain_id=8453 also overflowed the uint8_t token lookup.
133+
"""
134+
self.requires_fullFeature()
135+
self.requires_firmware("7.15.0")
136+
self.setup_mnemonic_allallall()
137+
138+
# USDC on Base (0x833589fcd6edb6e08f4c7c32d4f71b54bda02913)
139+
recipient = '0000000000000000000000001d1c328764a41bda0492b66baa30c4a339ff85ef'
140+
amount = '0000000000000000000000000000000000000000000000000000000000989680' # 10 USDC
141+
transfer_data = binascii.unhexlify('a9059cbb' + recipient + amount)
142+
143+
sig_v, sig_r, sig_s = self.client.ethereum_sign_tx(
144+
n=[0x80000000 | 44, 0x80000000 | 60, 0x80000000, 0, 0],
145+
nonce=0,
146+
gas_price=50000000, # 0.05 gwei
147+
gas_limit=65000,
148+
value=0,
149+
to=binascii.unhexlify('833589fcd6edb6e08f4c7c32d4f71b54bda02913'),
150+
chain_id=8453, # Base
151+
data=transfer_data,
152+
)
153+
154+
# EIP-155: v = 2*8453 + 35 or 36 = 16941 or 16942
155+
self.assertIn(sig_v, (16941, 16942),
156+
"Expected EIP-155 v for Base chain_id=8453, got %d" % sig_v)
157+
self.assertEqual(len(sig_r), 32)
158+
self.assertEqual(len(sig_s), 32)
159+
89160

90161
if __name__ == '__main__':
91162
unittest.main()

tests/test_msg_solana_signtx.py

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -575,6 +575,87 @@ def test_solana_sign_token_transfer_with_metadata(self):
575575
self.assertEqual(len(resp.signature), 64)
576576
self.assertFalse(all(b == 0 for b in resp.signature))
577577

578+
# ------------------------------------------------------------------ #
579+
# Regression: fix/solana-token-decimals #
580+
# The instruction-level decimals field in TokenTransferChecked was #
581+
# ignored; the host-supplied token_info.decimals were used instead. #
582+
# After the fix the instruction decimals override host metadata. #
583+
# ------------------------------------------------------------------ #
584+
585+
def test_solana_token_transfer_checked_decimals_6(self):
586+
"""Regression for fix/solana-token-decimals — TokenTransferChecked with decimals=6.
587+
588+
SPL Token TransferChecked (opcode=12) embeds explicit decimals in the
589+
instruction. Before the fix the firmware used host metadata decimals
590+
instead of the instruction field. This test verifies the instruction
591+
signs correctly with decimals=6 passed inside the instruction data.
592+
"""
593+
self.requires_fullFeature()
594+
self.requires_firmware("7.15.0")
595+
self.setup_mnemonic_allallall()
596+
597+
from_pubkey = self._get_from_pubkey()
598+
to_account = b'\x33' * 32 # destination token account
599+
mint_pubkey = b'\x44' * 32 # token mint
600+
601+
# SPL Token TransferChecked: opcode=12 (u8) + amount (LE u64) + decimals (u8)
602+
# Format: [12] [amount:8 LE] [decimals:1]
603+
amount = 5000000 # 5.0 USDC (6 decimals)
604+
decimals = 6
605+
instr_data = bytes([12]) + struct.pack('<Q', amount) + bytes([decimals])
606+
607+
# TransferChecked needs: source, mint, destination, owner (in addition to from_pubkey)
608+
# Build tx with 4 extra accounts: source_ata, mint, dest_ata, owner(=from_pubkey)
609+
source_ata = b'\x55' * 32
610+
611+
raw_tx = self._build_tx(
612+
from_pubkey,
613+
[source_ata, mint_pubkey, to_account],
614+
self.TOKEN_PROGRAM,
615+
instr_data,
616+
)
617+
618+
resp = self.client.call(messages.SolanaSignTx(
619+
address_n=parse_path("m/44'/501'/0'/0'"),
620+
raw_tx=raw_tx,
621+
))
622+
623+
self.assertEqual(len(resp.signature), 64)
624+
self.assertFalse(all(b == 0 for b in resp.signature))
625+
626+
def test_solana_token_transfer_checked_decimals_9(self):
627+
"""Regression for fix/solana-token-decimals — TokenTransferChecked with decimals=9.
628+
629+
Verifies a second decimal precision (SOL-like 9-decimal token) signs
630+
correctly — ensures the fix handles decimals != 6 too.
631+
"""
632+
self.requires_fullFeature()
633+
self.requires_firmware("7.15.0")
634+
self.setup_mnemonic_allallall()
635+
636+
from_pubkey = self._get_from_pubkey()
637+
to_account = b'\x33' * 32
638+
mint_pubkey = b'\x66' * 32
639+
source_ata = b'\x77' * 32
640+
641+
# TransferChecked with 9 decimals, amount 1_000_000_000 (= 1.0 token)
642+
instr_data = bytes([12]) + struct.pack('<Q', 1000000000) + bytes([9])
643+
644+
raw_tx = self._build_tx(
645+
from_pubkey,
646+
[source_ata, mint_pubkey, to_account],
647+
self.TOKEN_PROGRAM,
648+
instr_data,
649+
)
650+
651+
resp = self.client.call(messages.SolanaSignTx(
652+
address_n=parse_path("m/44'/501'/0'/0'"),
653+
raw_tx=raw_tx,
654+
))
655+
656+
self.assertEqual(len(resp.signature), 64)
657+
self.assertFalse(all(b == 0 for b in resp.signature))
658+
578659
# ================================================================
579660
# Path edge-case tests
580661
# ================================================================

0 commit comments

Comments
 (0)