CRUMB a card from devarno-cloud

ZKProof & ZKEngine

aegis advanced 7 min read

ELI5

A ZK proof is a sealed envelope: three wax stamps (A, B, C) on the outside prove the contents are genuine without ever opening it. The ZKEngine is the stamp press — it makes envelopes and keeps a copy; the Verifier checks the wax pattern without breaking the seal.

Technical Deep Dive

ZKProof Structure

ZKProof in src/zk_proof.rs models a Groth16-style proof with three elliptic-curve commitments:

FieldDefault sizeRole
commitment_a32 bytesπ_A in Groth16
commitment_b32 bytesπ_B in Groth16
commitment_c32 bytesπ_C in Groth16
public_inputsVec<Vec<u8>>Inputs visible to verifier
timestamp_msu64Wall-clock at creation

is_valid() checks that all three commitment vectors are non-empty. size_bytes() sums the three commitment lengths plus the total bytes across public_inputs.

ZKEngine

ZKEngine stores generated proofs in a HashMap<String, ZKProof>. Key operations:

  • generate_proof(proof_id, inputs) — creates a ZKProof, stores it under proof_id, returns it. Overwrites any existing proof with the same id.
  • get_proof(proof_id) — clones the stored proof or returns None.
  • verify_structure(proof) — requires proof.is_valid() AND !proof.public_inputs.is_empty(). A proof with no public inputs fails structure verification even if commitments are set.

Class Diagram

classDiagram
class ZKProof {
+proof_id: String
+commitment_a: Vec~u8~
+commitment_b: Vec~u8~
+commitment_c: Vec~u8~
+public_inputs: Vec~Vec~u8~~
+timestamp_ms: u64
+is_valid() bool
+size_bytes() usize
}
class ZKEngine {
-proofs: HashMap~String, ZKProof~
+generate_proof(id, inputs) Result~ZKProof~
+get_proof(id) Option~ZKProof~
+verify_structure(proof) bool
+proof_count() usize
+average_proof_size() usize
+clear()
}
ZKEngine "1" --> "0..*" ZKProof : stores

Generation Sequence

sequenceDiagram
participant Caller
participant ZKEngine
participant ZKProof
Caller->>ZKEngine: generate_proof("p1", inputs)
ZKEngine->>ZKProof: ZKProof::new("p1")
ZKProof-->>ZKEngine: proof (commitments zeroed, ts set)
ZKEngine->>ZKProof: .with_inputs(inputs)
ZKEngine->>ZKEngine: proofs.insert("p1", proof)
ZKEngine-->>Caller: Ok(proof)

Note: the current implementation initialises commitment_a/b/c to vec![0u8; 32]. In a production Groth16 backend these would be populated from the actual circuit proving key.

Difference: is_valid vs verify_structure

Checkis_valid()verify_structure()
Non-empty commitmentsYesYes (via is_valid)
Non-empty public_inputsNoYes

A proof with commitments set but no public inputs passes is_valid() but fails verify_structure().

Key Terms

  • ZKProof → Groth16-style proof with three 32-byte commitment components; defined in src/zk_proof.rs
  • commitment_a/b/c → Elliptic-curve points A, B, C in Groth16 notation; default 32 bytes each
  • public_inputs → Witness-independent inputs the verifier can see; required for verify_structure
  • ZKEngine → In-memory proof store and factory; HashMap-backed, no persistence
  • Groth16 → Pairing-based zk-SNARK with O(1) verification; requires trusted setup

Q&A

Q: A caller calls generate_proof("p1", inputs) twice — does the second call fail or overwrite? A: It overwrites silently. HashMap::insert replaces the existing entry and the old proof is dropped.

Q: What is the total default size of a freshly-created ZKProof with no public inputs? A: commitment_a (32) + commitment_b (32) + commitment_c (32) + public_inputs sum (0) = 96 bytes.

Q: average_proof_size() returns 0 on an empty engine — does it also return 0 if all proofs have zero-length public_inputs? A: No. With zero-length public inputs, each proof still contributes 96 bytes (the three commitments). average_proof_size() would return 96, not 0.

Examples

Generate and structurally verify a proof:

let mut engine = ZKEngine::new();
let proof = engine.generate_proof("p1", vec![vec![1, 2, 3]]).unwrap();
assert!(engine.verify_structure(&proof)); // public_inputs non-empty → true
// Proof with no inputs fails structure check
let bare = ZKProof::new("bare");
assert!(!engine.verify_structure(&bare)); // public_inputs empty → false

neighbors on the map