1use super::NetworkAddress;
23use crate::{
24 domain::journal::{Cap, Fact},
25 time::TimeStamp,
26 types::flow::FlowCost,
27 types::identifiers::{AuthorityId, ContextId},
28 AuraResult as Result, Receipt,
29};
30use async_trait::async_trait;
31use serde::{Deserialize, Serialize};
32use std::collections::HashMap;
33
34#[derive(Debug, Clone)]
40pub struct GuardSnapshot {
41 pub now: TimeStamp,
43 pub caps: Cap,
45 pub budgets: FlowBudgetView,
47 pub metadata: MetadataView,
49 pub rng_seed: [u8; 32],
51}
52
53#[derive(Debug, Clone, Default)]
55pub struct FlowBudgetView {
56 budgets: HashMap<(ContextId, AuthorityId), FlowCost>,
57}
58
59impl FlowBudgetView {
60 pub fn new(budgets: HashMap<(ContextId, AuthorityId), FlowCost>) -> Self {
62 Self { budgets }
63 }
64
65 pub fn get(&self, context: &ContextId, authority: &AuthorityId) -> Option<FlowCost> {
67 self.budgets.get(&(*context, *authority)).copied()
68 }
69
70 pub fn has_budget(
72 &self,
73 context: &ContextId,
74 authority: &AuthorityId,
75 amount: FlowCost,
76 ) -> bool {
77 self.get(context, authority)
78 .is_some_and(|budget| budget >= amount)
79 }
80}
81
82#[derive(Debug, Clone, Default)]
84pub struct MetadataView {
85 metadata: HashMap<String, String>,
86}
87
88impl MetadataView {
89 pub fn new(metadata: HashMap<String, String>) -> Self {
91 Self { metadata }
92 }
93
94 pub fn get(&self, key: &str) -> Option<&str> {
96 self.metadata.get(key).map(|s| s.as_str())
97 }
98
99 pub fn contains(&self, key: &str) -> bool {
101 self.metadata.contains_key(key)
102 }
103}
104
105#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
107pub enum Decision {
108 Authorized,
110 Denied(String),
112}
113
114impl Decision {
115 pub fn is_authorized(&self) -> bool {
117 matches!(self, Self::Authorized)
118 }
119
120 pub fn denial_reason(&self) -> Option<&str> {
122 match self {
123 Self::Denied(reason) => Some(reason),
124 Self::Authorized => None,
125 }
126 }
127}
128
129#[derive(Debug, Clone)]
131pub struct GuardOutcome {
132 pub decision: Decision,
134 pub effects: Vec<EffectCommand>,
136}
137
138impl GuardOutcome {
139 pub fn authorized(effects: Vec<EffectCommand>) -> Self {
141 Self {
142 decision: Decision::Authorized,
143 effects,
144 }
145 }
146
147 pub fn denied(reason: impl Into<String>) -> Self {
149 Self {
150 decision: Decision::Denied(reason.into()),
151 effects: Vec::new(),
152 }
153 }
154
155 pub fn is_authorized(&self) -> bool {
157 self.decision.is_authorized()
158 }
159}
160
161#[derive(Debug, Clone, Serialize, Deserialize)]
166pub enum EffectCommand {
167 ChargeBudget {
169 context: ContextId,
171 authority: AuthorityId,
173 peer: AuthorityId,
175 amount: FlowCost,
177 },
178 AppendJournal {
180 entry: JournalEntry,
182 },
183 RecordLeakage {
185 bits: u32,
187 },
188 StoreMetadata {
190 key: String,
192 value: String,
194 },
195 SendEnvelope {
197 to: NetworkAddress,
199 peer_id: Option<uuid::Uuid>,
201 envelope: Vec<u8>,
203 },
204 GenerateNonce {
206 bytes: u32,
208 },
209}
210
211#[derive(Debug, Clone, Serialize, Deserialize)]
213pub struct JournalEntry {
214 pub fact: Fact,
216 pub authority: AuthorityId,
218 pub timestamp: TimeStamp,
220}
221
222#[derive(Debug, Clone, Serialize, Deserialize)]
224pub enum EffectResult {
225 Success,
227 Failure(String),
229 Receipt(Receipt),
231 Nonce(Vec<u8>),
233 RemainingBudget(u32),
235}
236
237#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
244#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
245pub trait EffectInterpreter: Send + Sync {
246 async fn execute(&self, cmd: EffectCommand) -> Result<EffectResult>;
248
249 fn interpreter_type(&self) -> &'static str;
251}
252
253#[derive(Debug, Clone, Serialize, Deserialize)]
258pub enum SimulationEvent {
259 BudgetCharged {
261 time: TimeStamp,
263 authority: AuthorityId,
265 amount: u32,
267 remaining: u32,
269 },
270 JournalAppended {
272 time: TimeStamp,
274 entry: JournalEntry,
276 },
277 LeakageRecorded {
279 time: TimeStamp,
281 bits: u32,
283 },
284 MetadataStored {
286 time: TimeStamp,
288 key: String,
290 value: String,
292 },
293 EnvelopeQueued {
295 time: TimeStamp,
297 from: NetworkAddress,
299 to: NetworkAddress,
301 envelope: Vec<u8>,
303 },
304 NonceGenerated {
306 time: TimeStamp,
308 nonce: Vec<u8>,
310 },
311}
312
313#[cfg(test)]
314mod tests {
315 use super::*;
316
317 #[test]
318 fn test_flow_budget_view() {
319 let mut budgets = HashMap::new();
320 let authority = AuthorityId::new_from_entropy([31u8; 32]);
321 let context = ContextId::new_from_entropy([32u8; 32]);
322 budgets.insert((context, authority), FlowCost::new(1000));
323
324 let view = FlowBudgetView::new(budgets);
325
326 assert_eq!(view.get(&context, &authority), Some(FlowCost::new(1000)));
327 assert!(view.has_budget(&context, &authority, FlowCost::new(500)));
328 assert!(!view.has_budget(&context, &authority, FlowCost::new(2000)));
329
330 let unknown = AuthorityId::new_from_entropy([33u8; 32]);
331 assert_eq!(view.get(&context, &unknown), None);
332 }
333
334 #[test]
335 fn test_metadata_view() {
336 let mut metadata = HashMap::new();
337 metadata.insert("key1".to_string(), "value1".to_string());
338
339 let view = MetadataView::new(metadata);
340
341 assert_eq!(view.get("key1"), Some("value1"));
342 assert_eq!(view.get("key2"), None);
343 assert!(view.contains("key1"));
344 assert!(!view.contains("key2"));
345 }
346
347 #[test]
348 fn test_decision() {
349 let auth = Decision::Authorized;
350 assert!(auth.is_authorized());
351 assert_eq!(auth.denial_reason(), None);
352
353 let denied = Decision::Denied("Insufficient budget".to_string());
354 assert!(!denied.is_authorized());
355 assert_eq!(denied.denial_reason(), Some("Insufficient budget"));
356 }
357
358 #[test]
359 fn test_guard_outcome() {
360 let effects = vec![EffectCommand::ChargeBudget {
361 context: ContextId::new_from_entropy([34u8; 32]),
362 authority: AuthorityId::new_from_entropy([35u8; 32]),
363 peer: AuthorityId::new_from_entropy([36u8; 32]),
364 amount: FlowCost::new(100),
365 }];
366
367 let authorized = GuardOutcome::authorized(effects);
368 assert!(authorized.is_authorized());
369 assert_eq!(authorized.effects.len(), 1);
370
371 let denied = GuardOutcome::denied("Not allowed");
372 assert!(!denied.is_authorized());
373 assert!(denied.effects.is_empty());
374 }
375
376 #[test]
377 fn test_effect_command_serialization() {
378 let cmd = EffectCommand::ChargeBudget {
379 context: ContextId::new_from_entropy([37u8; 32]),
380 authority: AuthorityId::new_from_entropy([38u8; 32]),
381 peer: AuthorityId::new_from_entropy([39u8; 32]),
382 amount: FlowCost::new(100),
383 };
384
385 let serialized = crate::util::serialization::to_vec(&cmd).unwrap();
386 let deserialized: EffectCommand =
387 crate::util::serialization::from_slice(&serialized).unwrap();
388
389 match deserialized {
390 EffectCommand::ChargeBudget { amount, .. } => {
391 assert_eq!(amount, FlowCost::new(100));
392 }
393 _ => panic!("Wrong command type"),
394 }
395 }
396}