essential_state_read_vm/
lib.rs

1//! The essential state read VM implementation.
2//!
3//! ## Reading State
4//!
5//! The primary entrypoint for this crate is the [`Vm` type][Vm].
6//!
7//! The `Vm` allows for executing operations that read state and apply any
8//! necessary operations in order to form the final, expected state slot layout
9//! within the VM's [`Memory`]. The `Vm`'s memory can be accessed directly
10//! from the `Vm`, or the `Vm` can be consumed and state slots returned with
11//! [`Vm::into_state_slots`].
12//!
13//! ## Executing Ops
14//!
15//! There are three primary methods available for executing operations:
16//!
17//! - [`Vm::exec_ops`]
18//! - [`Vm::exec_bytecode`]
19//! - [`Vm::exec_bytecode_iter`]
20//!
21//! Each have slightly different performance implications, so be sure to read
22//! the docs before selecting a method.
23//!
24//! ## Execution Future
25//!
26//! The `Vm::exec_*` functions all return `Future`s that not only yield on
27//! async operations, but yield based on a user-specified gas limit too. See the
28//! [`ExecFuture`] docs for further details on the implementation.
29#![deny(missing_docs, unsafe_code)]
30
31use constraint::{ProgramControlFlow, Repeat};
32#[doc(inline)]
33pub use error::{OpAsyncResult, OpResult, OpSyncResult, StateMemoryResult, StateReadResult};
34use error::{OpError, OpSyncError, StateMemoryError, StateReadError};
35use essential_constraint_vm::LazyCache;
36#[doc(inline)]
37pub use essential_constraint_vm::{
38    self as constraint, Access, OpAccess, SolutionAccess, Stack, StateSlotSlice, StateSlots,
39};
40#[doc(inline)]
41pub use essential_state_asm as asm;
42use essential_state_asm::Op;
43pub use essential_types as types;
44use essential_types::{ContentAddress, Word};
45#[doc(inline)]
46pub use future::ExecFuture;
47pub use state_memory::StateMemory;
48pub use state_read::StateRead;
49
50pub mod error;
51mod future;
52mod state_memory;
53mod state_read;
54
55/// The operation execution state of the State Read VM.
56#[derive(Debug, Default, PartialEq)]
57pub struct Vm {
58    /// The program counter, i.e. index of the current operation within the program.
59    pub pc: usize,
60    /// The stack machine.
61    pub stack: Stack,
62    /// The memory for temporary storage of words.
63    pub temp_memory: essential_constraint_vm::Memory,
64    /// The repeat stack.
65    pub repeat: Repeat,
66    /// Lazily cached data for the VM.
67    pub cache: LazyCache,
68    /// The state memory that will be written to by this program.
69    pub state_memory: StateMemory,
70}
71
72/// Unit used to measure gas.
73pub type Gas = u64;
74
75/// Shorthand for the `BytecodeMapped` type representing a mapping to/from state read [`Op`]s.
76pub type BytecodeMapped<Bytes = Vec<u8>> = constraint::BytecodeMapped<Op, Bytes>;
77/// Shorthand for the `BytecodeMappedSlice` type for mapping [`Op`]s.
78pub type BytecodeMappedSlice<'a> = constraint::BytecodeMappedSlice<'a, Op>;
79/// Shorthand for the `BytecodeMappedLazy` type for mapping [`Op`]s.
80pub type BytecodeMappedLazy<I> = constraint::BytecodeMappedLazy<Op, I>;
81
82/// Gas limits.
83#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
84pub struct GasLimit {
85    /// The amount that may be spent synchronously until the execution future should yield.
86    pub per_yield: Gas,
87    /// The total amount of gas that may be spent.
88    pub total: Gas,
89}
90
91/// Distinguish between sync and async ops to ease `Future` implementation.
92#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq, PartialOrd, Ord)]
93pub(crate) enum OpKind {
94    /// Operations that yield immediately.
95    Sync(OpSync),
96    /// Operations returning a future.
97    Async(OpAsync),
98}
99
100/// The contract of operations performed synchronously.
101#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq, PartialOrd, Ord)]
102pub(crate) enum OpSync {
103    /// All operations available to the constraint checker.
104    Constraint(asm::Constraint),
105    /// Operations for interacting with mutable state slots.
106    StateMemory(asm::StateMemory),
107}
108
109/// The contract of operations that are performed asynchronously.
110#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq, PartialOrd, Ord)]
111pub(crate) enum OpAsync {
112    /// Read a range of values from state starting at the key.
113    StateReadKeyRange,
114    /// Read a range of values from external state starting at the key.
115    StateReadKeyRangeExt,
116}
117
118/// A mapping from an operation to its gas cost.
119pub trait OpGasCost {
120    /// The gas cost associated with the given op.
121    fn op_gas_cost(&self, op: &Op) -> Gas;
122}
123
124impl GasLimit {
125    /// The default value used for the `per_yield` limit.
126    // TODO: Adjust this to match recommended poll time limit on supported validator
127    // hardware.
128    pub const DEFAULT_PER_YIELD: Gas = 4_096;
129
130    /// Unlimited gas limit with default gas-per-yield.
131    pub const UNLIMITED: Self = Self {
132        per_yield: Self::DEFAULT_PER_YIELD,
133        total: Gas::MAX,
134    };
135}
136
137impl Vm {
138    /// Execute the given operations from the current state of the VM.
139    ///
140    /// Upon reaching a `Halt` operation or reaching the end of the operation
141    /// sequence, returns the gas spent and the `Vm` will be left in the
142    /// resulting state.
143    ///
144    /// This is a wrapper around [`Vm::exec`] that expects operation access in
145    /// the form of a `&[Op]`.
146    ///
147    /// If memory bloat is a concern, consider using the [`Vm::exec_bytecode`]
148    /// or [`Vm::exec_bytecode_iter`] methods which allow for providing a more
149    /// compact representation of the operations in the form of mapped bytecode.
150    pub async fn exec_ops<'a, S>(
151        &mut self,
152        ops: &[Op],
153        access: Access<'a>,
154        state_read: &S,
155        op_gas_cost: &impl OpGasCost,
156        gas_limit: GasLimit,
157    ) -> Result<Gas, StateReadError<S::Error>>
158    where
159        S: StateRead,
160    {
161        self.exec(access, state_read, ops, op_gas_cost, gas_limit)
162            .await
163    }
164
165    /// Execute the given mapped bytecode from the current state of the VM.
166    ///
167    /// Upon reaching a `Halt` operation or reaching the end of the operation
168    /// sequence, returns the gas spent and the `Vm` will be left in the
169    /// resulting state.
170    ///
171    /// This is a wrapper around [`Vm::exec`] that expects operation access in
172    /// the form of [`&BytecodeMapped`][BytecodeMapped].
173    ///
174    /// This can be a more memory efficient alternative to [`Vm::exec_ops`] due
175    /// to the compact representation of operations in the form of bytecode and
176    /// indices.
177    pub async fn exec_bytecode<'a, S, B>(
178        &mut self,
179        bytecode_mapped: &BytecodeMapped<B>,
180        access: Access<'a>,
181        state_read: &S,
182        op_gas_cost: &impl OpGasCost,
183        gas_limit: GasLimit,
184    ) -> Result<Gas, StateReadError<S::Error>>
185    where
186        S: StateRead,
187        B: core::ops::Deref<Target = [u8]>,
188    {
189        self.exec(access, state_read, bytecode_mapped, op_gas_cost, gas_limit)
190            .await
191    }
192
193    /// Execute the given bytecode from the current state of the VM.
194    ///
195    /// Upon reaching a `Halt` operation or reaching the end of the operation
196    /// sequence, returns the gas spent and the `Vm` will be left in the
197    /// resulting state.
198    ///
199    /// The given bytecode will be mapped lazily during execution. This
200    /// can be more efficient than pre-mapping the bytecode and using
201    /// [`Vm::exec_bytecode`] in the case that execution may fail early.
202    ///
203    /// However, successful execution still requires building the full
204    /// [`BytecodeMapped`] instance internally. So if bytecode has already been
205    /// mapped, [`Vm::exec_bytecode`] should be preferred.
206    pub async fn exec_bytecode_iter<'a, S, I>(
207        &mut self,
208        bytecode_iter: I,
209        access: Access<'a>,
210        state_read: &S,
211        op_gas_cost: &impl OpGasCost,
212        gas_limit: GasLimit,
213    ) -> Result<Gas, StateReadError<S::Error>>
214    where
215        S: StateRead,
216        I: IntoIterator<Item = u8>,
217        I::IntoIter: Unpin,
218    {
219        let bytecode_lazy = BytecodeMappedLazy::new(bytecode_iter);
220        self.exec(access, state_read, bytecode_lazy, op_gas_cost, gas_limit)
221            .await
222    }
223
224    /// Execute over the given operation access from the current state of the VM.
225    ///
226    /// Upon reaching a `Halt` operation or reaching the end of the operation
227    /// sequence, returns the gas spent and the `Vm` will be left in the
228    /// resulting state.
229    ///
230    /// The type requirements for the `op_access` argument can make this
231    /// finicky to use directly. You may prefer one of the convenience methods:
232    ///
233    /// - [`Vm::exec_ops`]
234    /// - [`Vm::exec_bytecode`]
235    /// - [`Vm::exec_bytecode_iter`]
236    pub async fn exec<'a, S, OA>(
237        &mut self,
238        access: Access<'a>,
239        state_read: &S,
240        op_access: OA,
241        op_gas_cost: &impl OpGasCost,
242        gas_limit: GasLimit,
243    ) -> Result<Gas, StateReadError<S::Error>>
244    where
245        S: StateRead,
246        OA: OpAccess<Op = Op> + Unpin,
247        OA::Error: Into<OpError<S::Error>>,
248    {
249        future::exec(self, access, state_read, op_access, op_gas_cost, gas_limit).await
250    }
251
252    /// Consumes the `Vm` and returns the read state slots.
253    ///
254    /// The returned slots correspond directly with the current memory content.
255    pub fn into_state_slots(self) -> Vec<Vec<Word>> {
256        self.state_memory.into()
257    }
258}
259
260impl From<Op> for OpKind {
261    fn from(op: Op) -> Self {
262        match op {
263            Op::Constraint(op) => OpKind::Sync(OpSync::Constraint(op)),
264            Op::StateMemory(op) => OpKind::Sync(OpSync::StateMemory(op)),
265            Op::KeyRange => OpKind::Async(OpAsync::StateReadKeyRange),
266            Op::KeyRangeExtern => OpKind::Async(OpAsync::StateReadKeyRangeExt),
267        }
268    }
269}
270
271impl<F> OpGasCost for F
272where
273    F: Fn(&Op) -> Gas,
274{
275    fn op_gas_cost(&self, op: &Op) -> Gas {
276        (*self)(op)
277    }
278}
279
280/// Step forward the VM by a single synchronous operation.
281///
282/// Returns a `Some(usize)` representing the new program counter resulting from
283/// this step, or `None` in the case that execution has halted.
284pub(crate) fn step_op_sync(op: OpSync, access: Access, vm: &mut Vm) -> OpSyncResult<Option<usize>> {
285    match op {
286        OpSync::Constraint(op) => {
287            let Vm {
288                stack,
289                repeat,
290                pc,
291                temp_memory,
292                cache,
293                ..
294            } = vm;
295            match constraint::step_op(access, op, stack, temp_memory, *pc, repeat, cache)? {
296                Some(ProgramControlFlow::Pc(pc)) => return Ok(Some(pc)),
297                Some(ProgramControlFlow::Halt) => return Ok(None),
298                None => (),
299            }
300        }
301        OpSync::StateMemory(op) => step_op_state_slots(op, &mut *vm)?,
302    }
303    // Every operation besides control flow steps forward program counter by 1.
304    let new_pc = vm.pc.checked_add(1).ok_or(OpSyncError::PcOverflow)?;
305    Ok(Some(new_pc))
306}
307
308/// Step forward state reading by the given state slot operation.
309pub(crate) fn step_op_state_slots(op: asm::StateMemory, vm: &mut Vm) -> OpSyncResult<()> {
310    match op {
311        asm::StateMemory::AllocSlots => {
312            state_memory::alloc_slots(&mut vm.stack, &mut vm.state_memory)
313        }
314        asm::StateMemory::Truncate => state_memory::truncate(&mut vm.stack, &mut vm.state_memory),
315        asm::StateMemory::Length => state_memory::length(&mut vm.stack, &vm.state_memory),
316        asm::StateMemory::ValueLen => state_memory::value_len(&mut vm.stack, &vm.state_memory),
317        asm::StateMemory::Load => state_memory::load(&mut vm.stack, &vm.state_memory),
318        asm::StateMemory::Store => state_memory::store(&mut vm.stack, &mut vm.state_memory),
319    }
320}