1use crate::{
2 hash_encoded, FatalError, FsNode, HostCallError, HostCallHandler, InitError, InnerVm,
3 InvokeArgs, KernelContext, Lookup, MemoryManager, OpenedFile, OuterVm, ProgramData, Reg,
4 RunError, TouchError,
5};
6use alloc::vec::Vec;
7use codec::{Compact, CompactLen, ConstEncodedLen, DecodeAll, Encode};
8use corevm_host::{
9 fs, AudioMode, CoreVmOutput, CoreVmPayload, ExecEnv, KernelFd, Outcome, RangeSet, VideoMode,
10 VmOutput, VmState, PAGE_SIZE,
11};
12use jam_pvm_common::{ApiError, InvokeOutcome};
13use jam_types::Hash;
14use log::{debug, trace};
15use polkakernel::{KernelState, Machine};
16use polkavm::{MemoryMapBuilder, RETURN_TO_HOST};
17
18pub struct Engine<O: OuterVm> {
20 pub(crate) args: InvokeArgs,
21 pub(crate) memory_man: MemoryManager<O>,
22 pub(crate) video: Option<VideoMode>,
23 pub(crate) audio: Option<AudioMode>,
24 pub(crate) exec: ExecEnv,
25 pub(crate) num_video_frames: u64,
27 pub(crate) num_audio_bytes: u64,
30 exec_ref: fs::BlockRef,
31 old_hash: Hash,
32 host_call_handlers: Vec<HostCallHandler<O>>,
33 current_host_call: Option<u64>,
35}
36
37impl<O: OuterVm> Engine<O> {
38 pub fn new(payload: CoreVmPayload, mut outer_vm: O) -> Result<Self, InitError> {
39 let CoreVmPayload { gas, vm_state, exec_ref } = payload;
40 let exec = outer_vm.read_file(&exec_ref)?;
41 let exec = ExecEnv::decode_all(&mut &exec[..]).map_err(|_| InitError::ExecBlob)?;
42 let max_exports = outer_vm.get_export_count();
43 let auth_output_len = outer_vm.get_auth_output_len();
44 let mut program_counter = vm_state.program_counter;
45 let program = {
46 let bytes = outer_vm.read_file(&exec.program)?;
47 let corevm_blob = jam_program_blob_common::CoreVmProgramBlob::from_bytes(&bytes)
48 .ok_or(InitError::ProgramBlob)?;
49 polkavm::ProgramParts::from_bytes(corevm_blob.pvm_blob.into())?
50 };
51 let memory_map = MemoryMapBuilder::new(PAGE_SIZE as u32)
52 .ro_data_size(program.ro_data_size)
53 .rw_data_size(program.rw_data_size)
54 .stack_size(program.stack_size)
55 .build()
56 .map_err(|_| InitError::ProgramBlob)?;
57 let code_and_jump_table = program.code_and_jump_table.clone();
58 let ro_data = program.ro_data.clone();
59 let rw_data = program.rw_data.clone();
60 let program_blob =
61 polkavm::ProgramBlob::from_parts(program).map_err(|_| InitError::ProgramBlob)?;
62 let host_call_handlers = Self::create_host_call_handlers(&program_blob);
63 let old_hash = compute_state_hash(&vm_state);
65 let mut args = InvokeArgs { regs: vm_state.regs, gas };
66 let initial_program_run = program_counter == 0;
67 let mut libc = false;
68 if initial_program_run {
69 debug!("This is initial program run");
70 program_counter = program_blob
71 .exports()
72 .find(|export| {
73 let name = export.symbol().as_bytes();
74 if name == b"main" {
75 debug!("Found `main` (no libc) entry point");
76 return true;
77 }
78 if name == b"_pvm_start" {
79 debug!("Found `_pvm_start` (libc) entry point");
80 libc = true;
81 return true;
82 }
83 false
84 })
85 .expect("Neither `main` nor `_pvm_start` entry point found")
86 .program_counter()
87 .0
88 .into();
89 }
90 let inner_vm = outer_vm
91 .machine(&code_and_jump_table[..], program_counter)
92 .expect("Failed to load the code");
93 let program_data = ProgramData::new(&memory_map, ro_data, rw_data);
94 let kernel_state = KernelState {
95 fds: vm_state
96 .kernel
97 .fds
98 .into_iter()
99 .map(|(fd, file)| {
100 let (block_ref, position) = (file.block_ref, file.position);
102 let mut lookup = Lookup { outer_vm: &mut outer_vm };
103 let node = fs::Node::open(&block_ref, &mut lookup)?;
104 let node = match node {
105 fs::Node::File(mut file) => {
106 file.seek(position)?;
107 FsNode::File(file)
108 },
109 fs::Node::Dir(dir) => FsNode::Dir { dir, position },
110 };
111 let opened_file = OpenedFile { block_ref, node };
112 Ok((fd, opened_file))
113 })
114 .collect::<Result<_, fs::Error>>()?,
115 };
116 let mut memory_man = MemoryManager::new(
117 outer_vm,
118 inner_vm,
119 program_data,
120 vm_state.mapped_heap_pages,
121 vm_state.resident_pages,
122 max_exports as usize,
123 auth_output_len as usize,
124 kernel_state,
125 )
126 .expect("Failed to initialize memory pages");
127 if initial_program_run {
128 let default_sp = memory_map.stack_address_high() as u64;
130 if libc {
131 let unused = fs::BlockRef { service_id: 0, hash: Default::default() };
132 let mut context = KernelContext {
133 memory_man: &mut memory_man,
134 regs: &mut args.regs,
135 root_dir: unused,
136 error: None,
137 };
138 context
139 .init(
140 default_sp,
141 RETURN_TO_HOST,
142 exec.args.iter().map(|a| a.as_ref()),
143 exec.env.iter().map(|a| a.as_ref()),
144 )
145 .expect("Failed to init arguments");
146 } else {
147 args.set_reg(Reg::RA, RETURN_TO_HOST);
148 args.set_reg(Reg::SP, default_sp);
149 }
150 }
151 Ok(Self {
152 args,
153 memory_man,
154 video: vm_state.video,
155 audio: vm_state.audio,
156 old_hash,
157 num_video_frames: 0,
158 num_audio_bytes: 0,
159 exec,
160 exec_ref,
161 host_call_handlers,
162 current_host_call: vm_state.restart_host_call,
163 })
164 }
165
166 pub fn run(mut self) -> Result<(CoreVmOutput, O), RunError> {
167 let mut restart_host_call = self.current_host_call.is_some();
170 let (output, outer_vm) = loop {
171 if let Some(index) = self.current_host_call.take() {
172 use Outcome::*;
173 if restart_host_call {
174 debug!("Restarting host-call {index}");
175 }
176 let outcome = match self.handle_host_call_fault(index) {
177 Ok(()) => {
178 None
180 },
181 Err(HostCallError::Outcome(
182 outcome @ Panic | outcome @ Halt | outcome @ OutOfGas,
183 )) => Some(outcome),
184 Err(HostCallError::Outcome(outcome @ TimeLimitReached)) => {
185 debug!("Time limit reached in host-call {index}");
187 Some(outcome)
188 },
189 Err(HostCallError::Outcome(outcome @ OutputLimitReached)) => {
190 if restart_host_call {
191 return Err(FatalError::NotEnoughOutputSpace.into());
192 }
193 debug!("Output limit reached in host-call {index}");
194 self.current_host_call = Some(index);
195 Some(outcome)
196 },
197 Err(HostCallError::Outcome(outcome @ PageFault { page, num_pages })) => {
198 if restart_host_call {
199 debug!(
200 "Hard page fault at {:#x?} while restarting host-call {index}",
201 page..page + num_pages
202 );
203 return Err(FatalError::NotEnoughInputSpace.into());
206 }
207 debug!(
208 "Hard page fault at {:#x?} in host-call {index}",
209 page..page + num_pages
210 );
211 self.current_host_call = Some(index);
212 Some(outcome)
213 },
214 Err(HostCallError::Jam(e)) => return Err(e.into()),
215 Err(HostCallError::Fs(e)) => return Err(e.into()),
216 Err(HostCallError::Fatal(e)) => return Err(e.into()),
217 };
218 if let Some(outcome) = outcome {
219 break self.into_host_output(outcome)?;
220 }
221 restart_host_call = false;
222 }
223 let (outcome, gas, regs) =
224 self.memory_man.inner_vm.invoke(self.args.gas, self.args.regs)?;
225 trace!("Invoke outcome {:?}", DebugInvokeOutcome(outcome));
226 self.args.gas = gas;
227 self.args.regs = regs;
228 match outcome {
229 InvokeOutcome::Halt => {
230 break self.into_host_output(Outcome::Halt)?;
231 },
232 InvokeOutcome::PageFault(address) => {
233 match self.memory_man.touch(address) {
234 Ok(..) => {},
235 Err(TouchError::PageFault { page, num_pages }) => {
236 trace!("Hard page fault at address {:#x}", page * PAGE_SIZE);
237 break self.into_host_output(Outcome::PageFault { page, num_pages })?;
241 },
242 Err(TouchError::Jam(e)) => return Err(e.into()),
243 }
244 },
245 InvokeOutcome::HostCallFault(index) => self.current_host_call = Some(index),
246 InvokeOutcome::Panic => {
247 break self.into_host_output(Outcome::Panic)?;
248 },
249 InvokeOutcome::OutOfGas => {
250 break self.into_host_output(Outcome::OutOfGas)?;
251 },
252 }
253 };
254 debug!("Finished with outcome {:?}", output.vm_output.outcome);
255 Ok((output, outer_vm))
256 }
257
258 fn handle_host_call_fault(&mut self, index: u64) -> Result<(), HostCallError> {
259 let Some(handler) = self.host_call_handlers.get(index as usize) else {
260 panic!("Unknown host call index: {index}");
261 };
262 handler(self)
263 }
264
265 fn into_host_output(self, outcome: Outcome) -> Result<(CoreVmOutput, O), ApiError> {
266 let (
267 outer_vm,
268 inner_vm,
269 mapped_heap_pages,
270 resident_pages,
271 touched_imported_pages,
272 updated_pages,
273 num_memory_pages,
274 stream_len,
275 kernel_state,
276 ) = self.memory_man.export()?;
277 let program_counter = inner_vm.expunge().expect("Failed to close VM handle");
278 let vm_state = VmState {
279 mapped_heap_pages,
280 resident_pages,
281 regs: self.args.regs,
282 program_counter,
283 kernel: kernel_state,
284 restart_host_call: self.current_host_call,
285 video: self.video,
286 audio: self.audio,
287 };
288 let new_hash = compute_state_hash(&vm_state);
289 let work_output = CoreVmOutput {
290 vm_output: VmOutput {
291 remaining_gas: self.args.gas,
292 outcome,
293 num_memory_pages,
294 stream_len,
295 },
296 vm_state,
297 old_hash: self.old_hash,
298 new_hash,
299 touched_imported_pages,
300 updated_pages,
301 exec_ref: self.exec_ref,
302 };
303 Ok((work_output, outer_vm))
304 }
305}
306
307pub fn compute_state_hash(vm: &VmState) -> Hash {
309 hash_encoded((&vm.regs, vm.program_counter, &vm.mapped_heap_pages, &vm.resident_pages))
310}
311
312pub fn initial_state_hash() -> Hash {
314 let initial_vm_state = VmState::initial();
315 compute_state_hash(&initial_vm_state)
316}
317
318pub(crate) struct DebugInvokeOutcome(pub InvokeOutcome);
319
320impl core::fmt::Debug for DebugInvokeOutcome {
321 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
322 match self.0 {
323 InvokeOutcome::Halt => f.write_str("Halt"),
324 InvokeOutcome::PageFault(address) =>
325 f.debug_tuple("PageFault").field(&format_args!("{address:#x}")).finish(),
326 InvokeOutcome::HostCallFault(i) => f.debug_tuple("HostCallFault").field(&i).finish(),
327 InvokeOutcome::Panic => f.write_str("Panic"),
328 InvokeOutcome::OutOfGas => f.write_str("OutOfGas"),
329 }
330 }
331}
332
333pub(crate) fn get_work_output_len(
334 mapped_heap_pages: &RangeSet,
335 resident_pages: &RangeSet,
336 num_imported_pages: usize,
337 num_updated_pages: usize,
338 num_opened_files: usize,
339) -> usize {
340 MAX_STATIC_WORK_OUTPUT_LEN +
341 mapped_heap_pages.encoded_size() +
342 resident_pages.encoded_size() +
343 vec_map_encoded_len::<u64, Hash>(num_imported_pages) +
344 vec_map_encoded_len::<u64, Hash>(num_updated_pages) +
345 vec_map_encoded_len::<u32, KernelFd>(num_opened_files)
346}
347
348fn vec_map_encoded_len<K: ConstEncodedLen, V: ConstEncodedLen>(len: usize) -> usize {
349 (K::max_encoded_len() + V::max_encoded_len()) * len + Compact::<u64>::compact_len(&(len as u64))
350}
351
352const MAX_STATIC_WORK_OUTPUT_LEN: usize = 269;
355
356#[cfg(test)]
357mod tests {
358 use super::*;
359 use corevm_host::{OutputStream, Range};
360 use jam_types::{max_exports, max_imports, Encode, ServiceId, SignedGas};
361 use rand::Rng;
362
363 #[test]
364 fn max_static_work_output_len_is_correct() {
365 assert_eq!(MAX_STATIC_WORK_OUTPUT_LEN, encoded_work_output_len(max_exports()));
366 }
367
368 fn encoded_work_output_len(num_exports: u32) -> usize {
369 let output = CoreVmOutput {
370 vm_output: VmOutput {
371 remaining_gas: SignedGas::MAX,
372 outcome: Outcome::Halt,
373 num_memory_pages: num_exports,
374 stream_len: [u32::MAX; OutputStream::COUNT],
375 },
376 vm_state: VmState {
377 regs: Default::default(),
378 program_counter: Default::default(),
379 mapped_heap_pages: Default::default(),
380 resident_pages: Default::default(),
381 kernel: Default::default(),
382 restart_host_call: Some(u64::MAX),
383 video: Some(
384 VideoMode::new(1, 1, 1, corevm_host::VideoFrameFormat::Rgb888Indexed8).unwrap(),
385 ),
386 audio: Some(AudioMode::new(1, 1, corevm_host::AudioSampleFormat::S16LE).unwrap()),
387 },
388 old_hash: Default::default(),
389 new_hash: Default::default(),
390 updated_pages: Default::default(),
391 touched_imported_pages: Default::default(),
392 exec_ref: fs::BlockRef { service_id: 0, hash: fs::Hash(Hash::default()) },
393 };
394 output.encode().len() -
395 output.updated_pages.encode().len() -
396 output.touched_imported_pages.encode().len() -
397 output.vm_state.mapped_heap_pages.encode().len() -
398 output.vm_state.resident_pages.encode().len() -
399 output.vm_state.kernel.fds.encode().len()
400 }
401
402 #[test]
403 fn get_work_output_len_works() {
404 let mut rng = rand::rng();
405 for num_imported_pages in [0, 1, 2, max_imports() as usize] {
406 for num_updated_pages in [0, 1, 2, max_exports() as usize] {
407 for num_opened_files in [0, 1, 2, 1000] {
408 let mapped_heap_pages = random_range_set(&mut rng);
409 let resident_pages = random_range_set(&mut rng);
410 let expected_len = get_work_output_len(
411 &mapped_heap_pages,
412 &resident_pages,
413 num_imported_pages,
414 num_updated_pages,
415 num_opened_files,
416 );
417 let actual_len = get_work_output_len_slow(
418 &mapped_heap_pages,
419 &resident_pages,
420 num_imported_pages,
421 num_updated_pages,
422 num_opened_files,
423 );
424 assert_eq!(
425 expected_len, actual_len,
426 "expected = {expected_len}, \
427 actual = {actual_len}, \
428 num_imported_pages = {num_imported_pages}, \
429 num_updated_pages = {num_updated_pages}, \
430 num_opened_files = {num_opened_files}"
431 );
432 }
433 }
434 }
435 }
436
437 fn get_work_output_len_slow(
438 mapped_heap_pages: &RangeSet,
439 resident_pages: &RangeSet,
440 num_imported_pages: usize,
441 num_updated_pages: usize,
442 num_opened_files: usize,
443 ) -> usize {
444 let output = CoreVmOutput {
445 vm_output: VmOutput {
446 remaining_gas: SignedGas::MAX,
447 outcome: Outcome::Halt,
448 num_memory_pages: max_exports(),
449 stream_len: [u32::MAX; OutputStream::COUNT],
450 },
451 vm_state: VmState {
452 regs: Default::default(),
453 program_counter: Default::default(),
454 mapped_heap_pages: mapped_heap_pages.clone(),
455 resident_pages: resident_pages.clone(),
456 kernel: corevm_host::KernelState {
457 fds: (0..num_opened_files)
458 .map(|i| {
459 (
460 i as u32,
461 KernelFd {
462 block_ref: fs::BlockRef {
463 service_id: ServiceId::MAX,
464 hash: fs::Hash::default(),
465 },
466 position: u64::MAX,
467 },
468 )
469 })
470 .collect(),
471 },
472 restart_host_call: Some(u64::MAX),
473 video: Some(
474 VideoMode::new(1, 1, 1, corevm_host::VideoFrameFormat::Rgb888Indexed8).unwrap(),
475 ),
476 audio: Some(AudioMode::new(1, 1, corevm_host::AudioSampleFormat::S16LE).unwrap()),
477 },
478 old_hash: Default::default(),
479 new_hash: Default::default(),
480 updated_pages: (0..num_updated_pages)
481 .map(|i| (i as u64 * PAGE_SIZE, Hash::default()))
482 .collect(),
483 touched_imported_pages: (0..num_imported_pages)
484 .map(|i| (i as u64 * PAGE_SIZE, Hash::default()))
485 .collect(),
486 exec_ref: fs::BlockRef { service_id: 0, hash: fs::Hash(Hash::default()) },
487 };
488 output.encode().len()
489 }
490
491 fn random_range_set<R: Rng>(rng: &mut R) -> RangeSet {
492 let mut ranges = RangeSet::new();
493 {
494 let mut offset = 0;
495 for _ in 0..10 {
496 let start = rng.random_range(offset..=20);
497 let end = rng.random_range(start..=20);
498 offset = end;
499 ranges.insert(Range::new(start, end));
500 }
501 }
502 ranges
503 }
504}