gabriel / muse public
test_core_slip010.py python
701 lines 25.8 KB
Raw
sha256:88ac91129873e6a496e9189515aa690eb893ae25d69c8f72af141a2be5068eb3 docs: docstring sprint contract→find-symbol — idiomatic run… Sonnet 4.6 patch 141 days ago
1 """Tests for muse.core.slip010 — SLIP-0010 Ed25519 hierarchical deterministic key derivation.
2
3 Test categories
4 ---------------
5 - Unit: individual function contracts, argument validation, return types
6 - Data integrity: official SLIP-0010 test vectors (Ed25519)
7 - Integration: full path derivation pipelines
8 - Stress: repeated derivation, deep paths, large indices
9 - Security: hardened-only enforcement, key material redaction, independence
10
11 Official test vectors
12 ---------------------
13 SLIP-0010 specifies test vectors for Ed25519 at:
14 https://github.com/satoshilabs/slips/blob/master/slip-0010.md#test-vector-2-for-ed25519
15
16 Two test seeds are defined (Seed1, Seed2). Each has multiple path steps with
17 known private key bytes and chain code bytes. We verify against both.
18 """
19
20 from __future__ import annotations
21
22 import hmac
23 import hashlib
24 from dataclasses import FrozenInstanceError
25 from typing import NamedTuple
26
27 import pytest
28
29 from muse.core.slip010 import (
30 HARDENED_OFFSET,
31 MUSE_PURPOSE,
32 DerivedKey,
33 Slip010Error,
34 child_key,
35 derive_path,
36 hardened,
37 master_key,
38 parse_path,
39 to_ed25519_private_key,
40 )
41
42
43 # ---------------------------------------------------------------------------
44 # Official SLIP-0010 test vectors — Ed25519
45 # ---------------------------------------------------------------------------
46 # Source: https://github.com/satoshilabs/slips/blob/master/slip-0010.md
47 #
48 # Format: (seed_hex, path, expected_chain_hex, expected_private_hex)
49 #
50 # All test paths use only hardened indices (SLIP-0010 Ed25519 restriction).
51
52 class _SlipVector(NamedTuple):
53 seed_hex: str
54 path: str
55 expected_chain_hex: str
56 expected_private_hex: str
57
58
59 # Test vector 1 (Seed1 = 000102030405060708090a0b0c0d0e0f)
60 _SEED1 = "000102030405060708090a0b0c0d0e0f"
61 _SEED2 = (
62 "fffcf9f6f3f0edeae7e4e1dedbd8d5d2cfccc9c6c3c0bdbab7b4b1aeaba8a5a29f9c"
63 "999693908d8a8784817e7b7875726f6c696663605d5a5754514e4b484542"
64 )
65
66 _SLIP010_VECTORS: list[_SlipVector] = [
67 # ── Test Vector 1 (Seed1 = 000102030405060708090a0b0c0d0e0f) ──────────
68 # Source: https://github.com/satoshilabs/slips/blob/master/slip-0010.md
69 _SlipVector(
70 seed_hex=_SEED1,
71 path="m/0'",
72 expected_chain_hex="8b59aa11380b624e81507a27fedda59fea6d0b779a778918a2fd3590e16e9c69",
73 expected_private_hex="68e0fe46dfb67e368c75379acec591dad19df3cde26e63b93a8e704f1dade7a3",
74 ),
75 _SlipVector(
76 seed_hex=_SEED1,
77 path="m/0'/1'",
78 expected_chain_hex="a320425f77d1b5c2505a6b1b27382b37368ee640e3557c315416801243552f14",
79 expected_private_hex="b1d0bad404bf35da785a64ca1ac54b2617211d2777696fbffaf208f746ae84f2",
80 ),
81 _SlipVector(
82 seed_hex=_SEED1,
83 path="m/0'/1'/2'",
84 expected_chain_hex="2e69929e00b5ab250f49c3fb1c12f252de4fed2c1db88387094a0f8c4c9ccd6c",
85 expected_private_hex="92a5b23c0b8a99e37d07df3fb9966917f5d06e02ddbd909c7e184371463e9fc9",
86 ),
87 _SlipVector(
88 seed_hex=_SEED1,
89 path="m/0'/1'/2'/2'",
90 expected_chain_hex="8f6d87f93d750e0efccda017d662a1b31a266e4a6f5993b15f5c1f07f74dd5cc",
91 expected_private_hex="30d1dc7e5fc04c31219ab25a27ae00b50f6fd66622f6e9c913253d6511d1e662",
92 ),
93 _SlipVector(
94 seed_hex=_SEED1,
95 path="m/0'/1'/2'/2'/1000000000'",
96 expected_chain_hex="68789923a0cac2cd5a29172a475fe9e0fb14cd6adb5ad98a3fa70333e7afa230",
97 expected_private_hex="8f94d394a8e8fd6b1bc2f3f49f5c47e385281d5c17e65324b0f62483e37e8793",
98 ),
99 # ── Test Vector 2 (Seed2 = fffcf9f6…4542) ────────────────────────────
100 _SlipVector(
101 seed_hex=_SEED2,
102 path="m/0'",
103 expected_chain_hex="0b78a3226f915c082bf118f83618a618ab6dec793752624cbeb622acb562862d",
104 expected_private_hex="1559eb2bbec5790b0c65d8693e4d0875b1747f4970ae8b650486ed7470845635",
105 ),
106 _SlipVector(
107 seed_hex=_SEED2,
108 path="m/0'/2147483647'",
109 expected_chain_hex="138f0b2551bcafeca6ff2aa88ba8ed0ed8de070841f0c4ef0165df8181eaad7f",
110 expected_private_hex="ea4f5bfe8694d8bb74b7b59404632fd5968b774ed545e810de9c32a4fb4192f4",
111 ),
112 _SlipVector(
113 seed_hex=_SEED2,
114 path="m/0'/2147483647'/1'",
115 expected_chain_hex="73bd9fff1cfbde33a1b846c27085f711c0fe2d66fd32e139d3ebc28e5a4a6b90",
116 expected_private_hex="3757c7577170179c7868353ada796c839135b3d30554bbb74a4b1e4a5a58505c",
117 ),
118 _SlipVector(
119 seed_hex=_SEED2,
120 path="m/0'/2147483647'/1'/2147483646'",
121 expected_chain_hex="0902fe8a29f9140480a00ef244bd183e8a13288e4412d8389d140aac1794825a",
122 expected_private_hex="5837736c89570de861ebc173b1086da4f505d4adb387c6a1b1342d5e4ac9ec72",
123 ),
124 _SlipVector(
125 seed_hex=_SEED2,
126 path="m/0'/2147483647'/1'/2147483646'/2'",
127 expected_chain_hex="5d70af781f3a37b829f0d060924d5e960bdc02e85423494afc0b1a41bbe196d4",
128 expected_private_hex="551d333177df541ad876a60ea71f00447931c0a9da16f227c11ea080d7391b8d",
129 ),
130 ]
131
132
133 # ---------------------------------------------------------------------------
134 # Helpers
135 # ---------------------------------------------------------------------------
136
137
138 def _seed_from_hex(hex_str: str) -> bytes:
139 return bytes.fromhex(hex_str.replace(" ", ""))
140
141
142 def _derive_master_private_key(seed_hex: str) -> tuple[str, str]:
143 """Return (chain_hex, private_hex) for the master key from a hex seed."""
144 seed = _seed_from_hex(seed_hex)
145 I = hmac.new(b"ed25519 seed", seed, hashlib.sha512).digest()
146 return I[32:].hex(), I[:32].hex()
147
148
149 # ---------------------------------------------------------------------------
150 # Unit — DerivedKey
151 # ---------------------------------------------------------------------------
152
153
154 class TestDerivedKey:
155 def test_construction(self) -> None:
156 dk = DerivedKey(private_bytes=b"\x01" * 32, chain_code=b"\x02" * 32)
157 assert dk.private_bytes == b"\x01" * 32
158 assert dk.chain_code == b"\x02" * 32
159
160 def test_frozen(self) -> None:
161 dk = DerivedKey(private_bytes=b"\x01" * 32, chain_code=b"\x02" * 32)
162 with pytest.raises((FrozenInstanceError, AttributeError)):
163 dk.private_bytes = b"\x03" * 32 # type: ignore[misc]
164
165 def test_repr_redacts_key_material(self) -> None:
166 dk = DerivedKey(private_bytes=b"\xde\xad" * 16, chain_code=b"\xbe\xef" * 16)
167 r = repr(dk)
168 assert "deaddeaddeaddeaddeaddeaddeaddeaddeaddeaddeaddeaddeaddeaddeaddead" not in r
169 assert "redacted" in r
170
171 def test_repr_does_not_leak_private_bytes(self) -> None:
172 secret = b"\xaa" * 32
173 dk = DerivedKey(private_bytes=secret, chain_code=b"\x00" * 32)
174 assert secret.hex() not in repr(dk)
175 assert "aa" * 32 not in repr(dk)
176
177 def test_equality(self) -> None:
178 dk1 = DerivedKey(private_bytes=b"\x01" * 32, chain_code=b"\x02" * 32)
179 dk2 = DerivedKey(private_bytes=b"\x01" * 32, chain_code=b"\x02" * 32)
180 assert dk1 == dk2
181
182 def test_inequality(self) -> None:
183 dk1 = DerivedKey(private_bytes=b"\x01" * 32, chain_code=b"\x02" * 32)
184 dk2 = DerivedKey(private_bytes=b"\x03" * 32, chain_code=b"\x02" * 32)
185 assert dk1 != dk2
186
187
188 # ---------------------------------------------------------------------------
189 # Unit — hardened()
190 # ---------------------------------------------------------------------------
191
192
193 class TestHardened:
194 def test_zero(self) -> None:
195 assert hardened(0) == HARDENED_OFFSET
196
197 def test_703(self) -> None:
198 assert hardened(703) == 703 + HARDENED_OFFSET
199
200 def test_max_valid(self) -> None:
201 assert hardened(HARDENED_OFFSET - 1) == (HARDENED_OFFSET - 1) + HARDENED_OFFSET
202
203 def test_negative_raises(self) -> None:
204 with pytest.raises(Slip010Error, match="out of range"):
205 hardened(-1)
206
207 def test_already_hardened_raises(self) -> None:
208 with pytest.raises(Slip010Error, match="out of range"):
209 hardened(HARDENED_OFFSET)
210
211 def test_returns_int(self) -> None:
212 assert isinstance(hardened(0), int)
213
214
215 # ---------------------------------------------------------------------------
216 # Unit — parse_path()
217 # ---------------------------------------------------------------------------
218
219
220 class TestParsePath:
221 def test_single_component(self) -> None:
222 result = parse_path("m/0'")
223 assert result == [HARDENED_OFFSET]
224
225 def test_four_component_muse_path(self) -> None:
226 result = parse_path("m/703'/0'/0'/0'")
227 assert result == [
228 703 + HARDENED_OFFSET,
229 HARDENED_OFFSET,
230 HARDENED_OFFSET,
231 HARDENED_OFFSET,
232 ]
233
234 def test_large_index(self) -> None:
235 result = parse_path("m/1000000000'")
236 assert result == [1_000_000_000 + HARDENED_OFFSET]
237
238 def test_strips_whitespace(self) -> None:
239 result = parse_path(" m/703'/0'/0'/0' ")
240 assert result == [703 + HARDENED_OFFSET, HARDENED_OFFSET, HARDENED_OFFSET, HARDENED_OFFSET]
241
242 def test_unhardened_component_raises(self) -> None:
243 with pytest.raises(Slip010Error, match="Invalid SLIP-0010 path"):
244 parse_path("m/0/1")
245
246 def test_missing_m_prefix_raises(self) -> None:
247 with pytest.raises(Slip010Error, match="Invalid SLIP-0010 path"):
248 parse_path("0'/1'")
249
250 def test_empty_string_raises(self) -> None:
251 with pytest.raises(Slip010Error, match="Invalid SLIP-0010 path"):
252 parse_path("")
253
254 def test_just_m_raises(self) -> None:
255 with pytest.raises(Slip010Error, match="Invalid SLIP-0010 path"):
256 parse_path("m")
257
258 def test_returns_list_of_ints(self) -> None:
259 result = parse_path("m/703'/0'/0'/0'")
260 assert all(isinstance(i, int) for i in result)
261
262
263 # ---------------------------------------------------------------------------
264 # Unit — master_key()
265 # ---------------------------------------------------------------------------
266
267
268 class TestMasterKey:
269 def test_returns_derived_key(self) -> None:
270 seed = bytes(64) # all zeros — not BIP39 but valid for derivation
271 dk = master_key(seed)
272 assert isinstance(dk, DerivedKey)
273
274 def test_private_bytes_length(self) -> None:
275 dk = master_key(bytes(64))
276 assert len(dk.private_bytes) == 32
277
278 def test_chain_code_length(self) -> None:
279 dk = master_key(bytes(64))
280 assert len(dk.chain_code) == 32
281
282 def test_deterministic(self) -> None:
283 seed = bytes(range(64))
284 assert master_key(seed) == master_key(seed)
285
286 def test_short_seed_raises(self) -> None:
287 with pytest.raises(Slip010Error, match="at least 16 bytes"):
288 master_key(b"\x00" * 15)
289
290 def test_15_byte_seed_raises(self) -> None:
291 with pytest.raises(Slip010Error):
292 master_key(b"\xff" * 15)
293
294 def test_16_byte_seed_accepted(self) -> None:
295 dk = master_key(b"\x00" * 16)
296 assert isinstance(dk, DerivedKey)
297
298 def test_empty_seed_raises(self) -> None:
299 with pytest.raises(Slip010Error):
300 master_key(b"")
301
302 def test_different_seeds_produce_different_keys(self) -> None:
303 dk1 = master_key(bytes(64))
304 dk2 = master_key(bytes([1] * 64))
305 assert dk1 != dk2
306
307
308 # ---------------------------------------------------------------------------
309 # Unit — child_key()
310 # ---------------------------------------------------------------------------
311
312
313 class TestChildKey:
314 @pytest.fixture
315 def parent(self) -> DerivedKey:
316 return master_key(bytes(64))
317
318 def test_hardened_index_succeeds(self, parent: DerivedKey) -> None:
319 child = child_key(parent, HARDENED_OFFSET)
320 assert isinstance(child, DerivedKey)
321
322 def test_unhardened_index_raises(self, parent: DerivedKey) -> None:
323 with pytest.raises(Slip010Error, match="hardened"):
324 child_key(parent, 0)
325
326 def test_unhardened_index_raises_for_all_under_offset(self, parent: DerivedKey) -> None:
327 with pytest.raises(Slip010Error):
328 child_key(parent, HARDENED_OFFSET - 1)
329
330 def test_returns_different_key_than_parent(self, parent: DerivedKey) -> None:
331 child = child_key(parent, hardened(0))
332 assert child != parent
333
334 def test_different_indices_produce_different_children(self, parent: DerivedKey) -> None:
335 c0 = child_key(parent, hardened(0))
336 c1 = child_key(parent, hardened(1))
337 assert c0 != c1
338
339 def test_child_private_bytes_length(self, parent: DerivedKey) -> None:
340 c = child_key(parent, hardened(0))
341 assert len(c.private_bytes) == 32
342
343 def test_child_chain_code_length(self, parent: DerivedKey) -> None:
344 c = child_key(parent, hardened(0))
345 assert len(c.chain_code) == 32
346
347 def test_deterministic(self, parent: DerivedKey) -> None:
348 c1 = child_key(parent, hardened(703))
349 c2 = child_key(parent, hardened(703))
350 assert c1 == c2
351
352
353 # ---------------------------------------------------------------------------
354 # Unit — derive_path()
355 # ---------------------------------------------------------------------------
356
357
358 class TestDerivePath:
359 def test_single_component_path(self) -> None:
360 seed = bytes(64)
361 dk = derive_path(seed, "m/0'")
362 expected = child_key(master_key(seed), hardened(0))
363 assert dk == expected
364
365 def test_four_component_path(self) -> None:
366 seed = bytes(64)
367 # Manual derivation
368 dk = master_key(seed)
369 dk = child_key(dk, hardened(703))
370 dk = child_key(dk, hardened(0))
371 dk = child_key(dk, hardened(0))
372 dk = child_key(dk, hardened(0))
373 assert derive_path(seed, "m/703'/0'/0'/0'") == dk
374
375 def test_unhardened_path_raises(self) -> None:
376 with pytest.raises(Slip010Error):
377 derive_path(bytes(64), "m/0/1")
378
379 def test_invalid_path_raises(self) -> None:
380 with pytest.raises(Slip010Error):
381 derive_path(bytes(64), "not-a-path")
382
383 def test_short_seed_raises(self) -> None:
384 with pytest.raises(Slip010Error):
385 derive_path(b"\x00" * 10, "m/703'/0'/0'/0'")
386
387
388 # ---------------------------------------------------------------------------
389 # Data integrity — official SLIP-0010 test vectors
390 # ---------------------------------------------------------------------------
391
392
393 class TestSlip010OfficialVectors:
394 """Verify against the official SLIP-0010 Ed25519 test vectors.
395
396 Each vector specifies a seed, a derivation path, expected chain code,
397 and expected private key bytes. A mismatch here indicates a bug in the
398 HMAC-SHA512 derivation or the index encoding.
399 """
400
401 @pytest.mark.parametrize("v", _SLIP010_VECTORS, ids=[v.path for v in _SLIP010_VECTORS])
402 def test_vector_matches(self, v: _SlipVector) -> None:
403 seed = _seed_from_hex(v.seed_hex)
404 dk = derive_path(seed, v.path)
405
406 expected_chain = bytes.fromhex(v.expected_chain_hex)
407 expected_priv = bytes.fromhex(v.expected_private_hex)
408
409 assert dk.chain_code == expected_chain, (
410 f"Chain code mismatch at {v.path}: "
411 f"got {dk.chain_code.hex()!r}, expected {v.expected_chain_hex!r}"
412 )
413 assert dk.private_bytes == expected_priv, (
414 f"Private key mismatch at {v.path}: "
415 f"got {dk.private_bytes.hex()!r}, expected {v.expected_private_hex!r}"
416 )
417
418 def test_all_vectors_produce_32_byte_fields(self) -> None:
419 for v in _SLIP010_VECTORS:
420 seed = _seed_from_hex(v.seed_hex)
421 dk = derive_path(seed, v.path)
422 assert len(dk.private_bytes) == 32
423 assert len(dk.chain_code) == 32
424
425
426 # ---------------------------------------------------------------------------
427 # Unit — to_ed25519_private_key()
428 # ---------------------------------------------------------------------------
429
430
431 class TestToEd25519PrivateKey:
432 def test_returns_signing_key(self) -> None:
433 from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey
434
435 seed = bytes(range(64))
436 dk = master_key(seed)
437 priv = to_ed25519_private_key(dk)
438 assert isinstance(priv, Ed25519PrivateKey)
439
440 def test_sign_and_verify(self) -> None:
441 seed = bytes(range(64))
442 dk = derive_path(seed, "m/703'/0'/0'/0'")
443 priv = to_ed25519_private_key(dk)
444 message = b"hello muse"
445 sig = priv.sign(message)
446 # verify does not raise on valid signature
447 priv.public_key().verify(sig, message)
448
449 def test_public_key_is_32_bytes(self) -> None:
450 seed = bytes(range(64))
451 dk = master_key(seed)
452 priv = to_ed25519_private_key(dk)
453 pub_bytes = priv.public_key().public_bytes_raw()
454 assert len(pub_bytes) == 32
455
456 def test_deterministic_public_key(self) -> None:
457 seed = bytes(range(64))
458 dk = master_key(seed)
459 pub1 = to_ed25519_private_key(dk).public_key().public_bytes_raw()
460 pub2 = to_ed25519_private_key(dk).public_key().public_bytes_raw()
461 assert pub1 == pub2
462
463 def test_different_paths_different_public_keys(self) -> None:
464 seed = bytes(range(64))
465 dk0 = derive_path(seed, "m/703'/0'/0'/0'")
466 dk1 = derive_path(seed, "m/703'/0'/0'/1'")
467 pub0 = to_ed25519_private_key(dk0).public_key().public_bytes_raw()
468 pub1 = to_ed25519_private_key(dk1).public_key().public_bytes_raw()
469 assert pub0 != pub1
470
471
472 # ---------------------------------------------------------------------------
473 # Integration — full Muse path pipeline
474 # ---------------------------------------------------------------------------
475
476
477 class TestMusePathPipeline:
478 def test_human_operator_msign_key(self) -> None:
479 from muse.core.bip39 import mnemonic_to_seed
480 seed = mnemonic_to_seed(
481 "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about"
482 )
483 dk = derive_path(seed, "m/703'/0'/0'/0'")
484 priv = to_ed25519_private_key(dk)
485 pub = priv.public_key().public_bytes_raw()
486 assert len(pub) == 32
487
488 def test_agent_slot_1_msign_key_differs_from_slot_0(self) -> None:
489 from muse.core.bip39 import mnemonic_to_seed
490 seed = mnemonic_to_seed(
491 "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about"
492 )
493 dk0 = derive_path(seed, "m/703'/0'/0'/0'")
494 dk1 = derive_path(seed, "m/703'/1'/0'/0'")
495 assert dk0 != dk1
496
497 def test_rotation_index_produces_different_key(self) -> None:
498 from muse.core.bip39 import mnemonic_to_seed
499 seed = mnemonic_to_seed(
500 "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about"
501 )
502 dk_current = derive_path(seed, "m/703'/0'/0'/0'")
503 dk_next = derive_path(seed, "m/703'/0'/0'/1'")
504 assert dk_current != dk_next
505
506
507 # ---------------------------------------------------------------------------
508 # Security
509 # ---------------------------------------------------------------------------
510
511
512 class TestSecurity:
513 def test_hardened_only_enforced_for_index_1(self) -> None:
514 """Index 1 (unhardened) must be rejected."""
515 seed = bytes(64)
516 dk = master_key(seed)
517 with pytest.raises(Slip010Error, match="hardened"):
518 child_key(dk, 1)
519
520 def test_hardened_only_enforced_for_max_unhardened(self) -> None:
521 seed = bytes(64)
522 dk = master_key(seed)
523 with pytest.raises(Slip010Error):
524 child_key(dk, HARDENED_OFFSET - 1)
525
526 def test_child_key_independence(self) -> None:
527 """Two sibling child keys share a parent but must be uncorrelated."""
528 seed = bytes(64)
529 parent = master_key(seed)
530 child_a = child_key(parent, hardened(0))
531 child_b = child_key(parent, hardened(1))
532 # Private bytes should differ in many positions
533 diff = sum(a != b for a, b in zip(child_a.private_bytes, child_b.private_bytes))
534 assert diff >= 10, f"Child keys are suspiciously similar: only {diff} bytes differ"
535
536 def test_parent_key_not_derivable_from_child(self) -> None:
537 """Hardened derivation: child cannot reveal parent (structural check).
538
539 We cannot formally prove this in a unit test, but we verify that the
540 child's private_bytes are not equal to, a substring of, or an XOR of
541 the parent's private_bytes — catching trivially broken implementations.
542 """
543 seed = bytes(range(64))
544 parent = master_key(seed)
545 child = child_key(parent, hardened(0))
546
547 assert child.private_bytes != parent.private_bytes
548 # Child bytes should not appear verbatim inside parent material
549 parent_material = parent.private_bytes + parent.chain_code
550 assert child.private_bytes not in parent_material
551
552 def test_repr_never_logs_hex_key_material(self) -> None:
553 seed = bytes(range(64))
554 dk = master_key(seed)
555 r = repr(dk)
556 assert dk.private_bytes.hex() not in r
557 assert dk.chain_code.hex() not in r
558
559 def test_muse_purpose_constant(self) -> None:
560 """MUSE_PURPOSE = sha256(b"muse")[:4] & 0x7FFFFFFF = 1_075_233_755."""
561 import hashlib
562 expected = int.from_bytes(hashlib.sha256(b"muse").digest()[:4], "big") & 0x7FFFFFFF
563 assert MUSE_PURPOSE == expected
564 assert MUSE_PURPOSE == 1_075_233_755
565
566 def test_hardened_offset_constant(self) -> None:
567 assert HARDENED_OFFSET == 0x80000000
568
569
570 # ---------------------------------------------------------------------------
571 # Stress
572 # ---------------------------------------------------------------------------
573
574
575 class TestStress:
576 def test_derive_path_deep_five_levels(self) -> None:
577 seed = bytes(range(64))
578 dk = derive_path(seed, f"m/{MUSE_PURPOSE}'/0'/0'/0'/0'/0'")
579 assert isinstance(dk, DerivedKey)
580 assert len(dk.private_bytes) == 32
581
582 def test_large_hardened_index(self) -> None:
583 seed = bytes(64)
584 parent = master_key(seed)
585 # Maximum valid hardened index: 2^32 - 1
586 max_index = 0xFFFFFFFF
587 child = child_key(parent, max_index)
588 assert isinstance(child, DerivedKey)
589
590 def test_100_sequential_children_all_unique(self) -> None:
591 seed = bytes(64)
592 parent = master_key(seed)
593 seen: set[bytes] = set()
594 for i in range(100):
595 c = child_key(parent, hardened(i))
596 key = c.private_bytes
597 assert key not in seen, f"Duplicate child key at index {i}"
598 seen.add(key)
599
600 def test_repeated_derivation_is_stable(self) -> None:
601 """Same inputs must always produce same output — no randomness in derivation."""
602 seed = bytes(range(64))
603 path = f"m/{MUSE_PURPOSE}'/0'/0'/0'/0'/0'"
604 dk_a = derive_path(seed, path)
605 dk_b = derive_path(seed, path)
606 dk_c = derive_path(seed, path)
607 assert dk_a == dk_b == dk_c
608
609
610 # ---------------------------------------------------------------------------
611 # Performance
612 # ---------------------------------------------------------------------------
613
614
615 class TestPerformance:
616 """Timing budgets for SLIP-0010 Ed25519 operations.
617
618 HMAC-SHA512 is fast. A single derivation step must stay under 1 ms.
619 A full six-level Muse path must complete in under 5 ms. Signing and
620 public-key extraction must complete in under 2 ms.
621 """
622
623 def test_master_key_under_1ms(self) -> None:
624 import time
625 seed = bytes(range(64))
626 start = time.perf_counter()
627 for _ in range(200):
628 master_key(seed)
629 elapsed = (time.perf_counter() - start) / 200
630 assert elapsed < 0.001, f"master_key averaged {elapsed*1000:.2f}ms — too slow"
631
632 def test_child_key_single_step_under_1ms(self) -> None:
633 import time
634 seed = bytes(range(64))
635 parent = master_key(seed)
636 start = time.perf_counter()
637 for _ in range(200):
638 child_key(parent, hardened(0))
639 elapsed = (time.perf_counter() - start) / 200
640 assert elapsed < 0.001, f"child_key averaged {elapsed*1000:.2f}ms — too slow"
641
642 def test_six_level_path_under_5ms(self) -> None:
643 import time
644 seed = bytes(range(64))
645 path = f"m/{MUSE_PURPOSE}'/0'/0'/0'/0'/0'"
646 start = time.perf_counter()
647 for _ in range(100):
648 derive_path(seed, path)
649 elapsed = (time.perf_counter() - start) / 100
650 assert elapsed < 0.005, f"derive_path(6 levels) averaged {elapsed*1000:.2f}ms — too slow"
651
652 def test_to_ed25519_private_key_under_2ms(self) -> None:
653 import time
654 seed = bytes(range(64))
655 dk = master_key(seed)
656 start = time.perf_counter()
657 for _ in range(200):
658 to_ed25519_private_key(dk)
659 elapsed = (time.perf_counter() - start) / 200
660 assert elapsed < 0.002, f"to_ed25519_private_key averaged {elapsed*1000:.2f}ms — too slow"
661
662 def test_sign_and_verify_under_5ms(self) -> None:
663 import time
664 seed = bytes(range(64))
665 dk = derive_path(seed, f"m/{MUSE_PURPOSE}'/0'/0'/0'/0'/0'")
666 priv = to_ed25519_private_key(dk)
667 msg = b"muse performance test"
668 start = time.perf_counter()
669 for _ in range(100):
670 sig = priv.sign(msg)
671 priv.public_key().verify(sig, msg)
672 elapsed = (time.perf_counter() - start) / 100
673 assert elapsed < 0.005, f"sign+verify averaged {elapsed*1000:.2f}ms — too slow"
674
675
676 # ---------------------------------------------------------------------------
677 # Docstrings
678 # ---------------------------------------------------------------------------
679
680
681 class TestDocstrings:
682 """Every public symbol in muse.core.slip010 must have a docstring."""
683
684 def test_module_has_docstring(self) -> None:
685 import muse.core.slip010 as mod
686 assert mod.__doc__, "muse.core.slip010 module has no docstring"
687
688 @pytest.mark.parametrize("name", [
689 "Slip010Error",
690 "DerivedKey",
691 "master_key",
692 "child_key",
693 "derive_path",
694 "parse_path",
695 "to_ed25519_private_key",
696 "hardened",
697 ])
698 def test_public_symbol_has_docstring(self, name: str) -> None:
699 import muse.core.slip010 as mod
700 obj = getattr(mod, name)
701 assert obj.__doc__, f"muse.core.slip010.{name} has no docstring"
File History 2 commits
sha256:88ac91129873e6a496e9189515aa690eb893ae25d69c8f72af141a2be5068eb3 docs: docstring sprint contract→find-symbol — idiomatic run… Sonnet 4.6 patch 141 days ago
sha256:a09b1b4f6838754495547f200aa0ce88e2f56ffc5b20b900f6f0cff2c3cdede9 fix(cursorignore): remove git-ism (.git/worktrees) Human minor 144 days ago