Skip to main content

zescrow_core/
condition.rs

1//! Deterministic cryptographic conditions and fulfillment verification.
2//!
3//! This module defines the [`Condition`] enum and its variants for
4//! verifying cryptographic proofs within the zkVM:
5//!
6//! - **Hashlock**: SHA-256 preimage verification
7//! - **Ed25519**: EdDSA signature verification
8//! - **Secp256k1**: ECDSA signature verification
9//! - **Threshold**: N-of-M multi-condition logic
10
11use bincode::{Decode, Encode};
12#[cfg(feature = "json")]
13use serde::{Deserialize, Serialize};
14use sha2::{Digest, Sha256};
15
16use crate::Result;
17use crate::error::ConditionError;
18
19/// Ed25519 signature over an arbitrary message.
20pub mod ed25519;
21/// XRPL-style hashlock: SHA-256(preimage) == hash.
22pub mod hashlock;
23/// Secp256k1 ECDSA signature over an arbitrary message.
24pub mod secp256k1;
25/// Threshold condition: at least `threshold` subconditions must hold.
26pub mod threshold;
27
28use ed25519::Ed25519;
29use hashlock::Hashlock;
30use secp256k1::Secp256k1;
31use threshold::Threshold;
32
33/// A cryptographic condition that can be deterministically verified.
34#[cfg_attr(feature = "json", derive(Serialize, Deserialize))]
35#[cfg_attr(
36    feature = "json",
37    serde(tag = "condition", content = "fulfillment", rename_all = "lowercase")
38)]
39#[derive(Debug, Clone, Encode, Decode, PartialEq, Eq)]
40pub enum Condition {
41    /// XRPL-style hashlock: SHA-256(preimage) == hash.
42    Hashlock(Hashlock),
43    /// Ed25519 signature over an arbitrary message.
44    Ed25519(Ed25519),
45    /// Secp256k1 ECDSA signature over an arbitrary message.
46    Secp256k1(Secp256k1),
47    /// Threshold condition: at least `threshold` subconditions must hold.
48    Threshold(Threshold),
49}
50
51impl Condition {
52    /// Validates the provided witness data against this cryptographic condition.
53    ///
54    /// # Errors
55    ///
56    /// Returns `EscrowError::Condition` under any of the following circumstances:
57    /// - **Hashlock**: `SHA-256(preimage)` does not match the expected hash.
58    /// - **Ed25519**: Public key parsing or signature verification fails.
59    /// - **Secp256k1**: Public key parsing or signature verification fails.
60    /// - **Threshold**: Fewer than `threshold` subconditions were satisfied.
61    #[inline]
62    pub fn verify(&self) -> Result<()> {
63        match self {
64            Self::Hashlock(hashlock) => hashlock.verify().map_err(ConditionError::Hashlock)?,
65            Self::Ed25519(ed25519) => ed25519.verify().map_err(ConditionError::Ed25519)?,
66            Self::Secp256k1(secp256k1) => secp256k1.verify().map_err(ConditionError::Secp256k1)?,
67            Self::Threshold(threshold) => threshold.verify().map_err(ConditionError::Threshold)?,
68        }
69        Ok(())
70    }
71
72    /// Construct a hashlock (preimage) condition.
73    pub fn hashlock(hash: [u8; 32], preimage: Vec<u8>) -> Self {
74        Self::Hashlock(Hashlock { hash, preimage })
75    }
76
77    /// Construct an Ed25519 signature condition.
78    pub fn ed25519(public_key: [u8; 32], message: Vec<u8>, signature: Vec<u8>) -> Self {
79        Self::Ed25519(Ed25519 {
80            public_key,
81            signature,
82            message,
83        })
84    }
85
86    /// Construct a Secp256k1 signature condition.
87    pub fn secp256k1(public_key: Vec<u8>, message: Vec<u8>, signature: Vec<u8>) -> Self {
88        Self::Secp256k1(Secp256k1 {
89            public_key,
90            signature,
91            message,
92        })
93    }
94
95    /// Construct a threshold condition.
96    pub fn threshold(threshold: usize, subconditions: Vec<Self>) -> Self {
97        Self::Threshold(Threshold {
98            threshold,
99            subconditions,
100        })
101    }
102
103    /// A 32-byte SHA-256 commitment binding to this condition's public
104    /// parameters, excluding the secret witness.
105    pub fn commitment(&self) -> [u8; 32] {
106        let mut hasher = Sha256::new();
107        match self {
108            Self::Hashlock(hashlock) => {
109                hasher.update(b"ZESCROW_COND_HASHLOCK_V1");
110                hasher.update(hashlock.hash);
111            }
112            Self::Ed25519(ed25519) => {
113                hasher.update(b"ZESCROW_COND_ED25519_V1");
114                hasher.update(ed25519.public_key);
115                hasher.update(&ed25519.message);
116            }
117            Self::Secp256k1(secp256k1) => {
118                hasher.update(b"ZESCROW_COND_SECP256K1_V1");
119                hasher.update(&secp256k1.public_key);
120                hasher.update(&secp256k1.message);
121            }
122            Self::Threshold(threshold) => {
123                hasher.update(b"ZESCROW_COND_THRESHOLD_V1");
124                hasher.update(saturating_u32(threshold.threshold).to_be_bytes());
125                hasher.update(saturating_u32(threshold.subconditions.len()).to_be_bytes());
126                threshold
127                    .subconditions
128                    .iter()
129                    .for_each(|sub| hasher.update(sub.commitment()));
130            }
131        }
132        hasher.finalize().into()
133    }
134}
135
136/// Normalizes a `usize` count to a fixed-width `u32` for cross-platform
137/// (32-bit guest vs 64-bit host) commitment determinism. Counts this large are
138/// unreachable for valid conditions; saturating keeps the function total.
139fn saturating_u32(value: usize) -> u32 {
140    u32::try_from(value).unwrap_or(u32::MAX)
141}
142
143#[cfg(feature = "json")]
144impl std::fmt::Display for Condition {
145    /// Serialize the condition to compact JSON for logging or write formats.
146    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
147        let json = serde_json::to_string(self).map_err(|_| std::fmt::Error)?;
148        write!(f, "{json}")
149    }
150}
151
152#[cfg(test)]
153mod tests {
154
155    use sha2::{Digest, Sha256};
156
157    use super::*;
158
159    #[test]
160    fn preimage() {
161        let preimage = b"secret".to_vec();
162        let hash = Sha256::digest(&preimage).into();
163        let cond = Condition::hashlock(hash, preimage);
164        assert!(cond.verify().is_ok());
165
166        // invalid preimage
167        let cond = Condition::hashlock(hash, b"wrong-secret".to_vec());
168        assert!(cond.verify().is_err());
169    }
170
171    #[test]
172    fn ed25519() {
173        use ed25519_dalek::ed25519::signature::rand_core::OsRng;
174        use ed25519_dalek::{Signer, SigningKey};
175
176        let mut csprng = OsRng;
177        let sk: SigningKey = SigningKey::generate(&mut csprng);
178
179        let message = b"zkEscrow".to_vec();
180        let signature = sk.sign(&message).to_bytes().to_vec();
181        let public_key = sk.verifying_key().to_bytes();
182        let cond = Condition::ed25519(public_key, message.clone(), signature.clone());
183        assert!(cond.verify().is_ok());
184
185        // tampered sig
186        let mut signature = signature;
187        signature[0] ^= 0xFF;
188        let cond = Condition::ed25519(public_key, message, signature);
189        assert!(cond.verify().is_err());
190    }
191
192    #[test]
193    fn secp256k1() {
194        use k256::ecdsa::signature::Signer;
195        use k256::ecdsa::{Signature, SigningKey};
196        use k256::elliptic_curve::rand_core::OsRng;
197
198        let sk = SigningKey::random(&mut OsRng);
199        let vk = sk.verifying_key();
200        let message = b"zkEscrow".to_vec();
201        let signature: Signature = sk.sign(&message);
202
203        let sig_bytes = signature.to_der().as_bytes().to_vec();
204        let pk_bytes = vk.to_encoded_point(false).as_bytes().to_vec();
205
206        let cond = Condition::secp256k1(pk_bytes.clone(), message, sig_bytes.clone());
207        assert!(cond.verify().is_ok());
208
209        // tampered message
210        let cond = Condition::secp256k1(pk_bytes, b"tampered".to_vec(), sig_bytes);
211        assert!(cond.verify().is_err());
212    }
213
214    #[test]
215    fn nonzero_threshold() {
216        // two trivial subconditions: one succeeds, one fails
217        let hash = Sha256::digest(b"zkEscrow").into();
218        let correct = Condition::hashlock(hash, b"zkEscrow".to_vec());
219        let wrong = Condition::hashlock(hash, b"wrong-preimage".to_vec());
220
221        // threshold == 1 should pass
222        let cond = Condition::threshold(1, vec![correct.clone(), wrong.clone()]);
223        assert!(cond.verify().is_ok());
224
225        // threshold == 2 should fail
226        let cond = Condition::threshold(2, vec![correct, wrong]);
227        assert!(cond.verify().is_err());
228
229        // threshold == 1 and no subconditions should fail
230        let cond = Condition::threshold(1, vec![]);
231        assert!(cond.verify().is_err());
232    }
233
234    #[test]
235    fn zero_threshold_rejected() {
236        // threshold == 0 is malformed and must be rejected, not trivially satisfied
237        let cond = Condition::threshold(0, vec![]);
238        assert!(cond.verify().is_err());
239
240        let preimage = b"zkEscrow".to_vec();
241        let hash = Sha256::digest(&preimage).into();
242        let subcond = Condition::hashlock(hash, preimage);
243        let cond = Condition::threshold(0, vec![subcond]);
244        assert!(cond.verify().is_err());
245    }
246
247    #[test]
248    fn threshold_exceeding_subconditions_rejected() {
249        let preimage = b"zkEscrow".to_vec();
250        let hash = Sha256::digest(&preimage).into();
251        let subcond = Condition::hashlock(hash, preimage);
252        // threshold of 2 over a single subcondition can never be met
253        let cond = Condition::threshold(2, vec![subcond]);
254        assert!(cond.verify().is_err());
255    }
256
257    #[test]
258    fn nested_thresholds() {
259        let preimage = b"zkEscrow".to_vec();
260        let hash = Sha256::digest(&preimage).into();
261        let leaf = Condition::hashlock(hash, preimage);
262
263        // inner threshold: need 1 of `leaf`
264        let inner = Condition::threshold(1, vec![leaf.clone()]);
265        // outer threshold: need 1 of `inner`
266        let outer = Condition::threshold(1, vec![inner]);
267        assert!(outer.verify().is_ok());
268
269        // if `leaf` wrong, `inner` fails, and so does `outer`
270        let wrong_leaf = Condition::hashlock(hash, b"wrong-preimage".to_vec());
271        let inner2 = Condition::threshold(1, vec![wrong_leaf]);
272        let outer2 = Condition::threshold(1, vec![inner2]);
273        assert!(outer2.verify().is_err());
274    }
275
276    #[cfg(feature = "json")]
277    #[test]
278    fn json_roundtrip_hashlock() {
279        let preimage = b"secret".to_vec();
280        let hash = Sha256::digest(&preimage).into();
281        let cond = Condition::hashlock(hash, preimage);
282        let json = serde_json::to_string(&cond).unwrap();
283        let decoded: Condition = serde_json::from_str(&json).unwrap();
284        assert_eq!(decoded, cond);
285    }
286
287    #[cfg(feature = "json")]
288    #[test]
289    fn json_roundtrip_threshold() {
290        let preimage = b"nested".to_vec();
291        let hash = Sha256::digest(&preimage).into();
292        let inner = Condition::hashlock(hash, preimage);
293        let cond = Condition::threshold(1, vec![inner]);
294        let json = serde_json::to_string(&cond).unwrap();
295        let decoded: Condition = serde_json::from_str(&json).unwrap();
296        assert_eq!(decoded, cond);
297    }
298}