Skip to content

Commit 69d28d6

Browse files
committed
test(zcash): split helper tests + cover client wrappers
Addresses review of PR #15. Test structure Helper tests (no device) move to a dedicated module: tests/test_zcash_seed_fingerprint_helper.py This module deliberately does NOT import common, transport, or any protobuf bindings, so it runs on a stock dev box: pytest tests/test_zcash_seed_fingerprint_helper.py The previous file inherited common.KeepKeyTest, whose setUp wipes the device — pytest -k 'helper' was never actually offline. Client wrapper coverage Device-backed tests now go through the public client helpers (self.client.zcash_display_address(... expected_seed_fingerprint=...) and self.client.zcash_sign_pczt(... expected_seed_fingerprint=...)) rather than building raw protobuf messages with self.client.call(). Confirms the kwarg pass-through end-to-end. New test test_device_fingerprint_matches_python_helper: cross-checks the device-computed fingerprint against the python-keepkey helper for the same seed (all-allallall mnemonic, empty passphrase). Ties the firmware C, python-keepkey helper, and ZIP-32 §6.1 reference vector to the same byte-for-byte output.
1 parent e88ff15 commit 69d28d6

2 files changed

Lines changed: 119 additions & 95 deletions

File tree

tests/test_msg_zcash_seed_fingerprint.py

Lines changed: 65 additions & 95 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,7 @@
1-
# Zcash seed_fingerprint binding tests (ZIP-32 §6.1).
1+
# Device-backed tests for ZIP-32 §6.1 seed_fingerprint binding.
22
#
3-
# Covers:
4-
# - calculate_seed_fingerprint() matches the Keystone3 reference vector
5-
# (cross-checked against keystone3-firmware
6-
# rust/keystore/src/algorithms/zcash/mod.rs::test_keystore_derive_zcash_ufvk).
7-
# - ZcashGetOrchardFVK returns the seed_fingerprint.
8-
# - The fingerprint is consistent across messages on the same device/seed
9-
# (FVK response, ZcashAddress response).
10-
# - expected_seed_fingerprint passes when matching, fails when wrong.
11-
# - Backward compat: omitting expected_seed_fingerprint still works.
3+
# Pure-Python helper tests live in test_zcash_seed_fingerprint_helper.py
4+
# (no common.KeepKeyTest dependency — runs offline).
125

136
import unittest
147
import pytest
@@ -24,41 +17,13 @@
2417

2518

2619
class TestMsgZcashSeedFingerprint(common.KeepKeyTest):
20+
"""Binding behavior on a real device. Wipes/initializes the device."""
2721

2822
def setUp(self):
2923
super().setUp()
3024
self.requires_firmware("7.15.0")
3125
self.requires_message("ZcashGetOrchardFVK")
3226

33-
# ── Pure helper: no device ────────────────────────────────────────
34-
35-
def test_helper_reference_vector(self):
36-
"""calculate_seed_fingerprint matches the keystone3-firmware vector.
37-
38-
seed = 000102...1f, fingerprint =
39-
deff604c246710f7176dead02aa746f2fd8d5389f7072556dcb555fdbe5e3ae3
40-
"""
41-
seed = bytes(range(32))
42-
fp = calculate_seed_fingerprint(seed)
43-
self.assertEqual(
44-
fp.hex(),
45-
"deff604c246710f7176dead02aa746f2fd8d5389f7072556dcb555fdbe5e3ae3",
46-
)
47-
48-
def test_helper_rejects_trivial_seeds(self):
49-
with pytest.raises(ValueError):
50-
calculate_seed_fingerprint(b"\x00" * 32)
51-
with pytest.raises(ValueError):
52-
calculate_seed_fingerprint(b"\xff" * 32)
53-
54-
def test_helper_rejects_out_of_range(self):
55-
with pytest.raises(ValueError):
56-
calculate_seed_fingerprint(b"\x01" * 31) # too short
57-
with pytest.raises(ValueError):
58-
calculate_seed_fingerprint(b"\x01" * 253) # too long
59-
60-
# ── Device-backed tests ───────────────────────────────────────────
61-
6227
def test_get_orchard_fvk_returns_seed_fingerprint(self):
6328
"""ZcashGetOrchardFVK response now includes a 32-byte seed_fingerprint."""
6429
self.setup_mnemonic_allallall()
@@ -69,7 +34,7 @@ def test_get_orchard_fvk_returns_seed_fingerprint(self):
6934
)
7035
self.assertTrue(fvk.HasField("seed_fingerprint"))
7136
self.assertEqual(len(fvk.seed_fingerprint), 32)
72-
# Not all zero (defensive: would mean BLAKE2b returned junk)
37+
# Defensive: BLAKE2b should never produce all-zero output for a real seed
7338
self.assertNotEqual(fvk.seed_fingerprint, b"\x00" * 32)
7439

