use std::collections::BTreeMap;
use anyhow::Result;
use tycho_types::prelude::*;
use crate::error::VmResult;
#[cfg(feature = "dump")]
use crate::error::{DumpError, DumpResult};
use crate::state::VmState;
pub trait OpcodeBase: Send + Sync {
fn range(&self) -> (u32, u32);
}
pub trait OpcodeExec: OpcodeBase {
fn exec(&self, st: &mut VmState, opcode: u32, bits: u16) -> VmResult<i32>;
}
#[cfg(feature = "dump")]
pub trait OpcodeDump: OpcodeBase {
fn dump(
&self,
code: &mut CellSlice<'_>,
opcode: u32,
bits: u16,
f: &mut dyn DumpOutput,
) -> DumpResult;
}
#[cfg(feature = "dump")]
pub trait DumpOutput {
fn record_gas(&mut self, gas: u64) -> DumpResult;
fn record_opcode(&mut self, value: &dyn std::fmt::Display) -> DumpResult;
fn record_cell(&mut self, value: Cell) -> DumpResult;
fn record_slice(&mut self, value: CellSlice<'_>) -> DumpResult;
fn record_cont(&mut self, cont: Cell) -> DumpResult;
fn record_cont_slice(&mut self, cont: CellSlice<'_>) -> DumpResult;
fn record_dict(&mut self, n: u16, slice: CellSlice<'_>) -> DumpResult;
}
pub struct DispatchTable {
id: u16,
exec_opcodes: Vec<(u32, Box<dyn OpcodeExec>)>,
#[cfg(feature = "dump")]
dump_opcodes: Vec<(u32, Box<dyn OpcodeDump>)>,
}
impl DispatchTable {
pub fn builder(id: u16) -> Opcodes {
Opcodes {
id,
exec_opcodes: Default::default(),
#[cfg(feature = "dump")]
dump_opcodes: Default::default(),
}
}
#[inline]
pub fn id(&self) -> u16 {
self.id
}
pub fn lookup(&self, opcode: u32) -> &dyn OpcodeExec {
Self::lookup_impl(opcode, &self.exec_opcodes)
}
pub fn dispatch(&self, st: &mut VmState) -> VmResult<i32> {
let (opcode, bits) = Self::get_opcode_from_slice(&st.code.apply());
let op = self.lookup(opcode);
op.exec(st, opcode, bits)
}
#[cfg(feature = "dump")]
pub fn dispatch_dump(&self, code: &mut CellSlice<'_>, f: &mut dyn DumpOutput) -> DumpResult {
let (opcode, bits) = Self::get_opcode_from_slice(code);
let op = Self::lookup_impl(opcode, &self.dump_opcodes);
op.dump(code, opcode, bits, f)
}
fn get_opcode_from_slice(slice: &CellSlice<'_>) -> (u32, u16) {
let bits = std::cmp::min(MAX_OPCODE_BITS, slice.size_bits());
let opcode = (slice.get_uint(0, bits).unwrap() as u32) << (MAX_OPCODE_BITS - bits);
(opcode, bits)
}
#[inline]
fn lookup_impl<T, V>(opcode: u32, opcodes: &[(u32, T)]) -> &V
where
T: AsRef<V>,
V: ?Sized,
{
debug_assert!(!opcodes.is_empty());
let mut i = 0;
let mut j = opcodes.len();
while j - i > 1 {
let k = (j + i) >> 1;
if opcodes[k].0 <= opcode {
i = k;
} else {
j = k;
}
}
opcodes[i].1.as_ref()
}
}
pub struct Opcodes {
id: u16,
exec_opcodes: BTreeMap<u32, Box<dyn OpcodeExec>>,
#[cfg(feature = "dump")]
dump_opcodes: BTreeMap<u32, Box<dyn OpcodeDump>>,
}
impl Opcodes {
pub fn build(self) -> DispatchTable {
let exec_opcodes = build_opcodes(self.exec_opcodes, |min, max| {
Box::new(DummyOpcode {
opcode_min: min,
opcode_max: max,
})
});
#[cfg(feature = "dump")]
let dump_opcodes = build_opcodes(self.dump_opcodes, |min, max| {
Box::new(DummyOpcode {
opcode_min: min,
opcode_max: max,
})
});
DispatchTable {
id: self.id,
exec_opcodes,
#[cfg(feature = "dump")]
dump_opcodes,
}
}
pub fn add_simple(
&mut self,
opcode: u32,
bits: u16,
exec: FnExecInstrSimple,
#[cfg(feature = "dump")] dump: FnDumpInstrSimple,
) -> Result<()> {
let remaining_bits = MAX_OPCODE_BITS - bits;
#[cfg(feature = "dump")]
self.add_dump_opcode(Box::new(SimpleOpcode {
f: dump,
opcode_min: opcode << remaining_bits,
opcode_max: (opcode + 1) << remaining_bits,
opcode_bits: bits,
}))?;
self.add_opcode(Box::new(SimpleOpcode {
f: exec,
opcode_min: opcode << remaining_bits,
opcode_max: (opcode + 1) << remaining_bits,
opcode_bits: bits,
}))
}
pub fn add_fixed(
&mut self,
opcode: u32,
opcode_bits: u16,
arg_bits: u16,
exec: FnExecInstrArg,
#[cfg(feature = "dump")] dump: FnDumpInstrArg,
) -> Result<()> {
let remaining_bits = MAX_OPCODE_BITS - opcode_bits;
#[cfg(feature = "dump")]
self.add_dump_opcode(Box::new(FixedOpcode {
f: dump,
opcode_min: opcode << remaining_bits,
opcode_max: (opcode + 1) << remaining_bits,
total_bits: opcode_bits + arg_bits,
}))?;
self.add_opcode(Box::new(FixedOpcode {
f: exec,
opcode_min: opcode << remaining_bits,
opcode_max: (opcode + 1) << remaining_bits,
total_bits: opcode_bits + arg_bits,
}))
}
pub fn add_fixed_range(
&mut self,
opcode_min: u32,
opcode_max: u32,
total_bits: u16,
_arg_bits: u16,
exec: FnExecInstrArg,
#[cfg(feature = "dump")] dump: FnDumpInstrArg,
) -> Result<()> {
let remaining_bits = MAX_OPCODE_BITS - total_bits;
#[cfg(feature = "dump")]
self.add_dump_opcode(Box::new(FixedOpcode {
f: dump,
opcode_min: opcode_min << remaining_bits,
opcode_max: opcode_max << remaining_bits,
total_bits,
}))?;
self.add_opcode(Box::new(FixedOpcode {
f: exec,
opcode_min: opcode_min << remaining_bits,
opcode_max: opcode_max << remaining_bits,
total_bits,
}))
}
pub fn add_ext(
&mut self,
opcode: u32,
opcode_bits: u16,
arg_bits: u16,
exec: FnExecInstrFull,
#[cfg(feature = "dump")] dump: FnDumpInstrFull,
) -> Result<()> {
let remaining_bits = MAX_OPCODE_BITS - opcode_bits;
#[cfg(feature = "dump")]
self.add_dump_opcode(Box::new(ExtOpcode {
f: dump,
opcode_min: opcode << remaining_bits,
opcode_max: (opcode + 1) << remaining_bits,
total_bits: opcode_bits + arg_bits,
}))?;
self.add_opcode(Box::new(ExtOpcode {
f: exec,
opcode_min: opcode << remaining_bits,
opcode_max: (opcode + 1) << remaining_bits,
total_bits: opcode_bits + arg_bits,
}))
}
pub fn add_ext_range(
&mut self,
opcode_min: u32,
opcode_max: u32,
total_bits: u16,
exec: FnExecInstrFull,
#[cfg(feature = "dump")] dump: FnDumpInstrFull,
) -> Result<()> {
let remaining_bits = MAX_OPCODE_BITS - total_bits;
#[cfg(feature = "dump")]
self.add_dump_opcode(Box::new(ExtOpcode {
f: dump,
opcode_min: opcode_min << remaining_bits,
opcode_max: opcode_max << remaining_bits,
total_bits,
}))?;
self.add_opcode(Box::new(ExtOpcode {
f: exec,
opcode_min: opcode_min << remaining_bits,
opcode_max: opcode_max << remaining_bits,
total_bits,
}))
}
pub fn add_opcode(&mut self, opcode: Box<dyn OpcodeExec>) -> Result<()> {
Self::add_opcode_impl(opcode, &mut self.exec_opcodes)
}
#[cfg(feature = "dump")]
pub fn add_dump_opcode(&mut self, opcode: Box<dyn OpcodeDump>) -> Result<()> {
Self::add_opcode_impl(opcode, &mut self.dump_opcodes)
}
#[inline]
fn add_opcode_impl<T: OpcodeBase + ?Sized>(
opcode: Box<T>,
opcodes: &mut BTreeMap<u32, Box<T>>,
) -> Result<()> {
let (min, max) = opcode.range();
debug_assert!(min < max);
debug_assert!(max <= MAX_OPCODE);
if let Some((other_min, _)) = opcodes.range(min..).next() {
anyhow::ensure!(
max <= *other_min,
"Opcode overlaps with next min: {other_min:06x}"
);
}
if let Some((k, prev)) = opcodes.range(..=min).next_back() {
let (prev_min, prev_max) = prev.range();
debug_assert!(prev_min < prev_max);
debug_assert!(prev_min == *k);
anyhow::ensure!(
prev_max <= min,
"Opcode overlaps with prev max: {prev_max:06x}"
);
}
opcodes.insert(min, opcode);
Ok(())
}
}
fn build_opcodes<T, F>(items: BTreeMap<u32, Box<T>>, f: F) -> Vec<(u32, Box<T>)>
where
T: OpcodeBase + ?Sized,
F: Fn(u32, u32) -> Box<T>,
{
let mut opcodes = Vec::with_capacity(items.len() * 2 + 1);
let mut upto = 0;
for (k, opcode) in items {
let (min, max) = opcode.range();
if min > upto {
opcodes.push((upto, f(upto, min)));
}
opcodes.push((k, opcode));
upto = max;
}
if upto < MAX_OPCODE {
opcodes.push((upto, f(upto, MAX_OPCODE)));
}
opcodes.shrink_to_fit();
opcodes
}
struct DummyOpcode {
opcode_min: u32,
opcode_max: u32,
}
impl OpcodeBase for DummyOpcode {
fn range(&self) -> (u32, u32) {
(self.opcode_min, self.opcode_max)
}
}
impl OpcodeExec for DummyOpcode {
fn exec(&self, st: &mut VmState, _: u32, _: u16) -> VmResult<i32> {
st.gas.try_consume(GAS_PER_INSTRUCTION)?;
vm_bail!(InvalidOpcode);
}
}
#[cfg(feature = "dump")]
impl OpcodeDump for DummyOpcode {
fn dump(&self, _: &mut CellSlice<'_>, _: u32, _: u16, _: &mut dyn DumpOutput) -> DumpResult {
Err(DumpError::InvalidOpcode)
}
}
struct SimpleOpcode<T> {
f: T,
opcode_min: u32,
opcode_max: u32,
opcode_bits: u16,
}
impl<T: Send + Sync> OpcodeBase for SimpleOpcode<T> {
fn range(&self) -> (u32, u32) {
(self.opcode_min, self.opcode_max)
}
}
impl OpcodeExec for SimpleOpcode<FnExecInstrSimple> {
fn exec(&self, st: &mut VmState, _: u32, bits: u16) -> VmResult<i32> {
st.gas.try_consume(opcode_gas(self.opcode_bits))?;
vm_ensure!(bits >= self.opcode_bits, InvalidOpcode);
st.code.range_mut().skip_first(self.opcode_bits, 0)?;
(self.f)(st)
}
}
#[cfg(feature = "dump")]
impl OpcodeDump for SimpleOpcode<FnDumpInstrSimple> {
fn dump(
&self,
code: &mut CellSlice<'_>,
_: u32,
bits: u16,
f: &mut dyn DumpOutput,
) -> DumpResult {
if bits >= self.opcode_bits {
f.record_gas(opcode_gas(self.opcode_bits))?;
code.skip_first(self.opcode_bits, 0)?;
(self.f)(f)
} else {
Err(DumpError::InvalidOpcode)
}
}
}
struct FixedOpcode<T> {
f: T,
opcode_min: u32,
opcode_max: u32,
total_bits: u16,
}
impl<T: Send + Sync> OpcodeBase for FixedOpcode<T> {
fn range(&self) -> (u32, u32) {
(self.opcode_min, self.opcode_max)
}
}
impl OpcodeExec for FixedOpcode<FnExecInstrArg> {
fn exec(&self, st: &mut VmState, opcode: u32, bits: u16) -> VmResult<i32> {
st.gas.try_consume(opcode_gas(self.total_bits))?;
vm_ensure!(bits >= self.total_bits, InvalidOpcode);
st.code.range_mut().skip_first(self.total_bits, 0)?;
(self.f)(st, opcode >> (MAX_OPCODE_BITS - self.total_bits))
}
}
#[cfg(feature = "dump")]
impl OpcodeDump for FixedOpcode<FnDumpInstrArg> {
fn dump(
&self,
code: &mut CellSlice<'_>,
opcode: u32,
bits: u16,
f: &mut dyn DumpOutput,
) -> DumpResult {
if bits >= self.total_bits {
f.record_gas(opcode_gas(self.total_bits))?;
code.skip_first(self.total_bits, 0)?;
(self.f)(opcode >> (MAX_OPCODE_BITS - self.total_bits), f)
} else {
Err(DumpError::InvalidOpcode)
}
}
}
struct ExtOpcode<T> {
f: T,
opcode_min: u32,
opcode_max: u32,
total_bits: u16,
}
impl<T: Send + Sync> OpcodeBase for ExtOpcode<T> {
fn range(&self) -> (u32, u32) {
(self.opcode_min, self.opcode_max)
}
}
impl OpcodeExec for ExtOpcode<FnExecInstrFull> {
fn exec(&self, st: &mut VmState, opcode: u32, bits: u16) -> VmResult<i32> {
st.gas.try_consume(opcode_gas(self.total_bits))?;
vm_ensure!(bits >= self.total_bits, InvalidOpcode);
(self.f)(
st,
opcode >> (MAX_OPCODE_BITS - self.total_bits),
self.total_bits,
)
}
}
#[cfg(feature = "dump")]
impl OpcodeDump for ExtOpcode<FnDumpInstrFull> {
fn dump(
&self,
code: &mut CellSlice<'_>,
opcode: u32,
bits: u16,
f: &mut dyn DumpOutput,
) -> DumpResult {
if bits >= self.total_bits {
f.record_gas(opcode_gas(self.total_bits))?;
(self.f)(
code,
opcode >> (MAX_OPCODE_BITS - self.total_bits),
self.total_bits,
f,
)
} else {
Err(DumpError::InvalidOpcode)
}
}
}
const fn opcode_gas(bits: u16) -> u64 {
GAS_PER_INSTRUCTION + bits as u64 * GAS_PER_BIT
}
pub type FnExecInstrSimple = fn(&mut VmState) -> VmResult<i32>;
#[cfg(feature = "dump")]
pub type FnDumpInstrSimple = fn(&mut dyn DumpOutput) -> DumpResult;
pub type FnExecInstrArg = fn(&mut VmState, u32) -> VmResult<i32>;
#[cfg(feature = "dump")]
pub type FnDumpInstrArg = fn(u32, &mut dyn DumpOutput) -> DumpResult;
pub type FnExecInstrFull = fn(&mut VmState, u32, u16) -> VmResult<i32>;
#[cfg(feature = "dump")]
pub type FnDumpInstrFull = fn(&mut CellSlice<'_>, u32, u16, &mut dyn DumpOutput) -> DumpResult;
const MAX_OPCODE_BITS: u16 = 24;
const MAX_OPCODE: u32 = 1 << MAX_OPCODE_BITS;
const GAS_PER_INSTRUCTION: u64 = 10;
const GAS_PER_BIT: u64 = 1;
#[cfg(test)]
mod tests {
use super::*;
use crate::cont::QuitCont;
use crate::error::VmError;
use crate::gas::{GasConsumer, GasParams};
use crate::saferc::SafeRc;
use crate::smc_info::VmVersion;
#[test]
fn dummy_codepage() {
let cp = DispatchTable::builder(123).build();
let mut state = VmState {
code: Default::default(),
throw_on_code_access: false,
stack: Default::default(),
signature_domains: Default::default(),
cr: Default::default(),
committed_state: Default::default(),
steps: 0,
quit0: SafeRc::from(QuitCont { exit_code: 0 }),
quit1: SafeRc::from(QuitCont { exit_code: 0 }),
gas: GasConsumer::new(GasParams::getter()),
cp: Box::leak(Box::new(cp)),
debug: None,
modifiers: Default::default(),
version: VmVersion::LATEST_TON,
parent: None,
};
let dummy = state.cp.lookup(0x800000);
assert_eq!(dummy.range(), (0x000000, 0x1000000));
let err = dummy.exec(&mut state, 0x800000, 24).unwrap_err();
assert!(matches!(*err, VmError::InvalidOpcode));
}
#[test]
fn opcode_overlap_check_works() {
{
let mut cp = DispatchTable::builder(123);
cp.add_simple(
0xab,
8,
|_| Ok(0),
#[cfg(feature = "dump")]
|_| Ok(()),
)
.unwrap();
cp.add_simple(
0xab,
8,
|_| Ok(0),
#[cfg(feature = "dump")]
|_| Ok(()),
)
.unwrap_err();
}
{
let mut cp = DispatchTable::builder(123);
cp.add_simple(
0xab,
8,
|_| Ok(0),
#[cfg(feature = "dump")]
|_| Ok(()),
)
.unwrap();
cp.add_fixed_range(
0xa0,
0xaf,
8,
4,
|_, _| Ok(0),
#[cfg(feature = "dump")]
|_, _| Ok(()),
)
.unwrap_err();
}
{
let mut cp = DispatchTable::builder(123);
cp.add_fixed_range(
0xa0,
0xaf,
8,
4,
|_, _| Ok(0),
#[cfg(feature = "dump")]
|_, _| Ok(()),
)
.unwrap();
cp.add_simple(
0xab,
8,
|_| Ok(0),
#[cfg(feature = "dump")]
|_| Ok(()),
)
.unwrap_err();
}
{
let mut cp = DispatchTable::builder(123);
cp.add_fixed_range(
0xa0,
0xaf,
8,
4,
|_, _| Ok(0),
#[cfg(feature = "dump")]
|_, _| Ok(()),
)
.unwrap();
cp.add_fixed_range(
0xa4,
0xa7,
8,
2,
|_, _| Ok(0),
#[cfg(feature = "dump")]
|_, _| Ok(()),
)
.unwrap_err();
}
}
}