Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
212 changes: 212 additions & 0 deletions contracts/src/integration_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -228,6 +228,189 @@ fn test_get_verification_by_identity() {
assert_eq!(found_id, v_id);
}

// ── Credential Revocation Tests ──────────────────────────────────────────────

#[test]
fn test_revoke_verification_credential() {
let (env, identity, verification, _, _, _) = setup();
let user = Address::generate(&env);
let verifier = Address::generate(&env);
let doc_hash = BytesN::from_array(&env, &[1u8; 32]);

let identity_id =
identity.register_identity(&user, &doc_hash, &String::from_str(&env, "QmRevoke"));

let proof_hash = BytesN::from_array(&env, &[42u8; 32]);
let commitment = BytesN::from_array(&env, &[99u8; 32]);
let v_id = verification.submit_proof(&identity_id, &verifier, &proof_hash, &commitment);

verification.approve_verification(&v_id);

assert!(!verification.is_verification_revoked(&v_id));

let reason = String::from_str(&env, "credential_compromised");
verification.revoke_verification(&v_id, &reason);

assert!(verification.is_verification_revoked(&v_id));
}

#[test]
fn test_revocation_status_updates() {
let (env, identity, verification, _, _, _) = setup();
let user = Address::generate(&env);
let verifier = Address::generate(&env);
let doc_hash = BytesN::from_array(&env, &[1u8; 32]);

let identity_id =
identity.register_identity(&user, &doc_hash, &String::from_str(&env, "QmStatus"));

let proof_hash = BytesN::from_array(&env, &[42u8; 32]);
let commitment = BytesN::from_array(&env, &[99u8; 32]);
let v_id = verification.submit_proof(&identity_id, &verifier, &proof_hash, &commitment);

verification.approve_verification(&v_id);

let (revoked, revoked_at, reason) = verification.get_revocation_status(&v_id);
assert!(!revoked);
assert_eq!(revoked_at, 0);
assert_eq!(reason, String::from_str(&env, ""));

let revoke_reason = String::from_str(&env, "security_breach");
verification.revoke_verification(&v_id, &revoke_reason);

let (revoked, revoked_at, reason) = verification.get_revocation_status(&v_id);
assert!(revoked);
let _ = revoked_at; // Timestamp is set but may be 0 in test environment
assert_eq!(reason, revoke_reason);
}

#[test]
fn test_revocation_list_maintenance() {
let (env, identity, verification, _, _, _) = setup();
let user = Address::generate(&env);
let verifier = Address::generate(&env);
let doc_hash = BytesN::from_array(&env, &[1u8; 32]);

let identity_id =
identity.register_identity(&user, &doc_hash, &String::from_str(&env, "QmList"));

// Create multiple verifications
let v_id1 = verification.submit_proof(
&identity_id,
&verifier,
&BytesN::from_array(&env, &[1u8; 32]),
&BytesN::from_array(&env, &[10u8; 32]),
);
let v_id2 = verification.submit_proof(
&identity_id,
&verifier,
&BytesN::from_array(&env, &[2u8; 32]),
&BytesN::from_array(&env, &[20u8; 32]),
);

let revoked_list = verification.get_revoked_verifications();
assert_eq!(revoked_list.len(), 0);

verification.revoke_verification(&v_id1, &String::from_str(&env, "reason1"));
let revoked_list = verification.get_revoked_verifications();
assert_eq!(revoked_list.len(), 1);
assert_eq!(revoked_list.get(0).unwrap(), v_id1);

verification.revoke_verification(&v_id2, &String::from_str(&env, "reason2"));
let revoked_list = verification.get_revoked_verifications();
assert_eq!(revoked_list.len(), 2);
assert_eq!(revoked_list.get(0).unwrap(), v_id1);
assert_eq!(revoked_list.get(1).unwrap(), v_id2);
}

