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