diff --git a/crates/vm/levm/src/opcode_handlers/frame_tx.rs b/crates/vm/levm/src/opcode_handlers/frame_tx.rs index 3bbd346a28c..bac69a4be98 100644 --- a/crates/vm/levm/src/opcode_handlers/frame_tx.rs +++ b/crates/vm/levm/src/opcode_handlers/frame_tx.rs @@ -457,10 +457,11 @@ impl OpcodeHandler for OpFrameParamHandler { /// SIGPARAM (0xB4) -- signature-scoped metadata and data copy (EIP-8141). /// Metadata (params 0x00-0x03): stack `[param, signatureIndex]` with /// `signatureIndex` on top; gas 2; returns one word (0x00 effective signer, -/// 0x01 scheme, 0x02 msg, 0x03 len(signature)). Copy (param 0x04): stack -/// `[memOffset, dataOffset, length, param, signatureIndex]` with `signatureIndex` -/// on top; CALLDATACOPY gas; copies an ARBITRARY signature's raw bytes into -/// memory (zero-filled past the end) and pushes nothing — any other scheme halts. +/// 0x01 scheme, 0x02 msg, 0x03 len(signature)). Copy (param 0x04): takes +/// `[signatureIndex, param, memOffset, dataOffset, length]` with `signatureIndex` +/// on top (popped first), matching `CALLDATACOPY`'s operand order; CALLDATACOPY +/// gas; copies an ARBITRARY signature's raw bytes into memory (zero-filled past +/// the end) and pushes nothing — any other scheme halts. pub struct OpSigParamHandler; impl OpcodeHandler for OpSigParamHandler { #[inline(always)] @@ -474,7 +475,7 @@ impl OpcodeHandler for OpSigParamHandler { // 0x04: copy the referenced ARBITRARY signature's raw bytes into memory, // CALLDATACOPY-style (see FRAMEDATACOPY). Pops three more operands. if param == 0x04 { - let [length, data_offset, mem_offset] = *vm.current_call_frame.stack.pop()?; + let [mem_offset, data_offset, length] = *vm.current_call_frame.stack.pop()?; let (length, mem_offset) = size_offset_to_usize(length, mem_offset)?; let data_offset_opt = u256_to_offset(data_offset); diff --git a/docs/eip-8141.md b/docs/eip-8141.md index 8a4e6c17d20..a67060988a4 100644 --- a/docs/eip-8141.md +++ b/docs/eip-8141.md @@ -316,12 +316,12 @@ signature-scoped metadata for `signatures[signatureIndex]`: | 0x02 | msg (0 when empty — the canonical sig-hash case — else the 32-byte digest) | | 0x03 | len(signature) | -**Copy (param 0x04)** — Stack input `[memOffset, dataOffset, length, param, -signatureIndex]` (signatureIndex on top); no stack output; gas cost matches -`CALLDATACOPY`. Copies `length` bytes of the referenced signature's raw -`signature`, starting at `dataOffset`, into memory at `memOffset`, zero-filling -past the end. Only valid for `ARBITRARY` signatures — any other scheme is an -exceptional halt. +**Copy (param 0x04)** — Stack input `[signatureIndex, param, memOffset, dataOffset, +length]`, listed top-first (signatureIndex on top, popped first), then the +`CALLDATACOPY` operand order; no stack output; gas cost matches `CALLDATACOPY`. +Copies `length` bytes of the referenced signature's raw `signature`, starting at +`dataOffset`, into memory at `memOffset`, zero-filling past the end. Only valid +for `ARBITRARY` signatures — any other scheme is an exceptional halt. Any other `param`, or an out-of-bounds `signatureIndex`, is an exceptional halt. diff --git a/test/tests/levm/eip8141_tests.rs b/test/tests/levm/eip8141_tests.rs index 0b9aa178167..d8908de2b7f 100644 --- a/test/tests/levm/eip8141_tests.rs +++ b/test/tests/levm/eip8141_tests.rs @@ -3287,26 +3287,26 @@ mod sigparam_execution_tests { ]) } - /// `SIGPARAM(0x04, index)` copying `length` bytes from `dataOffset` to memory - /// offset 0, then `SSTORE(0, MLOAD(0))`. The copy form takes - /// `[memOffset, dataOffset, length, param, signatureIndex]` with the index on + /// `SIGPARAM(0x04, index)` copying `length` bytes from `dataOffset` to + /// `memOffset`, then `SSTORE(0, MLOAD(memOffset))`. The copy form takes + /// `[signatureIndex, param, memOffset, dataOffset, length]` with the index on /// top, so the pushes run in reverse. - fn sigparam_copy_code(index: u8, length: u8, data_offset: u8) -> Bytes { + fn sigparam_copy_code(index: u8, length: u8, data_offset: u8, mem_offset: u8) -> Bytes { Bytes::from(vec![ 0x60, - 0x00, // PUSH1 memOffset + length, // PUSH1 length 0x60, data_offset, // PUSH1 dataOffset 0x60, - length, // PUSH1 length + mem_offset, // PUSH1 memOffset 0x60, 0x04, // PUSH1 param (copy) 0x60, index, // PUSH1 signatureIndex 0xB4, // SIGPARAM 0x60, - 0x00, // PUSH1 0 - 0x51, // MLOAD + mem_offset, // PUSH1 memOffset + 0x51, // MLOAD 0x60, 0x00, // PUSH1 0 (slot) 0x55, // SSTORE @@ -3403,7 +3403,7 @@ mod sigparam_execution_tests { // 4 payload bytes copied into memory at 0; MLOAD reads them left-aligned // in the word, with the rest zero-filled. let (result, db, reader) = run_reader( - sigparam_copy_code(0, 4, 0), + sigparam_copy_code(0, 4, 0, 0), vec![arbitrary_sig(vec![0xDE, 0xAD, 0xBE, 0xEF])], ); result.expect("valid tx"); @@ -3419,7 +3419,7 @@ mod sigparam_execution_tests { fn sigparam_0x04_zero_fills_past_the_end() { // Asking for 8 bytes of a 2-byte signature zero-fills the remainder. let (result, db, reader) = run_reader( - sigparam_copy_code(0, 8, 0), + sigparam_copy_code(0, 8, 0, 0), vec![arbitrary_sig(vec![0x11, 0x22])], ); result.expect("valid tx"); @@ -3430,6 +3430,25 @@ mod sigparam_execution_tests { U256::from_big_endian(&expected), ); } + + #[test] + fn sigparam_0x04_reads_operands_in_calldatacopy_order() { + // The copy operands follow `CALLDATACOPY`: memOffset above dataOffset + // above length. Distinct values for all three, and a destination past the + // first word, pin the order — reading them in any other order lands the + // bytes somewhere else (or copies a different count). + let (result, db, reader) = run_reader( + sigparam_copy_code(0, 3, 1, 0x20), + vec![arbitrary_sig(vec![0xAA, 0xBB, 0xCC, 0xDD])], + ); + result.expect("valid tx"); + let mut expected = [0u8; 32]; + expected[..3].copy_from_slice(&[0xBB, 0xCC, 0xDD]); + assert_eq!( + storage_of(&db, reader, U256::zero()), + U256::from_big_endian(&expected), + ); + } } // ==================== Default code: signature index by scope ====================