aranya_runtime/
vm_policy.rs

1//! VmPolicy implements a [Policy] that evaluates actions and commands via the [Policy
2//! VM](../../policy_vm/index.html).
3//!
4//! ## Creating a `VmPolicy` instance
5//!
6//! To use `VmPolicy` in your [`Engine`](super::Engine), you need to provide a Policy VM
7//! [`Machine`], a [`aranya_crypto::Engine`], and a Vec of Boxed FFI implementations. The Machine
8//! will be created by either compiling a policy document (see
9//! [`parse_policy_document()`](../../policy_lang/lang/fn.parse_policy_document.html) and
10//! [`Compiler`](../../policy_compiler/struct.Compiler.html)), or loading a compiled policy
11//! module (see [`Machine::from_module()`]). The crypto engine comes from your favorite
12//! implementation
13//! ([`DefaultEngine::from_entropy()`](aranya_crypto::default::DefaultEngine::from_entropy) is a
14//! good choice for testing). The list of FFIs is a list of things that implement
15//! [`FfiModule`](aranya_policy_vm::ffi::FfiModule), most likely via the [ffi attribute
16//! macro](../../policy_vm/ffi/attr.ffi.html). The list of FFI modules _must_ be in the same
17//! order as the FFI schemas given during VM construction.
18//!
19//! ```ignore
20//! // Create a `Machine` by compiling policy from source.
21//! let ast = parse_policy_document(policy_doc).unwrap();
22//! let machine = Compiler::new(&ast)
23//!     .ffi_modules(&[TestFfiEnvelope::SCHEMA])
24//!     .compile()
25//!     .unwrap();
26//! // Create a `aranya_crypto::Engine` implementation
27//! let (eng, _) = DefaultEngine::from_entropy(Rng);
28//! // Create a list of FFI module implementations
29//! let ffi_modules = vec![Box::from(TestFfiEnvelope {
30//!     device: DeviceId::random(&mut Rng),
31//! })];
32//! // And finally, create the VmPolicy
33//! let policy = VmPolicy::new(machine, eng, ffi_modules).unwrap();
34//! ```
35//!
36//! ## Actions and Effects
37//!
38//! The VM represents actions as a kind of function, which has a name and a list of
39//! parameters. [`VmPolicy`] represents those actions as [`VmAction`]. Calling an action
40//! via [`call_action()`](VmPolicy::call_action) requires you to give it an action of
41//! that type. You can use the [`vm_action!()`](crate::vm_action) macro to create this
42//! more comfortably.
43//!
44//! The VM represents effects as a named struct containing a set of fields. `VmPolicy`
45//! represents this as [`VmEffect`]. Effects captured via [`Sink`]s will have this type.
46//! You can use the [`vm_effect!()`](crate::vm_effect) macro to create effects.
47//!
48//! ## The "init" command and action
49//!
50//! To create a graph, there must be a command that is the ancestor of all commands in that
51//! graph - the "init" command. In `VmPolicy`, that command is created via a special action
52//! given as the second argument to
53//! [`ClientState::new_graph()`](crate::ClientState::new_graph). The first command produced
54//! by that action becomes the "init" command. It has basically all the same properties as
55//! any other command, except it has no parent.
56//!
57//! So for this example policy:
58//!
59//! ```policy
60//! command Init {
61//!     fields {
62//!         nonce int,
63//!     }
64//!     seal { ... }
65//!     open { ... }
66//!     policy {
67//!         finish {}
68//!     }
69//! }
70//!
71//! action init(nonce int) {
72//!     publish Init {
73//!         nonce: nonce,
74//!     }
75//! }
76//! ```
77//!
78//! This is an example of initializing a graph with `new_graph()`:
79//!
80//! ```ignore
81//! let engine = MyEngine::new();
82//! let provider = MyStorageProvider::new();
83//! let mut cs = ClientState::new(engine, provider);
84//! let mut sink = MySink::new();
85//!
86//! let storage_id = cs
87//!     .new_graph(&[0u8], vm_action!(init(0)), &mut sink)
88//!     .expect("could not create graph");
89//! ```
90//!
91//! Because the ID of this initial command is also the storage ID of the resulting graph,
92//! some data within the command must be present to ensure that multiple initial commands
93//! create distinct IDs for each graph. If no other suitable data exists, it is good
94//! practice to add a nonce field that is distinct for each graph.
95//!
96//! ## Priorities
97//!
98//! `VmPolicy` uses the policy language's attributes system to report command priorities to
99//! the runtime. You can specify the priority of a command by adding the `priority`
100//! attribute. It should be an `int` literal.
101//!
102//! ```policy
103//! command Foo {
104//!     attributes {
105//!         priority: 3
106//!     }
107//!     // ... fields, policy, etc.
108//! }
109//! ```
110//!
111//! ## Policy Interface Generator
112//!
113//! A more comfortable way to use `VmPolicy` is via the [Policy Interface
114//! Generator](../../policy_ifgen/index.html). It creates a Rust interface for actions and
115//! effects from a policy document.
116
117extern crate alloc;
118
119use alloc::{borrow::Cow, boxed::Box, collections::BTreeMap, rc::Rc, string::String, vec::Vec};
120use core::{borrow::Borrow as _, cell::RefCell, fmt};
121
122use aranya_crypto::BaseId;
123use aranya_policy_vm::{
124    ActionContext, CommandContext, CommandDef, ExitReason, KVPair, Machine, MachineIO,
125    MachineStack, OpenContext, PolicyContext, RunState, Stack as _, Struct, Value,
126    ast::{Identifier, Persistence},
127};
128use buggy::{BugExt as _, bug};
129use spin::Mutex;
130use tracing::{error, info, instrument};
131
132use crate::{
133    ActionPlacement, Address, CommandPlacement, FactPerspective, MergeIds, Perspective, Prior,
134    Priority,
135    command::{CmdId, Command},
136    engine::{EngineError, NullSink, Policy, Sink},
137};
138
139mod error;
140mod io;
141mod protocol;
142pub mod testing;
143
144pub use error::*;
145pub use io::*;
146pub use protocol::*;
147
148/// Creates a [`VmAction`].
149///
150/// This must be used directly to avoid lifetime issues, not assigned to a variable.
151///
152/// # Example
153///
154/// ```ignore
155/// let x = 42;
156/// let y = text!("asdf");
157/// client.action(storage_id, sink, vm_action!(foobar(x, y)))
158/// ```
159#[macro_export]
160macro_rules! vm_action {
161    ($name:ident($($arg:expr),* $(,)?)) => {
162        $crate::VmAction {
163            name: ::aranya_policy_vm::ident!(stringify!($name)),
164            args: [$(::aranya_policy_vm::Value::from($arg)),*].as_slice().into(),
165        }
166    };
167}
168
169/// Creates a [`VmEffectData`].
170///
171/// This is mostly useful for testing expected effects, and is expected to be compared
172/// against a [`VmEffect`].
173///
174/// # Example
175///
176/// ```ignore
177/// let val = 3;
178/// sink.add_expectation(vm_effect!(StuffHappened { x: 1, y: val }));
179///
180/// client.action(storage_id, sink, vm_action!(create(val)))
181/// ```
182#[macro_export]
183macro_rules! vm_effect {
184    ($name:ident { $($field:ident : $val:expr),* $(,)? }) => {
185        $crate::VmEffectData {
186            name: ::aranya_policy_vm::ident!(stringify!($name)),
187            fields: vec![$(
188                ::aranya_policy_vm::KVPair::new(::aranya_policy_vm::ident!(stringify!($field)), $val.into())
189            ),*],
190        }
191    };
192}
193
194/// A [Policy] implementation that uses the Policy VM.
195pub struct VmPolicy<E> {
196    machine: Machine,
197    engine: Mutex<E>,
198    ffis: Vec<Box<dyn FfiCallable<E> + Send + 'static>>,
199    priority_map: BTreeMap<Identifier, VmPriority>,
200}
201
202impl<E> VmPolicy<E> {
203    /// Create a new `VmPolicy` from a [Machine]
204    pub fn new(
205        machine: Machine,
206        engine: E,
207        ffis: Vec<Box<dyn FfiCallable<E> + Send + 'static>>,
208    ) -> Result<Self, VmPolicyError> {
209        let priority_map = get_command_priorities(&machine)?;
210        Ok(Self {
211            machine,
212            engine: Mutex::new(engine),
213            ffis,
214            priority_map,
215        })
216    }
217
218    fn source_location<M>(&self, rs: &RunState<'_, M>) -> String
219    where
220        M: MachineIO<MachineStack>,
221    {
222        rs.source_location()
223            .unwrap_or_else(|| String::from("(unknown location)"))
224    }
225}
226
227/// Scans command attributes for priorities and creates the priority map from them.
228fn get_command_priorities(
229    machine: &Machine,
230) -> Result<BTreeMap<Identifier, VmPriority>, AttributeError> {
231    let mut priority_map = BTreeMap::new();
232    for def in machine.command_defs.iter() {
233        let name = &def.name.name;
234        let attrs = PriorityAttrs::load(name.as_str(), def)?;
235        match def.persistence {
236            Persistence::Persistent => {
237                priority_map.insert(name.clone(), get_command_priority(name, &attrs)?);
238            }
239            Persistence::Ephemeral { .. } => {
240                if attrs != PriorityAttrs::default() {
241                    return Err(AttributeError(
242                        "ephemeral command must not have priority".into(),
243                    ));
244                }
245            }
246        }
247    }
248    Ok(priority_map)
249}
250
251#[derive(Default, PartialEq)]
252struct PriorityAttrs {
253    init: bool,
254    finalize: bool,
255    priority: Option<u32>,
256}
257
258impl PriorityAttrs {
259    fn load(name: &str, def: &CommandDef) -> Result<Self, AttributeError> {
260        let attrs = &def.attributes;
261        let init = attrs
262            .get("init")
263            .map(|attr| match attr.value {
264                Value::Bool(b) => Ok(b),
265                _ => Err(AttributeError::type_mismatch(
266                    name,
267                    "finalize",
268                    "Bool",
269                    &attr.value.type_name(),
270                )),
271            })
272            .transpose()?
273            == Some(true);
274        let finalize = attrs
275            .get("finalize")
276            .map(|attr| match attr.value {
277                Value::Bool(b) => Ok(b),
278                _ => Err(AttributeError::type_mismatch(
279                    name,
280                    "finalize",
281                    "Bool",
282                    &attr.value.type_name(),
283                )),
284            })
285            .transpose()?
286            == Some(true);
287        let priority: Option<u32> = attrs
288            .get("priority")
289            .map(|attr| match attr.value {
290                Value::Int(b) => b.try_into().map_err(|_| {
291                    AttributeError::int_range(name, "priority", u32::MIN.into(), u32::MAX.into())
292                }),
293                _ => Err(AttributeError::type_mismatch(
294                    name,
295                    "priority",
296                    "Int",
297                    &attr.value.type_name(),
298                )),
299            })
300            .transpose()?;
301        Ok(Self {
302            init,
303            finalize,
304            priority,
305        })
306    }
307}
308
309fn get_command_priority(
310    name: &Identifier,
311    attrs: &PriorityAttrs,
312) -> Result<VmPriority, AttributeError> {
313    match (attrs.init, attrs.finalize, attrs.priority) {
314        (true, true, _) => Err(AttributeError::exclusive(name.as_str(), "init", "finalize")),
315        (true, false, Some(_)) => Err(AttributeError::exclusive(name.as_str(), "init", "priority")),
316        (true, false, None) => Ok(VmPriority::Init),
317
318        (false, true, Some(_)) => Err(AttributeError::exclusive(
319            name.as_str(),
320            "finalize",
321            "priority",
322        )),
323        (false, true, None) => Ok(VmPriority::Finalize),
324
325        (false, false, Some(n)) => Ok(VmPriority::Basic(n)),
326
327        (false, false, None) => Err(AttributeError::missing(
328            name.as_str(),
329            "init | finalize | priority",
330        )),
331    }
332}
333
334impl<E: aranya_crypto::Engine> VmPolicy<E> {
335    #[allow(clippy::too_many_arguments)]
336    #[instrument(skip_all, fields(name = name.as_str()))]
337    fn evaluate_rule<'a, P>(
338        &self,
339        name: Identifier,
340        fields: &[KVPair],
341        envelope: Envelope<'_>,
342        facts: &'a mut P,
343        sink: &'a mut impl Sink<VmEffect>,
344        ctx: CommandContext,
345        placement: CommandPlacement,
346    ) -> Result<(), EngineError>
347    where
348        P: FactPerspective,
349    {
350        let facts = RefCell::new(facts);
351        let sink = RefCell::new(sink);
352        let io = RefCell::new(VmPolicyIO::new(&facts, &sink, &self.engine, &self.ffis));
353        let mut rs = self.machine.create_run_state(&io, ctx);
354        let this_data = Struct::new(name, fields);
355        match rs.call_command_policy(this_data.clone(), envelope.clone().into()) {
356            Ok(reason) => match reason {
357                ExitReason::Normal => Ok(()),
358                ExitReason::Yield => bug!("unexpected yield"),
359                ExitReason::Check => {
360                    info!("Check {}", self.source_location(&rs));
361
362                    match placement {
363                        CommandPlacement::OnGraphAtOrigin | CommandPlacement::OffGraph => {
364                            // Immediate check failure.
365                            return Err(EngineError::Check);
366                        }
367                        CommandPlacement::OnGraphInBraid => {
368                            // Perform recall.
369                        }
370                    }
371
372                    // Construct a new recall context from the policy context
373                    let CommandContext::Policy(policy_ctx) = rs.get_context() else {
374                        error!(
375                            "Non-policy context while evaluating rule: {:?}",
376                            rs.get_context()
377                        );
378                        return Err(EngineError::InternalError);
379                    };
380                    let recall_ctx = CommandContext::Recall(policy_ctx.clone());
381                    rs.set_context(recall_ctx);
382                    self.recall_internal(&mut rs, this_data, envelope)
383                }
384                ExitReason::Panic => {
385                    info!("Panicked {}", self.source_location(&rs));
386                    Err(EngineError::Panic)
387                }
388            },
389            Err(e) => {
390                error!("\n{e}");
391                Err(EngineError::InternalError)
392            }
393        }
394    }
395
396    fn recall_internal<M>(
397        &self,
398        rs: &mut RunState<'_, M>,
399        this_data: Struct,
400        envelope: Envelope<'_>,
401    ) -> Result<(), EngineError>
402    where
403        M: MachineIO<MachineStack>,
404    {
405        match rs.call_command_recall(this_data, envelope.into()) {
406            Ok(ExitReason::Normal) => Err(EngineError::Check),
407            Ok(ExitReason::Yield) => bug!("unexpected yield"),
408            Ok(ExitReason::Check) => {
409                info!("Recall failed: {}", self.source_location(rs));
410                Err(EngineError::Check)
411            }
412            Ok(ExitReason::Panic) | Err(_) => {
413                info!("Recall panicked: {}", self.source_location(rs));
414                Err(EngineError::Panic)
415            }
416        }
417    }
418
419    #[instrument(skip_all, fields(name = name.as_str()))]
420    fn open_command<P>(
421        &self,
422        name: Identifier,
423        envelope: Envelope<'_>,
424        facts: &mut P,
425    ) -> Result<Struct, EngineError>
426    where
427        P: FactPerspective,
428    {
429        let facts = RefCell::new(facts);
430        let mut sink = NullSink;
431        let sink2 = RefCell::new(&mut sink);
432        let io = RefCell::new(VmPolicyIO::new(&facts, &sink2, &self.engine, &self.ffis));
433        let ctx = CommandContext::Open(OpenContext { name: name.clone() });
434        let mut rs = self.machine.create_run_state(&io, ctx);
435        let status = rs.call_open(name, envelope.into());
436        match status {
437            Ok(reason) => match reason {
438                ExitReason::Normal => {
439                    let v = rs.consume_return().map_err(|e| {
440                        error!("Could not pull envelope from stack: {e}");
441                        EngineError::InternalError
442                    })?;
443                    Ok(v.try_into().map_err(|e| {
444                        error!("Envelope is not a struct: {e}");
445                        EngineError::InternalError
446                    })?)
447                }
448                ExitReason::Yield => bug!("unexpected yield"),
449                ExitReason::Check => {
450                    info!("Check {}", self.source_location(&rs));
451                    Err(EngineError::Check)
452                }
453                ExitReason::Panic => {
454                    info!("Panicked {}", self.source_location(&rs));
455                    Err(EngineError::Check)
456                }
457            },
458            Err(e) => {
459                error!("\n{e}");
460                Err(EngineError::InternalError)
461            }
462        }
463    }
464}
465
466/// [`VmPolicy`]'s actions.
467#[derive(Clone, Debug, PartialEq, Eq)]
468pub struct VmAction<'a> {
469    /// The name of the action.
470    pub name: Identifier,
471    /// The arguments of the action.
472    pub args: Cow<'a, [Value]>,
473}
474
475/// A partial version of [`VmEffect`] containing only the data. Created by
476/// [`vm_effect!`] and used to compare only the name and fields against the full
477/// `VmEffect`.
478#[derive(Debug)]
479pub struct VmEffectData {
480    /// The name of the effect.
481    pub name: Identifier,
482    /// The fields of the effect.
483    pub fields: Vec<KVPair>,
484}
485
486impl PartialEq<VmEffect> for VmEffectData {
487    fn eq(&self, other: &VmEffect) -> bool {
488        self.name == other.name && self.fields == other.fields
489    }
490}
491
492impl PartialEq<VmEffectData> for VmEffect {
493    fn eq(&self, other: &VmEffectData) -> bool {
494        self.name == other.name && self.fields == other.fields
495    }
496}
497
498/// [`VmPolicy`]'s effects.
499#[derive(Clone, Debug, PartialEq, Eq)]
500pub struct VmEffect {
501    /// The name of the effect.
502    pub name: Identifier,
503    /// The fields of the effect.
504    pub fields: Vec<KVPair>,
505    /// The command ID that produced this effect
506    pub command: CmdId,
507    /// Was this produced from a recall block?
508    pub recalled: bool,
509}
510
511#[derive(Copy, Clone, Debug, PartialEq)]
512enum VmPriority {
513    Init,
514    Basic(u32),
515    Finalize,
516}
517
518impl Default for VmPriority {
519    fn default() -> Self {
520        Self::Basic(0)
521    }
522}
523
524impl From<VmPriority> for Priority {
525    fn from(value: VmPriority) -> Self {
526        match value {
527            VmPriority::Init => Self::Init,
528            VmPriority::Basic(p) => Self::Basic(p),
529            VmPriority::Finalize => Self::Finalize,
530        }
531    }
532}
533
534impl<E> VmPolicy<E> {
535    fn get_command_priority(&self, name: &Identifier) -> VmPriority {
536        debug_assert!(self.machine.command_defs.contains(name));
537        self.priority_map.get(name).copied().unwrap_or_default()
538    }
539}
540
541impl<E: aranya_crypto::Engine> Policy for VmPolicy<E> {
542    type Action<'a> = VmAction<'a>;
543    type Effect = VmEffect;
544    type Command<'a> = VmProtocol<'a>;
545
546    fn serial(&self) -> u32 {
547        // TODO(chip): Implement an actual serial number
548        0u32
549    }
550
551    #[instrument(skip_all)]
552    fn call_rule(
553        &self,
554        command: &impl Command,
555        facts: &mut impl FactPerspective,
556        sink: &mut impl Sink<Self::Effect>,
557        placement: CommandPlacement,
558    ) -> Result<(), EngineError> {
559        let parent_id = match command.parent() {
560            Prior::None => CmdId::default(),
561            Prior::Single(parent) => parent.id,
562            Prior::Merge(_, _) => bug!("merge commands are not evaluated"),
563        };
564
565        let VmProtocolData {
566            author_id,
567            kind,
568            serialized_fields,
569            signature,
570        } = postcard::from_bytes(command.bytes()).map_err(|e| {
571            error!("Could not deserialize: {e:?}");
572            EngineError::Read
573        })?;
574
575        let expected_priority = self.get_command_priority(&kind).into();
576        if command.priority() != expected_priority {
577            error!(
578                "Expected priority {:?}, got {:?}",
579                expected_priority,
580                command.priority()
581            );
582            bug!("Command has invalid priority");
583        }
584
585        let def = self.machine.command_defs.get(&kind).ok_or_else(|| {
586            error!("unknown command {kind}");
587            EngineError::InternalError
588        })?;
589
590        let envelope = Envelope {
591            parent_id,
592            author_id,
593            command_id: command.id(),
594            payload: Cow::Borrowed(serialized_fields),
595            signature: Cow::Borrowed(signature),
596        };
597
598        match (placement, &def.persistence) {
599            (CommandPlacement::OnGraphAtOrigin, Persistence::Persistent) => {}
600            (CommandPlacement::OnGraphInBraid, Persistence::Persistent) => {}
601            (CommandPlacement::OffGraph, Persistence::Ephemeral(_)) => {}
602            (CommandPlacement::OnGraphAtOrigin, Persistence::Ephemeral(_)) => {
603                error!("cannot evaluate ephemeral command on-graph");
604                return Err(EngineError::InternalError);
605            }
606            (CommandPlacement::OnGraphInBraid, Persistence::Ephemeral(_)) => {
607                error!("cannot evaluate ephemeral command in braid");
608                return Err(EngineError::InternalError);
609            }
610            (CommandPlacement::OffGraph, Persistence::Persistent) => {
611                error!("cannot evaluate persistent command off-graph");
612                return Err(EngineError::InternalError);
613            }
614        }
615
616        let command_struct = self.open_command(kind.clone(), envelope.clone(), facts)?;
617        let fields: Vec<KVPair> = command_struct
618            .fields
619            .into_iter()
620            .map(|(k, v)| KVPair::new(k, v))
621            .collect();
622        let ctx = CommandContext::Policy(PolicyContext {
623            name: kind.clone(),
624            id: command.id(),
625            author: author_id,
626            version: BaseId::default(),
627        });
628        self.evaluate_rule(
629            kind,
630            fields.as_slice(),
631            envelope,
632            facts,
633            sink,
634            ctx,
635            placement,
636        )?;
637
638        Ok(())
639    }
640
641    #[instrument(skip_all, fields(name = action.name.as_str()))]
642    fn call_action(
643        &self,
644        action: Self::Action<'_>,
645        facts: &mut impl Perspective,
646        sink: &mut impl Sink<Self::Effect>,
647        action_placement: ActionPlacement,
648    ) -> Result<(), EngineError> {
649        let VmAction { name, args } = action;
650
651        let def = self.machine.action_defs.get(&name).ok_or_else(|| {
652            error!("action not found");
653            EngineError::InternalError
654        })?;
655
656        match (action_placement, &def.persistence) {
657            (ActionPlacement::OnGraph, Persistence::Persistent) => {}
658            (ActionPlacement::OffGraph, Persistence::Ephemeral(_)) => {}
659            (ActionPlacement::OnGraph, Persistence::Ephemeral(_)) => {
660                error!("cannot call ephemeral action on-graph");
661                return Err(EngineError::InternalError);
662            }
663            (ActionPlacement::OffGraph, Persistence::Persistent) => {
664                error!("cannot call persistent action off-graph");
665                return Err(EngineError::InternalError);
666            }
667        }
668
669        let parent = match facts.head_address()? {
670            Prior::None => None,
671            Prior::Single(id) => Some(id),
672            Prior::Merge(_, _) => bug!("cannot have a merge parent in call_action"),
673        };
674        // FIXME(chip): This is kind of wrong, but it avoids having to
675        // plumb `Option<CmdId>` into the VM and FFI
676        let ctx_parent = parent.unwrap_or_default();
677        let facts = Rc::new(RefCell::new(facts));
678        let sink = Rc::new(RefCell::new(sink));
679        let io = RefCell::new(VmPolicyIO::new(&facts, &sink, &self.engine, &self.ffis));
680        let ctx = CommandContext::Action(ActionContext {
681            name: name.clone(),
682            head_id: ctx_parent.id,
683        });
684        let command_placement = match action_placement {
685            ActionPlacement::OnGraph => CommandPlacement::OnGraphAtOrigin,
686            ActionPlacement::OffGraph => CommandPlacement::OffGraph,
687        };
688        {
689            let mut rs = self.machine.create_run_state(&io, ctx);
690            let mut exit_reason = match args {
691                Cow::Borrowed(args) => rs.call_action(name, args.iter().cloned()),
692                Cow::Owned(args) => rs.call_action(name, args),
693            }
694            .map_err(|e| {
695                error!("\n{e}");
696                EngineError::InternalError
697            })?;
698            loop {
699                match exit_reason {
700                    ExitReason::Normal => {
701                        // Action completed
702                        break;
703                    }
704                    ExitReason::Yield => {
705                        // Command was published.
706                        let command_struct: Struct = rs.stack.pop().map_err(|e| {
707                            error!("should have command struct: {e}");
708                            EngineError::InternalError
709                        })?;
710                        let command_name = command_struct.name.clone();
711
712                        let seal_ctx = rs.get_context().seal_from_action(command_name.clone())?;
713                        let mut rs_seal = self.machine.create_run_state(&io, seal_ctx);
714                        match rs_seal.call_seal(command_struct).map_err(|e| {
715                            error!("Cannot seal command: {}", e);
716                            EngineError::Panic
717                        })? {
718                            ExitReason::Normal => (),
719                            r @ (ExitReason::Yield | ExitReason::Check | ExitReason::Panic) => {
720                                error!("Could not seal command: {}", r);
721                                return Err(EngineError::Panic);
722                            }
723                        }
724
725                        // Grab sealed envelope from stack
726                        let envelope_struct: Struct = rs_seal.stack.pop().map_err(|e| {
727                            error!("Expected a sealed envelope {e}");
728                            EngineError::InternalError
729                        })?;
730                        let envelope = Envelope::try_from(envelope_struct).map_err(|e| {
731                            error!("Malformed envelope: {e}");
732                            EngineError::InternalError
733                        })?;
734
735                        // The parent of a basic command should be the command that was added to the perspective on the previous
736                        // iteration of the loop
737                        let parent = RefCell::borrow_mut(Rc::borrow(&facts)).head_address()?;
738                        let priority = self.get_command_priority(&command_name).into();
739
740                        let policy;
741                        match parent {
742                            Prior::None => {
743                                // TODO(chip): where does the policy value come from?
744                                policy = Some(0u64.to_le_bytes());
745                                if !matches!(priority, Priority::Init) {
746                                    error!(
747                                        "Command {command_name} has invalid priority {priority:?}"
748                                    );
749                                    return Err(EngineError::InternalError);
750                                }
751                            }
752                            Prior::Single(_) => {
753                                policy = None;
754                                if !matches!(priority, Priority::Basic(_) | Priority::Finalize) {
755                                    error!(
756                                        "Command {command_name} has invalid priority {priority:?}"
757                                    );
758                                    return Err(EngineError::InternalError);
759                                }
760                            }
761                            Prior::Merge(_, _) => bug!("cannot have a merge parent in call_action"),
762                        };
763
764                        let data = VmProtocolData {
765                            author_id: envelope.author_id,
766                            kind: command_name.clone(),
767                            serialized_fields: &envelope.payload,
768                            signature: &envelope.signature,
769                        };
770
771                        let wrapped = postcard::to_allocvec(&data)
772                            .assume("can serialize vm protocol data")?;
773
774                        let new_command = VmProtocol {
775                            id: envelope.command_id,
776                            priority,
777                            parent,
778                            policy,
779                            data: &wrapped,
780                        };
781
782                        self.call_rule(
783                            &new_command,
784                            *RefCell::borrow_mut(Rc::borrow(&facts)),
785                            *RefCell::borrow_mut(Rc::borrow(&sink)),
786                            command_placement,
787                        )?;
788                        RefCell::borrow_mut(Rc::borrow(&facts))
789                            .add_command(&new_command)
790                            .map_err(|e| {
791                                error!("{e}");
792                                EngineError::Write
793                            })?;
794
795                        // After publishing a new command, the RunState's context must be updated to reflect the new head
796                        rs.update_context_with_new_head(new_command.id())?;
797
798                        // Resume action after last Publish
799                        exit_reason = rs.run().map_err(|e| {
800                            error!("{e}");
801                            EngineError::InternalError
802                        })?;
803                    }
804                    ExitReason::Check => {
805                        info!("Check {}", self.source_location(&rs));
806                        return Err(EngineError::Check);
807                    }
808                    ExitReason::Panic => {
809                        info!("Panicked {}", self.source_location(&rs));
810                        return Err(EngineError::Panic);
811                    }
812                }
813            }
814        }
815
816        Ok(())
817    }
818
819    fn merge<'a>(
820        &self,
821        _target: &'a mut [u8],
822        ids: MergeIds,
823    ) -> Result<Self::Command<'a>, EngineError> {
824        let (left, right): (Address, Address) = ids.into();
825        let id = aranya_crypto::merge_cmd_id::<E::CS>(left.id, right.id);
826        Ok(VmProtocol {
827            id,
828            priority: Priority::Merge,
829            parent: Prior::Merge(left, right),
830            policy: None,
831            data: &[],
832        })
833    }
834}
835
836impl fmt::Display for VmAction<'_> {
837    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
838        let mut d = f.debug_tuple(self.name.as_str());
839        for arg in self.args.as_ref() {
840            d.field(&DebugViaDisplay(arg));
841        }
842        d.finish()
843    }
844}
845
846impl fmt::Display for VmEffect {
847    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
848        let mut d = f.debug_struct(self.name.as_str());
849        for field in &self.fields {
850            d.field(field.key().as_str(), &DebugViaDisplay(field.value()));
851        }
852        d.finish()
853    }
854}
855
856/// Implements `Debug` via `T`'s `Display` impl.
857struct DebugViaDisplay<T>(T);
858
859impl<T: fmt::Display> fmt::Debug for DebugViaDisplay<T> {
860    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
861        write!(f, "{}", self.0)
862    }
863}
864
865#[cfg(test)]
866mod test {
867    use alloc::format;
868
869    use aranya_policy_compiler::Compiler;
870    use aranya_policy_lang::lang::parse_policy_str;
871    use aranya_policy_vm::ast::Version;
872
873    use super::*;
874
875    #[test]
876    fn test_require_command_priority() {
877        let cases = [
878            r#"command Test {
879                fields {}
880                seal { return todo() }
881                open { return todo() }
882                policy {}
883            }"#,
884            r#"command Test {
885                attributes {}
886                fields {}
887                seal { return todo() }
888                open { return todo() }
889                policy {}
890            }"#,
891            r#"command Test {
892                attributes {
893                    init: false,
894                    finalize: false,
895                }
896                fields {}
897                seal { return todo() }
898                open { return todo() }
899                policy {}
900            }"#,
901        ];
902
903        for case in cases {
904            let ast = parse_policy_str(case, Version::V2).unwrap_or_else(|e| panic!("{e}"));
905            let module = Compiler::new(&ast)
906                .compile()
907                .unwrap_or_else(|e| panic!("{e}"));
908            let machine = Machine::from_module(module).expect("can create machine");
909            let err = get_command_priorities(&machine).expect_err("should fail");
910            assert_eq!(
911                err,
912                AttributeError::missing("Test", "init | finalize | priority")
913            );
914        }
915    }
916
917    #[test]
918    fn test_get_command_priorities() {
919        fn process(attrs: &str) -> Result<VmPriority, AttributeError> {
920            let policy = format!(
921                r#"
922                command Test {{
923                    attributes {{
924                        {attrs}
925                    }}
926                    fields {{ }}
927                    seal {{ return todo() }}
928                    open {{ return todo() }}
929                    policy {{ }}
930                }}
931                "#
932            );
933            let ast = parse_policy_str(&policy, Version::V2).unwrap_or_else(|e| panic!("{e}"));
934            let module = Compiler::new(&ast)
935                .compile()
936                .unwrap_or_else(|e| panic!("{e}"));
937            let machine = Machine::from_module(module).expect("can create machine");
938            let priorities = get_command_priorities(&machine)?;
939            Ok(*priorities.get("Test").expect("priorities are mandatory"))
940        }
941
942        assert_eq!(process("priority: 42"), Ok(VmPriority::Basic(42)));
943        assert_eq!(
944            process("finalize: false, priority: 42"),
945            Ok(VmPriority::Basic(42))
946        );
947        assert_eq!(
948            process("init: false, priority: 42, finalize: false"),
949            Ok(VmPriority::Basic(42))
950        );
951
952        assert_eq!(process("init: true"), Ok(VmPriority::Init));
953        assert_eq!(process("finalize: true"), Ok(VmPriority::Finalize));
954
955        assert_eq!(
956            process("finalize: 42"),
957            Err(AttributeError::type_mismatch(
958                "Test", "finalize", "Bool", "Int"
959            ))
960        );
961        assert_eq!(
962            process("priority: false"),
963            Err(AttributeError::type_mismatch(
964                "Test", "priority", "Int", "Bool"
965            ))
966        );
967        assert_eq!(
968            process("priority: -1"),
969            Err(AttributeError::int_range(
970                "Test",
971                "priority",
972                u32::MIN.into(),
973                u32::MAX.into(),
974            ))
975        );
976        assert_eq!(
977            process(&format!("priority: {}", i64::MAX)),
978            Err(AttributeError::int_range(
979                "Test",
980                "priority",
981                u32::MIN.into(),
982                u32::MAX.into(),
983            ))
984        );
985
986        assert_eq!(
987            process("finalize: true, priority: 42"),
988            Err(AttributeError::exclusive("Test", "finalize", "priority"))
989        );
990        assert_eq!(
991            process("init: true, priority: 42"),
992            Err(AttributeError::exclusive("Test", "init", "priority"))
993        );
994        assert_eq!(
995            process("init: true, finalize: true"),
996            Err(AttributeError::exclusive("Test", "init", "finalize"))
997        );
998    }
999}