Reference:
did:stellarv0.1 specification
A Decentralized Identifier anchored on the Stellar network. An identity is materialized as an opaque 128-bit identifier registered in a Soroban smart contract. The method is independent of any specific issuer, wallet-agnostic, and compliant with W3C DID Core 1.1.
did:stellar:{network}:{didId}
| Component | Value | Example |
|---|---|---|
network |
mainnet or testnet |
testnet |
didId |
16 random bytes encoded as base32 lowercase, exactly 26 characters | znfxngsh46vkyqu6inrx4omphi |
Validation regex (from src/identifier.ts):
^did:stellar:(mainnet|testnet):[a-z2-7]{26}$
Constants (from src/identifier.ts):
DID_ID_BYTES = 16— raw byte lengthDID_ID_LENGTH = 26— base32 string length
A did:stellar is intentionally not derived from a Stellar account.
The 128-bit didId is generated by the client using CSPRNG and has no
mathematical relationship to any G... address. This means:
- The DID survives key rotation. If the controller account is
compromised, call
transferControllerto a new wallet — the DID remains the same. - Multiple DIDs can be controlled by the same wallet.
- The wallet appears only as
controllerinside the on-chainDidRecord, not in the DID string itself.
| Field | Value |
|---|---|
| Contract name | did-stellar-registry |
| Testnet ID | CB7ATU7SF5QUKJMSULJDJVWJZVDXC23HTZX6NFUDTSFPVT6MA575NNZJ |
| Mainnet ID | Not deployed yet |
| Storage | Soroban persistent storage, one entry per DID |
| Read path | getLedgerEntries via Stellar RPC (free, no tx fee) |
Stored on-chain as DidDataKey::Record(BytesN<16>):
| Field | Type | Constraint |
|---|---|---|
controller |
Stellar G... address |
Classic accounts only in v0.1 |
authentication |
Array of DidKey |
1–3 keys (minimum 1 required) |
assertionMethod |
Array of DidKey |
0–3 keys |
keyAgreement |
Array of DidKey |
0–1 key |
services |
Array of DidService |
0–3 entries |
metadataUri |
Optional string | HTTPS URL, max 255 chars |
metadataHash |
Optional 32 bytes | SHA-256 of the metadata payload |
version |
u32 | Starts at 1, increments on every mutation |
createdLedger |
u32 | Ledger number at registration (immutable) |
updatedLedger |
u32 | Ledger number of last mutation |
deactivated |
bool | One-way flag, cannot be reset |
| Limit | Value |
|---|---|
MAX_KEY_MULTIBASE_LEN |
128 chars |
MIN_KEY_COUNT_AUTH |
1 |
MAX_KEY_COUNT_AUTH |
3 |
MAX_KEY_COUNT_ASSERT |
3 |
MAX_KEY_COUNT_AGREEMENT |
1 |
MAX_SERVICE_COUNT |
3 |
MAX_SERVICE_ID_LEN |
32 chars |
MAX_SERVICE_TYPE_LEN |
64 chars |
MAX_URL_LEN |
255 chars |
METADATA_HASH_LEN |
32 bytes |
| Operation | Authorization | Optimistic concurrency |
|---|---|---|
register(did_id, initial_record) |
initial_record.controller must sign |
No (new entry) |
update(did_id, expected_version, next_record) |
Current controller must sign |
Yes — expected_version must match |
transfer_controller(did_id, expected_version, new_controller) |
Current controller must sign |
Yes |
deactivate(did_id, expected_version) |
Current controller must sign |
Yes |
get(did_id) |
None (read-only) | No |
When a DidRecord is read from the chain, the SDK constructs a W3C
DID Document following these rules:
| Rule | Implementation |
|---|---|
@context |
["https://www.w3.org/ns/did/v1", "https://w3id.org/security/multikey/v1"] for active; ["https://www.w3.org/ns/did/v1"] for tombstone |
No root controller field |
Self-controlled per W3C DID Core 1.1 §5.1.2 |
verificationMethod |
One entry per key, type: "Multikey", controller: <the DID itself> |
| Verification relationships | Fragment references only (#auth-1, #assert-1, #keyagr-1), never inlined keys |
| Services | Fragment #service-{idSuffix} |
| Relationship | Curve | Multibase prefix | Example |
|---|---|---|---|
authentication, assertionMethod |
Ed25519 | z6Mk |
z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doY |
keyAgreement |
X25519 | z6LS |
z6LSnGSQaEk7SBZMmMLHTCqz6YUuiVVCmBNdAqSVdepqYAW1 |
When a DID is deactivated, the document becomes:
{
"@context": ["https://www.w3.org/ns/did/v1"],
"id": "did:stellar:testnet:...",
"verificationMethod": [],
"authentication": [],
"assertionMethod": [],
"keyAgreement": [],
"service": []
}HTTP resolver returns this with status 410 Gone.
The protocol for verifying off-chain DID control (e.g., DID-based login):
- Verifier sends a challenge:
{ did, domain, nonce, timestamp } - Signer canonicalizes with JCS (RFC 8785), signs with Ed25519
- Verifier checks: timestamp ±5 min → domain match → nonce uniqueness → signature against
authenticationkeys
The SDK implements this as buildChallenge() + verifyProofOfControl().
- Any verifier can resolve a DID using only a Stellar RPC URL and the registry contract ID
- No ACTA infrastructure is required for resolution
did.acta.buildis a convenience wrapper; the SDK talks directly to Stellar RPC- The HTTP service has no authentication — trust-minimised by design