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}