use serde::{Deserialize, Serialize};
use serde_big_array::BigArray;
use crate::agreement::{EncryptionAlgorithm, EncryptionMeta};
use crate::{Address, Balance, BlockHeight, Timestamp};
pub type SubjectId = [u8; 32];
pub type PolicyId = [u8; 32];
pub type ClaimId = [u8; 32];
pub type ProofId = [u8; 32];
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[repr(u8)]
pub enum ClaimType {
Identity = 0,
Eligibility = 1,
Education = 2,
License = 3,
Employment = 4,
Healthcare = 5,
Financial = 6,
Custom = 255,
}
impl ClaimType {
pub fn from_u8(v: u8) -> Option<Self> {
match v {
0 => Some(ClaimType::Identity),
1 => Some(ClaimType::Eligibility),
2 => Some(ClaimType::Education),
3 => Some(ClaimType::License),
4 => Some(ClaimType::Employment),
5 => Some(ClaimType::Healthcare),
6 => Some(ClaimType::Financial),
255 => Some(ClaimType::Custom),
_ => None,
}
}
pub fn name(&self) -> &'static str {
match self {
ClaimType::Identity => "Identity",
ClaimType::Eligibility => "Eligibility",
ClaimType::Education => "Education",
ClaimType::License => "License",
ClaimType::Employment => "Employment",
ClaimType::Healthcare => "Healthcare",
ClaimType::Financial => "Financial",
ClaimType::Custom => "Custom",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[repr(u16)]
pub enum DocSubcode {
Subject = 801,
IssuerRegistry = 802,
Policy = 803,
Claim = 804,
Revocation = 805,
ProofLog = 806,
IdentityRoot = 800,
EligibilityAttestation = 807,
AcademicTranscript = 810,
Diploma = 811,
EnrollmentVerification = 812,
ProfessionalLicense = 813,
GovernmentId = 814,
Employment = 815,
}
impl DocSubcode {
pub fn is_core_standard(&self) -> bool {
(*self as u16) >= 800 && (*self as u16) < 810
}
pub fn is_identity_class(&self) -> bool {
self.is_core_standard()
}
pub fn is_domain_claim(&self) -> bool {
(*self as u16) >= 810 && (*self as u16) < 820
}
pub fn is_academic_class(&self) -> bool {
self.is_domain_claim()
}
pub fn as_u16(&self) -> u16 {
*self as u16
}
pub fn from_u16(code: u16) -> Option<Self> {
match code {
800 => Some(DocSubcode::IdentityRoot),
801 => Some(DocSubcode::Subject),
802 => Some(DocSubcode::IssuerRegistry),
803 => Some(DocSubcode::Policy),
804 => Some(DocSubcode::Claim),
805 => Some(DocSubcode::Revocation),
806 => Some(DocSubcode::ProofLog),
807 => Some(DocSubcode::EligibilityAttestation),
810 => Some(DocSubcode::AcademicTranscript),
811 => Some(DocSubcode::Diploma),
812 => Some(DocSubcode::EnrollmentVerification),
813 => Some(DocSubcode::ProfessionalLicense),
814 => Some(DocSubcode::GovernmentId),
815 => Some(DocSubcode::Employment),
_ => None,
}
}
pub fn name(&self) -> &'static str {
match self {
DocSubcode::IdentityRoot => "Identity Root (Legacy)",
DocSubcode::Subject => "Subject",
DocSubcode::IssuerRegistry => "Issuer Registry",
DocSubcode::Policy => "Policy",
DocSubcode::Claim => "Claim",
DocSubcode::Revocation => "Revocation",
DocSubcode::ProofLog => "Proof Log",
DocSubcode::EligibilityAttestation => "Eligibility Attestation",
DocSubcode::AcademicTranscript => "Academic Transcript",
DocSubcode::Diploma => "Diploma/Degree",
DocSubcode::EnrollmentVerification => "Enrollment Verification",
DocSubcode::ProfessionalLicense => "Professional License",
DocSubcode::GovernmentId => "Government ID",
DocSubcode::Employment => "Employment",
}
}
pub fn to_claim_type(&self) -> Option<ClaimType> {
match self {
DocSubcode::EligibilityAttestation => Some(ClaimType::Eligibility),
DocSubcode::AcademicTranscript => Some(ClaimType::Education),
DocSubcode::Diploma => Some(ClaimType::Education),
DocSubcode::EnrollmentVerification => Some(ClaimType::Education),
DocSubcode::ProfessionalLicense => Some(ClaimType::License),
DocSubcode::GovernmentId => Some(ClaimType::Identity),
DocSubcode::Employment => Some(ClaimType::Employment),
_ => None,
}
}
}
pub type CredentialId = [u8; 32];
pub fn generate_credential_id(
issuer: &Address,
subcode: DocSubcode,
subject_commitment: &[u8; 32],
nonce: u64,
) -> CredentialId {
let mut data = Vec::with_capacity(20 + 2 + 32 + 8);
data.extend_from_slice(issuer.as_bytes());
data.extend_from_slice(&(subcode.as_u16()).to_be_bytes());
data.extend_from_slice(subject_commitment);
data.extend_from_slice(&nonce.to_be_bytes());
*blake3::hash(&data).as_bytes()
}
pub const COMMITMENT_DOMAIN_SEP: &[u8] = b"SRC-8XX-COMMITMENT-v1";
pub fn generate_commitment(
schema_hash: &[u8; 32],
canonical_attributes: &[u8],
salt: &[u8; 32],
) -> [u8; 32] {
let mut hasher = blake3::Hasher::new();
hasher.update(COMMITMENT_DOMAIN_SEP);
hasher.update(schema_hash);
hasher.update(canonical_attributes);
hasher.update(salt);
*hasher.finalize().as_bytes()
}
pub fn generate_subject_commitment(subject_identifier: &[u8], salt: &[u8; 32]) -> [u8; 32] {
let mut hasher = blake3::Hasher::new();
hasher.update(b"SRC-8XX-SUBJECT-v1");
hasher.update(subject_identifier);
hasher.update(salt);
*hasher.finalize().as_bytes()
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct IdentityRoot {
pub identity_id: CredentialId,
pub subject_commitment: [u8; 32],
pub controller: Address,
pub additional_controllers: Vec<Address>,
pub keys: Vec<IdentityKey>,
pub services: Vec<ServiceEndpoint>,
pub created_at: Timestamp,
pub updated_at: Timestamp,
pub status: IdentityStatus,
pub schema_hash: [u8; 32],
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct IdentityKey {
pub key_id: String,
pub key_type: KeyType,
pub public_key: [u8; 32],
pub purposes: Vec<KeyPurpose>,
pub added_at: Timestamp,
pub expires_at: Timestamp,
pub active: bool,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[repr(u8)]
pub enum KeyType {
Ed25519 = 0,
X25519 = 1,
Secp256k1 = 2,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[repr(u8)]
pub enum KeyPurpose {
Authentication = 0,
Assertion = 1,
KeyAgreement = 2,
CapabilityInvocation = 3,
CapabilityDelegation = 4,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ServiceEndpoint {
pub service_id: String,
pub service_type: String,
pub endpoint: String,
pub description: Option<String>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[repr(u8)]
pub enum IdentityStatus {
Active = 0,
Deactivated = 1,
Revoked = 2,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[repr(u8)]
pub enum EligibilityType {
Citizenship = 0,
Residency = 1,
AgeEligibility = 2,
VoterEligibility = 3,
CivilRegistry = 4,
Custom = 255,
}
impl EligibilityType {
pub fn from_u8(v: u8) -> Option<Self> {
match v {
0 => Some(EligibilityType::Citizenship),
1 => Some(EligibilityType::Residency),
2 => Some(EligibilityType::AgeEligibility),
3 => Some(EligibilityType::VoterEligibility),
4 => Some(EligibilityType::CivilRegistry),
255 => Some(EligibilityType::Custom),
_ => None,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct EligibilityAttestation {
pub credential_id: CredentialId,
pub subject_address: Address,
pub subcode: DocSubcode,
pub subject_commitment: [u8; 32],
pub issuer: Address,
pub jurisdiction: String,
pub eligibility_type: EligibilityType,
pub schema_hash: [u8; 32],
pub content_commitment: [u8; 32],
pub issued_at: Timestamp,
pub valid_from: Timestamp,
pub expires_at: Timestamp,
pub payload_hash: Option<[u8; 32]>,
pub payload_hint: Option<String>,
pub encryption_meta: Option<EncryptionMeta>,
#[serde(with = "BigArray")]
pub issuer_signature: [u8; 64],
pub issuer_key_id: String,
pub revocation_status: RevocationStatus,
pub superseded_by: Option<CredentialId>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[repr(u8)]
pub enum RevocationStatus {
Active = 0,
Suspended = 1,
Revoked = 2,
Superseded = 3,
Expired = 4,
}
impl RevocationStatus {
pub fn from_u8(v: u8) -> Option<Self> {
match v {
0 => Some(RevocationStatus::Active),
1 => Some(RevocationStatus::Suspended),
2 => Some(RevocationStatus::Revoked),
3 => Some(RevocationStatus::Superseded),
4 => Some(RevocationStatus::Expired),
_ => None,
}
}
pub fn is_valid(&self) -> bool {
matches!(self, RevocationStatus::Active)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[repr(u8)]
pub enum RevocationReason {
Unspecified = 0,
KeyCompromise = 1,
IssuerCompromise = 2,
AffiliationChanged = 3,
Superseded = 4,
CessationOfOperation = 5,
CertificateHold = 6,
PrivilegeWithdrawn = 7,
Expired = 8,
FraudulentIssuance = 9,
}
impl RevocationReason {
pub fn from_u8(v: u8) -> Option<Self> {
match v {
0 => Some(RevocationReason::Unspecified),
1 => Some(RevocationReason::KeyCompromise),
2 => Some(RevocationReason::IssuerCompromise),
3 => Some(RevocationReason::AffiliationChanged),
4 => Some(RevocationReason::Superseded),
5 => Some(RevocationReason::CessationOfOperation),
6 => Some(RevocationReason::CertificateHold),
7 => Some(RevocationReason::PrivilegeWithdrawn),
8 => Some(RevocationReason::Expired),
9 => Some(RevocationReason::FraudulentIssuance),
_ => None,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct RevocationRecord {
pub credential_id: CredentialId,
pub status: RevocationStatus,
pub reason: RevocationReason,
pub reason_details: Option<String>,
pub revoker: Address,
pub revoked_at: Timestamp,
pub revoked_at_height: BlockHeight,
pub superseded_by: Option<CredentialId>,
#[serde(with = "BigArray")]
pub signature: [u8; 64],
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct AcademicCredential {
pub credential_id: CredentialId,
pub subject_address: Address,
pub subcode: DocSubcode,
pub subject_commitment: [u8; 32],
pub issuer: Address,
pub institution_id: String,
pub jurisdiction: String,
pub schema_hash: [u8; 32],
pub content_commitment: [u8; 32],
pub metadata: CredentialMetadata,
pub issued_at: Timestamp,
pub valid_from: Timestamp,
pub expires_at: Timestamp,
pub payload_hash: Option<[u8; 32]>,
pub payload_hint: Option<String>,
pub encryption_meta: Option<EncryptionMeta>,
#[serde(with = "BigArray")]
pub issuer_signature: [u8; 64],
pub issuer_key_id: String,
pub revocation_status: RevocationStatus,
pub superseded_by: Option<CredentialId>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct CredentialMetadata {
pub title: String,
pub credential_type: String,
pub program: Option<String>,
pub issue_date: String,
pub completion_date: Option<String>,
pub attributes: Vec<CredentialAttribute>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct CredentialAttribute {
pub name: String,
pub value: String,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[repr(u8)]
pub enum DocClassOperation {
CreateIdentityRoot = 0,
AddKey = 1,
RemoveKey = 2,
RotateKey = 3,
AddController = 4,
RemoveController = 5,
UpdateService = 6,
DeactivateIdentity = 7,
ReactivateIdentity = 8,
IssueCredential = 10,
UpdateCredential = 11,
RevokeCredential = 20,
SuspendCredential = 21,
ReactivateCredential = 22,
SupersedeCredential = 23,
RegisterIssuer = 30,
UpdateIssuer = 31,
RotateIssuerKey = 32,
DeactivateIssuer = 33,
}
impl DocClassOperation {
pub fn from_u8(v: u8) -> Option<Self> {
match v {
0 => Some(DocClassOperation::CreateIdentityRoot),
1 => Some(DocClassOperation::AddKey),
2 => Some(DocClassOperation::RemoveKey),
3 => Some(DocClassOperation::RotateKey),
4 => Some(DocClassOperation::AddController),
5 => Some(DocClassOperation::RemoveController),
6 => Some(DocClassOperation::UpdateService),
7 => Some(DocClassOperation::DeactivateIdentity),
8 => Some(DocClassOperation::ReactivateIdentity),
10 => Some(DocClassOperation::IssueCredential),
11 => Some(DocClassOperation::UpdateCredential),
20 => Some(DocClassOperation::RevokeCredential),
21 => Some(DocClassOperation::SuspendCredential),
22 => Some(DocClassOperation::ReactivateCredential),
23 => Some(DocClassOperation::SupersedeCredential),
30 => Some(DocClassOperation::RegisterIssuer),
31 => Some(DocClassOperation::UpdateIssuer),
32 => Some(DocClassOperation::RotateIssuerKey),
33 => Some(DocClassOperation::DeactivateIssuer),
_ => None,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct DocClassTxData {
pub operation: DocClassOperation,
pub subcode: DocSubcode,
pub data: Vec<u8>,
pub recipient: crate::Address,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct DocClassIssuer {
pub address: Address,
pub name: String,
pub issuer_type: DocClassIssuerType,
pub jurisdictions: Vec<String>,
pub authorized_subcodes: Vec<DocSubcode>,
pub keys: Vec<IssuerKey>,
pub registered_at: Timestamp,
pub updated_at: Timestamp,
pub status: DocClassIssuerStatus,
pub stake_amount: Balance,
pub metadata: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct IssuerKey {
pub key_id: String,
pub public_key: [u8; 32],
pub key_type: KeyType,
pub added_at: Timestamp,
pub expires_at: Timestamp,
pub active: bool,
pub is_primary: bool,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[repr(u8)]
pub enum DocClassIssuerType {
Government = 0,
Educational = 1,
Professional = 2,
Corporate = 3,
Healthcare = 4,
Legal = 5,
SelfSovereign = 6,
}
impl DocClassIssuerType {
pub fn from_u8(v: u8) -> Option<Self> {
match v {
0 => Some(DocClassIssuerType::Government),
1 => Some(DocClassIssuerType::Educational),
2 => Some(DocClassIssuerType::Professional),
3 => Some(DocClassIssuerType::Corporate),
4 => Some(DocClassIssuerType::Healthcare),
5 => Some(DocClassIssuerType::Legal),
6 => Some(DocClassIssuerType::SelfSovereign),
_ => None,
}
}
pub fn can_issue(&self, subcode: DocSubcode) -> bool {
match self {
DocClassIssuerType::Government => {
matches!(
subcode,
DocSubcode::EligibilityAttestation
| DocSubcode::GovernmentId
| DocSubcode::Revocation
)
}
DocClassIssuerType::Educational => {
matches!(
subcode,
DocSubcode::AcademicTranscript
| DocSubcode::Diploma
| DocSubcode::EnrollmentVerification
| DocSubcode::Revocation
)
}
DocClassIssuerType::Professional => {
matches!(
subcode,
DocSubcode::ProfessionalLicense | DocSubcode::Revocation
)
}
DocClassIssuerType::Corporate => {
matches!(
subcode,
DocSubcode::Employment | DocSubcode::Revocation
)
}
DocClassIssuerType::SelfSovereign => {
matches!(subcode, DocSubcode::IdentityRoot | DocSubcode::Subject)
}
_ => false,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[repr(u8)]
pub enum DocClassIssuerStatus {
Active = 0,
Suspended = 1,
Revoked = 2,
}
impl DocClassIssuerStatus {
pub fn from_u8(v: u8) -> Option<Self> {
match v {
0 => Some(DocClassIssuerStatus::Active),
1 => Some(DocClassIssuerStatus::Suspended),
2 => Some(DocClassIssuerStatus::Revoked),
_ => None,
}
}
pub fn can_issue(&self) -> bool {
matches!(self, DocClassIssuerStatus::Active)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ZkProofInputs {
pub credential_id: CredentialId,
pub issuer_public_key: [u8; 32],
pub issuer_key_id: String,
pub content_commitment: [u8; 32],
pub subject_commitment: [u8; 32],
pub jurisdiction: String,
pub eligibility_type: Option<EligibilityType>,
pub valid_from: Timestamp,
pub expires_at: Timestamp,
pub revocation_merkle_root: Option<[u8; 32]>,
pub current_block_height: BlockHeight,
#[serde(with = "BigArray")]
pub issuer_signature: [u8; 64],
}
pub fn generate_nullifier(credential_id: &CredentialId, context: &[u8], secret: &[u8; 32]) -> [u8; 32] {
let mut hasher = blake3::Hasher::new();
hasher.update(b"SRC-8XX-NULLIFIER-v1");
hasher.update(credential_id);
hasher.update(context);
hasher.update(secret);
*hasher.finalize().as_bytes()
}
pub mod canonical {
use serde::Serialize;
use std::collections::BTreeMap;
pub fn to_canonical_json<T: Serialize>(value: &T) -> Result<Vec<u8>, serde_json::Error> {
let json_value = serde_json::to_value(value)?;
let canonical = canonical_json_value(&json_value);
Ok(canonical.into_bytes())
}
fn canonical_json_value(value: &serde_json::Value) -> String {
match value {
serde_json::Value::Null => "null".to_string(),
serde_json::Value::Bool(b) => b.to_string(),
serde_json::Value::Number(n) => n.to_string(),
serde_json::Value::String(s) => serde_json::to_string(s).unwrap(),
serde_json::Value::Array(arr) => {
let elements: Vec<String> = arr.iter().map(canonical_json_value).collect();
format!("[{}]", elements.join(","))
}
serde_json::Value::Object(obj) => {
let sorted: BTreeMap<_, _> = obj.iter().collect();
let pairs: Vec<String> = sorted
.iter()
.map(|(k, v)| format!("{}:{}", serde_json::to_string(k).unwrap(), canonical_json_value(v)))
.collect();
format!("{{{}}}", pairs.join(","))
}
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum DocClassEvent {
IdentityRootCreated {
identity_id: CredentialId,
controller: Address,
subject_commitment: [u8; 32],
},
KeyAdded {
identity_id: CredentialId,
key_id: String,
key_type: KeyType,
},
KeyRemoved {
identity_id: CredentialId,
key_id: String,
},
KeyRotated {
identity_id: CredentialId,
old_key_id: String,
new_key_id: String,
},
ControllerAdded {
identity_id: CredentialId,
controller: Address,
},
ControllerRemoved {
identity_id: CredentialId,
controller: Address,
},
ServiceUpdated {
identity_id: CredentialId,
service_id: String,
},
IdentityStatusChanged {
identity_id: CredentialId,
new_status: IdentityStatus,
},
CredentialIssued {
credential_id: CredentialId,
subcode: DocSubcode,
issuer: Address,
jurisdiction: String,
subject_commitment: [u8; 32],
schema_hash: [u8; 32],
expires_at: Timestamp,
},
CredentialRevoked {
credential_id: CredentialId,
issuer: Address,
reason: RevocationReason,
timestamp: Timestamp,
},
CredentialSuspended {
credential_id: CredentialId,
issuer: Address,
reason: RevocationReason,
timestamp: Timestamp,
},
CredentialReactivated {
credential_id: CredentialId,
issuer: Address,
timestamp: Timestamp,
},
CredentialSuperseded {
old_credential_id: CredentialId,
new_credential_id: CredentialId,
issuer: Address,
timestamp: Timestamp,
},
IssuerRegistered {
issuer: Address,
issuer_type: DocClassIssuerType,
jurisdictions: Vec<String>,
subcodes: Vec<DocSubcode>,
},
IssuerUpdated {
issuer: Address,
},
IssuerKeyRotated {
issuer: Address,
old_key_id: String,
new_key_id: String,
},
IssuerStatusChanged {
issuer: Address,
new_status: DocClassIssuerStatus,
},
}
pub mod schemas {
pub const CITIZENSHIP_SCHEMA: [u8; 32] = [
0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x43, 0x49, 0x54, 0x49, 0x5a, 0x45, 0x4e, 0x53,
0x48, 0x49, 0x50, 0x5f, 0x56, 0x31, 0x00, 0x00,
];
pub const RESIDENCY_SCHEMA: [u8; 32] = [
0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x52, 0x45, 0x53, 0x49, 0x44, 0x45, 0x4e, 0x43,
0x59, 0x5f, 0x56, 0x31, 0x00, 0x00, 0x00, 0x00,
];
pub const AGE_ELIGIBILITY_SCHEMA: [u8; 32] = [
0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x41, 0x47, 0x45, 0x5f, 0x45, 0x4c, 0x49, 0x47,
0x5f, 0x56, 0x31, 0x00, 0x00, 0x00, 0x00, 0x00,
];
pub const TRANSCRIPT_SCHEMA: [u8; 32] = [
0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x54, 0x52, 0x41, 0x4e, 0x53, 0x43, 0x52, 0x49,
0x50, 0x54, 0x5f, 0x56, 0x31, 0x00, 0x00, 0x00,
];
pub const DIPLOMA_SCHEMA: [u8; 32] = [
0x11, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x44, 0x49, 0x50, 0x4c, 0x4f, 0x4d, 0x41, 0x5f,
0x56, 0x31, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
];
pub const LICENSE_SCHEMA: [u8; 32] = [
0x13, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x4c, 0x49, 0x43, 0x45, 0x4e, 0x53, 0x45, 0x5f,
0x56, 0x31, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
];
pub const IDENTITY_ROOT_SCHEMA: [u8; 32] = [
0x00, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x49, 0x44, 0x5f, 0x52, 0x4f, 0x4f, 0x54, 0x5f,
0x56, 0x31, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
];
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_subcode_classification() {
assert!(DocSubcode::IdentityRoot.is_identity_class());
assert!(DocSubcode::EligibilityAttestation.is_identity_class());
assert!(!DocSubcode::AcademicTranscript.is_identity_class());
assert!(!DocSubcode::IdentityRoot.is_academic_class());
assert!(DocSubcode::AcademicTranscript.is_academic_class());
assert!(DocSubcode::Diploma.is_academic_class());
assert!(DocSubcode::ProfessionalLicense.is_academic_class());
}
#[test]
fn test_commitment_determinism() {
let schema_hash = [1u8; 32];
let attributes = b"test_attributes";
let salt = [2u8; 32];
let c1 = generate_commitment(&schema_hash, attributes, &salt);
let c2 = generate_commitment(&schema_hash, attributes, &salt);
assert_eq!(c1, c2);
let different_salt = [3u8; 32];
let c3 = generate_commitment(&schema_hash, attributes, &different_salt);
assert_ne!(c1, c3);
}
#[test]
fn test_credential_id_generation() {
let issuer = Address::new([1u8; 20]);
let subcode = DocSubcode::EligibilityAttestation;
let subject_commitment = [2u8; 32];
let nonce = 12345u64;
let id1 = generate_credential_id(&issuer, subcode, &subject_commitment, nonce);
let id2 = generate_credential_id(&issuer, subcode, &subject_commitment, nonce);
assert_eq!(id1, id2);
let id3 = generate_credential_id(&issuer, subcode, &subject_commitment, nonce + 1);
assert_ne!(id1, id3);
}
#[test]
fn test_nullifier_generation() {
let credential_id = [1u8; 32];
let context = b"election_2024";
let secret = [2u8; 32];
let n1 = generate_nullifier(&credential_id, context, &secret);
let n2 = generate_nullifier(&credential_id, context, &secret);
assert_eq!(n1, n2);
let n3 = generate_nullifier(&credential_id, b"election_2025", &secret);
assert_ne!(n1, n3);
}
#[test]
fn test_canonical_json() {
use serde_json::json;
let value = json!({
"name": "John",
"age": 21,
"country": "US"
});
let canonical = canonical::to_canonical_json(&value).unwrap();
let canonical_str = String::from_utf8(canonical).unwrap();
assert_eq!(canonical_str, r#"{"age":21,"country":"US","name":"John"}"#);
}
#[test]
fn test_issuer_type_permissions() {
assert!(DocClassIssuerType::Government.can_issue(DocSubcode::EligibilityAttestation));
assert!(!DocClassIssuerType::Government.can_issue(DocSubcode::AcademicTranscript));
assert!(DocClassIssuerType::Educational.can_issue(DocSubcode::Diploma));
assert!(DocClassIssuerType::Educational.can_issue(DocSubcode::AcademicTranscript));
assert!(!DocClassIssuerType::Educational.can_issue(DocSubcode::EligibilityAttestation));
assert!(DocClassIssuerType::Professional.can_issue(DocSubcode::ProfessionalLicense));
assert!(!DocClassIssuerType::Professional.can_issue(DocSubcode::Diploma));
assert!(DocClassIssuerType::SelfSovereign.can_issue(DocSubcode::IdentityRoot));
assert!(!DocClassIssuerType::SelfSovereign.can_issue(DocSubcode::EligibilityAttestation));
}
#[test]
fn test_revocation_status() {
assert!(RevocationStatus::Active.is_valid());
assert!(!RevocationStatus::Revoked.is_valid());
assert!(!RevocationStatus::Suspended.is_valid());
assert!(!RevocationStatus::Superseded.is_valid());
assert!(!RevocationStatus::Expired.is_valid());
}
}