Skip to content

Midnight DID Specification Draft v0.7.0

Implementation guides: Quickstart, Key Model, and Compact Contract Surface.

Status of This Document

This is a draft document and may be updated, replaced, or obsoleted by other documents at any time. It is inappropriate to cite this document as anything other than a work in progress.

Version 0.6 targets the method-specific profile of the W3C DID Core 1.0 Recommendation, 19 July 2022. Its DID Core 1.0 evidence matrix combines repository tests with a pinned external fixture harness; that evidence is bounded and supplemental, not W3C certification or endorsement. Version 0.7 introduces breaking method-profile changes on top of that published baseline but does not broaden the bounded conformance evidence.

Version 0.6 does not claim conformance to W3C DID Core 1.1 Candidate Recommendation Snapshot, 05 March 2026 or the 2026 W3C DID Resolution v1 Candidate Recommendation Snapshot. The DID Core 1.1 compatibility matrix, 2026 DID Resolution CR compatibility matrix, and current resolution limitations record those failures; #447 owns the coordinated migration. Version 0.7 makes no broader conformance assertion before that migration and its evidence are complete.

Contributions

IAMX AG
Dammstrasse 16
6300 Zug
Switzerland
https://iamx.id/

Contact: contact@iamx.id

Midnight Foundation Contact: contact@midnight.foundation

Hyperledger Identus Contact: contact@identus.io

Contributors:

  • Yurii Shynbuiev
  • Dennis Mittmann
  • Pat Losoponkul
  • Fabio Pinheiro
  • Shailesh Patil

Abstract

This specification defines the Midnight DID method for storing DID state on the Midnight blockchain. Version 0.6 targets a method-specific profile of the dated W3C DID Core 1.0 Recommendation, subject to the bounded evidence and explicit exclusions in the status above. Version 0.7 changes Jubjub JWK coordinates to fixed-width big-endian transport encoding while preserving native ledger points and historical offchain DID hashes; it does not broaden the bounded conformance claim.

Contents

  1. Conformance and Terminology
  2. Midnight DID Syntax
  3. Midnight DID Document
  4. Midnight DID Document Metadata Properties
  5. Private and Public Keys
  6. Midnight DID Ledger state
  7. DID operations
  8. Security Considerations
  9. Privacy Considerations
  10. Discoverability
  11. Appendix

1. Conformance and Terminology

This specification assumes a fair degree of understanding of W3C-DID.

The keywords "MUST", "MUST NOT", "SHOULD", "MAY", and "OPTIONAL" in this document are to be interpreted as described in RFC2119.

2. Midnight DID Syntax

Midnight DID is a URI conforming to RFC3986 and SHOULD be generated by each entity itself.

Midnight DID is generated in conformity with W3C-DID.

The specific-idstring in a Midnight DID is a unique identifier for each entity. For ledger-backed networks, it is the 32-byte Midnight smart contract address encoded as 64 hexadecimal characters. Ledger contract identifiers MUST use lowercase hexadecimal. For the offchain network, the identifier is the 64-hex hash of the encoded offchain DID state and MUST also use lowercase hexadecimal; see Section 2.1 for the hash-only short form and self-contained long form. Resolvers and applications MUST canonicalize incoming ledger addresses, offchain state hashes, and DID identifiers to these lowercase forms before constructing or resolving a DID.

The ABNF grammar used to generate the Midnight DID identifier is as follows:

midnight-did = ledger-did / offchain-did
ledger-did = "did:midnight:" ledger-network ":" ledger-contract-identifier
ledger-network = "undeployed" | "devnet" | "testnet" | "mainnet" | "preview" | "preprod"
offchain-did = "did:midnight:offchain:" offchain-state-hash [ ":" offchain-state ]
ledger-contract-identifier = 64HEXDIGL
offchain-state-hash = 64HEXDIGL
offchain-state = 1*base64urlchar
HEXDIGL = DIGIT / %x61-66
base64urlchar = ALPHA / DIGIT / "-" / "_"

The regular expression for each ledger contract identifier and offchain state hash is:

/^[0-9a-f]{64}$/

Below is an example of a Midnight DID:

did:midnight:undeployed:c569622e7f33d2d020ba1cae242e6077268941327846d62d8cbf0cc923ae41f6

2.1. Offchain Midnight DID long form

Offchain Midnight DIDs identify a DID Document state without publishing that state to the Midnight ledger. This design is inspired by did:peer:4 portability: a long-form DID carries enough initial state to resolve without consulting a public registry, while the hash-only short form remains compact after the state is already known.

Midnight offchain DIDs differ from did:peer:4 because they must support Midnight cryptography and stay compatible with the ledger-backed Midnight DID smart-contract model. The encoded state therefore uses the Midnight offchain DID state model, supports Midnight verification key material such as Jubjub keys, and is hashed with the persistent state hash used by the reference implementation.

The canonical long form is a self-contained DID:

did:midnight:offchain:<persistent-hash-of-state>:<encoded-state>

The short form is:

did:midnight:offchain:<persistent-hash-of-state>

The long form is analogous to did:peer:4: the first offchain segment is a persistent hash of the encoded state, and the second segment carries the encoded state itself. A resolver that receives the long form MUST decode the state, recompute the hash, and reject the DID if the hash does not match. A resolver that receives only the short form needs the encoded state from local storage or resolver input metadata; the short form alone is not self-resolving.

The method-specific identifier for the long form is <persistent-hash-of-state>:<encoded-state>. The encoded state MUST use the Midnight offchain DID state encoding midnight-offchain-did-state-v1.base64url. In the current encoding, the state is a Compact-native binary representation wrapped as canonical unpadded base64url text using the alphabet defined by RFC 4648 Section 5. The encoded state is part of the DID method-specific identifier, not a DID URL query parameter.

2.1.1. Offchain state encoding v1

The offchain state hash is the lowercase hexadecimal BLAKE2s-256 digest of the decoded offchain state bytes. A resolver MUST decode the canonical unpadded base64url payload, compute BLAKE2s with dkLen = 32, lowercase-hex encode the digest, and compare it with offchain-state-hash. A mismatch MUST be rejected.

The decoded byte stream is framed as a Compact value chunk array:

text
magic              = %x4d.4f.44.31               ; "MOD1"
chunk-count        = uint32be
chunk              = chunk-length chunk-bytes
chunk-length       = uint32be
encoded-state-v1   = magic chunk-count *chunk

chunk-count is the number of Compact value chunks that follow. Each chunk-length is the number of bytes in the following chunk. A decoder MUST reject an invalid magic value, a chunk count that cannot fit in the remaining byte stream, a chunk whose declared length exceeds the remaining bytes, or trailing bytes after the last declared chunk.

The chunk payloads are the Compact runtime value emitted by the following descriptor, in order:

  1. version: Uint<16>.
  2. alsoKnownAs: Vector<4, Opaque<"string">> padded with empty strings.
  3. verificationMethod: Vector<4, OffchainVerificationMethod> padded with empty slots.
  4. service: Vector<4, OffchainService> padded with empty slots.

OffchainVerificationMethod is encoded as:

  1. present: Boolean.
  2. id: Opaque<"string">, a relative DID URL reference such as #key-1, /keys/a#key-1, or ?version=1#key-1. The projected DID Document resolves this value to an absolute DID URL.
  3. keyKind: Uint<8> using the table below.
  4. x: Opaque<"string">, the canonical JWK x value.
  5. y: Opaque<"string">, the canonical JWK y value, or the empty-string sentinel for OKP keys.
  6. relationshipsMask: Uint<8>.

For every key kind except Jubjub, the v1 offchain state encoding stores x and y as literal JWK strings. The historical keyKind = 1 Jubjub wire profile instead stores each coordinate as canonical unpadded base64url of exactly 32 little-endian bytes. This internal encoding remains fixed so existing long-form DID bytes and state hashes stay immutable. The 0.7 domain boundary MUST reverse each keyKind = 1 coordinate between that legacy wire representation and the fixed-width big-endian JWK representation defined in Section 3.4.4.2. Resolved DID Documents and decoded domain state therefore expose only canonical big-endian Jubjub JWK values, while re-encoding that state reproduces the original v1 payload. Implementations MUST NOT expose the internal little-endian string as a JWK or guess byte order by trying both interpretations.

OffchainService is encoded as:

  1. present: Boolean.
  2. id: Opaque<"string">, a relative DID URL service reference. The projected DID Document resolves this value to an absolute URL.
  3. type: Opaque<"string">.
  4. serviceEndpoint: Opaque<"string">.

Empty verification-method slots have present = false, empty strings for id, x, and y, and zero for keyKind and relationshipsMask. Empty service slots have present = false and empty strings for id, type, and serviceEndpoint.

Opaque<"string"> chunks are UTF-8 bytes. Boolean chunks are a single byte 0x01 for true and an empty chunk for false. Uint<8> and Uint<16> chunks use Compact unsigned-integer value bytes: little-endian, minimal length, with trailing zero bytes omitted and an empty chunk representing zero.

keyKindJWK profile
1kty: "EC", crv: "Jubjub"
2kty: "OKP", crv: "Ed25519"
3kty: "EC", crv: "P-256"
4kty: "OKP", crv: "X25519"
5kty: "EC", crv: "secp256k1"
6kty: "OKP", crv: "BLS12381G1"
7kty: "OKP", crv: "BLS12381G2"

The relationshipsMask bits are: authentication = 1, assertionMethod = 2, keyAgreement = 4, capabilityInvocation = 8, and capabilityDelegation = 16.

The maximum v1 state contains four alsoKnownAs entries, four verification methods, and four services. A state MUST contain at least one verification method. A decoder MUST reject unsupported keyKind values, invalid JWK material, empty or malformed method/service references, invalid base64url payloads, non-canonical base64url payloads, and any decoded state that does not satisfy the domain schema.

2.1.2. Offchain state test vector

The following test vector uses a single Jubjub verification method and one service:

json
{
  "version": 1,
  "alsoKnownAs": ["https://example.org/holders/alice"],
  "verificationMethod": [
    {
      "id": "#holder-key-1",
      "publicKeyJwk": {
        "kty": "EC",
        "crv": "Jubjub",
        "x": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA",
        "y": "AQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQE"
      },
      "relationships": {
        "authentication": true,
        "assertionMethod": true,
        "keyAgreement": false,
        "capabilityInvocation": false,
        "capabilityDelegation": false
      }
    }
  ],
  "service": [
    {
      "id": "#profile",
      "type": "LinkedDomains",
      "serviceEndpoint": "https://example.org/profile/alice"
    }
  ]
}

Encoded state payload:

text
TU9EMQAAAC0AAAABAQAAACFodHRwczovL2V4YW1wbGUub3JnL2hvbGRlcnMvYWxpY2UAAAAAAAAAAAAAAAAAAAABAQAAAA0jaG9sZGVyLWtleS0xAAAAAQEAAAArQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQQAAACtBUUVCQVFFQkFRRUJBUUVCQVFFQkFRRUJBUUVCQVFFQkFRRUJBUUVCQVFFAAAAAQMAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAQAAAAgjcHJvZmlsZQAAAA1MaW5rZWREb21haW5zAAAAIWh0dHBzOi8vZXhhbXBsZS5vcmcvcHJvZmlsZS9hbGljZQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA

Short-form DID:

text
did:midnight:offchain:3c08b85758d973a6002942c730d077ede51920c184927aaf010562035203fc21

Long-form DID:

text
did:midnight:offchain:3c08b85758d973a6002942c730d077ede51920c184927aaf010562035203fc21:TU9EMQAAAC0AAAABAQAAACFodHRwczovL2V4YW1wbGUub3JnL2hvbGRlcnMvYWxpY2UAAAAAAAAAAAAAAAAAAAABAQAAAA0jaG9sZGVyLWtleS0xAAAAAQEAAAArQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQQAAACtBUUVCQVFFQkFRRUJBUUVCQVFFQkFRRUJBUUVCQVFFQkFRRUJBUUVCQVFFAAAAAQMAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAQAAAAgjcHJvZmlsZQAAAA1MaW5rZWREb21haW5zAAAAIWh0dHBzOi8vZXhhbXBsZS5vcmcvcHJvZmlsZS9hbGljZQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA

A trailing colon without offchain-state is invalid. The long-form DID is the DID subject identifier for the resolved DID Document: resolved id, controller, and verification method controller fields use the long form so the subject remains fully self-sufficient. This intentionally differs from did:peer:4, where the resolved document commonly uses the short form as id and represents the long form as an alias.

Propertydid:peer:4did:midnight:offchain
Portability modelLong-form DID carries encoded input document; short form is the hash over that long form.Long-form DID carries encoded Midnight offchain state; short form is the persistent hash of that state.
Public registry dependencyNone for long-form resolution.None for long-form resolution.
Encoded payloadDID-like input document with relative identifiers.Midnight offchain DID state that can be projected into a DID Document.
Cryptography profileGeneral DID Document key material used by peer DID implementations.Midnight-compatible key material, including Jubjub-oriented verification methods and smart-contract-compatible state shape.
Hashing roleIntegrity and compact short form.Integrity, compact short form, and compatibility with Midnight persistent state semantics.
Resolved DID Document identifierResolved document commonly uses the short form as id, with the long form available as an alias.Resolved document uses the long form as id so the DID subject remains self-contained.
Update modelNo in-place update; rotate or exchange a new DID.No ledger mutation for offchain state; publish/exchange a new long-form DID state when the state changes.

3. Midnight DID Document

Each Midnight DID will have a corresponding Midnight DID Document, which is a set of data describing this Midnight DID according to the W3C-DID specification.

Below is the basic structure of the Midnight DID Document:

json
{
  "@context": [
    "https://www.w3.org/ns/did/v1",
    "https://w3id.org/security/jwk/v1"
  ],
  "id": "did:midnight:undeployed:c569622e7f33d2d020ba1cae242e6077268941327846d62d8cbf0cc923ae41f6",
  "controller": "did:midnight:undeployed:c569622e7f33d2d020ba1cae242e6077268941327846d62d8cbf0cc923ae41f6",
  "alsoKnownAs": [
    "did:example:aka-2"
  ],
  "verificationMethod": [
    {
      "id": "#key-1",
      "type": "JsonWebKey",
      "controller": "did:midnight:undeployed:c569622e7f33d2d020ba1cae242e6077268941327846d62d8cbf0cc923ae41f6",
      "publicKeyJwk": {
        "kty": "OKP",
        "crv": "Ed25519",
        "x": "VCpo2LMLhn6iWku8MKvSLg2ZAoC-nlOyPVQaO3FxVeQ"
      }
    }
  ],
  "authentication": [
    "#key-1"
  ],
  "assertionMethod": [
    "#key-1"
  ],
  "keyAgreement": [],
  "capabilityInvocation": [],
  "capabilityDelegation": [],
  "service": []
}

3.1. Context

A Midnight DID document MUST include a @context property.

The value of the @context property MUST be an array containing the following URIs, in order:

  1. https://www.w3.org/ns/did/v1 (reference to the context of the DID Core specification v1)
  2. https://w3id.org/security/jwk/v1 (reference to the security vocabulary context that defines JsonWebKey and type-scoped publicKeyJwk)

For more information, see W3C-DID.

3.2. Identifier

Midnight DID documents MUST include an id property.

The value of the id property denotes the Midnight DID subject that the Midnight DID Document is about.

The value of id MUST be a valid Midnight DID. A Midnight DID MUST have exactly one DID subject.

The Midnight DID identifier is available right after the smart-contract deployment and equals the smart-contract address.

json
{
  "id": "did:midnight:undeployed:c569622e7f33d2d020ba1cae242e6077268941327846d62d8cbf0cc923ae41f6"
}

3.3. Alias

A DID subject can have multiple identifiers for different purposes, or at different times. The assertion that two or more DIDs (or other types of URI) refer to the same DID subject can be made using the alsoKnownAs property.

The alsoKnownAs property is OPTIONAL. If present, the value MUST be a set where each item in the set is a URI conforming to RFC3986. This relationship is a statement that the subject of this identifier is also identified by one or more other identifiers.

3.4. Verification Methods

A Midnight DID Document MAY include a verificationMethod property to specify a set of public keys linked to that Midnight DID.

Public and private key pairs can be used for the identity management, authorization, and verification of Midnight DID. A Midnight DID can be linked to multiple public and private key pairs, and one pair of public and private keys can also be used to manage multiple Midnight DIDs.

Every public key object linked to the verificationMethod property MUST include the fields id, type, controller, and specific public key properties, and MAY include other additional properties.

3.4.1. id

