-
Notifications
You must be signed in to change notification settings - Fork 38
Add on-chain zero-knowledge proof validation #171
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
095a863
19bbd4a
a422d1a
cc980f8
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,7 +1,8 @@ | ||
| extern crate std; | ||
|
|
||
| use soroban_sdk::IntoVal; | ||
| use soroban_sdk::{ | ||
| testutils::{Address as _, Events, Ledger}, | ||
| testutils::{Address as _, Events, Ledger, MockAuth, MockAuthInvoke}, | ||
| Address, Bytes, BytesN, Env, String, | ||
| }; | ||
|
|
||
|
|
@@ -264,6 +265,244 @@ fn test_get_verification_by_identity() { | |
| assert_eq!(found_id, v_id); | ||
| } | ||
|
|
||
| // ── Zero-Knowledge Proof Validation Tests ──────────────────────────────────── | ||
|
|
||
| #[test] | ||
| fn test_validate_proof_matching_hashes() { | ||
| 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, "QmZkValid")); | ||
|
|
||
| // Submit a proof whose committed hashes match the raw bytes below. | ||
| let raw_proof = Bytes::from_array(&env, &[7u8; 32]); | ||
| let public_signals = Bytes::from_array(&env, &[8u8; 32]); | ||
|
|
||
| let proof_hash: BytesN<32> = env.crypto().sha256(&raw_proof).into(); | ||
| let commitment: BytesN<32> = env.crypto().sha256(&public_signals).into(); | ||
|
|
||
| let v_id = verification.submit_proof(&identity_id, &verifier, &proof_hash, &commitment); | ||
| verification.approve_verification(&v_id); | ||
|
|
||
| assert!(verification.validate_proof(&v_id, &raw_proof, &public_signals)); | ||
| } | ||
|
|
||
| #[test] | ||
| fn test_validate_proof_pending_record_fails() { | ||
| 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, "QmZkPending")); | ||
|
|
||
| let raw_proof = Bytes::from_array(&env, &[7u8; 32]); | ||
| let public_signals = Bytes::from_array(&env, &[8u8; 32]); | ||
|
|
||
| let proof_hash: BytesN<32> = env.crypto().sha256(&raw_proof).into(); | ||
| let commitment: BytesN<32> = env.crypto().sha256(&public_signals).into(); | ||
|
|
||
| let v_id = verification.submit_proof(&identity_id, &verifier, &proof_hash, &commitment); | ||
|
|
||
| // Record is still pending, so even matching hashes must not validate. | ||
| assert!(!verification.validate_proof(&v_id, &raw_proof, &public_signals)); | ||
| } | ||
|
|
||
| #[test] | ||
| fn test_validate_proof_tampered_proof_fails() { | ||
| 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, "QmZkTampered")); | ||
|
|
||
| let raw_proof = Bytes::from_array(&env, &[7u8; 32]); | ||
| let public_signals = Bytes::from_array(&env, &[8u8; 32]); | ||
|
|
||
| let proof_hash: BytesN<32> = env.crypto().sha256(&raw_proof).into(); | ||
| let commitment: BytesN<32> = env.crypto().sha256(&public_signals).into(); | ||
|
|
||
| let v_id = verification.submit_proof(&identity_id, &verifier, &proof_hash, &commitment); | ||
| verification.approve_verification(&v_id); | ||
|
|
||
| // Alter one byte of the proof — the integrity check must fail. | ||
| let tampered = Bytes::from_array(&env, &[9u8; 32]); | ||
| assert!(!verification.validate_proof(&v_id, &tampered, &public_signals)); | ||
| } | ||
|
|
||
| #[test] | ||
| fn test_validate_proof_tampered_signals_fails() { | ||
| 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, "QmZkSignals")); | ||
|
|
||
| let raw_proof = Bytes::from_array(&env, &[7u8; 32]); | ||
| let public_signals = Bytes::from_array(&env, &[8u8; 32]); | ||
|
|
||
| let proof_hash: BytesN<32> = env.crypto().sha256(&raw_proof).into(); | ||
| let commitment: BytesN<32> = env.crypto().sha256(&public_signals).into(); | ||
|
|
||
| let v_id = verification.submit_proof(&identity_id, &verifier, &proof_hash, &commitment); | ||
| verification.approve_verification(&v_id); | ||
|
|
||
| // Tamper only the public signals — the commitment check must fail. | ||
| let tampered_signals = Bytes::from_array(&env, &[9u8; 32]); | ||
| assert!(!verification.validate_proof(&v_id, &raw_proof, &tampered_signals)); | ||
| } | ||
|
|
||
| #[test] | ||
| fn test_validate_proof_revoked_record_fails() { | ||
| 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, "QmZkRevoked")); | ||
|
|
||
| let raw_proof = Bytes::from_array(&env, &[7u8; 32]); | ||
| let public_signals = Bytes::from_array(&env, &[8u8; 32]); | ||
|
|
||
| let proof_hash: BytesN<32> = env.crypto().sha256(&raw_proof).into(); | ||
| let commitment: BytesN<32> = env.crypto().sha256(&public_signals).into(); | ||
|
|
||
| let v_id = verification.submit_proof(&identity_id, &verifier, &proof_hash, &commitment); | ||
| verification.approve_verification(&v_id); | ||
| verification.revoke_verification(&v_id, &String::from_str(&env, "compromised")); | ||
|
|
||
| assert!(!verification.validate_proof(&v_id, &raw_proof, &public_signals)); | ||
| } | ||
|
|
||
| #[test] | ||
| fn test_validate_proof_unknown_id_fails() { | ||
| let (env, _identity, verification, _, _, _) = setup(); | ||
|
|
||
| let raw_proof = Bytes::from_array(&env, &[7u8; 32]); | ||
| let public_signals = Bytes::from_array(&env, &[8u8; 32]); | ||
|
|
||
| let result = verification.try_validate_proof(&999u64, &raw_proof, &public_signals); | ||
| assert_eq!(result, Err(Ok(crate::errors::Error::VerificationNotFound))); | ||
| } | ||
|
|
||
| #[test] | ||
| fn test_validate_proof_unauthorized_verifier_fails() { | ||
| // Build a fresh env without mock_all_auths so the verifier's require_auth | ||
| // must be explicitly satisfied. | ||
| let env = Env::default(); | ||
|
|
||
| let identity_id = env.register_contract(None, IdentityRegistry {}); | ||
| let identity = IdentityRegistryClient::new(&env, &identity_id); | ||
|
|
||
| let verification_id = env.register_contract(None, Verification {}); | ||
| let verification = VerificationClient::new(&env, &verification_id); | ||
|
|
||
| let user = Address::generate(&env); | ||
| let verifier = Address::generate(&env); | ||
| let doc_hash = BytesN::from_array(&env, &[1u8; 32]); | ||
|
|
||
| // Authorize identity registration, proof submission, and approval, but NOT | ||
| // validate_proof. This lets us assert that validate_proof rejects when the | ||
| // stored verifier has not authorized the call. | ||
| env.mock_auths(&[MockAuth { | ||
| address: &user, | ||
| invoke: &MockAuthInvoke { | ||
| contract: &identity.address, | ||
| fn_name: "register_identity", | ||
| args: ( | ||
| user.clone(), | ||
| doc_hash.clone(), | ||
| String::from_str(&env, "QmZkAuth"), | ||
| ) | ||
| .into_val(&env), | ||
| sub_invokes: &[], | ||
| }, | ||
| }]); | ||
| let identity_id_value = | ||
| identity.register_identity(&user, &doc_hash, &String::from_str(&env, "QmZkAuth")); | ||
|
|
||
| let proof_hash = BytesN::from_array(&env, &[1u8; 32]); | ||
| let commitment = BytesN::from_array(&env, &[2u8; 32]); | ||
| env.mock_auths(&[MockAuth { | ||
| address: &verifier, | ||
| invoke: &MockAuthInvoke { | ||
| contract: &verification.address, | ||
| fn_name: "submit_proof", | ||
| args: ( | ||
| identity_id_value, | ||
| verifier.clone(), | ||
| proof_hash.clone(), | ||
| commitment.clone(), | ||
| ) | ||
| .into_val(&env), | ||
| sub_invokes: &[], | ||
| }, | ||
| }]); | ||
| let v_id = verification.submit_proof(&identity_id_value, &verifier, &proof_hash, &commitment); | ||
|
|
||
| env.mock_auths(&[MockAuth { | ||
| address: &verifier, | ||
| invoke: &MockAuthInvoke { | ||
| contract: &verification.address, | ||
| fn_name: "approve_verification", | ||
| args: (v_id,).into_val(&env), | ||
| sub_invokes: &[], | ||
| }, | ||
| }]); | ||
| verification.approve_verification(&v_id); | ||
|
|
||
| // With no mocked auths, require_auth for validate_proof must reject. | ||
| env.mock_auths(&[]); | ||
| let result = verification.try_validate_proof( | ||
| &v_id, | ||
| &Bytes::from_array(&env, &[1u8; 32]), | ||
| &Bytes::from_array(&env, &[2u8; 32]), | ||
| ); | ||
|
|
||
| // require_auth failures surface as an invocation (host) error wrapped in | ||
| // the outer Err — not as a contract error (Err(Ok(..))) and not as a | ||
| // successful call. Match that shape explicitly. | ||
| match result { | ||
| Err(Err(_)) => {} | ||
| Err(Ok(err)) => panic!("expected auth failure, got contract error: {err:?}"), | ||
| Ok(valid) => panic!("validate_proof must fail without authorization, got {valid:?}"), | ||
| } | ||
| } | ||
|
coderabbitai[bot] marked this conversation as resolved.
|
||
|
|
||
| #[test] | ||
| fn test_validate_proof_emits_event() { | ||
| 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, "QmZkEvent")); | ||
|
|
||
| let raw_proof = Bytes::from_array(&env, &[7u8; 32]); | ||
| let public_signals = Bytes::from_array(&env, &[8u8; 32]); | ||
|
|
||
| let proof_hash: BytesN<32> = env.crypto().sha256(&raw_proof).into(); | ||
| let commitment: BytesN<32> = env.crypto().sha256(&public_signals).into(); | ||
|
|
||
| let v_id = verification.submit_proof(&identity_id, &verifier, &proof_hash, &commitment); | ||
| verification.approve_verification(&v_id); | ||
|
|
||
| let events_before = env.events().all().len(); | ||
| let valid = verification.validate_proof(&v_id, &raw_proof, &public_signals); | ||
| assert!(valid); | ||
| assert_eq!(env.events().all().len(), events_before + 1); | ||
|
Comment on lines
+500
to
+503
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win 🧩 Analysis chain🏁 Script executed: #!/bin/bash
# Description: Check imports available in the integration test module and the visibility of VerificationEvent.
set -euo pipefail
fd -t f 'integration_tests.rs' | while IFS= read -r f; do
echo "=== $f (first 20 lines) ==="
sed -n '1,20p' "$f"
done
echo "=== VerificationEvent definition and visibility ==="
rg -nP -B3 -A8 '\benum\s+VerificationEvent\b' --type=rust
echo "=== Existing event-content assertions elsewhere in the repo ==="
rg -nP -C3 'events\(\)\.all\(\)' --type=rustRepository: GuardZero144/ValidFi Length of output: 809 🏁 Script executed: #!/bin/bash
set -euo pipefail
echo "=== target test ==="
sed -n '300,350p' contracts/src/integration_tests.rs
echo "=== verification module symbols and event emission ==="
rg -n -C4 'VerificationEvent|proof_validation|publish|events|Validated' contracts/src
echo "=== SDK and conversion imports ==="
rg -n -C3 'soroban-sdk|IntoVal|into_val|Events' contracts/Cargo.toml Cargo.toml contracts/src
echo "=== event assertions in Rust tests ==="
rg -n -C5 'events\(\)\.all\(\)|Events::|\.last\(\)' --glob '*.rs' .Repository: GuardZero144/ValidFi Length of output: 25140 🏁 Script executed: #!/bin/bash
set -euo pipefail
echo "=== integration test imports and setup ==="
sed -n '1,45p' contracts/src/integration_tests.rs
echo "=== verification event declaration and validation implementation ==="
sed -n '1,32p' contracts/src/verification.rs
sed -n '225,268p' contracts/src/verification.rs
echo "=== read-only source verifier ==="
python3 - <<'PY'
from pathlib import Path
import re
test = Path("contracts/src/integration_tests.rs").read_text()
verification = Path("contracts/src/verification.rs").read_text()
checks = {
"Events imported in integration test": bool(re.search(r'\bEvents\b', test.split("fn setup", 1)[0])),
"IntoVal imported in integration test": bool(re.search(r'\bIntoVal\b', test.split("fn setup", 1)[0])),
"VerificationEvent imported in integration test": bool(re.search(r'\bVerificationEvent\b', test.split("fn setup", 1)[0])),
"validation publishes proof_validation topic": '(String::from_str(env, "proof_validation"), verification_id)' in verification,
"validation publishes Validated(valid)": 'VerificationEvent::Validated(valid)' in verification,
"validation returns valid": 'Ok(valid)' in verification,
}
for name, result in checks.items():
print(f"{name}: {result}")
PYRepository: GuardZero144/ValidFi Length of output: 4443 Assert the event contents, not only the event count. For a valid proof, assert the final event's contract ID, topics 🤖 Prompt for AI Agents |
||
| } | ||
|
|
||
| // ── Credential Revocation Tests ────────────────────────────────────────────── | ||
|
|
||
| #[test] | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.