7540
def test_fingerprint_stable_across_accounts(self):
@@ -82,99 +47,104 @@ def test_fingerprint_stable_across_accounts(self):
8247
address_n=[H + 32, H + 133, H + 1], account=1)
8348
self.assertEqual(fvk0.seed_fingerprint, fvk1.seed_fingerprint)
8449

85-
# ── ZcashDisplayAddress: expected_seed_fingerprint binding ────────
50+
# ── ZcashDisplayAddress: through client.zcash_display_address(...) ──
51+
# These tests exercise the new expected_seed_fingerprint kwarg on the
52+
# public client helper, not just raw protobuf.
8653

87-
def test_display_address_accepts_matching_fingerprint(self):
88-
"""DisplayAddress with the device's own fingerprint succeeds."""
54+
def test_display_address_helper_accepts_matching_fingerprint(self):
55+
"""Helper passes expected_seed_fingerprint through; matching fp succeeds."""
8956
self.setup_mnemonic_allallall()
9057

9158
fvk = self.client.zcash_get_orchard_fvk(
9259
address_n=[H + 32, H + 133, H + 0], account=0)
9360

94-
resp = self.client.call(
95-
zcash_proto.ZcashDisplayAddress(
96-
address_n=[H + 32, H + 133, H + 0],
97-
account=0,
98-
address="u1placeholder",
99-
ak=fvk.ak,
100-
nk=fvk.nk,
101-
rivk=fvk.rivk,
102-
expected_seed_fingerprint=fvk.seed_fingerprint,
103-
)
61+
resp = self.client.zcash_display_address(
62+
address_n=[H + 32, H + 133, H + 0],
63+
address="u1placeholder",
64+
ak=fvk.ak,
65+
nk=fvk.nk,
66+
rivk=fvk.rivk,
67+
account=0,
68+
expected_seed_fingerprint=fvk.seed_fingerprint,
10469
)
10570
self.assertIsInstance(resp, zcash_proto.ZcashAddress)
106-
# Response also returns the device's seed_fingerprint
10771
self.assertTrue(resp.HasField("seed_fingerprint"))
10872
self.assertEqual(resp.seed_fingerprint, fvk.seed_fingerprint)
10973

110-
def test_display_address_rejects_wrong_fingerprint(self):
111-
"""DisplayAddress with a wrong fingerprint is rejected before display."""
74+
def test_display_address_helper_rejects_wrong_fingerprint(self):
75+
"""Helper passes expected_seed_fingerprint through; wrong fp rejected."""
11276
self.setup_mnemonic_allallall()
11377

11478
fvk = self.client.zcash_get_orchard_fvk(
11579
address_n=[H + 32, H + 133, H + 0], account=0)
11680

117-
# Flip one byte to fabricate a non-matching fingerprint
11881
bad = bytearray(fvk.seed_fingerprint)
11982
bad[0] ^= 0xFF
12083

12184
with pytest.raises(CallException):
122-
self.client.call(
123-
zcash_proto.ZcashDisplayAddress(
124-
address_n=[H + 32, H + 133, H + 0],
125-
account=0,
126-
address="u1placeholder",
127-
ak=fvk.ak,
128-
nk=fvk.nk,
129-
rivk=fvk.rivk,
130-
expected_seed_fingerprint=bytes(bad),
131-
)
85+
self.client.zcash_display_address(
86+
address_n=[H + 32, H + 133, H + 0],
87+
address="u1placeholder",
88+
ak=fvk.ak,
89+
nk=fvk.nk,
90+
rivk=fvk.rivk,
91+
account=0,
92+
expected_seed_fingerprint=bytes(bad),
13293
)
13394

134-
def test_display_address_backward_compat_no_fingerprint(self):
135-
"""Omitting expected_seed_fingerprint still works (existing flow)."""
95+
def test_display_address_helper_backward_compat(self):
96+
"""Helper without expected_seed_fingerprint still works (existing flow)."""
13697
self.setup_mnemonic_allallall()
13798

13899
fvk = self.client.zcash_get_orchard_fvk(
139100
address_n=[H + 32, H + 133, H + 0], account=0)
140101

141-
resp = self.client.call(
142-
zcash_proto.ZcashDisplayAddress(
143-
address_n=[H + 32, H + 133, H + 0],
144-
account=0,
145-
address="u1placeholder",
146-
ak=fvk.ak,
147-
nk=fvk.nk,
148-
rivk=fvk.rivk,
149-
)
102+
resp = self.client.zcash_display_address(
103+
address_n=[H + 32, H + 133, H + 0],
104+
address="u1placeholder",
105+
ak=fvk.ak,
106+
nk=fvk.nk,
107+
rivk=fvk.rivk,
108+
account=0,
150109
)
151110
self.assertIsInstance(resp, zcash_proto.ZcashAddress)
152-
# Device still populates seed_fingerprint on responses regardless
111+
# Device populates seed_fingerprint on responses regardless of request
153112
self.assertTrue(resp.HasField("seed_fingerprint"))
154113
self.assertEqual(resp.seed_fingerprint, fvk.seed_fingerprint)
155114

156-
# ── ZcashSignPCZT: expected_seed_fingerprint binding ──────────────
115+
def test_device_fingerprint_matches_python_helper(self):
116+
"""Cross-check: device-derived fingerprint == calculate_seed_fingerprint(seed)
117+
for the all-allallall mnemonic seed. Ties firmware C and python-keepkey
118+
helper to the same byte-for-byte output."""
119+
self.setup_mnemonic_allallall()
120+
121+
fvk = self.client.zcash_get_orchard_fvk(
122+
address_n=[H + 32, H + 133, H + 0], account=0)
157123

158-
def test_sign_pczt_rejects_wrong_fingerprint(self):
159-
"""SignPCZT with wrong fingerprint is rejected before any signing."""
124+
# all-all-all mnemonic, empty passphrase, BIP-39 seed
125+
from mnemonic import Mnemonic
126+
seed = Mnemonic.to_seed("all all all all all all all all all all all all", "")
127+
expected_fp = calculate_seed_fingerprint(seed)
128+
self.assertEqual(fvk.seed_fingerprint, expected_fp)
129+
130+
# ── ZcashSignPCZT: through client.zcash_sign_pczt(...) ──────────────
131+
132+
def test_sign_pczt_helper_rejects_wrong_fingerprint(self):
133+
"""Helper passes expected_seed_fingerprint through; wrong fp rejected
134+
before any signing crypto runs."""
160135
self.setup_mnemonic_allallall()
161136

162-
# Fabricate a fingerprint that's clearly not this seed's.
163137
wrong_fp = b"\x01" * 32
164138

165-
# Minimal action — won't actually sign because we expect rejection
166-
# at the seed-fingerprint check before any key derivation.
167139
with pytest.raises(CallException):
168-
self.client.call(
169-
zcash_proto.ZcashSignPCZT(
170-
address_n=[H + 32, H + 133, H + 0],
171-
account=0,
172-
n_actions=1,
173-
total_amount=100000,
174-
fee=10000,
175-
branch_id=0x37519621,
176-
expected_seed_fingerprint=wrong_fp,
177-
)
140+
self.client.zcash_sign_pczt(
141+
address_n=[H + 32, H + 133, H + 0],
142+
actions=[{}], # placeholder — won't be reached past the fp check
143+
account=0,
144+
total_amount=100000,
145+
fee=10000,
146+
branch_id=0x37519621,
147+
expected_seed_fingerprint=wrong_fp,
178148
)
179149

180150

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
# Pure-Python tests for the ZIP-32 §6.1 seed fingerprint helper.
2+
#
3+
# This module deliberately does NOT import `common`, `keepkeylib.transport`,
4+
# or any protobuf bindings — those would require a device/emulator to be
5+
# wired up. Tests here run on any plain dev box:
6+
#
7+
# pytest tests/test_zcash_seed_fingerprint_helper.py
8+
9+
import unittest
10+
11+
from keepkeylib.zcash import calculate_seed_fingerprint
12+
13+
14+
class TestSeedFingerprintHelper(unittest.TestCase):
15+
16+
def test_reference_vector(self):
17+
"""Cross-check against keystone3-firmware
18+
rust/keystore/src/algorithms/zcash/mod.rs::test_keystore_derive_zcash_ufvk:
19+
20+
seed = 000102...1f (32 bytes)
21+
fp = deff604c246710f7176dead02aa746f2fd8d5389f7072556dcb555fdbe5e3ae3
22+
"""
23+
seed = bytes(range(32))
24+
fp = calculate_seed_fingerprint(seed)
25+
self.assertEqual(
26+
fp.hex(),
27+
"deff604c246710f7176dead02aa746f2fd8d5389f7072556dcb555fdbe5e3ae3",
28+
)
29+
30+
def test_rejects_trivial_seeds(self):
31+
with self.assertRaises(ValueError):
32+
calculate_seed_fingerprint(b"\x00" * 32)
33+
with self.assertRaises(ValueError):
34+
calculate_seed_fingerprint(b"\xff" * 32)
35+
36+
def test_rejects_out_of_range(self):
37+
with self.assertRaises(ValueError):
38+
calculate_seed_fingerprint(b"\x01" * 31) # too short
39+
with self.assertRaises(ValueError):
40+
calculate_seed_fingerprint(b"\x01" * 253) # too long
41+
42+
def test_length_prefix_domain_separation(self):
43+
"""Two seeds where one is a prefix of the other must produce
44+
distinct fingerprints (this is what the I2LEBSP_8(len) prefix buys us)."""
45+
seed_short = bytes(range(32))
46+
seed_long = bytes(range(33))
47+
self.assertNotEqual(
48+
calculate_seed_fingerprint(seed_short),
49+
calculate_seed_fingerprint(seed_long),
50+
)
51+
52+
53+
if __name__ == '__main__':
54+
unittest.main()

0 commit comments

Comments
 (0)