gabriel / muse public
test_secp256k1_sign.py python
648 lines 28.8 KB
Raw
1 """Tests for muse.core.secp256k1_sign — BIP32 secp256k1 derivation, EIP-191 signing, AVAX addresses.
2
3 All eight categories:
4 1. Unit — BIP32 path parsing, master key, child derivation, keccak256, eip55_checksum
5 2. Integration — derive_avax_key → sign → verify full pipeline
6 3. E2E — known-answer vectors (MetaMask-compatible address, cross-verified signatures)
7 4. Stress — 200 sign/verify cycles, 50-level deep derivation, high-index paths
8 5. Data integrity — determinism, account/index isolation, v ∈ {27, 28}, signature is 65 bytes
9 6. Performance — key derivation and sign/verify within time budgets
10 7. Security — bad sig rejected, wrong addr rejected, bad v rejected, truncated sig, malformed path
11 8. Docstrings — all public symbols have docstrings; module docstring present
12 """
13
14 from __future__ import annotations
15
16 import hashlib
17 import time
18 import types
19 from typing import TYPE_CHECKING
20
21 import pytest
22
23 if TYPE_CHECKING:
24 from eth_keys.keys import PrivateKey as _EthKey
25
26 # ---------------------------------------------------------------------------
27 # Constants — fixed test mnemonic (all-abandon, never used in production)
28 # ---------------------------------------------------------------------------
29
30 _MNEMONIC = (
31 "abandon abandon abandon abandon abandon abandon abandon abandon "
32 "abandon abandon abandon about"
33 )
34
35 #: Known-good AVAX C-Chain address for _MNEMONIC, account=0, index=0.
36 #: Cross-verified against MetaMask and standard BIP44 tooling.
37 _KNOWN_ADDRESS_LOWER = "0x9858effd232b4033e47d90003d41ec34ecaeda94"
38
39
40 # ---------------------------------------------------------------------------
41 # Fixtures
42 # ---------------------------------------------------------------------------
43
44
45 @pytest.fixture(scope="module")
46 def bip39_seed() -> bytes:
47 """64-byte BIP39 seed derived from the all-abandon test mnemonic."""
48 from muse.core.bip39 import mnemonic_to_seed
49 return mnemonic_to_seed(_MNEMONIC)
50
51
52 @pytest.fixture(scope="module")
53 def default_key(bip39_seed: bytes) -> "_EthKey":
54 """eth_keys.PrivateKey at m/44'/60'/0'/0/0 from the test mnemonic."""
55 from muse.core.secp256k1_sign import derive_avax_key
56 return derive_avax_key(bip39_seed, account=0, index=0) # type: ignore[return-value]
57
58
59 @pytest.fixture(scope="module")
60 def default_address(default_key: "_EthKey") -> str:
61 """EIP-55 address for the default test key."""
62 from muse.core.secp256k1_sign import avax_c_chain_address
63 return avax_c_chain_address(default_key.public_key)
64
65
66 # ---------------------------------------------------------------------------
67 # 1. Unit — low-level building blocks
68 # ---------------------------------------------------------------------------
69
70
71 class TestBip32PathParsing:
72 """Unit tests for ``_parse_path``."""
73
74 def test_simple_path_parsed(self) -> None:
75 from muse.core.secp256k1_sign import _parse_path
76 result = _parse_path("m/44'/60'/0'/0/0")
77 assert result == [44 | 0x80000000, 60 | 0x80000000, 0x80000000, 0, 0]
78
79 def test_master_only_returns_empty(self) -> None:
80 from muse.core.secp256k1_sign import _parse_path
81 assert _parse_path("m") == []
82 assert _parse_path("m/") == []
83
84 def test_unhardened_indices(self) -> None:
85 from muse.core.secp256k1_sign import _parse_path
86 assert _parse_path("m/0/1/2") == [0, 1, 2]
87
88 def test_hardened_indices(self) -> None:
89 from muse.core.secp256k1_sign import _parse_path
90 H = 0x80000000
91 assert _parse_path("m/44'/60'/1'") == [44 | H, 60 | H, 1 | H]
92
93 def test_path_must_start_with_m(self) -> None:
94 from muse.core.secp256k1_sign import Bip32Error, _parse_path
95 with pytest.raises(Bip32Error, match="must start with 'm'"):
96 _parse_path("44'/60'/0'/0/0")
97
98 def test_non_integer_component_raises(self) -> None:
99 from muse.core.secp256k1_sign import Bip32Error, _parse_path
100 with pytest.raises(Bip32Error, match="Invalid BIP32 path component"):
101 _parse_path("m/44'/abc/0")
102
103 def test_negative_index_raises(self) -> None:
104 from muse.core.secp256k1_sign import Bip32Error, _parse_path
105 with pytest.raises(Bip32Error, match="Negative"):
106 _parse_path("m/-1")
107
108 def test_high_index_values(self) -> None:
109 from muse.core.secp256k1_sign import _parse_path
110 result = _parse_path("m/2147483647'/2147483647")
111 assert result == [0xFFFFFFFF, 2147483647]
112
113
114 class TestBip32MasterKey:
115 """Unit tests for ``bip32_master_key``."""
116
117 def test_master_key_returns_node(self, bip39_seed: bytes) -> None:
118 from muse.core.secp256k1_sign import _Bip32Node, bip32_master_key
119 node = bip32_master_key(bip39_seed)
120 assert isinstance(node, _Bip32Node)
121
122 def test_master_private_int_in_range(self, bip39_seed: bytes) -> None:
123 from muse.core.secp256k1_sign import _SECP256K1_N, bip32_master_key
124 node = bip32_master_key(bip39_seed)
125 assert 0 < node.private_int < _SECP256K1_N
126
127 def test_chain_code_is_32_bytes(self, bip39_seed: bytes) -> None:
128 from muse.core.secp256k1_sign import bip32_master_key
129 node = bip32_master_key(bip39_seed)
130 assert len(node.chain_code) == 32
131
132 def test_deterministic(self, bip39_seed: bytes) -> None:
133 from muse.core.secp256k1_sign import bip32_master_key
134 a = bip32_master_key(bip39_seed)
135 b = bip32_master_key(bip39_seed)
136 assert a.private_int == b.private_int
137 assert a.chain_code == b.chain_code
138
139 def test_different_seeds_different_keys(self, bip39_seed: bytes) -> None:
140 from muse.core.secp256k1_sign import bip32_master_key
141 other_seed = bytes(64)
142 try:
143 other = bip32_master_key(other_seed)
144 node = bip32_master_key(bip39_seed)
145 assert node.private_int != other.private_int
146 except Exception:
147 pass # all-zero seed may be invalid — that's fine
148
149
150 class TestKeccak256:
151 """Unit tests for ``keccak256``."""
152
153 def test_empty_string_known_vector(self) -> None:
154 from muse.core.secp256k1_sign import keccak256
155 # keccak256("") is a known constant
156 result = keccak256(b"")
157 assert result.hex() == "c5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470"
158
159 def test_returns_32_bytes(self) -> None:
160 from muse.core.secp256k1_sign import keccak256
161 assert len(keccak256(b"anything")) == 32
162
163 def test_is_not_sha3_256(self) -> None:
164 """keccak256 and FIPS SHA3-256 produce different outputs for the same input."""
165 import hashlib
166 from muse.core.secp256k1_sign import keccak256
167 sha3 = hashlib.sha3_256(b"").digest()
168 keccak = keccak256(b"")
169 assert sha3 != keccak
170
171
172 class TestEip55Checksum:
173 """Unit tests for ``eip55_checksum``."""
174
175 def test_known_vector(self) -> None:
176 from muse.core.secp256k1_sign import eip55_checksum
177 result = eip55_checksum("de0b295669a9fd93d5f28d9ec85e40f4cb697bae")
178 assert result == "0xde0B295669a9FD93d5F28D9Ec85E40f4cb697BAe"
179
180 def test_adds_0x_prefix(self) -> None:
181 from muse.core.secp256k1_sign import eip55_checksum
182 result = eip55_checksum("de0b295669a9fd93d5f28d9ec85e40f4cb697bae")
183 assert result.startswith("0x")
184
185 def test_idempotent_on_lowercase_bytes(self) -> None:
186 """eip55_checksum on already-lowercased input is deterministic."""
187 from muse.core.secp256k1_sign import eip55_checksum
188 addr = "9858effd232b4033e47d90003d41ec34ecaeda94"
189 assert eip55_checksum(addr) == eip55_checksum(addr)
190
191
192 class TestEip191Message:
193 """Unit tests for ``eip191_message``."""
194
195 def test_prefix_present(self) -> None:
196 from muse.core.secp256k1_sign import eip191_message
197 result = eip191_message("Hello")
198 assert result.startswith(b"\x19Ethereum Signed Message:\n5Hello")
199
200 def test_length_in_prefix(self) -> None:
201 from muse.core.secp256k1_sign import eip191_message
202 msg = "Hi"
203 result = eip191_message(msg)
204 assert b"2" in result # len("Hi") = 2
205
206 def test_bytes_input(self) -> None:
207 from muse.core.secp256k1_sign import eip191_message
208 a = eip191_message("test")
209 b = eip191_message(b"test")
210 assert a == b
211
212 def test_empty_message(self) -> None:
213 from muse.core.secp256k1_sign import eip191_message
214 result = eip191_message("")
215 assert result == b"\x19Ethereum Signed Message:\n0"
216
217 def test_multibyte_utf8_length_is_byte_count(self) -> None:
218 """Length prefix counts bytes, not Unicode code points."""
219 from muse.core.secp256k1_sign import eip191_message
220 msg = "\u00e9" # é — 2 bytes in UTF-8
221 result = eip191_message(msg)
222 assert b"\n2" in result
223
224
225 # ---------------------------------------------------------------------------
226 # 2. Integration — full pipeline
227 # ---------------------------------------------------------------------------
228
229
230 class TestSignVerifyPipeline:
231 """Integration tests: derive_avax_key → eip191_sign → eip191_verify."""
232
233 def test_sign_returns_65_bytes(self, default_key: "_EthKey", default_address: str) -> None:
234 from muse.core.secp256k1_sign import eip191_sign
235 sig = eip191_sign(default_key, "test message")
236 assert len(sig) == 65
237
238 def test_v_is_27_or_28(self, default_key: "_EthKey") -> None:
239 from muse.core.secp256k1_sign import eip191_sign
240 sig = eip191_sign(default_key, "test")
241 assert sig[64] in (27, 28)
242
243 def test_verify_correct_roundtrip(self, default_key: "_EthKey", default_address: str) -> None:
244 from muse.core.secp256k1_sign import eip191_sign, eip191_verify
245 msg = "MPAY2\ngabriel\nalice\n0.001\nAVAX\nnonce\n1234"
246 sig = eip191_sign(default_key, msg)
247 assert eip191_verify(sig, msg, default_address)
248
249 def test_verify_bytes_message(self, default_key: "_EthKey", default_address: str) -> None:
250 from muse.core.secp256k1_sign import eip191_sign, eip191_verify
251 msg = b"raw bytes payload"
252 sig = eip191_sign(default_key, msg)
253 assert eip191_verify(sig, msg, default_address)
254
255 def test_verify_rejects_wrong_message(self, default_key: "_EthKey", default_address: str) -> None:
256 from muse.core.secp256k1_sign import eip191_sign, eip191_verify
257 sig = eip191_sign(default_key, "original")
258 assert not eip191_verify(sig, "tampered", default_address)
259
260 def test_verify_rejects_wrong_address(self, default_key: "_EthKey", default_address: str) -> None:
261 from muse.core.secp256k1_sign import eip191_sign, eip191_verify
262 sig = eip191_sign(default_key, "msg")
263 wrong_addr = f"0x{'a' * 40}"
264 assert not eip191_verify(sig, "msg", wrong_addr)
265
266 def test_different_keys_different_addresses(self, bip39_seed: bytes) -> None:
267 from muse.core.secp256k1_sign import avax_c_chain_address, derive_avax_key
268 key0 = derive_avax_key(bip39_seed, account=0)
269 key1 = derive_avax_key(bip39_seed, account=1)
270 assert avax_c_chain_address(key0.public_key) != avax_c_chain_address(key1.public_key)
271
272
273 # ---------------------------------------------------------------------------
274 # 3. E2E — known-answer vectors
275 # ---------------------------------------------------------------------------
276
277
278 class TestKnownAnswerVectors:
279 """End-to-end tests using externally-verified reference values."""
280
281 def test_address_matches_metamask_all_abandon(self, default_address: str) -> None:
282 """MetaMask and standard BIP44 tooling produce this address for the all-abandon mnemonic."""
283 assert default_address.lower() == _KNOWN_ADDRESS_LOWER
284
285 def test_keccak256_known_vectors(self) -> None:
286 """Cross-verify keccak256 against known Ethereum constants."""
287 from muse.core.secp256k1_sign import keccak256
288 # keccak256("") — used in Ethereum's trie
289 assert keccak256(b"").hex() == "c5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470"
290 # keccak256(b"\x00" * 32) — well-known test vector
291 result = keccak256(b"\x00" * 32)
292 assert len(result) == 32
293
294 def test_eip55_known_vectors(self) -> None:
295 """EIP-55 checksum is deterministic across multiple known addresses."""
296 from muse.core.secp256k1_sign import eip55_checksum
297 # All-caps address — every nibble in hash >= 8
298 assert eip55_checksum("52908400098527886e0f7030069857d2e4169ee7") == "0x52908400098527886E0F7030069857D2E4169EE7"
299 # Mixed checksum address
300 assert eip55_checksum("de0b295669a9fd93d5f28d9ec85e40f4cb697bae") == "0xde0B295669a9FD93d5F28D9Ec85E40f4cb697BAe"
301
302 def test_signature_verify_across_accounts(self, bip39_seed: bytes) -> None:
303 """Signatures from account N only verify against account N's address."""
304 from muse.core.secp256k1_sign import avax_c_chain_address, derive_avax_key, eip191_sign, eip191_verify
305 for acct in range(3):
306 key = derive_avax_key(bip39_seed, account=acct)
307 addr = avax_c_chain_address(key.public_key)
308 sig = eip191_sign(key, "cross-account test")
309 assert eip191_verify(sig, "cross-account test", addr)
310 # Signature from account N must not verify against account N+1
311 if acct > 0:
312 other = derive_avax_key(bip39_seed, account=acct - 1)
313 other_addr = avax_c_chain_address(other.public_key)
314 assert not eip191_verify(sig, "cross-account test", other_addr)
315
316 def test_bip44_path_depth_correct(self, bip39_seed: bytes) -> None:
317 """Derivation uses exactly 5 levels: m/44'/60'/account'/0/index."""
318 from muse.core.secp256k1_sign import _HARDENED, _parse_path
319 path = f"m/44'/{60}'/{0}'/0/{0}"
320 parsed = _parse_path(path)
321 assert len(parsed) == 5
322 assert parsed[0] == 44 | _HARDENED # purpose
323 assert parsed[1] == 60 | _HARDENED # coin type
324 assert parsed[2] == 0 | _HARDENED # account (hardened)
325 assert parsed[3] == 0 # change (unhardened)
326 assert parsed[4] == 0 # index (unhardened)
327
328
329 # ---------------------------------------------------------------------------
330 # 4. Stress — volume and depth
331 # ---------------------------------------------------------------------------
332
333
334 @pytest.mark.slow
335 class TestStress:
336 """Stress tests: high volume sign/verify cycles, deep derivation paths."""
337
338 def test_200_sign_verify_cycles(self, default_key: "_EthKey", default_address: str) -> None:
339 from muse.core.secp256k1_sign import eip191_sign, eip191_verify
340 for i in range(200):
341 msg = f"stress message {i}"
342 sig = eip191_sign(default_key, msg)
343 assert eip191_verify(sig, msg, default_address), f"failed at iteration {i}"
344
345 def test_50_accounts_all_unique_addresses(self, bip39_seed: bytes) -> None:
346 from muse.core.secp256k1_sign import avax_c_chain_address, derive_avax_key
347 addresses = {
348 avax_c_chain_address(derive_avax_key(bip39_seed, account=i).public_key)
349 for i in range(50)
350 }
351 assert len(addresses) == 50
352
353 def test_50_indices_all_unique_addresses(self, bip39_seed: bytes) -> None:
354 from muse.core.secp256k1_sign import avax_c_chain_address, derive_avax_key
355 addresses = {
356 avax_c_chain_address(derive_avax_key(bip39_seed, account=0, index=i).public_key)
357 for i in range(50)
358 }
359 assert len(addresses) == 50
360
361 def test_high_index_derivation(self, bip39_seed: bytes) -> None:
362 """Derivation at high index values does not crash."""
363 from muse.core.secp256k1_sign import avax_c_chain_address, derive_avax_key
364 key = derive_avax_key(bip39_seed, account=0, index=100_000)
365 addr = avax_c_chain_address(key.public_key)
366 assert addr.startswith("0x") and len(addr) == 42
367
368 def test_many_unique_messages_all_verify(self, default_key: "_EthKey", default_address: str) -> None:
369 """100 distinct messages each produce a verifiable, distinct signature."""
370 from muse.core.secp256k1_sign import eip191_sign, eip191_verify
371 signatures: set[bytes] = set()
372 for i in range(100):
373 msg = f"unique payload {i}"
374 sig = eip191_sign(default_key, msg)
375 assert eip191_verify(sig, msg, default_address)
376 signatures.add(sig)
377 # ECDSA is deterministic (RFC 6979) — same key + same msg = same sig.
378 # 100 unique messages should produce 100 unique signatures.
379 assert len(signatures) == 100
380
381
382 # ---------------------------------------------------------------------------
383 # 5. Data integrity — determinism and isolation
384 # ---------------------------------------------------------------------------
385
386
387 class TestDataIntegrity:
388 """Data integrity: determinism, isolation, canonical encoding."""
389
390 def test_signing_is_deterministic(self, default_key: "_EthKey", default_address: str) -> None:
391 """RFC 6979 deterministic ECDSA: same key + same message = same signature."""
392 from muse.core.secp256k1_sign import eip191_sign
393 msg = "deterministic test"
394 sig1 = eip191_sign(default_key, msg)
395 sig2 = eip191_sign(default_key, msg)
396 assert sig1 == sig2
397
398 def test_address_derivation_deterministic(self, bip39_seed: bytes) -> None:
399 from muse.core.secp256k1_sign import avax_c_chain_address, derive_avax_key
400 addr1 = avax_c_chain_address(derive_avax_key(bip39_seed).public_key)
401 addr2 = avax_c_chain_address(derive_avax_key(bip39_seed).public_key)
402 assert addr1 == addr2
403
404 def test_account_isolation(self, bip39_seed: bytes) -> None:
405 """Keys at different accounts are independent — signature from acct 0 fails on acct 1."""
406 from muse.core.secp256k1_sign import avax_c_chain_address, derive_avax_key, eip191_sign, eip191_verify
407 key0 = derive_avax_key(bip39_seed, account=0)
408 key1 = derive_avax_key(bip39_seed, account=1)
409 addr1 = avax_c_chain_address(key1.public_key)
410 sig = eip191_sign(key0, "isolation test")
411 assert not eip191_verify(sig, "isolation test", addr1)
412
413 def test_index_isolation(self, bip39_seed: bytes) -> None:
414 """Keys at different indices within the same account are independent."""
415 from muse.core.secp256k1_sign import avax_c_chain_address, derive_avax_key, eip191_sign, eip191_verify
416 key0 = derive_avax_key(bip39_seed, account=0, index=0)
417 key1 = derive_avax_key(bip39_seed, account=0, index=1)
418 addr1 = avax_c_chain_address(key1.public_key)
419 sig = eip191_sign(key0, "index isolation")
420 assert not eip191_verify(sig, "index isolation", addr1)
421
422 def test_address_is_42_chars_0x_prefix(self, default_address: str) -> None:
423 assert default_address.startswith("0x")
424 assert len(default_address) == 42
425
426 def test_address_is_hex(self, default_address: str) -> None:
427 int(default_address[2:], 16) # raises ValueError if not hex
428
429 def test_signature_r_s_v_encoding(self, default_key: "_EthKey") -> None:
430 """Signature byte layout: r(32) || s(32) || v(1)."""
431 from muse.core.secp256k1_sign import eip191_sign
432 sig = eip191_sign(default_key, "layout test")
433 assert len(sig) == 65
434 r = int.from_bytes(sig[:32], "big")
435 s = int.from_bytes(sig[32:64], "big")
436 v = sig[64]
437 assert r > 0
438 assert s > 0
439 assert v in (27, 28)
440
441 def test_eip191_prefix_cannot_be_raw_tx(self) -> None:
442 """The 0x19 prefix ensures the message is not a valid RLP transaction prefix."""
443 from muse.core.secp256k1_sign import eip191_message
444 result = eip191_message("anything")
445 assert result[0] == 0x19
446
447 def test_bip32_master_chain_code_is_entropy(self, bip39_seed: bytes) -> None:
448 """Chain code differs from private key bytes — not a trivial copy."""
449 from muse.core.secp256k1_sign import bip32_master_key
450 node = bip32_master_key(bip39_seed)
451 assert node.chain_code != node.private_int.to_bytes(32, "big")
452
453
454 # ---------------------------------------------------------------------------
455 # 6. Performance — time budgets
456 # ---------------------------------------------------------------------------
457
458
459 @pytest.mark.perf
460 class TestPerformance:
461 """Performance: derivation and signing within acceptable time budgets."""
462
463 def test_key_derivation_under_200ms(self, bip39_seed: bytes) -> None:
464 """BIP44 key derivation (5 levels) completes in < 200 ms."""
465 from muse.core.secp256k1_sign import derive_avax_key
466 start = time.perf_counter()
467 derive_avax_key(bip39_seed, account=0, index=0)
468 elapsed = time.perf_counter() - start
469 assert elapsed < 0.2, f"key derivation took {elapsed:.3f}s (budget: 200ms)"
470
471 def test_sign_under_100ms(self, default_key: "_EthKey") -> None:
472 """EIP-191 signing completes in < 100 ms."""
473 from muse.core.secp256k1_sign import eip191_sign
474 start = time.perf_counter()
475 eip191_sign(default_key, "perf test message")
476 elapsed = time.perf_counter() - start
477 assert elapsed < 0.1, f"signing took {elapsed:.3f}s (budget: 100ms)"
478
479 def test_verify_under_100ms(self, default_key: "_EthKey", default_address: str) -> None:
480 """EIP-191 verification (with recovery) completes in < 100 ms."""
481 from muse.core.secp256k1_sign import eip191_sign, eip191_verify
482 sig = eip191_sign(default_key, "perf verify")
483 start = time.perf_counter()
484 eip191_verify(sig, "perf verify", default_address)
485 elapsed = time.perf_counter() - start
486 assert elapsed < 0.1, f"verify took {elapsed:.3f}s (budget: 100ms)"
487
488 def test_10_sign_verify_cycles_under_1s(self, default_key: "_EthKey", default_address: str) -> None:
489 """10 complete sign+verify cycles under 1 second."""
490 from muse.core.secp256k1_sign import eip191_sign, eip191_verify
491 start = time.perf_counter()
492 for i in range(10):
493 sig = eip191_sign(default_key, f"throughput {i}")
494 eip191_verify(sig, f"throughput {i}", default_address)
495 elapsed = time.perf_counter() - start
496 assert elapsed < 1.0, f"10 cycles took {elapsed:.3f}s (budget: 1s)"
497
498 def test_keccak256_throughput(self) -> None:
499 """keccak256 handles 10 000 calls in < 1 second."""
500 from muse.core.secp256k1_sign import keccak256
501 payload = b"benchmark" * 10
502 start = time.perf_counter()
503 for _ in range(10_000):
504 keccak256(payload)
505 elapsed = time.perf_counter() - start
506 assert elapsed < 1.0, f"10k keccak256 took {elapsed:.3f}s"
507
508
509 # ---------------------------------------------------------------------------
510 # 7. Security — rejection of bad inputs
511 # ---------------------------------------------------------------------------
512
513
514 class TestSecurity:
515 """Security: all invalid inputs are rejected cleanly; no information leakage."""
516
517 def test_verify_rejects_truncated_signature(self, default_address: str) -> None:
518 from muse.core.secp256k1_sign import eip191_verify
519 assert not eip191_verify(b"\x00" * 64, "msg", default_address)
520
521 def test_verify_rejects_empty_signature(self, default_address: str) -> None:
522 from muse.core.secp256k1_sign import eip191_verify
523 assert not eip191_verify(b"", "msg", default_address)
524
525 def test_verify_rejects_all_zero_signature(self, default_address: str) -> None:
526 from muse.core.secp256k1_sign import eip191_verify
527 assert not eip191_verify(b"\x00" * 65, "msg", default_address)
528
529 def test_verify_rejects_bad_v_byte(self, default_key: "_EthKey", default_address: str) -> None:
530 """v must be 27 or 28; other values are rejected immediately."""
531 from muse.core.secp256k1_sign import eip191_sign, eip191_verify
532 sig = eip191_sign(default_key, "bad v test")
533 for bad_v in (0, 1, 26, 29, 255):
534 bad_sig = sig[:64] + bytes([bad_v])
535 assert not eip191_verify(bad_sig, "bad v test", default_address), f"v={bad_v} should be rejected"
536
537 def test_verify_rejects_bit_flipped_signature(self, default_key: "_EthKey", default_address: str) -> None:
538 """A single bit flip in r or s invalidates the signature."""
539 from muse.core.secp256k1_sign import eip191_sign, eip191_verify
540 sig = bytearray(eip191_sign(default_key, "bit flip test"))
541 sig[0] ^= 0x01 # flip one bit in r
542 assert not eip191_verify(bytes(sig), "bit flip test", default_address)
543
544 def test_verify_rejects_bit_flipped_s(self, default_key: "_EthKey", default_address: str) -> None:
545 from muse.core.secp256k1_sign import eip191_sign, eip191_verify
546 sig = bytearray(eip191_sign(default_key, "flip s"))
547 sig[32] ^= 0x01 # flip one bit in s
548 assert not eip191_verify(bytes(sig), "flip s", default_address)
549
550 def test_malformed_bip32_path_raises(self) -> None:
551 from muse.core.secp256k1_sign import Bip32Error, bip32_derive_path
552 seed = bytes(64)
553 with pytest.raises(Bip32Error):
554 bip32_derive_path(seed, "not/a/path")
555
556 def test_address_case_insensitive_comparison(self, default_key: "_EthKey", default_address: str) -> None:
557 """eip191_verify accepts addresses in any case."""
558 from muse.core.secp256k1_sign import eip191_sign, eip191_verify
559 sig = eip191_sign(default_key, "case test")
560 assert eip191_verify(sig, "case test", default_address.upper())
561 assert eip191_verify(sig, "case test", default_address.lower())
562
563 def test_eip191_message_not_raw_private_key(self, default_key: "_EthKey") -> None:
564 """The private key scalar must not appear in the signed message."""
565 from muse.core.secp256k1_sign import eip191_message
566 private_bytes = default_key.to_bytes()
567 msg = eip191_message("safe message")
568 assert private_bytes not in msg
569
570 def test_different_seeds_produce_different_master_keys(self) -> None:
571 from muse.core.secp256k1_sign import bip32_master_key
572 seed1 = bytes(range(64))
573 seed2 = bytes(range(1, 65))
574 node1 = bip32_master_key(seed1)
575 node2 = bip32_master_key(seed2)
576 assert node1.private_int != node2.private_int
577 assert node1.chain_code != node2.chain_code
578
579 def test_no_custom_ec_math_in_module(self) -> None:
580 """Guard: no hand-written point_mul or point_add in the module."""
581 import inspect
582 import muse.core.secp256k1_sign as mod
583 src = inspect.getsource(mod)
584 assert "_point_mul" not in src, "hand-written EC point multiplication found"
585 assert "_point_add" not in src, "hand-written EC point addition found"
586 assert "_recover_public_key" not in src, "hand-written key recovery found"
587
588 def test_signing_uses_eth_keys_not_custom_ecdsa(self) -> None:
589 """Guard: eip191_sign delegates to eth_keys, not a custom ECDSA implementation."""
590 import inspect
591 from muse.core.secp256k1_sign import eip191_sign
592 src = inspect.getsource(eip191_sign)
593 assert "sign_msg_hash" in src, "eth_keys sign_msg_hash not called"
594
595
596 # ---------------------------------------------------------------------------
597 # 8. Docstrings — all public symbols documented
598 # ---------------------------------------------------------------------------
599
600
601 class TestDocstrings:
602 """Ensure all public symbols carry docstrings."""
603
604 def _public_functions(self) -> list[tuple[str, types.FunctionType]]:
605 import inspect
606 import muse.core.secp256k1_sign as mod
607 return [
608 (name, obj)
609 for name, obj in inspect.getmembers(mod, inspect.isfunction)
610 if not name.startswith("_")
611 ]
612
613 def test_module_docstring(self) -> None:
614 import muse.core.secp256k1_sign as mod
615 assert mod.__doc__ and len(mod.__doc__.strip()) > 20
616
617 def test_all_public_functions_have_docstrings(self) -> None:
618 for name, fn in self._public_functions():
619 assert fn.__doc__ and len(fn.__doc__.strip()) > 10, (
620 f"muse.core.secp256k1_sign.{name} missing docstring"
621 )
622
623 def test_bip32_error_has_docstring(self) -> None:
624 from muse.core.secp256k1_sign import Bip32Error
625 assert Bip32Error.__doc__
626
627 def test_docstring_mentions_eth_keys(self) -> None:
628 import muse.core.secp256k1_sign as mod
629 assert "eth_keys" in (mod.__doc__ or "")
630
631 def test_docstring_mentions_keccak(self) -> None:
632 import muse.core.secp256k1_sign as mod
633 assert "keccak" in (mod.__doc__ or "").lower()
634
635 def test_derive_avax_key_documents_path(self) -> None:
636 from muse.core.secp256k1_sign import derive_avax_key
637 assert "44'" in (derive_avax_key.__doc__ or "")
638 assert "60'" in (derive_avax_key.__doc__ or "")
639
640 def test_eip191_sign_documents_v_range(self) -> None:
641 from muse.core.secp256k1_sign import eip191_sign
642 doc = eip191_sign.__doc__ or ""
643 assert "27" in doc and "28" in doc
644
645 def test_eip191_verify_documents_return_type(self) -> None:
646 from muse.core.secp256k1_sign import eip191_verify
647 doc = eip191_verify.__doc__ or ""
648 assert "True" in doc or "bool" in doc.lower()
File History 1 commit