hip-0097

HIP-97: Node Identity and the did:hanzo: DID Method. Status Draft. Hanzo's own standard — read this before implementing against it.

HIP-0097: Node Identity and the did:hanzo: DID Method

Abstract

Every Hanzo node, profile, agent, and device is currently named with the Shinkai-legacy HanzoName format: a @@-prefixed label with a network suffix baked into the string, e.g. @@alice.sep-hanzo/main/agent/myChatGPTAgent. That string is signed into every HanzoMessage and is the lookup key for on-chain identity registration (Base Sepolia, contract 0x425fb20ba3874e887336aaa7f3fab32d08135ba9). It therefore is not a cosmetic label — it is consensus- and wire-critical.

This HIP proposes one clean replacement scheme: the canonical node identity is a did:hanzo:<name> Decentralized Identifier — the same W3C DID method this HIP specifies for every Hanzo subject. The legacy @@…<suffix> and the network-in-the-string design are retired. The network/chain is expressed as DID query parameters or resolved from the registry, never embedded in the human-readable name. A precise grammar and charset are defined, the signing and on-chain registration impact is specified, and a backward-compatible, three-phase migration and rollout is given so that no in-flight message or already-registered name breaks.

The same method serves every Hanzo subject — humans, agents, services and devices — so this HIP also specifies the DID Document a did:hanzo: identifier resolves to (§8), the resolution contract and lifecycle against the on-chain registry (§9), and the Verifiable Credential format that carries everything which is an attribute of a subject rather than its identity (§10).

Motivation

The @@<name>.<network>-hanzo format is inherited verbatim from Shinkai. It has four concrete problems, all of which touch code that is live today in hanzo-libs/hanzo-messages/src/schemas/hanzo_name.rs:

  1. Network is welded into the identity string. The valid endings are hard-coded:

.hanzo, .sepolia-hanzo, .arb-sep-hanzo, .sep-hanzo (HanzoName::VALID_ENDINGS). The same node on testnet vs. mainnet has a different identity string, a different signed sender, and a different registry key. Migrating a node from Base Sepolia to the Hanzo L1 mainnet (HIP-24, chain 36963) today means it is literally a different identity. This is the single worst property of the legacy scheme.

  1. @@ is non-standard and non-interoperable. It is not a URI, not a DID, not a

DNS name. Nothing outside Hanzo can resolve it. Meanwhile did:hanzo: is the W3C-compliant identity method for every other Hanzo subject — humans, agents, services and devices — with a W3C resolver and an on-chain registry (IHanzoDIDRegistry). Node identity and DID identity are needlessly two different namespaces.

  1. The format is already half-migrated, inconsistently. HanzoName already

accepts did:hanzo:<network> and did:lux:<network> (see is_did_format, validate_did_name), but only as a network selector (did:hanzo:mainnet, did:hanzo:sepolia, did:hanzo:local:node1) — the node's own name still has no home in that form, and a separate hanzo-libs/hanzo-did crate parses general W3C DIDs with yet another grammar. Three parsers, one concept.

  1. Charset and case rules are loose. The legacy regex

^@@[a-zA-Z0-9\_\.]+(\.hanzo|…)$ permits embedded dots in the node label (@@a.b.c.hanzo), mixes case then lowercases at construction, and silently auto-corrects (correct_node_name prepends @@ and appends .hanzo). Loose, auto-correcting identity parsing is a security smell for something that is signed.

Specification

1. Decision: did:hanzo:<name>, not <name>.hanzo

Two candidate clean schemes were considered:

did:hanzo:alice/main/agent/x.

This HIP selects B, did:hanzo:<name>. Rationale:

