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