Skip to content

Commit

Permalink
feat(evm): add MCOPY opcode (#1572)
Browse files Browse the repository at this point in the history
* mcopy initial commit wip

* improve readability of test

* add tests from SPEC

* failing tests

* clean up tests for MCOPY

* fix linter

* use copy_within for MCOPY

* indicate unused

* move size check earlier

* rust lint add else

* use region variables and remove unneeded casting

* fmt
  • Loading branch information
snissn authored Sep 30, 2024
1 parent 6453d8d commit 76b4765
Show file tree
Hide file tree
Showing 3 changed files with 285 additions and 0 deletions.
1 change: 1 addition & 0 deletions actors/evm/src/interpreter/execution.rs
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,7 @@ pub mod opcodes {
0x59: MSIZE,
0x5a: GAS,
0x5b: JUMPDEST,
0x5e: MCOPY,
0x5F: PUSH0,
0x60: PUSH1,
0x61: PUSH2,
Expand Down
283 changes: 283 additions & 0 deletions actors/evm/src/interpreter/instructions/memory.rs
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,48 @@ pub fn get_memory_region(
}))
}

#[inline]
pub fn mcopy(
state: &mut ExecutionState,
_: &System<impl Runtime>,
dest_index: U256,
src_index: U256,
size: U256,
) -> Result<(), ActorError> {
// Copy memory from src_index to dest_index.
// Handles overlapping slices as if using an intermediate buffer, ensuring correct copying.
// Expands memory if src_index + size or dest_index + size exceeds current bounds.
// Returns an error if memory regions are invalid or cannot be allocated.

if size > 0 {
copy_within_memory(&mut state.memory, dest_index, src_index, size)
} else {
Ok(())
}
}

pub fn copy_within_memory(
memory: &mut Memory,
dest_index: U256,
src_index: U256,
size: U256,
) -> Result<(), ActorError> {
// Expand memory to accommodate requested src_index + size
let src_region = get_memory_region(memory, src_index, size)?.expect("empty region");

// Expand memory to match dest_index + size
let destination_region = get_memory_region(memory, dest_index, size)?.expect("empty region");

// Named variables for clarity
let source_range = src_region.offset..(src_region.offset + src_region.size.get());
let destination_index = destination_region.offset;

// Copy memory
memory.copy_within(source_range, destination_index);

Ok(())
}

