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