Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
4004e6e
feat: versioned VK registry for circuit upgrades without invalidating…
Topmatrixmor2014 Jul 25, 2026
1aa20ab
Merge branch 'main' into feat/vk-versioning
Topmatrixmor2014 Jul 26, 2026
f6f717c
fix: add extend_ttl to DeprecatedVersion to prevent silent expiry
Topmatrixmor2014 Jul 26, 2026
86fd78e
fix: use plain number instead of BigInt for u32 in nativeToScVal
Topmatrixmor2014 Jul 26, 2026
44348ff
fix: reject Some(0) vk_version explicitly instead of silently resolvi…
Topmatrixmor2014 Jul 26, 2026
1754e8d
Merge branch 'main' into feat/vk-versioning
Topmatrixmor2014 Aug 1, 2026
258a772
fix: complete VK versioning write-path so contracts compile and CI pa…
Topmatrixmor2014 Aug 1, 2026
c6286e7
Merge branch 'main' into feat/vk-versioning
Topmatrixmor2014 Aug 3, 2026
7c0281f
fix(contracts): thread vk_version through batch and aggregate verific…
Topmatrixmor2014 Aug 3, 2026
79fe0e6
fix(circuits): regenerate range proof fixtures with corrected commitment
Topmatrixmor2014 Aug 3, 2026
a772a7f
fix(frontend): pass vk_version to on-chain submit_proof
Topmatrixmor2014 Aug 3, 2026
a21104f
fix(credential_verifier): refresh latest-version TTL on any set_vk
Topmatrixmor2014 Aug 3, 2026
009d9a7
fix(credential_verifier): guard VK immutability and add TTL refresh
Topmatrixmor2014 Aug 3, 2026
83e0b85
chore(ci): trigger workflow re-run after dropped push event
Topmatrixmor2014 Aug 3, 2026
9561401
Merge branch 'main' into feat/vk-versioning
Topmatrixmor2014 Aug 4, 2026
1aea47b
fix(gated_pool): restore versioned set_vk and submit_proof calls in t…
Topmatrixmor2014 Aug 4, 2026
50da91f
fix(credential_verifier): refresh VK blob TTL in refresh_latest_versi…
Topmatrixmor2014 Aug 4, 2026
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
82 changes: 72 additions & 10 deletions contracts/credential_verifier/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,14 @@ const VK_TTL: u32 = 180 * DAY_IN_LEDGERS;
#[contracttype]
pub enum DataKey {
Admin,
/// Verification key bytes, keyed by credential-type symbol.
Vk(Symbol),
/// Verification key bytes, keyed by (credential-type symbol, version).
Vk(Symbol, u32),
/// Tracks the latest VK version registered for a credential type.
LatestVersion(Symbol),
/// Marks a specific (credential_type, version) as deprecated — no longer
/// accepted for new submissions. Old proofs using this version remain
/// readable (the VK is not deleted).
DeprecatedVersion(Symbol, u32),
}

#[contracterror]
Expand All @@ -36,6 +42,9 @@ pub enum Error {
NotInitialized = 1,
VkNotSet = 2,
VkInvalid = 3,
/// The requested VK version has been deprecated by the admin; new
/// submissions against it are rejected.
VersionDeprecated = 4,
}

#[contract]
Expand All @@ -47,38 +56,73 @@ impl CredentialVerifier {
env.storage().instance().set(&DataKey::Admin, &admin);
}