#[test]
fn test_is_verification_valid() {
let (env, identity, verification, _, _, _) = setup();
let user = Address::generate(&env);
let verifier = Address::generate(&env);
let doc_hash = BytesN::from_array(&env, &[1u8; 32]);

let identity_id =
identity.register_identity(&user, &doc_hash, &String::from_str(&env, "QmValid"));

let proof_hash = BytesN::from_array(&env, &[42u8; 32]);
let commitment = BytesN::from_array(&env, &[99u8; 32]);
let v_id = verification.submit_proof(&identity_id, &verifier, &proof_hash, &commitment);

// Pending verification is not valid
assert!(!verification.is_verification_valid(&v_id));

verification.approve_verification(&v_id);

// Approved verification is valid
assert!(verification.is_verification_valid(&v_id));

verification.revoke_verification(&v_id, &String::from_str(&env, "test_reason"));

// Revoked verification is not valid
assert!(!verification.is_verification_valid(&v_id));
}

#[test]
fn test_revoke_rejected_verification() {
let (env, identity, verification, _, _, _) = setup();
let user = Address::generate(&env);
let verifier = Address::generate(&env);
let doc_hash = BytesN::from_array(&env, &[1u8; 32]);

let identity_id =
identity.register_identity(&user, &doc_hash, &String::from_str(&env, "QmRejRev"));

let proof_hash = BytesN::from_array(&env, &[42u8; 32]);
let commitment = BytesN::from_array(&env, &[99u8; 32]);
let v_id = verification.submit_proof(&identity_id, &verifier, &proof_hash, &commitment);

verification.reject_verification(&v_id, &String::from_str(&env, "invalid_proof"));

// Can still revoke even if rejected
verification.revoke_verification(&v_id, &String::from_str(&env, "fraudulent"));

assert!(verification.is_verification_revoked(&v_id));
assert!(!verification.is_verification_valid(&v_id));
}

#[test]
fn test_multiple_verifications_independent_revocation() {
let (env, identity, verification, _, _, _) = setup();
let user = Address::generate(&env);
let verifier1 = Address::generate(&env);
let verifier2 = Address::generate(&env);
let doc_hash = BytesN::from_array(&env, &[1u8; 32]);

let identity_id =
identity.register_identity(&user, &doc_hash, &String::from_str(&env, "QmMulti"));

let v_id1 = verification.submit_proof(
&identity_id,
&verifier1,
&BytesN::from_array(&env, &[1u8; 32]),
&BytesN::from_array(&env, &[10u8; 32]),
);
let v_id2 = verification.submit_proof(
&identity_id,
&verifier2,
&BytesN::from_array(&env, &[2u8; 32]),
&BytesN::from_array(&env, &[20u8; 32]),
);

verification.approve_verification(&v_id1);
verification.approve_verification(&v_id2);

// Revoke first verification
verification.revoke_verification(&v_id1, &String::from_str(&env, "reason1"));

// First is revoked, second is still valid
assert!(verification.is_verification_revoked(&v_id1));
assert!(!verification.is_verification_revoked(&v_id2));
assert!(!verification.is_verification_valid(&v_id1));
assert!(verification.is_verification_valid(&v_id2));
}

// ── Access Control Integration ────────────────────────────────────────────────

#[test]
Expand Down Expand Up @@ -556,6 +739,35 @@ fn test_get_verification_status_nonexistent_panics() {
verification.get_verification_status(&999);
}

#[test]
#[should_panic]
fn test_revoke_nonexistent_verification_panics() {
let (env, _, verification, _, _, _) = setup();
let reason = String::from_str(&env, "test");
verification.revoke_verification(&999, &reason);
}

#[test]
#[should_panic]
fn test_is_verification_revoked_nonexistent_panics() {
let (_, _, verification, _, _, _) = setup();
verification.is_verification_revoked(&999);
}

#[test]
#[should_panic]
fn test_get_revocation_status_nonexistent_panics() {
let (_, _, verification, _, _, _) = setup();
verification.get_revocation_status(&999);
}

