use crate::error::{Error, Result};
use serde::{Deserialize, Serialize};
use std::fmt;
#[cfg(feature = "pq")]
use crate::crypto::PQSignature;
const MULTIBASE_BASE58BTC: char = 'z';
const MULTICODEC_MLDSA65: [u8; 2] = [0x13, 0x09];
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum DidMethod {
Lux,
Key,
Web,
}
impl DidMethod {
pub fn as_str(&self) -> &'static str {
match self {
DidMethod::Lux => "lux",
DidMethod::Key => "key",
DidMethod::Web => "web",
}
}
pub fn from_str(s: &str) -> Result<Self> {
match s {
"lux" => Ok(DidMethod::Lux),
"key" => Ok(DidMethod::Key),
"web" => Ok(DidMethod::Web),
_ => Err(Error::Identity(format!("unknown DID method: {}", s))),
}
}
}
impl fmt::Display for DidMethod {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.as_str())
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct Did {
pub method: DidMethod,
pub id: String,
}
impl Did {
pub fn new(method: DidMethod, id: String) -> Self {
Self { method, id }
}
pub fn parse(s: &str) -> Result<Self> {
if !s.starts_with("did:") {
return Err(Error::Identity(format!(
"invalid DID: must start with 'did:', got '{}'",
s
)));
}
let rest = &s[4..]; let parts: Vec<&str> = rest.splitn(2, ':').collect();
if parts.len() != 2 {
return Err(Error::Identity(format!(
"invalid DID format: expected 'did:method:id', got '{}'",
s
)));
}
let method = DidMethod::from_str(parts[0])?;
let id = parts[1].to_string();
if id.is_empty() {
return Err(Error::Identity("DID identifier cannot be empty".to_string()));
}
Ok(Self { method, id })
}
pub fn from_mldsa_key(public_key: &[u8]) -> Result<Self> {
const MLDSA_PUBLIC_KEY_SIZE: usize = 1952;
if public_key.len() != MLDSA_PUBLIC_KEY_SIZE {
return Err(Error::Identity(format!(
"invalid ML-DSA public key size: expected {}, got {}",
MLDSA_PUBLIC_KEY_SIZE,
public_key.len()
)));
}
let mut prefixed = Vec::with_capacity(MULTICODEC_MLDSA65.len() + public_key.len());
prefixed.extend_from_slice(&MULTICODEC_MLDSA65);
prefixed.extend_from_slice(public_key);
let encoded = bs58::encode(&prefixed).into_string();
let id = format!("{}{}", MULTIBASE_BASE58BTC, encoded);
Ok(Self {
method: DidMethod::Key,
id,
})
}
pub fn from_mldsa_key_lux(public_key: &[u8]) -> Result<Self> {
let key_did = Self::from_mldsa_key(public_key)?;
Ok(Self {
method: DidMethod::Lux,
id: key_did.id,
})
}
pub fn from_web(domain: &str, path: Option<&str>) -> Result<Self> {
if domain.is_empty() {
return Err(Error::Identity("domain cannot be empty".to_string()));
}
if domain.contains('/') || domain.contains(':') {
return Err(Error::Identity(format!(
"invalid domain for did:web: {}",
domain
)));
}
let id = match path {
Some(p) if !p.is_empty() => {
let path_parts = p.replace('/', ":");
format!("{}:{}", domain, path_parts)
}
_ => domain.to_string(),
};
Ok(Self {
method: DidMethod::Web,
id,
})
}
pub fn uri(&self) -> String {
format!("did:{}:{}", self.method, self.id)
}
pub fn document(&self) -> Result<DidDocument> {
let did_uri = self.uri();
let verification_method = match self.method {
DidMethod::Key | DidMethod::Lux => {
let key_material = self.extract_key_material()?;
VerificationMethod {
id: format!("{}#keys-1", did_uri),
type_: VerificationMethodType::JsonWebKey2020,
controller: did_uri.clone(),
public_key_multibase: Some(self.id.clone()),
public_key_jwk: None,
blockchain_account_id: if self.method == DidMethod::Lux {
Some(format!("lux:{}", hex::encode(&key_material[..20])))
} else {
None
},
}
}
DidMethod::Web => VerificationMethod {
id: format!("{}#keys-1", did_uri),
type_: VerificationMethodType::JsonWebKey2020,
controller: did_uri.clone(),
public_key_multibase: None,
public_key_jwk: None,
blockchain_account_id: None,
},
};
let service = Service {
id: format!("{}#zap-agent", did_uri),
type_: ServiceType::ZapAgent,
service_endpoint: ServiceEndpoint::Uri(format!("zap://{}", self.id)),
};
Ok(DidDocument {
context: vec![
"https://www.w3.org/ns/did/v1".to_string(),
"https://w3id.org/security/suites/jws-2020/v1".to_string(),
],
id: did_uri.clone(),
controller: None,
verification_method: vec![verification_method],
authentication: vec![format!("{}#keys-1", did_uri)],
assertion_method: vec![format!("{}#keys-1", did_uri)],
key_agreement: vec![],
capability_invocation: vec![format!("{}#keys-1", did_uri)],
capability_delegation: vec![],
service: vec![service],
})
}
fn extract_key_material(&self) -> Result<Vec<u8>> {
if self.id.is_empty() {
return Err(Error::Identity("empty DID identifier".to_string()));
}
let first_char = self.id.chars().next().unwrap();
if first_char != MULTIBASE_BASE58BTC {
return Err(Error::Identity(format!(
"unsupported multibase encoding: expected '{}', got '{}'",
MULTIBASE_BASE58BTC, first_char
)));
}
let decoded = bs58::decode(&self.id[1..])
.into_vec()
.map_err(|e| Error::Identity(format!("invalid base58btc encoding: {}", e)))?;
if decoded.len() < 2 {
return Err(Error::Identity("DID identifier too short".to_string()));
}
if decoded[0..2] != MULTICODEC_MLDSA65 {
return Ok(decoded);
}
Ok(decoded[2..].to_vec())
}
}
impl fmt::Display for Did {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.uri())
}
}
impl std::str::FromStr for Did {
type Err = Error;
fn from_str(s: &str) -> Result<Self> {
Did::parse(s)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct DidDocument {
#[serde(rename = "@context")]
pub context: Vec<String>,
pub id: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub controller: Option<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub verification_method: Vec<VerificationMethod>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub authentication: Vec<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub assertion_method: Vec<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub key_agreement: Vec<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub capability_invocation: Vec<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub capability_delegation: Vec<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub service: Vec<Service>,
}
impl DidDocument {
pub fn primary_verification_method(&self) -> Option<&VerificationMethod> {
self.verification_method.first()
}
pub fn get_verification_method(&self, id: &str) -> Option<&VerificationMethod> {
self.verification_method.iter().find(|vm| vm.id == id)
}
pub fn get_service(&self, id: &str) -> Option<&Service> {
self.service.iter().find(|s| s.id == id)
}
pub fn to_json(&self) -> Result<String> {
serde_json::to_string_pretty(self).map_err(|e| Error::Identity(format!("JSON serialization failed: {}", e)))
}
pub fn from_json(json: &str) -> Result<Self> {
serde_json::from_str(json).map_err(|e| Error::Identity(format!("JSON deserialization failed: {}", e)))
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum VerificationMethodType {
JsonWebKey2020,
Multikey,
#[serde(rename = "MlDsa65VerificationKey2024")]
MlDsa65VerificationKey2024,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct VerificationMethod {
pub id: String,
#[serde(rename = "type")]
pub type_: VerificationMethodType,
pub controller: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub public_key_multibase: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub public_key_jwk: Option<serde_json::Value>,
#[serde(skip_serializing_if = "Option::is_none")]
pub blockchain_account_id: Option<String>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum ServiceType {
ZapAgent,
#[serde(rename = "DIDCommMessaging")]
DidCommMessaging,
LinkedDomains,
CredentialRegistry,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(untagged)]
pub enum ServiceEndpoint {
Uri(String),
Uris(Vec<String>),
Structured {
uri: String,
#[serde(skip_serializing_if = "Option::is_none")]
accept: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
routing_keys: Option<Vec<String>>,
},
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Service {
pub id: String,
#[serde(rename = "type")]
pub type_: ServiceType,
pub service_endpoint: ServiceEndpoint,
}
#[derive(Debug, Clone)]
pub struct NodeIdentity {
pub did: Did,
pub public_key: Vec<u8>,
pub stake: Option<u64>,
pub stake_registry: Option<String>,
#[cfg(feature = "pq")]
signer: Option<PQSignature>,
#[cfg(not(feature = "pq"))]
_signer: std::marker::PhantomData<()>,
}
impl NodeIdentity {
pub fn new(did: Did, public_key: Vec<u8>) -> Self {
Self {
did,
public_key,
stake: None,
stake_registry: None,
#[cfg(feature = "pq")]
signer: None,
#[cfg(not(feature = "pq"))]
_signer: std::marker::PhantomData,
}
}
#[cfg(feature = "pq")]
pub fn generate() -> Result<Self> {
let signer = PQSignature::generate()?;
let public_key = signer.public_key_bytes();
let did = Did::from_mldsa_key_lux(&public_key)?;
Ok(Self {
did,
public_key,
stake: None,
stake_registry: None,
signer: Some(signer),
})
}
#[cfg(not(feature = "pq"))]
pub fn generate() -> Result<Self> {
Err(Error::Identity(
"node identity generation requires 'pq' feature".to_string(),
))
}
#[cfg(feature = "pq")]
pub fn sign(&self, message: &[u8]) -> Result<Vec<u8>> {
let signer = self
.signer
.as_ref()
.ok_or_else(|| Error::Identity("no private key available for signing".to_string()))?;
signer.sign(message)
}
#[cfg(not(feature = "pq"))]
pub fn sign(&self, _message: &[u8]) -> Result<Vec<u8>> {
Err(Error::Identity(
"signing requires 'pq' feature".to_string(),
))
}
#[cfg(feature = "pq")]
pub fn verify(&self, message: &[u8], signature: &[u8]) -> Result<()> {
let verifier = match &self.signer {
Some(s) => s.verify(message, signature)?,
None => {
let v = PQSignature::from_public_key(&self.public_key)?;
v.verify(message, signature)?;
}
};
Ok(verifier)
}
#[cfg(not(feature = "pq"))]
pub fn verify(&self, _message: &[u8], _signature: &[u8]) -> Result<()> {
Err(Error::Identity(
"verification requires 'pq' feature".to_string(),
))
}
pub fn with_stake(mut self, amount: u64) -> Self {
self.stake = Some(amount);
self
}
pub fn with_registry(mut self, registry: String) -> Self {
self.stake_registry = Some(registry);
self
}
pub fn document(&self) -> Result<DidDocument> {
self.did.document()
}
pub fn can_sign(&self) -> bool {
#[cfg(feature = "pq")]
{
self.signer.is_some()
}
#[cfg(not(feature = "pq"))]
{
false
}
}
}
pub trait StakeRegistry: Send + Sync {
fn get_stake(&self, did: &Did) -> Result<u64>;
fn set_stake(&mut self, did: &Did, amount: u64) -> Result<()>;
fn has_sufficient_stake(&self, did: &Did, minimum: u64) -> Result<bool> {
Ok(self.get_stake(did)? >= minimum)
}
fn total_stake(&self) -> Result<u64>;
fn stake_weight(&self, did: &Did) -> Result<f64> {
let stake = self.get_stake(did)?;
let total = self.total_stake()?;
if total == 0 {
return Ok(0.0);
}
Ok(stake as f64 / total as f64)
}
}
#[derive(Debug, Default)]
pub struct InMemoryStakeRegistry {
stakes: std::collections::HashMap<String, u64>,
}
impl InMemoryStakeRegistry {
pub fn new() -> Self {
Self::default()
}
}
impl StakeRegistry for InMemoryStakeRegistry {
fn get_stake(&self, did: &Did) -> Result<u64> {
Ok(*self.stakes.get(&did.uri()).unwrap_or(&0))
}
fn set_stake(&mut self, did: &Did, amount: u64) -> Result<()> {
self.stakes.insert(did.uri(), amount);
Ok(())
}
fn total_stake(&self) -> Result<u64> {
Ok(self.stakes.values().sum())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_did_parse_lux() {
let did = Did::parse("did:lux:z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK").unwrap();
assert_eq!(did.method, DidMethod::Lux);
assert_eq!(did.id, "z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK");
}
#[test]
fn test_did_parse_key() {
let did = Did::parse("did:key:z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK").unwrap();
assert_eq!(did.method, DidMethod::Key);
}
#[test]
fn test_did_parse_web() {
let did = Did::parse("did:web:example.com:users:alice").unwrap();
assert_eq!(did.method, DidMethod::Web);
assert_eq!(did.id, "example.com:users:alice");
}
#[test]
fn test_did_parse_invalid() {
assert!(Did::parse("not-a-did").is_err());
assert!(Did::parse("did:unknown:abc").is_err());
assert!(Did::parse("did:lux:").is_err());
}
#[test]
fn test_did_from_web() {
let did = Did::from_web("example.com", Some("users/alice")).unwrap();
assert_eq!(did.uri(), "did:web:example.com:users:alice");
let did2 = Did::from_web("example.com", None).unwrap();
assert_eq!(did2.uri(), "did:web:example.com");
}
#[test]
fn test_did_method_display() {
assert_eq!(DidMethod::Lux.to_string(), "lux");
assert_eq!(DidMethod::Key.to_string(), "key");
assert_eq!(DidMethod::Web.to_string(), "web");
}
#[test]
fn test_did_document_generation() {
let did = Did::parse("did:key:z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK").unwrap();
let doc = did.document().unwrap();
assert_eq!(doc.id, "did:key:z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK");
assert!(!doc.verification_method.is_empty());
assert!(!doc.authentication.is_empty());
assert!(!doc.service.is_empty());
}
#[test]
fn test_did_document_json_roundtrip() {
let did = Did::parse("did:key:z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK").unwrap();
let doc = did.document().unwrap();
let json = doc.to_json().unwrap();
let parsed = DidDocument::from_json(&json).unwrap();
assert_eq!(doc.id, parsed.id);
assert_eq!(doc.verification_method.len(), parsed.verification_method.len());
}
#[test]
fn test_stake_registry() {
let mut registry = InMemoryStakeRegistry::new();
let did = Did::parse("did:lux:z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK").unwrap();
assert_eq!(registry.get_stake(&did).unwrap(), 0);
registry.set_stake(&did, 1000).unwrap();
assert_eq!(registry.get_stake(&did).unwrap(), 1000);
assert!(registry.has_sufficient_stake(&did, 500).unwrap());
assert!(!registry.has_sufficient_stake(&did, 2000).unwrap());
assert_eq!(registry.total_stake().unwrap(), 1000);
}
#[test]
fn test_stake_weight() {
let mut registry = InMemoryStakeRegistry::new();
let did1 = Did::parse("did:lux:z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK").unwrap();
let did2 = Did::parse("did:lux:z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doL").unwrap();
registry.set_stake(&did1, 750).unwrap();
registry.set_stake(&did2, 250).unwrap();
let weight1 = registry.stake_weight(&did1).unwrap();
let weight2 = registry.stake_weight(&did2).unwrap();
assert!((weight1 - 0.75).abs() < 0.001);
assert!((weight2 - 0.25).abs() < 0.001);
}
#[test]
fn test_node_identity_new() {
let did = Did::parse("did:lux:z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK").unwrap();
let identity = NodeIdentity::new(did.clone(), vec![0u8; 1952]);
assert_eq!(identity.did, did);
assert_eq!(identity.stake, None);
assert!(!identity.can_sign());
}
#[test]
fn test_node_identity_with_stake() {
let did = Did::parse("did:lux:z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK").unwrap();
let identity = NodeIdentity::new(did, vec![0u8; 1952])
.with_stake(5000)
.with_registry("lux:mainnet".to_string());
assert_eq!(identity.stake, Some(5000));
assert_eq!(identity.stake_registry, Some("lux:mainnet".to_string()));
}
#[cfg(feature = "pq")]
#[test]
fn test_node_identity_generate() {
let identity = NodeIdentity::generate().unwrap();
assert!(identity.can_sign());
assert_eq!(identity.did.method, DidMethod::Lux);
assert!(!identity.public_key.is_empty());
let message = b"test message";
let signature = identity.sign(message).unwrap();
identity.verify(message, &signature).unwrap();
}
#[cfg(feature = "pq")]
#[test]
fn test_did_from_mldsa_key() {
use crate::crypto::PQSignature;
let signer = PQSignature::generate().unwrap();
let public_key = signer.public_key_bytes();
let did = Did::from_mldsa_key(&public_key).unwrap();
assert_eq!(did.method, DidMethod::Key);
assert!(did.id.starts_with('z'));
let doc = did.document().unwrap();
assert!(!doc.verification_method.is_empty());
}
}