Skip to main content

shape_runtime/
execution_proof.rs

1//! Proof of execution for content-addressed functions.
2//!
3//! An execution proof attests that a specific function (identified by content hash)
4//! was executed with specific arguments and produced a specific result.
5
6use serde::{Deserialize, Serialize};
7use sha2::{Digest, Sha256};
8use std::collections::HashMap;
9use std::time::{SystemTime, UNIX_EPOCH};
10
11/// Compute SHA-256 of arbitrary bytes, returning a 32-byte array.
12pub fn hash_bytes(data: &[u8]) -> [u8; 32] {
13    let digest = Sha256::digest(data);
14    let mut out = [0u8; 32];
15    out.copy_from_slice(&digest);
16    out
17}
18
19/// A cryptographic proof that a function was executed with given inputs producing given outputs.
20#[derive(Debug, Clone, Serialize, Deserialize)]
21pub struct ExecutionProof {
22    /// Content hash of the function that was executed.
23    pub function_hash: [u8; 32],
24    /// SHA-256 hash of the serialized arguments.
25    pub args_hash: [u8; 32],
26    /// SHA-256 hash of the serialized result.
27    pub result_hash: [u8; 32],
28    /// Unix timestamp (seconds) when execution completed.
29    pub timestamp: u64,
30    /// Optional execution trace: hashes of intermediate states.
31    pub trace: Option<Vec<[u8; 32]>>,
32    /// Hash of this entire proof (excluding this field).
33    pub proof_hash: [u8; 32],
34}
35
36impl ExecutionProof {
37    /// Compute a deterministic hash over all proof fields except `proof_hash` itself.
38    pub fn compute_proof_hash(
39        function_hash: &[u8; 32],
40        args_hash: &[u8; 32],
41        result_hash: &[u8; 32],
42        timestamp: u64,
43        trace: &Option<Vec<[u8; 32]>>,
44    ) -> [u8; 32] {
45        let mut hasher = Sha256::new();
46        hasher.update(function_hash);
47        hasher.update(args_hash);
48        hasher.update(result_hash);
49        hasher.update(timestamp.to_le_bytes());
50        if let Some(entries) = trace {
51            // Prefix with entry count for unambiguous encoding.
52            hasher.update((entries.len() as u64).to_le_bytes());
53            for entry in entries {
54                hasher.update(entry);
55            }
56        } else {
57            // Distinguish None from empty vec.
58            hasher.update([0xff; 8]);
59        }
60        let digest = hasher.finalize();
61        let mut out = [0u8; 32];
62        out.copy_from_slice(&digest);
63        out
64    }
65
66    /// Verify that the proof's `proof_hash` matches its contents.
67    pub fn verify_integrity(&self) -> bool {
68        let expected = Self::compute_proof_hash(
69            &self.function_hash,
70            &self.args_hash,
71            &self.result_hash,
72            self.timestamp,
73            &self.trace,
74        );
75        expected == self.proof_hash
76    }
77}
78
79/// Builder for constructing execution proofs during function execution.
80pub struct ExecutionProofBuilder {
81    function_hash: [u8; 32],
82    args_hash: Option<[u8; 32]>,
83    trace: Vec<[u8; 32]>,
84    capture_trace: bool,
85}
86
87impl ExecutionProofBuilder {
88    /// Create a new builder for the given function content hash.
89    pub fn new(function_hash: [u8; 32]) -> Self {
90        Self {
91            function_hash,
92            args_hash: None,
93            trace: Vec::new(),
94            capture_trace: false,
95        }
96    }
97
98    /// Enable trace capture. When enabled, `record_trace_step` appends entries.
99    pub fn with_trace(mut self) -> Self {
100        self.capture_trace = true;
101        self
102    }
103
104    /// Record the hash of the serialized arguments.
105    pub fn set_args_hash(&mut self, hash: [u8; 32]) {
106        self.args_hash = Some(hash);
107    }
108
109    /// Record an intermediate state hash in the execution trace.
110    ///
111    /// This is a no-op if trace capture was not enabled via `with_trace`.
112    pub fn record_trace_step(&mut self, state_hash: [u8; 32]) {
113        if self.capture_trace {
114            self.trace.push(state_hash);
115        }
116    }
117
118    /// Finalize the proof with the result hash. Computes the proof hash and
119    /// returns the completed `ExecutionProof`.
120    pub fn finalize(self, result_hash: [u8; 32]) -> ExecutionProof {
121        let args_hash = self.args_hash.unwrap_or([0u8; 32]);
122        let timestamp = SystemTime::now()
123            .duration_since(UNIX_EPOCH)
124            .map(|d| d.as_secs())
125            .unwrap_or(0);
126        let trace = if self.capture_trace {
127            Some(self.trace)
128        } else {
129            None
130        };
131        let proof_hash = ExecutionProof::compute_proof_hash(
132            &self.function_hash,
133            &args_hash,
134            &result_hash,
135            timestamp,
136            &trace,
137        );
138        ExecutionProof {
139            function_hash: self.function_hash,
140            args_hash,
141            result_hash,
142            timestamp,
143            trace,
144            proof_hash,
145        }
146    }
147}
148
149/// Result of verifying an execution proof.
150#[derive(Debug, Clone, PartialEq, Eq)]
151pub enum VerificationResult {
152    /// Proof is internally consistent.
153    Valid,
154    /// Proof hash doesn't match computed hash.
155    InvalidProofHash,
156    /// Re-execution produced a different result hash.
157    ResultMismatch {
158        expected: [u8; 32],
159        actual: [u8; 32],
160    },
161    /// Trace verification failed at a specific step.
162    TraceMismatch { step: usize },
163}
164
165/// Registry of execution proofs, indexed by function hash.
166pub struct ProofRegistry {
167    proofs: Vec<ExecutionProof>,
168    by_function: HashMap<[u8; 32], Vec<usize>>,
169}
170
171impl ProofRegistry {
172    /// Create an empty registry.
173    pub fn new() -> Self {
174        Self {
175            proofs: Vec::new(),
176            by_function: HashMap::new(),
177        }
178    }
179
180    /// Register a proof. Returns the index of the newly registered proof.
181    pub fn register(&mut self, proof: ExecutionProof) -> usize {
182        let idx = self.proofs.len();
183        self.by_function
184            .entry(proof.function_hash)
185            .or_default()
186            .push(idx);
187        self.proofs.push(proof);
188        idx
189    }
190
191    /// Look up all proofs for a given function content hash.
192    pub fn lookup(&self, function_hash: &[u8; 32]) -> &[ExecutionProof] {
193        match self.by_function.get(function_hash) {
194            Some(indices) => {
195                // Return a contiguous slice when proofs were registered
196                // sequentially for this function; otherwise collect references.
197                // Since we always append, we can return the subset via indices.
198                // For simplicity, we return a slice of the full vec when there
199                // is exactly one contiguous run. For the general case, callers
200                // should use `lookup_indices`.
201                if indices.is_empty() {
202                    return &[];
203                }
204                let first = indices[0];
205                let last = *indices.last().unwrap();
206                // Check if the indices form a contiguous range in proofs vec.
207                if last - first + 1 == indices.len() {
208                    &self.proofs[first..=last]
209                } else {
210                    // Fallback: return empty. Use lookup_iter for non-contiguous.
211                    &[]
212                }
213            }
214            None => &[],
215        }
216    }
217
218    /// Iterate over all proofs for a given function hash.
219    pub fn lookup_iter<'a>(
220        &'a self,
221        function_hash: &[u8; 32],
222    ) -> impl Iterator<Item = &'a ExecutionProof> {
223        let indices = self.by_function.get(function_hash);
224        indices
225            .into_iter()
226            .flat_map(|v| v.iter())
227            .map(move |&idx| &self.proofs[idx])
228    }
229
230    /// Verify integrity of all registered proofs.
231    ///
232    /// Returns a list of `(proof_index, VerificationResult)` for every proof
233    /// that fails integrity verification.
234    pub fn verify_all(&self) -> Vec<(usize, VerificationResult)> {
235        let mut failures = Vec::new();
236        for (i, proof) in self.proofs.iter().enumerate() {
237            if !proof.verify_integrity() {
238                failures.push((i, VerificationResult::InvalidProofHash));
239            }
240        }
241        failures
242    }
243
244    /// Total number of registered proofs.
245    pub fn len(&self) -> usize {
246        self.proofs.len()
247    }
248
249    /// Whether the registry is empty.
250    pub fn is_empty(&self) -> bool {
251        self.proofs.is_empty()
252    }
253}
254
255impl Default for ProofRegistry {
256    fn default() -> Self {
257        Self::new()
258    }
259}
260
261#[cfg(test)]
262mod tests {
263    use super::*;
264
265    #[test]
266    fn test_hash_bytes() {
267        let h = hash_bytes(b"hello");
268        // SHA-256 of "hello" is well-known.
269        assert_eq!(
270            hex::encode(h),
271            "2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824"
272        );
273    }
274
275    #[test]
276    fn test_proof_integrity_valid() {
277        let func_hash = hash_bytes(b"my_function");
278        let args_hash = hash_bytes(b"args");
279        let result_hash = hash_bytes(b"result");
280
281        let mut builder = ExecutionProofBuilder::new(func_hash);
282        builder.set_args_hash(args_hash);
283        let proof = builder.finalize(result_hash);
284
285        assert!(proof.verify_integrity());
286    }
287
288    #[test]
289    fn test_proof_integrity_tampered() {
290        let func_hash = hash_bytes(b"my_function");
291        let args_hash = hash_bytes(b"args");
292        let result_hash = hash_bytes(b"result");
293
294        let mut builder = ExecutionProofBuilder::new(func_hash);
295        builder.set_args_hash(args_hash);
296        let mut proof = builder.finalize(result_hash);
297
298        // Tamper with the result hash.
299        proof.result_hash = hash_bytes(b"different_result");
300        assert!(!proof.verify_integrity());
301    }
302
303    #[test]
304    fn test_proof_with_trace() {
305        let func_hash = hash_bytes(b"traced_fn");
306        let args_hash = hash_bytes(b"args");
307
308        let mut builder = ExecutionProofBuilder::new(func_hash).with_trace();
309        builder.set_args_hash(args_hash);
310        builder.record_trace_step(hash_bytes(b"state_1"));
311        builder.record_trace_step(hash_bytes(b"state_2"));
312
313        let result_hash = hash_bytes(b"final");
314        let proof = builder.finalize(result_hash);
315
316        assert!(proof.verify_integrity());
317        assert!(proof.trace.is_some());
318        assert_eq!(proof.trace.as_ref().unwrap().len(), 2);
319    }
320
321    #[test]
322    fn test_trace_disabled_by_default() {
323        let mut builder = ExecutionProofBuilder::new([0u8; 32]);
324        builder.record_trace_step(hash_bytes(b"ignored"));
325        let proof = builder.finalize([1u8; 32]);
326
327        assert!(proof.trace.is_none());
328    }
329
330    #[test]
331    fn test_registry_register_and_lookup() {
332        let mut registry = ProofRegistry::new();
333        let func_hash = hash_bytes(b"fn1");
334
335        let mut b = ExecutionProofBuilder::new(func_hash);
336        b.set_args_hash(hash_bytes(b"a1"));
337        let p1 = b.finalize(hash_bytes(b"r1"));
338
339        let mut b2 = ExecutionProofBuilder::new(func_hash);
340        b2.set_args_hash(hash_bytes(b"a2"));
341        let p2 = b2.finalize(hash_bytes(b"r2"));
342
343        registry.register(p1);
344        registry.register(p2);
345
346        assert_eq!(registry.len(), 2);
347        let results: Vec<_> = registry.lookup_iter(&func_hash).collect();
348        assert_eq!(results.len(), 2);
349    }
350
351    #[test]
352    fn test_registry_verify_all_clean() {
353        let mut registry = ProofRegistry::new();
354        let mut b = ExecutionProofBuilder::new(hash_bytes(b"f"));
355        b.set_args_hash(hash_bytes(b"a"));
356        registry.register(b.finalize(hash_bytes(b"r")));
357
358        let failures = registry.verify_all();
359        assert!(failures.is_empty());
360    }
361
362    #[test]
363    fn test_registry_lookup_missing() {
364        let registry = ProofRegistry::new();
365        let missing = hash_bytes(b"nonexistent");
366        assert!(registry.lookup(&missing).is_empty());
367        assert_eq!(registry.lookup_iter(&missing).count(), 0);
368    }
369}