feat: implement did:dht creation and resolution via Mainline DHT

DHT-02: did:dht creation
- network/did_dht.rs: z-base-32 encoding, DNS packet encoding, BEP-44
  mutable item publication via mainline crate
- identity.create-dht-did RPC endpoint
- dht_did field added to IdentityRecord
- get_signing_key() exposed on IdentityManager

DHT-03: did:dht resolution
- did_dht::resolve() queries DHT, parses DNS → DID Document
- DhtDidCache with 1-hour TTL
- identity.resolve-dht-did, identity.refresh-dht-did, identity.dht-status

New dependencies: mainline 2, zbase32 0.1, simple-dns 0.7

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Dorian
2026-03-14 04:01:56 +00:00
parent 1f11926d2d
commit 66eba4a46d
7 changed files with 440 additions and 2 deletions

View File

@@ -2,6 +2,7 @@
use super::RpcHandler;
use crate::identity_manager::{IdentityManager, IdentityPurpose};
use crate::network::did_dht;
use anyhow::{Context, Result};
use nostr_sdk::ToBech32;
@@ -571,4 +572,113 @@ impl RpcHandler {
"cached": true,
}))
}
/// identity.create-dht-did — Publish an identity's DID to the Mainline DHT.
pub(super) async fn handle_identity_create_dht_did(
&self,
params: Option<serde_json::Value>,
) -> Result<serde_json::Value> {
let params = params.ok_or_else(|| anyhow::anyhow!("Missing params"))?;
let identity_id = params
.get("identity_id")
.and_then(|v| v.as_str())
.ok_or_else(|| anyhow::anyhow!("Missing identity_id"))?;
validate_identity_id(identity_id)?;
let manager = IdentityManager::new(&self.config.data_dir).await?;
let signing_key = manager.get_signing_key(identity_id).await?;
let dht_did = did_dht::create_and_publish(&signing_key, &[]).await?;
// Save the dht_did back to the identity record
did_dht::save_dht_did(&self.config.data_dir, identity_id, &dht_did).await?;
Ok(serde_json::json!({
"dht_did": dht_did,
"published": true,
}))
}
/// identity.resolve-dht-did — Resolve a did:dht from the DHT.
pub(super) async fn handle_identity_resolve_dht_did(
&self,
params: Option<serde_json::Value>,
) -> Result<serde_json::Value> {
let params = params.ok_or_else(|| anyhow::anyhow!("Missing params"))?;
let did = params
.get("did")
.and_then(|v| v.as_str())
.ok_or_else(|| anyhow::anyhow!("Missing did"))?;
if !did.starts_with("did:dht:") {
anyhow::bail!("Not a did:dht identifier");
}
let doc = did_dht::resolve(did, None).await?;
Ok(serde_json::json!({
"did": did,
"document": doc,
}))
}
/// identity.refresh-dht-did — Re-publish an identity's did:dht to keep it alive in the DHT.
pub(super) async fn handle_identity_refresh_dht_did(
&self,
params: Option<serde_json::Value>,
) -> Result<serde_json::Value> {
let params = params.ok_or_else(|| anyhow::anyhow!("Missing params"))?;
let identity_id = params
.get("identity_id")
.and_then(|v| v.as_str())
.ok_or_else(|| anyhow::anyhow!("Missing identity_id"))?;
validate_identity_id(identity_id)?;
let manager = IdentityManager::new(&self.config.data_dir).await?;
let record = manager.get(identity_id).await?;
if record.dht_did.is_none() {
anyhow::bail!("Identity has no did:dht — create one first with identity.create-dht-did");
}
let signing_key = manager.get_signing_key(identity_id).await?;
let dht_did = did_dht::create_and_publish(&signing_key, &[]).await?;
Ok(serde_json::json!({
"dht_did": dht_did,
"refreshed": true,
}))
}
/// identity.dht-status — Check if an identity's did:dht is published and resolvable.
pub(super) async fn handle_identity_dht_status(
&self,
params: Option<serde_json::Value>,
) -> Result<serde_json::Value> {
let params = params.ok_or_else(|| anyhow::anyhow!("Missing params"))?;
let identity_id = params
.get("identity_id")
.and_then(|v| v.as_str())
.ok_or_else(|| anyhow::anyhow!("Missing identity_id"))?;
validate_identity_id(identity_id)?;
let manager = IdentityManager::new(&self.config.data_dir).await?;
let record = manager.get(identity_id).await?;
let (published, resolvable) = match &record.dht_did {
Some(dht_did) => {
let resolvable = did_dht::resolve(dht_did, None).await.is_ok();
(true, resolvable)
}
None => (false, false),
};
Ok(serde_json::json!({
"identity_id": identity_id,
"did_key": record.did,
"dht_did": record.dht_did,
"published": published,
"resolvable": resolvable,
}))
}
}

View File

@@ -430,6 +430,10 @@ impl RpcHandler {
"identity.resolve-did" => self.handle_identity_resolve_did(params).await,
"identity.resolve-remote-did" => self.handle_identity_resolve_remote_did(params).await,
"identity.verify-did-document" => self.handle_identity_verify_did_document(params).await,
"identity.create-dht-did" => self.handle_identity_create_dht_did(params).await,
"identity.resolve-dht-did" => self.handle_identity_resolve_dht_did(params).await,
"identity.refresh-dht-did" => self.handle_identity_refresh_dht_did(params).await,
"identity.dht-status" => self.handle_identity_dht_status(params).await,
"identity.create-nostr-key" => self.handle_identity_create_nostr_key(params).await,
"identity.nostr-sign" => self.handle_identity_nostr_sign(params).await,
"identity.nostr-encrypt-nip04" => self.handle_identity_nostr_encrypt_nip04(params).await,