Skip to main content

corevm_engine/
engine.rs

1use crate::{
2	hash_encoded, BoundedInput, BoundedWorkOutput, FatalError, FsNode, HostCallError,
3	HostCallHandler, InitError, InnerVm, InvokeArgs, KernelContext, Lookup, MemoryMap, OpenedFile,
4	OuterVm, OutputBuffers, Reg, RunError, TouchError,
5};
6use alloc::vec::Vec;
7use codec::DecodeAll;
8use corevm_codec::video;
9use corevm_host::{
10	fs, CoreVmExtrinsics, CoreVmOutput, CoreVmPayload, ExecEnv, Outcome, OutputStream, PageNum,
11	Range, VmOutput, PAGE_SIZE,
12};
13use jam_pvm_common::InvokeOutcome;
14use jam_types::{max_exports, VecSet};
15use log::{debug, trace};
16use polkakernel::{KernelState, Machine};
17use polkavm::{ArcBytes, RETURN_TO_HOST};
18
19/// Parameters derived from work package.
20pub struct WorkPackageParams {
21	/// Total encoded size of the package.
22	pub encoded_size: u32,
23	/// The size of the authorizer output.
24	pub auth_output_size: u32,
25	/// The number of exports.
26	pub export_count: u16,
27}
28
29/// This structure records which portions of the input were read/accessed by the guest.
30#[derive(Debug)]
31pub struct InputStats {
32	/// Imported memory pages that were accessed by the guest.
33	pub accessed_imported_pages: VecSet<PageNum>,
34	/// The number of inter-service messages read by the guest.
35	pub num_service_messages_read: u32,
36	/// The number of input chunks read.
37	pub num_input_chunks_read: u32,
38}
39
40/// An engine that drives execution of `refine` CoreVM service entry point.
41pub struct Engine<O: OuterVm> {
42	pub(crate) args: InvokeArgs,
43	pub(crate) outer_vm: O,
44	pub(crate) inner_vm: O::InnerVm,
45	pub(crate) exec: ExecEnv,
46	/// The number of video frames produced by the guest so far.
47	pub(crate) num_video_frames: u64,
48	/// In bytes.
49	/// Total size of audio frames produced by the guest so far in bytes.
50	pub(crate) num_audio_bytes: u64,
51	/// The current timestamp within the time slot, measured as the no. of milliseconds since the
52	/// start of the slot.
53	///
54	/// Incremented when a video frame or audio samples are yielded by the program. Never goes
55	/// backwards.
56	pub(crate) timestamp: u64,
57	pub(crate) memory_map: MemoryMap,
58	/// Should be equal to `WorkItem::export_count`.
59	pub(crate) export_count: u16,
60	/// Guest program's output streams.
61	pub(crate) output: OutputBuffers,
62	pub(crate) ro_data: ArcBytes,
63	pub(crate) rw_data: ArcBytes,
64	pub(crate) work_output: BoundedWorkOutput,
65	pub(crate) host_call_handlers: Vec<HostCallHandler<O>>,
66	/// Empty when logs are disabled.
67	pub(crate) host_call_names: Vec<&'static str>,
68	pub(crate) input: BoundedInput,
69	/// Video output encoder.
70	pub(crate) video_encoder: Option<video::Encoder>,
71}
72
73impl<O: OuterVm> Engine<O> {
74	pub fn new(
75		payload: CoreVmPayload,
76		extrinsics: CoreVmExtrinsics,
77		work_package_params: WorkPackageParams,
78		mut outer_vm: O,
79	) -> Result<Self, InitError> {
80		let CoreVmPayload { gas, vm_state, exec_ref } = payload;
81		let exec = outer_vm.read_file(&exec_ref)?;
82		let exec = ExecEnv::decode_all(&mut &exec[..]).map_err(|_| InitError::ExecBlob)?;
83		let export_count = work_package_params.export_count;
84		let auth_output_len = work_package_params.auth_output_size;
85		let mut program_counter = vm_state.program_counter;
86		let program = {
87			let bytes = outer_vm.read_file(&exec.program)?;
88			let corevm_blob = jam_program_blob_common::CoreVmProgramBlob::from_bytes(&bytes)
89				.ok_or(InitError::ProgramBlob)?;
90			polkavm::ProgramParts::from_bytes(corevm_blob.pvm_blob.into())?
91		};
92		let polkavm_memory_map = polkavm::MemoryMapBuilder::new(PAGE_SIZE)
93			.ro_data_size(program.ro_data_size)
94			.rw_data_size(program.rw_data_size)
95			.stack_size(program.stack_size)
96			.build()
97			.map_err(|_| InitError::ProgramBlob)?;
98		let code_and_jump_table = program.code_and_jump_table.clone();
99		let ro_data = program.ro_data.clone();
100		let rw_data = program.rw_data.clone();
101		let program_blob =
102			polkavm::ProgramBlob::from_parts(program).map_err(|_| InitError::ProgramBlob)?;
103		let (host_call_handlers, host_call_names) = Self::create_host_call_handlers(&program_blob);
104		// Should include program hash and pages' hash.
105		let old_hash = hash_encoded(&vm_state);
106		let args = InvokeArgs { regs: vm_state.regs, gas };
107		let initial_program_run = program_counter == 0;
108		let mut libc = false;
109		if initial_program_run {
110			debug!("This is initial program run");
111			program_counter = program_blob
112				.exports()
113				.find(|export| {
114					let name = export.symbol().as_bytes();
115					if name == b"main" {
116						debug!("Found `main` (no libc) entry point");
117						return true;
118					}
119					if name == b"_pvm_start" {
120						debug!("Found `_pvm_start` (libc) entry point");
121						libc = true;
122						return true;
123					}
124					false
125				})
126				.expect("Neither `main` nor `_pvm_start` entry point found")
127				.program_counter()
128				.0
129				.into();
130		}
131		let input = BoundedInput::new(work_package_params.encoded_size, extrinsics);
132		let inner_vm = outer_vm
133			.machine(&code_and_jump_table[..], program_counter)
134			.expect("Failed to load the code");
135		let memory_map = MemoryMap::new(&polkavm_memory_map);
136		let kernel_state = KernelState {
137			fds: vm_state
138				.kernel
139				.fds
140				.iter()
141				.map(|(fd, file)| {
142					// TODO @ivan We probably want to open and read files lazily.
143					let (block_ref, position) = (file.block_ref, file.position);
144					let mut lookup = Lookup { outer_vm: &mut outer_vm };
145					let node = fs::Node::open(&block_ref, &mut lookup)?;
146					let node = match node {
147						fs::Node::File(mut file) => {
148							file.seek(position)?;
149							FsNode::File(file)
150						},
151						fs::Node::Dir(dir) => FsNode::Dir { dir, position },
152					};
153					let opened_file = OpenedFile { block_ref, node };
154					Ok((*fd, opened_file))
155				})
156				.collect::<Result<_, fs::Error>>()?,
157		};
158		let heap_address_range = memory_map.heap_range();
159		let heap_page_range =
160			Range::new(heap_address_range.start / PAGE_SIZE, heap_address_range.end / PAGE_SIZE);
161		// Sanity checks.
162		assert!(u32::from(export_count) <= max_exports());
163		assert!(
164			ro_data.len() as u32 <=
165				memory_map.ro_data_range().end - memory_map.ro_data_range().start
166		);
167		assert!(
168			rw_data.len() as u32 <=
169				memory_map.rw_data_range().end - memory_map.rw_data_range().start
170		);
171		let mut engine = Self {
172			// Pre-populate output with the data that we already have.
173			work_output: BoundedWorkOutput::try_from(
174				CoreVmOutput {
175					vm_output: VmOutput {
176						remaining_gas: 0,
177						outcome: Outcome::Halt,
178						num_memory_pages: u32::from(export_count),
179						stream_len: [u32::MAX; OutputStream::COUNT],
180					},
181					vm_state,
182					old_hash,
183					new_hash: Default::default(),
184					touched_imported_pages: Default::default(),
185					updated_pages: Default::default(),
186					exec_ref,
187					outgoing_messages: Default::default(),
188					processed_service_messages: Default::default(),
189				},
190				kernel_state,
191				auth_output_len,
192				heap_page_range,
193			)
194			.expect("Empty work output shouldn't exceed max_report_elective_data"),
195			args,
196			host_call_handlers,
197			host_call_names,
198			num_video_frames: 0,
199			num_audio_bytes: 0,
200			timestamp: 0,
201			outer_vm,
202			inner_vm,
203			memory_map,
204			ro_data,
205			rw_data,
206			export_count,
207			output: Default::default(),
208			exec,
209			input,
210			video_encoder: None,
211		};
212		if initial_program_run {
213			// Initialize the registers for the initial `invoke` call.
214			let default_sp = polkavm_memory_map.stack_address_high() as u64;
215			if libc {
216				let args = core::mem::take(&mut engine.exec.args);
217				let env = core::mem::take(&mut engine.exec.env);
218				let mut context = KernelContext { engine: &mut engine, error: None };
219				context
220					.init(
221						default_sp,
222						RETURN_TO_HOST,
223						args.iter().map(|a| a.as_ref()),
224						env.iter().map(|a| a.as_ref()),
225					)
226					.expect("Failed to init arguments");
227				engine.exec.args = args;
228				engine.exec.env = env;
229			} else {
230				engine.args.set_reg(Reg::RA, RETURN_TO_HOST);
231				engine.args.set_reg(Reg::SP, default_sp);
232			}
233		}
234		Ok(engine)
235	}
236
237	pub fn run(mut self) -> Result<(CoreVmOutput, O, InputStats), RunError> {
238		// Restart the host-call that the engine was executing while ran out of output
239		// space. Panic if the host-call needs to be restarted again.
240		let mut restart_host_call = self.work_output.vm_state.restart_host_call.is_some();
241		let (output, outer_vm, input_stats) = loop {
242			if let Some(index) = self.work_output.take_restart_host_call() {
243				use Outcome::*;
244				if restart_host_call {
245					debug!("Restarting host-call {:?}", self.pretty_host_call(index));
246				}
247				let outcome = match self.handle_host_call_fault(index) {
248					Ok(()) => {
249						// Continue normal execution.
250						None
251					},
252					Err(HostCallError::Outcome(
253						outcome @ Panic | outcome @ Halt | outcome @ OutOfGas,
254					)) => Some(outcome),
255					Err(HostCallError::Outcome(outcome @ TimeLimitReached)) => {
256						// NOTE We don't restart the host-call in this case.
257						debug!(
258							"Time limit reached in host-call {:?}",
259							self.pretty_host_call(index)
260						);
261						Some(outcome)
262					},
263					Err(HostCallError::Outcome(outcome @ OutputLimitReached)) => {
264						if restart_host_call {
265							return Err(FatalError::NotEnoughOutputSpace.into());
266						}
267						debug!(
268							"Output limit reached in host-call {:?}",
269							self.pretty_host_call(index)
270						);
271						self.work_output.set_restart_host_call(index);
272						Some(outcome)
273					},
274					Err(HostCallError::Outcome(outcome @ InputLimitReached)) => {
275						if restart_host_call {
276							return Err(FatalError::NotEnoughInputSpace.into());
277						}
278						self.work_output.set_restart_host_call(index);
279						Some(outcome)
280					},
281					Err(HostCallError::Outcome(outcome @ WaitingForInput(..))) => {
282						self.work_output.set_restart_host_call(index);
283						Some(outcome)
284					},
285					Err(HostCallError::Outcome(outcome @ PageFault { page, num_pages })) => {
286						if restart_host_call {
287							debug!(
288								"Hard page fault at {:?} while restarting host-call {:?}",
289								self.memory_map.pretty_page_range(page.0..page.0 + num_pages),
290								self.pretty_host_call(index)
291							);
292							// NOTE: This error might also occur if the builder haven't imported all
293							// the necessary pages. Normally this shouldn't happen.
294							return Err(FatalError::NotEnoughInputSpace.into());
295						}
296						debug!(
297							"Hard page fault at {:#x?} in host-call {:?}",
298							self.memory_map.pretty_page_range(page.0..page.0 + num_pages),
299							self.pretty_host_call(index)
300						);
301						self.work_output.set_restart_host_call(index);
302						Some(outcome)
303					},
304					Err(HostCallError::Jam(e)) => return Err(e.into()),
305					Err(HostCallError::Fs(e)) => return Err(e.into()),
306					Err(HostCallError::Fatal(e)) => return Err(e.into()),
307				};
308				if let Some(outcome) = outcome {
309					break self.suspend(outcome)?;
310				}
311				restart_host_call = false;
312			}
313			let (outcome, gas, regs) = self.inner_vm.invoke(self.args.gas, self.args.regs)?;
314			trace!("Invoke outcome {:?}", self.pretty_invoke_outcome(outcome));
315			self.args.gas = gas;
316			self.args.regs = regs;
317			match outcome {
318				InvokeOutcome::Halt => {
319					break self.suspend(Outcome::Halt)?;
320				},
321				InvokeOutcome::PageFault(address) => {
322					match self.touch(PageNum::from_address(address as u32)) {
323						Ok(..) => {},
324						Err(TouchError::PageFault { page, num_pages }) => {
325							trace!("Hard page fault at {:?}", self.memory_map.pretty_page(page.0));
326							// Suspend the execution when _either_ the program tries to
327							// access the page that was not imported _or_
328							// the max. no. of allocated pages is reached.
329							break self.suspend(Outcome::PageFault { page, num_pages })?;
330						},
331						Err(TouchError::Jam(e)) => return Err(e.into()),
332						Err(TouchError::InvalidMemoryAccess) =>
333							return Err(RunError::Fatal(FatalError::InvalidMemoryAccess)),
334						Err(TouchError::InputLimitReached) =>
335							break self.suspend(Outcome::InputLimitReached)?,
336					}
337				},
338				InvokeOutcome::HostCallFault(index) =>
339					self.work_output.set_restart_host_call(index),
340				InvokeOutcome::Panic => {
341					break self.suspend(Outcome::Panic)?;
342				},
343				InvokeOutcome::OutOfGas => {
344					break self.suspend(Outcome::OutOfGas)?;
345				},
346			}
347		};
348		debug!("Finished with outcome {:?}", output.vm_output.outcome);
349		Ok((output, outer_vm, input_stats))
350	}
351
352	fn handle_host_call_fault(&mut self, index: u64) -> Result<(), HostCallError> {
353		let handler =
354			self.host_call_handlers.get(index as usize).ok_or(FatalError::UnknownHostCall)?;
355		handler(self)
356	}
357}