gabriel / muse public
test_core_hdkeys.py python
639 lines 26.1 KB
Raw
sha256:b636f72dcba9e190afb980bece906fa5b717fbde014b76ef023df8cb96e01eb9 docs: expand cache plan with all seven testing tiers and do… Sonnet 4.6 140 days ago
1 """Tests for muse.core.hdkeys — six-level domain-first HD key derivation.
2
3 Test categories
4 ---------------
5 - Unit: constants, path construction, argument validation, return types
6 - Integration: full pipeline from mnemonic → seed → key → signature
7 - Stress: uniqueness across all dimensions (domain, entity, role, index)
8 - Security: domain isolation, agent sub-seed independence, least privilege
9 - Data integrity: consistency between hdkeys API and direct slip010 derivation
10 """
11
12 from __future__ import annotations
13
14 import pytest
15
16 from muse.core.bip39 import mnemonic_to_seed
17 from muse.core.hdkeys import (
18 DOMAIN_BLOCKCHAIN,
19 DOMAIN_CODE,
20 DOMAIN_IDENTITY,
21 DOMAIN_MIDI,
22 DOMAIN_MUSIC,
23 DOMAIN_PAYMENTS,
24 DOMAIN_PROSE,
25 ENTITY_AGENT,
26 ENTITY_HUMAN,
27 ENTITY_ORG,
28 HdKeyError,
29 ROLE_ATTEST,
30 ROLE_DELEGATE,
31 ROLE_PROVISION,
32 ROLE_RECEIVE,
33 ROLE_SIGN,
34 derive_agent_sub_seed,
35 derive_domain_key,
36 derive_identity_key,
37 derive_key,
38 dk_to_ed25519,
39 muse_path,
40 public_bytes_from_seed,
41 )
42 from muse.core.slip010 import (
43 MUSE_PURPOSE,
44 DerivedKey,
45 child_key,
46 derive_path,
47 hardened,
48 master_key,
49 )
50
51 _TEST_MNEMONIC = (
52 "abandon abandon abandon abandon abandon abandon "
53 "abandon abandon abandon abandon abandon about"
54 )
55
56
57 @pytest.fixture
58 def seed() -> bytes:
59 return mnemonic_to_seed(_TEST_MNEMONIC)
60
61
62 @pytest.fixture
63 def raw_seed() -> bytes:
64 return bytes(range(64))
65
66
67 # ---------------------------------------------------------------------------
68 # Unit — constants
69 # ---------------------------------------------------------------------------
70
71
72 class TestConstants:
73 def test_domain_values(self) -> None:
74 assert DOMAIN_IDENTITY == 0
75 assert DOMAIN_PAYMENTS == 1
76 assert DOMAIN_CODE == 2
77 assert DOMAIN_MUSIC == 3
78 assert DOMAIN_MIDI == 4
79 assert DOMAIN_PROSE == 5
80 assert DOMAIN_BLOCKCHAIN == 6
81
82 def test_entity_values(self) -> None:
83 assert ENTITY_HUMAN == 0
84 assert ENTITY_AGENT == 1
85 assert ENTITY_ORG == 2
86
87 def test_role_values(self) -> None:
88 assert ROLE_SIGN == 0
89 assert ROLE_RECEIVE == 1
90 assert ROLE_PROVISION == 2
91 assert ROLE_ATTEST == 3
92 assert ROLE_DELEGATE == 4
93
94 def test_all_domains_distinct(self) -> None:
95 domains = [DOMAIN_IDENTITY, DOMAIN_PAYMENTS, DOMAIN_CODE,
96 DOMAIN_MUSIC, DOMAIN_MIDI, DOMAIN_PROSE, DOMAIN_BLOCKCHAIN]
97 assert len(set(domains)) == 7
98
99 def test_all_entities_distinct(self) -> None:
100 entities = [ENTITY_HUMAN, ENTITY_AGENT, ENTITY_ORG]
101 assert len(set(entities)) == 3
102
103 def test_all_roles_distinct(self) -> None:
104 roles = [ROLE_SIGN, ROLE_RECEIVE, ROLE_PROVISION, ROLE_ATTEST, ROLE_DELEGATE]
105 assert len(set(roles)) == 5
106
107
108 # ---------------------------------------------------------------------------
109 # Unit — muse_path()
110 # ---------------------------------------------------------------------------
111
112
113 class TestMusePath:
114 def test_default_identity_path(self) -> None:
115 assert muse_path(DOMAIN_IDENTITY) == f"m/{MUSE_PURPOSE}'/0'/0'/0'/0'/0'"
116
117 def test_music_agent_path(self) -> None:
118 expected = f"m/{MUSE_PURPOSE}'/3'/1'/2'/0'/0'"
119 assert muse_path(DOMAIN_MUSIC, entity_type=ENTITY_AGENT, entity_id=2) == expected
120
121 def test_code_domain_rotation(self) -> None:
122 expected = f"m/{MUSE_PURPOSE}'/2'/0'/0'/0'/1'"
123 assert muse_path(DOMAIN_CODE, index=1) == expected
124
125 def test_payments_receive_role(self) -> None:
126 expected = f"m/{MUSE_PURPOSE}'/1'/0'/0'/1'/0'"
127 assert muse_path(DOMAIN_PAYMENTS, role=ROLE_RECEIVE) == expected
128
129 def test_all_six_levels_present(self) -> None:
130 path = muse_path(DOMAIN_IDENTITY)
131 parts = path.split("/")
132 assert parts[0] == "m"
133 assert len(parts) == 7 # m + 6 levels
134
135 def test_all_levels_hardened(self) -> None:
136 path = muse_path(DOMAIN_IDENTITY)
137 for part in path.split("/")[1:]:
138 assert part.endswith("'"), f"Level {part!r} is not hardened"
139
140 def test_negative_domain_raises(self) -> None:
141 with pytest.raises(HdKeyError, match="domain"):
142 muse_path(-1)
143
144 def test_negative_entity_id_raises(self) -> None:
145 with pytest.raises(HdKeyError, match="entity_id"):
146 muse_path(DOMAIN_IDENTITY, entity_id=-1)
147
148 def test_negative_index_raises(self) -> None:
149 with pytest.raises(HdKeyError, match="index"):
150 muse_path(DOMAIN_IDENTITY, index=-1)
151
152 def test_invalid_entity_type_raises(self) -> None:
153 with pytest.raises(HdKeyError, match="entity_type"):
154 muse_path(DOMAIN_IDENTITY, entity_type=4)
155
156 def test_invalid_role_raises(self) -> None:
157 with pytest.raises(HdKeyError, match="role"):
158 muse_path(DOMAIN_IDENTITY, role=5)
159
160 def test_returns_string(self) -> None:
161 assert isinstance(muse_path(DOMAIN_IDENTITY), str)
162
163 def test_purpose_embedded_in_path(self) -> None:
164 assert str(MUSE_PURPOSE) in muse_path(DOMAIN_IDENTITY)
165
166
167 # ---------------------------------------------------------------------------
168 # Unit — derive_key()
169 # ---------------------------------------------------------------------------
170
171
172 class TestDeriveKey:
173 def test_returns_derived_key(self, raw_seed: bytes) -> None:
174 assert isinstance(derive_key(raw_seed, DOMAIN_IDENTITY), DerivedKey)
175
176 def test_private_bytes_32(self, raw_seed: bytes) -> None:
177 assert len(derive_key(raw_seed, DOMAIN_IDENTITY).private_bytes) == 32
178
179 def test_chain_code_32(self, raw_seed: bytes) -> None:
180 assert len(derive_key(raw_seed, DOMAIN_IDENTITY).chain_code) == 32
181
182 def test_matches_direct_slip010_derivation(self, raw_seed: bytes) -> None:
183 path = muse_path(DOMAIN_IDENTITY, ENTITY_HUMAN, 0, ROLE_SIGN, 0)
184 dk_direct = derive_path(raw_seed, path)
185 dk_api = derive_key(raw_seed, DOMAIN_IDENTITY)
186 assert dk_api == dk_direct
187
188 def test_negative_domain_raises(self, raw_seed: bytes) -> None:
189 with pytest.raises(HdKeyError):
190 derive_key(raw_seed, -1)
191
192 def test_invalid_role_raises(self, raw_seed: bytes) -> None:
193 with pytest.raises(HdKeyError):
194 derive_key(raw_seed, DOMAIN_IDENTITY, role=9)
195
196 def test_deterministic(self, raw_seed: bytes) -> None:
197 dk1 = derive_key(raw_seed, DOMAIN_MUSIC)
198 dk2 = derive_key(raw_seed, DOMAIN_MUSIC)
199 assert dk1 == dk2
200
201
202 # ---------------------------------------------------------------------------
203 # Unit — derive_identity_key()
204 # ---------------------------------------------------------------------------
205
206
207 class TestDeriveIdentityKey:
208 def test_defaults_to_domain_identity(self, raw_seed: bytes) -> None:
209 dk = derive_identity_key(raw_seed)
210 expected = derive_path(raw_seed, muse_path(DOMAIN_IDENTITY))
211 assert dk == expected
212
213 def test_entity_type_agent(self, raw_seed: bytes) -> None:
214 dk = derive_identity_key(raw_seed, entity_type=ENTITY_AGENT, entity_id=0)
215 expected = derive_path(raw_seed, muse_path(DOMAIN_IDENTITY, ENTITY_AGENT, 0))
216 assert dk == expected
217
218 def test_rotation_index(self, raw_seed: bytes) -> None:
219 current = derive_identity_key(raw_seed, index=0)
220 rotated = derive_identity_key(raw_seed, index=1)
221 assert current != rotated
222
223 def test_negative_entity_id_raises(self, raw_seed: bytes) -> None:
224 with pytest.raises(HdKeyError):
225 derive_identity_key(raw_seed, entity_id=-1)
226
227
228 # ---------------------------------------------------------------------------
229 # Unit — derive_domain_key()
230 # ---------------------------------------------------------------------------
231
232
233 class TestDeriveDomainKey:
234 @pytest.mark.parametrize("domain", [
235 DOMAIN_IDENTITY, DOMAIN_PAYMENTS, DOMAIN_CODE,
236 DOMAIN_MUSIC, DOMAIN_MIDI, DOMAIN_PROSE, DOMAIN_BLOCKCHAIN,
237 ])
238 def test_all_named_domains(self, domain: int, raw_seed: bytes) -> None:
239 dk = derive_domain_key(raw_seed, domain)
240 assert isinstance(dk, DerivedKey)
241
242 def test_code_differs_from_music(self, raw_seed: bytes) -> None:
243 assert derive_domain_key(raw_seed, DOMAIN_CODE) != derive_domain_key(raw_seed, DOMAIN_MUSIC)
244
245 def test_role_receive_differs_from_sign(self, raw_seed: bytes) -> None:
246 sign = derive_domain_key(raw_seed, DOMAIN_PAYMENTS, role=ROLE_SIGN)
247 recv = derive_domain_key(raw_seed, DOMAIN_PAYMENTS, role=ROLE_RECEIVE)
248 assert sign != recv
249
250
251 # ---------------------------------------------------------------------------
252 # Unit — derive_agent_sub_seed()
253 # ---------------------------------------------------------------------------
254
255
256 class TestDeriveAgentSubSeed:
257 def test_returns_64_bytes(self, raw_seed: bytes) -> None:
258 sub = derive_agent_sub_seed(raw_seed, DOMAIN_MUSIC, agent_id=0)
259 assert isinstance(sub, (bytes, bytearray)) and len(sub) == 64
260
261 def test_different_domains_produce_different_sub_seeds(self, raw_seed: bytes) -> None:
262 sub_music = derive_agent_sub_seed(raw_seed, DOMAIN_MUSIC, agent_id=0)
263 sub_code = derive_agent_sub_seed(raw_seed, DOMAIN_CODE, agent_id=0)
264 assert sub_music != sub_code
265
266 def test_different_agent_ids_produce_different_sub_seeds(self, raw_seed: bytes) -> None:
267 sub0 = derive_agent_sub_seed(raw_seed, DOMAIN_MUSIC, agent_id=0)
268 sub1 = derive_agent_sub_seed(raw_seed, DOMAIN_MUSIC, agent_id=1)
269 assert sub0 != sub1
270
271 def test_deterministic(self, raw_seed: bytes) -> None:
272 sub1 = derive_agent_sub_seed(raw_seed, DOMAIN_MUSIC, agent_id=0)
273 sub2 = derive_agent_sub_seed(raw_seed, DOMAIN_MUSIC, agent_id=0)
274 assert sub1 == sub2
275
276 def test_differs_from_master_seed(self, raw_seed: bytes) -> None:
277 sub = derive_agent_sub_seed(raw_seed, DOMAIN_MUSIC, agent_id=0)
278 assert sub != raw_seed
279
280 def test_sub_seed_composition(self, raw_seed: bytes) -> None:
281 """Sub-seed = private_bytes + chain_code at m/purpose'/domain'/AGENT'/agent_id'."""
282 dk = master_key(raw_seed)
283 dk = child_key(dk, hardened(MUSE_PURPOSE))
284 dk = child_key(dk, hardened(DOMAIN_MUSIC))
285 dk = child_key(dk, hardened(ENTITY_AGENT))
286 dk = child_key(dk, hardened(0))
287 expected = dk.private_bytes + dk.chain_code
288 assert derive_agent_sub_seed(raw_seed, DOMAIN_MUSIC, agent_id=0) == expected
289
290 def test_agent_can_derive_identity_key_from_sub_seed(self, raw_seed: bytes) -> None:
291 auth_seed = derive_agent_sub_seed(raw_seed, DOMAIN_IDENTITY, agent_id=0)
292 dk = derive_identity_key(auth_seed)
293 assert isinstance(dk, DerivedKey)
294
295 def test_negative_domain_raises(self, raw_seed: bytes) -> None:
296 with pytest.raises(HdKeyError):
297 derive_agent_sub_seed(raw_seed, -1, agent_id=0)
298
299 def test_negative_agent_id_raises(self, raw_seed: bytes) -> None:
300 with pytest.raises(HdKeyError):
301 derive_agent_sub_seed(raw_seed, DOMAIN_MUSIC, agent_id=-1)
302
303
304 # ---------------------------------------------------------------------------
305 # Unit — dk_to_ed25519() and public_bytes_from_seed()
306 # ---------------------------------------------------------------------------
307
308
309 class TestKeyMaterialisation:
310 def test_dk_to_ed25519_returns_signing_key(self, raw_seed: bytes) -> None:
311 from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey
312 dk = derive_identity_key(raw_seed)
313 assert isinstance(dk_to_ed25519(dk), Ed25519PrivateKey)
314
315 def test_sign_and_verify(self, raw_seed: bytes) -> None:
316 dk = derive_identity_key(raw_seed)
317 priv = dk_to_ed25519(dk)
318 msg = b"muse hd path design"
319 priv.public_key().verify(priv.sign(msg), msg)
320
321 def test_public_bytes_32(self, raw_seed: bytes) -> None:
322 assert len(public_bytes_from_seed(raw_seed)) == 32
323
324 def test_public_bytes_deterministic(self, raw_seed: bytes) -> None:
325 assert public_bytes_from_seed(raw_seed) == public_bytes_from_seed(raw_seed)
326
327 def test_public_bytes_defaults_to_identity_domain(self, raw_seed: bytes) -> None:
328 pub = public_bytes_from_seed(raw_seed)
329 expected = dk_to_ed25519(derive_identity_key(raw_seed)).public_key().public_bytes_raw()
330 assert pub == expected
331
332 def test_different_domains_different_public_keys(self, raw_seed: bytes) -> None:
333 pub_id = public_bytes_from_seed(raw_seed, domain=DOMAIN_IDENTITY)
334 pub_code = public_bytes_from_seed(raw_seed, domain=DOMAIN_CODE)
335 assert pub_id != pub_code
336
337
338 # ---------------------------------------------------------------------------
339 # Integration — full pipeline
340 # ---------------------------------------------------------------------------
341
342
343 class TestFullPipeline:
344 def test_mnemonic_to_signature(self, seed: bytes) -> None:
345 dk = derive_identity_key(seed)
346 priv = dk_to_ed25519(dk)
347 msg = b"muse authentication"
348 priv.public_key().verify(priv.sign(msg), msg)
349
350 def test_wrong_key_cannot_verify(self, seed: bytes) -> None:
351 from cryptography.exceptions import InvalidSignature
352 current = derive_identity_key(seed, index=0)
353 rotated = derive_identity_key(seed, index=1)
354 msg = b"signed with current key"
355 sig = dk_to_ed25519(current).sign(msg)
356 with pytest.raises(InvalidSignature):
357 dk_to_ed25519(rotated).public_key().verify(sig, msg)
358
359 def test_human_and_agent_identity_keys_differ(self, seed: bytes) -> None:
360 auth_seed = derive_agent_sub_seed(seed, DOMAIN_IDENTITY, agent_id=0)
361 pub_human = public_bytes_from_seed(seed, domain=DOMAIN_IDENTITY)
362 pub_agent = dk_to_ed25519(derive_identity_key(auth_seed)).public_key().public_bytes_raw()
363 assert pub_human != pub_agent
364
365 def test_five_agents_distinct_identity_keys(self, seed: bytes) -> None:
366 pub_keys: set[bytes] = set()
367 for agent_id in range(5):
368 auth_seed = derive_agent_sub_seed(seed, DOMAIN_IDENTITY, agent_id=agent_id)
369 pub = dk_to_ed25519(derive_identity_key(auth_seed)).public_key().public_bytes_raw()
370 assert pub not in pub_keys, f"Duplicate at agent_id={agent_id}"
371 pub_keys.add(pub)
372
373
374 # ---------------------------------------------------------------------------
375 # Security — domain isolation and least privilege
376 # ---------------------------------------------------------------------------
377
378
379 class TestSecurity:
380 def test_identity_key_differs_from_all_domain_keys(self, raw_seed: bytes) -> None:
381 identity_pub = public_bytes_from_seed(raw_seed, domain=DOMAIN_IDENTITY)
382 for domain in (DOMAIN_PAYMENTS, DOMAIN_CODE, DOMAIN_MUSIC,
383 DOMAIN_MIDI, DOMAIN_PROSE, DOMAIN_BLOCKCHAIN):
384 other_pub = public_bytes_from_seed(raw_seed, domain=domain)
385 assert identity_pub != other_pub, f"Identity key leaked into domain={domain}"
386
387 def test_music_agent_cannot_derive_code_domain_key(self, raw_seed: bytes) -> None:
388 """A music agent's sub-seed produces a different code key than the operator's."""
389 music_agent_seed = derive_agent_sub_seed(raw_seed, DOMAIN_MUSIC, agent_id=0)
390 operator_code_pub = public_bytes_from_seed(raw_seed, domain=DOMAIN_CODE)
391 agent_code_pub = public_bytes_from_seed(music_agent_seed, domain=DOMAIN_CODE)
392 assert operator_code_pub != agent_code_pub
393
394 def test_domain_scoped_sub_seeds_are_independent(self, raw_seed: bytes) -> None:
395 domains = [DOMAIN_IDENTITY, DOMAIN_PAYMENTS, DOMAIN_CODE,
396 DOMAIN_MUSIC, DOMAIN_MIDI, DOMAIN_PROSE]
397 sub_seeds = [bytes(derive_agent_sub_seed(raw_seed, d, agent_id=0)) for d in domains]
398 assert len(set(sub_seeds)) == len(domains), "Domain sub-seeds are not independent"
399
400 def test_all_roles_produce_distinct_keys(self, raw_seed: bytes) -> None:
401 keys: set[bytes] = set()
402 for role in (ROLE_SIGN, ROLE_RECEIVE, ROLE_PROVISION, ROLE_ATTEST, ROLE_DELEGATE):
403 pub = public_bytes_from_seed(raw_seed, domain=DOMAIN_CODE, role=role)
404 assert pub not in keys, f"Duplicate key for role={role}"
405 keys.add(pub)
406
407 def test_sub_seed_does_not_expose_master_seed(self, raw_seed: bytes) -> None:
408 sub = derive_agent_sub_seed(raw_seed, DOMAIN_MUSIC, agent_id=0)
409 assert sub != raw_seed
410 # First 32 bytes of sub-seed are derived private bytes — must not equal
411 # anything trivially derivable from master seed
412 assert sub[:32] != raw_seed[:32]
413
414
415 # ---------------------------------------------------------------------------
416 # Stress — uniqueness across all six dimensions
417 # ---------------------------------------------------------------------------
418
419
420 class TestStress:
421 def test_100_domain_keys_all_unique(self, raw_seed: bytes) -> None:
422 # Extend domain namespace — open by design
423 pubs: set[bytes] = set()
424 for domain in range(100):
425 pub = public_bytes_from_seed(raw_seed, domain=domain)
426 assert pub not in pubs, f"Duplicate at domain={domain}"
427 pubs.add(pub)
428
429 def test_100_entity_ids_all_unique(self, raw_seed: bytes) -> None:
430 pubs: set[bytes] = set()
431 for entity_id in range(100):
432 pub = public_bytes_from_seed(raw_seed, domain=DOMAIN_IDENTITY, entity_id=entity_id)
433 assert pub not in pubs, f"Duplicate at entity_id={entity_id}"
434 pubs.add(pub)
435
436 def test_100_rotation_indices_all_unique(self, raw_seed: bytes) -> None:
437 pubs: set[bytes] = set()
438 for index in range(100):
439 pub = public_bytes_from_seed(raw_seed, domain=DOMAIN_IDENTITY, index=index)
440 assert pub not in pubs, f"Duplicate at index={index}"
441 pubs.add(pub)
442
443 def test_50_agent_sub_seeds_all_unique_per_domain(self, raw_seed: bytes) -> None:
444 for domain in (DOMAIN_IDENTITY, DOMAIN_MUSIC, DOMAIN_CODE):
445 subs: set[bytes] = set()
446 for agent_id in range(50):
447 sub = bytes(derive_agent_sub_seed(raw_seed, domain, agent_id=agent_id))
448 assert sub not in subs, f"Duplicate at domain={domain} agent_id={agent_id}"
449 subs.add(sub)
450
451 def test_all_7_domains_x_3_entities_x_5_roles_unique(self, raw_seed: bytes) -> None:
452 domains = [DOMAIN_IDENTITY, DOMAIN_PAYMENTS, DOMAIN_CODE, DOMAIN_MUSIC,
453 DOMAIN_MIDI, DOMAIN_PROSE, DOMAIN_BLOCKCHAIN]
454 entities = [ENTITY_HUMAN, ENTITY_AGENT, ENTITY_ORG]
455 roles = [ROLE_SIGN, ROLE_RECEIVE, ROLE_PROVISION, ROLE_ATTEST, ROLE_DELEGATE]
456 pubs: set[bytes] = set()
457 total = 0
458 for d in domains:
459 for e in entities:
460 for r in roles:
461 pub = public_bytes_from_seed(raw_seed, domain=d, entity_type=e, role=r)
462 assert pub not in pubs, f"Duplicate at domain={d} entity={e} role={r}"
463 pubs.add(pub)
464 total += 1
465 assert total == 7 * 3 * 5 # 105 unique keys
466
467
468 # ---------------------------------------------------------------------------
469 # End-to-end
470 # ---------------------------------------------------------------------------
471
472
473 class TestEndToEnd:
474 """Full workflow scenarios from mnemonic to network-ready public key.
475
476 These tests simulate real caller sequences: the path a MuseHub
477 registration, an agent spawn, and a key rotation take through this stack.
478 """
479
480 def test_musehub_registration_flow(self) -> None:
481 """Operator generates mnemonic, derives identity key, gets public key for registration."""
482 from muse.core.bip39 import generate_mnemonic, mnemonic_to_seed
483 mnemonic = generate_mnemonic()
484 seed = mnemonic_to_seed(mnemonic)
485 pub = public_bytes_from_seed(seed, domain=DOMAIN_IDENTITY)
486 assert len(pub) == 32
487 fingerprint = pub.hex()
488 assert len(fingerprint) == 64 # 32 bytes → 64 hex chars
489
490 def test_agent_spawn_flow(self) -> None:
491 """Operator spawns a music agent with domain-scoped sub-seed."""
492 from muse.core.bip39 import generate_mnemonic, mnemonic_to_seed
493 seed = mnemonic_to_seed(generate_mnemonic())
494
495 # Operator derives domain-scoped sub-seeds for the agent
496 music_seed = derive_agent_sub_seed(seed, DOMAIN_MUSIC, agent_id=0)
497 auth_seed = derive_agent_sub_seed(seed, DOMAIN_IDENTITY, agent_id=0)
498
499 # Agent uses each seed for its respective domain
500 agent_identity_pub = dk_to_ed25519(derive_identity_key(auth_seed)).public_key().public_bytes_raw()
501 agent_music_pub = dk_to_ed25519(derive_domain_key(music_seed, DOMAIN_MUSIC)).public_key().public_bytes_raw()
502
503 assert len(agent_identity_pub) == 32
504 assert len(agent_music_pub) == 32
505 # Agent's identity pub differs from operator's
506 operator_pub = public_bytes_from_seed(seed, domain=DOMAIN_IDENTITY)
507 assert agent_identity_pub != operator_pub
508
509 def test_key_rotation_flow(self) -> None:
510 """Operator pre-registers next key then rotates."""
511 from muse.core.bip39 import generate_mnemonic, mnemonic_to_seed
512 seed = mnemonic_to_seed(generate_mnemonic())
513
514 # Current key in use
515 current_pub = public_bytes_from_seed(seed, domain=DOMAIN_IDENTITY, index=0)
516 # Pre-register next key before rotation
517 next_pub = public_bytes_from_seed(seed, domain=DOMAIN_IDENTITY, index=1)
518
519 assert current_pub != next_pub
520 # After rotation, index 1 becomes the active key
521 # Both remain independently derivable from the same seed
522 assert len(current_pub) == len(next_pub) == 32
523
524 def test_commit_signing_flow(self) -> None:
525 """Operator signs a commit hash with their code domain key."""
526 from muse.core.bip39 import generate_mnemonic, mnemonic_to_seed
527 import hashlib
528 seed = mnemonic_to_seed(generate_mnemonic())
529 dk = derive_domain_key(seed, domain=DOMAIN_CODE)
530 priv = dk_to_ed25519(dk)
531 # Simulate a commit hash
532 commit_bytes = hashlib.sha256(b"tree abc123\nparent def456\nauthor gabriel").digest()
533 sig = priv.sign(commit_bytes)
534 # Any verifier with the public key can verify
535 priv.public_key().verify(sig, commit_bytes)
536
537 def test_music_project_signing_flow(self) -> None:
538 """Operator signs a Stori project snapshot with their music domain key."""
539 from muse.core.bip39 import generate_mnemonic, mnemonic_to_seed
540 seed = mnemonic_to_seed(generate_mnemonic())
541 dk = derive_domain_key(seed, domain=DOMAIN_MUSIC)
542 priv = dk_to_ed25519(dk)
543 project_manifest = b"track: neon_sunrise.wav\nbpm: 128\nkey: Am"
544 sig = priv.sign(project_manifest)
545 priv.public_key().verify(sig, project_manifest)
546
547
548 # ---------------------------------------------------------------------------
549 # Performance
550 # ---------------------------------------------------------------------------
551
552
553 class TestPerformance:
554 """Timing budgets for hdkeys high-level operations.
555
556 Key derivation is dominated by HMAC-SHA512 (fast). The only slow
557 operation is mnemonic_to_seed (PBKDF2) which lives in bip39 — once
558 the seed is in hand, all hdkeys operations must be fast.
559 """
560
561 def test_derive_identity_key_under_5ms(self) -> None:
562 import time
563 seed = bytes(range(64))
564 start = time.perf_counter()
565 for _ in range(100):
566 derive_identity_key(seed)
567 elapsed = (time.perf_counter() - start) / 100
568 assert elapsed < 0.005, f"derive_identity_key averaged {elapsed*1000:.2f}ms — too slow"
569
570 def test_derive_domain_key_under_5ms(self) -> None:
571 import time
572 seed = bytes(range(64))
573 start = time.perf_counter()
574 for _ in range(100):
575 derive_domain_key(seed, DOMAIN_MUSIC)
576 elapsed = (time.perf_counter() - start) / 100
577 assert elapsed < 0.005, f"derive_domain_key averaged {elapsed*1000:.2f}ms — too slow"
578
579 def test_derive_agent_sub_seed_under_5ms(self) -> None:
580 import time
581 seed = bytes(range(64))
582 start = time.perf_counter()
583 for _ in range(100):
584 derive_agent_sub_seed(seed, DOMAIN_MUSIC, agent_id=0)
585 elapsed = (time.perf_counter() - start) / 100
586 assert elapsed < 0.005, f"derive_agent_sub_seed averaged {elapsed*1000:.2f}ms — too slow"
587
588 def test_public_bytes_from_seed_under_5ms(self) -> None:
589 import time
590 seed = bytes(range(64))
591 start = time.perf_counter()
592 for _ in range(100):
593 public_bytes_from_seed(seed)
594 elapsed = (time.perf_counter() - start) / 100
595 assert elapsed < 0.005, f"public_bytes_from_seed averaged {elapsed*1000:.2f}ms — too slow"
596
597 def test_105_key_matrix_under_100ms(self) -> None:
598 """7 domains × 3 entity types × 5 roles = 105 derivations must complete in < 100ms."""
599 import time
600 seed = bytes(range(64))
601 domains = [DOMAIN_IDENTITY, DOMAIN_PAYMENTS, DOMAIN_CODE, DOMAIN_MUSIC,
602 DOMAIN_MIDI, DOMAIN_PROSE, DOMAIN_BLOCKCHAIN]
603 entities = [ENTITY_HUMAN, ENTITY_AGENT, ENTITY_ORG]
604 roles = [ROLE_SIGN, ROLE_RECEIVE, ROLE_PROVISION, ROLE_ATTEST, ROLE_DELEGATE]
605 start = time.perf_counter()
606 for d in domains:
607 for e in entities:
608 for r in roles:
609 derive_key(seed, domain=d, entity_type=e, role=r)
610 elapsed = time.perf_counter() - start
611 assert elapsed < 0.1, f"105-key matrix took {elapsed*1000:.1f}ms — too slow"
612
613
614 # ---------------------------------------------------------------------------
615 # Docstrings
616 # ---------------------------------------------------------------------------
617
618
619 class TestDocstrings:
620 """Every public symbol in muse.core.hdkeys must have a docstring."""
621
622 def test_module_has_docstring(self) -> None:
623 import muse.core.hdkeys as mod
624 assert mod.__doc__, "muse.core.hdkeys module has no docstring"
625
626 @pytest.mark.parametrize("name", [
627 "HdKeyError",
628 "muse_path",
629 "derive_key",
630 "derive_identity_key",
631 "derive_domain_key",
632 "derive_agent_sub_seed",
633 "dk_to_ed25519",
634 "public_bytes_from_seed",
635 ])
636 def test_public_symbol_has_docstring(self, name: str) -> None:
637 import muse.core.hdkeys as mod
638 obj = getattr(mod, name)
639 assert obj.__doc__, f"muse.core.hdkeys.{name} has no docstring"
File History 3 commits
sha256:b636f72dcba9e190afb980bece906fa5b717fbde014b76ef023df8cb96e01eb9 docs: expand cache plan with all seven testing tiers and do… Sonnet 4.6 140 days ago
sha256:7f9e2ef5286aedad9c1e6011b4c46ca27f39dbdad6e3409357e36b26e46b3b7c docs: docstring sprint for-each-ref→hotspots — idiomatic ru… Sonnet 4.6 patch 146 days ago
sha256:a09b1b4f6838754495547f200aa0ce88e2f56ffc5b20b900f6f0cff2c3cdede9 fix(cursorignore): remove git-ism (.git/worktrees) Human minor 149 days ago