#[test]
#[should_panic]
fn test_is_verification_valid_nonexistent_panics() {
let (_, _, verification, _, _, _) = setup();
verification.is_verification_valid(&999);
}

#[test]
#[should_panic]
fn test_revoke_nonexistent_permission_panics() {
Expand Down
95 changes: 94 additions & 1 deletion contracts/src/verification.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use soroban_sdk::{contract, contractimpl, contracttype, Address, BytesN, Env, String};
use soroban_sdk::{contract, contractimpl, contracttype, Address, BytesN, Env, String, Vec};

use crate::errors::Error;

Expand All @@ -11,6 +11,14 @@ pub struct VerificationRecord {
pub verification_commitment: BytesN<32>,
pub status: String,
pub created_at: u64,
pub revoked: bool,
pub revoked_at: u64,
pub revocation_reason: String,
}

#[contracttype]
pub enum VerificationEvent {
VerificationRevoked,
}

#[contract]
Expand Down Expand Up @@ -41,6 +49,9 @@ impl Verification {
verification_commitment,
status: String::from_str(env, "pending"),
created_at: env.ledger().timestamp(),
revoked: false,
revoked_at: 0,
revocation_reason: String::from_str(env, ""),
};

env.storage()
Expand Down Expand Up @@ -118,4 +129,86 @@ impl Verification {

Ok(record.status)
}

pub fn revoke_verification(
env: &Env,
verification_id: u64,
reason: String,
) -> Result<(), Error> {
let mut record: VerificationRecord = env
.storage()
.instance()
.get(&(verification_id, "record"))
.ok_or(Error::VerificationNotFound)?;

record.verifier.require_auth();

record.revoked = true;
record.revoked_at = env.ledger().timestamp();
record.revocation_reason = reason;

env.storage()
.instance()
.set(&(verification_id, "record"), &record);

// Add to revocation list
let mut revoked_list: Vec<u64> = env
.storage()
.instance()
.get(&"revoked_verifications")
.unwrap_or(Vec::new(env));

revoked_list.push_back(verification_id);
env.storage()
.instance()
.set(&"revoked_verifications", &revoked_list);

// Emit revocation event
env.events().publish(
(String::from_str(env, "revocation"), verification_id),
VerificationEvent::VerificationRevoked,
);

Ok(())
}

pub fn is_verification_revoked(env: &Env, verification_id: u64) -> Result<bool, Error> {
let record: VerificationRecord = env
.storage()
.instance()
.get(&(verification_id, "record"))
.ok_or(Error::VerificationNotFound)?;

Ok(record.revoked)
}

pub fn get_revocation_status(
env: &Env,
verification_id: u64,
) -> Result<(bool, u64, String), Error> {
let record: VerificationRecord = env
.storage()
.instance()
.get(&(verification_id, "record"))
.ok_or(Error::VerificationNotFound)?;

Ok((record.revoked, record.revoked_at, record.revocation_reason))
}

pub fn get_revoked_verifications(env: &Env) -> Vec<u64> {
env.storage()
.instance()
.get(&"revoked_verifications")
.unwrap_or(Vec::new(env))
}

pub fn is_verification_valid(env: &Env, verification_id: u64) -> Result<bool, Error> {
let record: VerificationRecord = env
.storage()
.instance()
.get(&(verification_id, "record"))
.ok_or(Error::VerificationNotFound)?;

Ok(record.status == String::from_str(env, "approved") && !record.revoked)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -387,6 +387,30 @@
"bytes": "2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a"
}
},
{
"key": {
"symbol": "revocation_reason"
},
"val": {
"string": ""
}
},
{
"key": {
"symbol": "revoked"
},
"val": {
"bool": false
}
},
{
"key": {
"symbol": "revoked_at"
},
"val": {
"u64": 0
}
},
{
"key": {
"symbol": "status"
Expand Down
Loading
Loading