pub fn copy_to_memory(
memory: &mut Memory,
dest_offset: U256,
Expand Down Expand Up @@ -200,6 +242,247 @@ mod tests {
assert_eq!(&mem[0..4], result_data);
}

#[test]
fn test_mcopy() {
const LENGTH: usize = 2;
const OFFSET: usize = 1;
const DEST_OFFSET: usize = 0;

evm_unit_test! {
(m) {
MCOPY;
}

// Grow memory and set initial values
m.state.memory.grow(32);
m.state.memory[..3].copy_from_slice(&[0x00, 0x01, 0x02]);

// Set up stack
m.state.stack.push(U256::from(LENGTH)).unwrap();
m.state.stack.push(U256::from(OFFSET)).unwrap();
m.state.stack.push(U256::from(DEST_OFFSET)).unwrap();

// Execute and assert
assert!(m.step().is_ok(), "execution step failed");
assert_eq!(m.state.stack.len(), 0);

// Setup expected memory and assert
let mut expected = [0u8; 32];
expected[..3].copy_from_slice(&[0x01, 0x02, 0x02]);
assert_eq!(&*m.state.memory, &expected);
};
}

#[test]
fn test_mcopy_0_32_32() {
evm_unit_test! {
(m) {
MCOPY;
}

// Grow memory and set initial values
m.state.memory.grow(64);
m.state.memory[32..64].copy_from_slice(&[
0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f,
0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f
]);

// Set up stack
m.state.stack.push(U256::from(32)).unwrap(); // length
m.state.stack.push(U256::from(32)).unwrap(); // source offset
m.state.stack.push(U256::from(0)).unwrap(); // destination offset

// Execute and assert
assert!(m.step().is_ok(), "execution step failed");
assert_eq!(m.state.stack.len(), 0);

// Setup expected memory and assert
let mut expected = [0u8; 64];
expected[0..64].copy_from_slice(&[
0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f,
0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f,
0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f,
0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f
]);
assert_eq!(&*m.state.memory, &expected);
};
}

#[test]
fn test_mcopy_0_0_32() {
evm_unit_test! {
(m) {
MCOPY;
}

// Grow memory and set initial values
m.state.memory.grow(32);
m.state.memory[..32].copy_from_slice(&[0x01; 32]);

// Set up stack
m.state.stack.push(U256::from(32)).unwrap(); // length
m.state.stack.push(U256::from(0)).unwrap(); // source offset
m.state.stack.push(U256::from(0)).unwrap(); // destination offset

// Execute and assert
assert!(m.step().is_ok(), "execution step failed");
assert_eq!(m.state.stack.len(), 0);

// Setup expected memory and assert
let expected = [0x01; 32];
assert_eq!(&m.state.memory[..32], &expected);
};
}

#[test]
fn test_mcopy_0_1_8() {
evm_unit_test! {
(m) {
MCOPY;
}

// Grow memory and set initial values
m.state.memory.grow(32);
m.state.memory[..8].copy_from_slice(&[0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07]);
m.state.memory[8] = 0x08;

// Set up stack
m.state.stack.push(U256::from(8)).unwrap(); // length
m.state.stack.push(U256::from(1)).unwrap(); // source offset
m.state.stack.push(U256::from(0)).unwrap(); // destination offset

// Execute and assert
assert!(m.step().is_ok(), "execution step failed");
assert_eq!(m.state.stack.len(), 0);

// Setup expected memory and assert
let mut expected = [0u8; 32];
expected[..8].copy_from_slice(&[0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08]);
expected[8] = 0x08;
assert_eq!(&m.state.memory[..9], &expected[..9]);
};
}

#[test]
fn test_mcopy_1_0_8() {
evm_unit_test! {
(m) {
MCOPY;
}

// Grow memory and set initial values
m.state.memory.grow(32);
m.state.memory[..8].copy_from_slice(&[0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07]);
m.state.memory[8] = 0x08;

// Set up stack
m.state.stack.push(U256::from(8)).unwrap(); // length
m.state.stack.push(U256::from(0)).unwrap(); // source offset
m.state.stack.push(U256::from(1)).unwrap(); // destination offset

// Execute and assert
assert!(m.step().is_ok(), "execution step failed");
assert_eq!(m.state.stack.len(), 0);

// Setup expected memory and assert
let mut expected = [0u8; 32];
expected[..8].copy_from_slice(&[0x00, 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06]);
expected[8] = 0x07;
assert_eq!(&m.state.memory[..9], &expected[..9]);
};
}

#[test]
fn test_mcopy_out_of_range_dest() {
evm_unit_test! {
(m) {
MCOPY;
}

// Initial memory setup
m.state.memory.grow(32);
m.state.memory[..4].copy_from_slice(&[0x01, 0x02, 0x03, 0x04]);

// Set up stack: Attempt to copy to a destination beyond the current memory range
m.state.stack.push(U256::from(4)).unwrap(); // length
m.state.stack.push(U256::from(0)).unwrap(); // source offset
m.state.stack.push(U256::from(64)).unwrap(); // out of range destination offset

// Execute and expect memory expansion
assert!(m.step().is_ok(), "execution step failed");
assert_eq!(m.state.stack.len(), 0);

// Check that memory was expanded correctly
assert_eq!(m.state.memory.len(), 96);

// Check the memory contents
let mut expected = [0u8; 96];
expected[..4].copy_from_slice(&[0x01, 0x02, 0x03, 0x04]);
expected[64..68].copy_from_slice(&[0x01, 0x02, 0x03, 0x04]);
assert_eq!(&*m.state.memory, &expected[..]);
};
}

#[test]
fn test_mcopy_partially_out_of_range_source() {
evm_unit_test! {
(m) {
MCOPY;
}

// Initial memory setup
m.state.memory.grow(32);
m.state.memory[..28].copy_from_slice(&[0x01; 28]);

// Set up stack: Source partially out of range
m.state.stack.push(U256::from(10)).unwrap(); // length
m.state.stack.push(U256::from(24)).unwrap(); // source offset (partially out of range)
m.state.stack.push(U256::from(0)).unwrap(); // destination offset

// Execute and expect memory expansion
assert!(m.step().is_ok(), "execution step failed");
assert_eq!(m.state.stack.len(), 0);

// Check the length of the memory after the operation
assert_eq!(m.state.memory.len(), 32+EVM_WORD_SIZE); // Memory should remain at 32 bytes after the operation

// Check that memory was expanded correctly
let mut expected = vec![0x01; 4]; // First 4 bytes copied
expected.extend_from_slice(&[0x00; 4]); // Remaining 4 bytes unchanged
assert_eq!(&m.state.memory[..8], &expected[..8]);
};
}

#[test]
fn test_mcopy_fully_out_of_range_dest_fails() {
evm_unit_test! {
(m) {
MCOPY;
}

// Initial memory setup
m.state.memory.grow(32);
m.state.memory[..4].copy_from_slice(&[0x01, 0x02, 0x03, 0x04]);

// Set up stack: Attempt to copy to a destination fully out of range
m.state.stack.push(U256::from(4)).unwrap(); // length
m.state.stack.push(U256::from(0)).unwrap(); // source offset
m.state.stack.push(U256::from(128)).unwrap(); // fully out of range destination offset


// Execute and assert memory grows
assert!(m.step().is_ok(), "expected step to succeed and grow memory");
assert_eq!(m.state.memory.len(), 160); // Expected memory to grow

// Check the memory contents
let mut expected = [0u8; 132];
expected[..4].copy_from_slice(&[0x01, 0x02, 0x03, 0x04]);
expected[128..132].copy_from_slice(&[0x01, 0x02, 0x03, 0x04]);
assert_eq!(&m.state.memory[0..132], &expected[0..132]);

};
}

#[test]
fn test_mload_nothing() {
evm_unit_test! {
Expand Down
1 change: 1 addition & 0 deletions actors/evm/src/interpreter/instructions/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -352,6 +352,7 @@ def_stdfun_code! { CODESIZE() => call::codesize }
def_stdproc_code! { CODECOPY(a, b, c) => call::codecopy }
def_stdfun! { CREATE(a, b, c) => lifecycle::create }
def_stdfun! { CREATE2(a, b, c, d) => lifecycle::create2 }
def_stdproc! { MCOPY(a,b,c) => memory::mcopy }
def_stdproc! { JUMPDEST() => control::nop }
def_stdproc! { INVALID() => control::invalid }
def_exit! { RETURN(a, b) => control::ret }
Expand Down

0 comments on commit 76b4765

Please sign in to comment.