Skip to content

Commit ee0c447

Browse files
committed
feat: expand test coverage for Solana, TRON, TON (58 tests total)
Solana (+11 tests → 23 total): - 4 negative/rejection: malformed, truncated, trailing bytes, oversized - 2 multi-instruction: 2x transfer, transfer+memo - 1 token metadata: SolanaTokenInfo with USDC mint/symbol/decimals - 2 path edge cases: 3-element path, wrong coin type - 1 versioned v0: opaque tx requiring AdvancedMode - 1 already existed: sign_message_blocked_without_advanced_mode TRON (+6 tests → 15 total): - 2 path edge cases: too short (2 levels), wrong coin type - 2 negative: empty raw_data, oversized raw_data - 1 determinism: same raw_data produces same signature - 1 different accounts: different keys produce different signatures TON (+9 tests → 20 total): - 2 path edge cases: too short (2 levels), wrong coin type - 2 negative: empty raw_tx, oversized raw_tx - 2 memo edge cases: empty memo, long memo (255 chars) - 2 workchain: explicit zero, default (verify match) - 1 different accounts: different keys produce different signatures Also: removed duplicate test_tron_show_address and test_ton_show_address
1 parent 3d36e92 commit ee0c447

5 files changed

Lines changed: 661 additions & 26 deletions

File tree

tests/test_msg_solana_signtx.py

Lines changed: 337 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -354,5 +354,342 @@ def test_solana_sign_memo(self):
354354
self.assertEqual(len(resp.signature), 64)
355355

356356

