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};
14
15use crate::error::ConditionError;
16use crate::Result;
17
18/// Ed25519 signature over an arbitrary message.
19pub mod ed25519;
20/// XRPL-style hashlock: SHA-256(preimage) == hash.
21pub mod hashlock;
22/// Secp256k1 ECDSA signature over an arbitrary message.
23pub mod secp256k1;
24/// Threshold condition: at least `threshold` subconditions must hold.
25pub mod threshold;
26
27use ed25519::Ed25519;
28use hashlock::Hashlock;
29use secp256k1::Secp256k1;
30use threshold::Threshold;
31
32/// A cryptographic condition that can be deterministically verified.
33#[cfg_attr(feature = "json", derive(Serialize, Deserialize))]
34#[cfg_attr(
35    feature = "json",
36    serde(tag = "condition", content = "fulfillment", rename_all = "lowercase")
37)]
38#[derive(Debug, Clone, Encode, Decode, PartialEq, Eq)]
39pub enum Condition {
40    /// XRPL-style hashlock: SHA-256(preimage) == hash.
41    Hashlock(Hashlock),
42    /// Ed25519 signature over an arbitrary message.
43    Ed25519(Ed25519),
44    /// Secp256k1 ECDSA signature over an arbitrary message.
45    Secp256k1(Secp256k1),
46    /// Threshold condition: at least `threshold` subconditions must hold.
47    Threshold(Threshold),
48}
49
50impl Condition {
51    /// Validates the provided witness data against this cryptographic condition.
52    ///
53    /// # Errors
54    ///
55    /// Returns `EscrowError::Condition` under any of the following circumstances:
56    /// - **Hashlock**: `SHA-256(preimage)` does not match the expected hash.
57    /// - **Ed25519**: Public key parsing or signature verification fails.
58    /// - **Secp256k1**: Public key parsing or signature verification fails.
59    /// - **Threshold**: Fewer than `threshold` subconditions were satisfied.
60    #[inline]
61    pub fn verify(&self) -> Result<()> {
62        match self {
63            Self::Hashlock(hashlock) => hashlock.verify().map_err(ConditionError::Hashlock)?,
64            Self::Ed25519(ed25519) => ed25519.verify().map_err(ConditionError::Ed25519)?,
65            Self::Secp256k1(secp256k1) => secp256k1.verify().map_err(ConditionError::Secp256k1)?,
66            Self::Threshold(threshold) => threshold.verify().map_err(ConditionError::Threshold)?,
67        }
68        Ok(())
69    }
70
71    /// Construct a hashlock (preimage) condition.
72    pub fn hashlock(hash: [u8; 32], preimage: Vec<u8>) -> Self {
73        Self::Hashlock(Hashlock { hash, preimage })
74    }
75
76    /// Construct an Ed25519 signature condition.
77    pub fn ed25519(public_key: [u8; 32], message: Vec<u8>, signature: Vec<u8>) -> Self {
78        Self::Ed25519(Ed25519 {
79            public_key,
80            signature,
81            message,
82        })
83    }
84
85    /// Construct a Secp256k1 signature condition.
86    pub fn secp256k1(public_key: Vec<u8>, message: Vec<u8>, signature: Vec<u8>) -> Self {
87        Self::Secp256k1(Secp256k1 {
88            public_key,
89            signature,
90            message,
91        })
92    }
93
94    /// Construct a threshold condition.
95    pub fn threshold(threshold: usize, subconditions: Vec<Self>) -> Self {
96        Self::Threshold(Threshold {
97            threshold,
98            subconditions,
99        })
100    }
101}
102
103#[cfg(feature = "json")]
104impl std::fmt::Display for Condition {
105    /// Serialize the condition to compact JSON for logging or write formats.
106    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
107        let json = serde_json::to_string(self).map_err(|_| std::fmt::Error)?;
108        write!(f, "{json}")
109    }
110}
111
112#[cfg(test)]
113mod tests {
114
115    use sha2::{Digest, Sha256};
116
117    use super::*;
118
119    #[test]
120    fn preimage() {
121        let preimage = b"secret".to_vec();
122        let hash = Sha256::digest(&preimage).into();
123        let cond = Condition::hashlock(hash, preimage);
124        assert!(cond.verify().is_ok());
125
126        // invalid preimage
127        let cond = Condition::hashlock(hash, b"wrong-secret".to_vec());
128        assert!(cond.verify().is_err());
129    }
130
131    #[test]
132    fn ed25519() {
133        use ed25519_dalek::ed25519::signature::rand_core::OsRng;
134        use ed25519_dalek::{Signer, SigningKey};
135
136        let mut csprng = OsRng;
137        let sk: SigningKey = SigningKey::generate(&mut csprng);
138
139        let message = b"zkEscrow".to_vec();
140        let signature = sk.sign(&message).to_bytes().to_vec();
141        let public_key = sk.verifying_key().to_bytes();
142        let cond = Condition::ed25519(public_key, message.clone(), signature.clone());
143        assert!(cond.verify().is_ok());
144
145        // tampered sig
146        let mut signature = signature;
147        signature[0] ^= 0xFF;
148        let cond = Condition::ed25519(public_key, message, signature);
149        assert!(cond.verify().is_err());
150    }
151
152    #[test]
153    fn secp256k1() {
154        use k256::ecdsa::signature::Signer;
155        use k256::ecdsa::{Signature, SigningKey};
156        use k256::elliptic_curve::rand_core::OsRng;
157
158        let sk = SigningKey::random(&mut OsRng);
159        let vk = sk.verifying_key();
160        let message = b"zkEscrow".to_vec();
161        let signature: Signature = sk.sign(&message);
162
163        let sig_bytes = signature.to_der().as_bytes().to_vec();
164        let pk_bytes = vk.to_encoded_point(false).as_bytes().to_vec();
165
166        let cond = Condition::secp256k1(pk_bytes.clone(), message, sig_bytes.clone());
167        assert!(cond.verify().is_ok());
168
169        // tampered message
170        let cond = Condition::secp256k1(pk_bytes, b"tampered".to_vec(), sig_bytes);
171        assert!(cond.verify().is_err());
172    }
173
174    #[test]
175    fn nonzero_threshold() {
176        // two trivial subconditions: one succeeds, one fails
177        let hash = Sha256::digest(b"zkEscrow").into();
178        let correct = Condition::hashlock(hash, b"zkEscrow".to_vec());
179        let wrong = Condition::hashlock(hash, b"wrong-preimage".to_vec());
180
181        // threshold == 1 should pass
182        let cond = Condition::threshold(1, vec![correct.clone(), wrong.clone()]);
183        assert!(cond.verify().is_ok());
184
185        // threshold == 2 should fail
186        let cond = Condition::threshold(2, vec![correct, wrong]);
187        assert!(cond.verify().is_err());
188
189        // threshold == 1 and no subconditions should fail
190        let cond = Condition::threshold(1, vec![]);
191        assert!(cond.verify().is_err());
192    }
193
194    #[test]
195    fn zero_threshold() {
196        // threshold == 0 with empty subconditions should pass
197        let cond = Condition::threshold(0, vec![]);
198        assert!(cond.verify().is_ok());
199
200        // threshold == 0 with subconditions should also pass
201        let preimage = b"zkEscrow".to_vec();
202        let hash = Sha256::digest(&preimage).into();
203        let subcond = Condition::hashlock(hash, preimage);
204        let cond = Condition::threshold(0, vec![subcond]);
205        assert!(cond.verify().is_ok());
206    }
207
208    #[test]
209    fn nested_thresholds() {
210        let preimage = b"zkEscrow".to_vec();
211        let hash = Sha256::digest(&preimage).into();
212        let leaf = Condition::hashlock(hash, preimage);
213
214        // inner threshold: need 1 of `leaf`
215        let inner = Condition::threshold(1, vec![leaf.clone()]);
216        // outer threshold: need 1 of `inner`
217        let outer = Condition::threshold(1, vec![inner]);
218        assert!(outer.verify().is_ok());
219
220        // if `leaf` wrong, `inner` fails, and so does `outer`
221        let wrong_leaf = Condition::hashlock(hash, b"wrong-preimage".to_vec());
222        let inner2 = Condition::threshold(1, vec![wrong_leaf]);
223        let outer2 = Condition::threshold(1, vec![inner2]);
224        assert!(outer2.verify().is_err());
225    }
226
227    #[cfg(feature = "json")]
228    #[test]
229    fn json_roundtrip_hashlock() {
230        let preimage = b"secret".to_vec();
231        let hash = Sha256::digest(&preimage).into();
232        let cond = Condition::hashlock(hash, preimage);
233        let json = serde_json::to_string(&cond).unwrap();
234        let decoded: Condition = serde_json::from_str(&json).unwrap();
235        assert_eq!(decoded, cond);
236    }
237
238    #[cfg(feature = "json")]
239    #[test]
240    fn json_roundtrip_threshold() {
241        let preimage = b"nested".to_vec();
242        let hash = Sha256::digest(&preimage).into();
243        let inner = Condition::hashlock(hash, preimage);
244        let cond = Condition::threshold(1, vec![inner]);
245        let json = serde_json::to_string(&cond).unwrap();
246        let decoded: Condition = serde_json::from_str(&json).unwrap();
247        assert_eq!(decoded, cond);
248    }
249}