Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
19 changes: 18 additions & 1 deletion peerx-contracts/counter/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
use soroban_sdk::{
contract, contractimpl, contracttype, symbol_short, Address, Env, Map, Symbol, Vec,
};
#[cfg(feature = "experimental")]
use soroban_sdk::Bytes;

// Bring in modules from parent directory
mod admin;
Expand Down Expand Up @@ -156,7 +158,7 @@ pub use zkp_proof_generation::ProofGenerator;
#[cfg(feature = "experimental")]
pub use zkp_types::{
AuditEventType, AuditLogEntry, BalanceProof, Commitment, PrivateTransaction, ProofScheme,
ProofVerificationResult, RangeProof, TransactionWitness, ZKProof,
ProofVerificationResult, RangeProof, Receipt, TransactionWitness, ZKProof,
};
#[cfg(feature = "experimental")]
pub use zkp_verification::ProofVerifier;
Expand Down Expand Up @@ -1524,9 +1526,24 @@ impl CounterContract {
pub fn withdraw_commission(env: Env, user: Address) -> i128 {
referral_system::withdraw_commission(&env, user)
}

// ── Zero-Knowledge Privacy ───────────────────────────────────────────────

/// Fetch the audit-friendly receipt for a private transaction, by its
/// transaction hash. Public: off-chain consumers (indexers, compliance
/// tooling) use this to verify a private transaction occurred without
/// the contract exposing the underlying private witness values.
///
/// Returns `ZKPError::ProofNotFound` for an empty or unrecognized hash.
#[cfg(feature = "experimental")]
pub fn private_tx_receipt(env: Env, tx_hash: Bytes) -> Result<Receipt, ZKPError> {
zkp_verification::receipts::get_receipt(&env, tx_hash)
}
}

#[cfg(all(test, feature = "experimental"))]
mod migration_tests;
#[cfg(all(test, feature = "experimental"))]
mod zkp_receipt_tests;
mod risk_management_tests;
mod governance_tests;
2 changes: 2 additions & 0 deletions peerx-contracts/counter/src/zkp_errors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -36,4 +36,6 @@ pub enum ZKPError {
ComplianceCheckFailed = 513,
/// Cryptographic operation failed
CryptoOperationFailed = 514,
/// No receipt found for the given (or empty) transaction hash
ProofNotFound = 515,
}
64 changes: 64 additions & 0 deletions peerx-contracts/counter/src/zkp_receipt_tests.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
#![cfg(test)]

use crate::zkp_types::ProofScheme;
use crate::zkp_verification::receipts::issue_receipt;
use crate::{CounterContract, CounterContractClient, Receipt, ZKPError, ZKProof};
use soroban_sdk::{Address, Bytes, Env};

fn setup() -> (Env, Address, CounterContractClient<'static>) {
let env = Env::default();
let contract_id = env.register_contract(None, CounterContract);
let client = CounterContractClient::new(&env, &contract_id);
(env, contract_id, client)
}

#[test]
fn private_tx_receipt_returns_proof_not_found_for_empty_hash() {
let (env, _contract_id, client) = setup();
let empty_hash = Bytes::new(&env);

assert_eq!(
client.try_private_tx_receipt(&empty_hash),
Err(Ok(ZKPError::ProofNotFound))
);
}

#[test]
fn private_tx_receipt_returns_proof_not_found_for_unknown_hash() {
let (env, _contract_id, client) = setup();
let unknown_hash = Bytes::from_array(&env, &[7u8; 32]);

assert_eq!(
client.try_private_tx_receipt(&unknown_hash),
Err(Ok(ZKPError::ProofNotFound))
);
}

