feat: hash-derived domain integers with multi-sig registry
Background
Muse's HD derivation path is:
m / 1075233755' / domain' / entity_type' / entity_id' / role' / index'
The purpose' level (1075233755) is already derived by hashing the string
"muse" — int.from_bytes(sha256(b"muse")[:4], "big") & 0x7FFFFFFF. This
was a deliberate design choice made at Ava Labs to avoid the BIP44 coin-type
registration war: instead of petitioning a committee for a number, you hash
your project name and own that slot forever.
The domain' level previously used small hardcoded integers (0–6). This created
the exact same registration-war problem for third-party domains. Phase 1 has
shipped — all domain constants are now hash-derived. See implementation notes
below.
Proposal: Hash-Derived Domain Integers + Multi-Sig Registry
The Core Insight
Every domain integer is derived from its canonical name string:
def domain_index(name: str) -> int:
digest = hashlib.sha256(name.encode("utf-8")).digest()
return int.from_bytes(digest[:4], "big") & 0x7FFFFFFF
Current canonical domain values (stable — do not change):
DOMAIN_IDENTITY = domain_index("muse/identity") # 1660078172
DOMAIN_PAYMENTS = domain_index("muse/payments") # 284229149
DOMAIN_CODE = domain_index("muse/code") # 678195575
DOMAIN_MUSIC = domain_index("muse/music") # 1755707987
DOMAIN_MIDI = domain_index("muse/midi") # 1444628350
DOMAIN_PROSE = domain_index("muse/prose") # 1658731548
DOMAIN_BLOCKCHAIN = domain_index("muse/blockchain") # 1556829714
DOMAIN_GENERIC = domain_index("muse/generic") # 2023564266
A third-party domain requires zero permission:
DOMAIN_GAMING = domain_index("acme/gaming")
DOMAIN_HEALTHCARE = domain_index("openhealth/records")
DOMAIN_LEGAL = domain_index("lexcorp/contracts")
Why This Cannot Conflict
Two domains collide only if sha256(a)[:4] == sha256(b)[:4]. For 10,000
registered domains, birthday collision probability is ~0.0023%. A collision
would be immediately visible in the registry and trivially resolved by
appending a disambiguating suffix to the name string.
The Registry
The domain integer is self-certifying — it derives from the name, needs no external validation, and keys work on day one without registration. The registry is purely a human-readable annotation layer for tooling.
Structure
Dedicated Muse repo: gabriel/muse-domains on MuseHub. This repo has been
created and pushed (Phase 2 complete).
muse-domains/
registry.json ← canonical name → index mapping
schema.json ← JSON Schema for registry entries
governance.json ← quorum policy
GOVERNANCE.md ← registration flow docs
registry.json format:
{
"schema": 1,
"domains": [
{
"name": "muse/identity",
"index": 1660078172,
"description": "MSign auth, MuseHub registration, cross-domain self",
"owner": "gabriel",
"registered_at": null
}
]
}
registered_at will be the commit ID of the merge that added each entry —
providing a tamper-evident timestamp and audit trail by construction.
Registration Flow
1. Anyone computes:
index = domain_index("myorg/mydomain")
2. Keys derived at that index work immediately — no permission needed.
3. Optional: open a proposal on gabriel/muse-domains adding the entry.
This is purely for discoverability — it does not gate key derivation.
4. Proposal is reviewed and merged by the quorum (see Governance).
5. Tooling annotates paths using the registry:
m/1075233755'/1660078172'/... → "muse/identity"
Unregistered domains display their raw integer — still functional.
Governance: Multi-Sig Quorum, Not a Single Signer
The muse-domains repo carries a governance.json at HEAD. Merging a
proposal requires ≥ threshold approved reviews from declared quorum members.
{
"schema": 1,
"quorum": {
"threshold": 1,
"policy": "1-of-1",
"members": ["sha256:d8faf8..."]
}
}
MuseHub quorum enforcement is wired and shipped (Phase 2 complete).
POST /repos/{repo_id}/proposals/{proposal_id}/merge returns 403 when
governance.json exists but quorum is not met. 10 TDD tests green.
A merge commit to muse-domains is only valid if it carries ≥ threshold
approvals from the declared member fingerprint set. Anyone can verify offline:
muse -C ~/ecosystem/muse-domains verify-commit HEAD --quorum --json
# → { "valid": true, "signatures": 1, "threshold": 1, "signers": [...] }
Tooling Changes
muse/core/hdkeys.py — DONE (Phase 1)
domain_index(name) added, all DOMAIN_* constants recomputed, exported in
__all__. All tests updated and green.
Registry CLI — TODO (Phase 3)
# Compute index for any name (no registry needed)
muse domain index "myorg/mycustom" --json
# → { "name": "myorg/mycustom", "index": 2938471023, "registered": false }
# Look up a domain name
muse domain lookup "muse/music" --json
# Look up by index
muse domain lookup --index 1755707987 --json
# List all registered domains
muse domain list --json
# Check if a name is registered (exit 0/1)
muse domain check "acme/gaming" --json
Path annotation CLI — TODO (Phase 3)
# Annotate a raw HD path to human-readable form
muse path annotate "m/1075233755'/1660078172'/0'/0'/0'/0'" --json
# → { "purpose": "muse", "domain": "muse/identity", "entity_type": "human", ... }
Migration
Phase 1 triggered a breaking key migration. The integer values of all
domain constants changed (e.g. DOMAIN_IDENTITY went from 0 to
1660078172). This was handled:
muse auth recoverre-derived identity key at the new HD path- MuseHub local DB updated with the new fingerprint
muse code migrate --force-resignre-signed all commits in bothmuseandmusehubrepos with the new key- Remote tracking refs cleared manually (exposes a bug — see issue #20)
Note on musehub/services/musehub_repository.py: create_repo now
passes "muse/generic" (not "") as the domain hash input for repos with
no explicit domain, so compute_repo_id produces the correct hash-derived ID.
Implementation Phases
Phase 1 — Core ✅ DONE
domain_index(name: str) -> intadded tomuse/core/hdkeys.py- All
DOMAIN_*constants recomputed,DOMAIN_GENERICadded domain_indexexported in__all__- All tests updated and green
musehubcreate_repofixed to use"muse/generic"
Phase 2 — Registry repo + quorum enforcement ✅ DONE
gabriel/muse-domainsrepo created withregistry.json,schema.json,governance.json,GOVERNANCE.mdmusehub/services/musehub_governance.py—load_governance+check_quorumproposals.pymerge route blocks with 403 when quorum not met- 10 TDD tests covering all quorum paths — all green
- Committed and pushed to musehub dev
Phase 3 — Tooling 🔲 TODO
muse domain index— compute index for any namemuse domain lookup— resolve name ↔ index via registrymuse domain list— enumerate registered domainsmuse domain check— boolean registration checkmuse path annotate— decode a raw HD path to human-readable form
Phase 4 — Migration tooling 🔲 TODO
muse migrate domain-integers— automated key re-derivation and hub re-registration for users upgrading from pre-Phase-1 installs- Deprecation warnings on old integer constants in the interim
Open Questions
Name canonicalization — should
"Muse/Music"and"muse/music"hash to the same index? Recommend: always lowercase, always UTF-8, reject uppercase at registration time.Collision detection —
muse domain indexshould warn if the computed index is already registered to a different name.Offline registry — should
~/.muse/cache a local copy of the registry for air-gapped use? Probably yes — ship a snapshot with each Muse release, update onmuse fetch.Registry forks — should
muse domain listsupport--registry <url>to point at an alternate registry? Free if the registry is just a Muse repo.
Status update — 2026-05-01
Completed this session
Phase 1 ✅ — Hash-derived domain constants
domain_index(name)added tomuse/core/hdkeys.py- All
DOMAIN_*constants recomputed from canonical name strings DOMAIN_GENERICadded (was missing)musehubcreate_repofixed to pass"muse/generic"instead of""
Phase 2 ✅ — Registry repo + quorum enforcement
gabriel/muse-domainsrepo created withregistry.json,governance.json,schema.json,GOVERNANCE.mdmusehub/services/musehub_governance.py—load_governance+check_quorum- Proposal merge route returns 403 when quorum not met
- 10 TDD tests — all green
Phase 3 ✅ — Domain + path CLI commands
muse domain index <name>— compute hash-derived index for any namemuse domain lookup <name>/--index <n>— registry lookup + reverse lookupmuse domain list— enumerate all registered domainsmuse domain check <name>— exit 0/1 registration checkmuse path annotate <path>— decode raw HD path to human-readable form- Ships a seed registry of all 8 first-party domains for offline use
musehub bumped to v0.2.0rc5 — deployed to staging
Side fixes
- Editable-install /
install.shincompatibility documented ininstall.pymodule docstring - Remote tracking ref bug noted in issue #20 (
muse code migrate --force-resignmust clear.muse/remotes/)
Remaining
Phase 4 🔲 — muse migrate domain-integers
Automated migration command for users upgrading from pre-Phase-1 installs (where DOMAIN_IDENTITY was 0 instead of 1660078172). Should re-derive keys at the new HD paths and re-register with the hub.
Phase 4 complete — muse migrate domain-integers shipped
All four phases are done. Issue #3 is fully closed.
What shipped
# Inspect — no writes
muse migrate domain-integers --dry-run --json
# Migrate + re-register with all hubs
muse migrate domain-integers --json
Core library (muse.core.domain_migration):
is_legacy_hd_path— detects pre-Phase-1 sequential domain integers (0–6)new_path_for_legacy— maps old integer → new hash-derived HD pathderive_fingerprint_at_path— derives Ed25519 fingerprint at any HD pathscan_for_legacy/build_plans/run_migration— full migration driver
45 TDD tests — all green.
Commit: sha256:db00d09935852
Phase 3 complete — domain + path CLI commands shipped
All five commands from Phase 3 are now live on
dev:Registry loading order:
$MUSE_DOMAIN_REGISTRYenv var (custom path for air-gapped use)~/.muse/domain-registry.json(cached copy from hub)Commit:
sha256:53310e88ac8cRemaining: Phase 4 —
muse migrate domain-integersautomated migration tooling.