357+
# ================================================================
358+
# Negative / rejection tests
359+
# ================================================================
360+
361+
def test_solana_sign_malformed_truncated(self):
362+
"""Reject raw_tx that is too short to contain header + accounts."""
363+
self.requires_fullFeature()
364+
self.setup_mnemonic_allallall()
365+
366+
raw_tx = b'\x00\x01\x00\x01\x01' # 5 bytes — header says 1 account but no data
367+
368+
with pytest.raises(CallException):
369+
self.client.call(messages.SolanaSignTx(
370+
address_n=parse_path("m/44'/501'/0'/0'"),
371+
raw_tx=raw_tx,
372+
))
373+
374+
def test_solana_sign_malformed_bad_account_count(self):
375+
"""Reject raw_tx whose header claims 33 accounts (exceeds 32 limit)."""
376+
self.requires_fullFeature()
377+
self.setup_mnemonic_allallall()
378+
379+
tx = bytearray()
380+
tx.append(0) # sig count
381+
tx.append(1) # num_required_sigs
382+
tx.append(0) # num_readonly_signed
383+
tx.append(1) # num_readonly_unsigned
384+
tx.append(33) # 33 accounts — over the 32-account parser limit
385+
# Pad 33 fake 32-byte account keys
386+
for _ in range(33):
387+
tx.extend(b'\xAA' * 32)
388+
tx.extend(b'\xBB' * 32) # blockhash
389+
tx.append(0) # 0 instructions
390+
391+
with pytest.raises(CallException):
392+
self.client.call(messages.SolanaSignTx(
393+
address_n=parse_path("m/44'/501'/0'/0'"),
394+
raw_tx=bytes(tx),
395+
))
396+
397+
def test_solana_sign_malformed_trailing_bytes(self):
398+
"""Reject a valid transaction that has extra trailing bytes."""
399+
self.requires_fullFeature()
400+
self.setup_mnemonic_allallall()
401+
402+
from_pubkey = self._get_from_pubkey()
403+
to_pubkey = b'\x22' * 32
404+
raw_tx = build_system_transfer_tx(from_pubkey, to_pubkey, 1000000000)
405+
406+
# Append 10 trailing garbage bytes
407+
raw_tx_bad = raw_tx + b'\xFF' * 10
408+
409+
with pytest.raises(CallException):
410+
self.client.call(messages.SolanaSignTx(
411+
address_n=parse_path("m/44'/501'/0'/0'"),
412+
raw_tx=raw_tx_bad,
413+
))
414+
415+
def test_solana_sign_oversized_raw_tx(self):
416+
"""Reject raw_tx that exceeds the proto max_size (1232 bytes)."""
417+
self.requires_fullFeature()
418+
self.setup_mnemonic_allallall()
419+
420+
# 1233 bytes — one byte over the nanopb field limit
421+
raw_tx = b'\x00' * 1233
422+
423+
with pytest.raises(CallException):
424+
self.client.call(messages.SolanaSignTx(
425+
address_n=parse_path("m/44'/501'/0'/0'"),
426+
raw_tx=raw_tx,
427+
))
428+
429+
# ================================================================
430+
# Multi-instruction tests
431+
# ================================================================
432+
433+
def test_solana_sign_multi_instruction_2x_transfer(self):
434+
"""Two system transfers in a single transaction."""
435+
self.requires_fullFeature()
436+
self.setup_mnemonic_allallall()
437+
438+
from_pubkey = self._get_from_pubkey()
439+
to_pubkey_1 = b'\x22' * 32
440+
to_pubkey_2 = b'\x33' * 32
441+
system_program = self.SYSTEM_PROGRAM
442+
blockhash = b'\xBB' * 32
443+
444+
tx = bytearray()
445+
tx.append(0) # sig count
446+
tx.append(1) # num_required_sigs
447+
tx.append(0) # num_readonly_signed
448+
tx.append(1) # num_readonly_unsigned (system program)
449+
450+
# 4 accounts: from, to_1, to_2, system_program
451+
tx.append(4)
452+
tx.extend(from_pubkey)
453+
tx.extend(to_pubkey_1)
454+
tx.extend(to_pubkey_2)
455+
tx.extend(system_program)
456+
457+
tx.extend(blockhash)
458+
459+
# 2 instructions
460+
tx.append(2)
461+
462+
# Instruction 1: transfer 1 SOL to to_1
463+
tx.append(3) # program_id index (system program)
464+
tx.append(2) # 2 account indices
465+
tx.append(0) # from
466+
tx.append(1) # to_1
467+
instr1 = struct.pack('<I', 2) + struct.pack('<Q', 1000000000)
468+
tx.append(len(instr1))
469+
tx.extend(instr1)
470+
471+
# Instruction 2: transfer 2 SOL to to_2
472+
tx.append(3) # program_id index (system program)
473+
tx.append(2) # 2 account indices
474+
tx.append(0) # from
475+
tx.append(2) # to_2
476+
instr2 = struct.pack('<I', 2) + struct.pack('<Q', 2000000000)
477+
tx.append(len(instr2))
478+
tx.extend(instr2)
479+
480+
resp = self.client.call(messages.SolanaSignTx(
481+
address_n=parse_path("m/44'/501'/0'/0'"),
482+
raw_tx=bytes(tx),
483+
))
484+
self.assertEqual(len(resp.signature), 64)
485+
self.assertFalse(all(b == 0 for b in resp.signature))
486+
487+
def test_solana_sign_multi_instruction_transfer_and_memo(self):
488+
"""System transfer + memo instruction in a single transaction."""
489+
self.requires_fullFeature()
490+
self.setup_mnemonic_allallall()
491+
492+
from_pubkey = self._get_from_pubkey()
493+
to_pubkey = b'\x22' * 32
494+
system_program = self.SYSTEM_PROGRAM
495+
memo_program = self.MEMO_PROGRAM
496+
blockhash = b'\xBB' * 32
497+
498+
tx = bytearray()
499+
tx.append(0) # sig count
500+
tx.append(1) # num_required_sigs
501+
tx.append(0) # num_readonly_signed
502+
tx.append(2) # num_readonly_unsigned (system_program + memo_program)
503+
504+
# 4 accounts: from, to, system_program, memo_program
505+
tx.append(4)
506+
tx.extend(from_pubkey)
507+
tx.extend(to_pubkey)
508+
tx.extend(system_program)
509+
tx.extend(memo_program)
510+
511+
tx.extend(blockhash)
512+
513+
# 2 instructions
514+
tx.append(2)
515+
516+
# Instruction 1: system transfer 1 SOL
517+
tx.append(2) # program_id index (system program)
518+
tx.append(2) # 2 account indices
519+
tx.append(0) # from
520+
tx.append(1) # to
521+
instr1 = struct.pack('<I', 2) + struct.pack('<Q', 1000000000)
522+
tx.append(len(instr1))
523+
tx.extend(instr1)
524+
525+
# Instruction 2: memo
526+
tx.append(3) # program_id index (memo program)
527+
tx.append(1) # 1 account index (signer)
528+
tx.append(0) # from (signer)
529+
memo_data = b"payment for services"
530+
tx.append(len(memo_data))
531+
tx.extend(memo_data)
532+
533+
resp = self.client.call(messages.SolanaSignTx(
534+
address_n=parse_path("m/44'/501'/0'/0'"),
535+
raw_tx=bytes(tx),
536+
))
537+
self.assertEqual(len(resp.signature), 64)
538+
self.assertFalse(all(b == 0 for b in resp.signature))
539+
540+
# ================================================================
541+
# Token metadata (token_info) tests
542+
# ================================================================
543+
544+
def test_solana_sign_token_transfer_with_metadata(self):
545+
"""SPL Token transfer with SolanaTokenInfo for OLED display of symbol + decimals."""
546+
self.requires_fullFeature()
547+
self.setup_mnemonic_allallall()
548+
549+
from_pubkey = self._get_from_pubkey()
550+
to_account = b'\x33' * 32 # destination token account
551+
552+
# USDC mint (EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v)
553+
usdc_mint = bytes([
554+
0xc6, 0xfa, 0x7a, 0xf3, 0xbe, 0xdb, 0xad, 0x3a,
555+
0x3d, 0x65, 0xf3, 0x6a, 0xab, 0xc9, 0x74, 0x31,
556+
0xb1, 0xbb, 0xe4, 0xc2, 0xd2, 0xf6, 0xe0, 0xe4,
557+
0x7c, 0xa6, 0x02, 0x03, 0x45, 0x20, 0x23, 0x34,
558+
])
559+
560+
# SPL Token Transfer instruction: opcode=3 (u8) + amount (LE u64)
561+
instr_data = bytes([3]) + struct.pack('<Q', 1000000) # 1.0 USDC (6 decimals)
562+
raw_tx = self._build_tx(from_pubkey, [to_account], self.TOKEN_PROGRAM, instr_data)
563+
564+
token_info = messages.SolanaTokenInfo(
565+
mint=usdc_mint,
566+
symbol="USDC",
567+
decimals=6,
568+
)
569+
570+
resp = self.client.call(messages.SolanaSignTx(
571+
address_n=parse_path("m/44'/501'/0'/0'"),
572+
raw_tx=raw_tx,
573+
token_info=[token_info],
574+
))
575+
self.assertEqual(len(resp.signature), 64)
576+
self.assertFalse(all(b == 0 for b in resp.signature))
577+
578+
# ================================================================
579+
# Path edge-case tests
580+
# ================================================================
581+
582+
def test_solana_path_3_elements(self):
583+
"""Non-standard 3-element path m/44'/501'/0' — should still derive and sign."""
584+
self.requires_fullFeature()
585+
self.setup_mnemonic_allallall()
586+
587+
# Get address with 3-element path
588+
addr_resp = self.client.call(messages.SolanaGetAddress(
589+
address_n=parse_path("m/44'/501'/0'"),
590+
show_display=False,
591+
))
592+
ALPHABET = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz'
593+
n = 0
594+
for c in addr_resp.address:
595+
n = n * 58 + ALPHABET.index(c)
596+
from_pubkey = n.to_bytes(32, 'big')
597+
to_pubkey = b'\x22' * 32
598+
599+
raw_tx = build_system_transfer_tx(from_pubkey, to_pubkey, 500000000)
600+
601+
resp = self.client.call(messages.SolanaSignTx(
602+
address_n=parse_path("m/44'/501'/0'"),
603+
raw_tx=raw_tx,
604+
))
605+
self.assertEqual(len(resp.signature), 64)
606+
self.assertFalse(all(b == 0 for b in resp.signature))
607+
608+
def test_solana_path_wrong_coin_type(self):
609+
"""Path with Ethereum coin type m/44'/60'/0'/0' — firmware should reject or warn."""
610+
self.requires_fullFeature()
611+
self.setup_mnemonic_allallall()
612+
613+
# Build a minimal valid-looking tx with a dummy from_pubkey
614+
from_pubkey = b'\x11' * 32
615+
to_pubkey = b'\x22' * 32
616+
raw_tx = build_system_transfer_tx(from_pubkey, to_pubkey, 100000000)
617+
618+
with pytest.raises(CallException):
619+
self.client.call(messages.SolanaSignTx(
620+
address_n=parse_path("m/44'/60'/0'/0'"),
621+
raw_tx=raw_tx,
622+
))
623+
624+
# ================================================================
625+
# Versioned transaction test
626+
# ================================================================
627+
628+
def test_solana_sign_versioned_v0_opaque(self):
629+
"""Versioned v0 transaction (first byte 0x80) — should require AdvancedMode
630+
for blind/opaque signing since firmware cannot parse address lookup tables."""
631+
self.requires_fullFeature()
632+
self.setup_mnemonic_allallall()
633+
634+
from_pubkey = self._get_from_pubkey()
635+
636+
# Build a versioned v0 transaction:
637+
# byte 0x80 = version prefix (bit 7 set = versioned, bits 0-6 = version 0)
638+
# Then a minimal legacy-format body after the version byte
639+
to_pubkey = b'\x22' * 32
640+
system_program = self.SYSTEM_PROGRAM
641+
blockhash = b'\xBB' * 32
642+
643+
tx = bytearray()
644+
tx.append(0x80) # version prefix: v0
645+
646+
# Header
647+
tx.append(1) # num_required_sigs
648+
tx.append(0) # num_readonly_signed
649+
tx.append(1) # num_readonly_unsigned
650+
651+
# 3 accounts
652+
tx.append(3)
653+
tx.extend(from_pubkey)
654+
tx.extend(to_pubkey)
655+
tx.extend(system_program)
656+
657+
# Recent blockhash
658+
tx.extend(blockhash)
659+
660+
# 1 instruction
661+
tx.append(1)
662+
tx.append(2) # program_id index
663+
tx.append(2) # 2 account indices
664+
tx.append(0) # from
665+
tx.append(1) # to
666+
instr_data = struct.pack('<I', 2) + struct.pack('<Q', 1000000000)
667+
tx.append(len(instr_data))
668+
tx.extend(instr_data)
669+
670+
# Address table lookups: 0 entries
671+
tx.append(0)
672+
673+
raw_tx = bytes(tx)
674+
675+
# Without AdvancedMode, versioned tx should be rejected
676+
self.client.apply_policy('AdvancedMode', False)
677+
with pytest.raises(CallException):
678+
self.client.call(messages.SolanaSignTx(
679+
address_n=parse_path("m/44'/501'/0'/0'"),
680+
raw_tx=raw_tx,
681+
))
682+
683+
# With AdvancedMode, it should succeed (opaque/blind sign)
684+
self.client.apply_policy('AdvancedMode', True)
685+
resp = self.client.call(messages.SolanaSignTx(
686+
address_n=parse_path("m/44'/501'/0'/0'"),
687+
raw_tx=raw_tx,
688+
))
689+
self.assertEqual(len(resp.signature), 64)
690+
self.assertFalse(all(b == 0 for b in resp.signature))
691+
self.client.apply_policy('AdvancedMode', False)
692+
693+
357694
if __name__ == '__main__':
358695
unittest.main()

0 commit comments

Comments
 (0)