|
| 1 | +//! On-chain nullifier registry, preventing the same ZK proof secret |
| 2 | +//! (a `blinding_factor` + `service_id` pair) from being used to prove the |
| 3 | +//! same payment twice. |
| 4 | +//! |
| 5 | +//! All submitted nullifiers live in a single persistent `Map`, so every |
| 6 | +//! lookup/insert reads and rewrites the whole set -- storage and per-call |
| 7 | +//! cost grow linearly with the number of nullifiers ever submitted. That's |
| 8 | +//! an accepted MVP tradeoff (see the issue this module was built for); |
| 9 | +//! revisit if proof volume grows large enough to make it a bottleneck. |
| 10 | +
|
| 11 | +use soroban_sdk::{Bytes, BytesN, Env, Map}; |
| 12 | + |
| 13 | +use crate::DataKey; |
| 14 | + |
| 15 | +/// Compute a nullifier: `SHA-256("syncro:payment:v1" || blinding_factor || service_id)`. |
| 16 | +/// |
| 17 | +/// Deterministic per `(blinding_factor, service_id)` pair, so the same |
| 18 | +/// secret can never produce two different nullifiers. It reveals nothing |
| 19 | +/// about the underlying payment (amount, user, timestamp) -- it's a |
| 20 | +/// one-way hash of values only the prover knows, so an observer learns |
| 21 | +/// only "some proof already used this nullifier," never which payment. |
| 22 | +pub fn compute_nullifier( |
| 23 | + env: &Env, |
| 24 | + blinding_factor: &BytesN<32>, |
| 25 | + service_id: &Bytes, |
| 26 | +) -> BytesN<32> { |
| 27 | + let mut payload = Bytes::from_slice(env, b"syncro:payment:v1"); |
| 28 | + payload.append(&Bytes::from_slice(env, &blinding_factor.to_array())); |
| 29 | + payload.append(&service_id.clone()); |
| 30 | + |
| 31 | + env.crypto().sha256(&payload).into() |
| 32 | +} |
| 33 | + |
| 34 | +fn load(env: &Env) -> Map<BytesN<32>, bool> { |
| 35 | + env.storage() |
| 36 | + .persistent() |
| 37 | + .get(&DataKey::Nullifiers) |
| 38 | + .unwrap_or(Map::new(env)) |
| 39 | +} |
| 40 | + |
| 41 | +/// Whether `nullifier` has already been recorded. |
| 42 | +pub fn is_used(env: &Env, nullifier: &BytesN<32>) -> bool { |
| 43 | + load(env).contains_key(nullifier.clone()) |
| 44 | +} |
| 45 | + |
| 46 | +/// Record `nullifier` as used. |
| 47 | +/// |
| 48 | +/// Returns `true` if it was freshly recorded, `false` if it was already |
| 49 | +/// present (a duplicate -- the existing entry is left untouched). |
| 50 | +pub fn record(env: &Env, nullifier: BytesN<32>) -> bool { |
| 51 | + let mut nullifiers = load(env); |
| 52 | + if nullifiers.contains_key(nullifier.clone()) { |
| 53 | + return false; |
| 54 | + } |
| 55 | + nullifiers.set(nullifier, true); |
| 56 | + env.storage() |
| 57 | + .persistent() |
| 58 | + .set(&DataKey::Nullifiers, &nullifiers); |
| 59 | + true |
| 60 | +} |
0 commit comments