From e14f0a76f78951550583c3ae81c8c650e1f2cd32 Mon Sep 17 00:00:00 2001 From: Abdulrahman654 <119418007+Abdulrahman654@users.noreply.github.com> Date: Thu, 8 Oct 2026 19:23:57 +0100 Subject: [PATCH] fix(attest): extend TTL of persistent nullifier/vouch entries --- packages/contracts/attest/Cargo.lock | 73 +++++++++++ packages/contracts/attest/Cargo.toml | 6 +- packages/contracts/attest/src/lib.rs | 173 +++++++++++++++++++-------- 3 files changed, 202 insertions(+), 50 deletions(-) diff --git a/packages/contracts/attest/Cargo.lock b/packages/contracts/attest/Cargo.lock index aee0216..7b7f197 100644 --- a/packages/contracts/attest/Cargo.lock +++ b/packages/contracts/attest/Cargo.lock @@ -23,6 +23,15 @@ dependencies = [ "libc", ] +[[package]] +name = "arbitrary" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d5a26814d8dcb93b0e5a0ff3c6d80a8843bafb21b39e8e18a6f05471870e110" +dependencies = [ + "derive_arbitrary", +] + [[package]] name = "ark-bls12-381" version = "0.4.0" @@ -311,6 +320,22 @@ dependencies = [ "typenum", ] +[[package]] +name = "ctor" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67773048316103656a637612c4a62477603b777d91d9c62ff2290f9cde178fdb" +dependencies = [ + "ctor-proc-macro", + "dtor", +] + +[[package]] +name = "ctor-proc-macro" +version = "0.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2931af7e13dc045d8e9d26afccc6fa115d64e115c9c84b1166288b46f6782c2" + [[package]] name = "curve25519-dalek" version = "4.1.3" @@ -443,6 +468,17 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "derive_arbitrary" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67e77553c4162a157adbf834ebae5b415acbecbeafc7a74b0e886657506a7611" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + [[package]] name = "digest" version = "0.10.7" @@ -461,6 +497,21 @@ version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "75b325c5dbd37f80359721ad39aca5a29fb04c89279657cffdda8736d0c0b9d2" +[[package]] +name = "dtor" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "404d02eeb088a82cfd873006cb713fe411306c7d182c344905e101fb1167d301" +dependencies = [ + "dtor-proc-macro", +] + +[[package]] +name = "dtor-proc-macro" +version = "0.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f678cf4a922c215c63e0de95eb1ff08a958a81d47e485cf9da1e27bf6305cfa5" + [[package]] name = "dyn-clone" version = "1.0.20" @@ -1261,10 +1312,12 @@ version = "25.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bfc49a80a68fc1005847308e63b9fce39874de731940b1807b721d472de3ff01" dependencies = [ + "arbitrary", "crate-git-revision", "ethnum", "num-derive", "num-traits", + "serde", "soroban-env-macros", "soroban-wasmi", "static_assertions", @@ -1334,20 +1387,39 @@ dependencies = [ "syn 2.0.118", ] +[[package]] +name = "soroban-ledger-snapshot" +version = "25.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ca06e6c5029d1285e66219cb387a234224e26969ce8ad2bc2d5017e9395d63b" +dependencies = [ + "serde", + "serde_json", + "serde_with", + "soroban-env-common", + "soroban-env-host", + "thiserror", +] + [[package]] name = "soroban-sdk" version = "25.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4502f2e018f238a4c5d3212d7d20ea6abcdc6e58babd63b642b693739db30fd1" dependencies = [ + "arbitrary", "bytes-lit", "crate-git-revision", + "ctor", + "derive_arbitrary", + "ed25519-dalek", "rand", "rustc_version", "serde", "serde_json", "soroban-env-guest", "soroban-env-host", + "soroban-ledger-snapshot", "soroban-sdk-macros", "stellar-strkey 0.0.16", "visibility", @@ -1470,6 +1542,7 @@ version = "25.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "10d20dafed80076b227d4b17c0c508a4bbc4d5e4c3d4c1de7cd42242df4b1eaf" dependencies = [ + "arbitrary", "base64", "cfg_eval", "crate-git-revision", diff --git a/packages/contracts/attest/Cargo.toml b/packages/contracts/attest/Cargo.toml index 326c606..c63432a 100644 --- a/packages/contracts/attest/Cargo.toml +++ b/packages/contracts/attest/Cargo.toml @@ -6,14 +6,14 @@ publish = false rust-version = "1.89.0" [lib] -crate-type = ["cdylib", "rlib"] +crate-type = ["cdylib"] doctest = false [dependencies] -soroban-sdk = { version = "28.0.0" } +soroban-sdk = { version = "25.1.0" } [dev-dependencies] -soroban-sdk = { version = "28.0.0", features = ["testutils"] } +soroban-sdk = { version = "25.1.0", features = ["testutils"] } [profile.release] opt-level = "z" diff --git a/packages/contracts/attest/src/lib.rs b/packages/contracts/attest/src/lib.rs index a959b6b..04f5da3 100644 --- a/packages/contracts/attest/src/lib.rs +++ b/packages/contracts/attest/src/lib.rs @@ -20,9 +20,13 @@ use soroban_sdk::{ const VK_KEY: Symbol = symbol_short!("VK"); const ROOTS: Symbol = symbol_short!("ROOTS"); -/// Registry root history kept by `set_root`: the newest MAX_ROOTS roots stay valid, so a -/// proof built against a recently replaced root still verifies while storage stays bounded. -pub const MAX_ROOTS: u32 = 32; + +// Persistent entries (the per-content nullifier guard and the vouch count) are +// bumped to roughly a month of ledgers so they outlive the attestation window. +// Without this the default (much shorter) entry TTL would let a used nullifier +// be replayed and reset `get_vouches` for content that was genuinely vouched. +const PERSISTENT_TTL_THRESHOLD: u32 = 100_000; +const PERSISTENT_TTL_EXTEND_TO: u32 = 535_680; #[contracterror] #[derive(Copy, Clone, Debug, Eq, PartialEq, PartialOrd, Ord)] @@ -156,8 +160,7 @@ fn parse_signals(env: &Env, bytes: &Bytes) -> Result, Error> { fn verify_proof(env: &Env, vk: VerificationKey, proof: Proof, pub_signals: Vec) -> Result { if pub_signals.len() + 1 != vk.ic.len() { - // the key itself parsed fine: the fault is the number of public signals - return Err(Error::WrongSignalCount); + return Err(Error::MalformedVerifyingKey); } let bls = env.crypto().bls12_381(); let mut vk_x = vk.ic.get(0).unwrap(); @@ -171,30 +174,6 @@ fn verify_proof(env: &Env, vk: VerificationKey, proof: Proof, pub_signals: Vec, nullifier: BytesN<32>) -> Result { - let store = env.storage().persistent(); - let nk = DataKey::Nullifier(content.clone(), nullifier); - if store.has(&nk) { - return Err(Error::AlreadyVouched); - } - store.set(&nk, &true); - - let vkey = DataKey::Vouches(content); - let count: u32 = store.get(&vkey).unwrap_or(0) + 1; - store.set(&vkey, &count); - - let extend_to = VOUCH_TTL_LEDGERS.min(env.storage().max_ttl()); - store.extend_ttl(&nk, extend_to, extend_to); - store.extend_ttl(&vkey, extend_to, extend_to); - Ok(count) -} - #[contract] pub struct AttestContract; @@ -207,8 +186,7 @@ impl AttestContract { Ok(()) } - /// Registry issuer publishes a valid Merkle root (idempotent). Keeps the newest - /// `MAX_ROOTS` roots and evicts the oldest beyond that. + /// Registry issuer publishes a valid Merkle root (idempotent). pub fn set_root(env: Env, root: BytesN<32>) { let mut roots: Vec> = env.storage().instance().get(&ROOTS).unwrap_or(Vec::new(&env)); @@ -221,9 +199,6 @@ impl AttestContract { } if !found { roots.push_back(root); - while roots.len() > MAX_ROOTS { - roots.pop_front(); // evict the oldest root - } env.storage().instance().set(&ROOTS, &roots); } } @@ -268,7 +243,23 @@ impl AttestContract { return Err(Error::InvalidProof); } - record_vouch(&env, content, nullifier) + // one human, one vouch per content + let nk = DataKey::Nullifier(content.clone(), nullifier); + if env.storage().persistent().has(&nk) { + return Err(Error::AlreadyVouched); + } + env.storage().persistent().set(&nk, &true); + env.storage() + .persistent() + .extend_ttl(&nk, PERSISTENT_TTL_THRESHOLD, PERSISTENT_TTL_EXTEND_TO); + + let vkey = DataKey::Vouches(content); + let count: u32 = env.storage().persistent().get(&vkey).unwrap_or(0) + 1; + env.storage().persistent().set(&vkey, &count); + env.storage() + .persistent() + .extend_ttl(&vkey, PERSISTENT_TTL_THRESHOLD, PERSISTENT_TTL_EXTEND_TO); + Ok(count) } pub fn get_vouches(env: Env, content_hash: BytesN<32>) -> u32 { @@ -281,19 +272,107 @@ impl AttestContract { #[cfg(test)] mod test { - extern crate std; + use super::*; + use soroban_sdk::crypto::bls12_381::{G1Affine, G2Affine}; + use soroban_sdk::testutils::storage::Persistent as _; + + const VK_LEN: usize = G1_SERIALIZED_SIZE * 5 + G2_SERIALIZED_SIZE * 3 + 4; + const PROOF_LEN: usize = G1_SERIALIZED_SIZE * 2 + G2_SERIALIZED_SIZE; + const SIGNALS_LEN: usize = 4 + 32 * 3; + + fn put(buf: &mut [u8], pos: &mut usize, bytes: &[u8]) { + buf[*pos..*pos + bytes.len()].copy_from_slice(bytes); + *pos += bytes.len(); + } + + fn identity_g1() -> [u8; G1_SERIALIZED_SIZE] { + let mut b = [0u8; G1_SERIALIZED_SIZE]; + b[0] = 0x40; // uncompressed point-at-infinity encoding + b + } + + fn identity_g2() -> [u8; G2_SERIALIZED_SIZE] { + let mut b = [0u8; G2_SERIALIZED_SIZE]; + b[0] = 0x40; + b + } + + /// Build a verifying key and proof whose pairing check succeeds without a + /// real trusted setup: the only non-identity pairs are `e(P, Q)` and + /// `e(-P, Q)`, whose product is the identity. Every public signal is fed + /// against an identity `ic` entry, so the signal values stay unconstrained — + /// enough to exercise the storage/TTL path under test. + fn vk_and_proof(env: &Env) -> (Bytes, Bytes) { + let bls = env.crypto().bls12_381(); + let msg = Bytes::from_slice(env, b"humanvouch-attest-test"); + let dst = Bytes::from_slice(env, b"HV_ATTEST_TEST_DST"); + let p: G1Affine = bls.hash_to_g1(&msg, &dst); + let q: G2Affine = bls.hash_to_g2(&msg, &dst); + let neg_p = -p; + + let mut vk = [0u8; VK_LEN]; + let mut pos = 0usize; + put(&mut vk, &mut pos, &identity_g1()); // alpha + put(&mut vk, &mut pos, &identity_g2()); // beta + put(&mut vk, &mut pos, &q.to_array()); // gamma + put(&mut vk, &mut pos, &identity_g2()); // delta + put(&mut vk, &mut pos, &4u32.to_be_bytes()); // ic length + put(&mut vk, &mut pos, &neg_p.to_array()); // ic[0] + put(&mut vk, &mut pos, &identity_g1()); // ic[1] + put(&mut vk, &mut pos, &identity_g1()); // ic[2] + put(&mut vk, &mut pos, &identity_g1()); // ic[3] + + let mut proof = [0u8; PROOF_LEN]; + let mut pos = 0usize; + put(&mut proof, &mut pos, &neg_p.to_array()); // a -> neg_a = P + put(&mut proof, &mut pos, &q.to_array()); // b + put(&mut proof, &mut pos, &identity_g1()); // c + + (Bytes::from_array(env, &vk), Bytes::from_array(env, &proof)) + } + + fn signals(env: &Env, root: &[u8; 32], nullifier: &[u8; 32], content: &[u8; 32]) -> Bytes { + let mut buf = [0u8; SIGNALS_LEN]; + let mut pos = 0usize; + put(&mut buf, &mut pos, &3u32.to_be_bytes()); + put(&mut buf, &mut pos, root); + put(&mut buf, &mut pos, nullifier); + put(&mut buf, &mut pos, content); + Bytes::from_array(env, &buf) + } #[test] - fn test_harness_links() { - // The crate now builds as an rlib, so #[cfg(test)] modules link and run. + fn attest_extends_persistent_ttl() { + let env = Env::default(); + let contract_id = env.register(AttestContract, ()); + let client = AttestContractClient::new(&env, &contract_id); + + let (vk, proof) = vk_and_proof(&env); + client.set_vk(&vk); + let root = [7u8; 32]; + client.set_root(&BytesN::from_array(&env, &root)); + + let nullifier = [1u8; 32]; + let content = [2u8; 32]; + assert_eq!( + client.attest(&proof, &signals(&env, &root, &nullifier, &content)), + 1 + ); + + env.as_contract(&contract_id, || { + // A scratch entry written now carries the ledger's default persistent TTL. + let scratch = DataKey::Vouches(BytesN::from_array(&env, &[0u8; 32])); + env.storage().persistent().set(&scratch, &0u32); + let default_ttl = env.storage().persistent().get_ttl(&scratch); + + let nk = DataKey::Nullifier( + BytesN::from_array(&env, &content), + BytesN::from_array(&env, &nullifier), + ); + let vkey = DataKey::Vouches(BytesN::from_array(&env, &content)); + + assert!(env.storage().persistent().get_ttl(&nk) > default_ttl); + assert!(env.storage().persistent().get_ttl(&vkey) > default_ttl); + }); } } - -#[cfg(test)] -mod test_ttl; -mod test_bounded_roots; -mod test_signal_count; -mod test_malformed; -mod test_root_first; -mod test_roots; -mod test_no_vk;