test_core_hdkeys.py
python
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