| Criterion | <name>.hanzo (A) | did:hanzo:<name> (B) — chosen | |---|---|---| | Aligns with existing standard | No — new third namespace | Yes — the did:hanzo: method itself | | Already parseable by HanzoName | No | Partly — did:hanzo: path exists today | | Network kept out of the name | Yes | Yes (query param / registry, never the label) | | Globally resolvable / W3C interop | No (looks like a DNS host but isn't) | Yes — resolves at did.hanzo.ai, did: URI scheme | | Collides with real DNS / TLD confusion | Yes (.hanzo reads as a gTLD) | No | | One identity across testnet/mainnet | Yes | Yes | | Verifiable Credentials, alsoKnownAs cross-chain (did:lux:, did:ai:) | No native hook | Native (§8, §10) |

A reads more cleanly to a human, but it manufactures a third identity grammar (after @@… and did:…) and looks deceptively like a DNS hostname while resolving through none of DNS. B collapses node identity into the DID method the ecosystem already standardized, makes node identity a first-class W3C DID, and gets cross-chain alsoKnownAs, Verifiable Credentials, and the did.hanzo.ai resolver for free. The human-friendly display form is recovered cheaply (see §6, UX) by rendering did:hanzo:alice as alice / @alice in UIs without changing the wire identity.

Note on did:hanzo:mainnet legacy usage: the existing code uses did:hanzo:<network> where the method-specific-id is a network. Under this HIP the method-specific-id is the node name, and the network moves to a query parameter (?chain=/?network=). did:hanzo:mainnet is reinterpreted during migration as the reserved node name mainnet on the default chain; deployments that used it purely as a network selector MUST migrate to did:hanzo:<node>?network=mainnet (see Migration §M0).

2. Canonical grammar (ABNF)

The canonical, signed, on-chain identity is the hanzo-id production:

hanzo-id     = did-node [ "/" profile [ "/" sub-type "/" sub-name ] ]

did-node     = "did:hanzo:" name [ params ]
name         = label *( "_" label )           ; node name, the method-specific-id
label        = lletter *( letterdigit )        ; MUST start with a letter
profile      = label *( "_" label )
sub-type     = "agent" / "device"
sub-name     = label *( "_" label )

params       = "?" param *( "&" param )        ; OPTIONAL; carried, never signed (§3)
param        = pkey "=" pval
pkey         = "network" / "chain" / "versionId" / "versionTime"
pval         = 1*( unreserved )

letterdigit  = lowletter / digit
lowletter    = %x61-7A                          ; a-z   (lowercase ONLY)
letter       = lowletter
digit        = %x30-39                           ; 0-9
unreserved   = lowletter / digit / "-" / "." / ":"

Equivalently, the node-name regex (replacing the legacy ^@@[a-zA-Z0-9\_\.]+(\.hanzo|…)$):

^did:hanzo:[a-z][a-z0-9]*(_[a-z0-9]+)*(\?[a-z]+=[a-z0-9.:-]+(&[a-z]+=[a-z0-9.:-]+)*)?$

Charset and length rules (normative)

[a-z][a-z0-9_]. No uppercase, no auto-lowercasing at parse time — a mixed-case input is rejected, not silently corrected. (The legacy lower-casing in HanzoName::new is removed; canonicalization is now the caller's* job before signing.)

.. Dots appear only inside params values (e.g. a chain RPC alias).

4-part structure (node / profile / agent|device / name) so downstream parsing in inbox_name.rs, tool_router_key.rs, etc. keeps the same split('/') shape.

(DNS-label-compatible bound, keeps registry keys bounded).

agent, device, did MUST NOT be used as a node name except for the reserved-mapping defined in Migration §M0. The default local dev identity becomes did:hanzo:localhost?network=local (replacing @@localhost.sep-hanzo).

Valid examples

did:hanzo:alice
did:hanzo:alice/main
did:hanzo:alice/main/agent/my_chatgpt_agent
did:hanzo:alice/main/device/my_phone
did:hanzo:alice?network=mainnet
did:hanzo:alice?chain=36963
did:hanzo:node1?network=sepolia          ; Base Sepolia during migration
did:hanzo:localhost?network=local

Invalid examples (and why)

@@alice.sep-hanzo                 ; legacy @@ form — rejected post-migration (accepted read-only in Phase 1)
did:hanzo:Alice                   ; uppercase
did:hanzo:al!ce                   ; illegal char
did:hanzo:a.b.c                   ; dot in node name
did:hanzo:_alice                  ; must start with a letter
did:hanzo:alice/main/myPhone      ; 3-part must be …/agent/… or …/device/…
did:hanzo:alice/main/agent        ; agent/device requires a 4th sub-name part
did:hanzo:alice.hanzo             ; no network/TLD suffix in the name

3. Network is a parameter, never part of the identity

The single most important rule: the chain/network is NOT part of the signed, on-chain identity. did:hanzo:alice is the same node whether it is registered on Base Sepolia (migration), the Hanzo L1 mainnet (chain 36963, HIP-24), or running locally.

external_metadata) and the value used as the registry key, the params are stripped. The canonical signed form is the bare did:hanzo:<name>[/path] with no query string.

same identity, the same signed sender, and the same registry key.

4. Signing impact

In hanzo_message_signing.rs, the name is not hashed field-by-field; rather external_metadata.sender / external_metadata.recipient (full HanzoName strings) are part of the message whose hash is signed with ed25519 (sign_outer_layer / sign_inner_layer). Therefore the exact bytes of the identity string are inside the signature preimage. Consequences:

  1. Changing a node's canonical string from @@alice.sep-hanzo to did:hanzo:alice

changes the signed bytes. A verifier expecting one form will fail signatures produced under the other. This is why a flag-day rename is unacceptable and why §7 defines an IdentityProtocolVersion negotiation.

  1. The canonicalization rule for the preimage is fixed by this HIP: the signed

sender/recipient MUST be the params-stripped, NFC, lowercase did:hanzo: form (§3). Because §2 forbids uppercase and auto-correction, canonicalization is now deterministic and side-effect-free — there is exactly one byte sequence per identity, removing the legacy ambiguity where @@Alice… and @@alice… could both be constructed and then lower-cased.

  1. PQ readiness. Outer/inner signatures stay ed25519 for wire compatibility in

this HIP. The did:hanzo:<name> DID Document MAY additionally carry an MLDSAVerificationKey2025 key (HIP-5, §8) so the same node identity can be verified post-quantum without another rename. PQ message signing is deferred to a follow-up but is unblocked by using the DID form now.

5. On-chain registration impact

Registration today goes through hanzo-bin/hanzo-node/src/managers/identity_network_manager.rs:

0x425fb20ba3874e887336aaa7f3fab32d08135ba9, via HanzoRegistry::get_identity_record(identity).

node_base.ends_with(".sepolia-hanzo") to decide proxy handling.

Changes required:

  1. Registry key becomes the canonical params-stripped did:hanzo:<name>. The

existing Base Sepolia registry contract is retained during Phase 1–2 (no contract redeploy needed to start), but the key string it stores transitions from @@alice.sepolia-hanzo to did:hanzo:alice. A node registers its DID form; the suffix-sniffing (.sepolia-hanzo) branch is replaced by reading the ?network=/?chain= hint (default sepolia during migration, 36963 after).

  1. Converge onto IHanzoDIDRegistry. The end-state registry is the

did:hanzo: registry on Hanzo L1 (chain 36963), which stores documentHash, controller, version, active per DID (§9). Node registration and DID registration become the same on-chain record: a node is a DID. This removes the separate Base Sepolia identity contract entirely at end-of-life (Phase 3).

  1. Dual-read during migration. The resolver MUST, given a query, attempt

resolution of both the new did:hanzo:<name> key and the legacy @@<name>.<suffix> key (deterministic mapping, §M1) so that a node which has re-registered under the DID form is still reachable by peers that only know its legacy name, and vice-versa.

6. UX / display form

To preserve readability lost by dropping the short @@alice look:

did:hanzo:alice/main/agent/researcher renders as alice › researcher.

(§8 cross-chain links). These are display/interop aliases, never the signed identity.

profile credential (the HanzoAgentCredential of §10), so the short name is a resolvable attribute, not part of the cryptographic identity.

7. HanzoName type changes (normative, Rust)

In hanzo-libs/hanzo-messages/src/schemas/hanzo_name.rs:

HanzoName, populated from params and excluded from Display, Hash, PartialEq, Serialize, and from the signing preimage (they are resolution-only).

fields profile_name, subidentity_type, subidentity_name are unchanged.

auto-correcting**: correct_node_name's @@/.hanzo injection is gated behind a legacy-compat flag and is off by default in Phase 2+.

envelope / handshake. A peer advertises which forms it can verify. Two Did peers sign/verify the DID form; if either side is Legacy-only, both fall back to the legacy string for that exchange (so signatures still match) until Phase 3.

HanzoName::new("did:hanzo:localhost/main?network=local").

canonical DID form after applying the §M1 legacy→DID mapping, so routing/dedup treat a legacy name and its migrated DID as one identity.

8. DID Document

A did:hanzo:<name> resolves to a document conforming to W3C DID Core 1.0. The identity is the identifier; the document is the key material and the endpoints it currently points at, and it MAY be rotated without the identity changing.

{
  "@context": [
    "https://www.w3.org/ns/did/v1",
    "https://w3id.org/security/suites/jws-2020/v1",
    "https://hanzo.ai/ns/did/v1"
  ],
  "id": "did:hanzo:dev",
  "controller": "did:hanzo:hanzo",
  "alsoKnownAs": [
    "did:lux:dev",
    "did:ai:dev"
  ],

  "verificationMethod": [
    {
      "id": "did:hanzo:dev#key-1",
      "type": "EcdsaSecp256k1VerificationKey2019",
      "controller": "did:hanzo:dev",
      "blockchainAccountId": "eip155:36963:0xAgentAddress"
    },
    {
      "id": "did:hanzo:dev#key-2",
      "type": "JsonWebKey2020",
      "controller": "did:hanzo:dev",
      "publicKeyJwk": {
        "kty": "OKP",
        "crv": "Ed25519",
        "x": "base64url-encoded-public-key"
      }
    },
    {
      "id": "did:hanzo:dev#key-pq",
      "type": "MLDSAVerificationKey2025",
      "controller": "did:hanzo:dev",
      "publicKeyMultibase": "z6Mk..."
    }
  ],

  "authentication": [
    "did:hanzo:dev#key-1",
    "did:hanzo:dev#key-2"
  ],

  "assertionMethod": [
    "did:hanzo:dev#key-1"
  ],

  "keyAgreement": [
    {
      "id": "did:hanzo:dev#key-agree-1",
      "type": "X25519KeyAgreementKey2020",
      "controller": "did:hanzo:dev",
      "publicKeyMultibase": "z6LS..."
    }
  ],

  "capabilityInvocation": [
    "did:hanzo:dev#key-1"
  ],

  "capabilityDelegation": [
    "did:hanzo:dev#key-1"
  ],

  "service": [
    {
      "id": "did:hanzo:dev#rpc",
      "type": "AgentRPCService",
      "serviceEndpoint": "https://bot.hanzo.ai/rpc/dev"
    },
    {
      "id": "did:hanzo:dev#wallet",
      "type": "SafeWallet",
      "serviceEndpoint": "safe:eip155:36963:0xSafeAddress"
    },
    {
      "id": "did:hanzo:dev#iam",
      "type": "OIDCProvider",
      "serviceEndpoint": "https://hanzo.id"
    }
  ]
}

The #key-2 ed25519 method is the node's message-signing key from §4, and #key-pq is the MLDSAVerificationKey2025 that makes the same identity verifiable post-quantum without a second rename.

Each verification relationship answers a different question, and a verifier MUST check the one that matches what it is about to allow:

| Relationship | Purpose | Example | |-------------|---------|---------| | authentication | Prove you are the DID subject | Node login to another node's API | | assertionMethod | Sign Verifiable Credentials | Agent attesting it completed a task | | keyAgreement | Establish encrypted channels | Node-to-node encrypted messaging | | capabilityInvocation | Invoke capabilities on resources | Agent executing a delegated action | | capabilityDelegation | Delegate capabilities to others | Organization granting agent authority |

A single key MAY appear in several relationships. Separate keys for authentication and keyAgreement are RECOMMENDED, so compromising one does not hand over the other.

9. Resolution

The registry record of §5 is authoritative: documentHash, controller, version, active, keyed by the params-stripped did:hanzo:<name>. The document itself lives off-chain, so a resolver MUST recompute SHA-256(document) and compare it against the registry documentHash before returning it. A document that does not match its on-chain hash MUST NOT be returned.

Resolution follows the W3C DID Resolution specification, and the versionId and versionTime params of §2 are its query parameters:

| Method | Endpoint | Description | |--------|----------|-------------| | GET | /1.0/identifiers/{did} | Resolve to the current document | | GET | /1.0/identifiers/{did}?versionId={n} | Resolve a specific version | | GET | /1.0/identifiers/{did}?versionTime={iso8601} | Resolve as of a point in time | | POST | /1.0/create | Register a DID | | POST | /1.0/update | Publish a new document version | | POST | /1.0/deactivate | Deactivate a DID |

The response carries the document plus resolution metadata: content type, created and updated timestamps, version, and the deactivated flag. Like every other resolution hint, these params are stripped from the signed and registry-key forms (§3).

The lifecycle is four operations:

constructs the document, computes documentHash, calls IHanzoDIDRegistry.register(did, documentHash), and publishes the document.

hash, and takes the verification method for the relationship it needs.

calls update(did, newDocumentHash). The version counter increments and prior versions stay in chain history, which is what makes versionId and versionTime answerable. Only the controller may update; control moves only via changeController().

deactivated=true. Credentials issued before deactivation remain historically verifiable, but a verifier MUST NOT accept a credential issued after it.

Deactivation is irreversible. A reactivated DID would leave verifiers unable to tell which validity window a credential belongs to; a subject that needs an identity after deactivation creates a new one.

10. Verifiable Credentials

Attributes that are not identity — capabilities, org membership, evaluation results — are Verifiable Credentials about the DID rather than fields inside it. IAM (HIP-26) is the issuer for identity and membership claims:

{
  "@context": [
    "https://www.w3.org/ns/credentials/v2",
    "https://hanzo.ai/ns/credentials/v1"
  ],
  "type": ["VerifiableCredential", "HanzoAgentCredential"],
  "issuer": "did:hanzo:iam",
  "issuanceDate": "2026-02-23T00:00:00Z",
  "expirationDate": "2027-02-23T00:00:00Z",
  "credentialSubject": {
    "id": "did:hanzo:dev",
    "type": "AIAgent",
    "name": "dev",
    "organization": "hanzo",
    "capabilities": [
      "code-generation",
      "code-review",
      "mcp-tool-use"
    ],
    "safetyEvaluation": {
      "framework": "HIP-0210",
      "result": "pass",
      "evaluatedAt": "2026-02-20T12:00:00Z"
    },
    "computeTier": "tier-3",
    "maxTokenBudget": 1000000
  },
  "credentialStatus": {
    "id": "https://did.hanzo.ai/credentials/status/1",
    "type": "StatusList2021Entry",
    "statusPurpose": "revocation",
    "statusListIndex": "42",
    "statusListCredential": "https://did.hanzo.ai/credentials/status-list/1"
  },
  "proof": {
    "type": "EcdsaSecp256k1Signature2019",
    "created": "2026-02-23T00:00:00Z",
    "verificationMethod": "did:hanzo:iam#key-1",
    "proofPurpose": "assertionMethod",
    "proofValue": "z58DAdFfa9SkqZMVPxAQpic7ndTn..."
  }
}

The proof.verificationMethod MUST appear in the issuer's assertionMethod set; a credential signed by a key the issuer never listed for assertion is invalid however well-formed it looks.

| Credential Type | Issuer | Subject | Purpose | |----------------|--------|---------|---------| | HanzoAgentCredential | IAM | Agent DID | Attest agent identity and capabilities | | SafetyEvaluationCredential | Safety framework (HIP-210) | Agent DID | Attest safety evaluation results | | OrganizationMembershipCredential | IAM | User or agent DID | Attest org membership and role | | ComputeAuthorizationCredential | Cloud (HIP-106) | Agent DID | Authorize compute resource access | | ModelTrainingCredential | Training pipeline | Model DID | Attest training data provenance | | BiasAuditCredential | Bias framework (HIP-220) | Model DID | Attest bias evaluation results |

Revocation is StatusList2021: a bitstring where flipping bit n revokes the credential holding statusListIndex n. A verifier downloads the whole list rather than asking about one credential, so checking revocation does not tell the issuer which credential is being verified.

Backward Compatibility & Migration

This is a signed, on-chain identifier; the migration is explicitly versioned and phased. No flag day.

M0 — Reserved-name and network-param remap (pre-flight)

Existing did:hanzo:<network> selectors (did:hanzo:mainnet, did:hanzo:sepolia, did:hanzo:local:node1) are reinterpreted:

did:hanzo:<node>?network=mainnet. Operators MUST update config to name the node.

M1 — Deterministic legacy ↔ DID mapping (the bridge)

A pure function maps any legacy name to its canonical DID form and back:

legacy:  @@<name>.<suffix>[/<profile>[/<type>/<sub>]]
canon:   did:hanzo:<name>[/<profile>[/<type>/<sub>]]  with params: network=<from suffix>

suffix → network/chain:
  .hanzo          → mainnet (chain 36963)
  .sep-hanzo      → sepolia (Base Sepolia)        ; current default
  .sepolia-hanzo  → sepolia (Base Sepolia)
  .arb-sep-hanzo  → arb-sepolia

<name> is copied verbatim iff it already satisfies §2 (lowercase, no dots). Legacy names containing dots or uppercase are flagged for operator-assisted rename (they are rare; the dominant case @@alice.sep-hanzo maps cleanly to did:hanzo:alice?network=sepolia). This mapping is the basis for dual-read (§5.3) and same_identity (§7).

Migration tooling

did:hanzo: form via §M1, re-registers it on-chain, and writes both keys until Phase 3.

did.hanzo.ai for the duration of Phases 1–2.

hanzo-libs/hanzo-messages/tests/hanzo_name_tests.rs so both forms hash to the expected signature inputs.

Rollout

  1. Land HanzoName changes (DID branch, hints, same_identity,

IdentityProtocolVersion) behind a feature flag; ship in a normal node release (Phase 1 behavior default).

  1. Update the ~60 call sites that build/validate HanzoName (identity manager,

inbox/tool-router keys, payments managers, tests) to use canonicalized DID construction helpers; behavior stays Legacy-signed.

  1. Ship resolver dual-read + hanzod identity migrate + did.hanzo.ai lookup

table.

  1. Flip default to Phase 2 once telemetry shows quorum of capable peers.
  2. Announce Phase 3 cutover height/date; freeze Base Sepolia contract; converge on

IHanzoDIDRegistry (chain 36963).

Security Considerations

auto-correction, each identity has exactly one canonical byte sequence; this closes the legacy ambiguity where multiple inputs canonicalized to the same lower-cased name, which is dangerous for a signed field.

auto-completed into a valid-looking one (legacy correct_node_name could turn a typo into a different, valid node).

registry forms means a signature or registration cannot be replayed as if it were for a different chain by swapping a suffix — the identity is chain-agnostic by construction.

where two strings denote one node; same_identity (§M1 mapping) MUST be used for all auth/routing decisions to prevent a peer from being treated as two identities (or two peers as one).

(HIP-5) to the same identity, so the eventual move to PQ message signing needs no further rename.

References