#[test]
fn private_tx_receipt_returns_issued_receipt_with_all_fields() {
let (env, contract_id, client) = setup();

let tx_hash = Bytes::from_array(&env, &[1u8; 32]);
let commitment = Bytes::from_array(&env, &[2u8; 32]);
let witness_hash = Bytes::from_array(&env, &[3u8; 32]);
let proof = ZKProof {
proof_data: Bytes::from_array(&env, &[4u8; 32]),
scheme: ProofScheme::ZkSnark,
};

env.as_contract(&contract_id, || {
issue_receipt(
&env,
&tx_hash,
commitment.clone(),
witness_hash.clone(),
proof.clone(),
);
});

let receipt: Receipt = client.private_tx_receipt(&tx_hash);

assert_eq!(receipt.commitment, commitment);
assert_eq!(receipt.witness, witness_hash);
assert_eq!(receipt.proof, proof);
}
21 changes: 20 additions & 1 deletion peerx-contracts/counter/src/zkp_types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ pub struct Commitment {
/// Represents a zero-knowledge proof of knowledge
/// Generic proof structure that can represent different ZKP schemes
#[contracttype]
#[derive(Clone, Debug)]
#[derive(Clone, Debug, PartialEq)]
pub struct ZKProof {
/// The proof data
pub proof_data: Bytes,
Expand Down Expand Up @@ -168,6 +168,25 @@ pub struct ProofMetrics {
pub verification_gas: u64,
}

/// Audit-friendly receipt for a private transaction, issued for off-chain
/// consumers (indexers, compliance tooling) so they can verify a private
/// transaction occurred without the contract exposing the underlying
/// private witness values.
#[contracttype]
#[derive(Clone, Debug, PartialEq)]
pub struct Receipt {
/// Commitment to the transacted amount.
pub commitment: Bytes,
/// Hash of the transaction witness - never the raw witness, which
/// carries private values (amount, sender balance, blinding factors)
/// that a private transaction is explicitly meant to keep off-chain.
pub witness: Bytes,
/// The zero-knowledge proof attached to the transaction.
pub proof: ZKProof,
/// When the receipt was issued.
pub timestamp: u64,
}

/// Configuration for the ZKP system
#[contracttype]
#[derive(Clone, Debug)]
Expand Down
101 changes: 101 additions & 0 deletions peerx-contracts/counter/src/zkp_verification.rs
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,107 @@ pub mod middleware {
}
}

/// Audit receipt issuance and lookup for private transactions.
///
/// Receipts are the audit-friendly artifact off-chain consumers (indexers,
/// compliance tooling) query instead of reconstructing a private
/// transaction's internals. Storage is keyed by transaction hash so lookups
/// don't require scanning; `issue_receipt` is the only writer, called once
/// a private transaction has been processed.
pub mod receipts {
use soroban_sdk::{symbol_short, Bytes, Env, Symbol};

use crate::zkp_errors::ZKPError;
use crate::zkp_types::{Receipt, ZKProof};

const RECEIPT_PREFIX: Symbol = symbol_short!("zkprcpt");

/// Durably records an audit-friendly receipt for a private transaction,
/// keyed by `tx_hash`. `witness_hash` must be a hash/commitment of the
/// witness, not the raw witness values.
pub fn issue_receipt(
env: &Env,
tx_hash: &Bytes,
commitment: Bytes,
witness_hash: Bytes,
proof: ZKProof,
) {
let receipt = Receipt {
commitment,
witness: witness_hash,
proof,
timestamp: env.ledger().timestamp(),
};
env.storage()
.persistent()
.set(&(RECEIPT_PREFIX, tx_hash.clone()), &receipt);
}

/// Fetches the audit receipt for `tx_hash`.
///
/// Returns `ZKPError::ProofNotFound` for an empty or unrecognized hash.
pub fn get_receipt(env: &Env, tx_hash: Bytes) -> Result<Receipt, ZKPError> {
if tx_hash.is_empty() {
return Err(ZKPError::ProofNotFound);
}

env.storage()
.persistent()
.get(&(RECEIPT_PREFIX, tx_hash))
.ok_or(ZKPError::ProofNotFound)
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn empty_hash_returns_proof_not_found() {
let env = Env::default();
let empty_hash = Bytes::new(&env);

assert_eq!(get_receipt(&env, empty_hash), Err(ZKPError::ProofNotFound));
}

#[test]
fn unknown_hash_returns_proof_not_found() {
let env = Env::default();
let unknown_hash = Bytes::from_array(&env, &[9u8; 32]);

assert_eq!(
get_receipt(&env, unknown_hash),
Err(ZKPError::ProofNotFound)
);
}

#[test]
fn issued_receipt_round_trips_with_all_fields() {
let env = Env::default();
let tx_hash = Bytes::from_array(&env, &[1u8; 32]);
let commitment = Bytes::from_array(&env, &[2u8; 32]);
let witness_hash = Bytes::from_array(&env, &[3u8; 32]);
let proof = ZKProof {
proof_data: Bytes::from_array(&env, &[4u8; 32]),
scheme: crate::zkp_types::ProofScheme::Bulletproof,
};

issue_receipt(
&env,
&tx_hash,
commitment.clone(),
witness_hash.clone(),
proof.clone(),
);

let receipt = get_receipt(&env, tx_hash).unwrap();
assert_eq!(receipt.commitment, commitment);
assert_eq!(receipt.witness, witness_hash);
assert_eq!(receipt.proof.proof_data, proof.proof_data);
assert_eq!(receipt.proof.scheme, proof.scheme);
}
}
}

/// State management for proof verification
pub mod state {
use soroban_sdk::{symbol_short, Bytes, Env, Map, Symbol};
Expand Down
Loading