From 5e7e36ee7e58a63dcf82503e8c7b9764fcccfba5 Mon Sep 17 00:00:00 2001 From: OthmanImam Date: Sat, 21 Feb 2026 22:18:36 +0100 Subject: [PATCH 1/2] Implemented Advance Decentralized --- Cargo.toml | 2 + contracts/IDENTITY_CREDENTIAL.md | 23 ++++++ contracts/credential_registry/Cargo.toml | 22 ++++++ contracts/credential_registry/src/lib.rs | 72 ++++++++++++++++++ contracts/identity_registry/Cargo.toml | 22 ++++++ contracts/identity_registry/src/lib.rs | 93 ++++++++++++++++++++++++ 6 files changed, 234 insertions(+) create mode 100644 contracts/IDENTITY_CREDENTIAL.md create mode 100644 contracts/credential_registry/Cargo.toml create mode 100644 contracts/credential_registry/src/lib.rs create mode 100644 contracts/identity_registry/Cargo.toml create mode 100644 contracts/identity_registry/src/lib.rs diff --git a/Cargo.toml b/Cargo.toml index e371d29..cfa13fe 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,6 +4,8 @@ members = [ "contracts/governance", "contracts/insurance", "contracts/teachlink", + "contracts/identity_registry", + "contracts/credential_registry", ] [workspace.package] diff --git a/contracts/IDENTITY_CREDENTIAL.md b/contracts/IDENTITY_CREDENTIAL.md new file mode 100644 index 0000000..117f0fa --- /dev/null +++ b/contracts/IDENTITY_CREDENTIAL.md @@ -0,0 +1,23 @@ +**Identity & Credential Contracts (Soroban)** + +Overview +- `identity-registry`: on-chain DID registry. Stores DID -> controller, auth methods, recovery address. +- `credential-registry`: on-chain credential index. Stores credential hash -> (issuer DID, subject DID, metadata pointer, expires_at, status). + +Key on-chain guarantees +- Deterministic verification can check presence and status of a credential on-chain. +- Full VC JSON and ZK proofs remain off-chain; only hashes/roots and status bits stored on-chain. + +Next steps / integration notes +- Wire `credential-registry` to call `identity-registry` for authoritative issuer controller checks. +- Add Merkle/bitmap-based revocation root support for efficient revocation proofs. +- Implement cross-contract calls and auth to allow DID controllers (not raw addresses) to issue/revoke. +- Add off-chain ZK proof verifier support: store verification circuits' commitment roots on-chain and provide helper APIs for verifiers. +- Marketplace, federation, selective-disclosure circuits, and biometric-binding are implemented off-chain; contracts store anchors/roots. + +Files added +- `contracts/identity_registry` — Cargo + src/lib.rs +- `contracts/credential_registry` — Cargo + src/lib.rs + +Testing & build +- Use the workspace's soroban toolchain and existing patterns (see other `contracts/*` crates) to build and test. diff --git a/contracts/credential_registry/Cargo.toml b/contracts/credential_registry/Cargo.toml new file mode 100644 index 0000000..bce73ef --- /dev/null +++ b/contracts/credential_registry/Cargo.toml @@ -0,0 +1,22 @@ +[package] +name = "credential-registry" +version = "0.1.0" +edition.workspace = true +repository.workspace = true +license.workspace = true + +[lib] +crate-type = ["cdylib", "rlib"] + +[dependencies] +soroban-sdk.workspace = true + +[dev-dependencies] +soroban-sdk = { workspace = true, features = ["testutils"] } + +[features] +testutils = ["soroban-sdk/testutils"] + +[lints] +workspace = true + diff --git a/contracts/credential_registry/src/lib.rs b/contracts/credential_registry/src/lib.rs new file mode 100644 index 0000000..ee1736d --- /dev/null +++ b/contracts/credential_registry/src/lib.rs @@ -0,0 +1,72 @@ + +#![no_std] + + +use soroban_sdk::{contract, contractimpl, symbol_short, Address, Env, Bytes, BytesN}; + +#[contract] +pub struct CredentialRegistryContract; + +#[derive(Clone)] +pub enum CredentialStatus { + Active, + Revoked, + Expired, +} + +#[contractimpl] +impl CredentialRegistryContract { + // Issue a credential by storing its hash and metadata pointer. + // `credential_hash` should be a deterministic hash (e.g., SHA-256) of the full VC JSON. + pub fn issue_credential( + env: &Env, + credential_hash: BytesN<32>, + issuer: Address, + issuer_did: Bytes, + subject_did: Bytes, + metadata_ptr: Bytes, + expires_at: i128, + ) { + issuer.require_auth(); + let key = (symbol_short!("cred"), credential_hash.clone()); + assert!(!env.storage().persistent().has(&key), "credential already exists"); + let record: (Bytes, Bytes, Bytes, i128, i32) = (issuer_did.clone(), subject_did.clone(), metadata_ptr.clone(), expires_at, 0i32); + env.storage().persistent().set(&key, &record); + env.events().publish((symbol_short!("crediss"),), (credential_hash, issuer_did, subject_did, metadata_ptr, expires_at)); + } + + // Revoke a credential. Caller must be issuer (signed address) + pub fn revoke_credential(env: &Env, credential_hash: BytesN<32>, issuer: Address) { + issuer.require_auth(); + let key = (symbol_short!("cred"), credential_hash.clone()); + let opt: Option<(Bytes, Bytes, Bytes, i128, i32)> = env.storage().persistent().get(&key); + match opt { + Some((issuer_did, subject_did, metadata_ptr, expires_at, _status)) => { + let record: (Bytes, Bytes, Bytes, i128, i32) = (issuer_did.clone(), subject_did.clone(), metadata_ptr.clone(), expires_at, 1i32); + env.storage().persistent().set(&key, &record); + env.events().publish((symbol_short!("credrev"),), (credential_hash, issuer_did, subject_did)); + } + None => panic!("credential not found"), + } + } + + // Get credential record: returns (issuer_did, subject_did, metadata_ptr, expires_at, status) + pub fn get_credential(env: &Env, credential_hash: BytesN<32>) -> Option<(Bytes, Bytes, Bytes, i128, i32)> { + let key = (symbol_short!("cred"), credential_hash.clone()); + env.storage().persistent().get(&key) + } + + // Check if credential is active (not revoked and not expired) + pub fn is_active(env: &Env, credential_hash: BytesN<32>, now_ts: i128) -> bool { + match Self::get_credential(env, credential_hash.clone()) { + Some((_issuer, _subject, _meta, expires_at, status)) => { + if status == 1 { return false; } + if expires_at > 0 && now_ts > expires_at { return false; } + true + } + None => false, + } + } +} + +fn main() {} diff --git a/contracts/identity_registry/Cargo.toml b/contracts/identity_registry/Cargo.toml new file mode 100644 index 0000000..e90a9b5 --- /dev/null +++ b/contracts/identity_registry/Cargo.toml @@ -0,0 +1,22 @@ +[package] +name = "identity-registry" +version = "0.1.0" +edition.workspace = true +repository.workspace = true +license.workspace = true + +[lib] +crate-type = ["cdylib", "rlib"] + +[dependencies] +soroban-sdk.workspace = true + +[dev-dependencies] +soroban-sdk = { workspace = true, features = ["testutils"] } + +[features] +testutils = ["soroban-sdk/testutils"] + +[lints] +workspace = true + diff --git a/contracts/identity_registry/src/lib.rs b/contracts/identity_registry/src/lib.rs new file mode 100644 index 0000000..967a5a8 --- /dev/null +++ b/contracts/identity_registry/src/lib.rs @@ -0,0 +1,93 @@ +#![no_std] + +use soroban_sdk::{contract, contractimpl, symbol_short, Address, Env, Bytes, BytesN}; + +#[contract] +pub struct IdentityRegistryContract; + +#[contractimpl] +impl IdentityRegistryContract { + // Create a new DID mapping to a controller address. + pub fn create_did(env: &Env, identity_id: BytesN<32>, controller: Address) { + controller.require_auth(); + let key = (symbol_short!("didctl"), identity_id.clone()); + assert!(!env.storage().persistent().has(&key), "DID already exists"); + env.storage().persistent().set(&key, &controller); + env.events().publish((symbol_short!("didcrt"),), (identity_id, controller)); + } + + // Get controller for a DID + pub fn get_controller(env: &Env, identity_id: BytesN<32>) -> Option
{ + let key = (symbol_short!("didctl"), identity_id.clone()); + env.storage().persistent().get(&key) + } + + // Update controller. Caller must pass the current controller and sign the call. + pub fn set_controller(env: &Env, identity_id: BytesN<32>, current_controller: Address, new_controller: Address) { + current_controller.require_auth(); + let key = (symbol_short!("did_ctrl"), identity_id.clone()); + let opt: Option
= env.storage().persistent().get(&key); + match opt { + Some(stored) => { + assert!(stored == current_controller, "only controller can change controller"); + env.storage().persistent().set(&key, &new_controller); + env.events().publish((symbol_short!("didchg"),), (identity_id, new_controller)); + } + None => panic!("DID not found"), + } + } + + // Add or update an authentication method (e.g., key, service) for a DID + pub fn set_auth_method(env: &Env, identity_id: BytesN<32>, controller: Address, method_id: Bytes, public_key: Bytes) { + controller.require_auth(); + let ctrl_key = (symbol_short!("didctl"), identity_id.clone()); + let current: Option
= env.storage().persistent().get(&ctrl_key); + assert!(current.is_some(), "DID not found"); + assert!(current.unwrap() == controller, "only controller can set auth methods"); + let key = (symbol_short!("auth"), identity_id.clone(), method_id.clone()); + env.storage().persistent().set(&key, &public_key); + env.events().publish((symbol_short!("authset"),), (identity_id, method_id, public_key)); + } + + // Remove an auth method + pub fn remove_auth_method(env: &Env, identity_id: BytesN<32>, controller: Address, method_id: Bytes) { + controller.require_auth(); + let ctrl_key = (symbol_short!("did_ctrl"), identity_id.clone()); + let current: Option
= env.storage().persistent().get(&ctrl_key); + assert!(current.is_some(), "DID not found"); + assert!(current.unwrap() == controller, "only controller can remove auth methods"); + let key = (symbol_short!("auth"), identity_id.clone(), method_id.clone()); + env.storage().persistent().remove(&key); + env.events().publish((symbol_short!("authrem"),), (identity_id, method_id)); + } + + // Set a recovery address that may be used to recover control of the DID + pub fn set_recovery(env: &Env, identity_id: BytesN<32>, controller: Address, recovery: Address) { + controller.require_auth(); + let ctrl_key = (symbol_short!("did_ctrl"), identity_id.clone()); + let current: Option
= env.storage().persistent().get(&ctrl_key); + assert!(current.is_some(), "DID not found"); + assert!(current.unwrap() == controller, "only controller can set recovery"); + let key = (symbol_short!("recovery"), identity_id.clone()); + env.storage().persistent().set(&key, &recovery); + env.events().publish((symbol_short!("recovery"),), (identity_id, recovery)); + } + + // Recover controller using the configured recovery address + pub fn recover(env: &Env, identity_id: BytesN<32>, recovery: Address, new_controller: Address) { + recovery.require_auth(); + let rec_key = (symbol_short!("recovery"), identity_id.clone()); + let rec_opt: Option
= env.storage().persistent().get(&rec_key); + match rec_opt { + Some(recovery_addr) => { + assert!(recovery_addr == recovery, "only recovery address can perform recovery"); + let ctrl_key = (symbol_short!("did_ctrl"), identity_id.clone()); + env.storage().persistent().set(&ctrl_key, &new_controller); + env.events().publish((symbol_short!("didrec"),), (identity_id, new_controller)); + } + None => panic!("no recovery configured for DID"), + } + } +} + +fn main() {} From 06eb41593a23579d3e1e22e9a42c889635b57dd8 Mon Sep 17 00:00:00 2001 From: OthmanImam Date: Sat, 21 Feb 2026 22:19:08 +0100 Subject: [PATCH 2/2] run format --- contracts/credential_registry/src/lib.rs | 54 ++++++++++++--- contracts/identity_registry/src/lib.rs | 88 +++++++++++++++++++----- 2 files changed, 113 insertions(+), 29 deletions(-) diff --git a/contracts/credential_registry/src/lib.rs b/contracts/credential_registry/src/lib.rs index ee1736d..8b69510 100644 --- a/contracts/credential_registry/src/lib.rs +++ b/contracts/credential_registry/src/lib.rs @@ -1,8 +1,6 @@ - #![no_std] - -use soroban_sdk::{contract, contractimpl, symbol_short, Address, Env, Bytes, BytesN}; +use soroban_sdk::{contract, contractimpl, symbol_short, Address, Bytes, BytesN, Env}; #[contract] pub struct CredentialRegistryContract; @@ -29,10 +27,28 @@ impl CredentialRegistryContract { ) { issuer.require_auth(); let key = (symbol_short!("cred"), credential_hash.clone()); - assert!(!env.storage().persistent().has(&key), "credential already exists"); - let record: (Bytes, Bytes, Bytes, i128, i32) = (issuer_did.clone(), subject_did.clone(), metadata_ptr.clone(), expires_at, 0i32); + assert!( + !env.storage().persistent().has(&key), + "credential already exists" + ); + let record: (Bytes, Bytes, Bytes, i128, i32) = ( + issuer_did.clone(), + subject_did.clone(), + metadata_ptr.clone(), + expires_at, + 0i32, + ); env.storage().persistent().set(&key, &record); - env.events().publish((symbol_short!("crediss"),), (credential_hash, issuer_did, subject_did, metadata_ptr, expires_at)); + env.events().publish( + (symbol_short!("crediss"),), + ( + credential_hash, + issuer_did, + subject_did, + metadata_ptr, + expires_at, + ), + ); } // Revoke a credential. Caller must be issuer (signed address) @@ -42,16 +58,28 @@ impl CredentialRegistryContract { let opt: Option<(Bytes, Bytes, Bytes, i128, i32)> = env.storage().persistent().get(&key); match opt { Some((issuer_did, subject_did, metadata_ptr, expires_at, _status)) => { - let record: (Bytes, Bytes, Bytes, i128, i32) = (issuer_did.clone(), subject_did.clone(), metadata_ptr.clone(), expires_at, 1i32); + let record: (Bytes, Bytes, Bytes, i128, i32) = ( + issuer_did.clone(), + subject_did.clone(), + metadata_ptr.clone(), + expires_at, + 1i32, + ); env.storage().persistent().set(&key, &record); - env.events().publish((symbol_short!("credrev"),), (credential_hash, issuer_did, subject_did)); + env.events().publish( + (symbol_short!("credrev"),), + (credential_hash, issuer_did, subject_did), + ); } None => panic!("credential not found"), } } // Get credential record: returns (issuer_did, subject_did, metadata_ptr, expires_at, status) - pub fn get_credential(env: &Env, credential_hash: BytesN<32>) -> Option<(Bytes, Bytes, Bytes, i128, i32)> { + pub fn get_credential( + env: &Env, + credential_hash: BytesN<32>, + ) -> Option<(Bytes, Bytes, Bytes, i128, i32)> { let key = (symbol_short!("cred"), credential_hash.clone()); env.storage().persistent().get(&key) } @@ -60,8 +88,12 @@ impl CredentialRegistryContract { pub fn is_active(env: &Env, credential_hash: BytesN<32>, now_ts: i128) -> bool { match Self::get_credential(env, credential_hash.clone()) { Some((_issuer, _subject, _meta, expires_at, status)) => { - if status == 1 { return false; } - if expires_at > 0 && now_ts > expires_at { return false; } + if status == 1 { + return false; + } + if expires_at > 0 && now_ts > expires_at { + return false; + } true } None => false, diff --git a/contracts/identity_registry/src/lib.rs b/contracts/identity_registry/src/lib.rs index 967a5a8..9fd5934 100644 --- a/contracts/identity_registry/src/lib.rs +++ b/contracts/identity_registry/src/lib.rs @@ -1,6 +1,6 @@ #![no_std] -use soroban_sdk::{contract, contractimpl, symbol_short, Address, Env, Bytes, BytesN}; +use soroban_sdk::{contract, contractimpl, symbol_short, Address, Bytes, BytesN, Env}; #[contract] pub struct IdentityRegistryContract; @@ -13,7 +13,8 @@ impl IdentityRegistryContract { let key = (symbol_short!("didctl"), identity_id.clone()); assert!(!env.storage().persistent().has(&key), "DID already exists"); env.storage().persistent().set(&key, &controller); - env.events().publish((symbol_short!("didcrt"),), (identity_id, controller)); + env.events() + .publish((symbol_short!("didcrt"),), (identity_id, controller)); } // Get controller for a DID @@ -23,54 +24,101 @@ impl IdentityRegistryContract { } // Update controller. Caller must pass the current controller and sign the call. - pub fn set_controller(env: &Env, identity_id: BytesN<32>, current_controller: Address, new_controller: Address) { + pub fn set_controller( + env: &Env, + identity_id: BytesN<32>, + current_controller: Address, + new_controller: Address, + ) { current_controller.require_auth(); let key = (symbol_short!("did_ctrl"), identity_id.clone()); let opt: Option
= env.storage().persistent().get(&key); match opt { Some(stored) => { - assert!(stored == current_controller, "only controller can change controller"); + assert!( + stored == current_controller, + "only controller can change controller" + ); env.storage().persistent().set(&key, &new_controller); - env.events().publish((symbol_short!("didchg"),), (identity_id, new_controller)); + env.events() + .publish((symbol_short!("didchg"),), (identity_id, new_controller)); } None => panic!("DID not found"), } } // Add or update an authentication method (e.g., key, service) for a DID - pub fn set_auth_method(env: &Env, identity_id: BytesN<32>, controller: Address, method_id: Bytes, public_key: Bytes) { + pub fn set_auth_method( + env: &Env, + identity_id: BytesN<32>, + controller: Address, + method_id: Bytes, + public_key: Bytes, + ) { controller.require_auth(); let ctrl_key = (symbol_short!("didctl"), identity_id.clone()); let current: Option
= env.storage().persistent().get(&ctrl_key); assert!(current.is_some(), "DID not found"); - assert!(current.unwrap() == controller, "only controller can set auth methods"); - let key = (symbol_short!("auth"), identity_id.clone(), method_id.clone()); + assert!( + current.unwrap() == controller, + "only controller can set auth methods" + ); + let key = ( + symbol_short!("auth"), + identity_id.clone(), + method_id.clone(), + ); env.storage().persistent().set(&key, &public_key); - env.events().publish((symbol_short!("authset"),), (identity_id, method_id, public_key)); + env.events().publish( + (symbol_short!("authset"),), + (identity_id, method_id, public_key), + ); } // Remove an auth method - pub fn remove_auth_method(env: &Env, identity_id: BytesN<32>, controller: Address, method_id: Bytes) { + pub fn remove_auth_method( + env: &Env, + identity_id: BytesN<32>, + controller: Address, + method_id: Bytes, + ) { controller.require_auth(); let ctrl_key = (symbol_short!("did_ctrl"), identity_id.clone()); let current: Option
= env.storage().persistent().get(&ctrl_key); assert!(current.is_some(), "DID not found"); - assert!(current.unwrap() == controller, "only controller can remove auth methods"); - let key = (symbol_short!("auth"), identity_id.clone(), method_id.clone()); + assert!( + current.unwrap() == controller, + "only controller can remove auth methods" + ); + let key = ( + symbol_short!("auth"), + identity_id.clone(), + method_id.clone(), + ); env.storage().persistent().remove(&key); - env.events().publish((symbol_short!("authrem"),), (identity_id, method_id)); + env.events() + .publish((symbol_short!("authrem"),), (identity_id, method_id)); } // Set a recovery address that may be used to recover control of the DID - pub fn set_recovery(env: &Env, identity_id: BytesN<32>, controller: Address, recovery: Address) { + pub fn set_recovery( + env: &Env, + identity_id: BytesN<32>, + controller: Address, + recovery: Address, + ) { controller.require_auth(); let ctrl_key = (symbol_short!("did_ctrl"), identity_id.clone()); let current: Option
= env.storage().persistent().get(&ctrl_key); assert!(current.is_some(), "DID not found"); - assert!(current.unwrap() == controller, "only controller can set recovery"); + assert!( + current.unwrap() == controller, + "only controller can set recovery" + ); let key = (symbol_short!("recovery"), identity_id.clone()); env.storage().persistent().set(&key, &recovery); - env.events().publish((symbol_short!("recovery"),), (identity_id, recovery)); + env.events() + .publish((symbol_short!("recovery"),), (identity_id, recovery)); } // Recover controller using the configured recovery address @@ -80,10 +128,14 @@ impl IdentityRegistryContract { let rec_opt: Option
= env.storage().persistent().get(&rec_key); match rec_opt { Some(recovery_addr) => { - assert!(recovery_addr == recovery, "only recovery address can perform recovery"); + assert!( + recovery_addr == recovery, + "only recovery address can perform recovery" + ); let ctrl_key = (symbol_short!("did_ctrl"), identity_id.clone()); env.storage().persistent().set(&ctrl_key, &new_controller); - env.events().publish((symbol_short!("didrec"),), (identity_id, new_controller)); + env.events() + .publish((symbol_short!("didrec"),), (identity_id, new_controller)); } None => panic!("no recovery configured for DID"), }