corevm_engine/
engine.rs

1use crate::{
2	hash_encoded, AppendOutputError, Arg, InnerVm, InvokeArgs, MemoryManager, OuterVm,
3	PageFaultError, ProgramData, Reg,
4};
5use codec::{Compact, CompactLen};
6use corevm_host::{
7	CoreVmOutput, CoreVmPayload, Outcome, OutputStream, RangeSet, VideoMode, VmOutput, VmState,
8	PAGE_SIZE,
9};
10use jam_pvm_common::{ApiError, InvokeOutcome};
11use jam_types::{Hash, ServiceId};
12use log::{debug, trace};
13use polkavm::{MemoryMapBuilder, RETURN_TO_HOST};
14
15pub struct Engine<O: OuterVm> {
16	outer_vm: O,
17	args: InvokeArgs,
18	memory_man: MemoryManager<O::InnerVm>,
19	video: Option<VideoMode>,
20	frame_number: u64,
21	old_hash: Hash,
22	code_hash: Hash,
23	code_host: ServiceId,
24}
25
26impl<O: OuterVm> Engine<O> {
27	pub fn new(payload: CoreVmPayload, mut outer_vm: O) -> Result<Self, ApiError> {
28		let CoreVmPayload { gas, vm_state, program: guest_code_ref } = payload;
29		let max_exports = outer_vm.get_export_count();
30		let auth_output_len = outer_vm.get_auth_output_len();
31		let program_counter = vm_state.program_counter;
32		let program = {
33			let bytes = outer_vm.read_file(&guest_code_ref).expect("Failed to fetch program");
34			let corevm_blob = jam_program_blob::CoreVmProgramBlob::from_bytes(&bytes)
35				.expect("Failed to parse CoreVM blob");
36			polkavm::ProgramParts::from_bytes(corevm_blob.pvm_blob.into())
37				.expect("Failed to parse PolkaVM blob")
38		};
39		let memory_map = MemoryMapBuilder::new(PAGE_SIZE as u32)
40			.ro_data_size(program.ro_data_size)
41			.rw_data_size(program.rw_data_size)
42			.stack_size(program.stack_size)
43			.build()
44			.expect("Failed to build memory map");
45		// Should include program hash and pages' hash.
46		let old_hash = compute_state_hash(&vm_state);
47		let mut args = InvokeArgs { regs: vm_state.regs, gas };
48		if program_counter == 0 {
49			// Initialize the registers for the initial `invoke` call.
50			args.set_reg(Reg::RA, RETURN_TO_HOST);
51			args.set_reg(Reg::SP, memory_map.stack_address_high() as u64);
52		}
53		let code = &program.code_and_jump_table[..];
54		let inner_vm = outer_vm.machine(code, program_counter).expect("Failed to load the code");
55		let program_data = ProgramData::new(&memory_map, program.ro_data, program.rw_data);
56		let memory_man = MemoryManager::new(
57			inner_vm,
58			&mut outer_vm,
59			program_data,
60			vm_state.mapped_heap_pages,
61			vm_state.resident_pages,
62			max_exports as usize,
63			auth_output_len as usize,
64		)
65		.expect("Failed to initialize memory pages");
66		Ok(Self {
67			outer_vm,
68			args,
69			memory_man,
70			video: None,
71			frame_number: vm_state.frame_number,
72			old_hash,
73			code_hash: guest_code_ref.hash,
74			code_host: guest_code_ref.service_id,
75		})
76	}
77
78	pub fn run(mut self) -> Result<(CoreVmOutput, O), ApiError> {
79		let (output, outer_vm) = loop {
80			let (outcome, gas, regs) =
81				self.memory_man.inner_vm.invoke(self.args.gas, self.args.regs)?;
82			trace!("Invoke outcome {:?}", DebugInvokeOutcome(outcome));
83			self.args.gas = gas;
84			self.args.regs = regs;
85			match outcome {
86				InvokeOutcome::Halt => {
87					break self.into_host_output(Outcome::Halt)?;
88				},
89				InvokeOutcome::PageFault(address) => {
90					match self.memory_man.touch(address) {
91						Ok(..) => {},
92						Err(PageFaultError::PageFault { page, num_pages }) => {
93							trace!("Hard page fault at address {:#x}", page * PAGE_SIZE);
94							// Suspend the execution when _either_ the program tries to
95							// access the page that was not imported _or_
96							// the max. no. of allocated pages is reached.
97							break self.into_host_output(Outcome::PageFault { page, num_pages })?;
98						},
99						Err(PageFaultError::ApiError(e)) => return Err(e),
100					}
101				},
102				InvokeOutcome::HostCallFault(index) => match self.handle_host_call_fault(index) {
103					Ok(..) => {},
104					Err(AppendOutputError::OutputLimitReached) => {
105						trace!("Output limit reached");
106						break self.into_host_output(Outcome::OutputLimitReached)?;
107					},
108					Err(AppendOutputError::PageFault { page, num_pages }) => {
109						trace!(
110							"Hard page fault at address {:#x}-{:#x}",
111							page * PAGE_SIZE,
112							(page + num_pages) * PAGE_SIZE
113						);
114						break self.into_host_output(Outcome::PageFault { page, num_pages })?;
115					},
116					Err(AppendOutputError::ApiError(e)) => return Err(e),
117				},
118				InvokeOutcome::Panic => {
119					break self.into_host_output(Outcome::Panic)?;
120				},
121				InvokeOutcome::OutOfGas => {
122					break self.into_host_output(Outcome::OutOfGas)?;
123				},
124			}
125		};
126		debug!("Finished with outcome {:?}", output.vm_output.outcome);
127		Ok((output, outer_vm))
128	}
129
130	fn handle_host_call_fault(&mut self, index: u64) -> Result<(), AppendOutputError> {
131		let args = &mut self.args;
132		match index {
133			index_for_corevm_call!(gas) => {
134				args.set_return_value(args.gas);
135			},
136			index_for_corevm_call!(yield_console_data) => {
137				let mut i = 0;
138				let stream: u64 = Arg::get(args, &mut i);
139				let inner_src: u64 = Arg::get(args, &mut i);
140				let length: u64 = Arg::get(args, &mut i);
141				let stream = match stream {
142					1 => OutputStream::Stdout,
143					2 => OutputStream::Stderr,
144					other => panic!("Invalid output stream specified: {other}"),
145				};
146				trace!("Call yield_console_data({stream:?}, {inner_src:#x}, {length})");
147				args.set_return_value(1_u64);
148				self.memory_man.append_output(stream, inner_src, length)?;
149				args.set_return_value(0_u64);
150			},
151			index_for_corevm_call!(yield_video_frame) => {
152				let mut i = 0;
153				let inner_src: u64 = Arg::get(args, &mut i);
154				let length: u64 = Arg::get(args, &mut i);
155				trace!("Call yield_video_frame({inner_src:#x}, {length})");
156				args.set_return_value(1_u64);
157				self.memory_man.append_output(OutputStream::Video, inner_src, length)?;
158				args.set_return_value(0_u64);
159				self.frame_number += 1;
160			},
161			index_for_corevm_call!(alloc) => {
162				let mut i = 0;
163				let size: u64 = Arg::get(args, &mut i);
164				let (address, _size) = match self.memory_man.alloc(size) {
165					Some((address, size)) => (address, size),
166					None => {
167						debug!("Failed to map guest memory block of size {}", size);
168						(0, 0)
169					},
170				};
171				trace!("Call alloc({size}) = {address:#x}");
172				args.set_return_value(address);
173			},
174			index_for_corevm_call!(free) => {
175				let mut i = 0;
176				let address: u64 = Arg::get(args, &mut i);
177				let size: u64 = Arg::get(args, &mut i);
178				self.memory_man.dealloc(address, size)?;
179				trace!("Call free({address:#x}, {size})");
180				args.set_return_value(0_u64);
181			},
182			index_for_corevm_call!(video_mode) => {
183				let mut i = 0;
184				let width: u64 = Arg::get(args, &mut i);
185				let height: u64 = Arg::get(args, &mut i);
186				let refresh_rate: u64 = Arg::get(args, &mut i);
187				let format: u64 = Arg::get(args, &mut i);
188				trace!("Call video_mode({width}, {height}, {refresh_rate}, {format})");
189				self.video = Some(VideoMode {
190					width: width as u32,
191					height: height as u32,
192					mode: format as u32,
193					refresh_rate: refresh_rate as u16,
194				});
195				args.set_return_value(0_u64);
196			},
197			index => {
198				panic!("Unknown host call index: {}", index);
199			},
200		}
201		Ok(())
202	}
203
204	fn into_host_output(mut self, outcome: Outcome) -> Result<(CoreVmOutput, O), ApiError> {
205		let (
206			mapped_heap_pages,
207			resident_pages,
208			touched_imported_pages,
209			updated_pages,
210			num_memory_pages,
211			stream_len,
212		) = self.memory_man.export(&mut self.outer_vm)?;
213		let program_counter =
214			self.memory_man.inner_vm.expunge().expect("Failed to close VM handle");
215		let vm_state = VmState {
216			mapped_heap_pages,
217			resident_pages,
218			regs: self.args.regs,
219			program_counter,
220			frame_number: self.frame_number,
221		};
222		let new_hash = compute_state_hash(&vm_state);
223		let work_output = CoreVmOutput {
224			vm_output: VmOutput {
225				remaining_gas: self.args.gas,
226				outcome,
227				num_memory_pages,
228				stream_len,
229			},
230			vm_state,
231			old_hash: self.old_hash,
232			new_hash,
233			touched_imported_pages,
234			updated_pages,
235			video: self.video,
236			guest_code_hash: self.code_hash,
237			guest_code_host: self.code_host,
238		};
239		Ok((work_output, self.outer_vm))
240	}
241}
242
243/// This hash is used to check the validity of the state transition in JAM's `accumulate`.
244pub fn compute_state_hash(vm: &VmState) -> Hash {
245	hash_encoded((
246		&vm.regs,
247		vm.program_counter,
248		vm.frame_number,
249		&vm.mapped_heap_pages,
250		&vm.resident_pages,
251	))
252}
253
254/// The hash of the initial state.
255pub fn initial_state_hash() -> Hash {
256	let initial_vm_state = VmState::initial();
257	compute_state_hash(&initial_vm_state)
258}
259
260pub(crate) struct DebugInvokeOutcome(pub InvokeOutcome);
261
262impl core::fmt::Debug for DebugInvokeOutcome {
263	fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
264		match self.0 {
265			InvokeOutcome::Halt => f.write_str("Halt"),
266			InvokeOutcome::PageFault(address) =>
267				f.debug_tuple("PageFault").field(&format_args!("{address:#x}")).finish(),
268			InvokeOutcome::HostCallFault(i) => f.debug_tuple("HostCallFault").field(&i).finish(),
269			InvokeOutcome::Panic => f.write_str("Panic"),
270			InvokeOutcome::OutOfGas => f.write_str("OutOfGas"),
271		}
272	}
273}
274
275// TODO @ivan Handle imports and exports by name, not by index.
276#[rustfmt::skip]
277macro_rules! index_for_corevm_call {
278	(gas) => {0};
279	(alloc) => {1};
280	(free) => {2};
281	(yield_console_data) => {3};
282	(yield_video_frame) => {4};
283	(video_mode) => {5};
284}
285
286use index_for_corevm_call;
287
288pub fn get_work_output_len(
289	mapped_heap_pages: &RangeSet,
290	resident_pages: &RangeSet,
291	num_imported_pages: usize,
292	num_updated_pages: usize,
293) -> usize {
294	let imported_pages_encoded_len = core::mem::size_of::<(u64, Hash)>() * num_imported_pages +
295		Compact::<u64>::compact_len(&(num_imported_pages as u64));
296	let updated_pages_encoded_len = core::mem::size_of::<(u64, Hash)>() * num_updated_pages +
297		Compact::<u64>::compact_len(&(num_updated_pages as u64));
298	MAX_STATIC_WORK_OUTPUT_LEN +
299		mapped_heap_pages.encoded_len() +
300		resident_pages.encoded_len() +
301		imported_pages_encoded_len +
302		updated_pages_encoded_len
303}
304
305/// Maximum length of the work output in bytes without dynamically allocated components (e.g.
306/// pages).
307const MAX_STATIC_WORK_OUTPUT_LEN: usize = 264;
308
309/// The maximum total size of all output blobs in a work-report, in octets.
310pub const MAX_TOTAL_OUTPUT_BLOB_SIZE: usize = 48 * 1024;
311
312#[cfg(test)]
313mod tests {
314	use super::*;
315	use jam_types::{max_exports, Encode, SignedGas};
316
317	#[test]
318	fn max_static_work_output_len_is_correct() {
319		assert_eq!(MAX_STATIC_WORK_OUTPUT_LEN, encoded_work_output_len(max_exports()));
320	}
321
322	fn encoded_work_output_len(num_exports: u32) -> usize {
323		let output = CoreVmOutput {
324			vm_output: VmOutput {
325				remaining_gas: SignedGas::MAX,
326				outcome: Outcome::Halt,
327				num_memory_pages: num_exports,
328				stream_len: [u32::MAX; OutputStream::COUNT],
329			},
330			vm_state: VmState {
331				frame_number: Default::default(),
332				regs: Default::default(),
333				program_counter: Default::default(),
334				mapped_heap_pages: Default::default(),
335				resident_pages: Default::default(),
336			},
337			old_hash: Default::default(),
338			new_hash: Default::default(),
339			updated_pages: Default::default(),
340			touched_imported_pages: Default::default(),
341			video: Some(VideoMode {
342				mode: Default::default(),
343				width: Default::default(),
344				height: Default::default(),
345				refresh_rate: Default::default(),
346			}),
347			guest_code_hash: Default::default(),
348			guest_code_host: Default::default(),
349		};
350		output.encode().len() -
351			output.updated_pages.encode().len() -
352			output.touched_imported_pages.encode().len() -
353			output.vm_state.mapped_heap_pages.encode().len() -
354			output.vm_state.resident_pages.encode().len()
355	}
356}