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::{
120    borrow::Cow, boxed::Box, collections::BTreeMap, rc::Rc, string::String, sync::Arc, vec::Vec,
121};
122use core::{borrow::Borrow, cell::RefCell, fmt};
123
124use aranya_policy_vm::{
125    ActionContext, CommandContext, ExitReason, KVPair, Machine, MachineIO, MachineStack,
126    OpenContext, PolicyContext, RunState, Stack, Struct, Value,
127};
128use buggy::{bug, BugExt};
129use spin::Mutex;
130use tracing::{error, info, instrument};
131
132use crate::{
133    command::{Command, CommandId},
134    engine::{EngineError, NullSink, Policy, Sink},
135    CommandRecall, FactPerspective, MergeIds, Perspective, Prior,
136};
137
138mod error;
139mod io;
140mod protocol;
141pub mod testing;
142
143pub use error::*;
144pub use io::*;
145pub use protocol::*;
146
147/// Creates a [`VmAction`].
148///
149/// This must be used directly to avoid lifetime issues, not assigned to a variable.
150///
151/// # Example
152///
153/// ```ignore
154/// let x = 42;
155/// let y = String::from("asdf");
156/// client.action(storage_id, sink, vm_action!(foobar(x, y)))
157/// ```
158#[macro_export]
159macro_rules! vm_action {
160    ($name:ident($($arg:expr),* $(,)?)) => {
161        $crate::VmAction {
162            name: stringify!($name),
163            args: [$(::aranya_policy_vm::Value::from($arg)),*].as_slice().into(),
164        }
165    };
166}
167
168/// Creates a [`VmEffectData`].
169///
170/// This is mostly useful for testing expected effects, and is expected to be compared
171/// against a [`VmEffect`].
172///
173/// # Example
174///
175/// ```ignore
176/// let val = 3;
177/// sink.add_expectation(vm_effect!(StuffHappened { x: 1, y: val }));
178///
179/// client.action(storage_id, sink, vm_action!(create(val)))
180/// ```
181#[macro_export]
182macro_rules! vm_effect {
183    ($name:ident { $($field:ident : $val:expr),* $(,)? }) => {
184        $crate::VmEffectData {
185            name: stringify!($name).into(),
186            fields: vec![$(
187                ::aranya_policy_vm::KVPair::new(stringify!($field), $val.into())
188            ),*],
189        }
190    };
191}
192
193/// A [Policy] implementation that uses the Policy VM.
194pub struct VmPolicy<E> {
195    machine: Machine,
196    engine: Mutex<E>,
197    ffis: Vec<Box<dyn FfiCallable<E> + Send + 'static>>,
198    // TODO(chip): replace or fill this with priorities from attributes
199    priority_map: Arc<BTreeMap<String, u32>>,
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 = VmPolicy::<E>::get_command_priorities(&machine)?;
210        Ok(Self {
211            machine,
212            engine: Mutex::new(engine),
213            ffis,
214            priority_map: Arc::new(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(String::from("(unknown location)"))
224    }
225
226    /// Scans command attributes for priorities and creates the priority map from them.
227    fn get_command_priorities(machine: &Machine) -> Result<BTreeMap<String, u32>, VmPolicyError> {
228        let mut priority_map = BTreeMap::new();
229        for (name, attrs) in &machine.command_attributes {
230            if let Some(Value::Int(p)) = attrs.get("priority") {
231                let pv = (*p).try_into().map_err(|e| {
232                    error!(
233                        ?e,
234                        "Priority out of range in {name}: {p} does not fit in u32"
235                    );
236                    VmPolicyError::Unknown
237                })?;
238                priority_map.insert(name.clone(), pv);
239            }
240        }
241        Ok(priority_map)
242    }
243}
244
245impl<E: aranya_crypto::Engine> VmPolicy<E> {
246    #[allow(clippy::too_many_arguments)]
247    #[instrument(skip_all, fields(name = name))]
248    fn evaluate_rule<'a, P>(
249        &self,
250        name: &str,
251        fields: &[KVPair],
252        envelope: Envelope<'_>,
253        facts: &'a mut P,
254        sink: &'a mut impl Sink<VmEffect>,
255        ctx: CommandContext<'_>,
256        recall: CommandRecall,
257    ) -> Result<(), EngineError>
258    where
259        P: FactPerspective,
260    {
261        let facts = RefCell::new(facts);
262        let sink = RefCell::new(sink);
263        let io = RefCell::new(VmPolicyIO::new(&facts, &sink, &self.engine, &self.ffis));
264        let mut rs = self.machine.create_run_state(&io, ctx);
265        let self_data = Struct::new(name, fields);
266        match rs.call_command_policy(&self_data.name, &self_data, envelope.clone().into()) {
267            Ok(reason) => match reason {
268                ExitReason::Normal => Ok(()),
269                ExitReason::Yield => bug!("unexpected yield"),
270                ExitReason::Check => {
271                    info!("Check {}", self.source_location(&rs));
272                    // Construct a new recall context from the policy context
273                    let CommandContext::Policy(policy_ctx) = rs.get_context() else {
274                        error!(
275                            "Non-policy context while evaluating rule: {:?}",
276                            rs.get_context()
277                        );
278                        return Err(EngineError::InternalError);
279                    };
280                    let recall_ctx = CommandContext::Recall(policy_ctx.clone());
281                    rs.set_context(recall_ctx);
282                    self.recall_internal(recall, &mut rs, name, &self_data, envelope)
283                }
284                ExitReason::Panic => {
285                    info!("Panicked {}", self.source_location(&rs));
286                    Err(EngineError::Panic)
287                }
288            },
289            Err(e) => {
290                error!("\n{e}");
291                Err(EngineError::InternalError)
292            }
293        }
294    }
295
296    fn recall_internal<M>(
297        &self,
298        recall: CommandRecall,
299        rs: &mut RunState<'_, M>,
300        name: &str,
301        self_data: &Struct,
302        envelope: Envelope<'_>,
303    ) -> Result<(), EngineError>
304    where
305        M: MachineIO<MachineStack>,
306    {
307        match recall {
308            CommandRecall::None => Err(EngineError::Check),
309            CommandRecall::OnCheck => {
310                match rs.call_command_recall(name, self_data, envelope.into()) {
311                    Ok(ExitReason::Normal) => Err(EngineError::Check),
312                    Ok(ExitReason::Yield) => bug!("unexpected yield"),
313                    Ok(ExitReason::Check) => {
314                        info!("Recall failed: {}", self.source_location(rs));
315                        Err(EngineError::Check)
316                    }
317                    Ok(ExitReason::Panic) | Err(_) => {
318                        info!("Recall panicked: {}", self.source_location(rs));
319                        Err(EngineError::Panic)
320                    }
321                }
322            }
323        }
324    }
325
326    #[instrument(skip_all, fields(name = name))]
327    fn open_command<P>(
328        &self,
329        name: &str,
330        envelope: Envelope<'_>,
331        facts: &mut P,
332    ) -> Result<Struct, EngineError>
333    where
334        P: FactPerspective,
335    {
336        let facts = RefCell::new(facts);
337        let mut sink = NullSink;
338        let sink2 = RefCell::new(&mut sink);
339        let io = RefCell::new(VmPolicyIO::new(&facts, &sink2, &self.engine, &self.ffis));
340        let ctx = CommandContext::Open(OpenContext { name });
341        let mut rs = self.machine.create_run_state(&io, ctx);
342        let status = rs.call_open(name, envelope.into());
343        match status {
344            Ok(reason) => match reason {
345                ExitReason::Normal => {
346                    let v = rs.consume_return().map_err(|e| {
347                        error!("Could not pull envelope from stack: {e}");
348                        EngineError::InternalError
349                    })?;
350                    Ok(v.try_into().map_err(|e| {
351                        error!("Envelope is not a struct: {e}");
352                        EngineError::InternalError
353                    })?)
354                }
355                ExitReason::Yield => bug!("unexpected yield"),
356                ExitReason::Check => {
357                    info!("Check {}", self.source_location(&rs));
358                    Err(EngineError::Check)
359                }
360                ExitReason::Panic => {
361                    info!("Panicked {}", self.source_location(&rs));
362                    Err(EngineError::Check)
363                }
364            },
365            Err(e) => {
366                error!("\n{e}");
367                Err(EngineError::InternalError)
368            }
369        }
370    }
371}
372
373/// [`VmPolicy`]'s actions.
374#[derive(Clone, Debug, PartialEq, Eq)]
375pub struct VmAction<'a> {
376    /// The name of the action.
377    pub name: &'a str,
378    /// The arguments of the action.
379    pub args: Cow<'a, [Value]>,
380}
381
382/// A partial version of [`VmEffect`] containing only the data. Created by
383/// [`vm_effect!`] and used to compare only the name and fields against the full
384/// `VmEffect`.
385#[derive(Debug)]
386pub struct VmEffectData {
387    /// The name of the effect.
388    pub name: String,
389    /// The fields of the effect.
390    pub fields: Vec<KVPair>,
391}
392
393impl PartialEq<VmEffect> for VmEffectData {
394    fn eq(&self, other: &VmEffect) -> bool {
395        self.name == other.name && self.fields == other.fields
396    }
397}
398
399impl PartialEq<VmEffectData> for VmEffect {
400    fn eq(&self, other: &VmEffectData) -> bool {
401        self.name == other.name && self.fields == other.fields
402    }
403}
404
405/// [`VmPolicy`]'s effects.
406#[derive(Clone, Debug, PartialEq, Eq)]
407pub struct VmEffect {
408    /// The name of the effect.
409    pub name: String,
410    /// The fields of the effect.
411    pub fields: Vec<KVPair>,
412    /// The command ID that produced this effect
413    pub command: CommandId,
414    /// Was this produced from a recall block?
415    pub recalled: bool,
416}
417
418impl<E: aranya_crypto::Engine> Policy for VmPolicy<E> {
419    type Action<'a> = VmAction<'a>;
420    type Effect = VmEffect;
421    type Command<'a> = VmProtocol<'a>;
422
423    fn serial(&self) -> u32 {
424        // TODO(chip): Implement an actual serial number
425        0u32
426    }
427
428    #[instrument(skip_all)]
429    fn call_rule(
430        &self,
431        command: &impl Command,
432        facts: &mut impl FactPerspective,
433        sink: &mut impl Sink<Self::Effect>,
434        recall: CommandRecall,
435    ) -> Result<(), EngineError> {
436        let unpacked: VmProtocolData<'_> = postcard::from_bytes(command.bytes()).map_err(|e| {
437            error!("Could not deserialize: {e:?}");
438            EngineError::Read
439        })?;
440        let command_info = {
441            match unpacked {
442                VmProtocolData::Init {
443                    author_id,
444                    kind,
445                    serialized_fields,
446                    signature,
447                    ..
448                } => Some((
449                    Envelope {
450                        parent_id: CommandId::default(),
451                        author_id,
452                        command_id: command.id(),
453                        payload: Cow::Borrowed(serialized_fields),
454                        signature: Cow::Borrowed(signature),
455                    },
456                    kind,
457                    author_id,
458                )),
459                VmProtocolData::Basic {
460                    parent,
461                    kind,
462                    author_id,
463                    serialized_fields,
464                    signature,
465                } => Some((
466                    Envelope {
467                        parent_id: parent.id,
468                        author_id,
469                        command_id: command.id(),
470                        payload: Cow::Borrowed(serialized_fields),
471                        signature: Cow::Borrowed(signature),
472                    },
473                    kind,
474                    author_id,
475                )),
476                // Merges always pass because they're an artifact of the graph
477                _ => None,
478            }
479        };
480
481        if let Some((envelope, kind, author_id)) = command_info {
482            let command_struct = self.open_command(kind, envelope.clone(), facts)?;
483            let fields: Vec<KVPair> = command_struct
484                .fields
485                .into_iter()
486                .map(|(k, v)| KVPair::new(&k, v))
487                .collect();
488            let ctx = CommandContext::Policy(PolicyContext {
489                name: kind,
490                id: command.id().into(),
491                author: author_id,
492                version: CommandId::default().into(),
493            });
494            self.evaluate_rule(kind, fields.as_slice(), envelope, facts, sink, ctx, recall)?
495        }
496        Ok(())
497    }
498
499    #[instrument(skip_all, fields(name = action.name))]
500    fn call_action(
501        &self,
502        action: Self::Action<'_>,
503        facts: &mut impl Perspective,
504        sink: &mut impl Sink<Self::Effect>,
505    ) -> Result<(), EngineError> {
506        let VmAction { name, args } = action;
507
508        let parent = match facts.head_address()? {
509            Prior::None => None,
510            Prior::Single(id) => Some(id),
511            Prior::Merge(_, _) => bug!("cannot have a merge parent in call_action"),
512        };
513        // FIXME(chip): This is kind of wrong, but it avoids having to
514        // plumb Option<Id> into the VM and FFI
515        let ctx_parent = parent.unwrap_or_default();
516        let facts = Rc::new(RefCell::new(facts));
517        let sink = Rc::new(RefCell::new(sink));
518        let io = RefCell::new(VmPolicyIO::new(&facts, &sink, &self.engine, &self.ffis));
519        let ctx = CommandContext::Action(ActionContext {
520            name,
521            head_id: ctx_parent.id.into(),
522        });
523        {
524            let mut rs = self.machine.create_run_state(&io, ctx);
525            let mut exit_reason = match args {
526                Cow::Borrowed(args) => rs.call_action(name, args.iter().cloned()),
527                Cow::Owned(args) => rs.call_action(name, args),
528            }
529            .map_err(|e| {
530                error!("\n{e}");
531                EngineError::InternalError
532            })?;
533            loop {
534                match exit_reason {
535                    ExitReason::Normal => {
536                        // Action completed
537                        break;
538                    }
539                    ExitReason::Yield => {
540                        // Command was published.
541                        let command_struct: Struct = rs.stack.pop().map_err(|e| {
542                            error!("should have command struct: {e}");
543                            EngineError::InternalError
544                        })?;
545
546                        let fields = command_struct
547                            .fields
548                            .iter()
549                            .map(|(k, v)| KVPair::new(k, v.clone()));
550                        io.try_borrow_mut()
551                            .assume("should be able to borrow io")?
552                            .publish(command_struct.name.clone(), fields);
553
554                        let seal_ctx = rs.get_context().seal_from_action(&command_struct.name)?;
555                        let mut rs_seal = self.machine.create_run_state(&io, seal_ctx);
556                        match rs_seal
557                            .call_seal(&command_struct.name, &command_struct)
558                            .map_err(|e| {
559                                error!("Cannot seal command: {}", e);
560                                EngineError::Panic
561                            })? {
562                            ExitReason::Normal => (),
563                            r @ ExitReason::Yield
564                            | r @ ExitReason::Check
565                            | r @ ExitReason::Panic => {
566                                error!("Could not seal command: {}", r);
567                                return Err(EngineError::Panic);
568                            }
569                        }
570
571                        // Grab sealed envelope from stack
572                        let envelope_struct: Struct = rs_seal.stack.pop().map_err(|e| {
573                            error!("Expected a sealed envelope {e}");
574                            EngineError::InternalError
575                        })?;
576                        let envelope = Envelope::try_from(envelope_struct).map_err(|e| {
577                            error!("Malformed envelope: {e}");
578                            EngineError::InternalError
579                        })?;
580
581                        // The parent of a basic command should be the command that was added to the perspective on the previous
582                        // iteration of the loop
583                        let parent = match RefCell::borrow_mut(Rc::borrow(&facts)).head_address()? {
584                            Prior::None => None,
585                            Prior::Single(id) => Some(id),
586                            Prior::Merge(_, _) => bug!("cannot have a merge parent in call_action"),
587                        };
588
589                        let data = match parent {
590                            None => VmProtocolData::Init {
591                                // TODO(chip): where does the policy value come from?
592                                policy: 0u64.to_le_bytes(),
593                                author_id: envelope.author_id,
594                                kind: &command_struct.name,
595                                serialized_fields: &envelope.payload,
596                                signature: &envelope.signature,
597                            },
598                            Some(parent) => VmProtocolData::Basic {
599                                author_id: envelope.author_id,
600                                parent,
601                                kind: &command_struct.name,
602                                serialized_fields: &envelope.payload,
603                                signature: &envelope.signature,
604                            },
605                        };
606                        let wrapped = postcard::to_allocvec(&data)?;
607                        let new_command = VmProtocol::new(
608                            &wrapped,
609                            envelope.command_id,
610                            data,
611                            Arc::clone(&self.priority_map),
612                        );
613
614                        self.call_rule(
615                            &new_command,
616                            *RefCell::borrow_mut(Rc::borrow(&facts)),
617                            *RefCell::borrow_mut(Rc::borrow(&sink)),
618                            CommandRecall::None,
619                        )?;
620                        RefCell::borrow_mut(Rc::borrow(&facts))
621                            .add_command(&new_command)
622                            .map_err(|e| {
623                                error!("{e}");
624                                EngineError::Write
625                            })?;
626
627                        // After publishing a new command, the RunState's context must be updated to reflect the new head
628                        rs.update_context_with_new_head(new_command.id().into())?;
629
630                        // Resume action after last Publish
631                        exit_reason = rs.run().map_err(|e| {
632                            error!("{e}");
633                            EngineError::InternalError
634                        })?;
635                    }
636                    ExitReason::Check => {
637                        info!("Check {}", self.source_location(&rs));
638                        return Err(EngineError::Check);
639                    }
640                    ExitReason::Panic => {
641                        info!("Panicked {}", self.source_location(&rs));
642                        return Err(EngineError::Panic);
643                    }
644                };
645            }
646        }
647
648        Ok(())
649    }
650
651    fn merge<'a>(
652        &self,
653        target: &'a mut [u8],
654        ids: MergeIds,
655    ) -> Result<Self::Command<'a>, EngineError> {
656        let (left, right) = ids.into();
657        let c = VmProtocolData::Merge { left, right };
658        let data = postcard::to_slice(&c, target).map_err(|e| {
659            error!("{e}");
660            EngineError::Write
661        })?;
662        let id = CommandId::hash_for_testing_only(data);
663        Ok(VmProtocol::new(data, id, c, Arc::clone(&self.priority_map)))
664    }
665}
666
667impl fmt::Display for VmAction<'_> {
668    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
669        let mut d = f.debug_tuple(self.name);
670        for arg in self.args.as_ref() {
671            d.field(&DebugViaDisplay(arg));
672        }
673        d.finish()
674    }
675}
676
677impl fmt::Display for VmEffect {
678    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
679        let mut d = f.debug_struct(&self.name);
680        for field in &self.fields {
681            d.field(field.key(), &DebugViaDisplay(field.value()));
682        }
683        d.finish()
684    }
685}
686
687/// Implements `Debug` via `T`'s `Display` impl.
688struct DebugViaDisplay<T>(T);
689
690impl<T: fmt::Display> fmt::Debug for DebugViaDisplay<T> {
691    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
692        write!(f, "{}", self.0)
693    }
694}