1use super::StackSection;
32use crate::bytecode::{Bytecode, Inst, InstFlags, Interner, U256Imm};
33use bitvec::vec::BitVec;
34use either::Either;
35use oxc_index::{IndexVec, index_vec};
36use revm_bytecode::opcode as op;
37use smallvec::SmallVec;
38use std::{cmp::Ordering, collections::VecDeque, ops::Range};
39
40oxc_index::define_nonmax_u32_index_type! {
41 pub(crate) struct ConstSetIdx;
43}
44crate::bytecode::impl_index_display!(ConstSetIdx, "{}");
45
46pub(crate) type OperandSnapshot = SmallVec<[AbsValue; 4]>;
51
52#[derive(Clone, Default)]
54pub(crate) struct Snapshots {
55 pub(crate) inputs: IndexVec<Inst, OperandSnapshot>,
57 pub(crate) outputs: IndexVec<Inst, Option<AbsValue>>,
59}
60
61impl Snapshots {
62 pub(crate) fn restore_from(&mut self, insts: impl Iterator<Item = Inst>, saved: &Self) {
64 for inst in insts {
65 self.inputs[inst].clone_from(&saved.inputs[inst]);
66 self.outputs[inst] = saved.outputs[inst];
67 }
68 }
69}
70
71#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
73pub(crate) enum AbsValue {
74 Const(U256Imm),
75 ConstSet(ConstSetIdx),
77 #[default]
79 Top,
80}
81
82impl AbsValue {
83 pub(crate) fn as_const(self) -> Option<U256Imm> {
85 match self {
86 Self::Const(v) => Some(v),
87 _ => None,
88 }
89 }
90}
91
92struct ConstSetInterner {
94 interner: Interner<ConstSetIdx, Box<[U256Imm]>>,
95}
96
97impl ConstSetInterner {
98 fn new() -> Self {
99 Self { interner: Interner::new() }
100 }
101
102 fn get(&self, idx: ConstSetIdx) -> &[U256Imm] {
104 self.interner.get(idx)
105 }
106
107 fn abs_const_set<'a>(&'a self, val: &'a AbsValue) -> Option<&'a [U256Imm]> {
109 match val {
110 AbsValue::Top => None,
111 AbsValue::Const(v) => Some(std::slice::from_ref(v)),
112 AbsValue::ConstSet(idx) => Some(self.get(*idx)),
113 }
114 }
115
116 fn intern_set(&mut self, set: &[U256Imm]) -> AbsValue {
118 match set.len() {
119 0 => unreachable!("empty const set"),
120 1 => AbsValue::Const(set[0]),
121 _ => AbsValue::ConstSet(self.interner.intern(set.into())),
122 }
123 }
124
125 fn join(&mut self, a: AbsValue, b: AbsValue) -> AbsValue {
127 match (a, b) {
128 (AbsValue::Top, _) | (_, AbsValue::Top) => AbsValue::Top,
129 (AbsValue::Const(x), AbsValue::Const(y)) if x == y => AbsValue::Const(x),
130 _ => {
131 let a = self.abs_const_set(&a).unwrap();
132 let b = self.abs_const_set(&b).unwrap();
133 let mut merged = SmallVec::<[U256Imm; 8]>::new();
135 let (mut i, mut j) = (0, 0);
136 while i < a.len() && j < b.len() {
137 match a[i].cmp(&b[j]) {
138 Ordering::Less => {
139 merged.push(a[i]);
140 i += 1;
141 }
142 Ordering::Greater => {
143 merged.push(b[j]);
144 j += 1;
145 }
146 Ordering::Equal => {
147 merged.push(a[i]);
148 i += 1;
149 j += 1;
150 }
151 }
152 }
153 merged.extend_from_slice(&a[i..]);
154 merged.extend_from_slice(&b[j..]);
155 self.intern_set(&merged)
156 }
157 }
158 }
159}
160
161const MAX_ABS_STACK_DEPTH: usize = 64;
169
170#[derive(Clone, Debug)]
172enum BlockState {
173 Bottom,
175 Known(Vec<AbsValue>),
177}
178
179impl BlockState {
180 fn join(&mut self, incoming: &[AbsValue], sets: &mut ConstSetInterner) -> bool {
187 match self {
188 Self::Bottom => {
189 let start = incoming.len().saturating_sub(MAX_ABS_STACK_DEPTH);
190 *self = Self::Known(incoming[start..].to_vec());
191 true
192 }
193 Self::Known(existing) => {
194 let new_len = existing.len().max(incoming.len());
195 let mut changed = false;
196
197 let join_len = new_len.min(MAX_ABS_STACK_DEPTH);
201
202 if existing.len() < join_len {
204 let pad = join_len - existing.len();
205 existing.splice(0..0, std::iter::repeat_n(AbsValue::Top, pad));
206 changed = true;
207 } else if existing.len() > join_len {
208 existing.drain(..existing.len() - join_len);
209 changed = true;
210 }
211
212 let incoming_start = incoming.len().saturating_sub(join_len);
215 let incoming_top = &incoming[incoming_start..];
216 let pad = join_len - incoming_top.len();
218 for i in 0..join_len {
219 let inc = if i < pad { AbsValue::Top } else { incoming_top[i - pad] };
220 let joined = sets.join(existing[i], inc);
221 if joined != existing[i] {
222 existing[i] = joined;
223 changed = true;
224 }
225 }
226
227 changed
228 }
229 }
230 }
231}
232
233oxc_index::define_nonmax_u32_index_type! {
234 pub(crate) struct Block;
236}
237crate::bytecode::impl_index_display!(Block, "bb{}");
238
239struct Worklist {
241 queue: VecDeque<Block>,
242 in_queue: BitVec,
243}
244
245impl Worklist {
246 fn new(size: usize) -> Self {
247 Self { queue: VecDeque::new(), in_queue: BitVec::repeat(false, size) }
248 }
249
250 fn push(&mut self, id: Block) {
251 let idx = id.index();
252 if !self.in_queue[idx] {
253 self.in_queue.set(idx, true);
254 self.queue.push_back(id);
255 }
256 }
257
258 fn pop(&mut self) -> Option<Block> {
259 let id = self.queue.pop_front()?;
260 self.in_queue.set(id.index(), false);
261 Some(id)
262 }
263}
264
265pub(crate) struct BlockData {
267 pub(crate) insts: Range<Inst>,
269 pub(crate) preds: SmallVec<[Block; 4]>,
271 pub(crate) succs: SmallVec<[Block; 4]>,
273}
274
275impl BlockData {
276 #[inline]
277 pub(crate) fn terminator(&self) -> Inst {
278 self.insts.end - 1
279 }
280
281 #[inline]
283 pub(crate) fn insts(
284 &self,
285 ) -> impl DoubleEndedIterator<Item = Inst> + ExactSizeIterator + use<> {
286 (self.insts.start.index()..self.insts.end.index()).map(Inst::from_usize)
287 }
288}
289
290#[derive(Clone, Debug)]
292struct JumpResolution {
293 target: JumpTarget,
294 condition: JumpCondition,
295}
296
297#[derive(Clone, Debug)]
299enum JumpTarget {
300 Bottom,
302 Resolved(SmallVec<[Inst; 4]>),
304 Invalid,
306 Top,
308}
309
310#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
311enum JumpCondition {
312 #[default]
313 Unknown,
314 AlwaysTrue,
315 AlwaysFalse,
316}
317
318impl JumpResolution {
319 fn new(target: JumpTarget) -> Self {
320 Self { target, condition: JumpCondition::Unknown }
321 }
322
323 fn bottom() -> Self {
324 Self::new(JumpTarget::Bottom)
325 }
326
327 fn invalid() -> Self {
328 Self::new(JumpTarget::Invalid)
329 }
330
331 fn top() -> Self {
332 Self::new(JumpTarget::Top)
333 }
334
335 fn resolved(targets: SmallVec<[Inst; 4]>) -> Self {
336 Self::new(JumpTarget::Resolved(targets))
337 }
338
339 fn single(inst: Inst) -> Self {
341 Self::resolved(SmallVec::from_elem(inst, 1))
342 }
343
344 fn as_single(&self) -> Option<Inst> {
346 match &self.target {
347 JumpTarget::Resolved(targets) if targets.len() == 1 => Some(targets[0]),
348 _ => None,
349 }
350 }
351
352 fn with_condition(mut self, condition: JumpCondition) -> Self {
353 self.condition = condition;
354 self
355 }
356
357 fn is_top(&self) -> bool {
358 matches!(self.target, JumpTarget::Top)
359 }
360
361 fn is_resolved(&self) -> bool {
362 matches!(self.target, JumpTarget::Resolved(_) | JumpTarget::Invalid)
363 }
364}
365
366#[derive(Default)]
368pub(crate) struct Cfg {
369 pub(crate) blocks: IndexVec<Block, BlockData>,
370 pub(crate) inst_to_block: IndexVec<Inst, Option<Block>>,
372}
373
374impl Bytecode<'_> {
375 fn init_snapshots(&mut self) {
377 let n = self.insts.len();
378 self.snapshots.inputs.resize(n, SmallVec::new());
379 self.snapshots.outputs.resize(n, None);
380 }
381
382 #[instrument(name = "local_jumps", level = "debug", skip_all)]
387 #[inline(never)]
388 pub(crate) fn block_analysis_local(&mut self) {
389 self.init_snapshots();
390
391 let empty_sets = ConstSetInterner::new();
392 let mut resolved = Vec::new();
393 let mut stack = Vec::new();
394 let trace_logs = enabled!(tracing::Level::TRACE);
395
396 for bid in self.cfg.blocks.indices() {
397 let block = &self.cfg.blocks[bid];
398
399 let section =
401 StackSection::from_stack_io(block.insts().map(|i| self.insts[i].stack_io()));
402
403 stack.clear();
405 stack.resize(section.inputs as usize, AbsValue::Top);
406 if !self.interpret_block(block.insts(), &mut stack) {
407 continue;
408 }
409
410 let block = &self.cfg.blocks[bid];
413 let term_inst = block.terminator();
414 let term = &self.insts[term_inst];
415 if !term.is_jump() || term.flags.contains(InstFlags::STATIC_JUMP) {
416 continue;
417 }
418
419 let target = self.resolve_jump(term_inst, &empty_sets);
420 let Some(target_inst) = target.as_single() else { continue };
421
422 if trace_logs
424 && let is_adjacent = (term_inst > 0 && {
425 let prev_inst = term_inst - 1;
426 let prev = &self.insts[prev_inst];
427 matches!(prev.opcode, op::PUSH0..=op::PUSH32)
428 && !prev.is_dead_code()
429 && block.insts.contains(&prev_inst)
430 })
431 && !is_adjacent
432 {
433 trace!(%term_inst, %target_inst, pc = self.pc(term_inst), "resolved non-adjacent jump");
434 }
435
436 resolved.push((term_inst, target));
437 }
438
439 let newly_resolved = self.commit_resolved_jumps(&resolved);
440 debug!(newly_resolved, "finished");
441 self.recompute_has_dynamic_jumps();
442 }
443
444 #[instrument(name = "ba", level = "debug", skip_all)]
448 #[inline(never)]
449 pub(crate) fn block_analysis(&mut self, local_snapshots: &Snapshots) {
450 self.init_snapshots();
451 let (resolved, count) = self.run_abstract_interp(local_snapshots);
452
453 let has_const_condition =
454 resolved.iter().any(|(_, target)| target.condition != JumpCondition::Unknown);
455 if count > 0 || has_const_condition {
456 let newly_resolved = self.commit_resolved_jumps(&resolved);
457 debug!(newly_resolved, "resolved jumps");
458 }
459
460 self.recompute_has_dynamic_jumps();
461 }
462
463 fn commit_resolved_jumps(&mut self, resolved: &[(Inst, JumpResolution)]) -> u32 {
467 let has_top_jump = resolved.iter().any(|(_, target)| target.is_top());
468
469 let mut newly_resolved = 0u32;
470 for &(jump_inst, ref target) in resolved {
471 let was_static = self.insts[jump_inst].flags.contains(InstFlags::STATIC_JUMP);
472 if was_static && target.condition == JumpCondition::Unknown {
473 continue;
474 }
475
476 if target.condition != JumpCondition::Unknown {
477 self.insts[jump_inst].flags |= InstFlags::CONST_JUMP_CONDITION;
478 }
479
480 match &target.target {
481 JumpTarget::Resolved(targets) => {
482 self.insts[jump_inst]
483 .flags
484 .remove(InstFlags::INVALID_JUMP | InstFlags::MULTI_JUMP);
485 if target.condition != JumpCondition::AlwaysFalse {
486 for &target_inst in targets {
487 debug_assert_eq!(
488 self.insts[target_inst].opcode,
489 op::JUMPDEST,
490 "block_analysis resolved to non-JUMPDEST"
491 );
492 self.insts[target_inst].set_jumpdest_reachable();
493 }
494 }
495 if targets.len() == 1 {
496 self.multi_jump_targets.remove(&jump_inst);
497 self.insts[jump_inst].flags |= InstFlags::STATIC_JUMP;
498 self.insts[jump_inst].set_static_jump_target(targets[0]);
499 trace!(%jump_inst, target_inst = %targets[0], "resolved jump");
500 } else {
501 self.insts[jump_inst].flags |=
502 InstFlags::STATIC_JUMP | InstFlags::MULTI_JUMP;
503 self.multi_jump_targets.insert(jump_inst, targets.clone());
504 trace!(%jump_inst, n_targets = targets.len(), "resolved multi-target jump");
505 }
506 if !was_static {
507 newly_resolved += 1;
508 }
509 }
510 JumpTarget::Invalid => {
511 self.multi_jump_targets.remove(&jump_inst);
512 self.insts[jump_inst].flags |= InstFlags::STATIC_JUMP | InstFlags::INVALID_JUMP;
513 self.insts[jump_inst].flags.remove(InstFlags::MULTI_JUMP);
514 if !was_static {
515 newly_resolved += 1;
516 }
517 trace!(%jump_inst, "resolved invalid jump");
518 }
519 JumpTarget::Bottom if !has_top_jump => {
520 self.multi_jump_targets.remove(&jump_inst);
523 self.insts[jump_inst].flags |= InstFlags::STATIC_JUMP | InstFlags::INVALID_JUMP;
524 self.insts[jump_inst].flags.remove(InstFlags::MULTI_JUMP);
525 if !was_static {
526 newly_resolved += 1;
527 }
528 trace!(%jump_inst, "unreachable jump");
529 }
530 JumpTarget::Bottom => {
531 trace!(%jump_inst, "unreachable jump (not marking, has_top_jump)");
535 }
536 JumpTarget::Top => {
537 trace!(%jump_inst, "unresolved jump (Top)");
538 }
539 }
540 }
541 self.recompute_reachable_jumpdests();
542 newly_resolved
543 }
544
545 #[instrument(level = "debug", name = "re_cfg", skip_all)]
547 #[inline(never)]
548 pub(crate) fn refresh_cfg(&mut self) {
549 self.recompute_has_dynamic_jumps();
550 self.recompute_reachable_jumpdests(); self.mark_dead_code(); self.compress_redirects();
553 self.rebuild_cfg();
554 }
555
556 #[instrument(level = "debug", name = "redirects", skip_all)]
557 fn compress_redirects(&mut self) {
558 for inst in self.redirects.keys().copied().collect::<Vec<_>>() {
564 let mut target = self.redirects[&inst];
565 let original = target;
566 while let Some(&next) = self.redirects.get(&target) {
567 target = next;
568 }
569 if target != original {
570 self.redirects.insert(inst, target);
571 }
572 }
573 }
574
575 #[instrument(level = "debug", name = "cfg", skip_all)]
577 fn rebuild_cfg(&mut self) {
578 let finish_block = |cfg: &mut Cfg, start: usize, end: usize| {
579 debug_assert!(start < end, "empty block range: {start}..{end}");
580 let bid = cfg.blocks.push(BlockData {
581 insts: Inst::from_usize(start)..Inst::from_usize(end),
582 preds: SmallVec::new(),
583 succs: SmallVec::new(),
584 });
585 for i in start..end {
586 cfg.inst_to_block[Inst::from_usize(i)] = Some(bid);
587 }
588 };
589
590 let n = self.insts.len();
591 assert_ne!(n, 0, "insts should never be empty");
592
593 let cfg = &mut self.cfg;
594 cfg.blocks.clear();
595 cfg.inst_to_block.clear();
596
597 let mut is_leader: BitVec = BitVec::repeat(false, n);
599 is_leader.set(0, true);
600
601 for (i, inst) in self.insts.raw.iter().enumerate() {
602 if inst.is_reachable_jumpdest(self.has_dynamic_jumps) {
608 is_leader.set(i, true);
609 }
610 if inst.is_dead_code() {
611 continue;
612 }
613 if (inst.is_branching() || inst.is_diverging()) && i + 1 < n {
614 is_leader.set(i + 1, true);
615 }
616 }
617
618 cfg.inst_to_block.resize(n, None);
621 let mut current_start = None;
622 let mut current_end = 0;
623 let mut pending_leader = false;
624
625 for i in 0..n {
626 if self.insts.raw[i].is_dead_code() {
627 pending_leader |= is_leader[i];
633 continue;
634 }
635
636 if is_leader[i] || pending_leader || current_start.is_none() {
637 if let Some(start) = current_start {
638 finish_block(cfg, start, current_end);
639 }
640 current_start = Some(i);
641 }
642 pending_leader = false;
643 current_end = i + 1;
644 }
645
646 if let Some(start) = current_start {
648 finish_block(cfg, start, current_end);
649 }
650
651 for bid in cfg.blocks.indices() {
653 let term = &self.insts[cfg.blocks[bid].terminator()];
654
655 let resolve = |target: Inst| -> Option<Block> {
657 let target = self.redirects.get(&target).copied().unwrap_or(target);
658 cfg.inst_to_block.get(target).copied().flatten()
659 };
660
661 if term.can_fall_through()
663 && let Some(target_block) = resolve(cfg.blocks[bid].terminator() + 1)
664 {
665 cfg.blocks[target_block].preds.push(bid);
666 cfg.blocks[bid].succs.push(target_block);
667 }
668
669 let term_inst = cfg.blocks[bid].terminator();
671 if term.flags.contains(InstFlags::MULTI_JUMP)
672 && let Some(targets) = self.multi_jump_targets.get(&term_inst)
673 {
674 for &t in targets {
675 if let Some(target_block) = resolve(t)
676 && !cfg.blocks[bid].succs.contains(&target_block)
677 {
678 cfg.blocks[target_block].preds.push(bid);
679 cfg.blocks[bid].succs.push(target_block);
680 }
681 }
682 } else if term.is_static_jump()
683 && !term.flags.contains(InstFlags::INVALID_JUMP)
684 && let Some(target_block) = resolve(term.static_jump_target())
685 && !cfg.blocks[bid].succs.contains(&target_block)
686 {
687 cfg.blocks[target_block].preds.push(bid);
688 cfg.blocks[bid].succs.push(target_block);
689 }
690 }
691
692 assert_ne!(cfg.blocks.len(), 0, "should always build at least one block");
693 }
694
695 fn run_abstract_interp(
700 &mut self,
701 local_snapshots: &Snapshots,
702 ) -> (Vec<(Inst, JumpResolution)>, usize) {
703 let num_blocks = self.cfg.blocks.len();
704
705 let mut block_states: IndexVec<Block, BlockState> =
707 index_vec![BlockState::Bottom; num_blocks];
708 block_states[Block::from_usize(0)] = BlockState::Known(Vec::new());
709
710 let mut jump_insts: Vec<Inst> = Vec::new();
713 for (i, inst) in self.insts.iter_enumerated() {
714 if inst.is_jump()
715 && (!inst.flags.contains(InstFlags::STATIC_JUMP)
716 || (inst.opcode == op::JUMPI && !inst.has_const_jumpi_condition()))
717 {
718 jump_insts.push(i);
719 }
720 }
721
722 let mut const_sets = ConstSetInterner::new();
723 let (discovered_edges, converged) = self.run_fixpoint(&mut block_states, &mut const_sets);
724
725 if !converged {
728 self.snapshots.restore_from(self.insts.indices(), local_snapshots);
729 }
730
731 let mut jump_targets: Vec<(Inst, JumpResolution)> = Vec::new();
733 let mut has_top_jump = false;
734 for &jump_inst in &jump_insts {
735 let target = self.resolve_jump(jump_inst, &const_sets);
736 if target.is_top() {
737 has_top_jump = true;
738 }
739 jump_targets.push((jump_inst, target));
740 }
741
742 if has_top_jump || !converged {
746 self.invalidate_suspect_jumps(
747 &mut jump_targets,
748 &block_states,
749 &discovered_edges,
750 local_snapshots,
751 );
752 }
753
754 let count = jump_targets.iter().filter(|(_, target)| target.is_resolved()).count();
755
756 (jump_targets, count)
757 }
758
759 fn resolve_jump(&self, jump_inst: Inst, const_sets: &ConstSetInterner) -> JumpResolution {
760 let snap = &self.snapshots.inputs[jump_inst];
761 let condition = if self.insts[jump_inst].opcode == op::JUMPI {
762 snap.first()
763 .map(|&value| self.resolve_jump_condition(value, const_sets))
764 .unwrap_or_default()
765 } else {
766 JumpCondition::Unknown
767 };
768
769 if condition == JumpCondition::AlwaysFalse {
770 return JumpResolution::single(jump_inst + 1)
771 .with_condition(JumpCondition::AlwaysFalse);
772 }
773
774 match snap.last() {
775 Some(&operand) => {
776 self.resolve_jump_operand(operand, const_sets).with_condition(condition)
777 }
778 None => {
779 trace!(%jump_inst, pc = self.pc(jump_inst), "jump in unreached block");
780 JumpResolution::bottom()
781 }
782 }
783 }
784
785 fn resolve_jump_operand(
787 &self,
788 operand: AbsValue,
789 const_sets: &ConstSetInterner,
790 ) -> JumpResolution {
791 match operand {
792 AbsValue::Const(imm) => {
793 let val = imm.get(&self.u256_interner.borrow());
794 match usize::try_from(val) {
795 Ok(target_pc) if self.is_valid_jump(target_pc) => {
796 JumpResolution::single(self.pc_to_inst(target_pc))
797 }
798 _ => JumpResolution::invalid(),
799 }
800 }
801 AbsValue::ConstSet(set_idx) => {
802 let consts = const_sets.get(set_idx);
803 let interner = self.u256_interner.borrow();
804 let mut targets = SmallVec::new();
805 for &imm in consts {
806 let val = imm.get(&interner);
807 match usize::try_from(val) {
808 Ok(pc) if self.is_valid_jump(pc) => {
809 targets.push(self.pc_to_inst(pc));
810 }
811 _ => {
812 return JumpResolution::top();
815 }
816 }
817 }
818 if !targets.is_empty() {
819 JumpResolution::resolved(targets)
820 } else {
821 JumpResolution::invalid()
822 }
823 }
824 AbsValue::Top => JumpResolution::top(),
825 }
826 }
827
828 fn resolve_jump_condition(
829 &self,
830 condition: AbsValue,
831 const_sets: &ConstSetInterner,
832 ) -> JumpCondition {
833 let consts = match condition {
834 AbsValue::Const(imm) => Either::Left(std::iter::once(imm)),
835 AbsValue::ConstSet(set_idx) => Either::Right(const_sets.get(set_idx).iter().copied()),
836 AbsValue::Top => return JumpCondition::Unknown,
837 };
838 let interner = self.u256_interner.borrow();
839 let mut saw_zero = false;
840 let mut saw_nonzero = false;
841 for imm in consts {
842 if imm.get(&interner).is_zero() {
843 saw_zero = true;
844 } else {
845 saw_nonzero = true;
846 }
847 }
848 match (saw_zero, saw_nonzero) {
849 (true, false) => JumpCondition::AlwaysFalse,
850 (false, true) => JumpCondition::AlwaysTrue,
851 _ => JumpCondition::Unknown,
852 }
853 }
854
855 fn local_jumpi_condition(&self, jump_inst: Inst, local_snapshots: &Snapshots) -> JumpCondition {
856 if self.insts[jump_inst].opcode != op::JUMPI {
857 return JumpCondition::Unknown;
858 }
859 let Some(condition) = local_snapshots.inputs[jump_inst].first().copied() else {
860 return JumpCondition::Unknown;
861 };
862 let Some(imm) = condition.as_const() else {
863 return JumpCondition::Unknown;
864 };
865 if imm.get(&self.u256_interner.borrow()).is_zero() {
866 JumpCondition::AlwaysFalse
867 } else {
868 JumpCondition::AlwaysTrue
869 }
870 }
871
872 fn discover_jump_edges(
874 &self,
875 operand: AbsValue,
876 bid: Block,
877 const_sets: &ConstSetInterner,
878 discovered: &mut IndexVec<Block, SmallVec<[Block; 4]>>,
879 disc_preds: &mut IndexVec<Block, SmallVec<[Block; 4]>>,
880 ) {
881 let consts = match operand {
882 AbsValue::Const(imm) => Either::Left(std::iter::once(imm)),
883 AbsValue::ConstSet(set_idx) => Either::Right(const_sets.get(set_idx).iter().copied()),
884 AbsValue::Top => return,
885 };
886 let interner = self.u256_interner.borrow();
887 for imm in consts {
888 let Ok(target_pc) = usize::try_from(imm.get(&interner)) else { continue };
889 if !self.is_valid_jump(target_pc) {
890 continue;
891 }
892 let ti = self.pc_to_inst(target_pc);
893 if let Some(tb) = self.cfg.inst_to_block[ti]
894 && !discovered[bid].contains(&tb)
895 {
896 discovered[bid].push(tb);
897 disc_preds[tb].push(bid);
898 }
899 }
900 }
901
902 fn invalidate_suspect_jumps(
914 &mut self,
915 jump_targets: &mut [(Inst, JumpResolution)],
916 block_states: &IndexVec<Block, BlockState>,
917 discovered_edges: &IndexVec<Block, SmallVec<[Block; 4]>>,
918 local_snapshots: &Snapshots,
919 ) {
920 let num_blocks = self.cfg.blocks.len();
921
922 let mut suspect: BitVec = BitVec::repeat(false, num_blocks);
924 for bid in self.cfg.blocks.indices() {
925 if !matches!(block_states[bid], BlockState::Bottom)
926 && self.insts[self.cfg.blocks[bid].insts.start].is_jumpdest()
927 {
928 suspect.set(bid.index(), true);
929 }
930 }
931
932 let mut propagate = Worklist::new(num_blocks);
934 for bid in self.cfg.blocks.indices() {
935 if suspect[bid.index()] {
936 propagate.push(bid);
937 }
938 }
939 while let Some(bid) = propagate.pop() {
940 for &succ in self.cfg.blocks[bid].succs.iter().chain(discovered_edges[bid].iter()) {
941 let si = succ.index();
942 if !suspect[si] {
943 suspect.set(si, true);
944 propagate.push(succ);
945 }
946 }
947 }
948
949 for (inst, target) in jump_targets.iter_mut() {
950 if let Some(bid) = self.cfg.inst_to_block[*inst]
951 && suspect[bid.index()]
952 {
953 let condition = self.local_jumpi_condition(*inst, local_snapshots);
954 if condition == JumpCondition::AlwaysFalse {
955 *target = JumpResolution::single(*inst + 1).with_condition(condition);
956 continue;
957 }
958 target.condition = condition;
959 if target.is_resolved() {
960 *target = JumpResolution::top().with_condition(condition);
961 }
962 }
963 }
964
965 for bid in self.cfg.blocks.indices() {
970 if !suspect[bid.index()] {
971 continue;
972 }
973 self.snapshots.restore_from(self.cfg.blocks[bid].insts(), local_snapshots);
974 }
975 }
976
977 fn run_fixpoint(
981 &mut self,
982 block_states: &mut IndexVec<Block, BlockState>,
983 const_sets: &mut ConstSetInterner,
984 ) -> (IndexVec<Block, SmallVec<[Block; 4]>>, bool) {
985 let num_blocks = self.cfg.blocks.len();
986 let mut worklist = Worklist::new(num_blocks);
987 worklist.push(Block::from_usize(0));
988
989 let mut discovered: IndexVec<Block, SmallVec<[Block; 4]>> =
991 IndexVec::from_vec(vec![SmallVec::new(); num_blocks]);
992 let mut disc_preds: IndexVec<Block, SmallVec<[Block; 4]>> =
994 IndexVec::from_vec(vec![SmallVec::new(); num_blocks]);
995
996 let max_iterations = num_blocks * 8;
997 let mut iterations = 0;
998 let mut converged = true;
999
1000 let mut stack_buf: Vec<AbsValue> = Vec::with_capacity(MAX_ABS_STACK_DEPTH);
1002
1003 while let Some(bid) = worklist.pop() {
1004 iterations += 1;
1005 if iterations > max_iterations {
1006 converged = false;
1007 break;
1008 }
1009
1010 stack_buf.clear();
1012 match &block_states[bid] {
1013 BlockState::Known(s) => stack_buf.extend_from_slice(s),
1014 BlockState::Bottom => continue,
1015 };
1016
1017 let block = &self.cfg.blocks[bid];
1018 if !self.interpret_block(block.insts(), &mut stack_buf) {
1019 continue;
1020 }
1021 let block = &self.cfg.blocks[bid];
1022
1023 let term_inst = block.terminator();
1025 let term = &self.insts[term_inst];
1026 if term.is_jump()
1027 && !term.flags.contains(InstFlags::STATIC_JUMP)
1028 && let Some(&operand) = self.snapshots.inputs[term_inst].last()
1029 {
1030 self.discover_jump_edges(
1031 operand,
1032 bid,
1033 const_sets,
1034 &mut discovered,
1035 &mut disc_preds,
1036 );
1037 }
1038
1039 for &succ in block.succs.iter().chain(&discovered[bid]) {
1041 if block_states[succ].join(&stack_buf, const_sets) {
1042 worklist.push(succ);
1043 }
1044 }
1045 }
1046
1047 debug!(
1048 "{msg} after {iterations} iterations (max={max_iterations})",
1049 msg = if converged { "converged" } else { "did not converge" },
1050 );
1051
1052 (discovered, converged)
1053 }
1054
1055 fn interpret_block(
1061 &mut self,
1062 insts: impl IntoIterator<Item = Inst>,
1063 stack: &mut Vec<AbsValue>,
1064 ) -> bool {
1065 for i in insts {
1066 let inst = &self.insts[i];
1067 if inst.is_dead_code() {
1068 continue;
1069 }
1070
1071 let (inp, out) = inst.stack_io();
1072 let inp = inp as usize;
1073 let out = out as usize;
1074
1075 if inp > 0 {
1077 let start = stack.len().saturating_sub(inp);
1078 let snap = &mut self.snapshots.inputs[i];
1079 snap.clear();
1080 snap.extend_from_slice(&stack[start..]);
1081 }
1082
1083 match inst.opcode {
1084 op::PUSH0..=op::PUSH32 => {
1085 stack.push(AbsValue::Const(inst.imm()));
1086 }
1087 op::POP => {
1088 if stack.pop().is_none() {
1089 return false;
1090 }
1091 }
1092 op::DUP1..=op::DUP16 => {
1093 let depth = (inst.opcode - op::DUP1 + 1) as usize;
1094 if stack.len() < depth {
1095 return false;
1096 }
1097 stack.push(stack[stack.len() - depth]);
1098 }
1099 op::SWAP1..=op::SWAP16 => {
1100 let depth = (inst.opcode - op::SWAP1 + 1) as usize;
1101 let len = stack.len();
1102 if len < depth + 1 {
1103 return false;
1104 }
1105 stack.swap(len - 1, len - 1 - depth);
1106 }
1107 op::DUPN => {
1108 let depth = crate::decode_single(inst.imm_byte());
1109 match depth {
1110 Some(n) => {
1111 let n = n as usize;
1112 if stack.len() < n {
1113 stack.push(AbsValue::Top);
1117 } else {
1118 stack.push(stack[stack.len() - n]);
1119 }
1120 }
1121 None => return false,
1122 }
1123 }
1124 op::SWAPN => {
1125 let depth = crate::decode_single(inst.imm_byte());
1126 match depth {
1127 Some(n) => {
1128 let n = n as usize;
1129 let len = stack.len();
1130 if len < n + 1 {
1131 if let Some(tos) = stack.last_mut() {
1134 *tos = AbsValue::Top;
1135 }
1136 } else {
1137 stack.swap(len - 1, len - 1 - n);
1138 }
1139 }
1140 None => return false,
1141 }
1142 }
1143 op::EXCHANGE => {
1144 let pair = crate::decode_pair(inst.imm_byte());
1145 match pair {
1146 Some((n, m)) => {
1147 let (n, m) = (n as usize, m as usize);
1148 let len = stack.len();
1149 if len < m + 1 {
1150 if len > n {
1153 stack[len - 1 - n] = AbsValue::Top;
1154 }
1155 } else {
1156 stack.swap(len - 1 - n, len - 1 - m);
1157 }
1158 }
1159 None => return false,
1160 }
1161 }
1162 _ => {
1163 if stack.len() < inp {
1164 return false;
1165 }
1166
1167 let result = if out > 0 && self.compiler_gas_used < self.compiler_gas_limit {
1169 let inputs_slice = &stack[stack.len() - inp..];
1170 let mut interner = self.u256_interner.borrow_mut();
1171
1172 let gas =
1174 super::const_fold::const_fold_gas(inst.opcode, inputs_slice, &interner);
1175 if let Some(cost) = gas
1176 && self.compiler_gas_used.saturating_add(cost)
1177 <= self.compiler_gas_limit
1178 {
1179 let folded = super::const_fold::try_const_fold(
1180 inst,
1181 inputs_slice,
1182 &mut interner,
1183 self.code.len(),
1184 );
1185 if folded.is_some() {
1186 self.compiler_gas_used += cost;
1187 }
1188 folded
1189 } else {
1190 None
1191 }
1192 } else {
1193 None
1194 };
1195
1196 stack.truncate(stack.len() - inp);
1198
1199 if let Some(folded) = result {
1201 debug_assert_eq!(out, 1);
1202 stack.push(folded);
1203 } else {
1204 stack.resize(stack.len() + out, AbsValue::Top);
1205 }
1206 }
1207 }
1208
1209 if out > 0 && !matches!(inst.opcode, op::SWAP1..=op::SWAP16 | op::SWAPN | op::EXCHANGE)
1212 {
1213 self.snapshots.outputs[i] = stack.last().copied();
1214 }
1215
1216 #[cfg(test)]
1217 if inst.opcode == crate::TEST_SUSPEND {
1218 stack.fill(AbsValue::Top);
1219 }
1220 }
1221
1222 true
1223 }
1224}
1225
1226#[cfg(test)]
1227pub(crate) mod tests {
1228 use super::*;
1229 pub(crate) use crate::bytecode::Inst;
1230 pub(crate) use revm_primitives::{U256, hardfork::SpecId, hex};
1231
1232 pub(crate) fn analyze_hex(hex: &str) -> Bytecode<'static> {
1233 let code = hex::decode(hex.trim()).unwrap();
1234 analyze_code(code)
1235 }
1236
1237 pub(crate) fn analyze_code(code: Vec<u8>) -> Bytecode<'static> {
1238 analyze_code_with(code, Default::default())
1239 }
1240
1241 pub(crate) fn analyze_code_spec(code: Vec<u8>, spec_id: SpecId) -> Bytecode<'static> {
1242 crate::tests::init_tracing();
1243
1244 eprintln!("{}", hex::encode(&code));
1245 let mut bytecode = Bytecode::new(code, spec_id, None);
1246 bytecode.analyze().unwrap();
1247 eprintln!("{bytecode}");
1248 bytecode
1249 }
1250
1251 pub(crate) fn analyze_code_with(
1252 code: Vec<u8>,
1253 config: crate::bytecode::AnalysisConfig,
1254 ) -> Bytecode<'static> {
1255 crate::tests::init_tracing();
1256
1257 eprintln!("{}", hex::encode(&code));
1258 let mut bytecode = Bytecode::test(code);
1259 bytecode.config = config;
1260 bytecode.analyze().unwrap();
1261 eprintln!("{bytecode}");
1262 bytecode
1263 }
1264
1265 pub(crate) fn analyze_asm(src: &str) -> Bytecode<'static> {
1266 analyze_code(crate::parse_asm(src).unwrap())
1267 }
1268
1269 pub(crate) fn analyze_asm_spec(src: &str, spec_id: SpecId) -> Bytecode<'static> {
1270 analyze_code_spec(crate::parse_asm(src).unwrap(), spec_id)
1271 }
1272
1273 pub(crate) fn analyze_asm_with(
1274 src: &str,
1275 config: crate::bytecode::AnalysisConfig,
1276 ) -> Bytecode<'static> {
1277 analyze_code_with(crate::parse_asm(src).unwrap(), config)
1278 }
1279
1280 #[test]
1281 fn revert_sub_call_storage_oog() {
1282 let bytecode = analyze_hex(
1283 "60606040526000357c0100000000000000000000000000000000000000000000000000000000900463ffffffff168063b28175c4146046578063c0406226146052575b6000565b3460005760506076565b005b34600057605c6081565b604051808215151515815260200191505060405180910390f35b600c6000819055505b565b600060896076565b600d600181905550600e600281905550600190505b905600a165627a7a723058202a8a75d7d795b5bcb9042fb18b283daa90b999a11ddec892f5487322",
1284 );
1285 assert!(bytecode.has_dynamic_jumps());
1286 }
1287
1288 #[test]
1289 fn revert_remote_sub_call_storage_oog() {
1290 let bytecode = analyze_hex(
1291 "608060405234801561001057600080fd5b506004361061002b5760003560e01c806373027f6d14610030575b600080fd5b61004a600480360381019061004591906101a9565b61004c565b005b6000808273ffffffffffffffffffffffffffffffffffffffff166040516024016040516020818303038152906040527fb28175c4000000000000000000000000000000000000000000000000000000007bffffffffffffffffffffffffffffffffffffffffffffffffffffffff19166020820180517bffffffffffffffffffffffffffffffffffffffffffffffffffffff83818316178352505050506040516100f69190610247565b6000604051808303816000865af19150503d8060008114610133576040519150601f19603f3d011682016040523d82523d6000602084013e610138565b606091505b509150915081600155505050565b600080fd5b600073ffffffffffffffffffffffffffffffffffffffff82169050919050565b60006101768261014b565b9050919050565b6101868161016b565b811461019157600080fd5b50565b6000813590506101a38161017d565b92915050565b6000602082840312156101bf576101be610146565b5b60006101cd84828501610194565b91505092915050565b600081519050919050565b600081905092915050565b60005b8381101561020a5780820151818401526020810190506101ef565b60008484015250505050565b6000610221826101d6565b61022b81856101e1565b935061023b8185602086016101ec565b80840191505092915050565b60006102538284610216565b91508190509291505056fea2646970667358221220b4673c55c7b0268d7d118059e6509196d2185bb7fe040a7d3900f902c8542ea464736f6c63430008180033",
1292 );
1293 assert!(bytecode.has_dynamic_jumps());
1294 }
1295
1296 #[test]
1297 fn trans_storage_ok() {
1298 let contracts = [
1299 "366012575b600b5f6020565b5f5260205ff35b601c6160a75f6024565b6004565b5c90565b5d56",
1300 "60106001600a5f6012565b015f6016565b005b5c90565b5d56",
1301 "3033146033575b303303600e57005b601b5f35806001555f608d565b5f80808080305af1600255602e60016089565b600355005b603a5f6089565b8015608757604a600182035f608d565b5f80808080305af1156083576001606191035f608d565b5f80808080305af115608357607f60016078816089565b016001608d565b6006565b5f80fd5b005b5c90565b5d56",
1302 "60065f601d565b5f5560106001601d565b6001555f80808080335af1005b5c9056",
1303 ];
1304 let has_dynamic = [false, false, true, false];
1305 for (hex, &expected) in contracts.iter().zip(&has_dynamic) {
1306 let bytecode = analyze_hex(hex);
1307 assert_eq!(bytecode.has_dynamic_jumps(), expected, "contract: {hex}");
1308 }
1309 }
1310
1311 #[test]
1312 fn const_operand_basic() {
1313 let bytecode = analyze_asm(
1321 "
1322 PUSH1 0x42
1323 PUSH1 0x01
1324 ADD
1325 PUSH1 0x00
1326 MSTORE
1327 JUMPDEST
1328 STOP
1329 ",
1330 );
1331 assert_eq!(bytecode.const_operand(Inst::from_usize(2), 0), Some(U256::from(0x01)));
1333 assert_eq!(bytecode.const_operand(Inst::from_usize(2), 1), Some(U256::from(0x42)));
1334 assert_eq!(bytecode.const_operand(Inst::from_usize(4), 0), Some(U256::from(0x00)));
1336 assert_eq!(bytecode.const_operand(Inst::from_usize(4), 1), Some(U256::from(0x43)));
1337 }
1338
1339 #[test]
1340 fn const_operand_dynamic() {
1341 let bytecode = analyze_asm(
1343 "
1344 PUSH0
1345 CALLDATALOAD
1346 PUSH0
1347 MSTORE
1348 STOP
1349 ",
1350 );
1351 assert_eq!(bytecode.const_operand(Inst::from_usize(3), 0), Some(U256::ZERO));
1353 assert_eq!(bytecode.const_operand(Inst::from_usize(3), 1), None);
1354 }
1355
1356 #[test]
1357 fn const_output_basic() {
1358 let bytecode = analyze_asm(
1365 "
1366 PUSH1 0x42
1367 PUSH1 0x01
1368 ADD
1369 PUSH0
1370 MSTORE
1371 STOP
1372 ",
1373 );
1374 assert_eq!(bytecode.const_output(Inst::from_usize(0)), Some(U256::from(0x42)));
1375 assert_eq!(bytecode.const_output(Inst::from_usize(1)), Some(U256::from(0x01)));
1376 assert_eq!(bytecode.const_output(Inst::from_usize(2)), Some(U256::from(0x43)));
1377 assert_eq!(bytecode.const_output(Inst::from_usize(3)), Some(U256::ZERO));
1378 assert_eq!(bytecode.const_output(Inst::from_usize(4)), None);
1380 }
1381
1382 #[test]
1383 fn const_output_dynamic() {
1384 let bytecode = analyze_asm(
1386 "
1387 PUSH0
1388 CALLDATALOAD
1389 STOP
1390 ",
1391 );
1392 assert_eq!(bytecode.const_output(Inst::from_usize(0)), Some(U256::ZERO));
1393 assert_eq!(bytecode.const_output(Inst::from_usize(1)), None);
1394 }
1395
1396 #[test]
1398 fn const_snapshot_eof_stack_ops() {
1399 let mut code: Vec<u8> = vec![op::PUSH0; 16];
1402 code.extend([op::PUSH1, 0xAA]); code.extend([op::DUPN, 0x80]); code.push(op::STOP); let bytecode = analyze_code_spec(code, SpecId::AMSTERDAM);
1406 assert_eq!(bytecode.const_output(Inst::from_usize(17)), Some(U256::ZERO));
1408 assert_eq!(bytecode.const_output(Inst::from_usize(16)), Some(U256::from(0xAA)));
1410 }
1411
1412 #[test]
1413 fn snapshots_all_blocks() {
1414 let bytecode = analyze_asm(
1419 "
1420 PUSH1 0x42 ; inst 0
1421 PUSH1 0x01 ; inst 1
1422 ADD ; inst 2: 0x42 + 0x01 = 0x43
1423 PUSH0 ; inst 3
1424 CALLDATALOAD ; inst 4: opaque value
1425 JUMP ; inst 5: dynamic, unresolvable
1426 target1:
1427 JUMPDEST ; inst 6
1428 PUSH1 0x01 ; inst 7
1429 ADD ; inst 8
1430 STOP ; inst 9
1431 target2:
1432 JUMPDEST ; inst 10
1433 PUSH1 0x02 ; inst 11
1434 SUB ; inst 12
1435 STOP ; inst 13
1436 ",
1437 );
1438 assert!(bytecode.has_dynamic_jumps, "expected unresolved dynamic jump");
1440 let jump = bytecode.inst(Inst::from_usize(5));
1441 assert!(jump.is_jump());
1442 assert!(!jump.flags.contains(InstFlags::STATIC_JUMP));
1443 assert_eq!(bytecode.const_operand(Inst::from_usize(5), 0), None);
1445
1446 assert_eq!(bytecode.const_operand(Inst::from_usize(2), 0), Some(U256::from(0x01)));
1448 assert_eq!(bytecode.const_operand(Inst::from_usize(2), 1), Some(U256::from(0x42)));
1449 assert_eq!(bytecode.const_output(Inst::from_usize(2)), Some(U256::from(0x43)));
1450
1451 assert_eq!(bytecode.const_operand(Inst::from_usize(8), 0), Some(U256::from(0x01)));
1453 assert_eq!(bytecode.const_operand(Inst::from_usize(8), 1), None);
1454 assert_eq!(bytecode.const_output(Inst::from_usize(8)), None);
1455
1456 assert_eq!(bytecode.const_operand(Inst::from_usize(12), 0), Some(U256::from(0x02)));
1458 assert_eq!(bytecode.const_operand(Inst::from_usize(12), 1), None);
1459 assert_eq!(bytecode.const_output(Inst::from_usize(12)), None);
1460 }
1461
1462 #[test]
1463 fn multi_target_jump() {
1464 let bytecode = analyze_asm(
1467 "
1468 ; Call site 1.
1469 PUSH %ret1 ; push return address
1470 PUSH %func ; push function entry
1471 JUMP ; call function (static: PUSH+JUMP)
1472 ret1:
1473 JUMPDEST
1474 POP ; consume function result
1475 ; Call site 2.
1476 PUSH %ret2 ; push return address
1477 PUSH %func ; push function entry
1478 JUMP ; call function (static: PUSH+JUMP)
1479 ret2:
1480 JUMPDEST
1481 POP ; consume function result
1482 STOP
1483 ; Internal function.
1484 func:
1485 JUMPDEST
1486 PUSH1 0x42 ; push a result
1487 SWAP1 ; swap result and return address
1488 JUMP ; return (dynamic)
1489 ",
1490 );
1491
1492 let return_jump = bytecode
1494 .iter_insts()
1495 .find(|(_, d)| d.is_jump() && d.flags.contains(InstFlags::MULTI_JUMP));
1496 assert!(return_jump.is_some(), "expected a multi-target jump");
1497 let (rj_inst, _) = return_jump.unwrap();
1498 let targets = bytecode.multi_jump_targets(rj_inst).unwrap();
1499 assert_eq!(targets.len(), 2, "expected 2 targets, got {}", targets.len());
1500 for &t in targets {
1502 assert_eq!(bytecode.inst(t).opcode, op::JUMPDEST);
1503 }
1504 assert!(!bytecode.has_dynamic_jumps, "expected no dynamic jumps");
1506 }
1507
1508 #[test]
1529 fn known_jumpdest_unsound_resolution() {
1530 let bytecode = analyze_asm(
1531 "
1532 ; Call site 1.
1533 PUSH %ret1 ; pc=0
1534 PUSH %fn_entry ; pc=2
1535 JUMP ; pc=4: -> fn_entry
1536 ret1:
1537 JUMPDEST ; pc=5
1538 POP ; pc=6: drop result
1539 ; Call site 2.
1540 PUSH %ret2 ; pc=7
1541 PUSH %fn_entry ; pc=9
1542 JUMP ; pc=11: -> fn_entry
1543 ret2:
1544 JUMPDEST ; pc=12
1545 POP ; pc=13: drop result
1546 ; Opaque jump: loads target from memory (Top).
1547 PUSH0 ; pc=14
1548 MLOAD ; pc=15: -> Top
1549 JUMP ; pc=16: dynamic, target = Top
1550 INVALID ; pc=17
1551 INVALID ; pc=18
1552 INVALID ; pc=19
1553 ; Internal function: pushes result, swaps with return addr, jumps back.
1554 fn_entry:
1555 JUMPDEST ; pc=20
1556 PUSH1 0x42 ; pc=21: result
1557 SWAP1 ; pc=23: swap result and return addr
1558 JUMP ; pc=24: return (UNSOUND: resolved to Multi)
1559 ",
1560 );
1561
1562 let return_jump = bytecode.iter_insts().find(|(i, d)| bytecode.pc(*i) == 24 && d.is_jump());
1566 let (_, rj) = return_jump.unwrap();
1567 assert!(
1568 !rj.flags.contains(InstFlags::STATIC_JUMP),
1569 "unsound: return jump should not be resolved"
1570 );
1571 }
1572
1573 #[test]
1574 fn nested_call_return() {
1575 let bytecode = analyze_asm(
1589 "
1590 ; Call site 1: direct call to inner.
1591 PUSH %ret1 ; pc=0
1592 PUSH %inner ; pc=2
1593 JUMP ; pc=4: -> inner
1594 ret1:
1595 JUMPDEST ; pc=5
1596 POP ; pc=6: drop result
1597 ; Call site 2: call through wrapper.
1598 PUSH %ret2 ; pc=7
1599 PUSH1 0x42 ; pc=9: an argument
1600 PUSH %wrapper ; pc=11
1601 JUMP ; pc=13: -> wrapper
1602 ret2:
1603 JUMPDEST ; pc=14
1604 POP ; pc=15: drop result
1605 POP ; pc=16: drop arg
1606 STOP ; pc=17
1607 INVALID ; pc=18
1608 ; Wrapper function: calls inner, then returns to caller.
1609 ; Entry stack: [outer_ret, arg]
1610 wrapper:
1611 JUMPDEST ; pc=19
1612 PUSH %ret_w ; pc=20: push return addr for inner
1613 PUSH %inner ; pc=22: push inner entry
1614 JUMP ; pc=24: -> inner
1615 INVALID ; pc=25
1616 INVALID ; pc=26
1617 ret_w:
1618 JUMPDEST ; pc=27: inner returned here
1619 ; Stack: [outer_ret, arg, inner_result]
1620 POP ; pc=28: drop inner_result
1621 POP ; pc=29: drop arg
1622 JUMP ; pc=30: return to caller via outer_ret (dynamic)
1623 INVALID ; pc=31
1624 ; Inner function: pushes a result and returns.
1625 ; Entry stack: [..., ret_addr]
1626 inner:
1627 JUMPDEST ; pc=32
1628 PUSH1 0x42 ; pc=33: push result
1629 SWAP1 ; pc=35
1630 JUMP ; pc=36: jump to ret_addr
1631 ",
1632 );
1633
1634 assert!(bytecode.has_dynamic_jumps, "expected dynamic jumps to remain");
1640 }
1641
1642 #[test]
1645 fn amsterdam_deep_dupn_jump_not_invalid() {
1646 let code = crate::parse_asm(
1650 "
1651 PUSH %target
1652 ; 69 × PUSH0
1653 PUSH0 PUSH0 PUSH0 PUSH0 PUSH0 PUSH0 PUSH0 PUSH0 PUSH0 PUSH0
1654 PUSH0 PUSH0 PUSH0 PUSH0 PUSH0 PUSH0 PUSH0 PUSH0 PUSH0 PUSH0
1655 PUSH0 PUSH0 PUSH0 PUSH0 PUSH0 PUSH0 PUSH0 PUSH0 PUSH0 PUSH0
1656 PUSH0 PUSH0 PUSH0 PUSH0 PUSH0 PUSH0 PUSH0 PUSH0 PUSH0 PUSH0
1657 PUSH0 PUSH0 PUSH0 PUSH0 PUSH0 PUSH0 PUSH0 PUSH0 PUSH0 PUSH0
1658 PUSH0 PUSH0 PUSH0 PUSH0 PUSH0 PUSH0 PUSH0 PUSH0 PUSH0 PUSH0
1659 PUSH0 PUSH0 PUSH0 PUSH0 PUSH0 PUSH0 PUSH0 PUSH0 PUSH0
1660 PUSH %resolved
1661 PUSH %dispatcher
1662 JUMP
1663 dispatcher:
1664 JUMPDEST
1665 JUMP
1666 resolved:
1667 JUMPDEST
1668 DUPN 70
1669 POP
1670 problem:
1671 JUMPDEST
1672 DUPN 70
1673 JUMP
1674 target:
1675 JUMPDEST
1676 STOP
1677 ",
1678 )
1679 .unwrap();
1680 let bytecode = analyze_code_spec(code, SpecId::AMSTERDAM);
1681
1682 let jumps: Vec<_> = bytecode.iter_insts().filter(|(_, d)| d.opcode == op::JUMP).collect();
1684 for &(inst, data) in &jumps {
1685 assert!(
1686 !data.flags.contains(InstFlags::INVALID_JUMP),
1687 "JUMP inst={inst} pc={} incorrectly marked INVALID_JUMP",
1688 bytecode.pc(inst),
1689 );
1690 }
1691 }
1692
1693 #[test]
1694 fn hash_10k() {
1695 let code = fixture_entry_code(include_str!("../../../../../data/hash_10k.json"));
1696 let mut bytecode = Bytecode::test(code);
1697 bytecode.analyze().unwrap();
1698 assert!(!bytecode.has_dynamic_jumps, "expected all jumps to be resolved");
1699 }
1700
1701 fn fixture_entry_code(json: &str) -> Vec<u8> {
1702 let v: serde_json::Value = serde_json::from_str(json).unwrap();
1703 let case = v.as_object().unwrap().values().next().unwrap();
1704 let to = case["transaction"][0]["to"].as_str().unwrap();
1705 let code = case["pre"][to]["code"].as_str().unwrap().trim_start_matches("0x");
1706 revm_primitives::hex::decode(code).unwrap()
1707 }
1708}
1709
1710#[cfg(test)]
1711mod tests_edge_cases {
1712 use super::{tests::*, *};
1713
1714 #[test]
1717 fn multi_target_jump_three_callers() {
1718 let bytecode = analyze_asm(
1719 "
1720 ; Call site 1.
1721 PUSH %ret1
1722 PUSH %func
1723 JUMP
1724 ret1:
1725 JUMPDEST
1726 POP
1727 ; Call site 2.
1728 PUSH %ret2
1729 PUSH %func
1730 JUMP
1731 ret2:
1732 JUMPDEST
1733 POP
1734 ; Call site 3.
1735 PUSH %ret3
1736 PUSH %func
1737 JUMP
1738 ret3:
1739 JUMPDEST
1740 POP
1741 STOP
1742 ; Internal function.
1743 func:
1744 JUMPDEST
1745 PUSH1 0x42
1746 SWAP1
1747 JUMP
1748 ",
1749 );
1750
1751 let return_jump = bytecode
1752 .iter_insts()
1753 .find(|(_, d)| d.is_jump() && d.flags.contains(InstFlags::MULTI_JUMP));
1754 assert!(return_jump.is_some(), "expected a multi-target jump");
1755 let (rj_inst, _) = return_jump.unwrap();
1756 let targets = bytecode.multi_jump_targets(rj_inst).unwrap();
1757 assert_eq!(targets.len(), 3, "expected 3 targets, got {}", targets.len());
1758 assert!(!bytecode.has_dynamic_jumps, "expected no dynamic jumps");
1759 }
1760
1761 #[test]
1763 fn dup_swap_const_propagation() {
1764 let bytecode = analyze_asm(
1772 "
1773 PUSH1 0xAA ; inst 0
1774 PUSH1 0xBB ; inst 1
1775 DUP2 ; inst 2
1776 SWAP1 ; inst 3
1777 PUSH0 ; inst 4
1778 MSTORE ; inst 5
1779 STOP ; inst 6
1780 ",
1781 );
1782 assert_eq!(bytecode.const_output(Inst::from_usize(2)), Some(U256::from(0xAA)));
1784 assert_eq!(bytecode.const_operand(Inst::from_usize(5), 0), Some(U256::ZERO));
1786 assert_eq!(bytecode.const_operand(Inst::from_usize(5), 1), Some(U256::from(0xBB)));
1787 }
1788
1789 #[test]
1791 fn jumpi_const_condition_snapshot() {
1792 let bytecode = analyze_asm(
1793 "
1794 PUSH1 0x01 ; inst 0: condition = 1 (always taken)
1795 PUSH %target ; inst 1: target
1796 JUMPI ; inst 2: static JUMPI
1797 STOP ; inst 3: fallthrough
1798 target:
1799 JUMPDEST ; inst 4: target
1800 STOP ; inst 5
1801 ",
1802 );
1803 let (jump_inst, jump) =
1805 bytecode.iter_insts().find(|(_, data)| data.opcode == op::JUMPI).unwrap();
1806 assert!(jump.flags.contains(InstFlags::STATIC_JUMP));
1807 assert_eq!(bytecode.inst(jump.static_jump_target()).opcode, op::JUMPDEST);
1808 assert_ne!(jump.static_jump_target(), jump_inst + 1);
1809 assert!(!bytecode.has_dynamic_jumps);
1810 }
1811
1812 #[test]
1815 fn loop_stack_growth_clamping() {
1816 let bytecode = analyze_asm(
1822 "
1823 PUSH1 0x42 ; pc=0: initial value
1824 PUSH1 0x01 ; pc=2: another
1825 loop_header:
1826 JUMPDEST ; loop header (bb1)
1827 PUSH1 0x01 ; push each iteration
1828 PUSH %loop_header ; loop target
1829 JUMP ; back-edge
1830 ",
1831 );
1832 assert!(!bytecode.has_dynamic_jumps);
1835 }
1836
1837 #[test]
1840 fn single_inst_block_jump_target() {
1841 let bytecode = analyze_asm(
1842 "
1843 PUSH %target ; pc=0
1844 JUMP ; static jump
1845 INVALID
1846 INVALID
1847 target:
1848 JUMPDEST ; single-inst block
1849 STOP
1850 ",
1851 );
1852 assert!(!bytecode.has_dynamic_jumps);
1853 }
1854
1855 #[test]
1857 fn invalid_jump_target() {
1858 let bytecode = analyze_asm(
1861 "
1862 PUSH1 0xFF ; pc=0: push invalid target
1863 PUSH %func ; pc=2: push function entry
1864 JUMP ; pc=4: -> func
1865 ; Function: swap and jump to the caller-provided target.
1866 func:
1867 JUMPDEST
1868 JUMP ; jump to 0xFF (invalid)
1869 ",
1870 );
1871 let jump_inst = bytecode
1873 .iter_insts()
1874 .find(|(_, d)| d.is_jump() && d.flags.contains(InstFlags::INVALID_JUMP));
1875 assert!(jump_inst.is_some(), "expected an invalid jump");
1876 }
1877
1878 #[test]
1879 fn jumpi_with_zero_condition_ignores_unknown_target() {
1880 let bytecode = analyze_asm(
1881 "
1882 PUSH0
1883 CALLDATASIZE
1884 JUMPI
1885 STOP
1886 target:
1887 JUMPDEST
1888 STOP
1889 ",
1890 );
1891
1892 let (_, jump) = bytecode.iter_insts().find(|(_, data)| data.opcode == op::JUMPI).unwrap();
1893 assert!(jump.flags.contains(InstFlags::STATIC_JUMP));
1894 assert!(!jump.flags.contains(InstFlags::INVALID_JUMP));
1895 assert_eq!(jump.static_jump_target(), Inst::from_usize(3));
1896 assert!(!bytecode.has_dynamic_jumps);
1897 }
1898
1899 #[test]
1900 fn jumpi_with_zero_condition_ignores_valid_target() {
1901 let bytecode = analyze_asm(
1902 "
1903 PUSH0
1904 PUSH %target
1905 JUMPI
1906 STOP
1907 target:
1908 JUMPDEST
1909 STOP
1910 ",
1911 );
1912
1913 let (_, jump) = bytecode.iter_insts().find(|(_, data)| data.opcode == op::JUMPI).unwrap();
1914 assert!(jump.flags.contains(InstFlags::STATIC_JUMP));
1915 assert!(!jump.flags.contains(InstFlags::INVALID_JUMP));
1916 assert_eq!(jump.static_jump_target(), Inst::from_usize(3));
1917 assert!(!bytecode.has_dynamic_jumps);
1918 }
1919
1920 #[test]
1921 fn jumpi_with_zero_condition_cfg_only_falls_through() {
1922 let bytecode = analyze_asm(
1923 "
1924 PUSH0
1925 PUSH %target
1926 JUMPI
1927 PUSH1 0xAA
1928 STOP
1929 target:
1930 JUMPDEST
1931 STOP
1932 ",
1933 );
1934
1935 let (jump_inst, jump) =
1936 bytecode.iter_insts().find(|(_, data)| data.opcode == op::JUMPI).unwrap();
1937 let jump_block = bytecode.cfg.inst_to_block[jump_inst].unwrap();
1938 let fallthrough_block = bytecode.cfg.inst_to_block[jump_inst + 1].unwrap();
1939 let target_inst = bytecode
1940 .iter_all_insts()
1941 .rev()
1942 .find(|(_, data)| data.opcode == op::JUMPDEST)
1943 .unwrap()
1944 .0;
1945
1946 assert!(jump.has_const_jumpi_condition());
1947 assert_eq!(jump.static_jump_target(), jump_inst + 1);
1948 assert_eq!(bytecode.cfg.blocks[jump_block].succs.as_slice(), &[fallthrough_block]);
1949 assert!(
1950 bytecode.cfg.inst_to_block[target_inst].is_none(),
1951 "never-taken JUMPI target should be dead"
1952 );
1953 }
1954
1955 #[test]
1956 fn jumpi_with_zero_condition_from_predecessor_rewrites_local_static_cfg() {
1957 let bytecode = analyze_asm(
1958 "
1959 PUSH0
1960 PUSH %branch
1961 JUMP
1962 branch:
1963 JUMPDEST
1964 PUSH %target
1965 JUMPI
1966 PUSH1 0xAA
1967 STOP
1968 target:
1969 JUMPDEST
1970 STOP
1971 ",
1972 );
1973
1974 let (jump_inst, jump) =
1975 bytecode.iter_insts().find(|(_, data)| data.opcode == op::JUMPI).unwrap();
1976 let jump_block = bytecode.cfg.inst_to_block[jump_inst].unwrap();
1977 let fallthrough_block = bytecode.cfg.inst_to_block[jump_inst + 1].unwrap();
1978 let target_inst = bytecode
1979 .iter_all_insts()
1980 .rev()
1981 .find(|(_, data)| data.opcode == op::JUMPDEST)
1982 .unwrap()
1983 .0;
1984
1985 assert!(jump.has_const_jumpi_condition());
1986 assert_eq!(jump.static_jump_target(), jump_inst + 1);
1987 assert_eq!(bytecode.cfg.blocks[jump_block].succs.as_slice(), &[fallthrough_block]);
1988 assert!(
1989 bytecode.cfg.inst_to_block[target_inst].is_none(),
1990 "locally resolved but never-taken JUMPI target should be dead"
1991 );
1992 }
1993
1994 #[test]
1995 fn jumpi_with_true_condition_cfg_only_jumps_to_target() {
1996 let bytecode = analyze_asm(
1997 "
1998 PUSH1 0x01
1999 PUSH %target
2000 JUMPI
2001 PUSH1 0xAA
2002 STOP
2003 target:
2004 JUMPDEST
2005 STOP
2006 ",
2007 );
2008
2009 let (jump_inst, jump) =
2010 bytecode.iter_insts().find(|(_, data)| data.opcode == op::JUMPI).unwrap();
2011 let jump_block = bytecode.cfg.inst_to_block[jump_inst].unwrap();
2012 let target_block = bytecode.cfg.inst_to_block[jump.static_jump_target()].unwrap();
2013
2014 assert!(jump.has_const_jumpi_condition());
2015 assert_ne!(jump.static_jump_target(), jump_inst + 1);
2016 assert_eq!(bytecode.cfg.blocks[jump_block].succs.as_slice(), &[target_block]);
2017 assert!(
2018 bytecode.cfg.inst_to_block[jump_inst + 1].is_none(),
2019 "always-taken JUMPI fallthrough should be dead"
2020 );
2021 }
2022
2023 #[test]
2024 fn jumpi_with_true_condition_invalid_target_has_no_fallthrough() {
2025 let bytecode = analyze_asm(
2026 "
2027 PUSH1 0x01
2028 PUSH1 0xFF
2029 JUMPI
2030 STOP
2031 ",
2032 );
2033
2034 let (jump_inst, jump) =
2035 bytecode.iter_insts().find(|(_, data)| data.opcode == op::JUMPI).unwrap();
2036 let jump_block = bytecode.cfg.inst_to_block[jump_inst].unwrap();
2037
2038 assert!(jump.has_const_jumpi_condition());
2039 assert!(jump.flags.contains(InstFlags::STATIC_JUMP | InstFlags::INVALID_JUMP));
2040 assert!(bytecode.cfg.blocks[jump_block].succs.is_empty());
2041 assert!(
2042 bytecode.cfg.inst_to_block[jump_inst + 1].is_none(),
2043 "always-taken invalid JUMPI fallthrough should be dead"
2044 );
2045 }
2046
2047 #[test]
2048 fn jumpi_with_true_condition_keeps_unknown_target_dynamic() {
2049 let bytecode = analyze_asm(
2050 "
2051 PUSH1 0x01
2052 CALLDATASIZE
2053 JUMPI
2054 STOP
2055 target:
2056 JUMPDEST
2057 STOP
2058 ",
2059 );
2060
2061 let (_, jump) = bytecode.iter_insts().find(|(_, data)| data.opcode == op::JUMPI).unwrap();
2062 assert!(!jump.flags.contains(InstFlags::STATIC_JUMP));
2063 assert!(jump.has_const_jumpi_condition());
2064 assert!(!jump.can_fall_through());
2065 assert!(bytecode.has_dynamic_jumps);
2066 }
2067
2068 #[test]
2071 fn diamond_cfg_same_const() {
2072 let bytecode = analyze_asm(
2073 "
2074 PUSH1 0x42 ; push same const on both paths
2075 CALLDATASIZE ; condition
2076 PUSH %then_pc ; then target
2077 JUMPI ; branch
2078 ; Else: push same const.
2079 PUSH %merge
2080 JUMP ; -> merge
2081 ; Then:
2082 then_pc:
2083 JUMPDEST
2084 PUSH %merge
2085 JUMP ; -> merge
2086 ; Merge:
2087 merge:
2088 JUMPDEST
2089 PUSH0
2090 MSTORE ; MSTORE(0, 0x42)
2091 STOP
2092 ",
2093 );
2094 let mstore = bytecode.iter_insts().find(|(_, d)| d.opcode == op::MSTORE).unwrap().0;
2097 assert_eq!(bytecode.const_operand(mstore, 1), Some(U256::from(0x42)));
2098 }
2099
2100 #[test]
2103 fn diamond_cfg_different_const() {
2104 let bytecode = analyze_asm(
2105 "
2106 CALLDATASIZE ; condition
2107 PUSH %then_pc
2108 JUMPI ; branch
2109 ; Else: push 0xAA.
2110 PUSH1 0xAA
2111 PUSH %merge
2112 JUMP ; -> merge
2113 INVALID
2114 ; Then: push 0xBB.
2115 then_pc:
2116 JUMPDEST
2117 PUSH1 0xBB
2118 PUSH %merge
2119 JUMP ; -> merge
2120 ; Merge:
2121 merge:
2122 JUMPDEST
2123 PUSH0
2124 MSTORE ; MSTORE(0, ???)
2125 STOP
2126 ",
2127 );
2128 let mstore = bytecode.iter_insts().find(|(_, d)| d.opcode == op::MSTORE).unwrap().0;
2131 assert_eq!(bytecode.const_operand(mstore, 1), None);
2132 }
2133
2134 #[test]
2138 fn trampoline_private_return_unsound() {
2139 let bytecode = analyze_asm(
2140 "
2141 ; Entry: branch to opaque path or call site 1.
2142 PUSH0 ; pc=0
2143 CALLDATALOAD ; pc=1
2144 PUSH %opaque_path ; pc=2
2145 JUMPI ; pc=4
2146 ; Fallthrough: call site 1.
2147 PUSH %ret1 ; pc=5
2148 PUSH %fn_entry ; pc=7
2149 JUMP ; pc=9
2150 ret1:
2151 JUMPDEST ; pc=10
2152 POP ; pc=11
2153 ; Call site 2.
2154 PUSH %ret2 ; pc=12
2155 PUSH %fn_entry ; pc=14
2156 JUMP ; pc=16
2157 ret2:
2158 JUMPDEST ; pc=17
2159 POP ; pc=18
2160 STOP ; pc=19
2161 ; Opaque path (reachable from JUMPI).
2162 opaque_path:
2163 JUMPDEST ; pc=20
2164 PUSH0 ; pc=21
2165 MLOAD ; pc=22: any_ret (Top)
2166 PUSH %tramp ; pc=23
2167 JUMP ; pc=25
2168 ; Trampoline: bare JUMPDEST + JUMP (is_private_return = true).
2169 tramp:
2170 JUMPDEST ; pc=26
2171 JUMP ; pc=27: target = any_ret (Top)
2172 ; Internal function.
2173 fn_entry:
2174 JUMPDEST ; pc=28
2175 PUSH1 0x42 ; pc=29
2176 SWAP1 ; pc=31
2177 JUMP ; pc=32: return
2178 ",
2179 );
2180
2181 let return_jump = bytecode.iter_insts().find(|(i, d)| bytecode.pc(*i) == 32 && d.is_jump());
2184 let (_, rj) = return_jump.unwrap();
2185 assert!(
2186 !rj.flags.contains(InstFlags::STATIC_JUMP),
2187 "unsound: return jump should not be resolved when trampoline exists"
2188 );
2189 }
2190
2191 #[test]
2194 fn jumpi_return_unsound() {
2195 let bytecode = analyze_asm(
2196 "
2197 ; Call site 1.
2198 PUSH %ret1 ; pc=0
2199 PUSH %fn_entry ; pc=2
2200 JUMP ; pc=4
2201 ret1:
2202 JUMPDEST ; pc=5
2203 POP ; pc=6
2204 ; Call site 2.
2205 PUSH %ret2 ; pc=7
2206 PUSH %fn_entry ; pc=9
2207 JUMP ; pc=11
2208 ret2:
2209 JUMPDEST ; pc=12
2210 POP ; pc=13
2211 ; Opaque jump.
2212 PUSH0 ; pc=14
2213 MLOAD ; pc=15
2214 JUMP ; pc=16: Top
2215 INVALID ; pc=17
2216 INVALID ; pc=18
2217 INVALID ; pc=19
2218 ; Internal function using JUMPI to return.
2219 fn_entry:
2220 JUMPDEST ; pc=20
2221 PUSH1 0x42 ; pc=21: result
2222 SWAP1 ; pc=23: [result, ret_addr]
2223 PUSH1 0x01 ; pc=24: condition (always true)
2224 SWAP1 ; pc=26: [result, 1, ret_addr]
2225 JUMPI ; pc=27: conditional return (always taken)
2226 STOP ; pc=28: fallthrough (dead)
2227 ",
2228 );
2229
2230 let return_jumpi =
2233 bytecode.iter_insts().find(|(i, d)| bytecode.pc(*i) == 27 && d.is_jump());
2234 let (_, rj) = return_jumpi.unwrap();
2235 assert!(
2236 !rj.flags.contains(InstFlags::STATIC_JUMP),
2237 "unsound: JUMPI return should not be resolved"
2238 );
2239 }
2240
2241 #[test]
2244 fn suspect_jumpi_top_target_invalidates_const_condition() {
2245 let bytecode = analyze_asm(
2246 "
2247 ; Branch to an opaque caller or fall through to a known caller.
2248 PUSH0 ; pc=0
2249 CALLDATALOAD ; pc=1
2250 PUSH %opaque_caller ; pc=2
2251 JUMPI ; pc=4
2252 ; Known caller reaches fn_entry with condition=1 and a Top target.
2253 PUSH1 0x01 ; pc=5: condition
2254 PUSH0 ; pc=7
2255 MLOAD ; pc=8: target = Top
2256 PUSH %fn_entry ; pc=9
2257 JUMP ; pc=11
2258 ; Opaque caller could reach fn_entry with condition=0.
2259 opaque_caller:
2260 JUMPDEST ; pc=12
2261 PUSH0 ; pc=13: condition
2262 PUSH %taken ; pc=14: target
2263 PUSH0 ; pc=16
2264 MLOAD ; pc=17: jump destination = Top
2265 JUMP ; pc=18
2266 fn_entry:
2267 JUMPDEST ; pc=19
2268 JUMPI ; pc=20
2269 STOP ; pc=21
2270 taken:
2271 JUMPDEST ; pc=22
2272 STOP ; pc=23
2273 ",
2274 );
2275
2276 let (_, jumpi) = bytecode
2277 .iter_insts()
2278 .find(|(i, d)| bytecode.pc(*i) == 20 && d.opcode == op::JUMPI)
2279 .unwrap();
2280 assert!(
2281 !jumpi.has_const_jumpi_condition(),
2282 "suspect JUMPI should not keep a non-local const condition"
2283 );
2284 assert!(
2285 !jumpi.flags.contains(InstFlags::STATIC_JUMP),
2286 "suspect JUMPI target should remain dynamic"
2287 );
2288 }
2289
2290 #[test]
2292 fn suspect_jumpi_top_target_keeps_local_false_condition() {
2293 let bytecode = analyze_asm(
2294 "
2295 PUSH0
2296 CALLDATALOAD
2297 PUSH %opaque
2298 JUMPI
2299 PUSH %fn_entry
2300 JUMP
2301 opaque:
2302 JUMPDEST
2303 PUSH0
2304 MLOAD
2305 JUMP
2306 fn_entry:
2307 JUMPDEST
2308 PUSH0
2309 PUSH0
2310 MLOAD
2311 JUMPI
2312 STOP
2313 taken:
2314 JUMPDEST
2315 STOP
2316 ",
2317 );
2318
2319 let (jump_inst, jumpi) =
2320 bytecode.iter_insts().rev().find(|(_, data)| data.opcode == op::JUMPI).unwrap();
2321 assert!(jumpi.has_const_jumpi_condition());
2322 assert!(jumpi.flags.contains(InstFlags::STATIC_JUMP));
2323 assert_eq!(jumpi.static_jump_target(), jump_inst + 1);
2324 }
2325
2326 #[test]
2329 fn suspect_jumpi_top_target_keeps_local_true_condition() {
2330 let bytecode = analyze_asm(
2331 "
2332 PUSH0
2333 CALLDATALOAD
2334 PUSH %opaque
2335 JUMPI
2336 PUSH %fn_entry
2337 JUMP
2338 opaque:
2339 JUMPDEST
2340 PUSH0
2341 MLOAD
2342 JUMP
2343 fn_entry:
2344 JUMPDEST
2345 PUSH1 0x01
2346 PUSH0
2347 MLOAD
2348 JUMPI
2349 STOP
2350 taken:
2351 JUMPDEST
2352 STOP
2353 ",
2354 );
2355
2356 let (jump_inst, jumpi) =
2357 bytecode.iter_insts().rev().find(|(_, data)| data.opcode == op::JUMPI).unwrap();
2358 assert!(jumpi.has_const_jumpi_condition());
2359 assert!(!jumpi.flags.contains(InstFlags::STATIC_JUMP));
2360 assert!(!jumpi.can_fall_through());
2361 assert!(bytecode.has_dynamic_jumps);
2362 assert!(
2363 bytecode.cfg.inst_to_block[jump_inst + 1].is_none(),
2364 "always-taken dynamic JUMPI fallthrough should be dead"
2365 );
2366 }
2367
2368 #[test]
2372 fn opaque_return_addr_caller_unsound() {
2373 let bytecode = analyze_asm(
2374 "
2375 ; Entry: branch to opaque caller or fallthrough.
2376 PUSH0 ; pc=0
2377 CALLDATALOAD ; pc=1
2378 PUSH %opaque_caller ; pc=2
2379 JUMPI ; pc=4
2380 ; Fallthrough: call site 1.
2381 PUSH %ret1 ; pc=5
2382 PUSH %fn_entry ; pc=7
2383 JUMP ; pc=9
2384 ret1:
2385 JUMPDEST ; pc=10
2386 POP ; pc=11
2387 ; Call site 2.
2388 PUSH %ret2 ; pc=12
2389 PUSH %fn_entry ; pc=14
2390 JUMP ; pc=16
2391 ret2:
2392 JUMPDEST ; pc=17
2393 POP ; pc=18
2394 STOP ; pc=19
2395 ; Opaque third caller: loads return addr from memory (reachable via JUMPI).
2396 opaque_caller:
2397 JUMPDEST ; pc=20
2398 PUSH0 ; pc=21
2399 MLOAD ; pc=22: any_ret
2400 PUSH %fn_entry ; pc=23
2401 JUMP ; pc=25: -> fn_entry with arbitrary return addr
2402 ; Internal function.
2403 fn_entry:
2404 JUMPDEST ; pc=26
2405 PUSH1 0x42 ; pc=27
2406 SWAP1 ; pc=29
2407 JUMP ; pc=30: return
2408 ",
2409 );
2410
2411 let return_jump = bytecode.iter_insts().find(|(i, d)| bytecode.pc(*i) == 30 && d.is_jump());
2414 let (_, rj) = return_jump.unwrap();
2415 assert!(
2416 !rj.flags.contains(InstFlags::STATIC_JUMP),
2417 "unsound: return jump should not be resolved with opaque caller"
2418 );
2419 }
2420
2421 #[test]
2423 fn const_snapshot_loop_counter_inline() {
2424 let bytecode = analyze_asm(
2425 "
2426 PUSH1 0x00 ; inst 0: i = 0
2427 loop:
2428 JUMPDEST ; inst 1
2429 DUP1 ; inst 2
2430 PUSH1 0x0a ; inst 3
2431 LT ; inst 4
2432 ISZERO ; inst 5
2433 PUSH %exit ; inst 6
2434 JUMPI ; inst 7
2435 PUSH1 0x01 ; inst 8
2436 ADD ; inst 9: i++
2437 PUSH %loop ; inst 10
2438 JUMP ; inst 11
2439 exit:
2440 JUMPDEST ; inst 12
2441 POP ; inst 13
2442 STOP ; inst 14
2443 ",
2444 );
2445
2446 assert_eq!(bytecode.const_operand(Inst::from_usize(2), 0), None);
2448 assert_eq!(bytecode.const_operand(Inst::from_usize(9), 0), Some(U256::from(1)));
2450 assert_eq!(bytecode.const_operand(Inst::from_usize(9), 1), None);
2451 }
2452
2453 #[test]
2455 fn const_snapshot_loop_counter_subroutine() {
2456 let bytecode = analyze_asm(
2457 "
2458 PUSH1 0x00
2459 loop:
2460 JUMPDEST ; inst 1
2461 DUP1 ; inst 2
2462 PUSH1 0x0a
2463 LT
2464 ISZERO
2465 PUSH %exit
2466 JUMPI
2467 ; call add_fn(counter, 1) -> counter'
2468 PUSH1 0x01
2469 DUP2
2470 PUSH %ret
2471 SWAP2
2472 SWAP1
2473 PUSH %add_fn
2474 JUMP
2475 ret:
2476 JUMPDEST
2477 SWAP1
2478 POP
2479 PUSH %loop
2480 JUMP
2481 exit:
2482 JUMPDEST
2483 POP
2484 STOP
2485 add_fn:
2486 JUMPDEST
2487 PUSH1 0x00
2488 DUP3
2489 DUP3
2490 ADD
2491 SWAP1
2492 POP
2493 DUP1
2494 DUP3
2495 GT
2496 ISZERO
2497 PUSH %add_ok
2498 JUMPI
2499 INVALID
2500 add_ok:
2501 JUMPDEST
2502 SWAP3
2503 SWAP2
2504 POP
2505 POP
2506 JUMP
2507 ",
2508 );
2509
2510 assert_eq!(bytecode.const_operand(Inst::from_usize(2), 0), None);
2511 }
2512
2513 #[test]
2515 fn const_snapshot_loop_counter_multi_caller() {
2516 let bytecode = analyze_asm(
2517 "
2518 ; Caller 1 (outside loop) to make add_fn multi-target.
2519 PUSH1 0x02
2520 PUSH1 0x03
2521 PUSH %ret_outer
2522 SWAP2
2523 SWAP1
2524 PUSH %add_fn
2525 JUMP
2526 ret_outer:
2527 JUMPDEST ; inst 7
2528 POP
2529 PUSH1 0x00 ; i = 0
2530 loop:
2531 JUMPDEST ; inst 10
2532 DUP1 ; inst 11
2533 PUSH1 0x0a
2534 LT
2535 ISZERO
2536 PUSH %exit
2537 JUMPI
2538 ; Caller 2 (inside loop).
2539 PUSH1 0x01
2540 DUP2
2541 PUSH %ret_loop
2542 SWAP2
2543 SWAP1
2544 PUSH %add_fn
2545 JUMP
2546 ret_loop:
2547 JUMPDEST
2548 SWAP1
2549 POP
2550 PUSH %loop
2551 JUMP
2552 exit:
2553 JUMPDEST
2554 POP
2555 STOP
2556 add_fn:
2557 JUMPDEST
2558 PUSH1 0x00
2559 DUP3
2560 DUP3
2561 ADD
2562 SWAP1
2563 POP
2564 DUP1
2565 DUP3
2566 GT
2567 ISZERO
2568 PUSH %add_ok
2569 JUMPI
2570 INVALID
2571 add_ok:
2572 JUMPDEST
2573 SWAP3
2574 SWAP2
2575 POP
2576 POP
2577 JUMP
2578 ",
2579 );
2580
2581 assert_eq!(bytecode.const_operand(Inst::from_usize(11), 0), None);
2583 }
2584
2585 #[test]
2587 fn const_snapshot_loop_exp() {
2588 let bytecode = analyze_hex(
2590 "608060405234801561001057600080fd5b506004361061004c5760003560e01c806363ad973214610051578063a34f51e414610081578063dfaf4a87146100b1578063f078245b146100e1575b600080fd5b61006b60048036038101906100669190610275565b610111565b60405161007891906102d7565b60405180910390f35b61009b60048036038101906100969190610275565b610199565b6040516100a891906102d7565b60405180910390f35b6100cb60048036038101906100c69190610275565b6101cb565b6040516100d891906102d7565b60405180910390f35b6100fb60048036038101906100f69190610275565b610208565b60405161010891906102d7565b60405180910390f35b60008083905060005b838110156101865785820a915085820a915085820a915085820a915085820a915085820a915085820a915085820a915085820a915085820a915085820a915085820a915085820a915085820a915085820a915085820a915060108161017f9190610321565b905061011a565b5080600081905550809150509392505050565b6000805b828110156101b9576001816101b29190610321565b905061019d565b508260008190555082905093925050505650565b60008083905060005b838110156101f55785820a91506001816101ee9190610321565b90506101d4565b5080600081905550809150509392505050565b6000805b82811015610228576010816102219190610321565b905061020c565b50826000819055508290509392505050565b600080fd5b6000819050919050565b6102528161023f565b811461025d57600080fd5b50565b60008135905061026f81610249565b92915050565b60008060006060848603121561028e5761028d61023a565b5b600061029c86828701610260565b93505060206102ad86828701610260565b92505060406102be86828701610260565b9150509250925092565b6102d18161023f565b82525050565b60006020820190506102ec60008301846102c8565b92915050565b7f4e487b71000000000000000000000000000000000000000000000000000000006000526011600452602460006000fd5b600061032c8261023f565b91506103378361023f565b9250828201905080821115610340f5761034e6102f2565b5b9291505056fea264697066735822122000b253a2b246ddfeb0f09ddf5ec7dc70e2235369b8a2f3f9b55e32a5ca01e04ff64736f6c63430008180033",
2591 );
2592
2593 let lt_inst = bytecode
2595 .insts
2596 .iter_enumerated()
2597 .find(|(i, inst)| inst.opcode == op::LT && bytecode.pc(*i) == 416)
2598 .map(|(i, _)| i)
2599 .expect("LT at pc=416 not found");
2600 assert_eq!(bytecode.const_operand(lt_inst, 0), None);
2601 }
2602
2603 #[test]
2610 fn suspect_block_preserves_local_const_operand() {
2611 let bytecode = analyze_asm(
2612 "
2613 PUSH1 0xAA ; inst 0: inherited into target
2614 CALLDATASIZE ; inst 1: unknown condition
2615 PUSH %target ; inst 2
2616 JUMPI ; inst 3: target reachable, fallthrough reachable
2617
2618 PUSH0 ; inst 4
2619 CALLDATALOAD ; inst 5: Top
2620 JUMP ; inst 6: unresolved -> triggers suspect invalidation
2621
2622 target:
2623 JUMPDEST ; inst 7: suspect seed
2624 PUSH0 ; inst 8: block-local constant
2625 MSTORE ; inst 9: offset=0 (local), value=inherited 0xAA
2626 STOP ; inst 10
2627 ",
2628 );
2629 assert!(bytecode.has_dynamic_jumps);
2630 assert_eq!(bytecode.const_output(Inst::from_usize(8)), Some(U256::ZERO));
2632 assert_eq!(bytecode.const_operand(Inst::from_usize(9), 0), Some(U256::ZERO));
2633 assert_eq!(bytecode.const_operand(Inst::from_usize(9), 1), None);
2635 }
2636
2637 #[test]
2640 fn suspect_block_preserves_local_const_output() {
2641 let bytecode = analyze_asm(
2642 "
2643 PUSH1 0xAA ; inst 0: inherited into target
2644 CALLDATASIZE ; inst 1
2645 PUSH %target ; inst 2
2646 JUMPI ; inst 3
2647
2648 PUSH0 ; inst 4
2649 CALLDATALOAD ; inst 5: Top
2650 JUMP ; inst 6: unresolvable
2651
2652 target:
2653 JUMPDEST ; inst 7: suspect seed
2654 PUSH1 0x01 ; inst 8: block-local const output
2655 ADD ; inst 9: 0xAA + 0x01 in fixpoint, Top + 0x01 after restore
2656 STOP ; inst 10
2657 ",
2658 );
2659 assert!(bytecode.has_dynamic_jumps);
2660 assert_eq!(bytecode.const_output(Inst::from_usize(8)), Some(U256::from(0x01)));
2662 assert_eq!(bytecode.const_operand(Inst::from_usize(9), 0), Some(U256::from(0x01)));
2664 assert_eq!(bytecode.const_operand(Inst::from_usize(9), 1), None);
2665 assert_eq!(bytecode.const_output(Inst::from_usize(9)), None);
2666 }
2667
2668 #[test]
2672 fn suspect_jumpi_fallthrough_preserves_consts() {
2673 let bytecode = analyze_asm(
2674 "
2675 PUSH1 0xAA ; inst 0: inherited into target
2676 CALLDATASIZE ; inst 1: unknown condition
2677 PUSH %target ; inst 2
2678 JUMPI ; inst 3
2679
2680 PUSH0 ; inst 4
2681 CALLDATALOAD ; inst 5: Top
2682 JUMP ; inst 6: unresolved
2683
2684 target:
2685 JUMPDEST ; inst 7: suspect seed, entry stack [0xAA]
2686 PUSH1 0x01 ; inst 8: always take
2687 PUSH %after ; inst 9
2688 JUMPI ; inst 10: leaves [0xAA] for after
2689 STOP ; inst 11
2690
2691 after:
2692 JUMPDEST ; inst 12: suspect via forward propagation
2693 PUSH0 ; inst 13: block-local constant
2694 MSTORE ; inst 14: offset=0 (local), value=inherited 0xAA
2695 STOP ; inst 15
2696 ",
2697 );
2698 assert!(bytecode.has_dynamic_jumps);
2699 assert_eq!(bytecode.const_output(Inst::from_usize(13)), Some(U256::ZERO));
2701 assert_eq!(bytecode.const_operand(Inst::from_usize(14), 0), Some(U256::ZERO));
2702 assert_eq!(bytecode.const_operand(Inst::from_usize(14), 1), None);
2704 }
2705
2706 #[test]
2712 fn nonsuspect_fallthrough_keeps_fixpoint_snapshots() {
2713 let bytecode = analyze_asm(
2714 "
2715 PUSH1 0x69 ; inst 0: cross-block value
2716 CALLDATASIZE ; inst 1: unknown condition
2717 PUSH %target ; inst 2
2718 JUMPI ; inst 3
2719 ; Non-suspect fallthrough block (no JUMPDEST).
2720 PUSH1 0x0A ; inst 4
2721 PUSH1 0x0B ; inst 5
2722 ADD ; inst 6: 0x0A + 0x0B = 0x15, block-local
2723 PUSH0 ; inst 7
2724 MSTORE ; inst 8
2725 MLOAD ; inst 9: cross-block via single predecessor
2726 PUSH1 0x01 ; inst 10
2727 MSTORE ; inst 11
2728 STOP ; inst 12
2729 ; Opaque dynamic jump — triggers suspect invalidation.
2730 JUMPDEST ; inst 13
2731 PUSH0 ; inst 14
2732 CALLDATALOAD ; inst 15: Top
2733 JUMP ; inst 16: unresolvable
2734 target:
2735 JUMPDEST ; inst 17: suspect seed
2736 PUSH1 0x42 ; inst 18
2737 STOP ; inst 19
2738 ",
2739 );
2740 assert!(bytecode.has_dynamic_jumps);
2741 assert_eq!(bytecode.const_operand(Inst::from_usize(6), 0), Some(U256::from(0x0B)));
2744 assert_eq!(bytecode.const_operand(Inst::from_usize(6), 1), Some(U256::from(0x0A)));
2745 assert_eq!(bytecode.const_output(Inst::from_usize(6)), Some(U256::from(0x15)));
2746 assert_eq!(bytecode.const_operand(Inst::from_usize(8), 0), Some(U256::from(0)));
2748 assert_eq!(bytecode.const_operand(Inst::from_usize(8), 1), Some(U256::from(0x15)));
2749 assert_eq!(bytecode.const_operand(Inst::from_usize(9), 0), Some(U256::from(0x69)));
2751 assert_eq!(bytecode.const_output(Inst::from_usize(18)), Some(U256::from(0x42)));
2753 }
2754
2755 #[test]
2760 fn suspect_jumpi_fallthrough_mixed_operands() {
2761 let bytecode = analyze_asm(
2762 "
2763 ; Jump into the suspect JUMPDEST block with a value on the stack.
2764 PUSH1 0xAA ; inst 0: will be on stack at entry to target
2765 PUSH %target ; inst 1
2766 JUMP ; inst 2: static jump
2767 ; Opaque dynamic jump — triggers suspect invalidation.
2768 JUMPDEST ; inst 3
2769 PUSH0 ; inst 4
2770 CALLDATALOAD ; inst 5: Top
2771 JUMP ; inst 6: unresolvable
2772 target:
2773 ; Suspect JUMPDEST block with JUMPI.
2774 ; Entry stack: [0xAA] (from pred, but unknown to block-local pass).
2775 JUMPDEST ; inst 7: suspect seed
2776 PUSH1 0x01 ; inst 8: condition
2777 PUSH %after ; inst 9
2778 JUMPI ; inst 10: pops condition+target, [0xAA] remains
2779 STOP ; inst 11
2780 after:
2781 ; Fallthrough/target of JUMPI — suspect via propagation.
2782 ; Entry stack: [0xAA] (inherited, but Top in block-local pass).
2783 JUMPDEST ; inst 12
2784 PUSH1 0xBB ; inst 13: block-local
2785 ADD ; inst 14: 0xAA + 0xBB — depth 1 is incoming
2786 STOP ; inst 15
2787 ",
2788 );
2789 assert!(bytecode.has_dynamic_jumps);
2790 assert_eq!(bytecode.const_operand(Inst::from_usize(14), 0), Some(U256::from(0xBB)));
2793 assert_eq!(bytecode.const_operand(Inst::from_usize(14), 1), None);
2794 assert_eq!(bytecode.const_output(Inst::from_usize(14)), None);
2795 }
2796
2797 #[test]
2799 fn empty_bytecode() {
2800 let bytecode = analyze_asm("STOP");
2801 assert!(!bytecode.has_dynamic_jumps);
2802 }
2803
2804 #[test]
2806 fn dead_code_dynamic_jump() {
2807 let bytecode = analyze_asm(
2808 "
2809 STOP
2810 PUSH0
2811 CALLDATALOAD
2812 JUMP
2813 ",
2814 );
2815 assert!(!bytecode.has_dynamic_jumps);
2816 }
2817
2818 #[test]
2822 fn non_converged_multi_jump_stays_dynamic() {
2823 let k = 15;
2829 let b = 31;
2830 let mut lines = Vec::new();
2831 lines.push("PUSH %call0".to_string());
2832 lines.push("JUMP".to_string());
2833 for i in 0..k {
2834 lines.push(format!("call{i}:"));
2835 lines.push("JUMPDEST".to_string());
2836 lines.push(format!("PUSH %ret{i}"));
2837 lines.push("PUSH %relay0".to_string());
2838 lines.push("JUMP".to_string());
2839
2840 lines.push(format!("ret{i}:"));
2841 lines.push("JUMPDEST".to_string());
2842 lines.push("POP".to_string());
2843 if i + 1 < k {
2844 lines.push(format!("PUSH %call{}", i + 1));
2845 lines.push("JUMP".to_string());
2846 } else {
2847 lines.push("STOP".to_string());
2848 }
2849 }
2850 for i in 0..b {
2851 lines.push(format!("relay{i}:"));
2852 lines.push("JUMPDEST".to_string());
2853 if i + 1 < b {
2854 lines.push(format!("PUSH %relay{}", i + 1));
2855 } else {
2856 lines.push("PUSH %fn_entry".to_string());
2857 }
2858 lines.push("JUMP".to_string());
2859 }
2860 lines.push("fn_entry:".to_string());
2861 lines.push("JUMPDEST".to_string());
2862 lines.push("PUSH1 0x42".to_string());
2863 lines.push("SWAP1".to_string());
2864 lines.push("JUMP".to_string());
2865
2866 let bytecode = analyze_asm(&lines.join("\n"));
2867
2868 let return_jump = bytecode
2872 .iter_insts()
2873 .find(|(_, d)| d.is_jump() && d.flags.contains(InstFlags::MULTI_JUMP));
2874 assert!(return_jump.is_none(), "non-converged fixpoint must not commit partial MULTI_JUMP");
2875 assert!(
2877 bytecode.has_dynamic_jumps,
2878 "return jump should remain dynamic when fixpoint doesn't converge"
2879 );
2880 }
2881}