Skip to main content

tycho_vm/
dispatch.rs

1use std::collections::BTreeMap;
2
3use anyhow::Result;
4use tycho_types::prelude::*;
5
6use crate::error::VmResult;
7#[cfg(feature = "dump")]
8use crate::error::{DumpError, DumpResult};
9use crate::state::VmState;
10
11pub trait OpcodeBase: Send + Sync {
12    /// Opcode range aligned to 24 bits.
13    fn range(&self) -> (u32, u32);
14}
15
16/// Opcode description.
17pub trait OpcodeExec: OpcodeBase {
18    /// Execute this opcode.
19    fn exec(&self, st: &mut VmState, opcode: u32, bits: u16) -> VmResult<i32>;
20}
21
22/// Opcode description.
23#[cfg(feature = "dump")]
24pub trait OpcodeDump: OpcodeBase {
25    /// Dump this opcode.
26    fn dump(
27        &self,
28        code: &mut CellSlice<'_>,
29        opcode: u32,
30        bits: u16,
31        f: &mut dyn DumpOutput,
32    ) -> DumpResult;
33}
34
35/// Dump output visitor.
36#[cfg(feature = "dump")]
37pub trait DumpOutput {
38    fn record_gas(&mut self, gas: u64) -> DumpResult;
39    fn record_opcode(&mut self, value: &dyn std::fmt::Display) -> DumpResult;
40    fn record_cell(&mut self, value: Cell) -> DumpResult;
41    fn record_slice(&mut self, value: CellSlice<'_>) -> DumpResult;
42    fn record_cont(&mut self, cont: Cell) -> DumpResult;
43    fn record_cont_slice(&mut self, cont: CellSlice<'_>) -> DumpResult;
44    fn record_dict(&mut self, n: u16, slice: CellSlice<'_>) -> DumpResult;
45}
46
47/// Code page.
48pub struct DispatchTable {
49    id: u16,
50    exec_opcodes: Vec<(u32, Box<dyn OpcodeExec>)>,
51    #[cfg(feature = "dump")]
52    dump_opcodes: Vec<(u32, Box<dyn OpcodeDump>)>,
53}
54
55impl DispatchTable {
56    pub fn builder(id: u16) -> Opcodes {
57        Opcodes {
58            id,
59            exec_opcodes: Default::default(),
60            #[cfg(feature = "dump")]
61            dump_opcodes: Default::default(),
62        }
63    }
64
65    #[inline]
66    pub fn id(&self) -> u16 {
67        self.id
68    }
69
70    pub fn lookup(&self, opcode: u32) -> &dyn OpcodeExec {
71        Self::lookup_impl(opcode, &self.exec_opcodes)
72    }
73
74    pub fn dispatch(&self, st: &mut VmState) -> VmResult<i32> {
75        let (opcode, bits) = Self::get_opcode_from_slice(&st.code.apply());
76        let op = self.lookup(opcode);
77        op.exec(st, opcode, bits)
78    }
79
80    #[cfg(feature = "dump")]
81    pub fn dispatch_dump(&self, code: &mut CellSlice<'_>, f: &mut dyn DumpOutput) -> DumpResult {
82        let (opcode, bits) = Self::get_opcode_from_slice(code);
83        let op = Self::lookup_impl(opcode, &self.dump_opcodes);
84        op.dump(code, opcode, bits, f)
85    }
86
87    fn get_opcode_from_slice(slice: &CellSlice<'_>) -> (u32, u16) {
88        let bits = std::cmp::min(MAX_OPCODE_BITS, slice.size_bits());
89        let opcode = (slice.get_uint(0, bits).unwrap() as u32) << (MAX_OPCODE_BITS - bits);
90        (opcode, bits)
91    }
92
93    #[inline]
94    fn lookup_impl<T, V>(opcode: u32, opcodes: &[(u32, T)]) -> &V
95    where
96        T: AsRef<V>,
97        V: ?Sized,
98    {
99        debug_assert!(!opcodes.is_empty());
100
101        let mut i = 0;
102        let mut j = opcodes.len();
103        while j - i > 1 {
104            let k = (j + i) >> 1;
105            if opcodes[k].0 <= opcode {
106                i = k;
107            } else {
108                j = k;
109            }
110        }
111        opcodes[i].1.as_ref()
112    }
113}
114
115/// A builder for [`DispatchTable`].
116pub struct Opcodes {
117    id: u16,
118    exec_opcodes: BTreeMap<u32, Box<dyn OpcodeExec>>,
119    #[cfg(feature = "dump")]
120    dump_opcodes: BTreeMap<u32, Box<dyn OpcodeDump>>,
121}
122
123impl Opcodes {
124    pub fn build(self) -> DispatchTable {
125        let exec_opcodes = build_opcodes(self.exec_opcodes, |min, max| {
126            Box::new(DummyOpcode {
127                opcode_min: min,
128                opcode_max: max,
129            })
130        });
131
132        #[cfg(feature = "dump")]
133        let dump_opcodes = build_opcodes(self.dump_opcodes, |min, max| {
134            Box::new(DummyOpcode {
135                opcode_min: min,
136                opcode_max: max,
137            })
138        });
139
140        DispatchTable {
141            id: self.id,
142            exec_opcodes,
143            #[cfg(feature = "dump")]
144            dump_opcodes,
145        }
146    }
147
148    pub fn add_simple(
149        &mut self,
150        opcode: u32,
151        bits: u16,
152        exec: FnExecInstrSimple,
153        #[cfg(feature = "dump")] dump: FnDumpInstrSimple,
154    ) -> Result<()> {
155        let remaining_bits = MAX_OPCODE_BITS - bits;
156
157        #[cfg(feature = "dump")]
158        self.add_dump_opcode(Box::new(SimpleOpcode {
159            f: dump,
160            opcode_min: opcode << remaining_bits,
161            opcode_max: (opcode + 1) << remaining_bits,
162            opcode_bits: bits,
163        }))?;
164
165        self.add_opcode(Box::new(SimpleOpcode {
166            f: exec,
167            opcode_min: opcode << remaining_bits,
168            opcode_max: (opcode + 1) << remaining_bits,
169            opcode_bits: bits,
170        }))
171    }
172
173    pub fn add_fixed(
174        &mut self,
175        opcode: u32,
176        opcode_bits: u16,
177        arg_bits: u16,
178        exec: FnExecInstrArg,
179        #[cfg(feature = "dump")] dump: FnDumpInstrArg,
180    ) -> Result<()> {
181        let remaining_bits = MAX_OPCODE_BITS - opcode_bits;
182
183        #[cfg(feature = "dump")]
184        self.add_dump_opcode(Box::new(FixedOpcode {
185            f: dump,
186            opcode_min: opcode << remaining_bits,
187            opcode_max: (opcode + 1) << remaining_bits,
188            total_bits: opcode_bits + arg_bits,
189        }))?;
190
191        self.add_opcode(Box::new(FixedOpcode {
192            f: exec,
193            opcode_min: opcode << remaining_bits,
194            opcode_max: (opcode + 1) << remaining_bits,
195            total_bits: opcode_bits + arg_bits,
196        }))
197    }
198
199    pub fn add_fixed_range(
200        &mut self,
201        opcode_min: u32,
202        opcode_max: u32,
203        total_bits: u16,
204        _arg_bits: u16,
205        exec: FnExecInstrArg,
206        #[cfg(feature = "dump")] dump: FnDumpInstrArg,
207    ) -> Result<()> {
208        let remaining_bits = MAX_OPCODE_BITS - total_bits;
209
210        #[cfg(feature = "dump")]
211        self.add_dump_opcode(Box::new(FixedOpcode {
212            f: dump,
213            opcode_min: opcode_min << remaining_bits,
214            opcode_max: opcode_max << remaining_bits,
215            total_bits,
216        }))?;
217
218        self.add_opcode(Box::new(FixedOpcode {
219            f: exec,
220            opcode_min: opcode_min << remaining_bits,
221            opcode_max: opcode_max << remaining_bits,
222            total_bits,
223        }))
224    }
225
226    pub fn add_ext(
227        &mut self,
228        opcode: u32,
229        opcode_bits: u16,
230        arg_bits: u16,
231        exec: FnExecInstrFull,
232        #[cfg(feature = "dump")] dump: FnDumpInstrFull,
233    ) -> Result<()> {
234        let remaining_bits = MAX_OPCODE_BITS - opcode_bits;
235
236        #[cfg(feature = "dump")]
237        self.add_dump_opcode(Box::new(ExtOpcode {
238            f: dump,
239            opcode_min: opcode << remaining_bits,
240            opcode_max: (opcode + 1) << remaining_bits,
241            total_bits: opcode_bits + arg_bits,
242        }))?;
243
244        self.add_opcode(Box::new(ExtOpcode {
245            f: exec,
246            opcode_min: opcode << remaining_bits,
247            opcode_max: (opcode + 1) << remaining_bits,
248            total_bits: opcode_bits + arg_bits,
249        }))
250    }
251
252    pub fn add_ext_range(
253        &mut self,
254        opcode_min: u32,
255        opcode_max: u32,
256        total_bits: u16,
257        exec: FnExecInstrFull,
258        #[cfg(feature = "dump")] dump: FnDumpInstrFull,
259    ) -> Result<()> {
260        let remaining_bits = MAX_OPCODE_BITS - total_bits;
261
262        #[cfg(feature = "dump")]
263        self.add_dump_opcode(Box::new(ExtOpcode {
264            f: dump,
265            opcode_min: opcode_min << remaining_bits,
266            opcode_max: opcode_max << remaining_bits,
267            total_bits,
268        }))?;
269
270        self.add_opcode(Box::new(ExtOpcode {
271            f: exec,
272            opcode_min: opcode_min << remaining_bits,
273            opcode_max: opcode_max << remaining_bits,
274            total_bits,
275        }))
276    }
277
278    pub fn add_opcode(&mut self, opcode: Box<dyn OpcodeExec>) -> Result<()> {
279        Self::add_opcode_impl(opcode, &mut self.exec_opcodes)
280    }
281
282    #[cfg(feature = "dump")]
283    pub fn add_dump_opcode(&mut self, opcode: Box<dyn OpcodeDump>) -> Result<()> {
284        Self::add_opcode_impl(opcode, &mut self.dump_opcodes)
285    }
286
287    #[inline]
288    fn add_opcode_impl<T: OpcodeBase + ?Sized>(
289        opcode: Box<T>,
290        opcodes: &mut BTreeMap<u32, Box<T>>,
291    ) -> Result<()> {
292        let (min, max) = opcode.range();
293        debug_assert!(min < max);
294        debug_assert!(max <= MAX_OPCODE);
295
296        if let Some((other_min, _)) = opcodes.range(min..).next() {
297            anyhow::ensure!(
298                max <= *other_min,
299                "Opcode overlaps with next min: {other_min:06x}"
300            );
301        }
302
303        if let Some((k, prev)) = opcodes.range(..=min).next_back() {
304            let (prev_min, prev_max) = prev.range();
305            debug_assert!(prev_min < prev_max);
306            debug_assert!(prev_min == *k);
307            anyhow::ensure!(
308                prev_max <= min,
309                "Opcode overlaps with prev max: {prev_max:06x}"
310            );
311        }
312
313        opcodes.insert(min, opcode);
314        Ok(())
315    }
316}
317
318fn build_opcodes<T, F>(items: BTreeMap<u32, Box<T>>, f: F) -> Vec<(u32, Box<T>)>
319where
320    T: OpcodeBase + ?Sized,
321    F: Fn(u32, u32) -> Box<T>,
322{
323    let mut opcodes = Vec::with_capacity(items.len() * 2 + 1);
324
325    let mut upto = 0;
326    for (k, opcode) in items {
327        let (min, max) = opcode.range();
328        if min > upto {
329            opcodes.push((upto, f(upto, min)));
330        }
331
332        opcodes.push((k, opcode));
333        upto = max;
334    }
335
336    if upto < MAX_OPCODE {
337        opcodes.push((upto, f(upto, MAX_OPCODE)));
338    }
339
340    opcodes.shrink_to_fit();
341    opcodes
342}
343
344// === Opcodes ===
345
346struct DummyOpcode {
347    opcode_min: u32,
348    opcode_max: u32,
349}
350
351impl OpcodeBase for DummyOpcode {
352    fn range(&self) -> (u32, u32) {
353        (self.opcode_min, self.opcode_max)
354    }
355}
356
357impl OpcodeExec for DummyOpcode {
358    fn exec(&self, st: &mut VmState, _: u32, _: u16) -> VmResult<i32> {
359        st.gas.try_consume(GAS_PER_INSTRUCTION)?;
360        vm_bail!(InvalidOpcode);
361    }
362}
363
364#[cfg(feature = "dump")]
365impl OpcodeDump for DummyOpcode {
366    fn dump(&self, _: &mut CellSlice<'_>, _: u32, _: u16, _: &mut dyn DumpOutput) -> DumpResult {
367        Err(DumpError::InvalidOpcode)
368    }
369}
370
371struct SimpleOpcode<T> {
372    f: T,
373    opcode_min: u32,
374    opcode_max: u32,
375    opcode_bits: u16,
376}
377
378impl<T: Send + Sync> OpcodeBase for SimpleOpcode<T> {
379    fn range(&self) -> (u32, u32) {
380        (self.opcode_min, self.opcode_max)
381    }
382}
383
384impl OpcodeExec for SimpleOpcode<FnExecInstrSimple> {
385    fn exec(&self, st: &mut VmState, _: u32, bits: u16) -> VmResult<i32> {
386        st.gas.try_consume(opcode_gas(self.opcode_bits))?;
387        vm_ensure!(bits >= self.opcode_bits, InvalidOpcode);
388        st.code.range_mut().skip_first(self.opcode_bits, 0)?;
389        (self.f)(st)
390    }
391}
392
393#[cfg(feature = "dump")]
394impl OpcodeDump for SimpleOpcode<FnDumpInstrSimple> {
395    fn dump(
396        &self,
397        code: &mut CellSlice<'_>,
398        _: u32,
399        bits: u16,
400        f: &mut dyn DumpOutput,
401    ) -> DumpResult {
402        if bits >= self.opcode_bits {
403            f.record_gas(opcode_gas(self.opcode_bits))?;
404            code.skip_first(self.opcode_bits, 0)?;
405            (self.f)(f)
406        } else {
407            Err(DumpError::InvalidOpcode)
408        }
409    }
410}
411
412struct FixedOpcode<T> {
413    f: T,
414    opcode_min: u32,
415    opcode_max: u32,
416    total_bits: u16,
417}
418
419impl<T: Send + Sync> OpcodeBase for FixedOpcode<T> {
420    fn range(&self) -> (u32, u32) {
421        (self.opcode_min, self.opcode_max)
422    }
423}
424
425impl OpcodeExec for FixedOpcode<FnExecInstrArg> {
426    fn exec(&self, st: &mut VmState, opcode: u32, bits: u16) -> VmResult<i32> {
427        st.gas.try_consume(opcode_gas(self.total_bits))?;
428        vm_ensure!(bits >= self.total_bits, InvalidOpcode);
429        st.code.range_mut().skip_first(self.total_bits, 0)?;
430        (self.f)(st, opcode >> (MAX_OPCODE_BITS - self.total_bits))
431    }
432}
433
434#[cfg(feature = "dump")]
435impl OpcodeDump for FixedOpcode<FnDumpInstrArg> {
436    fn dump(
437        &self,
438        code: &mut CellSlice<'_>,
439        opcode: u32,
440        bits: u16,
441        f: &mut dyn DumpOutput,
442    ) -> DumpResult {
443        if bits >= self.total_bits {
444            f.record_gas(opcode_gas(self.total_bits))?;
445            code.skip_first(self.total_bits, 0)?;
446            (self.f)(opcode >> (MAX_OPCODE_BITS - self.total_bits), f)
447        } else {
448            Err(DumpError::InvalidOpcode)
449        }
450    }
451}
452
453struct ExtOpcode<T> {
454    f: T,
455    opcode_min: u32,
456    opcode_max: u32,
457    total_bits: u16,
458}
459
460impl<T: Send + Sync> OpcodeBase for ExtOpcode<T> {
461    fn range(&self) -> (u32, u32) {
462        (self.opcode_min, self.opcode_max)
463    }
464}
465
466impl OpcodeExec for ExtOpcode<FnExecInstrFull> {
467    fn exec(&self, st: &mut VmState, opcode: u32, bits: u16) -> VmResult<i32> {
468        st.gas.try_consume(opcode_gas(self.total_bits))?;
469        vm_ensure!(bits >= self.total_bits, InvalidOpcode);
470        (self.f)(
471            st,
472            opcode >> (MAX_OPCODE_BITS - self.total_bits),
473            self.total_bits,
474        )
475    }
476}
477
478#[cfg(feature = "dump")]
479impl OpcodeDump for ExtOpcode<FnDumpInstrFull> {
480    fn dump(
481        &self,
482        code: &mut CellSlice<'_>,
483        opcode: u32,
484        bits: u16,
485        f: &mut dyn DumpOutput,
486    ) -> DumpResult {
487        if bits >= self.total_bits {
488            f.record_gas(opcode_gas(self.total_bits))?;
489            (self.f)(
490                code,
491                opcode >> (MAX_OPCODE_BITS - self.total_bits),
492                self.total_bits,
493                f,
494            )
495        } else {
496            Err(DumpError::InvalidOpcode)
497        }
498    }
499}
500
501const fn opcode_gas(bits: u16) -> u64 {
502    GAS_PER_INSTRUCTION + bits as u64 * GAS_PER_BIT
503}
504
505/// Fn pointer for a simple opcode execution logic.
506pub type FnExecInstrSimple = fn(&mut VmState) -> VmResult<i32>;
507
508/// Fn pointer for a simple opcode dump logic.
509#[cfg(feature = "dump")]
510pub type FnDumpInstrSimple = fn(&mut dyn DumpOutput) -> DumpResult;
511
512/// Fn pointer for an opcode execution logic with a single argument.
513pub type FnExecInstrArg = fn(&mut VmState, u32) -> VmResult<i32>;
514
515/// Fn pointer for an opcode dump logic with a single argument.
516#[cfg(feature = "dump")]
517pub type FnDumpInstrArg = fn(u32, &mut dyn DumpOutput) -> DumpResult;
518
519/// Fn pointer for an extended opcode execution logic.
520pub type FnExecInstrFull = fn(&mut VmState, u32, u16) -> VmResult<i32>;
521
522/// Fn pointer for an extended opcode dump logic.
523#[cfg(feature = "dump")]
524pub type FnDumpInstrFull = fn(&mut CellSlice<'_>, u32, u16, &mut dyn DumpOutput) -> DumpResult;
525
526const MAX_OPCODE_BITS: u16 = 24;
527const MAX_OPCODE: u32 = 1 << MAX_OPCODE_BITS;
528
529const GAS_PER_INSTRUCTION: u64 = 10;
530const GAS_PER_BIT: u64 = 1;
531
532#[cfg(test)]
533mod tests {
534    use super::*;
535    use crate::cont::QuitCont;
536    use crate::error::VmError;
537    use crate::gas::{GasConsumer, GasParams};
538    use crate::saferc::SafeRc;
539    use crate::smc_info::VmVersion;
540
541    #[test]
542    fn dummy_codepage() {
543        let cp = DispatchTable::builder(123).build();
544
545        let mut state = VmState {
546            code: Default::default(),
547            throw_on_code_access: false,
548            stack: Default::default(),
549            signature_domains: Default::default(),
550            cr: Default::default(),
551            committed_state: Default::default(),
552            steps: 0,
553            quit0: SafeRc::from(QuitCont { exit_code: 0 }),
554            quit1: SafeRc::from(QuitCont { exit_code: 0 }),
555            gas: GasConsumer::new(GasParams::getter()),
556            cp: Box::leak(Box::new(cp)),
557            debug: None,
558            modifiers: Default::default(),
559            version: VmVersion::LATEST_TON,
560            parent: None,
561        };
562
563        let dummy = state.cp.lookup(0x800000);
564        assert_eq!(dummy.range(), (0x000000, 0x1000000));
565
566        let err = dummy.exec(&mut state, 0x800000, 24).unwrap_err();
567        assert!(matches!(*err, VmError::InvalidOpcode));
568    }
569
570    #[test]
571    fn opcode_overlap_check_works() {
572        // Simple overlap
573        {
574            let mut cp = DispatchTable::builder(123);
575            cp.add_simple(
576                0xab,
577                8,
578                |_| Ok(0),
579                #[cfg(feature = "dump")]
580                |_| Ok(()),
581            )
582            .unwrap();
583            cp.add_simple(
584                0xab,
585                8,
586                |_| Ok(0),
587                #[cfg(feature = "dump")]
588                |_| Ok(()),
589            )
590            .unwrap_err();
591        }
592
593        // Range-simple overlap
594        {
595            let mut cp = DispatchTable::builder(123);
596            cp.add_simple(
597                0xab,
598                8,
599                |_| Ok(0),
600                #[cfg(feature = "dump")]
601                |_| Ok(()),
602            )
603            .unwrap();
604            cp.add_fixed_range(
605                0xa0,
606                0xaf,
607                8,
608                4,
609                |_, _| Ok(0),
610                #[cfg(feature = "dump")]
611                |_, _| Ok(()),
612            )
613            .unwrap_err();
614        }
615
616        // Simple-range overlap
617        {
618            let mut cp = DispatchTable::builder(123);
619            cp.add_fixed_range(
620                0xa0,
621                0xaf,
622                8,
623                4,
624                |_, _| Ok(0),
625                #[cfg(feature = "dump")]
626                |_, _| Ok(()),
627            )
628            .unwrap();
629            cp.add_simple(
630                0xab,
631                8,
632                |_| Ok(0),
633                #[cfg(feature = "dump")]
634                |_| Ok(()),
635            )
636            .unwrap_err();
637        }
638
639        // Range-range overlap
640        {
641            let mut cp = DispatchTable::builder(123);
642            cp.add_fixed_range(
643                0xa0,
644                0xaf,
645                8,
646                4,
647                |_, _| Ok(0),
648                #[cfg(feature = "dump")]
649                |_, _| Ok(()),
650            )
651            .unwrap();
652            cp.add_fixed_range(
653                0xa4,
654                0xa7,
655                8,
656                2,
657                |_, _| Ok(0),
658                #[cfg(feature = "dump")]
659                |_, _| Ok(()),
660            )
661            .unwrap_err();
662        }
663    }
664}