use crate::{
hash_encoded, BoundedInput, BoundedWorkOutput, FatalError, FsNode, HostCallError,
HostCallHandler, InitError, InnerVm, InvokeArgs, KernelContext, Lookup, MemoryMap, OpenedFile,
OuterVm, OutputBuffers, Reg, RunError, TouchError,
};
use alloc::vec::Vec;
use codec::DecodeAll;
use corevm_codec::video;
use corevm_host::{
fs, CoreVmExtrinsics, CoreVmOutput, CoreVmPayload, ExecEnv, Outcome, OutputStream, PageNum,
Range, VmOutput, PAGE_SIZE,
};
use jam_pvm_common::InvokeOutcome;
use jam_types::{max_exports, VecSet};
use log::{debug, trace};
use polkakernel::{KernelState, Machine};
use polkavm::{ArcBytes, RETURN_TO_HOST};
pub struct WorkPackageParams {
pub encoded_size: u32,
pub auth_output_size: u32,
pub export_count: u16,
}
#[derive(Debug)]
pub struct InputStats {
pub accessed_imported_pages: VecSet<PageNum>,
pub num_service_messages_read: u32,
pub num_input_chunks_read: u32,
}
pub struct Engine<O: OuterVm> {
pub(crate) args: InvokeArgs,
pub(crate) outer_vm: O,
pub(crate) inner_vm: O::InnerVm,
pub(crate) exec: ExecEnv,
pub(crate) num_video_frames: u64,
pub(crate) num_audio_bytes: u64,
pub(crate) timestamp: u64,
pub(crate) memory_map: MemoryMap,
pub(crate) export_count: u16,
pub(crate) output: OutputBuffers,
pub(crate) ro_data: ArcBytes,
pub(crate) rw_data: ArcBytes,
pub(crate) work_output: BoundedWorkOutput,
pub(crate) host_call_handlers: Vec<HostCallHandler<O>>,
pub(crate) host_call_names: Vec<&'static str>,
pub(crate) input: BoundedInput,
pub(crate) video_encoder: Option<video::Encoder>,
}
impl<O: OuterVm> Engine<O> {
pub fn new(
payload: CoreVmPayload,
extrinsics: CoreVmExtrinsics,
work_package_params: WorkPackageParams,
mut outer_vm: O,
) -> Result<Self, InitError> {
let CoreVmPayload { gas, vm_state, exec_ref } = payload;
let exec = outer_vm.read_file(&exec_ref)?;
let exec = ExecEnv::decode_all(&mut &exec[..]).map_err(|_| InitError::ExecBlob)?;
let export_count = work_package_params.export_count;
let auth_output_len = work_package_params.auth_output_size;
let mut program_counter = vm_state.program_counter;
let program = {
let bytes = outer_vm.read_file(&exec.program)?;
let corevm_blob = jam_program_blob_common::CoreVmProgramBlob::from_bytes(&bytes)
.ok_or(InitError::ProgramBlob)?;
polkavm::ProgramParts::from_bytes(corevm_blob.pvm_blob.into())?
};
let polkavm_memory_map = polkavm::MemoryMapBuilder::new(PAGE_SIZE)
.ro_data_size(program.ro_data_size)
.rw_data_size(program.rw_data_size)
.stack_size(program.stack_size)
.build()
.map_err(|_| InitError::ProgramBlob)?;
let code_and_jump_table = program.code_and_jump_table.clone();
let ro_data = program.ro_data.clone();
let rw_data = program.rw_data.clone();
let program_blob =
polkavm::ProgramBlob::from_parts(program).map_err(|_| InitError::ProgramBlob)?;
let (host_call_handlers, host_call_names) = Self::create_host_call_handlers(&program_blob);
let old_hash = hash_encoded(&vm_state);
let args = InvokeArgs { regs: vm_state.regs, gas };
let initial_program_run = program_counter == 0;
let mut libc = false;
if initial_program_run {
debug!("This is initial program run");
program_counter = program_blob
.exports()
.find(|export| {
let name = export.symbol().as_bytes();
if name == b"main" {
debug!("Found `main` (no libc) entry point");
return true;
}
if name == b"_pvm_start" {
debug!("Found `_pvm_start` (libc) entry point");
libc = true;
return true;
}
false
})
.expect("Neither `main` nor `_pvm_start` entry point found")
.program_counter()
.0
.into();
}
let input = BoundedInput::new(work_package_params.encoded_size, extrinsics);
let inner_vm = outer_vm
.machine(&code_and_jump_table[..], program_counter)
.expect("Failed to load the code");
let memory_map = MemoryMap::new(&polkavm_memory_map);
let kernel_state = KernelState {
fds: vm_state
.kernel
.fds
.iter()
.map(|(fd, file)| {
let (block_ref, position) = (file.block_ref, file.position);
let mut lookup = Lookup { outer_vm: &mut outer_vm };
let node = fs::Node::open(&block_ref, &mut lookup)?;
let node = match node {
fs::Node::File(mut file) => {
file.seek(position)?;
FsNode::File(file)
},
fs::Node::Dir(dir) => FsNode::Dir { dir, position },
};
let opened_file = OpenedFile { block_ref, node };
Ok((*fd, opened_file))
})
.collect::<Result<_, fs::Error>>()?,
};
let heap_address_range = memory_map.heap_range();
let heap_page_range =
Range::new(heap_address_range.start / PAGE_SIZE, heap_address_range.end / PAGE_SIZE);
assert!(u32::from(export_count) <= max_exports());
assert!(
ro_data.len() as u32 <=
memory_map.ro_data_range().end - memory_map.ro_data_range().start
);
assert!(
rw_data.len() as u32 <=
memory_map.rw_data_range().end - memory_map.rw_data_range().start
);
let mut engine = Self {
work_output: BoundedWorkOutput::try_from(
CoreVmOutput {
vm_output: VmOutput {
remaining_gas: 0,
outcome: Outcome::Halt,
num_memory_pages: u32::from(export_count),
stream_len: [u32::MAX; OutputStream::COUNT],
},
vm_state,
old_hash,
new_hash: Default::default(),
touched_imported_pages: Default::default(),
updated_pages: Default::default(),
exec_ref,
outgoing_messages: Default::default(),
processed_service_messages: Default::default(),
},
kernel_state,
auth_output_len,
heap_page_range,
)
.expect("Empty work output shouldn't exceed max_report_elective_data"),
args,
host_call_handlers,
host_call_names,
num_video_frames: 0,
num_audio_bytes: 0,
timestamp: 0,
outer_vm,
inner_vm,
memory_map,
ro_data,
rw_data,
export_count,
output: Default::default(),
exec,
input,
video_encoder: None,
};
if initial_program_run {
let default_sp = polkavm_memory_map.stack_address_high() as u64;
if libc {
let args = core::mem::take(&mut engine.exec.args);
let env = core::mem::take(&mut engine.exec.env);
let mut context = KernelContext { engine: &mut engine, error: None };
context
.init(
default_sp,
RETURN_TO_HOST,
args.iter().map(|a| a.as_ref()),
env.iter().map(|a| a.as_ref()),
)
.expect("Failed to init arguments");
engine.exec.args = args;
engine.exec.env = env;
} else {
engine.args.set_reg(Reg::RA, RETURN_TO_HOST);
engine.args.set_reg(Reg::SP, default_sp);
}
}
Ok(engine)
}
pub fn run(mut self) -> Result<(CoreVmOutput, O, InputStats), RunError> {
let mut restart_host_call = self.work_output.vm_state.restart_host_call.is_some();
let (output, outer_vm, input_stats) = loop {
if let Some(index) = self.work_output.take_restart_host_call() {
use Outcome::*;
if restart_host_call {
debug!("Restarting host-call {:?}", self.pretty_host_call(index));
}
let outcome = match self.handle_host_call_fault(index) {
Ok(()) => {
None
},
Err(HostCallError::Outcome(
outcome @ Panic | outcome @ Halt | outcome @ OutOfGas,
)) => Some(outcome),
Err(HostCallError::Outcome(outcome @ TimeLimitReached)) => {
debug!(
"Time limit reached in host-call {:?}",
self.pretty_host_call(index)
);
Some(outcome)
},
Err(HostCallError::Outcome(outcome @ OutputLimitReached)) => {
if restart_host_call {
return Err(FatalError::NotEnoughOutputSpace.into());
}
debug!(
"Output limit reached in host-call {:?}",
self.pretty_host_call(index)
);
self.work_output.set_restart_host_call(index);
Some(outcome)
},
Err(HostCallError::Outcome(outcome @ InputLimitReached)) => {
if restart_host_call {
return Err(FatalError::NotEnoughInputSpace.into());
}
self.work_output.set_restart_host_call(index);
Some(outcome)
},
Err(HostCallError::Outcome(outcome @ WaitingForInput(..))) => {
self.work_output.set_restart_host_call(index);
Some(outcome)
},
Err(HostCallError::Outcome(outcome @ PageFault { page, num_pages })) => {
if restart_host_call {
debug!(
"Hard page fault at {:?} while restarting host-call {:?}",
self.memory_map.pretty_page_range(page.0..page.0 + num_pages),
self.pretty_host_call(index)
);
return Err(FatalError::NotEnoughInputSpace.into());
}
debug!(
"Hard page fault at {:#x?} in host-call {:?}",
self.memory_map.pretty_page_range(page.0..page.0 + num_pages),
self.pretty_host_call(index)
);
self.work_output.set_restart_host_call(index);
Some(outcome)
},
Err(HostCallError::Jam(e)) => return Err(e.into()),
Err(HostCallError::Fs(e)) => return Err(e.into()),
Err(HostCallError::Fatal(e)) => return Err(e.into()),
};
if let Some(outcome) = outcome {
break self.suspend(outcome)?;
}
restart_host_call = false;
}
let (outcome, gas, regs) = self.inner_vm.invoke(self.args.gas, self.args.regs)?;
trace!("Invoke outcome {:?}", self.pretty_invoke_outcome(outcome));
self.args.gas = gas;
self.args.regs = regs;
match outcome {
InvokeOutcome::Halt => {
break self.suspend(Outcome::Halt)?;
},
InvokeOutcome::PageFault(address) => {
match self.touch(PageNum::from_address(address as u32)) {
Ok(..) => {},
Err(TouchError::PageFault { page, num_pages }) => {
trace!("Hard page fault at {:?}", self.memory_map.pretty_page(page.0));
break self.suspend(Outcome::PageFault { page, num_pages })?;
},
Err(TouchError::Jam(e)) => return Err(e.into()),
Err(TouchError::InvalidMemoryAccess) =>
return Err(RunError::Fatal(FatalError::InvalidMemoryAccess)),
Err(TouchError::InputLimitReached) =>
break self.suspend(Outcome::InputLimitReached)?,
}
},
InvokeOutcome::HostCallFault(index) =>
self.work_output.set_restart_host_call(index),
InvokeOutcome::Panic => {
break self.suspend(Outcome::Panic)?;
},
InvokeOutcome::OutOfGas => {
break self.suspend(Outcome::OutOfGas)?;
},
}
};
debug!("Finished with outcome {:?}", output.vm_output.outcome);
Ok((output, outer_vm, input_stats))
}
fn handle_host_call_fault(&mut self, index: u64) -> Result<(), HostCallError> {
let handler =
self.host_call_handlers.get(index as usize).ok_or(FatalError::UnknownHostCall)?;
handler(self)
}
}