/// Register/replace the verification key for a credential circuit. Admin-only.
/// The VK is validated by parsing it before storage, rejecting malformed keys
/// at set time.
pub fn set_vk(env: Env, credential_type: Symbol, vk: Bytes) {
/// Register a verification key for a credential circuit at the given version.
/// Admin-only. The VK is validated by parsing it before storage, rejecting
/// malformed keys at set time.
///
/// If `version` is greater than the currently-tracked latest version for
/// `credential_type`, the latest-version pointer is updated automatically.
pub fn set_vk(env: Env, credential_type: Symbol, version: u32, vk: Bytes) {
Self::require_admin(&env);
if UltraHonkVerifier::new(&env, &vk).is_err() {
panic_with_error!(&env, Error::VkInvalid);
}
let key = DataKey::Vk(credential_type);
let key = DataKey::Vk(credential_type.clone(), version);
env.storage().persistent().set(&key, &vk);
env.storage()
.persistent()
.extend_ttl(&key, VK_BUMP_THRESHOLD, VK_TTL);

// Update the latest-version pointer when registering a newer version.
let latest_key = DataKey::LatestVersion(credential_type);
let current: u32 = env.storage().persistent().get(&latest_key).unwrap_or(0);
if version > current {
env.storage().persistent().set(&latest_key, &version);
env.storage()
.persistent()
.extend_ttl(&latest_key, VK_BUMP_THRESHOLD, VK_TTL);
}
Comment thread
Topmatrixmor2014 marked this conversation as resolved.
}

/// Verify an UltraHonk proof for any registered credential type. Looks up
/// the VK by `credential_type` Symbol and returns true iff the proof is valid.
/// Panics with `VkNotSet` if no VK has been registered for this type.
/// the VK by `(credential_type, vk_version)`. Pass `vk_version = None` to
/// use the latest registered version automatically.
///
/// Returns `true` iff the proof is valid. Returns `false` for malformed
/// inputs or invalid proofs; panics with `VkNotSet` if no VK has been
/// registered for this type/version, or with `VersionDeprecated` if the
/// requested version has been deprecated.
pub fn verify_proof(
env: Env,
credential_type: Symbol,
proof: Bytes,
public_inputs: Bytes,
vk_version: Option<u32>,
) -> bool {
// Proofs are fixed-length; reject early before touching the verifier.
if proof.len() as usize != PROOF_BYTES {
return false;
}

let version = match vk_version {
Some(v) if v > 0 => v,
_ => env
.storage()
.persistent()
.get(&DataKey::LatestVersion(credential_type.clone()))
.unwrap_or_else(|| panic_with_error!(&env, Error::VkNotSet)),
};
Comment thread
Topmatrixmor2014 marked this conversation as resolved.

// Reject submissions against a deprecated VK version.
let dep_key = DataKey::DeprecatedVersion(credential_type.clone(), version);
if env.storage().persistent().get::<_, bool>(&dep_key).unwrap_or(false) {
panic_with_error!(&env, Error::VersionDeprecated);
}

let vk: Bytes = env
.storage()
.persistent()
.get(&DataKey::Vk(credential_type))
.get(&DataKey::Vk(credential_type, version))
.unwrap_or_else(|| panic_with_error!(&env, Error::VkNotSet));

match UltraHonkVerifier::new(&env, &vk) {
Expand All @@ -87,6 +131,24 @@ impl CredentialVerifier {
}
}

/// Mark a VK version as deprecated. Admin-only. New proof submissions against
/// this version will be rejected (`VersionDeprecated`), but the VK is not
/// deleted so old proofs already stored by ProofRegistry remain readable.
pub fn deprecate_version(env: Env, credential_type: Symbol, version: u32) {
Self::require_admin(&env);
let key = DataKey::DeprecatedVersion(credential_type, version);
env.storage().persistent().set(&key, &true);
}
Comment thread
greptile-apps[bot] marked this conversation as resolved.
Outdated

/// Returns the highest registered VK version for `credential_type`, or 0 if
/// no VK has been registered yet.
pub fn get_latest_version(env: Env, credential_type: Symbol) -> u32 {
env.storage()
.persistent()
.get(&DataKey::LatestVersion(credential_type))
.unwrap_or(0)
}

fn require_admin(env: &Env) {
let admin: Address = env
.storage()
Expand Down
166 changes: 160 additions & 6 deletions contracts/credential_verifier/src/test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,12 @@ fn verifies_kyc() {
let env = Env::default();
env.mock_all_auths();
let c = setup(&env);
c.set_vk(&symbol_short!("kyc"), &Bytes::from_slice(&env, fixture!("kyc", "vk")));
c.set_vk(&symbol_short!("kyc"), &1u32, &Bytes::from_slice(&env, fixture!("kyc", "vk")));
assert!(c.verify_proof(
&symbol_short!("kyc"),
&Bytes::from_slice(&env, fixture!("kyc", "proof")),
&Bytes::from_slice(&env, fixture!("kyc", "public_inputs")),
&None,
));
}

Expand All @@ -36,11 +37,12 @@ fn verifies_age() {
let env = Env::default();
env.mock_all_auths();
let c = setup(&env);
c.set_vk(&symbol_short!("age"), &Bytes::from_slice(&env, fixture!("age", "vk")));
c.set_vk(&symbol_short!("age"), &1u32, &Bytes::from_slice(&env, fixture!("age", "vk")));
assert!(c.verify_proof(
&symbol_short!("age"),
&Bytes::from_slice(&env, fixture!("age", "proof")),
&Bytes::from_slice(&env, fixture!("age", "public_inputs")),
&None,
));
}

Expand All @@ -49,11 +51,12 @@ fn verifies_income() {
let env = Env::default();
env.mock_all_auths();
let c = setup(&env);
c.set_vk(&symbol_short!("income"), &Bytes::from_slice(&env, fixture!("income", "vk")));
c.set_vk(&symbol_short!("income"), &1u32, &Bytes::from_slice(&env, fixture!("income", "vk")));
assert!(c.verify_proof(
&symbol_short!("income"),
&Bytes::from_slice(&env, fixture!("income", "proof")),
&Bytes::from_slice(&env, fixture!("income", "public_inputs")),
&None,
));
}

Expand All @@ -64,12 +67,14 @@ fn verifies_jurisdiction() {
let c = setup(&env);
c.set_vk(
&Symbol::new(&env, "jurisdiction"),
&1u32,
&Bytes::from_slice(&env, fixture!("jurisdiction", "vk")),
);
assert!(c.verify_proof(
&Symbol::new(&env, "jurisdiction"),
&Bytes::from_slice(&env, fixture!("jurisdiction", "proof")),
&Bytes::from_slice(&env, fixture!("jurisdiction", "public_inputs")),
&None,
));
}

Expand All @@ -78,14 +83,15 @@ fn rejects_tampered_proof() {
let env = Env::default();
env.mock_all_auths();
let c = setup(&env);
c.set_vk(&symbol_short!("kyc"), &Bytes::from_slice(&env, fixture!("kyc", "vk")));
c.set_vk(&symbol_short!("kyc"), &1u32, &Bytes::from_slice(&env, fixture!("kyc", "vk")));

let mut bad = fixture!("kyc", "proof").to_vec();
bad[5000] ^= 0xff;
assert!(!c.verify_proof(
&symbol_short!("kyc"),
&Bytes::from_slice(&env, &bad),
&Bytes::from_slice(&env, fixture!("kyc", "public_inputs")),
&None,
));
}

Expand All @@ -94,11 +100,12 @@ fn rejects_wrong_length_proof() {
let env = Env::default();
env.mock_all_auths();
let c = setup(&env);
c.set_vk(&symbol_short!("kyc"), &Bytes::from_slice(&env, fixture!("kyc", "vk")));
c.set_vk(&symbol_short!("kyc"), &1u32, &Bytes::from_slice(&env, fixture!("kyc", "vk")));
assert!(!c.verify_proof(
&symbol_short!("kyc"),
&Bytes::from_array(&env, &[0u8; 16]),
&Bytes::from_slice(&env, fixture!("kyc", "public_inputs")),
&None,
));
}

Expand All @@ -108,7 +115,7 @@ fn set_vk_rejects_garbage() {
let env = Env::default();
env.mock_all_auths();
let c = setup(&env);
c.set_vk(&symbol_short!("kyc"), &Bytes::from_array(&env, &[1, 2, 3]));
c.set_vk(&symbol_short!("kyc"), &1u32, &Bytes::from_array(&env, &[1, 2, 3]));
}

#[test]
Expand All @@ -121,5 +128,152 @@ fn panics_without_vk() {
&symbol_short!("age"),
&Bytes::from_slice(&env, fixture!("kyc", "proof")),
&Bytes::from_slice(&env, fixture!("kyc", "public_inputs")),
&None,
);
}

#[test]
fn verifies_when_vk_set_and_inputs_present() {
let env = Env::default();
env.mock_all_auths();
let c = setup(&env);
c.set_vk(&symbol_short!("kyc"), &1u32, &Bytes::from_slice(&env, fixture!("kyc", "vk")));
assert!(c.verify_proof(
&symbol_short!("kyc"),
&Bytes::from_slice(&env, fixture!("kyc", "proof")),
&Bytes::from_slice(&env, fixture!("kyc", "public_inputs")),
&None,
));
}

#[test]
fn rejects_empty_proof() {
let env = Env::default();
env.mock_all_auths();
let c = setup(&env);
c.set_vk(&symbol_short!("kyc"), &1u32, &Bytes::from_slice(&env, fixture!("kyc", "vk")));
assert!(!c.verify_proof(
&symbol_short!("kyc"),
&Bytes::from_array(&env, &[0u8; 0]),
&Bytes::from_slice(&env, fixture!("kyc", "public_inputs")),
&None,
));
}

#[test]
fn verifies_real_proof() {
let env = Env::default();
env.mock_all_auths();
let c = setup(&env);
c.set_vk(&symbol_short!("kyc"), &1u32, &Bytes::from_slice(&env, fixture!("kyc", "vk")));
assert!(c.verify_proof(
&symbol_short!("kyc"),
&Bytes::from_slice(&env, fixture!("kyc", "proof")),
&Bytes::from_slice(&env, fixture!("kyc", "public_inputs")),
&None,
));
}

// ── VK versioning tests ────────────────────────────────────────────────────

/// After registering VK version 2, old proofs verified against version 1 still
/// pass because each version's VK is stored independently.
#[test]
fn old_proof_valid_after_upgrade() {
let env = Env::default();
env.mock_all_auths();
let c = setup(&env);

// Register version 1 VK and verify a proof against it.
c.set_vk(&symbol_short!("kyc"), &1u32, &Bytes::from_slice(&env, fixture!("kyc", "vk")));
assert!(c.verify_proof(
&symbol_short!("kyc"),
&Bytes::from_slice(&env, fixture!("kyc", "proof")),
&Bytes::from_slice(&env, fixture!("kyc", "public_inputs")),
&Some(1u32),
));

// Register version 2 VK (same fixture — in practice it would be different).
c.set_vk(&symbol_short!("kyc"), &2u32, &Bytes::from_slice(&env, fixture!("kyc", "vk")));

// Old proof against version 1 must still verify.
assert!(c.verify_proof(
&symbol_short!("kyc"),
&Bytes::from_slice(&env, fixture!("kyc", "proof")),
&Bytes::from_slice(&env, fixture!("kyc", "public_inputs")),
&Some(1u32),
));

// New proof against version 2 (latest) also verifies.
assert!(c.verify_proof(
&symbol_short!("kyc"),
&Bytes::from_slice(&env, fixture!("kyc", "proof")),
&Bytes::from_slice(&env, fixture!("kyc", "public_inputs")),
&Some(2u32),
));

// None defaults to latest (version 2).
assert!(c.verify_proof(
&symbol_short!("kyc"),
&Bytes::from_slice(&env, fixture!("kyc", "proof")),
&Bytes::from_slice(&env, fixture!("kyc", "public_inputs")),
&None,
));

// get_latest_version returns 2.
assert_eq!(c.get_latest_version(&symbol_short!("kyc")), 2);
}

/// After deprecating version 1, new submissions against it must be rejected.
#[test]
#[should_panic]
fn deprecated_version_rejected() {
let env = Env::default();
env.mock_all_auths();
let c = setup(&env);

c.set_vk(&symbol_short!("kyc"), &1u32, &Bytes::from_slice(&env, fixture!("kyc", "vk")));
c.set_vk(&symbol_short!("kyc"), &2u32, &Bytes::from_slice(&env, fixture!("kyc", "vk")));

// Version 2 still works before deprecation.
assert!(c.verify_proof(
&symbol_short!("kyc"),
&Bytes::from_slice(&env, fixture!("kyc", "proof")),
&Bytes::from_slice(&env, fixture!("kyc", "public_inputs")),
&Some(2u32),
));

// Deprecate version 1.
c.deprecate_version(&symbol_short!("kyc"), &1u32);

// Attempting to verify against deprecated version 1 must panic.
c.verify_proof(
&symbol_short!("kyc"),
&Bytes::from_slice(&env, fixture!("kyc", "proof")),
&Bytes::from_slice(&env, fixture!("kyc", "public_inputs")),
&Some(1u32),
);
}

/// Basic smoke test for `get_latest_version` — starts at 0, increments as
/// higher versions are registered.
#[test]
fn get_latest_version_tracks_highest() {
let env = Env::default();
env.mock_all_auths();
let c = setup(&env);

// Initially 0 — no VK registered.
assert_eq!(c.get_latest_version(&symbol_short!("kyc")), 0);

c.set_vk(&symbol_short!("kyc"), &3u32, &Bytes::from_slice(&env, fixture!("kyc", "vk")));
assert_eq!(c.get_latest_version(&symbol_short!("kyc")), 3);

// Registering a lower version does not roll back the latest pointer.
c.set_vk(&symbol_short!("kyc"), &2u32, &Bytes::from_slice(&env, fixture!("kyc", "vk")));
assert_eq!(c.get_latest_version(&symbol_short!("kyc")), 3);

// Registering a higher version updates the pointer.
c.set_vk(&symbol_short!("kyc"), &5u32, &Bytes::from_slice(&env, fixture!("kyc", "vk")));
assert_eq!(c.get_latest_version(&symbol_short!("kyc")), 5);
}
Loading
Loading