Skip to main content

zk402_facilitator/
lib.rs

1//! zk402 facilitator implementation
2//!
3//! Implements the five-step verification pipeline and settlement logic
4//! for the zk402 payment scheme.
5
6use std::collections::HashMap;
7use std::time::{SystemTime, UNIX_EPOCH};
8use zk402_core::*;
9
10pub mod settlement;
11pub use settlement::SettlementConfig;
12
13/// Static verifying key registry loaded from configuration.
14pub struct StaticRegistry<VK> {
15    keys: HashMap<(String, String), VK>,
16}
17
18impl<VK: Clone> StaticRegistry<VK> {
19    pub fn new() -> Self {
20        Self {
21            keys: HashMap::new(),
22        }
23    }
24
25    pub fn register(&mut self, proof_system: String, circuit_id: String, vk: VK) {
26        self.keys.insert((proof_system, circuit_id), vk);
27    }
28}
29
30impl<VK: Clone> Default for StaticRegistry<VK> {
31    fn default() -> Self {
32        Self::new()
33    }
34}
35
36impl<VK: Clone> VerifyingKeyRegistry for StaticRegistry<VK> {
37    type VerifyingKeyHandle = VK;
38
39    fn lookup(
40        &self,
41        proof_system: &str,
42        circuit_id: &str,
43    ) -> Option<Self::VerifyingKeyHandle> {
44        self.keys
45            .get(&(proof_system.to_string(), circuit_id.to_string()))
46            .cloned()
47    }
48}
49
50/// zk402 facilitator implementing the Scheme trait.
51pub struct ZkSettleFacilitator<PS: ProofSystem, VKR: VerifyingKeyRegistry> {
52    registry: VKR,
53    settlement_config: Option<SettlementConfig>,
54    _phantom: std::marker::PhantomData<PS>,
55}
56
57impl<PS: ProofSystem, VKR: VerifyingKeyRegistry<VerifyingKeyHandle = PS::VerifyingKey>>
58    ZkSettleFacilitator<PS, VKR>
59{
60    pub fn new(registry: VKR) -> Self {
61        Self {
62            registry,
63            settlement_config: None,
64            _phantom: std::marker::PhantomData,
65        }
66    }
67
68    pub fn with_settlement(mut self, config: SettlementConfig) -> Self {
69        self.settlement_config = Some(config);
70        self
71    }
72
73    /// Step 1: Structural match validation
74    fn validate_structural_match(
75        req: &PaymentRequirements,
76        payload: &PaymentPayload,
77    ) -> Result<(), SchemeError> {
78        let pi = &payload.public_inputs;
79
80        if pi.amount != req.amount {
81            return Err(SchemeError::StructuralMismatch(format!(
82                "amount mismatch: expected {}, got {}",
83                req.amount, pi.amount
84            )));
85        }
86
87        if pi.asset != req.asset {
88            return Err(SchemeError::StructuralMismatch(format!(
89                "asset mismatch: expected 0x{}, got 0x{}",
90                hex::encode(req.asset),
91                hex::encode(pi.asset)
92            )));
93        }
94
95        if pi.pay_to != req.pay_to {
96            return Err(SchemeError::StructuralMismatch(format!(
97                "payTo mismatch: expected 0x{}, got 0x{}",
98                hex::encode(req.pay_to),
99                hex::encode(pi.pay_to)
100            )));
101        }
102
103        if pi.network != req.network {
104            return Err(SchemeError::StructuralMismatch(format!(
105                "network mismatch: expected {}, got {}",
106                req.network, pi.network
107            )));
108        }
109
110        Ok(())
111    }
112
113    /// Step 2: Trust check - lookup verifying key
114    fn trust_check(&self, extra: &ZkExtra) -> Result<PS::VerifyingKey, SchemeError> {
115        self.registry
116            .lookup(&extra.proof_system, &extra.circuit_id)
117            .ok_or(SchemeError::UnsupportedCircuit)
118    }
119
120    /// Step 3: Cryptographic verification
121    fn cryptographic_verification(
122        vk: &PS::VerifyingKey,
123        payload: &PaymentPayload,
124    ) -> Result<(), SchemeError> {
125        PS::verify(vk, &payload.proof, &payload.public_inputs)
126            .map_err(|_| SchemeError::Internal("proof verification error".to_string()))?
127            .then_some(())
128            .ok_or(SchemeError::CryptographicVerificationFailed)
129    }
130
131    /// Step 4: Freshness check
132    fn freshness_check(public_inputs: &PublicInputs) -> Result<(), SchemeError> {
133        let now = SystemTime::now()
134            .duration_since(UNIX_EPOCH)
135            .map_err(|e| SchemeError::Internal(format!("system time error: {}", e)))?
136            .as_secs();
137
138        if now < public_inputs.valid_after {
139            return Err(SchemeError::Expired);
140        }
141
142        if now >= public_inputs.valid_before {
143            return Err(SchemeError::Expired);
144        }
145
146        Ok(())
147    }
148}
149
150#[async_trait::async_trait]
151impl<PS: ProofSystem + Send + Sync, VKR: VerifyingKeyRegistry<VerifyingKeyHandle = PS::VerifyingKey> + Send + Sync>
152    Scheme for ZkSettleFacilitator<PS, VKR>
153{
154    fn scheme_name(&self) -> &str {
155        "zk-settle"
156    }
157
158    async fn verify(
159        &self,
160        req: &PaymentRequirements,
161        payload: &PaymentPayload,
162    ) -> Result<VerifyResponse, SchemeError> {
163        // Step 1: Structural match
164        Self::validate_structural_match(req, payload)?;
165
166        // Step 2: Trust check
167        let vk = self.trust_check(&req.extra)?;
168
169        // Step 3: Cryptographic verification
170        Self::cryptographic_verification(&vk, payload)?;
171
172        // Step 4: Freshness
173        Self::freshness_check(&payload.public_inputs)?;
174
175        // Step 5: Simulation (optional for v1) - not implemented yet
176
177        Ok(VerifyResponse {
178            is_valid: true,
179            invalid_reason: None,
180        })
181    }
182
183    async fn settle(
184        &self,
185        _req: &PaymentRequirements,
186        payload: &PaymentPayload,
187    ) -> Result<SettleResponse, SchemeError> {
188        let config = self
189            .settlement_config
190            .as_ref()
191            .ok_or_else(|| SchemeError::Internal("settlement not configured".to_string()))?;
192
193        match settlement::submit_settlement(config, payload).await {
194            Ok(tx_hash) => Ok(SettleResponse {
195                success: true,
196                tx_hash: Some(tx_hash),
197                error: None,
198            }),
199            Err(e) => Ok(SettleResponse {
200                success: false,
201                tx_hash: None,
202                error: Some(e.to_string()),
203            }),
204        }
205    }
206}
207
208#[cfg(test)]
209mod tests {
210    use super::*;
211
212    struct MockProofSystem;
213
214    impl ProofSystem for MockProofSystem {
215        type ProvingKey = Vec<u8>;
216        type VerifyingKey = Vec<u8>;
217
218        fn prove(
219            _pk: &Self::ProvingKey,
220            _witness: &Witness,
221            _public_inputs: &PublicInputs,
222        ) -> Result<Vec<u8>, ProofSystemError> {
223            Ok(vec![0x42])
224        }
225
226        fn verify(
227            _vk: &Self::VerifyingKey,
228            proof: &[u8],
229            _public_inputs: &PublicInputs,
230        ) -> Result<bool, ProofSystemError> {
231            // Valid if proof is [0x42], invalid otherwise
232            Ok(proof == &[0x42])
233        }
234    }
235
236    fn test_requirements() -> PaymentRequirements {
237        PaymentRequirements {
238            scheme: "zk-settle".to_string(),
239            network: "eip155:8453".to_string(),
240            amount: "100".to_string(),
241            asset: [1u8; 20],
242            pay_to: [2u8; 20],
243            max_timeout_seconds: 3600,
244            extra: ZkExtra {
245                proof_system: "groth16".to_string(),
246                circuit_id: "zk-settle-transfer-v1".to_string(),
247                verifier_address: [3u8; 20],
248            },
249        }
250    }
251
252    fn test_payload(proof: Vec<u8>) -> PaymentPayload {
253        let req = test_requirements();
254        PaymentPayload {
255            x402_version: 1,
256            accepted: req.clone(),
257            proof,
258            public_inputs: PublicInputs {
259                amount: req.amount.clone(),
260                asset: req.asset,
261                pay_to: req.pay_to,
262                from: [4u8; 20],
263                network: req.network.clone(),
264                nonce: [5u8; 32],
265                valid_after: current_timestamp() - 60,
266                valid_before: current_timestamp() + 3600,
267                public_key: vec![6u8; 32],
268            },
269            proof_system_extra: None,
270        }
271    }
272
273    fn current_timestamp() -> u64 {
274        SystemTime::now()
275            .duration_since(UNIX_EPOCH)
276            .unwrap()
277            .as_secs()
278    }
279
280    #[tokio::test]
281    async fn valid_proof_passes_verification() {
282        let mut registry = StaticRegistry::new();
283        registry.register(
284            "groth16".to_string(),
285            "zk-settle-transfer-v1".to_string(),
286            vec![0u8],
287        );
288
289        let facilitator = ZkSettleFacilitator::<MockProofSystem, _>::new(registry);
290        let req = test_requirements();
291        let payload = test_payload(vec![0x42]);
292
293        let result = facilitator.verify(&req, &payload).await;
294        assert!(result.is_ok());
295        assert!(result.unwrap().is_valid);
296    }
297
298    #[tokio::test]
299    async fn tampered_amount_fails_structural_match() {
300        let mut registry = StaticRegistry::new();
301        registry.register(
302            "groth16".to_string(),
303            "zk-settle-transfer-v1".to_string(),
304            vec![0u8],
305        );
306
307        let facilitator = ZkSettleFacilitator::<MockProofSystem, _>::new(registry);
308        let req = test_requirements();
309        let mut payload = test_payload(vec![0x42]);
310        payload.public_inputs.amount = "101".to_string();
311
312        let result = facilitator.verify(&req, &payload).await;
313        assert!(matches!(result, Err(SchemeError::StructuralMismatch(_))));
314    }
315
316    #[tokio::test]
317    async fn unknown_circuit_id_fails_trust_check() {
318        let registry = StaticRegistry::<Vec<u8>>::new();
319        let facilitator = ZkSettleFacilitator::<MockProofSystem, _>::new(registry);
320        let req = test_requirements();
321        let payload = test_payload(vec![0x42]);
322
323        let result = facilitator.verify(&req, &payload).await;
324        assert!(matches!(result, Err(SchemeError::UnsupportedCircuit)));
325    }
326
327    #[tokio::test]
328    async fn invalid_proof_fails_crypto_verification() {
329        let mut registry = StaticRegistry::new();
330        registry.register(
331            "groth16".to_string(),
332            "zk-settle-transfer-v1".to_string(),
333            vec![0u8],
334        );
335
336        let facilitator = ZkSettleFacilitator::<MockProofSystem, _>::new(registry);
337        let req = test_requirements();
338        let payload = test_payload(vec![0x99]); // Invalid proof
339
340        let result = facilitator.verify(&req, &payload).await;
341        assert!(matches!(
342            result,
343            Err(SchemeError::CryptographicVerificationFailed)
344        ));
345    }
346
347    #[tokio::test]
348    async fn expired_window_fails_freshness() {
349        let mut registry = StaticRegistry::new();
350        registry.register(
351            "groth16".to_string(),
352            "zk-settle-transfer-v1".to_string(),
353            vec![0u8],
354        );
355
356        let facilitator = ZkSettleFacilitator::<MockProofSystem, _>::new(registry);
357        let req = test_requirements();
358        let mut payload = test_payload(vec![0x42]);
359        payload.public_inputs.valid_before = current_timestamp() - 10; // Already expired
360
361        let result = facilitator.verify(&req, &payload).await;
362        assert!(matches!(result, Err(SchemeError::Expired)));
363    }
364}