Skip to content

Midnight DID Specification Draft v0.4.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.

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 describes a new DID method called Midnight for storing DIDs using blockchains as public storage, also referred to as Midnight DID. This specification conforms to the requirements specified in the W3C-DID currently published by the W3C Credentials Community Group.

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 conforms to the structure of a Midnight smart contract address and network segment. The contract address is 32 bytes and is encoded as 64 hex characters (lowercase recommended). For the offchain network, the identifier starts with the lowercase 64-hex hash of the encoded offchain DID state; see Section 2.1 for the hash-only short form and self-contained long form.

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 ":" specific-idstring
ledger-network = "undeployed" | "devnet" | "testnet" | "mainnet" | "preview" | "preprod"
offchain-did = "did:midnight:offchain:" offchain-state-hash [ ":" offchain-state ]
specific-idstring = 64HEXDIG
offchain-state-hash = 64HEXDIGL
offchain-state = 1*base64urlchar
HEXDIGL = DIGIT / %x61-66
base64urlchar = ALPHA / DIGIT / "-" / "_"

The regular expression for the specific-idstring component is as follows:

/^[0-9a-fA-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 fragment identifier such as #key-1.
  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>.

OffchainService is encoded as:

  1. present: Boolean.
  2. id: Opaque<"string">, a fragment identifier.
  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, non-fragment method or service identifiers, 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 id. The identifier MUST be either a DID URL for the subject with a fragment (for example, did:midnight:<network>:<addr>#key-1) or a relative reference that resolves against the DID (for example, #key-1). Midnight normalizes identifiers to fragment form (#...) at the SDK/contract boundary for on-ledger storage and relation management. During DID Document resolution, Midnight emits canonical absolute DID URLs for verificationMethod.id.

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 JWK in uncompressed format with 32-byte little-endian field-element encodings:

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

Jubjub keys are stored on ledger as native JubjubPoint values in the schnorrJubjubVerificationMethods map. Resolvers project those native points into DID Document publicKeyJwk entries by encoding the x and y field elements as 32-byte little-endian, canonical unpadded base64url strings.

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.

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 either a DID URL for the Midnight DID subject (for example, did:midnight:<network>:<addr>#service-1) or a relative URL resolved against that DID (for example, #service-1, /routing, or ?service=messaging). Midnight DID deployments normalize service IDs to fragment identifiers (#fragment) when serializing services in ledger transactions. During DID Document resolution, service IDs are emitted as canonical absolute DID URLs. A conforming producer MUST NOT emit multiple service entries with the same 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
  • an array composed of 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.

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/version number to support upgrades and compatibility checks.
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 keyed by canonical fragment id. Values store opaque canonical JWK material. Resolver output expands ids to absolute DID URLs.
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 keyed by canonical fragment id. 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 verification method identifiers (stored in canonical fragment form, for example #key-1) authorized for authentication. The DIDDocument's authentication property is reconstructed from this state.
assertionMethodRelationSet<Opaque<"string">>Set of verification method identifiers (stored in canonical fragment form, for example #key-1) authorized for assertionMethod. The DIDDocument's assertionMethod property is reconstructed from this state.
keyAgreementRelationSet<Opaque<"string">>Set of verification method identifiers (stored in canonical fragment form, for example #key-1) authorized for keyAgreement. The DIDDocument's keyAgreement property is reconstructed from this state.
capabilityInvocationRelationSet<Opaque<"string">>Set of verification method identifiers (stored in canonical fragment form, for example #key-1) authorized for capabilityInvocation. The DIDDocument's capabilityInvocation property is reconstructed from this state.
capabilityDelegationRelationSet<Opaque<"string">>Set of verification method identifiers (stored in canonical fragment form, for example #key-1) authorized for capabilityDelegation. The DIDDocument's capabilityDelegation property is reconstructed from this state.
servicesMap<Opaque<"string">, Service>Map from service identifiers (canonicalized to fragment form for storage) to service definitions. The serviceEndpoint value is stored as a JSON string so that any DID Core-compliant representation (string, object, or array) can be rehydrated when reconstructing the DID Document. Resolver output emits canonical absolute DID URLs for service id values.

The verificationMethods and schnorrJubjubVerificationMethods maps share one verification method identifier namespace. Resolvers MUST reject duplicate identifiers across the two maps after canonical fragment 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 canonical representation. This avoids consistency hazards while preserving both W3C publicKeyJwk interoperability for non-Jubjub keys and native JubjubPoint storage for SchnorrJubjub keys.

Because Compact treats Opaque<"string"> identifiers as opaque values, canonical DID URL subject binding and fragment normalization are SDK/resolver responsibilities. The TypeScript API normalizes identifiers before submission; resolvers reject states that would produce duplicate normalized verification method IDs.

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 update is a separate circuit call; batching multiple logical operations into a single on-chain call is not supported in this version.

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.

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

DID Core distinguishes between resolve and resolveRepresentation; see DID Core Section 7.1. Midnight resolvers MUST preserve that distinction when composing responses:

  • resolve(did, resolutionOptions) returns the abstract data model triple: didResolutionMetadata, didDocument, and didDocumentMetadata. The accept option MUST NOT be used with resolve, and didResolutionMetadata.contentType MUST NOT be present on successful abstract resolution results.
  • resolveRepresentation(did, resolutionOptions) returns didResolutionMetadata, didDocumentStream, and didDocumentMetadata. The optional accept value selects the preferred DID Document representation. On successful representation resolution, didResolutionMetadata.contentType MUST be present and MUST describe the returned didDocumentStream.

Midnight implementations SHOULD support the following response composition rules:

Request modeRequested media typeResponse bodyDID resolution metadata
resolvenoneDID Resolution Result object containing didDocument, didResolutionMetadata, and didDocumentMetadataEmpty object on success; no contentType
resolveRepresentationomitted or application/did+ld+jsonDID Document byte stream serialized as JSON-LD{ "contentType": "application/did+ld+json" }
resolveRepresentationapplication/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 abstract resolve
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 abstract resolve

If a caller requests a DID Document representation that is not supported, the resolver MUST return didResolutionMetadata.error = "representationNotSupported" and MUST NOT return a didDocument or didDocumentStream.

Failure responses MUST set didResolutionMetadata.error to a DID Core error keyword. Midnight resolvers SHOULD use invalidDid, notFound, and representationNotSupported for those DID Core-defined cases, and SHOULD use registered DID resolution keywords such as methodNotSupported and internalError for broader resolver failures. Resolver-specific extension values MAY be used when they are registered or documented as a single ASCII keyword that starts with a letter.

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.

Conformance note: due to Compact language limitations for rich URI/data-model validation, normative checks for DID URL subject binding and DID Core structure conformance (for example serviceEndpoint shape), JWK/base64url canonicality, opaque JWK shape (for example OKP omits y while EC includes y), and non-native key parsing are enforced at the SDK/resolver layers (domain, api, did). The smart contract enforces authorization, exact ledger identifier existence/uniqueness, supported opaque JWK key/curve profiles, native SchnorrJubjub point storage, and state-transition invariants.

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, derives the next public key locally with the contract package's deriveControllerPublicKey helper, submits the rotation or recovery transaction, and stores the new secret in private state after the transaction succeeds.
  • If the transaction finalizes but private-state persistence fails, the wallet must recover the same new secret to continue updating the DID.
  • 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). On-ledger, Midnight canonicalizes to fragment form (#key-1) for storage. Resolver output emits the absolute DID URL form.
    • 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,
  '#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 Jubjub method MUST use removeSchnorrJubjubVerificationMethod, which applies the same relation cleanup behavior as the generic API helper.

Example:

typescript
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 DID URL for the subject or a relative identifier (e.g., #key-1).
    • 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.

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 either a DID URL for the DID subject or a relative identifier (for example, #service-1). Midnight canonicalizes service IDs to fragment form for storage and emits absolute DID URL form in resolved documents.
    • 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. A resolver that trusts a compromised, rogue, stale, or unfinalized indexer response can return a forged or stale DID Document. Indexer and resolver operators SHOULD use trusted indexer deployments, protect endpoint transport, monitor freshness, and prefer finalized or pinned reads when provider APIs expose block-height or block-hash constraints.

Resolvers and consumers MUST treat indexer or resolver failures as availability failures, not as proof that a DID does not exist or has been deactivated. If an application requires a finality latency bound or independent state integrity check, it MUST define that policy above this specification version or use a resolver profile that exposes the required proof or block pin.

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. The secret key is provided as a witness to authorize updates 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": "Kg",
        "y": "VA"
      }
    }
  ],
  "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 - Decentralized Identifiers (DID) v1.0
  • 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)
  • RFC8037 - CFRG Elliptic Curve Diffie-Hellman and Signatures in JOSE
  • VC-DATA-MODEL - Verifiable Credentials Data Model
  • DID-SPEC-REGISTRIES - DID Specification Registries

Informative References

Midnight DID reference implementation