Enterprise-grade W3C DID Resolution for the did:nostr method.
resolve("did:nostr:abc...#key1")
│
▼
DID URL Parser ──► Cache Check ──► Resolution Chain
│
┌───────────────┼───────────────┐
▼ ▼ ▼
RelayBackend HttpBackend OfflineBackend
(WebSocket) (.well-known) (pure math)
+ circuit + pooled
breaker client
│ │ │
└───────────────┼───────────────┘
▼
Build Result
│
▼
Dereference (if fragment)
│
▼
Cache (moka, bounded TTL)
│
▼
Metrics + Return
- Three-tier resolution chain — Relay (WebSocket) → HTTP (.well-known) → Offline (pure math). Always resolves.
- Per-relay circuit breakers — Exponential backoff prevents hammering dead relays.
- Event ID verification — Recomputation of SHA-256 canonical serialization before signature check catches relay tampering.
- Schnorr signature verification — Every event cryptographically verified via BIP-340/secp256k1.
- NIP-65 relay discovery — Kind 10002 events populate service endpoints.
- NIP-39 identity claims —
itags populatealsoKnownAsfor cross-network identity linking. - DID URL dereferencing —
#key1,#relay1fragments return specific resources per W3C spec §7.2. - W3C-compliant metadata —
didResolutionMetadata,didDocumentMetadata, content types, error codes. - Bounded TTL cache — moka-backed, LRU eviction, wait-free reads, no manual sweep.
- Burst-absorbing rate limiter — Semaphore-based with configurable burst timeout.
- Prometheus-style metrics — Lock-free atomic counters for every operational signal.
- Builder validation — Zero values rejected at construction time, not at runtime.
use nostr_did_resolver::Resolve;
#[tokio::main]
async fn main() {
let resolver = Resolve::builder()
.with_relay("wss://relay.damus.io")
.with_relay("wss://nos.lol")
.with_cache_ttl(300)
.build()
.unwrap();
let result = resolver
.resolve("did:nostr:124c0fa99407182ece5a24fad9b7f6674902fc422843d3128d38a0afbee0fdd2")
.await;
println!("{}", serde_json::to_string_pretty(&result).unwrap());
}| Tier | Backend | Network | Failure mode | Provides |
|---|---|---|---|---|
| 1 | Relay | WebSocket | Circuit breaker, retry, timeout | Profile, follows, NIP-65 relays, NIP-39 identity claims |
| 2 | HTTP | HTTPS GET | Cross-verify key, pooled client, redirect limit | Cached DID Document from .well-known |
| 3 | Offline | None | Never fails for valid DIDs | Verification method only |
let result = resolver
.dereference("did:nostr:124c0f...#key1")
.await;
// Returns just the verificationMethod object per W3C spec §7.2let result = resolver.resolve_offline("did:nostr:124c0f...");
// Works without network — pure math from the public keylet snapshot = resolver.metrics();
println!("Cache hit rate: {}/{}",
snapshot.cache_hits,
snapshot.resolutions_total
);| Metric | Description |
|---|---|
resolutions_total |
Total resolution attempts |
cache_hits |
Served from cache |
rate_limit_rejections |
Rejected by rate limiter |
relay_successes |
Relay backend successes |
relay_failures |
Relay backend failures |
http_successes |
HTTP backend successes |
http_failures |
HTTP backend failures |
offline_resolutions |
Offline fallback resolutions |
sig_verification_failures |
Events failing Schnorr verification |
event_id_mismatches |
Events with tampered IDs |
resolution_errors |
Total errors (invalidDid, notFound, etc.) |
let resolver = Resolve::builder()
.with_relay("wss://relay.damus.io") // Tier 1 relays
.with_http_origin("https://example.com") // Tier 2 HTTP cache
.with_cache_ttl(300) // Cache TTL in seconds
.with_cache_max_capacity(10_000) // Max cache entries
.with_relay_timeout(15) // Relay query timeout (sec)
.with_http_timeout(10) // HTTP request timeout (sec)
.with_max_concurrent(100) // Max concurrent resolutions
.with_max_parallel_relays(4) // Max parallel relay queries
.with_max_relay_retries(2) // Max retries per relay
.with_burst_timeout_ms(100) // Rate limiter burst window
.build()
.unwrap();All numeric fields reject zero values — misconfiguration is caught at startup.
| Crate | Purpose |
|---|---|
nostr-did |
DID Document types and builder |
nostr-did-key |
BIP-340 → Multikey cryptographic transform |
secp256k1 |
Schnorr signature verification |
sha2 |
Event ID recomputation |
tokio-tungstenite |
WebSocket relay connections |
reqwest |
HTTP .well-known cache |
moka |
Bounded TTL cache |
uuid |
Subscription ID generation |
tracing |
Instrumented logging |
thiserror |
Error type derivation |
static_assertions |
Compile-time Send + Sync verification |
- Event ID verified before signature — Cheap hash check catches relay tampering before expensive Schnorr verification.
- Cross-verification of HTTP cache —
publicKeyMultibasedecoded and checked against the DID being resolved. - NIP-65 relay validation — Only
wss://andws://schemes accepted. HTTP relays rejected. - Follow pubkey validation — Kind 3
ptags validated as 64-char hex before inclusion. - Compile-time Send + Sync — Build breaks if any field is not thread-safe.
- Zero values rejected — Builder returns error, not silent defaults.
MIT OR Apache-2.0
nostr-did-key— BIP-340 → Multikey transformationnostr-did— DID Document generation