diff --git a/rskj-core/src/test/java/co/rsk/vm/opcode/MCopyDslTest.java b/rskj-core/src/test/java/co/rsk/vm/opcode/MCopyDslTest.java index 82517cf205..968c33312a 100644 --- a/rskj-core/src/test/java/co/rsk/vm/opcode/MCopyDslTest.java +++ b/rskj-core/src/test/java/co/rsk/vm/opcode/MCopyDslTest.java @@ -108,6 +108,29 @@ void testMCOPY_OnEachTestCase_ExecutesAsExpected(String dslFile) throws FileNotF } + @Test + void testMCOPY_zeroLengthOutOfBoundsDestination_runsOutOfGasAsExpected() throws FileNotFoundException, DslProcessorException { + + DslParser parser = DslParser.fromResource("dsl/opcode/mcopy/testZeroLengthOutOfBoundsDestination.txt"); + World world = new World(); + WorldDslProcessor processor = new WorldDslProcessor(world); + processor.processCommands(parser); + + // Assertions + + assertBlockExistsAndContainsExpectedNumberOfTxs(world, "b01", 1); + assertTxExistsWithExpectedReceiptStatus(world, "txTestMCopy", true); + + assertBlockExistsAndContainsExpectedNumberOfTxs(world, "b02", 1); + TransactionReceipt txTestMCopyOKCallReceipt = assertTxExistsWithExpectedReceiptStatus(world, "txTestMCopyOKCall", false); + + // Event Assertions + + Assertions.assertEquals(0, TransactionReceiptUtil.getEventCount(txTestMCopyOKCallReceipt, "OK", null)); + Assertions.assertEquals(0, TransactionReceiptUtil.getEventCount(txTestMCopyOKCallReceipt, "ERROR", null)); + + } + private static Stream provideParametersForMCOPYTestCases() { return Stream.of( diff --git a/rskj-core/src/test/java/co/rsk/vm/opcode/MCopyGasTest.java b/rskj-core/src/test/java/co/rsk/vm/opcode/MCopyGasTest.java index 644c31a46a..38fb65befa 100644 --- a/rskj-core/src/test/java/co/rsk/vm/opcode/MCopyGasTest.java +++ b/rskj-core/src/test/java/co/rsk/vm/opcode/MCopyGasTest.java @@ -71,13 +71,7 @@ void testMCopy_ShouldConsumeTheCorrectAmountOfGas(String[] initMemory, int dst, program.stackPush(DataWord.valueOf(dst)); // When - try { - while (!program.isStopped()) { - vm.step(program); - } - } catch(Program.StackTooSmallException e) { - Assertions.fail("Stack too small exception"); - } + executeProgram(vm, program); // Then Assertions.assertEquals(0, program.getStack().size()); @@ -95,4 +89,73 @@ private static Stream provideParametersForMCOPYGasTest() { ); } + @ParameterizedTest + @MethodSource("provideParametersForMCOPYOutOfGasExceptionTest") + void testMCopy_ShouldThrowOutOfGasException(String[] initMemory, long dst, long src, long length) { + // Given + byte[] code = compiler.compile("MCOPY"); + VM vm = new VM(vmConfig, precompiledContracts); + + Program program = new Program(vmConfig, precompiledContracts, blockFactory, activationConfig, code, invoke, transaction, new HashSet<>(), new BlockTxSignatureCache(new ReceivedTxSignatureCache())); + + int address = 0; + for (String entry : initMemory) { + program.memorySave(DataWord.valueOf(address), DataWord.valueFromHex(entry)); + address += 32; + } + + program.stackPush(DataWord.valueOf(length)); // Mind the stack order!! + program.stackPush(DataWord.valueOf(src)); + program.stackPush(DataWord.valueOf(dst)); + + // Then + Program.OutOfGasException ex = Assertions.assertThrows(Program.OutOfGasException.class, () -> executeProgram(vm, program)); + Assertions.assertTrue(ex.getMessage().contains("Not enough gas for 'MCOPY' operation")); + } + + private static Stream provideParametersForMCOPYOutOfGasExceptionTest() { + + String[] emptyMemory = new String[]{}; + String[] existentMemory = new String[]{ + "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f", + "202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f", + "404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f", + "606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f", + "808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9f", + "a0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebf", + "c0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedf", + "e0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fafbfcfdfeff" + }; + + return Stream.of( + + // From an empty memory + Arguments.of(emptyMemory, Program.MAX_MEMORY, 0, 1), + Arguments.of(emptyMemory, Program.MAX_MEMORY, 0, 1), + Arguments.of(emptyMemory, Program.MAX_MEMORY, 0, 1), + Arguments.of(emptyMemory, 0, 0, Program.MAX_MEMORY + 1), + Arguments.of(emptyMemory, 0, 0, Program.MAX_MEMORY + 1), + Arguments.of(emptyMemory, 0, 0, Program.MAX_MEMORY + 1), + + // From existent memory + Arguments.of(existentMemory, Program.MAX_MEMORY, 0, 1), + Arguments.of(existentMemory, Program.MAX_MEMORY, 0, 1), + Arguments.of(existentMemory, Program.MAX_MEMORY, 0, 1), + Arguments.of(existentMemory, 0, 0, Program.MAX_MEMORY + 1), + Arguments.of(existentMemory, 0, 0, Program.MAX_MEMORY + 1), + Arguments.of(existentMemory, 0, 0, Program.MAX_MEMORY + 1) + + ); + } + + private static void executeProgram(VM vm, Program program) { + try { + while (!program.isStopped()) { + vm.step(program); + } + } catch(Program.StackTooSmallException e) { + Assertions.fail("Stack too small exception"); + } + } + } \ No newline at end of file diff --git a/rskj-core/src/test/resources/dsl/opcode/mcopy/testZeroLengthOutOfBoundsDestination.txt b/rskj-core/src/test/resources/dsl/opcode/mcopy/testZeroLengthOutOfBoundsDestination.txt new file mode 100644 index 0000000000..096eaef8cb --- /dev/null +++ b/rskj-core/src/test/resources/dsl/opcode/mcopy/testZeroLengthOutOfBoundsDestination.txt @@ -0,0 +1,112 @@ +comment + +// CONTRACT CODE +// + +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.20; + +contract TestMCopy { + constructor() {} + + event OK(); + event ERROR(); + + function checkMCopy() external { + if (testZeroLengthOutOfBoundsDestination()) { + emit OK(); + } else { + emit ERROR(); + } + } + + function testZeroLengthOutOfBoundsDestination() public pure returns (bool status) { + + bytes32 word1 = 0x000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f; + uint256 dst = (2 ** 256) - 1; + + assembly { + mstore(0, word1) // ... Initialize Memory + mcopy(dst, 1, 33) // Use MCOPY to copy 0 bytes starting from offset 0 to offset (2^256 - 1) in memory + } + status = true; // This line must not be reached as OOG Exception should occur when trying to execute MCOPY + } + +} + +// DESCRIPTION + +This contract contains two functions: checkMCopy, and testZeroLengthOutOfBoundsDestination. + +* checkMCopy simply checks the result of the memory copying against an expected value and: + - If returned value is true, then the OK event is emitted. + - ERROR event is emitted otherwise. + +* testZeroLengthOutOfBoundsDestination manage the memory by initializing the memory, and then executing MCOPY with the corresponding values as follows: + - First it stores a value to memory on offset 0 + - Then uses MCOPY to copy (2 ** 256) - 1 bytes starting on offset 0 to offset 0 + - Finally it returns true. (This line must never be reached, is added simply to avoid misleading and ugly code). + +Executing checkMCopy must never emit any event as executing the MCOPY instruction with the given parameters will produce an Out Of Gas exception. + +// CONTRACT BYTECODE + +6080604052348015600e575f80fd5b506101928061001c5f395ff3fe608060405234801561000f575f80fd5b5060043610610034575f3560e01c80638c2bcab914610038578063cdae7c4114610042575b5f80fd5b610040610060565b005b61004a6100cd565b6040516100579190610143565b60405180910390f35b6100686100cd565b1561009e577fd48fe2800bace8f5ca2450feacbd6efc681b1cd0115019bb49fa529b6171bf6760405160405180910390a16100cb565b7f1c9c433b57013295d61f5c5738f5e2cb1de70bb5ba5b2896edfa8efae345965e60405160405180910390a15b565b5f807e0102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f5f1b90505f7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff9050815f5260216001825e60019250505090565b5f8115159050919050565b61013d81610129565b82525050565b5f6020820190506101565f830184610134565b9291505056fea264697066735822122060f880beef67959f26009305aa48e5f6abb8519686cf1974841b6873d619dfb964736f6c634300081a0033 + +// CONTRACT CALL + +- checkMCopy() + + 8c2bcab9 + +end + +# Create and fund new account +account_new acc1 10000000 + +# Create transaction to deploy TestMCopy contract +transaction_build txTestMCopy + sender acc1 + receiverAddress 00 + value 0 + data 6080604052348015600e575f80fd5b506101928061001c5f395ff3fe608060405234801561000f575f80fd5b5060043610610034575f3560e01c80638c2bcab914610038578063cdae7c4114610042575b5f80fd5b610040610060565b005b61004a6100cd565b6040516100579190610143565b60405180910390f35b6100686100cd565b1561009e577fd48fe2800bace8f5ca2450feacbd6efc681b1cd0115019bb49fa529b6171bf6760405160405180910390a16100cb565b7f1c9c433b57013295d61f5c5738f5e2cb1de70bb5ba5b2896edfa8efae345965e60405160405180910390a15b565b5f807e0102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f5f1b90505f7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff9050815f5260216001825e60019250505090565b5f8115159050919050565b61013d81610129565b82525050565b5f6020820190506101565f830184610134565b9291505056fea264697066735822122060f880beef67959f26009305aa48e5f6abb8519686cf1974841b6873d619dfb964736f6c634300081a0033 + gas 200000 + build + +# Create block to hold txTestMCopy transaction +block_build b01 + parent g00 + transactions txTestMCopy + build + +# Connect block +block_connect b01 + +# Check b01 is best block +assert_best b01 + +# Check txTestMCopy succeeded +assert_tx_success txTestMCopy + +# Create transaction to execute checkMCopy() method +transaction_build txTestMCopyOKCall + sender acc1 + nonce 1 + contract txTestMCopy + value 0 + data 8c2bcab9 + gas 30000 + build + +# Create block to hold txTestMCopyOKCall transaction +block_build b02 + parent b01 + transactions txTestMCopyOKCall + gasLimit 30000 + build + +# Connect block +block_connect b02 + +# Check b02 is best block +assert_best b02 \ No newline at end of file