Skip to main content

corevm_engine/
accumulate.rs

1use crate::hash_encoded;
2use alloc::{borrow::Cow, vec::Vec};
3use codec::{Decode, DecodeAll, Encode};
4use corevm_host::{
5	fs, CoreVmInstruction, CoreVmOutput, PageInfo, PageNum, Range, RangeSet, StorageKey, VmSpec,
6	VmState,
7};
8use jam_types::{
9	AccumulateItem, Balance, Hash, Memo, SegmentTreeRoot, ServiceId, SignedGas, Slot, UnsignedGas,
10	WorkError, WorkOutput,
11};
12use log::debug;
13
14mod messages;
15
16const ALL_STORAGE_KEYS_WITHOUT_PARAMS: &[StorageKey] = {
17	use StorageKey::*;
18	&[
19		Gas,
20		StateHash,
21		VmSpec,
22		VideoMode,
23		AudioMode,
24		ExecEnvRef,
25		Owner,
26		StoredPages,
27		IncomingServiceMessages,
28		OutgoingServiceMessages,
29	]
30};
31
32const _: () = assert!(ALL_STORAGE_KEYS_WITHOUT_PARAMS.len() == StorageKey::COUNT_WITHOUT_PARAMS);
33
34/// Storage keys that should be removed on VM reset.
35const REMOVE_ON_RESET_KEYS: &[StorageKey] = {
36	use StorageKey::*;
37	&[VmSpec, VideoMode, AudioMode, StoredPages, IncomingServiceMessages, OutgoingServiceMessages]
38};
39
40/// Storage keys that should _not_ be removed on VM reset.
41const DO_NOT_REMOVE_ON_RESET_KEYS: &[StorageKey] = {
42	use StorageKey::*;
43	&[Gas, ExecEnvRef, StateHash, Owner]
44};
45
46const _: () = assert!(
47	REMOVE_ON_RESET_KEYS.len() + DO_NOT_REMOVE_ON_RESET_KEYS.len() ==
48		StorageKey::COUNT_WITHOUT_PARAMS
49);
50
51/// Operations that [`AccumulateEngine`] uses during accumulation.
52///
53/// Each operation corresponds to a JAM host-call.
54pub trait AccumulateOps {
55	/// Implemenatation-specific error type.
56	type Error: core::fmt::Debug;
57
58	/// Read value under the specified key from the storage of the specified service.
59	fn get(&self, id: ServiceId, key: &StorageKey) -> Option<Cow<'_, [u8]>>;
60
61	/// Write value under the specified key to the storage.
62	fn set(&mut self, key: StorageKey, value: Cow<'_, [u8]>) -> Result<(), Self::Error>;
63
64	/// Remove value under the specified key from the storage of the service being accumulated.
65	///
66	/// Returns `true` if the value was present in the storage before the removal.
67	fn remove(&mut self, key: &StorageKey) -> bool;
68
69	/// Same as [`set`](Self::set) but encodes the provided value using JAM codec.
70	fn set_typed(&mut self, key: StorageKey, value: &impl Encode) -> Result<(), Self::Error> {
71		value.using_encoded(|bytes| self.set(key, bytes.into()))
72	}
73
74	/// Same as [`get`](Self::get) but decodes the retrieved value using JAM codec.
75	fn get_typed<T: Decode>(
76		&self,
77		id: ServiceId,
78		key: &StorageKey,
79	) -> Result<Option<T>, codec::Error> {
80		self.get(id, key).map(|bytes| T::decode_all(&mut bytes.as_ref())).transpose()
81	}
82
83	/// Get min. memo gas of the specified service.
84	fn min_memo_gas(&self, service_id: ServiceId) -> Option<UnsignedGas>;
85
86	/// Transfer data and/or funds to another service.
87	fn transfer(
88		&self,
89		destination: ServiceId,
90		amount: Balance,
91		gas_limit: UnsignedGas,
92		memo: &Memo,
93	) -> Result<(), Self::Error>;
94
95	/// Same as [`transfer`](Self::transfer) but encodes the provided memo using JAM codec.
96	///
97	/// Panics if the encoded value doesn't fit into [`Memo`].
98	fn transfer_typed(
99		&self,
100		destination: ServiceId,
101		amount: Balance,
102		gas_limit: UnsignedGas,
103		memo: &impl Encode,
104	) -> Result<(), Self::Error> {
105		let mut raw_memo = Memo::zero();
106		memo.encode_to(&mut SliceOutput(&mut raw_memo[..]));
107		self.transfer(destination, amount, gas_limit, &raw_memo)
108	}
109
110	/// Reset the privileged services.
111	fn bless<'a>(
112		&mut self,
113		manager: ServiceId,
114		assigner: ServiceId,
115		designator: ServiceId,
116		registrar: ServiceId,
117		always_acc: impl IntoIterator<Item = &'a (ServiceId, UnsignedGas)>,
118	);
119
120	/// Zombify the service.
121	fn zombify(&mut self, ejector: ServiceId);
122}
123
124impl<A: AccumulateOps + ?Sized> AccumulateOps for &mut A {
125	type Error = A::Error;
126
127	fn get(&self, service_id: ServiceId, key: &StorageKey) -> Option<Cow<'_, [u8]>> {
128		AccumulateOps::get(*self, service_id, key)
129	}
130
131	fn set(&mut self, key: StorageKey, value: Cow<'_, [u8]>) -> Result<(), Self::Error> {
132		AccumulateOps::set(*self, key, value)
133	}
134
135	fn remove(&mut self, key: &StorageKey) -> bool {
136		AccumulateOps::remove(*self, key)
137	}
138
139	fn min_memo_gas(&self, service_id: ServiceId) -> Option<UnsignedGas> {
140		AccumulateOps::min_memo_gas(*self, service_id)
141	}
142
143	fn transfer(
144		&self,
145		destination: ServiceId,
146		amount: Balance,
147		gas_limit: UnsignedGas,
148		memo: &Memo,
149	) -> Result<(), Self::Error> {
150		AccumulateOps::transfer(*self, destination, amount, gas_limit, memo)
151	}
152
153	fn bless<'a>(
154		&mut self,
155		manager: ServiceId,
156		assigner: ServiceId,
157		designator: ServiceId,
158		registrar: ServiceId,
159		always_acc: impl IntoIterator<Item = &'a (ServiceId, UnsignedGas)>,
160	) {
161		AccumulateOps::bless(*self, manager, assigner, designator, registrar, always_acc);
162	}
163
164	fn zombify(&mut self, ejector: ServiceId) {
165		AccumulateOps::zombify(*self, ejector);
166	}
167}
168
169/// An engine that drives execution of `accumulate` and `on_transfer` CoreVM service entry points.
170pub struct AccumulateEngine<A: AccumulateOps> {
171	ops: A,
172	/// The current slot.
173	slot: Slot,
174	/// Id of the service that is being accumulated.
175	service_id: ServiceId,
176}
177
178impl<A: AccumulateOps> AccumulateEngine<A> {
179	/// Create new engine with the specified accumulation API implementation.
180	pub fn new(ops: A, slot: Slot, service_id: ServiceId) -> Self {
181		Self { ops, slot, service_id }
182	}
183
184	/// Accumulate the specified items.
185	///
186	/// Returns accumulation result for each item.
187	pub fn run(
188		&mut self,
189		items: &[AccumulateItem],
190	) -> Vec<Result<(), AccumulationError<A::Error>>> {
191		items
192			.iter()
193			.map(|item| match item {
194				AccumulateItem::WorkItem(item) =>
195					self.accumulate(item.result.as_ref(), item.exports_root),
196				AccumulateItem::Transfer(item) => self.transfer(item.source, item.memo),
197			})
198			.collect()
199	}
200
201	/// Get the underlying accumulation API implementation.
202	pub fn into_inner(self) -> A {
203		self.ops
204	}
205
206	fn accumulate(
207		&mut self,
208		result: Result<&WorkOutput, &WorkError>,
209		exports_root: SegmentTreeRoot,
210	) -> Result<(), AccumulationError<A::Error>> {
211		use AccumulationError::*;
212		let output = result.map_err(|e| Work(e.clone()))?;
213		let output = CoreVmOutput::decode_all(&mut &output[..])?;
214
215		if self.get_typed::<fs::BlockRef>(&StorageKey::ExecEnvRef)? != Some(output.exec_ref) {
216			return Err(ExecEnvRefMismatch);
217		}
218
219		// Update VM state hash.
220		if self.get_typed::<Hash>(&StorageKey::StateHash)? != Some(output.old_hash) {
221			return Err(PriorStateMismatch);
222		}
223		self.set_typed(StorageKey::StateHash, &output.new_hash)?;
224		let mut stored_pages =
225			self.get_typed::<RangeSet>(&StorageKey::StoredPages)?.unwrap_or_default();
226		// Check imported pages' hashes.
227		for (page, our_hash) in output.touched_imported_pages.iter() {
228			let key = StorageKey::PageInfo(*page);
229			let their_hash = self.get_typed::<PageInfo>(&key)?.ok_or(NoSuchPage(*page))?.hash;
230			if &their_hash != our_hash {
231				return Err(PageHashMismatch);
232			}
233		}
234		// Update memory pages' metadata.
235		let mut export_index = 0;
236		for (page, hash) in output.updated_pages.iter() {
237			let key = StorageKey::PageInfo(*page);
238			if hash == &[0; 32] {
239				// The page was freed.
240				self.get_typed::<PageInfo>(&key)?.ok_or(NoSuchPage(*page))?;
241				stored_pages.remove(&Range::new(page.0, page.0 + 1));
242				self.ops.remove(&key);
243			} else {
244				stored_pages.insert(Range::new(page.0, page.0 + 1));
245				let info = PageInfo { hash: *hash, exports_root, export_index };
246				self.set_typed(key, &info)?;
247				export_index += 1;
248			}
249		}
250		self.set_typed(StorageKey::StoredPages, &stored_pages)?;
251		// Update video mode.
252		if let Some(ref mode) = output.vm_state.video {
253			self.set_typed(StorageKey::VideoMode, mode)?;
254		}
255		// Update audio mode.
256		if let Some(ref mode) = output.vm_state.audio {
257			self.set_typed(StorageKey::AudioMode, mode)?;
258		}
259		self.remove_processed_incoming_service_messages(&output.processed_service_messages)?;
260		self.send_new_outgoing_service_messages_and_remove_old(output.outgoing_messages)?;
261		// Store VM output specification.
262		let spec = VmSpec { exports_root, output: output.vm_output, state: output.vm_state };
263		self.set_typed(StorageKey::VmSpec, &spec)?;
264
265		Ok(())
266	}
267
268	fn transfer(
269		&mut self,
270		source: ServiceId,
271		memo: Memo,
272	) -> Result<(), AccumulationError<A::Error>> {
273		use AccumulationError::*;
274		let instr = CoreVmInstruction::decode(&mut &memo[..])?;
275		let owner = self.get_typed::<ServiceId>(&StorageKey::Owner)?;
276		if let Some(owner) = owner {
277			if owner != source {
278				return Err(NotTheOwner(owner, source));
279			}
280		}
281		match instr {
282			CoreVmInstruction::Reset { gas, exec_ref } => {
283				if owner.is_none() {
284					debug!("Setting owner to {source:x}");
285					self.set_typed(StorageKey::Owner, &source)?;
286				}
287				self.reset(gas, exec_ref)?;
288			},
289			CoreVmInstruction::SetOwner(owner) => {
290				debug!("Setting owner to {owner:x}");
291				self.set_typed(StorageKey::Owner, &owner)?;
292			},
293			CoreVmInstruction::PushServiceMessage(message) => {
294				self.push_incoming_service_message(message)?;
295			},
296			CoreVmInstruction::Destroy { ejector } => {
297				self.destroy()?;
298				self.ops.bless(ejector, ejector, ejector, ejector, []);
299				self.ops.zombify(ejector);
300			},
301		}
302		Ok(())
303	}
304
305	/// Reset the VM.
306	///
307	/// Sets `ExecEnv` and `Gas`; resets everything else.
308	fn reset(
309		&mut self,
310		gas: SignedGas,
311		exec_ref: fs::BlockRef,
312	) -> Result<(), AccumulationError<A::Error>> {
313		debug!("Resetting VM: gas = {gas}, exec = {exec_ref}");
314		let state_hash = hash_encoded(VmState::initial());
315		self.set_typed(StorageKey::Gas, &gas)?;
316		self.set_typed(StorageKey::ExecEnvRef, &exec_ref)?;
317		self.set_typed(StorageKey::StateHash, &state_hash)?;
318		self.remove_memory_pages()?;
319		self.remove_all_incoming_service_messages()?;
320		self.remove_all_outgoing_service_messages()?;
321		// We should remove every key except the ones that we've overwritten above and except
322		// Owner.
323		for key in REMOVE_ON_RESET_KEYS {
324			self.ops.remove(key);
325		}
326		Ok(())
327	}
328
329	/// Destroy the VM.
330	///
331	/// Removes everything from the storage.
332	fn destroy(&mut self) -> Result<(), AccumulationError<A::Error>> {
333		self.remove_memory_pages()?;
334		self.remove_all_incoming_service_messages()?;
335		self.remove_all_outgoing_service_messages()?;
336		for key in ALL_STORAGE_KEYS_WITHOUT_PARAMS.iter() {
337			self.ops.remove(key);
338		}
339		Ok(())
340	}
341
342	fn remove_memory_pages(&mut self) -> Result<(), AccumulationError<A::Error>> {
343		let stored_pages =
344			self.get_typed::<RangeSet>(&StorageKey::StoredPages)?.unwrap_or_default();
345		for range in stored_pages.as_slice().iter() {
346			for page in range.start..range.end {
347				self.ops.remove(&StorageKey::PageInfo(PageNum(page)));
348			}
349		}
350		Ok(())
351	}
352
353	fn set_typed(
354		&mut self,
355		key: StorageKey,
356		value: &impl Encode,
357	) -> Result<(), AccumulationError<A::Error>> {
358		self.ops.set_typed(key, value).map_err(AccumulationError::Api)
359	}
360
361	fn get_typed<T: Decode>(&self, key: &StorageKey) -> Result<Option<T>, codec::Error> {
362		self.ops.get_typed::<T>(self.service_id, key)
363	}
364}
365
366/// Accumulation error.
367#[derive(Debug, thiserror::Error)]
368pub enum AccumulationError<E: core::fmt::Debug> {
369	#[error("Accumulation API error: {0:?}")]
370	Api(E),
371	#[error("Work error: {0:?}")]
372	Work(WorkError),
373	#[error("JAM codec error: {0:?}")]
374	Codec(codec::Error),
375	#[error("Prior VM state mismatch")]
376	PriorStateMismatch,
377	#[error("Page {0} not found in storage")]
378	NoSuchPage(PageNum),
379	#[error("Page hash mismatch")]
380	PageHashMismatch,
381	#[error("ExecEnvRef mismatch")]
382	ExecEnvRefMismatch,
383	#[error("Processed message doesn't match the message in the queue")]
384	MessageMismatch,
385	#[error("Invalid message")]
386	InvalidMessage,
387	#[error("Unknown service {0:x}")]
388	UnknownService(ServiceId),
389	#[error("Transfer source is not the owner of the service: actual owner {0:x}, transfer source {1:x}")]
390	NotTheOwner(ServiceId, ServiceId),
391}
392
393impl<E: core::fmt::Debug> From<codec::Error> for AccumulationError<E> {
394	fn from(e: codec::Error) -> Self {
395		Self::Codec(e)
396	}
397}
398
399struct SliceOutput<'a>(&'a mut [u8]);
400
401impl codec::Output for SliceOutput<'_> {
402	fn write(&mut self, src: &[u8]) {
403		let (dst, rest) = core::mem::take(&mut self.0).split_at_mut(src.len());
404		dst.copy_from_slice(src);
405		self.0 = rest;
406	}
407}