Skip to main content

world_id_primitives/
proof.rs

1use ruint::aliases::U256;
2use serde::{Deserialize, Deserializer, Serialize, Serializer, de::Error as _};
3
4use crate::FieldElement;
5
6/// Encoded World ID Proof.
7///
8/// Internally, the first 4 elements are a compressed Groth16 proof
9/// [a (G1), b (G2), b (G2), c (G1)]. Proofs also require the root hash of the Merkle tree
10/// in the `WorldIDRegistry` as a public input. To simplify transmission, that root is encoded as the last element
11/// with the proof.
12///
13/// The `WorldIDVerifier.sol` contract handles the decoding and verification.
14#[derive(Debug, Default, Clone, PartialEq, Eq)]
15pub struct ZeroKnowledgeProof {
16    /// Array of 5 U256 values: first 4 are compressed Groth16 proof, last is Merkle root.
17    inner: [U256; 5],
18}
19
20impl ZeroKnowledgeProof {
21    /// Outputs the proof as a Solidity-friendly representation.
22    #[must_use]
23    pub const fn as_ethereum_representation(&self) -> [U256; 5] {
24        self.inner
25    }
26
27    /// Initializes a proof from an encoded Solidity-friendly representation.
28    #[must_use]
29    pub const fn from_ethereum_representation(value: [U256; 5]) -> Self {
30        Self { inner: value }
31    }
32
33    /// Converts the proof to compressed bytes (160 bytes total: 5 × 32 bytes).
34    #[must_use]
35    pub fn to_compressed_bytes(&self) -> Vec<u8> {
36        self.inner
37            .iter()
38            .flat_map(U256::to_be_bytes::<32>)
39            .collect()
40    }
41
42    /// Constructs a proof from compressed bytes (must be exactly 160 bytes).
43    ///
44    /// # Errors
45    /// Returns an error if the input is not exactly 160 bytes or if bytes cannot be parsed.
46    pub fn from_compressed_bytes(bytes: &[u8]) -> Result<Self, String> {
47        if bytes.len() != 160 {
48            return Err(format!(
49                "Invalid length: expected 160 bytes, got {}",
50                bytes.len()
51            ));
52        }
53
54        let mut inner = [U256::ZERO; 5];
55        for (i, chunk) in bytes.chunks_exact(32).enumerate() {
56            let mut arr = [0u8; 32];
57            arr.copy_from_slice(chunk);
58            inner[i] = U256::from_be_bytes(arr);
59        }
60
61        Ok(Self { inner })
62    }
63}
64
65impl Serialize for ZeroKnowledgeProof {
66    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
67    where
68        S: Serializer,
69    {
70        let bytes = self.to_compressed_bytes();
71        if serializer.is_human_readable() {
72            serializer.serialize_str(&hex::encode(bytes))
73        } else {
74            serializer.serialize_bytes(&bytes)
75        }
76    }
77}
78
79impl<'de> Deserialize<'de> for ZeroKnowledgeProof {
80    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
81    where
82        D: Deserializer<'de>,
83    {
84        let bytes = if deserializer.is_human_readable() {
85            let hex_str = String::deserialize(deserializer)?;
86            hex::decode(hex_str).map_err(D::Error::custom)?
87        } else {
88            Vec::deserialize(deserializer)?
89        };
90
91        Self::from_compressed_bytes(&bytes).map_err(D::Error::custom)
92    }
93}
94
95impl From<ZeroKnowledgeProof> for [U256; 5] {
96    fn from(value: ZeroKnowledgeProof) -> Self {
97        value.inner
98    }
99}
100
101/// A WIP-103 Ownership Proof.
102///
103/// Contains the ZKP and the Merkle root public input that the verifier
104/// doesn't initially provide.
105#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
106pub struct OwnershipProof {
107    /// The WHIR R1CS proof from ProveKit.
108    pub proof: provekit_common::WhirR1CSProof,
109    /// Merkle tree root used for inclusion within the circuit.
110    pub merkle_root: FieldElement,
111}
112
113#[cfg(test)]
114mod tests {
115    use ruint::uint;
116
117    use super::*;
118
119    #[test]
120    fn test_encoding_round_trip() {
121        let proof = ZeroKnowledgeProof::default();
122        let compressed_bytes = proof.to_compressed_bytes();
123
124        assert_eq!(compressed_bytes.len(), 160);
125
126        let encoded = serde_json::to_string(&proof).unwrap();
127        assert_eq!(
128            encoded,
129            "\"00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000\""
130        );
131
132        let proof_from = ZeroKnowledgeProof::from_compressed_bytes(&compressed_bytes).unwrap();
133
134        assert_eq!(proof.inner, proof_from.inner);
135    }
136
137    #[test]
138    fn test_json_deserialization() {
139        let proof = ZeroKnowledgeProof::default();
140
141        // Test roundtrip serialization
142        let json_str = serde_json::to_string(&proof).unwrap();
143        let deserialized_proof: ZeroKnowledgeProof = serde_json::from_str(&json_str).unwrap();
144
145        // Verify the roundtrip preserved all values
146        assert_eq!(proof.inner, deserialized_proof.inner);
147    }
148
149    #[test]
150    fn test_from_ethereum_representation() {
151        let values = [
152            uint!(0x0000000000000000000000000000000000000000000000000000000000000001_U256),
153            uint!(0x0000000000000000000000000000000000000000000000000000000000000002_U256),
154            uint!(0x0000000000000000000000000000000000000000000000000000000000000003_U256),
155            uint!(0x0000000000000000000000000000000000000000000000000000000000000004_U256),
156            uint!(0x11d223ce7b91ac212f42cf50f0a3439ae3fcdba4ea32acb7f194d1051ed324c2_U256),
157        ];
158
159        let proof = ZeroKnowledgeProof::from_ethereum_representation(values);
160        assert_eq!(proof.as_ethereum_representation(), values);
161
162        // Test serialization roundtrip
163        let bytes = proof.to_compressed_bytes();
164        assert_eq!(bytes.len(), 160);
165
166        let proof_from_bytes = ZeroKnowledgeProof::from_compressed_bytes(&bytes).unwrap();
167        assert_eq!(proof.inner, proof_from_bytes.inner);
168    }
169
170    #[test]
171    fn test_invalid_bytes_length() {
172        let too_short = vec![0u8; 159];
173        let result = ZeroKnowledgeProof::from_compressed_bytes(&too_short);
174        assert!(result.is_err());
175        assert!(result.unwrap_err().contains("Invalid length"));
176
177        let too_long = vec![0u8; 161];
178        let result = ZeroKnowledgeProof::from_compressed_bytes(&too_long);
179        assert!(result.is_err());
180        assert!(result.unwrap_err().contains("Invalid length"));
181    }
182
183    #[test]
184    fn test_ownership_proof_json_roundtrip() {
185        let whir_proof = provekit_common::WhirR1CSProof {
186            narg_string: vec![1, 2, 3],
187            hints: vec![4, 5],
188            #[cfg(debug_assertions)]
189            pattern: vec![],
190        };
191        let proof = OwnershipProof {
192            proof: whir_proof,
193            merkle_root: crate::FieldElement::from(999u64),
194        };
195        let json = serde_json::to_string(&proof).unwrap();
196        assert!(json.contains("proof"));
197        assert!(json.contains("merkle_root"));
198        let decoded: OwnershipProof = serde_json::from_str(&json).unwrap();
199        assert_eq!(proof, decoded);
200    }
201
202    /// A proof with a wrong (flipped) merkle root must not equal the original.
203    ///
204    /// This test simulates the data-level check that a verifier would perform:
205    /// if the merkle root stored inside an [`OwnershipProof`] has been tampered
206    /// with, the proof object is distinguishable from the original.
207    #[test]
208    fn test_ownership_proof_wrong_merkle_root_is_detected() {
209        let whir_proof = provekit_common::WhirR1CSProof {
210            narg_string: vec![10, 20, 30],
211            hints: vec![],
212            #[cfg(debug_assertions)]
213            pattern: vec![],
214        };
215        let proof = OwnershipProof {
216            proof: whir_proof.clone(),
217            merkle_root: crate::FieldElement::from(12345u64),
218        };
219
220        // Construct a proof with a different merkle root (simulating tampering).
221        let tampered = OwnershipProof {
222            proof: whir_proof,
223            merkle_root: crate::FieldElement::from(99999u64),
224        };
225
226        assert_ne!(
227            proof.merkle_root, tampered.merkle_root,
228            "a flipped merkle root must differ from the original"
229        );
230        assert_ne!(proof, tampered);
231    }
232
233    /// A proof with tampered proof bytes must not equal the original.
234    ///
235    /// This test simulates the data-level check that a verifier would perform:
236    /// if the proof bytes inside an [`OwnershipProof`] have been tampered with,
237    /// the proof object is distinguishable from the original.
238    #[test]
239    fn test_ownership_proof_tampered_bytes_is_detected() {
240        let original_bytes = vec![0xde, 0xad, 0xbe, 0xef];
241        let proof = OwnershipProof {
242            proof: provekit_common::WhirR1CSProof {
243                narg_string: original_bytes.clone(),
244                hints: vec![],
245                #[cfg(debug_assertions)]
246                pattern: vec![],
247            },
248            merkle_root: crate::FieldElement::from(42u64),
249        };
250
251        // Flip the first byte to simulate proof tampering.
252        let mut tampered_bytes = original_bytes;
253        tampered_bytes[0] ^= 0xFF;
254
255        let tampered = OwnershipProof {
256            proof: provekit_common::WhirR1CSProof {
257                narg_string: tampered_bytes,
258                hints: vec![],
259                #[cfg(debug_assertions)]
260                pattern: vec![],
261            },
262            merkle_root: crate::FieldElement::from(42u64),
263        };
264
265        assert_ne!(
266            proof.proof.narg_string, tampered.proof.narg_string,
267            "tampered proof bytes must differ from the original"
268        );
269        assert_ne!(proof, tampered);
270    }
271}