Skip to content
Open
Show file tree
Hide file tree
Changes from 16 commits
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
4 changes: 2 additions & 2 deletions circuits/range_proof/Prover.toml
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
value = "40000"
salt = "2024"
commitment = "10939069837981219911031933662244407434460168120308516075205226378763146828090"
commitment = "11464784431290725290890825015648660771764477860874280613603757226488105877198"
min = "30000"
max = "50000"
issuer_x = [62, 72, 233, 139, 1, 120, 194, 91, 166, 62, 68, 43, 79, 232, 156, 91, 89, 165, 40, 83, 125, 90, 232, 8, 228, 119, 17, 59, 196, 72, 32, 251]
issuer_y = [7, 217, 196, 88, 103, 117, 28, 168, 249, 234, 195, 188, 47, 23, 13, 0, 168, 88, 125, 213, 56, 190, 32, 144, 120, 211, 244, 170, 55, 97, 227, 101]
sig = [6, 105, 8, 213, 3, 158, 238, 245, 119, 101, 102, 155, 46, 22, 172, 87, 38, 148, 247, 73, 236, 31, 63, 131, 118, 151, 67, 188, 119, 51, 198, 25, 123, 53, 204, 249, 188, 186, 132, 223, 10, 143, 113, 160, 33, 167, 46, 176, 255, 56, 190, 210, 121, 163, 161, 185, 239, 161, 229, 62, 250, 49, 216, 164]
sig = [183, 31, 113, 105, 4, 237, 193, 53, 220, 59, 139, 54, 5, 130, 81, 214, 75, 186, 151, 114, 83, 34, 249, 86, 182, 178, 171, 251, 85, 116, 108, 234, 115, 194, 112, 134, 200, 136, 234, 45, 115, 241, 167, 64, 159, 213, 89, 30, 214, 233, 212, 177, 145, 218, 53, 16, 247, 231, 236, 97, 104, 92, 189, 129]
140 changes: 130 additions & 10 deletions contracts/credential_verifier/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,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 @@ -49,6 +55,12 @@ 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,
/// A VK is already registered at the requested (credential_type,
/// version); VKs are immutable once set — register a new version instead.
VkAlreadySet = 5,
}

#[contract]
Expand All @@ -60,24 +72,59 @@ 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.
/// Register the verification key for a credential circuit. Admin-only.
/// A version's VK is immutable once set — re-registering an existing
/// (credential_type, version) panics with `VkAlreadySet`; register a new
/// (higher) version to upgrade. The VK is validated by parsing it before
/// storage, rejecting malformed keys at set time.
// NOTE: We suppress the deprecation warning for `env.events().publish` here.
// The idiomatic Soroban v26 replacement is `#[contractevent]`; we use
// value-based publish to stay consistent with the rest of the codebase.
#[allow(deprecated)]
pub fn set_vk(env: Env, credential_type: Symbol, vk: Bytes) {
pub fn set_vk(env: Env, credential_type: Symbol, version: u32, vk: Bytes) {
let admin = Self::require_admin(&env);
// Version 0 is reserved — it is the ProofRecord sentinel meaning
// "no version" (see ProofRegistry), and no VK may be registered at 0.
if version == 0 {
panic_with_error!(&env, Error::VkInvalid);
}
let key = DataKey::Vk(credential_type.clone(), version);
// A version's VK is immutable once registered: proofs are verified
// and cached against it, so silently replacing the bytes would
// invalidate every existing proof. Register a new (higher) version
// to upgrade the circuit.
if env.storage().persistent().has(&key) {
panic_with_error!(&env, Error::VkAlreadySet);
}
if UltraHonkVerifier::new(&env, &vk).is_err() {
panic_with_error!(&env, Error::VkInvalid);
}
let key = DataKey::Vk(credential_type.clone());
env.storage().persistent().set(&key, &vk);
env.storage()
.persistent()
.extend_ttl(&key, VK_BUMP_THRESHOLD, VK_TTL);

// Auto-advance the latest-version pointer for this credential type so
// `verify_proof(None)` resolves to the newest registered VK. Re-setting
// an older version (out-of-order admin call) never moves it backwards.
let latest_key = DataKey::LatestVersion(credential_type.clone());
let current = env
.storage()
.persistent()
.get::<_, u32>(&latest_key)
.unwrap_or(0);
if version > current {
env.storage().persistent().set(&latest_key, &version);
}
Comment thread
Topmatrixmor2014 marked this conversation as resolved.
// Always refresh the latest pointer's TTL on any successful
// registration — not only when the version advances — so that
// `verify_proof(..., None)`, the default path for new submissions,
// can never lapse into `VkNotSet` after 180 days without a new
// circuit version.
env.storage()
.persistent()
.extend_ttl(&latest_key, VK_BUMP_THRESHOLD, VK_TTL);

// Emit: topics = ("cred_ver", "vk_set", credential_type)
// data = EventVkSet { admin }
Comment thread
Topmatrixmor2014 marked this conversation as resolved.
env.events().publish(
Expand All @@ -90,23 +137,96 @@ impl CredentialVerifier {
);
}

/// Admin-only. Marks a specific `(credential_type, version)` VK as
/// deprecated. New submissions against a deprecated version are rejected
/// by `verify_proof` (panics with `VersionDeprecated`), but the VK is not
/// deleted, so existing cached proofs that were verified against that
/// version remain readable in ProofRegistry.
pub fn deprecate_version(env: Env, credential_type: Symbol, version: u32) {
Self::require_admin(&env);

// Only versions that actually have a registered VK can be deprecated.
env.storage()
.persistent()
.get::<_, Bytes>(&DataKey::Vk(credential_type.clone(), version))
.unwrap_or_else(|| panic_with_error!(&env, Error::VkNotSet));

let dep_key = DataKey::DeprecatedVersion(credential_type, version);
env.storage().persistent().set(&dep_key, &true);
env.storage()
.persistent()
.extend_ttl(&dep_key, VK_BUMP_THRESHOLD, VK_TTL);
}

/// Admin-only. Refreshes the TTL of the `LatestVersion` pointer so that
/// `verify_proof(..., None)` — the default submission path — keeps
/// resolving in long-lived deployments that register no new circuit
/// versions. Panics with `VkNotSet` if no VK has been registered for the
/// credential type yet.
pub fn refresh_latest_version_ttl(env: Env, credential_type: Symbol) {
Self::require_admin(&env);
let latest_key = DataKey::LatestVersion(credential_type);
env.storage()
.persistent()
.get::<_, u32>(&latest_key)
.unwrap_or_else(|| panic_with_error!(&env, Error::VkNotSet));
env.storage()
.persistent()
.extend_ttl(&latest_key, VK_BUMP_THRESHOLD, VK_TTL);
}
Comment thread
Topmatrixmor2014 marked this conversation as resolved.

/// Returns the highest VK version registered for `credential_type`, or
/// panics with `VkNotSet` if no VK has been registered for the type yet.
pub fn get_latest_version(env: Env, credential_type: Symbol) -> u32 {
env.storage()
.persistent()
.get(&DataKey::LatestVersion(credential_type))
.unwrap_or_else(|| panic_with_error!(&env, Error::VkNotSet))
}

/// 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;
}

// Version 0 is reserved — it is the ProofRecord sentinel meaning
// "no version", and no VK may be registered at version 0.
// Reject an explicit `Some(0)` so callers don't silently hit latest.
let version = match vk_version {
Some(0) => panic_with_error!(&env, Error::VkNotSet),
Some(v) => v,
None => 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 Down
Loading
Loading