From 6e2c8dda642af3e3ae1b0f6e875f1d19489dd229 Mon Sep 17 00:00:00 2001 From: Francisco Giordano Date: Wed, 10 Dec 2025 23:41:19 -0300 Subject: [PATCH 1/6] feat: Implement EIP-8024 for Amsterdam --- crates/bytecode/src/opcode.rs | 14 +- crates/interpreter/src/instruction_result.rs | 6 + crates/interpreter/src/instructions.rs | 4 + crates/interpreter/src/instructions/stack.rs | 171 ++++++++++++++++++- crates/interpreter/src/interpreter_types.rs | 1 + 5 files changed, 190 insertions(+), 6 deletions(-) diff --git a/crates/bytecode/src/opcode.rs b/crates/bytecode/src/opcode.rs index 4f1d4247f7..cc1fb61f98 100644 --- a/crates/bytecode/src/opcode.rs +++ b/crates/bytecode/src/opcode.rs @@ -623,9 +623,9 @@ opcodes! { // 0xE3 // 0xE4 // 0xE5 - // 0xE6 - // 0xE7 - // 0xE8 + 0xE6 => DUPN => stack_io(0, 1), immediate_size(1); + 0xE7 => SWAPN => stack_io(0, 0), immediate_size(1); + 0xE8 => EXCHANGE => stack_io(0, 0), immediate_size(1); // 0xE9 // 0xEA // 0xEB @@ -668,11 +668,15 @@ mod tests { #[test] fn test_immediate_size() { let mut expected = [0u8; 256]; - // PUSH opcodes + for push in PUSH1..=PUSH32 { expected[push as usize] = push - PUSH1 + 1; } + for stack_op in [DUPN, SWAPN, EXCHANGE] { + expected[stack_op as usize] = 1; + } + for (i, opcode) in OPCODE_INFO.iter().enumerate() { if let Some(opcode) = opcode { assert_eq!( @@ -718,7 +722,7 @@ mod tests { for _ in OPCODE_INFO.into_iter().flatten() { opcode_num += 1; } - assert_eq!(opcode_num, 150); + assert_eq!(opcode_num, 153); } #[test] diff --git a/crates/interpreter/src/instruction_result.rs b/crates/interpreter/src/instruction_result.rs index 74b295e64d..b7cdedaba5 100644 --- a/crates/interpreter/src/instruction_result.rs +++ b/crates/interpreter/src/instruction_result.rs @@ -81,6 +81,8 @@ pub enum InstructionResult { CreateInitCodeSizeLimit, /// Fatal external error. Returned by database. FatalExternalError, + /// Invalid encoding of an instruction's immediate operand. + InvalidImmediateEncoding, } impl From for InstructionResult { @@ -190,6 +192,7 @@ macro_rules! return_error { | $crate::InstructionResult::CreateContractStartingWithEF | $crate::InstructionResult::CreateInitCodeSizeLimit | $crate::InstructionResult::FatalExternalError + | $crate::InstructionResult::InvalidImmediateEncoding }; } @@ -348,6 +351,9 @@ impl> From for SuccessOrHalt { Self::Internal(InternalResult::InvalidExtDelegateCallTarget) } + InstructionResult::InvalidImmediateEncoding => { + Self::Halt(HaltReason::OpcodeNotFound.into()) + } } } } diff --git a/crates/interpreter/src/instructions.rs b/crates/interpreter/src/instructions.rs index cae338c29f..ebde810b5b 100644 --- a/crates/interpreter/src/instructions.rs +++ b/crates/interpreter/src/instructions.rs @@ -278,6 +278,10 @@ const fn instruction_table_impl() -> [Instructi table[SWAP15 as usize] = Instruction::new(stack::swap::<15, _, _>, 3); table[SWAP16 as usize] = Instruction::new(stack::swap::<16, _, _>, 3); + table[DUPN as usize] = Instruction::new(stack::dupn, 3); + table[SWAPN as usize] = Instruction::new(stack::swapn, 3); + table[EXCHANGE as usize] = Instruction::new(stack::exchange, 3); + table[LOG0 as usize] = Instruction::new(host::log::<0, _>, gas::LOG); table[LOG1 as usize] = Instruction::new(host::log::<1, _>, gas::LOG); table[LOG2 as usize] = Instruction::new(host::log::<2, _>, gas::LOG); diff --git a/crates/interpreter/src/instructions/stack.rs b/crates/interpreter/src/instructions/stack.rs index f2fb7f4cba..e7717594cb 100644 --- a/crates/interpreter/src/instructions/stack.rs +++ b/crates/interpreter/src/instructions/stack.rs @@ -34,7 +34,6 @@ pub fn push( return; } - // Can ignore return. as relative N jump is safe operation context.interpreter.bytecode.relative_jump(N as isize); } @@ -60,3 +59,173 @@ pub fn swap( context.interpreter.halt(InstructionResult::StackOverflow); } } + +/// Implements the DUPN instruction. +/// +/// Duplicates the Nth stack item to the top of the stack, with N given by an immediate. +pub fn dupn( + context: InstructionContext<'_, H, WIRE>, +) { + check!(context.interpreter, AMSTERDAM); + let x: usize = context.interpreter.bytecode.read_u8().into(); + if let Some(n) = decode_single(x) { + if !context.interpreter.stack.dup(n) { + context.interpreter.halt(InstructionResult::StackOverflow); + } + context.interpreter.bytecode.relative_jump(1); + } else { + context.interpreter.halt(InstructionResult::InvalidImmediateEncoding); + } +} + +/// Implements the SWAPN instruction. +/// +/// Swaps the top stack item with the N+1th stack item, with N given by an immediate. +pub fn swapn( + context: InstructionContext<'_, H, WIRE>, +) { + check!(context.interpreter, AMSTERDAM); + let x: usize = context.interpreter.bytecode.read_u8().into(); + if let Some(n) = decode_single(x) { + if !context.interpreter.stack.exchange(0, n) { + context.interpreter.halt(InstructionResult::StackOverflow); + } + context.interpreter.bytecode.relative_jump(1); + } else { + context.interpreter.halt(InstructionResult::InvalidImmediateEncoding); + } +} + +/// Implements the EXCHANGE instruction. +/// +/// Swaps the N+1th stack item with the M+1th stack item, with N, M given by an immediate. +pub fn exchange( + context: InstructionContext<'_, H, WIRE>, +) { + check!(context.interpreter, AMSTERDAM); + let x: usize = context.interpreter.bytecode.read_u8().into(); + if let Some((n, m)) = decode_pair(x) { + if !context.interpreter.stack.exchange(n, m - n) { + context.interpreter.halt(InstructionResult::StackOverflow); + } + context.interpreter.bytecode.relative_jump(1); + } else { + context.interpreter.halt(InstructionResult::InvalidImmediateEncoding); + } +} + +fn decode_single(x: usize) -> Option { + if x <= 90 { + Some(x + 17) + } else if x >= 128 { + Some(x - 20) + } else { + None + } +} + +fn decode_pair(x: usize) -> Option<(usize, usize)> { + let k = if x <= 79 { + x + } else if x >= 128 { + x - 48 + } else { + return None; + }; + let q = k / 16; + let r = k % 16; + if q < r { + Some((q + 1, r + 1)) + } else { + Some((r + 1, 29 - q)) + } +} + +#[cfg(test)] +mod tests { + use crate::{ + gas::params::GasParams, host::DummyHost, instructions::instruction_table, interpreter::{EthInterpreter, ExtBytecode, InputsImpl, SharedMemory}, interpreter_types::LoopControl, Interpreter + }; + use bytecode::Bytecode; + use primitives::{hardfork::SpecId, Bytes, U256}; + + fn run_bytecode(code: &[u8]) -> Interpreter { + let bytecode = Bytecode::new_raw(Bytes::copy_from_slice(code)); + let mut interpreter = Interpreter::::new( + SharedMemory::new(), + ExtBytecode::new(bytecode), + InputsImpl::default(), + false, + SpecId::OSAKA, + u64::MAX, + GasParams::default(), + ); + let table = instruction_table::(); + let mut host = DummyHost; + interpreter.run_plain(&table, &mut host); + interpreter + } + + #[test] + fn test_dupn() { + let interpreter = run_bytecode(&[ + 0x60, 0x01, 0x60, 0x00, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, + 0x80, 0x80, 0x80, 0x80, 0x80, 0xe6, 0x00, + ]); + assert_eq!(interpreter.stack.len(), 18); + assert_eq!(interpreter.stack.data()[17], U256::from(1)); + assert_eq!(interpreter.stack.data()[0], U256::from(1)); + for i in 1..17 { + assert_eq!(interpreter.stack.data()[i], U256::ZERO); + } + } + + #[test] + fn test_swapn() { + let interpreter = run_bytecode(&[ + 0x60, 0x01, 0x60, 0x00, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, + 0x80, 0x80, 0x80, 0x80, 0x80, 0x60, 0x02, 0xe7, 0x00, + ]); + assert_eq!(interpreter.stack.len(), 18); + assert_eq!(interpreter.stack.data()[17], U256::from(1)); + assert_eq!(interpreter.stack.data()[0], U256::from(2)); + for i in 1..17 { + assert_eq!(interpreter.stack.data()[i], U256::ZERO); + } + } + + #[test] + fn test_exchange() { + let interpreter = run_bytecode(&[0x60, 0x00, 0x60, 0x01, 0x60, 0x02, 0xe8, 0x01]); + assert_eq!(interpreter.stack.len(), 3); + assert_eq!(interpreter.stack.data()[2], U256::from(2)); + assert_eq!(interpreter.stack.data()[1], U256::from(0)); + assert_eq!(interpreter.stack.data()[0], U256::from(1)); + } + + #[test] + fn test_swapn_invalid_immediate() { + let mut interpreter = run_bytecode(&[0xe7, 0x5b]); + assert!(interpreter + .bytecode + .instruction_result() + .is_none()); + } + + #[test] + fn test_jump_over_invalid_dupn() { + let interpreter = run_bytecode(&[0x60, 0x04, 0x56, 0xe6, 0x5b]); + assert!(interpreter + .bytecode + .is_not_end()); + } + + #[test] + fn test_exchange_with_iszero() { + let interpreter = run_bytecode(&[0x60, 0x00, 0x60, 0x00, 0x60, 0x00, 0xe8, 0x01, 0x15]); + assert_eq!(interpreter.stack.len(), 3); + assert_eq!(interpreter.stack.data()[2], U256::from(1)); + assert_eq!(interpreter.stack.data()[1], U256::ZERO); + assert_eq!(interpreter.stack.data()[0], U256::ZERO); + } +} diff --git a/crates/interpreter/src/interpreter_types.rs b/crates/interpreter/src/interpreter_types.rs index 0195ee93fa..60f2631564 100644 --- a/crates/interpreter/src/interpreter_types.rs +++ b/crates/interpreter/src/interpreter_types.rs @@ -227,6 +227,7 @@ pub trait StackTr { /// Exchanges two values on the stack. /// /// Indexes are based from the top of the stack. + /// `n` is the first index, and the second index is calculated as `n + m`. /// /// Returns `true` if swap was successful, `false` if stack underflow. #[must_use] From 2a3e5515767376ecd01df95eb8f32b98137da117 Mon Sep 17 00:00:00 2001 From: Francisco Giordano Date: Thu, 11 Dec 2025 10:03:47 -0300 Subject: [PATCH 2/6] fmt --- crates/interpreter/src/instructions/stack.rs | 40 ++++++++++---------- 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/crates/interpreter/src/instructions/stack.rs b/crates/interpreter/src/instructions/stack.rs index e7717594cb..4673d336fa 100644 --- a/crates/interpreter/src/instructions/stack.rs +++ b/crates/interpreter/src/instructions/stack.rs @@ -63,9 +63,7 @@ pub fn swap( /// Implements the DUPN instruction. /// /// Duplicates the Nth stack item to the top of the stack, with N given by an immediate. -pub fn dupn( - context: InstructionContext<'_, H, WIRE>, -) { +pub fn dupn(context: InstructionContext<'_, H, WIRE>) { check!(context.interpreter, AMSTERDAM); let x: usize = context.interpreter.bytecode.read_u8().into(); if let Some(n) = decode_single(x) { @@ -74,16 +72,16 @@ pub fn dupn( } context.interpreter.bytecode.relative_jump(1); } else { - context.interpreter.halt(InstructionResult::InvalidImmediateEncoding); + context + .interpreter + .halt(InstructionResult::InvalidImmediateEncoding); } } /// Implements the SWAPN instruction. /// /// Swaps the top stack item with the N+1th stack item, with N given by an immediate. -pub fn swapn( - context: InstructionContext<'_, H, WIRE>, -) { +pub fn swapn(context: InstructionContext<'_, H, WIRE>) { check!(context.interpreter, AMSTERDAM); let x: usize = context.interpreter.bytecode.read_u8().into(); if let Some(n) = decode_single(x) { @@ -92,16 +90,16 @@ pub fn swapn( } context.interpreter.bytecode.relative_jump(1); } else { - context.interpreter.halt(InstructionResult::InvalidImmediateEncoding); + context + .interpreter + .halt(InstructionResult::InvalidImmediateEncoding); } } /// Implements the EXCHANGE instruction. /// /// Swaps the N+1th stack item with the M+1th stack item, with N, M given by an immediate. -pub fn exchange( - context: InstructionContext<'_, H, WIRE>, -) { +pub fn exchange(context: InstructionContext<'_, H, WIRE>) { check!(context.interpreter, AMSTERDAM); let x: usize = context.interpreter.bytecode.read_u8().into(); if let Some((n, m)) = decode_pair(x) { @@ -110,7 +108,9 @@ pub fn exchange( } context.interpreter.bytecode.relative_jump(1); } else { - context.interpreter.halt(InstructionResult::InvalidImmediateEncoding); + context + .interpreter + .halt(InstructionResult::InvalidImmediateEncoding); } } @@ -144,7 +144,12 @@ fn decode_pair(x: usize) -> Option<(usize, usize)> { #[cfg(test)] mod tests { use crate::{ - gas::params::GasParams, host::DummyHost, instructions::instruction_table, interpreter::{EthInterpreter, ExtBytecode, InputsImpl, SharedMemory}, interpreter_types::LoopControl, Interpreter + gas::params::GasParams, + host::DummyHost, + instructions::instruction_table, + interpreter::{EthInterpreter, ExtBytecode, InputsImpl, SharedMemory}, + interpreter_types::LoopControl, + Interpreter, }; use bytecode::Bytecode; use primitives::{hardfork::SpecId, Bytes, U256}; @@ -206,18 +211,13 @@ mod tests { #[test] fn test_swapn_invalid_immediate() { let mut interpreter = run_bytecode(&[0xe7, 0x5b]); - assert!(interpreter - .bytecode - .instruction_result() - .is_none()); + assert!(interpreter.bytecode.instruction_result().is_none()); } #[test] fn test_jump_over_invalid_dupn() { let interpreter = run_bytecode(&[0x60, 0x04, 0x56, 0xe6, 0x5b]); - assert!(interpreter - .bytecode - .is_not_end()); + assert!(interpreter.bytecode.is_not_end()); } #[test] From ceaa4be951ff4eec443d27b7f20b91af68bfeb51 Mon Sep 17 00:00:00 2001 From: Francisco Giordano Date: Thu, 11 Dec 2025 10:06:45 -0300 Subject: [PATCH 3/6] fix specid --- crates/interpreter/src/instructions/stack.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/interpreter/src/instructions/stack.rs b/crates/interpreter/src/instructions/stack.rs index 4673d336fa..3fb42dff9d 100644 --- a/crates/interpreter/src/instructions/stack.rs +++ b/crates/interpreter/src/instructions/stack.rs @@ -161,7 +161,7 @@ mod tests { ExtBytecode::new(bytecode), InputsImpl::default(), false, - SpecId::OSAKA, + SpecId::AMSTERDAM, u64::MAX, GasParams::default(), ); From c1f0b99711a478676304972c85d98c21e5aaef9b Mon Sep 17 00:00:00 2001 From: Francisco Giordano Date: Thu, 11 Dec 2025 10:10:38 -0300 Subject: [PATCH 4/6] use constants in tests --- crates/interpreter/src/instructions/stack.rs | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/crates/interpreter/src/instructions/stack.rs b/crates/interpreter/src/instructions/stack.rs index 3fb42dff9d..54f9fbf7c3 100644 --- a/crates/interpreter/src/instructions/stack.rs +++ b/crates/interpreter/src/instructions/stack.rs @@ -151,6 +151,7 @@ mod tests { interpreter_types::LoopControl, Interpreter, }; + use bytecode::opcode::*; use bytecode::Bytecode; use primitives::{hardfork::SpecId, Bytes, U256}; @@ -174,8 +175,8 @@ mod tests { #[test] fn test_dupn() { let interpreter = run_bytecode(&[ - 0x60, 0x01, 0x60, 0x00, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, - 0x80, 0x80, 0x80, 0x80, 0x80, 0xe6, 0x00, + PUSH1, 0x01, PUSH1, 0x00, DUP1, DUP1, DUP1, DUP1, DUP1, DUP1, DUP1, DUP1, DUP1, DUP1, + DUP1, DUP1, DUP1, DUP1, DUP1, DUPN, 0x00, ]); assert_eq!(interpreter.stack.len(), 18); assert_eq!(interpreter.stack.data()[17], U256::from(1)); @@ -188,8 +189,8 @@ mod tests { #[test] fn test_swapn() { let interpreter = run_bytecode(&[ - 0x60, 0x01, 0x60, 0x00, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, - 0x80, 0x80, 0x80, 0x80, 0x80, 0x60, 0x02, 0xe7, 0x00, + PUSH1, 0x01, PUSH1, 0x00, DUP1, DUP1, DUP1, DUP1, DUP1, DUP1, DUP1, DUP1, DUP1, DUP1, + DUP1, DUP1, DUP1, DUP1, DUP1, PUSH1, 0x02, SWAPN, 0x00, ]); assert_eq!(interpreter.stack.len(), 18); assert_eq!(interpreter.stack.data()[17], U256::from(1)); @@ -201,7 +202,7 @@ mod tests { #[test] fn test_exchange() { - let interpreter = run_bytecode(&[0x60, 0x00, 0x60, 0x01, 0x60, 0x02, 0xe8, 0x01]); + let interpreter = run_bytecode(&[PUSH1, 0x00, PUSH1, 0x01, PUSH1, 0x02, EXCHANGE, 0x01]); assert_eq!(interpreter.stack.len(), 3); assert_eq!(interpreter.stack.data()[2], U256::from(2)); assert_eq!(interpreter.stack.data()[1], U256::from(0)); @@ -210,19 +211,21 @@ mod tests { #[test] fn test_swapn_invalid_immediate() { - let mut interpreter = run_bytecode(&[0xe7, 0x5b]); + let mut interpreter = run_bytecode(&[SWAPN, JUMPDEST]); assert!(interpreter.bytecode.instruction_result().is_none()); } #[test] fn test_jump_over_invalid_dupn() { - let interpreter = run_bytecode(&[0x60, 0x04, 0x56, 0xe6, 0x5b]); + let interpreter = run_bytecode(&[PUSH1, 0x04, JUMP, DUPN, JUMPDEST]); assert!(interpreter.bytecode.is_not_end()); } #[test] fn test_exchange_with_iszero() { - let interpreter = run_bytecode(&[0x60, 0x00, 0x60, 0x00, 0x60, 0x00, 0xe8, 0x01, 0x15]); + let interpreter = run_bytecode(&[ + PUSH1, 0x00, PUSH1, 0x00, PUSH1, 0x00, EXCHANGE, 0x01, ISZERO, + ]); assert_eq!(interpreter.stack.len(), 3); assert_eq!(interpreter.stack.data()[2], U256::from(1)); assert_eq!(interpreter.stack.data()[1], U256::ZERO); From 242c57e812b61dacb017f8b3390b445594facc8c Mon Sep 17 00:00:00 2001 From: Francisco Giordano Date: Thu, 11 Dec 2025 23:28:24 -0300 Subject: [PATCH 5/6] fix legacy_analysis --- crates/bytecode/src/legacy/analysis.rs | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/crates/bytecode/src/legacy/analysis.rs b/crates/bytecode/src/legacy/analysis.rs index 39e4a1e1d0..5b5f99e2c1 100644 --- a/crates/bytecode/src/legacy/analysis.rs +++ b/crates/bytecode/src/legacy/analysis.rs @@ -29,13 +29,17 @@ pub fn analyze_legacy(bytecode: Bytes) -> (JumpTable, Bytes) { iterator = unsafe { iterator.add(1) }; } else { let push_offset = opcode.wrapping_sub(opcode::PUSH1); - if push_offset < 32 { - // SAFETY: Iterator access range is checked in the while loop - iterator = unsafe { iterator.add(push_offset as usize + 2) }; - } else { - // SAFETY: Iterator access range is checked in the while loop - iterator = unsafe { iterator.add(1) }; - } + let dupn_offset = opcode.wrapping_sub(opcode::DUPN); + let skip = + if push_offset < 32 { + 2 + push_offset as usize + } else if dupn_offset < 3 { + 2 + } else { + 1 + }; + // SAFETY: Iterator access range is checked in the while loop + iterator = unsafe { iterator.add(skip) }; } } From 9892671053b0197c23a27a03c0a32ce11cab83e0 Mon Sep 17 00:00:00 2001 From: Francisco Giordano Date: Thu, 11 Dec 2025 23:29:21 -0300 Subject: [PATCH 6/6] fmt --- crates/bytecode/src/legacy/analysis.rs | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/crates/bytecode/src/legacy/analysis.rs b/crates/bytecode/src/legacy/analysis.rs index 5b5f99e2c1..e06ee173d3 100644 --- a/crates/bytecode/src/legacy/analysis.rs +++ b/crates/bytecode/src/legacy/analysis.rs @@ -30,14 +30,13 @@ pub fn analyze_legacy(bytecode: Bytes) -> (JumpTable, Bytes) { } else { let push_offset = opcode.wrapping_sub(opcode::PUSH1); let dupn_offset = opcode.wrapping_sub(opcode::DUPN); - let skip = - if push_offset < 32 { - 2 + push_offset as usize - } else if dupn_offset < 3 { - 2 - } else { - 1 - }; + let skip = if push_offset < 32 { + 2 + push_offset as usize + } else if dupn_offset < 3 { + 2 + } else { + 1 + }; // SAFETY: Iterator access range is checked in the while loop iterator = unsafe { iterator.add(skip) }; }