1use std::collections::HashMap;
4
5use serde::{Deserialize, Serialize};
6use thiserror::Error;
7
8#[derive(Debug, Clone, Serialize, Deserialize)]
10pub struct Specification {
11 pub name: String,
13 pub description: String,
15 pub preconditions: Vec<Condition>,
17 pub postconditions: Vec<Condition>,
19 pub invariants: Vec<Condition>,
21}
22
23impl Specification {
24 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 pub fn with_description(mut self, desc: impl Into<String>) -> Self {
37 self.description = desc.into();
38 self
39 }
40
41 pub fn requires(mut self, condition: Condition) -> Self {
43 self.preconditions.push(condition);
44 self
45 }
46
47 pub fn ensures(mut self, condition: Condition) -> Self {
49 self.postconditions.push(condition);
50 self
51 }
52
53 pub fn maintains(mut self, condition: Condition) -> Self {
55 self.invariants.push(condition);
56 self
57 }
58}
59
60#[derive(Debug, Clone, Serialize, Deserialize)]
62pub struct Condition {
63 pub name: String,
65 pub expression: String,
67 pub variables: Vec<String>,
69}
70
71impl Condition {
72 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 pub fn with_var(mut self, var: impl Into<String>) -> Self {
83 self.variables.push(var.into());
84 self
85 }
86}
87
88#[derive(Debug, Clone, Error, Serialize, Deserialize)]
90#[error("specification '{spec}' violated: {condition} - {message}")]
91pub struct SpecViolation {
92 pub spec: String,
94 pub condition: String,
96 pub message: String,
98 pub context: HashMap<String, String>,
100}
101
102impl SpecViolation {
103 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 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#[allow(clippy::module_inception)]
126pub mod specs {
127 use super::*;
128
129 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 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 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 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}