Each linked public key has its own identifier specified using the field id. The value of verificationMethod MUST NOT contain multiple entries with the same canonical id. New mutation inputs MUST use a subject-bound DID URL with a non-empty fragment, or a relative reference that resolves to one (for example, #key-1 or /keys/a#key-1). For read compatibility, Midnight DID Document parsing and ledger rendering also accept historical root-path and dot-relative identifiers without fragments (for example, /keys/key-1 and ./keys/key-1) and their canonical subject-bound absolute forms. Midnight resolves relative references against the DID subject and preserves path, query, and fragment components. Canonical relation comparison and resolved Document output use the resulting absolute DID URL. This compatibility does not admit bare labels, query-only method ids, network-path references, foreign-DID ids, or external URLs. Current mutation helpers do not update or remove path-only physical ledger keys; deployments that must migrate them require replacement DID state with fragment-bearing ids or purpose-built ledger migration tooling that targets the exact historical key.

3.4.2. type

The value of the type field MUST be JsonWebKey. Midnight uses this name to identify verification methods whose public key material is carried in the publicKeyJwk property and encoded according to RFC7517 JSON Web Key (JWK) rules.

Midnight does not emit JsonWebKey2020. JsonWebKey2020 is associated with older vc-jws-2020 / JSON-LD context naming, while the Midnight DID method uses the https://w3id.org/security/jwk/v1 context that defines the verification material form directly as JsonWebKey with type-scoped publicKeyJwk. Consumers that require a JsonWebKey2020 term or context mapping MUST adapt the resolved DID Document at the integration boundary; the canonical Midnight DID Document representation remains JsonWebKey.

3.4.3. controller

The value of the controller field, which identifies the controller of the corresponding private key MUST be a valid Midnight DID, implying that the public key is controlled by this Midnight DID. For Midnight DID, the canonical resolved representation sets controller equal to the DID subject. The controller value is derived during ledger-to-document reconstruction and is not stored as a separate on-ledger field.

3.4.4. publicKeyJwk

The value of the publicKeyJwk field conforms to the RFC7517 JSON Web Key (JWK) specification, and contains the following properties:

  • kty - key type
  • crv - curve
  • x - canonical unpadded base64url-encoded public key material or x coordinate, with the byte length defined by the supported key profile
  • y - canonical unpadded base64url-encoded y coordinate when required by the key type

Public JWKs MUST NOT contain private key material such as d.

The Midnight DID supports the following cryptographic algorithms: Ed25519, X25519, Jubjub (Midnight compatible), P-256, secp256k1, BLS12-381 G1, and BLS12-381 G2. Ed25519, X25519, P-256, and secp256k1 use established JOSE/JWK curve names. Jubjub is a Midnight-private curve name for native SchnorrJubjub methods. BLS12381G1 and BLS12381G2 follow draft BLS JOSE/COSE key-representation naming and are interoperability-limited until the relevant standards and library support stabilize. Generic JOSE libraries that validate crv against only the established JWK curve registry can reject the Midnight-private and draft curve names.

Based on the cryptography suite, the values of the properties are as follows:

3.4.4.1 Ed25519

Uses EdDSA over Ed25519 for signatures. Keys are represented as JWK in compressed format with:

  • kty=OKP,
  • crv=Ed25519, and
  • x parameter.
3.4.4.2 Jubjub (Midnight compatible)

Uses Schnorr over Jubjub for signatures inside Midnight's ZK context (smart contract and Midnight JS library). Keys are represented as uncompressed EC JWKs:

  • kty=EC,
  • crv=Jubjub,
  • x, and
  • y parameters.

Each x and y value MUST be the canonical unpadded base64url encoding of exactly 32 octets containing the unsigned coordinate in big-endian order, left-padded with zero octets when necessary. The decoded integer MUST be less than the following Jubjub base-field modulus:

text
52435875175126190479447740508185965837690552500527637822603658699938581184513

This follows the EC JWK coordinate convention in RFC7518 Sections 6.2.1.2 and 6.2.1.3 and the field-element octet-string conversion in SEC1 Sections 2.3.5 and 2.3.7. Implementations MUST NOT use a minimal-width integer encoding, reduce an out-of-range value, or infer little-endian order from Midnight's native field representation.

Jubjub keys are stored on ledger as native JubjubPoint values in the schnorrJubjubVerificationMethods map. Resolvers project those native points into DID Document publicKeyJwk entries using the fixed-width big-endian transport encoding above. The native ledger point, contract arithmetic, and Schnorr transcript are unaffected by this representation boundary.

Reference implementation versions through 0.6 emitted the same coordinates as fixed-width little-endian bytes. Version 0.7 corrects this transport representation. Persisted 0.6 DID Document snapshots, JWK thumbprints, JWK-derived key identifiers, and caches are not byte-compatible with the 0.7 output; consumers must re-resolve ledger DIDs and rebuild derived indexes rather than auto-detecting an unmarked byte order.

crv=Jubjub is Midnight-private and is not a registered JOSE curve name. It is intended for Midnight-native SchnorrJubjub signing and verification flows, not for generic JOSE/JWK verification libraries. Big-endian coordinates align the x and y semantics with RFC 7518 but do not make this private curve generally supported by JOSE implementations.

Current Midnight DID mutation circuits do not parse opaque JWK coordinate strings in contract code. SDK producers MUST use the SchnorrJubjub verification method API for Jubjub keys so the native JubjubPoint is the on-ledger source of truth. Jubjub signing and verification flows use the dedicated jubjub-schnorr package rather than additional exported circuits on the DID contract.

This split is required by the current Compact language and proving environment. Compact circuits do not provide a general-purpose JWK/base64url parser suitable for reconstructing arbitrary public keys from opaque strings, and arbitrary 32-byte JWK coordinates are not uniformly safe to cast into Compact Field values because field casts are bounded by the native proof-system field. Midnight DID therefore stores interoperability-first JWK material as opaque canonical strings for non-Jubjub profiles, while storing Jubjub keys as native JubjubPoint values when those points must remain usable by Midnight-native cryptographic flows.

3.4.4.3 X25519

Uses X25519 keys for key agreement. Keys are represented as JWK with:

  • kty=OKP,
  • crv=X25519, and
  • x parameter.

X25519 keys are stored and resolved as DID Document key material; they are not used by current Midnight DID smart-contract verification circuits.

3.4.4.4 P-256

Uses NIST P-256 keys represented as JWK with:

  • kty=EC,
  • crv=P-256,
  • x, and
  • y parameters.

P-256 keys are stored and resolved as DID Document key material; they are not used by current Midnight DID smart-contract verification circuits.

3.4.4.5 secp256k1

Uses secp256k1 keys represented as JWK with:

  • kty=EC,
  • crv=secp256k1,
  • x, and
  • y parameters.

secp256k1 keys are stored and resolved as DID Document key material; they are not used by current Midnight DID smart-contract verification circuits.

3.4.4.6 BLS12-381 G1 and G2

Uses BLS12-381 key material for pairing-friendly cryptographic suites outside the current Midnight DID smart-contract verification circuits. The Midnight DID JWK profile follows the current IETF BLS JOSE/COSE key-representation draft IETF-BLS-KEY-REPRESENTATIONS:

  • kty=OKP,
  • crv=BLS12381G1 with x containing a 48-byte compressed serialized G1 public key, or
  • crv=BLS12381G2 with x containing a 96-byte compressed serialized G2 public key.

BLS12-381 JWK public keys MUST omit y and d. They are stored and resolved as DID Document key material; they are not used by current Midnight DID smart-contract verification circuits. The contract stores the BLS public key as an opaque canonical string, so the key can be larger than the Midnight proof-system field without casting the bytes to Field.

The BLS12381G1 and BLS12381G2 names are draft/provisional JWK curve names. Midnight DID preserves these names for explicit BLS12-381 key storage, but generic JOSE libraries can reject them until matching standards and implementation support are available.

Some W3C Data Integrity and BBS-oriented suites require type: "Multikey" with publicKeyMultibase. DID Core permits that verification material form, but a verification method MUST NOT contain both publicKeyJwk and publicKeyMultibase for the same key material W3C-DID. The current Midnight DID ledger profile supports JsonWebKey / publicKeyJwk only. Multikey / publicKeyMultibase verification methods are not supported by this method version.

3.5. Verification Relationships

In a Midnight DID Document, according to the DID Core verification-relationships section DID-CORE-VERIFICATION-RELATIONSHIPS, the following verification relationships are supported:

  • assertionMethod
  • authentication
  • capabilityInvocation
  • capabilityDelegation
  • keyAgreement

The verification relationships are represented by the corresponding properties by referencing the verification method id defined in the DID Document. Empty verification relationships MUST be omitted from the DID Document.

Embedded verification method definition is not supported by the Midnight DID method.

3.5.1. Assertion Method

The assertionMethod verification relationship is used to specify how the DID subject is expected to express claims, such as for the purposes of issuing a Verifiable Credential VC-DATA-MODEL.

The assertionMethod property is OPTIONAL. If present, the associated value MUST be a set of one or more verification methods. Each verification method MUST be referenced.

Example:

json
{
  "assertionMethod": [
    "#key-1"
  ]
}

3.5.2. Authentication

The authentication verification relationship is used to specify how the DID subject is expected to be authenticated, for purposes such as logging into a website or engaging in any sort of challenge-response protocol.

The authentication property is OPTIONAL. If present, the associated value MUST be a set of one or more verification methods. Each verification method MUST be referenced.

Example:

json
{
  "authentication": [
    "#key-1"
  ]
}

3.5.3. Capability Invocation

The capabilityInvocation verification relationship is used to specify a verification method that might be used by the DID subject to invoke a cryptographic capability, such as the authorization to update the DID Document.

The capabilityInvocation property is OPTIONAL. If present, the associated value MUST be a set of one or more verification methods. Each verification method MUST be referenced.

Example:

json
{
  "capabilityInvocation": [
    "#key-1"
  ]
}

3.5.4. Capability Delegation

The capabilityDelegation verification relationship is used to specify a mechanism that might be used by the DID subject to delegate a cryptographic capability to another party, such as delegating the authority to access a specific HTTP API to a subordinate.

The capabilityDelegation property is OPTIONAL. If present, the associated value MUST be a set of one or more verification methods. Each verification method MUST be referenced.

Example:

json
{
  "capabilityDelegation": [
    "#key-1"
  ]
}

3.5.5. KeyAgreement

The keyAgreement verification relationship is used to specify how an entity can generate encryption material in order to transmit confidential information intended for the DID subject, such as for the purposes of establishing a secure communication channel with the recipient.

The keyAgreement property is OPTIONAL. If present, the associated value MUST be a set of one or more verification methods. Each verification method MUST be referenced.

Example:

json
{
  "keyAgreement": [
    "#key-1"
  ]
}

3.6. Services

Services are used in DID documents to express ways of communicating with the DID subject or associated entities. A service can be any type of service the DID subject wants to advertise, including decentralized identity management services for further discovery, authentication, authorization, or interaction.

Services are expressed using the service property, which is described below:

3.6.1. Service

The service property is OPTIONAL. If present, the associated value MUST be a set of services, where each service is described by a map. Each service map MUST contain id, type, and serviceEndpoint properties.

3.6.1.1. Id

The value of the id property MUST be a valid absolute URL or a relative DID URL that resolves against the DID subject. For example, valid values include did:midnight:<network>:<addr>#service-1, #service-1, /routing, ?service=messaging, and an absolute non-DID URL such as https://example.com/service. New service writes using an absolute DID URL MUST use the current Midnight DID subject; the API rejects foreign-DID service identifiers. Ledger resolution has one narrow compatibility exception: it preserves foreign-DID service ids already present in historical ledger state and validates the reconstructed document under that same read-only policy. Midnight resolves relative values using RFC3986 reference-resolution cases and preserves path, query, and fragment components. Canonical storage, duplicate comparison, API mutation, and DID Document resolution use the resulting full absolute URL; /a#service and /b#service are distinct identifiers. A conforming producer MUST NOT emit multiple service entries with the same canonical id, and a conforming consumer MUST produce an error if duplicate id values are detected.

3.6.1.2. Type

The value of the type property MUST be either:

  • a string; or
  • a set represented as an array of unique strings.

When persisted on-ledger, Midnight serialises a multi-value set as a JSON array string and rehydrates it during DID Document reconstruction. In order to maximize interoperability, the service type and its associated properties SHOULD be registered in the DID Specification Registries DID-SPEC-REGISTRIES.

3.6.1.3. ServiceEndpoint

The value of the serviceEndpoint property MUST comply with the DID-CORE-SERVICES definition. In particular, it MAY be:

  • a string that is a valid URI conforming to RFC3986;
  • an object (map) whose members describe transport data (for example, DIDComm service metadata as profiled in CID-1.0); or
  • a set represented as a non-empty array of unique strings and/or objects of the above forms.

When persisted on-ledger, Midnight serialises the serviceEndpoint value as a JSON string so that all conforming representations can be recovered when reconstructing the DID Document. Endpoint values in each service are normalized and MUST be unique; URI-equivalent strings and structurally equivalent maps count as the same set member. The same endpoint MAY appear in different services.

Example of the service property:

json
{
  ...
  "service": [
  {
    "id": "#didcomm-1",
    "type": "SomeServiceType",
    "serviceEndpoint": [
      "https://localhost/sst",
      { "uri": "wss://localhost/sst", "routingKeys": ["did:example:mediator"] }
    ]
  }
  ]
}

4. Midnight DID Document Metadata Properties

The Midnight DID Document metadata supports the following properties according to DID-CORE-DOCUMENT-METADATA:

4.1. Created

The property created represents the controller/prover-asserted date when the DID state was created.

For ledger-backed Midnight DIDs, created is copied from the currentTimestamp witness supplied during contract deployment. It is stored on ledger after the deployment transaction succeeds, but it is not derived from Midnight consensus time, block time, indexer time, or any other ledger-authoritative clock. Resolvers and SDKs can sanity-check this value for obviously invalid or implausible values, such as dates before the supported epoch/profile minimum or far-future dates outside a local policy window, but they cannot prove the exact wall-clock creation time from DID state alone.

4.2. Updated

The property updated represents the controller/prover-asserted date when the DID state was last updated.

For ledger-backed Midnight DIDs, updated is copied from the currentTimestamp witness supplied to each successful mutating circuit. It is stored on ledger after the transaction succeeds, but it is not ledger-authoritative operation time metadata. Resolvers and SDKs can reject, omit, warn on, or otherwise mark values that are obviously too low, too high, unparsable, non-monotonic for a local observation, or outside an application policy window; they cannot convert the value into a consensus timestamp without independent time attestation.

4.3. Deactivated

The property deactivated is a boolean value that represents the status of a Midnight DID.

A deactivated Midnight DID can no longer be used or reactivated.

4.4. VersionId

The property versionId is a string representing the constantly increasing number of the updates applied to the current Midnight DID.

5. Private and Public Keys

There are three types of DID Keys associated with the Midnight DID:

5.1. ZK keys

Midnight DID ZK prover, verifier, and ZKIR artifacts are generated from the Compact source during build or release. They are contract/runtime artifacts, not DID controller keys.

The keys are generated by the compact CLI (compact compile) during the compilation phase:

  • <circuit>.prover - ZK private key
  • <circuit>.verifier - ZK public key
  • <circuit>.bzkir - binary ZK intermediate representation

Each exported circuit (e.g., setVerificationMethod, setService, deactivate) has its own prover/verifier key pair. These artifacts MUST be available to the smart-contract runtime context used to deploy the contract and execute circuits. Possessing the ZK artifacts does not authorize DID updates; controller-gated updates are authorized by wallet-held private state as defined in section 5.2.

Implementations SHOULD distribute runtime ZK artifacts in a bundle that preserves the provider layout:

text
keys/<circuit>.prover
keys/<circuit>.verifier
zkir/<circuit>.bzkir

This layout is important because Midnight JS ZK config providers resolve artifacts by circuit id from keys/ and zkir/ roots. Package publication and ZK artifact publication are separate concerns: TypeScript packages expose the contract/API surface, while the ZK artifact bundle provides the proving material needed to deploy and call exported circuits.

5.2. Smart-contract access control

Smart-contract access control is independent from the ZK prover/verifier keys described in section 5.1. A Midnight DID controller is authorized by wallet-held private state, not by possession of the Compact prover artifacts.

The contract uses wallet-local Jubjub Schnorr signatures for controller-gated operations. The wallet or SDK creates a random 32-byte controller secret, derives a Jubjub controller public key, and stores only the public key on ledger as controllerPublicKey.

For each controller-gated operation, the wallet signs a domain-separated authorization digest with four field lanes:

  • a domain hash for midnight-did-ctrl-sig:v1,
  • a DID state hash over the DID contract id and current ledger version,
  • an operation-name hash, and
  • an operation-arguments hash.

The proof submitted to the contract carries the signature and expected version. The circuit recomputes the operation and argument hashes from the public arguments it is about to apply, verifies the signature against the stored controllerPublicKey, and rejects stale versions before mutating DID state. The controller secret is not a circuit witness and MUST NOT be sent to delegated proving infrastructure.

The controller secret is not stored on ledger. SDKs MUST persist it in the wallet's private-state storage. The contract also stores a dedicated recoveryAuthorityPublicKey: JubjubPoint. The recovery authority is not a DID Document verification method and is not a second active controller. It can only authorize recoverControllerKey, which rotates controllerPublicKey after verifying a recovery-authority Schnorr signature bound to the DID contract id, current version, recovery operation name, and replacement controller public key.

Wallets SHOULD back up the active controller private state and the recovery authority private state separately. Private state created before this recovery authority surface has no recovery authority secret: it can still authorize ordinary controller-gated operations for a compatible contract, but it cannot perform recoverControllerKey unless the recovery secret is imported or supplied explicitly. Deployments of contractVersion = 2 MUST initialize a recovery authority secret. Loss of the controller secret makes ordinary controller-gated updates, controller rotation, and deactivation impossible until recoverControllerKey is used with the recovery authority. Loss of both the controller secret and recovery authority secret makes subsequent DID updates impossible. The current contract does not provide multi-controller, threshold, social-recovery, recovery-authority rotation, or emergency-deactivation circuits.

Controller rotation is performed with a locally derived replacement Jubjub controllerPublicKey. The replacement controller secret is generated locally by the wallet or SDK. Only the new controllerPublicKey, the current-version controller signature, and the expected version are supplied to the circuit. After the rotation transaction finalizes, the SDK MUST persist the new secret as the DID private state while preserving any stored recovery authority secret. See Appendix 11.1 for the proof-server trust boundary.

5.3. Keys associated with the DID Document

Midnight DID Controllers MUST manage the keys associated with the DID Document. Implementations MAY generate DID controller keys and DID Document verification keys randomly, or MAY derive them from wallet/private-state seed material with an implementation-defined HD convention. Implementations that use HD derivation SHOULD domain-separate Midnight DID material from payment, dust, metadata, and other wallet roles, and SHOULD document their path/index allocation so backups and recovery flows can reproduce the same keys.

This version of the specification does not standardize a single HD derivation path for Midnight DID controller keys or DID Document verification keys. A future version may define a BIP32/BIP44/CIP1852-like convention, or reserve Midnight HD wallet roles, for deriving all Midnight DID keys from a single seed.

6. Midnight DID Ledger state

The following table summarizes the on-chain ledger state exported by the contract and how each field should be interpreted when reconstructing a DID Document and its metadata.

FieldTypeDescription
contractVersionUint<32>Contract schema/compatibility discriminator. It identifies the ledger/circuit shape a consumer expects; it is not a promise that the deployed contract can be upgraded in place.
controllerPublicKeyJubjubPointActive controller public key used to verify wallet-local controller authorization signatures.
recoveryAuthorityPublicKeyJubjubPointDedicated recovery authority public key used only by recoverControllerKey to rotate controllerPublicKey. It is not emitted as a DID Document verification method.
idContractAddressSmart‑contract address (32‑byte / 64‑hex) that uniquely identifies the DID on Midnight.
alsoKnownAsSet<Opaque<"string">>The DID Document’s alsoKnownAs field; allows to set the alias for the DID identity.
versionCounterMonotonic on-chain revision counter for the DID state. Must be set to the versionId property of the DIDDocument.
createdUint<64>Client/controller-asserted creation timestamp (UNIX epoch, milliseconds) supplied by the currentTimestamp witness during deployment. Exposed as the DID Document Metadata created property (ISO 8601 UTC, second precision) after resolver/SDK sanity checks. This is not ledger-authoritative operation time.
updatedUint<64>Client/controller-asserted last update timestamp (UNIX epoch, milliseconds) supplied by the currentTimestamp witness during the most recent mutating operation. Exposed as the DID Document Metadata updated property (ISO 8601 UTC, second precision) after resolver/SDK sanity checks. This is not ledger-authoritative operation time.
deactivatedBooleanWhether the DID has been deactivated. When true, the resolver surfaces deactivated: true in metadata and reuses the updated timestamp as the deactivation time.
activeBooleanWhether the DID is active (true) or deactivated (false). If active is false, the resolver MUST set deactivated: true in DID Document metadata.
operationCountCounterTotal number of DID update operations applied to this DID. Used for internal statistics.
verificationMethodsMap<Opaque<"string">, VerificationMethod>Non-Jubjub verification methods. New records are keyed by full canonical absolute DID URL; compatible legacy records can use the exact current-subject #fragment key described in Section 6.1. Values store opaque canonical JWK material. Resolver output retains the complete identifier.
verificationMethods[*].publicKeyJwk.xOpaque<"string">Canonical unpadded base64url JWK x. Byte lengths: Ed25519, X25519, P-256, and secp256k1 use 32; BLS12381G1 uses 48; BLS12381G2 uses 96.
verificationMethods[*].publicKeyJwk.yOpaque<"string">EC y coordinate. OKP entries store an empty string sentinel; resolver output omits y.
schnorrJubjubVerificationMethodsMap<Opaque<"string">, SchnorrJubjubVerificationMethod>SchnorrJubjub methods. New records use full canonical absolute DID URL keys; exact legacy current-subject fragment keys remain operable. Resolver output merges them into verificationMethod as JsonWebKey entries with crv: "Jubjub".
schnorrJubjubVerificationMethods[*].publicKeyJubjubPointNative Jubjub public key point (Field x, Field y). Canonical on-ledger representation for Jubjub methods.
authenticationRelationSet<Opaque<"string">>Set of physical verification-method keys authorized for authentication. New entries use full canonical identifiers; compatible legacy entries retain the target method's exact fragment key. The DIDDocument's authentication property is reconstructed as a canonical URL.
assertionMethodRelationSet<Opaque<"string">>Set of physical verification-method keys authorized for assertionMethod, with the same canonical/legacy compatibility rule.
keyAgreementRelationSet<Opaque<"string">>Set of physical verification-method keys authorized for keyAgreement, with the same canonical/legacy compatibility rule.
capabilityInvocationRelationSet<Opaque<"string">>Set of physical verification-method keys authorized for capabilityInvocation, with the same canonical/legacy compatibility rule.
capabilityDelegationRelationSet<Opaque<"string">>Set of physical verification-method keys authorized for capabilityDelegation, with the same canonical/legacy compatibility rule.
servicesMap<Opaque<"string">, Service>Service definitions. New records use full canonical absolute URL keys; exact legacy current-subject fragment keys remain operable. The serviceEndpoint value is stored as a JSON string so that any DID Core-compliant representation can be rehydrated. Resolver output retains canonical absolute service URLs.

6.1. Canonical identity and legacy physical keys

Canonical logical identity is always the complete resolved URL. New verification-method, verification-relationship, and service records use that canonical value as the physical ledger key. Earlier deployments can contain a #fragment physical key for the same did:midnight:...#fragment logical identity.

Before an update, removal, relationship operation, service operation, or ledger-bound Schnorr verification, the SDK checks both the canonical physical key and this one exact legacy alias. It MUST use the sole existing physical key in the circuit input and authorization digest. If both forms exist, it MUST fail as ambiguous rather than selecting one. New inserts MUST reject an occupied legacy alias and MUST write the canonical key. A legacy alias MUST NOT be derived for path, query, foreign-DID, or external URL identities; for example, did:midnight:.../routing is not an alias for the historical fragment identity did:midnight:...#/routing. The deprecated four-argument Schnorr verifier performs this state-aware lookup when its contract handle was created by the Midnight DID API (deploy, createDID, or joinContract); unregistered third-party handles retain the deprecated historical fragment-key fallback and SHOULD migrate to the provider-aware overload.

This lookup preserves existing subject-fragment deployments without automatic state migration or redeployment. The verificationMethods and schnorrJubjubVerificationMethods maps share one logical identifier namespace. Resolvers MUST reject duplicate identifiers across the two maps after full absolute-DID-URL normalization, and relation sets MAY target entries from either map. The two maps are not duplicate storage for the same key material: each verification method is stored in exactly one representation.

Because Compact treats Opaque<"string"> identifiers as opaque values, DID URL reference resolution and subject binding are SDK/resolver responsibilities in the 0.6 ledger schema. The TypeScript API resolves and subject-binds references before submission. Resolvers reject foreign verification-method subjects instead of emitting them and reject identifiers that collide after full canonical normalization.

6.2. Contract version, maintenance authority, and migration

contractVersion is a schema and compatibility discriminator. The 0.6 contract constructor writes 2; consumers can use that value when deciding whether the ledger shape and circuit surface are compatible with the software they are running. The field does not dispatch an upgrade, promise forward compatibility, or authorize a state transition merely because a higher version exists.

A deployed Compact contract also has a Contract Maintenance Authority (CMA), which is separate from the DID controller and recovery authority. The upstream deployContract interface accepts an optional signing key; when it is omitted, the runtime generates a fresh signing key, installs its public authority as the CMA, and stores the signing key through PrivateStateProvider.setSigningKey under the deployed contract address. CMA possession authorizes contract-maintenance operations exposed by the Midnight runtime. It does not authorize controller-gated DID Document mutations, and controller or recovery secrets do not substitute for a lost CMA signing key.

Implementations MUST treat the CMA signing key as separate high-value custody material. The upstream private-state provider interface deliberately separates exportPrivateStates() from exportSigningKeys(): private-state export does not include signing keys.

The following export details are bounded to that upstream interface and the pinned reference Level private-state provider, levelPrivateStateProvider from @midnight-ntwrk/midnight-js-level-private-state-provider; other provider implementations can define different custody surfaces. In the reference provider, exportSigningKeys() exports the account/provider-wide signing-key set, not only the CMA key for one DID or deployed contract.

An operational backup that needs future maintenance capability MUST therefore protect the whole encrypted export at the highest sensitivity of any included key, supply an explicit independent high-entropy export password, and store the export and that password separately. The reference setup's default private-storage password is derived from wallet secret material; it MUST NOT be reused as the export password or treated as a low-sensitivity backup password. Although the reference interface permits omission of an explicit export password and then falls back to its storage password provider, applications following this custody profile MUST NOT rely on that fallback.

Loss of the CMA key (or of the only usable export/password) removes the ability to authorize future maintenance with that authority; it does not by itself deactivate the DID or prevent ordinary controller-authorized updates. Loss of controller/recovery material has the distinct consequences described in section 8.1.

Version 0.6 provides no generic in-place contract migration or upgrade system. Runtime maintenance interfaces and CMA custody do not automatically transform DID ledger schema, rewrite stored identifiers, or migrate a deployment to a newer DID contract. The compatibility reads in section 6.1 preserve narrowly defined historical records without migration. Any future schema transition requires an explicitly designed, version-specific migration or replacement-deployment procedure with its own authorization, state mapping, and validation; applications MUST NOT infer such a procedure from contractVersion alone.

Example DID Document metadata emitted by the resolver layer:

json
{
  "created": "2024-01-01T09:30:00Z",
  "updated": "2024-01-15T14:12:05Z",
  "deactivated": false,
  "versionId": "5"
}

Metadata trust note: created and updated are public ledger fields, but their values originate from the controller/prover's currentTimestamp witness. A resolver or SDK MAY apply profile-specific bounds and monotonicity checks before surfacing them, but consumers MUST treat them as advisory DID Document Metadata unless a separate application, indexer, ledger event, or timestamping service supplies an independent time attestation.

7. DID operations

The publisher of the smart contract is the DID Controller, who keeps the private keys associated with the corresponding smart contract and the private keys for the public key material of the DID Document.

All update operations are performed by executing one of the smart contract circuits corresponding to the desired modification.

The created and updated ledger fields are populated from the currentTimestamp witness during contract deployment and each successful update circuit call. DID Document metadata is then composed by the resolver layer from these on-ledger values (created, updated, deactivated, version).

The currentTimestamp value is client-asserted metadata, not a Midnight consensus timestamp. A conforming implementation MUST NOT treat created or updated as proof that an operation occurred at a particular wall-clock time unless an application adds an independent time attestation. Resolvers and SDKs MAY sanity-check the values for obviously too-low or too-high timestamps, malformed conversions, and local monotonicity expectations, but those checks only bound implausible values; they do not make the metadata ledger-authoritative. Resolvers SHOULD surface the values as DID Document Metadata only and SHOULD document that they are controller/prover-supplied.

Each API update operation maps to one circuit call; this method version defines no batch circuit. This design choice is separate from a hard Midnight transaction constraint: transaction contract-call sections cannot be merged, so separately submitted contract calls cannot be combined into one atomic transaction. Application workflows that compose multiple DID operations MUST treat them as independently finalized, non-atomic state transitions and MUST own ordering, ledger re-reads, and retry reconciliation.

7.1. Create

Creating a DID involves deploying the corresponding smart contract instance to the Midnight blockchain.

To deploy the smart-contract instance, the following prerequisites MUST be met:

  • Smart-contract prover and verifier ZK-keys MUST be generated
  • Controller and recovery authority private keys MUST be available to the deploying wallet or SDK
  • The initial controller public key MUST be distinct from the recovery authority public key

After the smart-contract publishing, the Midnight DID is deployed, but doesn't contain the public information. It's still resolvable and contains the following properties:

  • id the smart-contract address
  • created - the creation timestamp (ISO 8601 UTC string with second precision)

7.2. Read

Reading of a Midnight DID Document is done by attaching to the corresponding smart contract by the address in the Midnight DID in the network specified by the network segment of the DID ID. The smart contract public state contains all information required to reconstruct the DID Document. See Section 6. Midnight DID Ledger state.

There are a couple of options to get the DID Ledger state:

7.2.1 Midnight Ledger and Runtime TypeScript libraries

The example of the Midnight DID resolution is available in the api module of the current repository.

The Midnight JS library is used to attach to the smart-contract ledger state, and the TypeScript package contract is used to deserialize the state and reconstruct the DIDDocument associated with the DID.

7.2.2 Midnight GraphQL API in the Midnight Indexer

The smart-contract ledger state can be fetched by id. The ledger state is deserialized to reconstruct the corresponding DIDDocument.

The configured indexer/public-data provider is a trusted input to this resolution profile. A null result can be interpreted as notFound only under that provider trust assumption. An unavailable provider, an exception, a stale or unfinalized response, or a response from an untrusted endpoint does not prove that the DID is absent, deactivated, or final. The 0.6 resolver does not run a light client and does not independently verify an indexer state proof or consensus finality. Block-height or block-hash selection, where an upstream provider exposes it, identifies the requested read point but does not by itself add independent proof verification.

Example of the implementation: Midnight DID Resolver in Rust

NOTE: The DID ledger state will be available within the smart-contract compact language as well when the support for the smart-contract composability is implemented.

7.2.3. Resolution response composition and media types

Version 0.6 profile note: This shipped profile FAILS the pinned 2026 DID Core 1.1 and DID Resolution Candidate Recommendation snapshots. It documents the current DID Core 1.0-era API and is not a compatibility claim. Version 0.6 exposes a bare-document resolve(did) plus separate resolution-result and representation helpers, negotiates application/did+json and application/did+ld+json, emits https://www.w3.org/ns/did/v1, returns keyword-string errors, and keeps a deactivated DID Document readable while marking its metadata. The 2026 CRs instead require application/did and the v1.1 context, the unaltered resolve(did, resolutionOptions) contract, structured errors with W3C URL-valued type fields, and a null DID Document after deactivation. These breaking changes are deliberately not backported to 0.6; #447 owns the coordinated migration. The rules below describe only the shipped 0.6 behavior.

The version 0.6 response-composition APIs described here are:

  • resolve(did) accepts only the DID, returns the bare MidnightDIDDocument, and throws when the DID is not found. It does not accept resolution options and does not return a resolution envelope.
  • resolveDIDResolutionResult(did) is the separate envelope helper. It returns didResolutionMetadata, didDocument, and didDocumentMetadata; successful results have empty resolution metadata with no contentType.
  • resolveRepresentation(did, options) is the separate stream helper. Its optional options.accept value selects the preferred DID Document representation. It returns didResolutionMetadata, didDocumentStream, and didDocumentMetadata; successful results include the selected contentType.

HTTP adapters MUST enforce their normal deployment-appropriate header field-size limits and runtime-validate inbound Accept values before passing them to the representation helper. The package-level parser intentionally imposes no separate arbitrary resource cap.

The version 0.6 operations compose responses as follows:

Request modeRequested media typeResponse bodyDID resolution metadata
resolve(did)noneBare MidnightDIDDocument; throws when the DID is not foundNone; this operation does not return an envelope
resolveDIDResolutionResult(did)noneObject containing didDocument, didResolutionMetadata, and didDocumentMetadataEmpty object on success; no contentType
resolveRepresentation(did, options)omitted or application/did+ld+jsonDID Document byte stream serialized as JSON-LD{ "contentType": "application/did+ld+json" }
resolveRepresentation(did, options)application/did+jsonDID Document byte stream serialized as DID Core JSON{ "contentType": "application/did+json" }
HTTP/service envelopeapplication/jsonDID Resolution Result object encoded as JSONHTTP response Content-Type is application/json; do not copy this value into didResolutionMetadata.contentType for the abstract envelope helper
HTTP/service envelopeapplication/ld+jsonDID Resolution Result object encoded as JSON-LD, when the service supports a JSON-LD envelopeHTTP response Content-Type is application/ld+json; do not copy this value into didResolutionMetadata.contentType for the abstract envelope helper

If resolveRepresentation receives an unsupported representation request, it returns didResolutionMetadata.error = "representationNotSupported" with no didDocumentStream and does not read the ledger.

Failure responses from resolveDIDResolutionResult and resolveRepresentation set didResolutionMetadata.error to a keyword string. They use invalidDid, notFound, and representationNotSupported for those DID Core-defined cases and use keywords such as methodNotSupported and internalError for broader resolver failures. In contrast, public resolve(did) throws instead of returning error metadata. This implementation uses the following deterministic malformed-ledger-state policy:

Ledger projection failure0.6 keyword
Malformed, non-canonical, or wrong-length profile-specific JWK coordinate; non-empty OKP yinvalidPublicKey
Malformed service, alias, verification-method subject, or DID URL referenceinvalidDid
Verification-method identifiers that collide after canonicalization, within or across physical storesnotAllowedLocalDuplicateKey
Unexpected provider, runtime, or programmer failureinternalError

For these failures, resolveDIDResolutionResult returns a null didDocument and resolveRepresentation returns a null didDocumentStream; both carry the keyword in didResolutionMetadata.error. The convenience resolve(did) API retains its throwing behavior. Resolver-specific extension values are documented as single ASCII keywords that start with a letter. These diagnostics classify projection of the configured reader's response; they do not authenticate that response or prove freshness or finality.

The method document profile requires @context in resolved Midnight DID Documents. Producers of application/did+ld+json MUST include it. Producers of application/did+json MUST follow DID Core JSON production rules and MUST omit the representation-specific @context member. This keeps the JSON and JSON-LD media types distinct while preserving the required context in the canonical Midnight DID Document and its JSON-LD representation.

7.3. Update

Updating the Midnight DID implies that the DID Controller calls one of the smart contract's individual circuits for each type of modification.

Each update circuit requires a controller Schnorr signature over the DID contract id, current version, operation name, and operation arguments, verified against the on-chain controllerPublicKey. The currentTimestamp witness is used to populate the updated ledger field after each successful operation. The value is not constrained by ledger time and therefore remains informational metadata that can only be sanity-checked by resolvers and SDKs for obviously implausible values.

For an operation targeting an existing verification method or service, the SDK resolves canonical identity to the sole existing canonical or compatible legacy physical key according to Section 6.1. The selected physical key is used consistently for state preflight, authorization, and circuit submission. Canonical and legacy records for the same identity are an error, not a precedence rule.

The TypeScript API is the mandatory supported integration boundary for updates. Integrators MUST NOT submit exported Compact mutation circuits directly. Such a call is still controller-authorized, versioned, and argument-bound, but it bypasses SDK validation and can store opaque values outside the resolver's accepted document domain.

The 0.6 validation boundary has three categories:

  1. Enforced by the current contract. Controller authorization, expected version, exact physical identifier existence/uniqueness (including an exact raw key shared by the opaque and native stores), supported opaque JWK kty/crv profiles, native SchnorrJubjub point storage, relation compatibility, and state-transition invariants.
  2. Expressible as a bounded Compact defense but deferred in 0.6. For a supported OKP profile, equality against the empty Opaque<"string"> sentinel could reject non-empty y. Adding that defense changes circuit artifacts and was not justified for the compatibility-frozen 0.6 release and is not delivered by the 0.7 coordinate-codec change. Future contract hardening is tracked separately for 0.8. Resolver and SDK validation remain normative at this boundary.
  3. SDK/resolver-only with the current schema, or requiring a future migration. Base64url parsing/canonicality, URI and JSON service parsing, DID-subject binding, and canonical identifier normalization require rich string/data-model processing unavailable for opaque values. Enforcing profile-specific coordinate lengths in contract requires replacing or profile-tagging opaque coordinates with fixed-size byte representations; fragment-only identifiers and structured services likewise require a ledger schema/circuit design and migration. These remain separate post-0.6 work and are not delivered by this coordinate-codec change.

The resolver applies the malformed-ledger-state policy in Section 7.2.3 and fails closed: foreign verification-method subjects are rejected rather than emitted, and same-store or cross-store canonical aliases are rejected as notAllowedLocalDuplicateKey. Raw state is durable but is not necessarily permanent. While the contract is active and controller custody remains, an operator can repair it through supported operations: remove every verification relationship that references an offending physical verification method before removing or updating that method, or update/remove an offending service. Each repair is separately finalized, so the operator MUST re-read trusted finalized state between steps. If controller custody is lost or the contract is inactive, 0.6 provides no automatic repair or migration path.

Each update operation is implemented by a small set/toggle circuit surface in the did.compact contract:

  • rotateControllerKey - rotates the DID controller commitment to a locally derived controller public key
  • recoverControllerKey - rotates the DID controller commitment using a dedicated recovery authority signature
  • setVerificationMethod - adds or updates an opaque JWK verification method according to a MapMutation.Insert or MapMutation.Update value
  • removeVerificationMethod - removes an opaque JWK verification method
  • setSchnorrJubjubVerificationMethod - adds or updates a native SchnorrJubjub verification method according to a MapMutation.Insert or MapMutation.Update value
  • removeSchnorrJubjubVerificationMethod - removes a native SchnorrJubjub verification method
  • verifySchnorrJubjubDigestSignature - verifies a SchnorrJubjub digest signature against the native public key stored under a verification method id
  • setVerificationMethodRelation - adds or removes a verification method relationship according to a SetMutation.Insert or SetMutation.Remove value
  • setService - adds or updates a service endpoint according to a MapMutation.Insert or MapMutation.Update value
  • removeService - removes a service endpoint
  • setAlsoKnownAs - adds or removes an alternative identifier according to a SetMutation.Insert or SetMutation.Remove value
  • deactivate - deactivates the DID

Keeping the exported circuit count low is a deployment requirement, not just a packaging preference. Every exported Compact circuit produces proving/verifier artifacts and contributes to the deploy transaction footprint. A symmetric add/update/remove circuit for every logical API helper can exceed current standalone Midnight block limits. The public TypeScript API can still expose ergonomic add/update/remove helpers, but those helpers SHOULD map onto the compact set/toggle circuit surface where possible. The circuit mutation enums are intentionally explicit so that the compact surface remains small without relying on ambiguous boolean flags. The single SchnorrJubjub verifier is intentionally ledger-bound: it takes a verification method id and reads the public key from schnorrJubjubVerificationMethods, avoiding caller-supplied-key verification that could drift from DID state. It is a transaction-backed proof circuit rather than an off-chain verifier so the proof is tied to current ledger state while the digest and signature remain private circuit inputs.

Each mutating circuit increments the version counter and updates the updated timestamp. verifySchnorrJubjubDigestSignature is non-mutating and MUST NOT change DID version metadata.

The circuit implementations are in packages/contract/src/did.compact, and the API helpers that call these circuits are in packages/api/src/lib.ts.

Controller rotation and recovery notes:

  • rotateControllerKey accepts only the next controllerPublicKey, not the next secret; authorization is supplied as a current-version controller signature. The next controller public key MUST differ from the current controller public key and from the recovery authority public key.
  • recoverControllerKey accepts only the next controllerPublicKey, a recovery-authority signature, and the expected version. The recovery authority can rotate the active controller key but cannot mutate DID Document content, verification methods, services, aliases, deactivation state, or the recovery authority itself.
  • The API helper generates a new 32-byte secret, persists it in a pending private-state slot, derives the next public key locally with the contract package's deriveControllerPublicKey helper, and submits the rotation or recovery transaction. After finalized transaction data returns, it promotes the pending secret to active private state and clears the pending slot.
  • If callTx is invoked but submission or finality throws before returning finalized transaction data, the helper MUST retain pending state because a thrown receipt/finality stream cannot distinguish rejection from successful on-chain finalization. After connectivity is restored, the application MUST obtain trusted finalized ledger state and re-read the on-ledger controllerPublicKey before retrying; reconnection or the first available read alone MUST NOT be treated as proof of non-finalization. If the replacement key is the finalized current key and the retained secret derives it, the application can promote the pending state with the public recovery helper by passing the providers and { contractAddress, rotationFinalized: true }. The rotationFinalized value is a caller assertion; the helpers do not query ledger state or add a finality or freshness guarantee to the configured public data provider. The candidate MUST be discarded with { contractAddress, rotationFinalized: false } only after authoritative reconciliation confirms that the operation did not finalize; until then it MUST remain pending even when an available read still shows the old key. If failure is definitely before callTx invocation, the helper MUST attempt to clear the just-created candidate inside the held lease. A cleanup rejection MUST warn that deletion disposition is unknown and retain explicit discard guidance for a record that remains.
  • While pending controller state exists, a later rotation or recovery MUST fail before authorization or a transaction-call attempt with PendingControllerPrivateStateExistsError (pending_controller_private_state_exists) instead of replacing that state. Reservation acquisition is fail-fast: rotation, recovery, promotion, or discard racing an unresolved owner MUST immediately fail with PendingControllerPrivateStateBusyError (pending_controller_private_state_busy) without mutation or queueing. An unresolved owner MUST remain busy until underlying work is cancelled and the owner operation settles, the operation otherwise terminates or settles, or the process exits. The reservation MUST NOT expire merely because time elapsed: stale provider or transaction work could later complete and overwrite, promote, or remove state owned by a newer operation. After cancellation or termination, the application MUST reconcile ledger and private state before another mutation.
  • Public rotation and recovery MUST auto-bind or assert the operation's canonical contract address, and public promotion/discard reconciliation MUST require contractAddress. The supported API baseline assumes one application writer process per DID. API-bound wrappers for one DID share the process-local critical section from preflight through pending persistence, transaction settlement, promotion, and cleanup. Join MUST acquire the same fail-fast owner-token lease before binding, reserve its source and target addresses, and hold them through private-state loading and deployed-contract lookup. Competing lifecycle or binding operations on either address MUST fail busy before mutation, and join failure MUST release its owned keys. A known different idle binding MUST fail with PrivateStateProviderContractMismatchError. Provider-object fallback is only for internal/deep unbound use. Direct provider mutation and independently unbound wrappers are outside this guarantee. Multiple application processes that intentionally write the same DID are outside the supported baseline and MUST use a distributed lock or equivalent fencing mechanism because the provider interface has no cross-process atomic conditional write.
  • Pending promotion MUST load a valid pending controller state. A missing or malformed candidate fails without mutation with PendingControllerPrivateStateUnavailableError (pending_controller_private_state_missing_or_malformed). Pending discard with { rotationFinalized: false } MUST remove any non-null pending record, including malformed state, because the caller has independently asserted non-finalization; an absent record MUST fail with the same typed unavailable error.
  • If the transaction finalizes but active private-state persistence fails, the wallet must recover the same pending secret to continue updating the DID. If explicit pending promotion writes active state but pending cleanup rejects, it MUST warn and return the promoted state while stating that the pending record MAY remain or MAY already have been removed. Later reconciliation processes retained state or returns PendingControllerPrivateStateUnavailableError if deletion committed.
  • Implementations that bypass the API and submit an arbitrary newControllerPublicKey are responsible for retaining the matching preimage. Losing the matching secret makes subsequent DID updates impossible unless the recovery authority remains available.

7.3.1 Add Verification Method

Adds a new verification method entry and (optionally, in a subsequent operation) links it to one or more verification relationships.

  • Inputs: verificationMethod object with id, type, controller, and publicKeyJwk fields.
  • Constraints:
    • id MUST be either a DID URL bound to this DID (for example, did:midnight:<network>:<addr>#key-1) or a relative identifier that resolves against the DID (for example, #key-1). The SDK resolves references before ledger submission and preserves the complete absolute DID URL for storage and resolver output.
    • controller MUST equal the DID subject.
    • type MUST be JsonWebKey.
    • publicKeyJwk MUST follow the opaque JWK profiles defined in section 3.4.4 for Ed25519, X25519, P-256, secp256k1, BLS12381G1, or BLS12381G2. Jubjub keys MUST use the SchnorrJubjub verification method API because their canonical ledger representation is a native JubjubPoint.
    • Adding a method with an existing id MUST fail.

Example (Ed25519):

typescript
await addVerificationMethod(didContract, providers, {
  id: '#key-1',
  type: 'JsonWebKey',
  controller: 'did:midnight:testnet:c569622e7f33d2d020ba1cae242e6077268941327846d62d8cbf0cc923ae41f6',
  publicKeyJwk: {
    kty: 'OKP',
    crv: 'Ed25519',
    x: 'AQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQE'
  }
});

Ledger normalization note:

  • The API maps type to the contract field typ.
  • publicKeyJwk.x / publicKeyJwk.y are validated as canonical unpadded base64url values with profile-specific byte lengths, then stored as Opaque<"string"> ledger values.
  • For OKP keys (Ed25519, X25519, BLS12381G1, BLS12381G2), y is omitted at API level and normalized to an empty string sentinel in ledger storage.
  • API-level addVerificationMethod is for opaque JWK profiles (Ed25519, X25519, P-256, secp256k1, BLS12381G1, and BLS12381G2). For Jubjub, use addSchnorrJubjubVerificationMethod with a native JubjubPoint.
  • API-level addVerificationMethod and updateVerificationMethod call the contract's setVerificationMethod circuit with MapMutation.Insert and MapMutation.Update, respectively.
  • addSchnorrJubjubVerificationMethod and updateSchnorrJubjubVerificationMethod call the contract's setSchnorrJubjubVerificationMethod circuit and store the entry in schnorrJubjubVerificationMethods, avoiding duplicate opaque/native Jubjub key storage.
  • verifySchnorrJubjubDigestSignature verifies SchnorrJubjub signatures by method id. It looks up the native public key in schnorrJubjubVerificationMethods, so the verification proof is tied to the current ledger state.

Example (SchnorrJubjub):

typescript
await addSchnorrJubjubVerificationMethod(didContract, providers, {
  id: '#key-jubjub-1',
  publicKey: {
    x: 12345n,
    y: 67890n
  }
});

Example (ledger-bound SchnorrJubjub verification):

typescript
await verifySchnorrJubjubDigestSignature(
  didContract,
  providers,
  '#key-jubjub-1',
  digestVector4,
  signature
);

7.3.2 Update Verification Method

Replaces the stored definition of an existing verification method (same id).

  • Inputs: verificationMethod object with the same id and updated key material or properties.
  • Constraints:
    • The method id MUST already exist; otherwise, the update MUST fail.
    • Relationships that refer to this id remain valid after the update.
    • Jubjub updates MUST use updateSchnorrJubjubVerificationMethod with a native JubjubPoint.

Example:

typescript
await updateVerificationMethod(didContract, providers, {
  id: '#key-1',
  type: 'JsonWebKey',
  controller: 'did:midnight:testnet:c569622e7f33d2d020ba1cae242e6077268941327846d62d8cbf0cc923ae41f6',
  publicKeyJwk: {
    kty: 'OKP',
    crv: 'Ed25519',
    x: 'CA'
  }
});

7.3.3. Remove Verification Method

Deletes a verification method by its id.

  • Inputs: id — the verification method identifier.
  • Constraints:
    • Removing a non-existent method MUST fail.
    • Removing a method that remains in any verification relationship MUST fail. The Compact circuit checks all five relation sets for both opaque JWK and native SchnorrJubjub stores, including direct calls that bypass API preflight.
    • Removing a Jubjub method MUST use removeSchnorrJubjubVerificationMethod, which follows the same single-operation semantics.
    • The API MUST NOT remove relationships implicitly. Its state preflight throws VerificationMethodReferencedError with code verification_method_referenced, the selected physical methodId, and referenced relationships in canonical order before authorization/submission.

Applications explicitly remove selected relationships first, then remove the method. Each call is an independently finalized transaction. After a partial or ambiguous failure, the application MUST re-read current state and submit only outstanding operations; no purge/delete sequence is atomic or automatically resumable.

Example:

typescript
await removeVerificationMethodRelation(
  didContract,
  providers,
  VerificationMethodRelationType.Authentication,
  '#key-1'
);
await removeVerificationMethod(didContract, providers, '#key-1');

7.3.4 Add Verification Relation

Associate an existing verification method methodId with a DID Core verification relationship.

  • Inputs: relation ∈ { Authentication, AssertionMethod, KeyAgreement, CapabilityInvocation, CapabilityDelegation }, methodId.
  • Constraints:
    • methodId MUST refer to an existing verification method and MUST be expressed as a subject-bound DID URL or a relative reference resolving to its complete identifier (e.g., #key-1 or /keys/a#key-1). Path, query, and fragment components are preserved for equality.
    • Adding the same relation twice MUST fail.
    • The verification method curve MUST be compatible with the requested relationship. X25519 methods are key-agreement methods and MUST only be added to KeyAgreement. Signing-capable methods (Ed25519, P-256, secp256k1, BLS12381G1, BLS12381G2, and native SchnorrJubjub/Jubjub) MUST NOT be added to KeyAgreement; they are valid for Authentication, AssertionMethod, CapabilityInvocation, and CapabilityDelegation.

Example:

typescript
await addVerificationMethodRelation(didContract, 'Authentication', '#key-1');

7.3.5 Remove Verification Relation

Removes a verification method methodId from a DID Core verification relationship.

  • Inputs: relation, methodId.
  • Constraints:
    • Removing an unknown pair (relation, methodId) MUST fail. Explicit relation removal remains non-idempotent; applications retrying a multi-step cleanup MUST re-read state and skip pairs already removed.

Example:

typescript
await removeVerificationMethodRelation(didContract, 'Authentication', '#key-1');

7.3.6 Add Service

Adds a service entry identified by a unique id with a type and serviceEndpoint.

  • Inputs: service with fields id, type, serviceEndpoint.
  • Constraints:
    • id MUST be unique across services and MUST be a valid absolute URL or a relative DID URL reference (for example, #service-1, /routing, or ?service=messaging). Relative values are resolved against the DID and the complete absolute URL is used for storage, comparison, and resolved output.
    • type MUST be either a string or an array of unique strings.
    • serviceEndpoint MUST be encodable as JSON and MUST conform to the DID-CORE-SERVICES data model (string, object, or array of strings/objects). The value is persisted as a JSON string on-ledger.

Example:

typescript
await addService(didContract, providers, {
  id: '#didcomm-1',
  type: 'DIDCommV2',
  serviceEndpoint: 'https://localhost/didcomm/v2'
});

7.3.7 Update Service

Replaces the service definition with the same id.

  • Inputs: service with fields id, type, serviceEndpoint.
  • Constraints:
    • The service id MUST already exist; otherwise, the update MUST fail.
    • serviceEndpoint rules are the same as for add (DID Core–compliant JSON value).

Example:

typescript
await updateService(didContract, providers, {
  id: '#didcomm-1',
  type: 'DIDCommV2',
  serviceEndpoint: 'https://new-endpoint.com/didcomm'
});

7.3.8 Remove Service

Deletes a service entry by serviceId.

  • Inputs: serviceId — the service identifier.
  • Constraints:
    • Removing a non-existent service MUST fail.

Example:

typescript
await removeService(didContract, providers, '#didcomm-1');

7.3.9. Add AlsoKnownAs

Adds an alias URI to the alsoKnownAs set. See section 3.3 for semantics.

  • Inputs: aliasUri — a URI conforming to RFC3986 (e.g., a DID or other URI).
  • Constraints:
    • Adding a duplicate alias MUST fail.
    • Implementations SHOULD validate that aliasUri is a syntactically valid URI.

Example:

typescript
await addAlsoKnownAs(didContract, providers, 'did:example:aka-1');

7.3.10. Remove AlsoKnownAs

Removes an alias URI from the alsoKnownAs set.

  • Inputs: aliasUri — the alias URI to remove.
  • Constraints:
    • Removing an alias that does not exist MUST fail.

Example:

typescript
await removeAlsoKnownAs(didContract, providers, 'did:example:aka-1');

7.3.11. Recover Controller Key

Rotates the active controller public key using the dedicated recovery authority. This is a break-glass recovery operation for loss of the active controller secret; it is not a general DID Document update authority.

  • Inputs:
    • newControllerPublicKey — replacement Jubjub controller public key.
    • recoverySignature — Schnorr signature by the private key corresponding to recoveryAuthorityPublicKey.
    • expectedVersion — caller-expected current DID version.
  • Effects:
    • Replaces controllerPublicKey with newControllerPublicKey.
    • Increments the DID version and updates updated.
  • Constraints:
    • The recovery signature MUST be bound to the DID contract id, expected version, recovery operation name, and newControllerPublicKey.
    • Stale expectedVersion values MUST fail.
    • The new controller public key MUST differ from the current controller public key and from recoveryAuthorityPublicKey.
    • The circuit MUST NOT update DID Document verification methods, verification relationships, services, aliases, deactivation state, or recoveryAuthorityPublicKey.
    • Recovery after deactivation MUST fail.

SDKs MAY load the recovery secret from contract-scoped private state or accept it as an explicit call argument. Explicitly supplied recovery secrets MUST be used only for the recovery authorization unless the caller separately imports them into private-state storage; implementations SHOULD preserve an already stored recovery secret only when it matches the on-ledger recovery authority while promoting the recovered controller secret.

Example:

typescript
await recoverControllerKey(didContract, providers);

7.3.12. Deactivate

Marks the DID as deactivated on-chain. The public state remains readable for auditability, but no further update operations are permitted.

  • Inputs: none.
  • Effects:
    • Sets active = false in the ledger state.
  • Implementations SHOULD surface the contract’s deactivated flag (and accompanying updated timestamp) via DID Document metadata as described in section 4.
  • Constraints:
    • After deactivation, any subsequent update operation (add/update/remove key, relation, or service) MUST fail.

Example:

typescript
await deactivate(didContract, providers);

8. Security Considerations

The security of the Midnight DID method depends on the Midnight ledger, the DID contract verifier keys deployed for each DID, wallet-local controller custody, resolver/indexer integrity, and the release supply chain for packages and ZK artifacts. The Midnight protocol provides the ledger consensus and proof system; this method specification defines the DID-specific trust boundaries that implementations MUST account for. For more details on the underlying protocol, see the white paper: MIDNIGHT-WHITEPAPER.

A Midnight DID is identified by the DID contract address, but update authority is not simple address possession. Controller-gated updates are authorized by the controllerPublicKey stored in contract state and by wallet-local signatures as described in section 5.2. Implementations MUST protect the corresponding controller secret and MUST NOT send it to delegated proof servers.

Implementations SHOULD use secure storage for private keys. They MAY use random keys or an implementation-defined HD derivation convention for Midnight DID keys; an HD convention can simplify backup and recovery but is not standardized by this specification version. The concrete implementation of secret storage depends on the target platform and is outside the scope of this specification.

8.1. Controller custody, key loss, and deactivation

The controller secret is a high-value wallet-local secret. A party that can produce valid controller authorization signatures for the current DID version can perform controller-gated mutations. Loss of the controller secret prevents ordinary controller-gated updates until recoverControllerKey is used with the dedicated recovery authority. Loss of both the controller secret and recovery authority secret makes further updates impossible in this specification version.

The recovery authority secret is also high-value custody material. A party that can produce a valid recovery authorization can rotate the active controller key, and this method version does not provide an on-chain recovery-authority rotation circuit. Implementations SHOULD keep the recovery authority secret in separate or colder custody than the active controller secret where operationally possible.

Implementations SHOULD generate distinct controller and recovery authority secrets for distinct DIDs. Reusing either secret across DID contracts causes the same public key to appear in multiple public states and can become a correlation handle.

Deactivation is irreversible. A deactivated DID cannot be reactivated, updated, or recovered, but its public state and historical ledger transactions remain visible to ledger observers, indexers, resolvers, and archives. Deactivation MUST NOT be treated as data erasure and is not a recovery mechanism after controller custody has already been lost.

8.2. Delegated proving and proof servers

Controller authorization is designed so a delegated proof server receives an operation-bound signature, expected version, and public operation inputs rather than the controller secret. A proof server that receives only this material can construct proofs for that exact signed operation, but cannot choose a different operation, change public arguments, or replay the signature after the version changes.

Applications that delegate proving still trust the proof server and its transport for availability, correct proof generation, and confidentiality of any other witness material supplied to the proving job. They SHOULD authenticate proof server endpoints, bind wallet prompts to exact operation inputs, and avoid sending DID Document private keys or controller secrets to remote proving infrastructure.

8.3. Resolver, indexer, and finality trust

Resolution reads are indexer-backed in the reference implementation, and the configured indexer/public-data provider is trusted. A compromised, rogue, stale, or unfinalized response can produce a forged or stale DID Document or an incorrect notFound result. Indexer and resolver operators SHOULD protect endpoint transport, monitor freshness, and use finalized or explicitly pinned reads when their trusted provider exposes block-height or block-hash constraints.

Resolvers and consumers MUST treat unavailable, failed, stale, or untrusted reads as inconclusive. They do not prove that a DID is absent or deactivated, and they do not prove transaction finality. A provider-returned null maps to notFound only inside the configured provider's trust boundary. Version 0.6 supplies no light client, independent indexer state-proof verification, or independent consensus/finality check. Applications requiring those assurances MUST define and implement them above this resolver profile; selecting a block height or hash alone is not proof verification.

8.4. Client-asserted metadata

The created and updated metadata fields are supplied by controller/prover witnesses and then stored on ledger. They are not derived from Midnight consensus time. Resolvers MAY enforce local sanity bounds or expose these values as advisory metadata, but consumers MUST NOT treat them as authoritative timestamps without an independent ledger, indexer, timestamping, or application attestation.

8.5. Raw Compact calls and DID state validity

The contract enforces authorization and core state-transition invariants. The SDK and resolver enforce additional DID-domain constraints for URI syntax, normalization, JWK canonicality, service endpoint shape, and other values that are represented as opaque strings or structured data at the Compact boundary. A caller that bypasses the SDK with raw Compact transactions can create durable ledger state that strict resolvers or DID consumers reject.

Production implementations SHOULD use the SDK validation path for mutations. Resolvers SHOULD fail clearly on malformed ledger state and operators SHOULD treat such failures as security-relevant interoperability incidents.

8.6. ZK artifact supply chain and audit posture

ZK prover, verifier, and ZKIR artifacts are release artifacts for this method. The verifier keys deployed with a DID contract are the proof-verification trust root for that contract, while the published ZK bundle supplies the matching runtime artifacts used by clients. Consumers SHOULD use version-matched packages and ZK bundles and SHOULD verify published manifests and checksums before using artifact bundles. Signed or provenanced release assets are preferred when available.

This repository uses CI, code scanning, dependency automation, external review, and issue-based hardening work, but those controls do not by themselves constitute an independent production audit. Deployments SHOULD perform their own security review appropriate to their custody, resolver, and release-artifact trust requirements.

8.7. Binding to Physical Identity

Conforming Midnight DID producers MUST NOT intentionally publish personal data in DID Document fields. Ledger-backed DID Documents can still contain controller-supplied public values, so controllers and wallets are responsible for data minimization before publication. Ownership is proved by:

  • Control over the blockchain address.
  • Control over the private keys associated with the Midnight DID Document.

It is recommended to use Verifiable Credentials as described in VC-DATA-MODEL to bind physical identity to the Midnight DID, and to expose an endpoint in the DID Document to share the corresponding credentials or to fetch them.

Using the DID extension to share the VC as a public ledger state is possible, but not recommended.

8.8. DID document changes

All Midnight DIDs are created by deploying the smart contract with the corresponding public ledger state. Controller-gated updates are authorized with wallet-local Jubjub Schnorr signatures over the current operation intent; the controller secret is used only by the wallet or SDK to create the signature, is not sent to the authorization circuit or delegated proof server, and is not published on-chain.

NOTE: Ledger-backed Midnight DID state exposes the latest contract state through the resolver profile, but the public on-chain transaction transcript already records the mutation history for the DID contract address. A future resolver or indexer profile could make that history easier to query and represent, but the privacy impact exists even without a dedicated history API.

9. Privacy Considerations

This section addresses the DID Core Section 7.5 privacy considerations that are relevant to the Midnight DID method, including correlation and unlinkability, denial of service, service-endpoint correlation, and DID Document history leakage.

9.1. Surveillance

In the public Midnight network, all transactions are visible by watching the blockchain.

For ledger-backed Midnight DIDs, each DID is a distinct smart-contract deployment. The contract address in the DID is a permanent public correlation handle for all DID operations against that instance, including deployment, key updates, verification relationship changes, service changes, controller rotation, and deactivation. Indexers and observers can link those operations to the same DID contract even when the resolved DID Document does not contain personal data.

9.2. Stored data compromise

The DID Document data is stored in the blockchain state. The Blockchain state is secured by the consensus method. For details, see the Midnight white paper: MIDNIGHT-WHITEPAPER.

The Midnight Indexer service follows the changes to the Midnight blockchain and indexes the public ledger state for fetching by other parties.

9.3. Identification

If personal information is added to the blockchain, potentially making it a viable credential, it can be permanently accessed. With enough identifying information, an identity can be deduced.

For this reason, it is strongly suggested that personal information not be added to the blockchain.

Conforming Midnight DID producers MUST NOT intentionally place personal data in DID Document fields. This includes verification method identifiers, service endpoint URLs, service metadata, alsoKnownAs aliases, and other extension fields. The method cannot guarantee that controller-supplied public values are non-personal, so applications and wallets are responsible for data minimization before publication.

9.4. Separation of concerns

The Midnight DID method separates concerns between the following roles:

  • Midnight DID smart‑contract publisher and updater (the role can assemble and submit transactions, while controller authorization is supplied as wallet-local signatures).
  • Midnight DID Document key holder (the role implies managing the private and public keys associated with the DID ledger state).
  • Midnight DID Document reader (has access to the public ledger state and can reconstruct the DID Document).

This separation of concerns allows the use of the Midnight DID method for both custodial and non‑custodial solutions.

9.5. Correlation and unlinkability

Ledger-backed Midnight DIDs are intentionally resolvable from public ledger state, so they are linkable by design once disclosed. The contract address embedded in did:midnight:<network>:<address> links the DID subject to one public on-chain contract instance and its mutation sequence. Publishing the same DID in Verifiable Credentials, trust registries, presentations, service endpoints, or application logs creates additional correlation points across those contexts.

The offchain long form has a different privacy tradeoff. It avoids public ledger publication, but the long-form DID string contains the encoded initial DID state. Any party that observes the long-form DID can decode that state and learn the included public verification methods, service endpoints, aliases, and other public fields without contacting a resolver. Because offchain state has no in-place update mechanism, every changed long-form DID discloses the full state for that version to any party that receives it. Historical long-form DIDs that a subject previously shared remain independently decodable.

Applications that need unlinkability SHOULD avoid reusing the same ledger-backed DID across unrelated contexts. They SHOULD minimize alsoKnownAs, avoid stable service endpoints that identify the same operator across DIDs, and prefer purpose-specific DIDs where correlation risk is material. Applications that exchange offchain DIDs SHOULD prefer the hash-only short form after the recipient already has the encoded state through an authorized channel, and SHOULD avoid placing long-form DIDs in public registries, credentials, or logs unless full-state disclosure is acceptable.

9.6. Denial of service

Midnight DID resolution depends on ledger access, indexer availability, resolver availability, and, for updates, transaction submission and proof generation. Attackers can attempt denial-of-service by flooding indexers or resolvers with resolution requests, targeting popular DID contract addresses, submitting high volumes of DID transactions, or exhausting proof-server resources used by wallet or SDK flows.

Resolver and indexer operators SHOULD apply normal service protections such as rate limits, request size limits, caching of resolved public state, backpressure, abuse monitoring, and resource isolation between public query handling and update/proving paths. Wallets and SDKs SHOULD treat resolver/indexer failures as availability failures, not as proof that a DID does not exist or has been deactivated.

9.7. Service-endpoint correlation

Service endpoints are public DID Document data. Endpoint hostnames, paths, query parameters, DIDComm routing metadata, mediator identifiers, and shared infrastructure can correlate multiple DIDs to the same operator, account, device, organization, or network location. Updating a service endpoint on ledger also creates a public before/after link for that DID contract address.

Controllers SHOULD publish only endpoints needed for the intended interaction, avoid embedding account identifiers or personal data in endpoint values, use pairwise or purpose-specific endpoints where possible, and rotate or remove endpoints when they are no longer needed. DID consumers SHOULD avoid assuming that a service endpoint is private merely because it is contained in a DID Document rather than in a credential.

9.8. DID history leakage

Ledger-backed Midnight DID updates are public transactions against a stable contract address. Even if a resolver returns only the latest DID Document, observers with access to ledger or indexer history can reconstruct prior public states, including removed verification methods, prior service endpoints, previous aliases, relationship changes, controller rotations, deactivation time metadata, and operation ordering.

The created and updated metadata fields are not reliable mitigations for history analysis because their values are client-asserted. They can help applications order a locally observed sequence after sanity checks, but they do not hide or authenticate the public transaction history. Controllers SHOULD assume that public DID mutations are permanent, linkable, and recoverable by sufficiently capable observers.

10. Discoverability

The ability to discover and resolve a Midnight DID depends on the network segment encoded in the identifier (undeployed, devnet, testnet, mainnet, preview, or preprod). Each segment offers different durability guarantees.

  • undeployed — DIDs created on the local undeployed network are discoverable by the local tooling (for example, the Midnight Indexer and resolver running in that environment) while the environment is alive. Because this network can be recreated or destroyed at any time, no long-term discoverability guarantees are provided.
  • devnet — The shared developer network exposes the same discoverability behaviour as the Midnight Indexer and resolver stack, but uptime is best-effort. DIDs remain discoverable for as long as the devnet infrastructure is running; the network can be reset without notice.
  • testnet — DIDs published to the public testnet are discoverable by any participant connected to the network. The Midnight testnet is an actively maintained network; more information is available in the Introducing the Midnight Testnet blog post.
  • mainnet — Once the Midnight mainnet is live, DIDs deployed to the mainnet segment inherit the same discoverability guarantees as any production Midnight transaction: as long as the mainnet ledger exists, the DID state can be resolved.
  • preview — The preview network is a pre-release shared environment. DIDs are discoverable while preview infrastructure is running, but persistence and compatibility guarantees are lower than testnet/mainnet.
  • preprod — The preprod network is a staging environment intended to mirror production behavior more closely than preview. Discoverability is expected while preprod infrastructure is operating, but long-term guarantees remain below mainnet.

In every network, discoverability is ultimately provided by the Midnight ledger and its indexing infrastructure. Operators are responsible for running indexers and resolvers appropriate to their deployment model.

11. Appendix

11.1. Controller authorization and proof servers

Midnight DID controller authorization is designed so delegated proof servers do not need the controller secret. Each mutating circuit receives a wallet-local Jubjub Schnorr signature and an expected ledger version. The signed digest is domain-separated with midnight-did-ctrl-sig:v1 and includes the DID contract id, current version, operation name, and operation arguments.

The controller secret is private from the ledger, indexers, resolvers, DID Document readers, and proof servers. A remote proof server that receives only the signature cannot mint an authorization for a later DID version, a different DID contract, a different controller operation, or changed operation arguments. If a submitted transaction races with another successful mutation, the version check fails and the wallet must sign a fresh authorization for the new state.

Wallets and SDKs SHOULD create the controller authorization immediately before submitting the intended mutation and MUST NOT reuse it after a failed stale version check. A proof service may still be trusted with transaction assembly and submission, but it is not trusted with controller secret custody.

11.2. Example DID Document

A simple example of a Midnight DID Document is as follows:

json
{
  "@context": [
    "https://www.w3.org/ns/did/v1",
    "https://w3id.org/security/jwk/v1"
  ],
  "id": "did:midnight:undeployed:c569622e7f33d2d020ba1cae242e6077268941327846d62d8cbf0cc923ae41f6",
  "alsoKnownAs": [
    "did:midnight:undeployed:c569622e7f33d2d020ba1cae242e6077268941327846d62d8cbf0cc923ae41f6",
    "did:example:aka-2"
  ],
  "controller": "did:midnight:undeployed:c569622e7f33d2d020ba1cae242e6077268941327846d62d8cbf0cc923ae41f6",
  "verificationMethod": [
    {
      "id": "#key-1",
      "type": "JsonWebKey",
      "controller": "did:midnight:undeployed:c569622e7f33d2d020ba1cae242e6077268941327846d62d8cbf0cc923ae41f6",
      "publicKeyJwk": {
        "kty": "OKP",
        "crv": "Ed25519",
        "x": "Kg"
      }
    },
    {
      "id": "#key-2",
      "type": "JsonWebKey",
      "controller": "did:midnight:undeployed:c569622e7f33d2d020ba1cae242e6077268941327846d62d8cbf0cc923ae41f6",
      "publicKeyJwk": {
        "kty": "OKP",
        "crv": "Ed25519",
        "x": "Kg"
      }
    },
    {
      "id": "#key-3",
      "type": "JsonWebKey",
      "controller": "did:midnight:undeployed:c569622e7f33d2d020ba1cae242e6077268941327846d62d8cbf0cc923ae41f6",
      "publicKeyJwk": {
        "kty": "EC",
        "crv": "Jubjub",
        "x": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACo",
        "y": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFQ"
      }
    }
  ],
  "authentication": [
    "#key-1"
  ],
  "assertionMethod": [
    "#key-2"
  ],
  "service": [
    {
      "id": "#linked-domain-1",
      "type": "LinkedDomains",
      "serviceEndpoint": [
        "https://example.org/inbox",
        {
          "uri": "https://backup.example.org/inbox",
          "routingKeys": ["did:example:mediator#key-1"],
          "accept": ["didcomm/v2"]
        }
      ]
    }
  ]
}

Normative References

  • W3C-DID - W3C Decentralized Identifiers (DID) v1.0 Recommendation, 19 July 2022
  • RFC2119 - Keywords for use in RFCs to Indicate Requirement Levels
  • RFC3986 - Uniform Resource Identifier (URI): Generic Syntax
  • RFC4648 - The Base16, Base32, and Base64 Data Encodings
  • RFC7517 - JSON Web Key (JWK)
  • RFC7518 - JSON Web Algorithms (JWA)
  • RFC8037 - CFRG Elliptic Curve Diffie-Hellman and Signatures in JOSE
  • SEC1 - SEC 1: Elliptic Curve Cryptography, Version 2.0
  • VC-DATA-MODEL - Verifiable Credentials Data Model
  • DID-SPEC-REGISTRIES - DID Specification Registries

Informative References

Midnight DID reference implementation