Skip to main content

moloch_verify/
specs.rs

1//! Formal specifications for chain behavior.
2
3use std::collections::HashMap;
4
5use serde::{Deserialize, Serialize};
6use thiserror::Error;
7
8/// A formal specification of expected behavior.
9#[derive(Debug, Clone, Serialize, Deserialize)]
10pub struct Specification {
11    /// Specification name.
12    pub name: String,
13    /// Description.
14    pub description: String,
15    /// Preconditions that must hold.
16    pub preconditions: Vec<Condition>,
17    /// Postconditions that must hold after execution.
18    pub postconditions: Vec<Condition>,
19    /// Invariants that must hold throughout.
20    pub invariants: Vec<Condition>,
21}
22
23impl Specification {
24    /// Create a new specification.
25    pub fn new(name: impl Into<String>) -> Self {
26        Self {
27            name: name.into(),
28            description: String::new(),
29            preconditions: Vec::new(),
30            postconditions: Vec::new(),
31            invariants: Vec::new(),
32        }
33    }
34
35    /// Add a description.
36    pub fn with_description(mut self, desc: impl Into<String>) -> Self {
37        self.description = desc.into();
38        self
39    }
40
41    /// Add a precondition.
42    pub fn requires(mut self, condition: Condition) -> Self {
43        self.preconditions.push(condition);
44        self
45    }
46
47    /// Add a postcondition.
48    pub fn ensures(mut self, condition: Condition) -> Self {
49        self.postconditions.push(condition);
50        self
51    }
52
53    /// Add an invariant.
54    pub fn maintains(mut self, condition: Condition) -> Self {
55        self.invariants.push(condition);
56        self
57    }
58}
59
60/// A condition in a specification.
61#[derive(Debug, Clone, Serialize, Deserialize)]
62pub struct Condition {
63    /// Condition name.
64    pub name: String,
65    /// Condition expression (human-readable).
66    pub expression: String,
67    /// Variables used in the condition.
68    pub variables: Vec<String>,
69}
70
71impl Condition {
72    /// Create a new condition.
73    pub fn new(name: impl Into<String>, expression: impl Into<String>) -> Self {
74        Self {
75            name: name.into(),
76            expression: expression.into(),
77            variables: Vec::new(),
78        }
79    }
80
81    /// Add a variable.
82    pub fn with_var(mut self, var: impl Into<String>) -> Self {
83        self.variables.push(var.into());
84        self
85    }
86}
87
88/// Violation of a specification.
89#[derive(Debug, Clone, Error, Serialize, Deserialize)]
90#[error("specification '{spec}' violated: {condition} - {message}")]
91pub struct SpecViolation {
92    /// Specification name.
93    pub spec: String,
94    /// Violated condition.
95    pub condition: String,
96    /// Violation message.
97    pub message: String,
98    /// Context values.
99    pub context: HashMap<String, String>,
100}
101
102impl SpecViolation {
103    /// Create a new violation.
104    pub fn new(
105        spec: impl Into<String>,
106        condition: impl Into<String>,
107        message: impl Into<String>,
108    ) -> Self {
109        Self {
110            spec: spec.into(),
111            condition: condition.into(),
112            message: message.into(),
113            context: HashMap::new(),
114        }
115    }
116
117    /// Add context.
118    pub fn with_context(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
119        self.context.insert(key.into(), value.into());
120        self
121    }
122}
123
124/// Well-known specifications for Moloch.
125#[allow(clippy::module_inception)]
126pub mod specs {
127    use super::*;
128
129    /// Specification for block production.
130    pub fn block_production() -> Specification {
131        Specification::new("block_production")
132            .with_description("Block production must follow consensus rules")
133            .requires(
134                Condition::new(
135                    "valid_proposer",
136                    "proposer == validators[height % len(validators)]",
137                )
138                .with_var("proposer")
139                .with_var("validators")
140                .with_var("height"),
141            )
142            .requires(Condition::new("valid_parent", "block.parent == chain.tip"))
143            .ensures(Condition::new(
144                "height_incremented",
145                "new_height == old_height + 1",
146            ))
147            .ensures(Condition::new("hash_unique", "!chain.contains(block.hash)"))
148            .maintains(Condition::new(
149                "monotonic_time",
150                "block.timestamp >= parent.timestamp",
151            ))
152    }
153
154    /// Specification for event creation.
155    pub fn event_creation() -> Specification {
156        Specification::new("event_creation")
157            .with_description("Events must be properly signed and unique")
158            .requires(Condition::new(
159                "valid_signature",
160                "verify(event.signature, event.pubkey, event.hash)",
161            ))
162            .requires(Condition::new(
163                "valid_timestamp",
164                "event.timestamp <= now + max_drift",
165            ))
166            .ensures(Condition::new("id_unique", "!chain.has_event(event.id)"))
167            .ensures(Condition::new("in_mmr", "mmr.contains(event.id)"))
168    }
169
170    /// Specification for MMR operations.
171    pub fn mmr_append() -> Specification {
172        Specification::new("mmr_append")
173            .with_description("MMR append must maintain consistency")
174            .requires(Condition::new("valid_leaf", "leaf.len() == 32"))
175            .ensures(Condition::new("size_increased", "new_size == old_size + 1"))
176            .ensures(Condition::new("root_changed", "new_root != old_root"))
177            .maintains(Condition::new(
178                "proofs_valid",
179                "forall pos: verify_proof(mmr, pos)",
180            ))
181    }
182
183    /// Specification for finality.
184    pub fn finality() -> Specification {
185        Specification::new("finality")
186            .with_description("Finality requires 2/3+ validator signatures")
187            .requires(Condition::new(
188                "sufficient_votes",
189                "votes.len() >= (validators.len() * 2 / 3) + 1",
190            ))
191            .requires(Condition::new(
192                "valid_votes",
193                "forall v in votes: verify(v.sig, v.pubkey, block.hash)",
194            ))
195            .ensures(Condition::new(
196                "block_finalized",
197                "chain.finalized.contains(block)",
198            ))
199            .maintains(Condition::new("no_revert", "!chain.can_revert(block)"))
200    }
201}
202
203#[cfg(test)]
204mod tests {
205    use super::*;
206
207    #[test]
208    fn test_specification_builder() {
209        let spec = Specification::new("test_spec")
210            .with_description("A test specification")
211            .requires(Condition::new("pre1", "x > 0"))
212            .ensures(Condition::new("post1", "y == x + 1"));
213
214        assert_eq!(spec.name, "test_spec");
215        assert_eq!(spec.preconditions.len(), 1);
216        assert_eq!(spec.postconditions.len(), 1);
217    }
218
219    #[test]
220    fn test_spec_violation() {
221        let violation =
222            SpecViolation::new("test_spec", "pre1", "precondition failed").with_context("x", "-1");
223
224        assert_eq!(violation.spec, "test_spec");
225        assert_eq!(violation.context.get("x"), Some(&"-1".to_string()));
226    }
227
228    #[test]
229    fn test_builtin_specs() {
230        let block_spec = specs::block_production();
231        assert!(!block_spec.preconditions.is_empty());
232        assert!(!block_spec.postconditions.is_empty());
233
234        let mmr_spec = specs::mmr_append();
235        assert!(!mmr_spec.invariants.is_empty());
236    }
237}