From f6ac16845a0ac6ca05dec44d7102975d36006a0c Mon Sep 17 00:00:00 2001 From: Diego Nehab <1635557+diegonehab@users.noreply.github.com> Date: Mon, 13 Apr 2026 15:00:20 +0100 Subject: [PATCH 1/7] feat: improve coverage --- Makefile | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/Makefile b/Makefile index df7b10e..65c4b78 100644 --- a/Makefile +++ b/Makefile @@ -77,9 +77,10 @@ coverage-prod: dep COVERAGE_OUTPUT_DIR ?= coverage coverage-report: $(COVERAGE_OUTPUT_DIR) - lcov -a lcov-mock.info -a lcov-prod.info -o $(COVERAGE_OUTPUT_DIR)/lcov.info + sed 's|SF:src/AccessLogs.sol|SF:src/AccessLogsMock.sol|' lcov-mock.info > lcov-mock-patched.info + lcov -a lcov-mock-patched.info -a lcov-prod.info -o $(COVERAGE_OUTPUT_DIR)/lcov.info lcov --summary $(COVERAGE_OUTPUT_DIR)/lcov.info | tee $(COVERAGE_OUTPUT_DIR)/coverage.txt - genhtml --ignore-errors unmapped $(COVERAGE_OUTPUT_DIR)/lcov.info -o $(COVERAGE_OUTPUT_DIR)/html + genhtml --ignore-errors unmapped --substitute 's|AccessLogsMock.sol|AccessLogs.sol|' $(COVERAGE_OUTPUT_DIR)/lcov.info -o $(COVERAGE_OUTPUT_DIR)/html $(COVERAGE_OUTPUT_DIR): mkdir -p $(COVERAGE_OUTPUT_DIR) From 91d4f0c3a6865fd66a372ecaae541332b7509c3e Mon Sep 17 00:00:00 2001 From: Diego Nehab <1635557+diegonehab@users.noreply.github.com> Date: Wed, 10 Jun 2026 11:16:48 +0100 Subject: [PATCH 2/7] feat: take revert root hash in sendCmioResponse The emulator now records the revert root hash as part of send_cmio_response, so the generated SendCmioResponse.sol takes a bytes32 revertRootHash argument and writes it via EmulatorCompat.writeRevertRootHash. Rename EmulatorCompat.getRevertRootHash and setRevertRootHash to readRevertRootHash and writeRevertRootHash to match the emulator naming. Update generate_SendCmioResponse.sh for the new C++ signature and explicit-instantiation layout. Fix generate_EmulatorConstants.lua to query the renamed cartesi.HTIF_YIELD_* Lua constants and regenerate EmulatorConstants.sol, which also picks up the new UARCH_PRISTINE_STATE_HASH. The SendCmioResponse test passes the catalog's initialRootHash as the revert root hash, matching the regenerated test logs. --- helper_scripts/generate_EmulatorConstants.lua | 8 ++++---- helper_scripts/generate_SendCmioResponse.sh | 4 ++-- src/EmulatorCompat.sol | 4 ++-- src/EmulatorConstants.sol | 2 +- src/SendCmioResponse.sol | 3 +++ test/SendCmioResponse.t.sol | 7 ++++++- 6 files changed, 18 insertions(+), 10 deletions(-) diff --git a/helper_scripts/generate_EmulatorConstants.lua b/helper_scripts/generate_EmulatorConstants.lua index 3e3d9ff..83c9f7b 100755 --- a/helper_scripts/generate_EmulatorConstants.lua +++ b/helper_scripts/generate_EmulatorConstants.lua @@ -40,15 +40,15 @@ out:write(' uint64 constant HTIF_FROMHOST_ADDRESS = 0x' .. out:write(' uint64 constant HTIF_TOHOST_ADDRESS = 0x' .. hex(cartesi.machine:get_reg_address("htif_tohost")) .. ';\n') out:write(' uint8 constant CMIO_YIELD_REASON_ADVANCE_STATE = 0x' .. - hex(cartesi.CMIO_YIELD_REASON_ADVANCE_STATE) .. ';\n') + hex(cartesi.HTIF_YIELD_REASON_ADVANCE_STATE) .. ';\n') out:write(' uint32 constant HASH_TREE_LOG2_WORD_SIZE = 0x' .. hex(cartesi.HASH_TREE_LOG2_WORD_SIZE) .. ';\n') out:write(' uint32 constant HASH_TREE_WORD_SIZE = uint32(1) << HASH_TREE_LOG2_WORD_SIZE;\n') out:write(' uint16 constant CMIO_YIELD_MANUAL_REASON_RX_ACCEPTED = 0x' .. - hex(cartesi.CMIO_YIELD_MANUAL_REASON_RX_ACCEPTED) .. ';\n') + hex(cartesi.HTIF_YIELD_MANUAL_REASON_RX_ACCEPTED) .. ';\n') out:write(' uint16 constant CMIO_YIELD_MANUAL_REASON_RX_REJECTED = 0x' .. - hex(cartesi.CMIO_YIELD_MANUAL_REASON_RX_REJECTED) .. ';\n') + hex(cartesi.HTIF_YIELD_MANUAL_REASON_RX_REJECTED) .. ';\n') out:write(' uint16 constant CMIO_YIELD_MANUAL_REASON_TX_EXCEPTION = 0x' .. - hex(cartesi.CMIO_YIELD_MANUAL_REASON_TX_EXCEPTION) .. ';\n') + hex(cartesi.HTIF_YIELD_MANUAL_REASON_TX_EXCEPTION) .. ';\n') out:write(' uint8 constant UARCH_STATE_LOG2_SIZE = ' .. cartesi.UARCH_STATE_LOG2_SIZE .. ';\n') out:write(' uint64 constant AR_CMIO_RX_BUFFER_START = 0x' .. hex(cartesi.AR_CMIO_RX_BUFFER_START) .. ';\n') out:write(' uint8 constant AR_CMIO_RX_BUFFER_LOG2_SIZE = 0x' .. hex(cartesi.AR_CMIO_RX_BUFFER_LOG2_SIZE) .. ';\n') diff --git a/helper_scripts/generate_SendCmioResponse.sh b/helper_scripts/generate_SendCmioResponse.sh index 4fe7081..4213d67 100755 --- a/helper_scripts/generate_SendCmioResponse.sh +++ b/helper_scripts/generate_SendCmioResponse.sh @@ -37,12 +37,12 @@ pattern="namespace cartesi \{(.*)\}" cpp_src=`echo "${BASH_REMATCH[1]}" \ | $SED "/Explicit instantiatio/d" \ | $SED "/template/d" \ - | $SED "/ uint32 length);/d" \ + | $SED "/uint32 length);/d" \ | $SED "s/machine_merkle_tree::get_log2_word_size()/TREE_LOG2_WORD_SIZE/g" \ | $SED -E "s/($COMPAT_FNS)/EmulatorCompat.\1/g" \ | $SED "s/writeMemoryWithPadding(a, AR_CMIO_RX_BUFFER_START, data, dataLength, writeLengthLog2Size);/a.writeRegion(Memory.regionFromPhysicalAddress(AR_CMIO_RX_BUFFER_START.toPhysicalAddress(),Memory.alignedSizeFromLog2(uint8(writeLengthLog2Size - HASH_TREE_LOG2_WORD_SIZE))),dataHash);"/g \ | $SED -E "s/($CONSTANTS)([^a-zA-Z])/EmulatorConstants.\1\2/g" \ - | $SED "s/void send_cmio_response(STATE_ACCESS a, uint16 reason, bytes data, uint32 dataLength) {/function sendCmioResponse(AccessLogs.Context memory a, uint16 reason, bytes32 dataHash, uint32 dataLength) internal pure {/" \ + | $SED "s/void send_cmio_response(STATE_ACCESS a, bytes32 revertRootHash, uint16 reason, bytes data, uint32 dataLength) {/function sendCmioResponse(AccessLogs.Context memory a, bytes32 revertRootHash, uint16 reason, bytes32 dataHash, uint32 dataLength) internal pure {/" \ | $SED "s/const uint64/uint64/g" \ | $SED "s/const uint32/uint32/g" \ | $SED "/^$/N;/^\n$/D" diff --git a/src/EmulatorCompat.sol b/src/EmulatorCompat.sol index 59bb62d..a9583a6 100644 --- a/src/EmulatorCompat.sol +++ b/src/EmulatorCompat.sol @@ -23,7 +23,7 @@ library EmulatorCompat { using Buffer for Buffer.Context; using Memory for uint64; - function getRevertRootHash(AccessLogs.Context memory a) + function readRevertRootHash(AccessLogs.Context memory a) internal pure returns (bytes32) @@ -107,7 +107,7 @@ library EmulatorCompat { ); } - function setRevertRootHash( + function writeRevertRootHash( AccessLogs.Context memory a, bytes32 revertRootHash ) internal pure { diff --git a/src/EmulatorConstants.sol b/src/EmulatorConstants.sol index 92832af..300b292 100644 --- a/src/EmulatorConstants.sol +++ b/src/EmulatorConstants.sol @@ -25,7 +25,7 @@ library EmulatorConstants { // START OF AUTO-GENERATED CODE bytes32 constant UARCH_PRISTINE_STATE_HASH = - 0xa2f4f0018081d795e47c7feae9300055e8551eda5bd6473e54ca80ece64ea620; + 0x245a715ed3343e88ea5b2810584faea872991cca0200979e8f43056cd2db59c6; uint64 constant UARCH_CYCLE_ADDRESS = 0x400008; uint64 constant UARCH_CYCLE_MAX = 0x100000; uint64 constant UARCH_HALT_FLAG_ADDRESS = 0x400000; diff --git a/src/SendCmioResponse.sol b/src/SendCmioResponse.sol index fdf57bb..9d86deb 100644 --- a/src/SendCmioResponse.sol +++ b/src/SendCmioResponse.sol @@ -31,6 +31,7 @@ library SendCmioResponse { function sendCmioResponse( AccessLogs.Context memory a, + bytes32 revertRootHash, uint16 reason, bytes32 dataHash, uint32 dataLength @@ -38,6 +39,8 @@ library SendCmioResponse { if (!EmulatorCompat.readIflagsY(a)) { EmulatorCompat.throwRuntimeError(a, "iflags.Y is not set"); } + // Record the machine root hash to revert to in case the response is eventually rejected + EmulatorCompat.writeRevertRootHash(a, revertRootHash); // A zero length data is a valid response. We just skip writing to the rx buffer. if (dataLength > 0) { // Find the write length: the smallest power of 2 that is >= dataLength and >= tree leaf size diff --git a/test/SendCmioResponse.t.sol b/test/SendCmioResponse.t.sol index 14de898..562520e 100644 --- a/test/SendCmioResponse.t.sol +++ b/test/SendCmioResponse.t.sol @@ -98,8 +98,13 @@ contract SendCmioResponse_Test is AccessLogJsonParse { } bytes32 paddedResponseHash = keccak256(paddedResponse); // call sendCmioResponse + // the test log file was generated passing the initial root hash as the revert root hash SendCmioResponse.sendCmioResponse( - accessLogs, reason, paddedResponseHash, uint32(response.length) + accessLogs, + initialRootHash, + reason, + paddedResponseHash, + uint32(response.length) ); // ensure that the final root hash matches the expected value assertEq( From d6da51fc9954dcf2a8d34e9d38be4b464af91356 Mon Sep 17 00:00:00 2001 From: Diego Nehab <1635557+diegonehab@users.noreply.github.com> Date: Wed, 10 Jun 2026 20:33:34 +0100 Subject: [PATCH 3/7] feat: fold revert-on-reject into UArchReset Regenerate UArchReset.sol from the emulator. After resetting the uarch state, reset() now reads iflags.Y and conditionally htif.tohost, decodes the dev/cmd/reason fields, and on a rejected manual yield calls the new EmulatorCompat.revertState, which replaces currentRootHash with the value read from the revert root hash leaf. Callers need no substitution logic, the boundary transition in the dispute becomes just step and reset. Delete AdvanceStatus.sol. Its only consumer was Dave's revertIfNeeded, which the fold makes obsolete. Rename the EmulatorConstants yield reason trio from CMIO_YIELD_MANUAL_REASON_* to HTIF_YIELD_MANUAL_REASON_*, matching the emulator naming now that AdvanceStatus is gone, and add the HTIF field shift and mask constants plus HTIF_DEV_YIELD and HTIF_YIELD_CMD_MANUAL, all queried from the emulator by generate_EmulatorConstants.lua. CMIO_YIELD_REASON_ADVANCE_STATE keeps its name, Dave references it. Update generate_UArchReset.sh to prefix EmulatorConstants names like the other generators. Generalize the test json parser to derive per-access sibling counts and to emit the value followed by the leaf hash for leaf read accesses, so mixed-shape logs replay. UArchReset.t.sol now also replays the rejected-reset fixture, whose final root hash is the recorded revert root hash. --- helper_scripts/generate_EmulatorConstants.lua | 14 +++- helper_scripts/generate_UArchReset.sh | 5 ++ src/AdvanceStatus.sol | 76 ------------------- src/EmulatorCompat.sol | 4 + src/EmulatorConstants.sol | 14 +++- src/UArchReset.sol | 29 +++++++ templates/UArchReplay.t.sol.template | 2 +- test/AccessLogJsonParse.sol | 12 ++- test/SendCmioResponse.t.sol | 2 +- test/UArchReset.t.sol | 32 +++----- 10 files changed, 81 insertions(+), 109 deletions(-) delete mode 100644 src/AdvanceStatus.sol diff --git a/helper_scripts/generate_EmulatorConstants.lua b/helper_scripts/generate_EmulatorConstants.lua index 83c9f7b..55e51c2 100755 --- a/helper_scripts/generate_EmulatorConstants.lua +++ b/helper_scripts/generate_EmulatorConstants.lua @@ -43,11 +43,19 @@ out:write(' uint8 constant CMIO_YIELD_REASON_ADVANCE_STATE = 0x' .. hex(cartesi.HTIF_YIELD_REASON_ADVANCE_STATE) .. ';\n') out:write(' uint32 constant HASH_TREE_LOG2_WORD_SIZE = 0x' .. hex(cartesi.HASH_TREE_LOG2_WORD_SIZE) .. ';\n') out:write(' uint32 constant HASH_TREE_WORD_SIZE = uint32(1) << HASH_TREE_LOG2_WORD_SIZE;\n') -out:write(' uint16 constant CMIO_YIELD_MANUAL_REASON_RX_ACCEPTED = 0x' .. +out:write(' uint32 constant HTIF_DEV_SHIFT = 0x' .. hex(cartesi.HTIF_DEV_SHIFT) .. ';\n') +out:write(' uint32 constant HTIF_CMD_SHIFT = 0x' .. hex(cartesi.HTIF_CMD_SHIFT) .. ';\n') +out:write(' uint32 constant HTIF_REASON_SHIFT = 0x' .. hex(cartesi.HTIF_REASON_SHIFT) .. ';\n') +out:write(' uint64 constant HTIF_DEV_MASK = 0x' .. hex(cartesi.HTIF_DEV_MASK) .. ';\n') +out:write(' uint64 constant HTIF_CMD_MASK = 0x' .. hex(cartesi.HTIF_CMD_MASK) .. ';\n') +out:write(' uint64 constant HTIF_REASON_MASK = 0x' .. hex(cartesi.HTIF_REASON_MASK) .. ';\n') +out:write(' uint64 constant HTIF_DEV_YIELD = 0x' .. hex(cartesi.HTIF_DEV_YIELD) .. ';\n') +out:write(' uint64 constant HTIF_YIELD_CMD_MANUAL = 0x' .. hex(cartesi.HTIF_YIELD_CMD_MANUAL) .. ';\n') +out:write(' uint16 constant HTIF_YIELD_MANUAL_REASON_RX_ACCEPTED = 0x' .. hex(cartesi.HTIF_YIELD_MANUAL_REASON_RX_ACCEPTED) .. ';\n') -out:write(' uint16 constant CMIO_YIELD_MANUAL_REASON_RX_REJECTED = 0x' .. +out:write(' uint16 constant HTIF_YIELD_MANUAL_REASON_RX_REJECTED = 0x' .. hex(cartesi.HTIF_YIELD_MANUAL_REASON_RX_REJECTED) .. ';\n') -out:write(' uint16 constant CMIO_YIELD_MANUAL_REASON_TX_EXCEPTION = 0x' .. +out:write(' uint16 constant HTIF_YIELD_MANUAL_REASON_TX_EXCEPTION = 0x' .. hex(cartesi.HTIF_YIELD_MANUAL_REASON_TX_EXCEPTION) .. ';\n') out:write(' uint8 constant UARCH_STATE_LOG2_SIZE = ' .. cartesi.UARCH_STATE_LOG2_SIZE .. ';\n') out:write(' uint64 constant AR_CMIO_RX_BUFFER_START = 0x' .. hex(cartesi.AR_CMIO_RX_BUFFER_START) .. ';\n') diff --git a/helper_scripts/generate_UArchReset.sh b/helper_scripts/generate_UArchReset.sh index bf3f136..e3901b3 100755 --- a/helper_scripts/generate_UArchReset.sh +++ b/helper_scripts/generate_UArchReset.sh @@ -9,6 +9,7 @@ CPP_RESET_PATH=${EMULATOR_DIR}"/src/uarch-reset-state.cpp" TEMPLATE_FILE="./templates/UArchReset.sol.template" TARGET_FILE="src/UArchReset.sol" COMPAT_FILE="src/EmulatorCompat.sol" +CONSTANTS_FILE="src/EmulatorConstants.sol" KEYWORD_START="START OF AUTO-GENERATED CODE" KEYWORD_END="END OF AUTO-GENERATED CODE" @@ -16,6 +17,9 @@ KEYWORD_END="END OF AUTO-GENERATED CODE" COMPAT_FNS=`cat $COMPAT_FILE | grep -o "function [^(]*(" | $SED "s/function//g" | $SED "s/(//g"` COMPAT_FNS=`echo $COMPAT_FNS | $SED -E "s/( |\n)/|/g"` +# get constant names from EmulatorConstants.sol +CONSTANTS=`cat $CONSTANTS_FILE | grep -E -o 'constant\s+[^ ]*' | $SED -E "s/constant//g; s/ //g" | tr '\n' '|' | sed "s/.$//"` + # grab head and tail of the template start=`cat "$TEMPLATE_FILE" | grep "$KEYWORD_START" -n | grep -Eo "[0-9]*"` end=`cat "$TEMPLATE_FILE" | grep "$KEYWORD_END" -n | grep -Eo "[0-9]*"` @@ -34,6 +38,7 @@ cpp_src=`echo "${BASH_REMATCH[1]}" \ | $SED "/Explicit instantiatio/d" \ | $SED "/template/d" \ | $SED -E "s/($COMPAT_FNS)/EmulatorCompat.\1/g" \ + | $SED -E "s/($CONSTANTS)([^a-zA-Z])/EmulatorConstants.\1\2/g" \ | $SED "s/void uarch_reset_state(UarchState &a) {/function reset(AccessLogs.Context memory a) internal pure {/"` # compose the solidity file from all components diff --git a/src/AdvanceStatus.sol b/src/AdvanceStatus.sol deleted file mode 100644 index c297e1f..0000000 --- a/src/AdvanceStatus.sol +++ /dev/null @@ -1,76 +0,0 @@ -// Copyright Cartesi and individual authors (see AUTHORS) -// SPDX-License-Identifier: Apache-2.0 -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// - -/// @title AdvanceStatus -/// @notice Return advance status - -pragma solidity ^0.8.30; - -import "./EmulatorCompat.sol"; -import "./EmulatorConstants.sol"; - -library AdvanceStatus { - enum Status { - NOT_YIELDED, - ACCEPTED, - REJECTED, - EXCEPTION - } - - /* - typedef struct cmt_io_yield { - uint8_t dev; - uint8_t cmd; - uint16_t reason; - uint32_t data; - } cmt_io_yield_t; - */ - - error InvalidReason(uint16 reason); - - function advanceStatus(AccessLogs.Context memory a) - internal - pure - returns (Status) - { - if (!EmulatorCompat.readIflagsY(a)) { - return Status.NOT_YIELDED; - } - - // the following two approaches are equivalent: - // 1. swap the whole struct and then extract the reason - // 2. extract the reason from the struct and then swap the value - // EmulatorCompat.readWord already swaps the struct, so we can extract the reason directly - - uint64 tohost = - EmulatorCompat.readWord(a, EmulatorConstants.HTIF_TOHOST_ADDRESS); - uint16 reason = uint16(tohost >> 32); - - if (reason == EmulatorConstants.CMIO_YIELD_MANUAL_REASON_RX_ACCEPTED) { - return Status.ACCEPTED; - } else if ( - reason == EmulatorConstants.CMIO_YIELD_MANUAL_REASON_RX_REJECTED - ) { - return Status.REJECTED; - } else if ( - reason == EmulatorConstants.CMIO_YIELD_MANUAL_REASON_TX_EXCEPTION - ) { - return Status.EXCEPTION; - } else { - revert InvalidReason(reason); - } - } -} diff --git a/src/EmulatorCompat.sol b/src/EmulatorCompat.sol index a9583a6..90ba81e 100644 --- a/src/EmulatorCompat.sol +++ b/src/EmulatorCompat.sol @@ -23,6 +23,10 @@ library EmulatorCompat { using Buffer for Buffer.Context; using Memory for uint64; + function revertState(AccessLogs.Context memory a) internal pure { + a.currentRootHash = readRevertRootHash(a); + } + function readRevertRootHash(AccessLogs.Context memory a) internal pure diff --git a/src/EmulatorConstants.sol b/src/EmulatorConstants.sol index 300b292..e9b39a7 100644 --- a/src/EmulatorConstants.sol +++ b/src/EmulatorConstants.sol @@ -49,9 +49,17 @@ library EmulatorConstants { uint8 constant CMIO_YIELD_REASON_ADVANCE_STATE = 0x0; uint32 constant HASH_TREE_LOG2_WORD_SIZE = 0x5; uint32 constant HASH_TREE_WORD_SIZE = uint32(1) << HASH_TREE_LOG2_WORD_SIZE; - uint16 constant CMIO_YIELD_MANUAL_REASON_RX_ACCEPTED = 0x1; - uint16 constant CMIO_YIELD_MANUAL_REASON_RX_REJECTED = 0x2; - uint16 constant CMIO_YIELD_MANUAL_REASON_TX_EXCEPTION = 0x4; + uint32 constant HTIF_DEV_SHIFT = 0x38; + uint32 constant HTIF_CMD_SHIFT = 0x30; + uint32 constant HTIF_REASON_SHIFT = 0x20; + uint64 constant HTIF_DEV_MASK = 0xff00000000000000; + uint64 constant HTIF_CMD_MASK = 0xff000000000000; + uint64 constant HTIF_REASON_MASK = 0xffff00000000; + uint64 constant HTIF_DEV_YIELD = 0x2; + uint64 constant HTIF_YIELD_CMD_MANUAL = 0x1; + uint16 constant HTIF_YIELD_MANUAL_REASON_RX_ACCEPTED = 0x1; + uint16 constant HTIF_YIELD_MANUAL_REASON_RX_REJECTED = 0x2; + uint16 constant HTIF_YIELD_MANUAL_REASON_TX_EXCEPTION = 0x4; uint8 constant UARCH_STATE_LOG2_SIZE = 22; uint64 constant AR_CMIO_RX_BUFFER_START = 0x60000000; uint8 constant AR_CMIO_RX_BUFFER_LOG2_SIZE = 0x15; diff --git a/src/UArchReset.sol b/src/UArchReset.sol index 21bd8e7..bbfddc4 100644 --- a/src/UArchReset.sol +++ b/src/UArchReset.sol @@ -28,6 +28,35 @@ library UArchReset { function reset(AccessLogs.Context memory a) internal pure { EmulatorCompat.resetState(a); + // When the machine has rejected an input, the canonical state after the operation is + // the one recorded in the revert root hash (which has a pristine uarch) + uint64 iflagsY = + EmulatorCompat.readWord(a, EmulatorConstants.IFLAGS_Y_ADDRESS); + if (iflagsY != 0) { + uint64 tohost = EmulatorCompat.readWord( + a, EmulatorConstants.HTIF_TOHOST_ADDRESS + ); + uint64 dev = EmulatorCompat.uint64ShiftRight( + tohost & EmulatorConstants.HTIF_DEV_MASK, + EmulatorConstants.HTIF_DEV_SHIFT + ); + uint64 cmd = EmulatorCompat.uint64ShiftRight( + tohost & EmulatorConstants.HTIF_CMD_MASK, + EmulatorConstants.HTIF_CMD_SHIFT + ); + uint64 reason = EmulatorCompat.uint64ShiftRight( + tohost & EmulatorConstants.HTIF_REASON_MASK, + EmulatorConstants.HTIF_REASON_SHIFT + ); + if ( + dev == EmulatorConstants.HTIF_DEV_YIELD + && cmd == EmulatorConstants.HTIF_YIELD_CMD_MANUAL + && reason + == EmulatorConstants.HTIF_YIELD_MANUAL_REASON_RX_REJECTED + ) { + EmulatorCompat.revertState(a); + } + } } // END OF AUTO-GENERATED CODE diff --git a/templates/UArchReplay.t.sol.template b/templates/UArchReplay.t.sol.template index 028c354..ccf5fb2 100644 --- a/templates/UArchReplay.t.sol.template +++ b/templates/UArchReplay.t.sol.template @@ -122,6 +122,6 @@ contract UArchReplay_@X@_Test is AccessLogJsonParse { ); RawAccess[] memory rawAccesses = abi.decode(raw, (RawAccess[])); Buffer.Context memory buffer = Buffer.Context(data, 0); - _fillBufferFromRawAccesses(rawAccesses, buffer, siblingsLength); + _fillBufferFromRawAccesses(rawAccesses, buffer); } } diff --git a/test/AccessLogJsonParse.sol b/test/AccessLogJsonParse.sol index 5f1e68a..93e09f7 100644 --- a/test/AccessLogJsonParse.sol +++ b/test/AccessLogJsonParse.sol @@ -33,18 +33,24 @@ abstract contract AccessLogJsonParse is Test { function _fillBufferFromRawAccesses( RawAccess[] memory rawAccesses, - Buffer.Context memory buffer, - uint256 fixedSiblingsLength + Buffer.Context memory buffer ) internal pure { uint256 n = rawAccesses.length; for (uint256 i = 0; i < n; i++) { RawAccess memory a = rawAccesses[i]; if (a.log2_size == 3) { buffer.writeBytes32(_parseHex32FromLogString(a.read_value)); + } else if ( + keccak256(bytes(a.accessType)) == keccak256(bytes("read")) + ) { + // a leaf read carries the read value followed by the leaf hash + buffer.writeBytes32(_parseHex32FromLogString(a.read_value)); + buffer.writeBytes32(_parseHex32FromLogString(a.read_hash)); } else { buffer.writeBytes32(_parseHex32FromLogString(a.read_hash)); } - for (uint256 j = 0; j < fixedSiblingsLength; j++) { + uint256 siblingCount = a.sibling_hashes.length; + for (uint256 j = 0; j < siblingCount; j++) { buffer.writeBytes32( _parseHex32FromLogString(a.sibling_hashes[j]) ); diff --git a/test/SendCmioResponse.t.sol b/test/SendCmioResponse.t.sol index 562520e..e388453 100644 --- a/test/SendCmioResponse.t.sol +++ b/test/SendCmioResponse.t.sol @@ -145,6 +145,6 @@ contract SendCmioResponse_Test is AccessLogJsonParse { ); RawAccess[] memory rawAccesses = abi.decode(raw, (RawAccess[])); Buffer.Context memory buffer = Buffer.Context(data, 0); - _fillBufferFromRawAccesses(rawAccesses, buffer, siblingsLength); + _fillBufferFromRawAccesses(rawAccesses, buffer); } } diff --git a/test/UArchReset.t.sol b/test/UArchReset.t.sol index 49f9056..fbbcf38 100644 --- a/test/UArchReset.t.sol +++ b/test/UArchReset.t.sol @@ -33,7 +33,6 @@ contract UArchReset_Test is AccessLogJsonParse { // configure the tests string constant JSON_PATH = "./test/uarch-log/"; string constant CATALOG_PATH = "catalog.json"; - string constant RESET_PATH = "reset-uarch-steps.json"; uint256 constant siblingsLength = 42; @@ -51,7 +50,6 @@ contract UArchReset_Test is AccessLogJsonParse { function testReset() public { Entry[] memory catalog = loadCatalog(string.concat(JSON_PATH, CATALOG_PATH)); - string memory resetLog = string.concat(JSON_PATH, RESET_PATH); // all tests combined can easily run out of gas, stop metering // also raise memory_limit in foundry.toml per https://github.com/foundry-rs/foundry/issues/3971 @@ -60,15 +58,22 @@ contract UArchReset_Test is AccessLogJsonParse { bytes memory buffer = new bytes(100 * (siblingsLength + 1) * 32); for (uint256 i = 0; i < catalog.length; i++) { + // run the plain reset log and the rejected-input log, whose final + // root hash is the recorded revert root hash if ( keccak256(abi.encodePacked(catalog[i].logFilename)) - != keccak256(abi.encodePacked("reset-uarch-steps.json")) + != keccak256(abi.encodePacked("reset-uarch-steps.json")) + && keccak256(abi.encodePacked(catalog[i].logFilename)) + != keccak256( + abi.encodePacked("reset-uarch-rejected-steps.json") + ) ) { continue; } console.log("Replaying log file %s ...", catalog[i].logFilename); - string memory rj = loadJsonLog(resetLog); + string memory rj = + loadJsonLog(string.concat(JSON_PATH, catalog[i].logFilename)); bytes32 initialRootHash = vm.parseBytes32( string.concat("0x", catalog[i].initialRootHash) @@ -123,24 +128,7 @@ contract UArchReset_Test is AccessLogJsonParse { rawJson, ".accesses", RAW_ACCESS_TYPE_DESCRIPTION ); RawAccess[] memory rawAccesses = abi.decode(raw, (RawAccess[])); - assertEq(rawAccesses.length, 1, "should be only 1 access in reset"); - - RawAccess memory a = rawAccesses[0]; - if (keccak256(bytes(a.accessType)) == keccak256(bytes("read"))) { - revert("should'nt have read access in reset"); - } - assertEq( - a.accessAddress, - EmulatorConstants.UARCH_STATE_START_ADDRESS, - "position should be (0x400000)" - ); - assertEq( - a.log2_size, - EmulatorConstants.UARCH_STATE_LOG2_SIZE, - "log2Size should be 22" - ); - Buffer.Context memory buffer = Buffer.Context(data, 0); - _fillBufferFromRawAccesses(rawAccesses, buffer, siblingsLength); + _fillBufferFromRawAccesses(rawAccesses, buffer); } } From 331e86e25de449662dae43eac52ddf058591c4f9 Mon Sep 17 00:00:00 2001 From: Diego Nehab <1635557+diegonehab@users.noreply.github.com> Date: Wed, 10 Jun 2026 22:22:07 +0100 Subject: [PATCH 4/7] chore: update emulator tag --- Makefile | 4 ++-- shasum-download | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Makefile b/Makefile index 65c4b78..3310b5e 100644 --- a/Makefile +++ b/Makefile @@ -3,8 +3,8 @@ TEST_DIR := test DOWNLOADDIR := downloads SRC_DIR := src -EMULATOR_VERSION ?= v0.20.0 -EMULATOR_TAG ?= +EMULATOR_VERSION ?= v0.21.0 +EMULATOR_TAG ?= -test1 SOLIDITY_VERSION ?= 0.8.30 diff --git a/shasum-download b/shasum-download index 22fb8d4..ca0e1c0 100644 --- a/shasum-download +++ b/shasum-download @@ -1,2 +1,2 @@ -fef8844a306d83eef9c30828986645f2b0ff149654490b0f7d47ec2883bcb693 downloads/machine-emulator-tests-data.deb -bc7a0cc14724167c2826967eaf62feb6a2c902b2b45a73f3c684e1c5ceaa8f69 downloads/uarch-riscv-tests-json-logs.tar.gz +b52a2b54cf18b1215108281ed5c6df93ed4ba3922a09f4155d9f945e703f9fd7 downloads/machine-emulator-tests-data.deb +5bfb82f7a6665fe264d7d849d8b90f7b476de5f17d01a2743c15952bf8de0069 downloads/uarch-riscv-tests-json-logs.tar.gz From e60f6eb16c37b19ffe31347ef6c0f1671ecdb3ca Mon Sep 17 00:00:00 2001 From: Diego Nehab <1635557+diegonehab@users.noreply.github.com> Date: Fri, 12 Jun 2026 23:22:16 +0100 Subject: [PATCH 5/7] feat: turn send_cmio_response failures into no-ops in log and verify The core send_cmio_response can no longer fail. If verification could fail for a reachable machine state and response, there would be states for which the honest party cannot produce a log that proves the resulting state transition. The iflags.Y and response-length checks now return without touching the state, like the advance-state yield check already did. The length check also moves ahead of the revert root hash write, where it can still prevent all state changes. iflags.Y remains the first access, so a no-op log is never empty, which replay requires. The host-facing machine::send_cmio_response still refuses misuse upfront. check_pending_cmio_request takes over the dropped checks, requiring a manual yield for every response reason and a response that fits in the rx buffer, with the same error messages as before. machine::log_send_cmio_response no longer fails for any machine state or response argument and logs a no-op instead. --- Makefile | 2 +- helper_scripts/generate_EmulatorConstants.lua | 2 +- shasum-download | 4 +- src/EmulatorCompat.sol | 32 ++++++++ src/EmulatorConstants.sol | 2 +- src/SendCmioResponse.sol | 31 ++++++-- src/UArchReset.sol | 22 +----- test/SendCmioResponse.t.sol | 77 +++++++++++++++++++ 8 files changed, 142 insertions(+), 30 deletions(-) diff --git a/Makefile b/Makefile index 3310b5e..955e985 100644 --- a/Makefile +++ b/Makefile @@ -4,7 +4,7 @@ DOWNLOADDIR := downloads SRC_DIR := src EMULATOR_VERSION ?= v0.21.0 -EMULATOR_TAG ?= -test1 +EMULATOR_TAG ?= -test2 SOLIDITY_VERSION ?= 0.8.30 diff --git a/helper_scripts/generate_EmulatorConstants.lua b/helper_scripts/generate_EmulatorConstants.lua index 55e51c2..f932b82 100755 --- a/helper_scripts/generate_EmulatorConstants.lua +++ b/helper_scripts/generate_EmulatorConstants.lua @@ -39,7 +39,7 @@ out:write(' uint64 constant HTIF_FROMHOST_ADDRESS = 0x' .. hex(cartesi.machine:get_reg_address("htif_fromhost")) .. ';\n') out:write(' uint64 constant HTIF_TOHOST_ADDRESS = 0x' .. hex(cartesi.machine:get_reg_address("htif_tohost")) .. ';\n') -out:write(' uint8 constant CMIO_YIELD_REASON_ADVANCE_STATE = 0x' .. +out:write(' uint8 constant HTIF_YIELD_REASON_ADVANCE_STATE = 0x' .. hex(cartesi.HTIF_YIELD_REASON_ADVANCE_STATE) .. ';\n') out:write(' uint32 constant HASH_TREE_LOG2_WORD_SIZE = 0x' .. hex(cartesi.HASH_TREE_LOG2_WORD_SIZE) .. ';\n') out:write(' uint32 constant HASH_TREE_WORD_SIZE = uint32(1) << HASH_TREE_LOG2_WORD_SIZE;\n') diff --git a/shasum-download b/shasum-download index ca0e1c0..ccd3368 100644 --- a/shasum-download +++ b/shasum-download @@ -1,2 +1,2 @@ -b52a2b54cf18b1215108281ed5c6df93ed4ba3922a09f4155d9f945e703f9fd7 downloads/machine-emulator-tests-data.deb -5bfb82f7a6665fe264d7d849d8b90f7b476de5f17d01a2743c15952bf8de0069 downloads/uarch-riscv-tests-json-logs.tar.gz +7ae0df341e8d14b08461748a9f2f639d1cc23bf6c7f1a1aa31185503fef56230 downloads/machine-emulator-tests-data.deb +b828dc0be96c3f46d61fd973e730a13e44afaca0a95670f7c56371cfc784d705 downloads/uarch-riscv-tests-json-logs.tar.gz diff --git a/src/EmulatorCompat.sol b/src/EmulatorCompat.sol index 90ba81e..f8c1c9f 100644 --- a/src/EmulatorCompat.sol +++ b/src/EmulatorCompat.sol @@ -187,6 +187,16 @@ library EmulatorCompat { ); } + function readHtifTohost(AccessLogs.Context memory a) + internal + pure + returns (uint64) + { + return a.readWord( + EmulatorConstants.HTIF_TOHOST_ADDRESS.toPhysicalAddress() + ); + } + // Conversions and arithmetic functions function int8ToUint64(int8 val) internal pure returns (uint64) { @@ -380,4 +390,26 @@ library EmulatorCompat { return n; } + + function isYieldedManualWith(uint64 tohost, uint64 yieldReason) + internal + pure + returns (bool) + { + uint64 dev = uint64ShiftRight( + tohost & EmulatorConstants.HTIF_DEV_MASK, + EmulatorConstants.HTIF_DEV_SHIFT + ); + uint64 cmd = uint64ShiftRight( + tohost & EmulatorConstants.HTIF_CMD_MASK, + EmulatorConstants.HTIF_CMD_SHIFT + ); + uint64 reason = uint64ShiftRight( + tohost & EmulatorConstants.HTIF_REASON_MASK, + EmulatorConstants.HTIF_REASON_SHIFT + ); + return dev == EmulatorConstants.HTIF_DEV_YIELD + && cmd == EmulatorConstants.HTIF_YIELD_CMD_MANUAL + && reason == yieldReason; + } } diff --git a/src/EmulatorConstants.sol b/src/EmulatorConstants.sol index e9b39a7..ce3b614 100644 --- a/src/EmulatorConstants.sol +++ b/src/EmulatorConstants.sol @@ -46,7 +46,7 @@ library EmulatorConstants { uint64 constant IFLAGS_Y_ADDRESS = 0x300; uint64 constant HTIF_FROMHOST_ADDRESS = 0x330; uint64 constant HTIF_TOHOST_ADDRESS = 0x328; - uint8 constant CMIO_YIELD_REASON_ADVANCE_STATE = 0x0; + uint8 constant HTIF_YIELD_REASON_ADVANCE_STATE = 0x0; uint32 constant HASH_TREE_LOG2_WORD_SIZE = 0x5; uint32 constant HASH_TREE_WORD_SIZE = uint32(1) << HASH_TREE_LOG2_WORD_SIZE; uint32 constant HTIF_DEV_SHIFT = 0x38; diff --git a/src/SendCmioResponse.sol b/src/SendCmioResponse.sol index 9d86deb..4bcb0e7 100644 --- a/src/SendCmioResponse.sol +++ b/src/SendCmioResponse.sol @@ -36,15 +36,29 @@ library SendCmioResponse { bytes32 dataHash, uint32 dataLength ) internal pure { + // This function cannot fail. When a failure is detected, the operation is a no-op instead, + // so the honest party can always log and prove the resulting state transition. + // A response to a machine that is not waiting on a manual yield is a no-op. if (!EmulatorCompat.readIflagsY(a)) { - EmulatorCompat.throwRuntimeError(a, "iflags.Y is not set"); + return; + } + if (reason == EmulatorConstants.HTIF_YIELD_REASON_ADVANCE_STATE) { + // Advance-state responses are the input boundary of the rollups flow. They only apply to a + // machine waiting for an input on an rx-accepted manual yield. Sending one to a machine that + // yielded manual with any other reason (e.g., rejected an input or threw an exception) is a no-op. + uint64 tohost = EmulatorCompat.readHtifTohost(a); + if (!EmulatorCompat.isYieldedManualWith( + tohost, + EmulatorConstants.HTIF_YIELD_MANUAL_REASON_RX_ACCEPTED + )) { + return; + } } - // Record the machine root hash to revert to in case the response is eventually rejected - EmulatorCompat.writeRevertRootHash(a, revertRootHash); // A zero length data is a valid response. We just skip writing to the rx buffer. + uint32 writeLengthLog2Size = 0; if (dataLength > 0) { // Find the write length: the smallest power of 2 that is >= dataLength and >= tree leaf size - uint32 writeLengthLog2Size = EmulatorCompat.uint32Log2(dataLength); + writeLengthLog2Size = EmulatorCompat.uint32Log2(dataLength); if ( writeLengthLog2Size < EmulatorConstants.HASH_TREE_LOG2_WORD_SIZE ) { @@ -56,14 +70,17 @@ library SendCmioResponse { ) { writeLengthLog2Size += 1; } + // A response with data that does not fit in the rx buffer is a no-op if ( writeLengthLog2Size > EmulatorConstants.AR_CMIO_RX_BUFFER_LOG2_SIZE ) { - EmulatorCompat.throwRuntimeError( - a, "CMIO response data is too large" - ); + return; } + } + // Record the machine root hash to revert to in case the response is eventually rejected + EmulatorCompat.writeRevertRootHash(a, revertRootHash); + if (dataLength > 0) { a.writeRegion( Memory.regionFromPhysicalAddress( EmulatorConstants.AR_CMIO_RX_BUFFER_START diff --git a/src/UArchReset.sol b/src/UArchReset.sol index bbfddc4..33bb070 100644 --- a/src/UArchReset.sol +++ b/src/UArchReset.sol @@ -36,24 +36,10 @@ library UArchReset { uint64 tohost = EmulatorCompat.readWord( a, EmulatorConstants.HTIF_TOHOST_ADDRESS ); - uint64 dev = EmulatorCompat.uint64ShiftRight( - tohost & EmulatorConstants.HTIF_DEV_MASK, - EmulatorConstants.HTIF_DEV_SHIFT - ); - uint64 cmd = EmulatorCompat.uint64ShiftRight( - tohost & EmulatorConstants.HTIF_CMD_MASK, - EmulatorConstants.HTIF_CMD_SHIFT - ); - uint64 reason = EmulatorCompat.uint64ShiftRight( - tohost & EmulatorConstants.HTIF_REASON_MASK, - EmulatorConstants.HTIF_REASON_SHIFT - ); - if ( - dev == EmulatorConstants.HTIF_DEV_YIELD - && cmd == EmulatorConstants.HTIF_YIELD_CMD_MANUAL - && reason - == EmulatorConstants.HTIF_YIELD_MANUAL_REASON_RX_REJECTED - ) { + if (EmulatorCompat.isYieldedManualWith( + tohost, + EmulatorConstants.HTIF_YIELD_MANUAL_REASON_RX_REJECTED + )) { EmulatorCompat.revertState(a); } } diff --git a/test/SendCmioResponse.t.sol b/test/SendCmioResponse.t.sol index e388453..1b4086c 100644 --- a/test/SendCmioResponse.t.sol +++ b/test/SendCmioResponse.t.sol @@ -34,6 +34,8 @@ contract SendCmioResponse_Test is AccessLogJsonParse { string constant JSON_PATH = "./test/uarch-log/"; string constant CATALOG_PATH = "catalog.json"; string constant SEND_CMIO_RESPONSE_PATH = "send-cmio-response-steps.json"; + string constant SEND_CMIO_RESPONSE_NOOP_PATH = + "send-cmio-response-noop-steps.json"; uint256 constant siblingsLength = 59; @@ -115,6 +117,81 @@ contract SendCmioResponse_Test is AccessLogJsonParse { } } + function testSendCmioResponseNoop() public { + Entry[] memory catalog = + loadCatalog(string.concat(JSON_PATH, CATALOG_PATH)); + string memory noopLog = + string.concat(JSON_PATH, SEND_CMIO_RESPONSE_NOOP_PATH); + + // all tests combined can easily run out of gas, stop metering + // also raise memory_limit in foundry.toml per https://github.com/foundry-rs/foundry/issues/3971 + vm.pauseGasMetering(); + // create a large buffer and reuse it + bytes memory buffer = new bytes(100 * (siblingsLength + 1) * 32); + bool found = false; + + for (uint256 i = 0; i < catalog.length; i++) { + if ( + keccak256(abi.encodePacked(catalog[i].logFilename)) + != keccak256( + abi.encodePacked("send-cmio-response-noop-steps.json") + ) + ) { + continue; + } + found = true; + console.log("Replaying log file %s ...", catalog[i].logFilename); + + string memory rj = loadJsonLog(noopLog); + + bytes32 initialRootHash = vm.parseBytes32( + string.concat("0x", catalog[i].initialRootHash) + ); + bytes32 finalRootHash = + vm.parseBytes32(string.concat("0x", catalog[i].finalRootHash)); + // the log was taken from a machine that yielded manual with reason + // rx-rejected, so the advance-state response must be a no-op + assertEq( + initialRootHash, + finalRootHash, + "initial and final root hashes must match" + ); + + loadBufferFromRawJson(buffer, rj); + + AccessLogs.Context memory accessLogs = + AccessLogs.Context(initialRootHash, Buffer.Context(buffer, 0)); + + // Prepare arguments for sendCmioResponse + // These values are hard-coded in order to match the values used when generating the test log file + uint16 reason = EmulatorConstants.HTIF_YIELD_REASON_ADVANCE_STATE; + bytes memory response = bytes("This is a test cmio response"); + bytes memory paddedResponse = new bytes(32); + for (uint256 j = 0; j < response.length; j++) { + paddedResponse[j] = response[j]; + } + bytes32 paddedResponseHash = keccak256(paddedResponse); + // call sendCmioResponse + // the test log file was generated passing the initial root hash as the revert root hash + SendCmioResponse.sendCmioResponse( + accessLogs, + initialRootHash, + reason, + paddedResponseHash, + uint32(response.length) + ); + // ensure that the final root hash matches the expected value + assertEq( + accessLogs.currentRootHash, + finalRootHash, + "final root hash must match" + ); + } + assertTrue( + found, "catalog has no send-cmio-response-noop-steps.json entry" + ); + } + function loadCatalog(string memory path) private view From f12646b82942ef2bd77fd050f5fcf8c314fcc9db Mon Sep 17 00:00:00 2001 From: Diego Nehab <1635557+diegonehab@users.noreply.github.com> Date: Mon, 15 Jun 2026 14:02:59 +0100 Subject: [PATCH 6/7] test: cover accepted-input path in UArchReset replay The UArchReset.sol reset() generated in PR 98 now reads iflags.Y, conditionally reads htif.tohost, decodes the manual-yield reason, and reverts only when the reason is RX_REJECTED. The replay test previously exercised only the plain path (iflags.Y unset, no tohost read) and the rejected path (RX_REJECTED, reverts). This adds reset-uarch-accepted-steps.json to the filter so the third path is also replayed: iflags.Y is set, tohost is read and decoded, the reason is RX_ACCEPTED, reset does not revert, and the final root hash is the post-reset root. This addresses the review comment from GCdePaula requesting this missing scenario. --- shasum-download | 4 ++-- test/UArchReset.t.sol | 20 ++++++++++++++++---- 2 files changed, 18 insertions(+), 6 deletions(-) diff --git a/shasum-download b/shasum-download index ccd3368..6bc3a76 100644 --- a/shasum-download +++ b/shasum-download @@ -1,2 +1,2 @@ -7ae0df341e8d14b08461748a9f2f639d1cc23bf6c7f1a1aa31185503fef56230 downloads/machine-emulator-tests-data.deb -b828dc0be96c3f46d61fd973e730a13e44afaca0a95670f7c56371cfc784d705 downloads/uarch-riscv-tests-json-logs.tar.gz +fd4c5d56f5515626762602991d7e8de09f85958ac2ece813e6b9bea148dea016 downloads/machine-emulator-tests-data.deb +96843bf71e5a8cd1628ef6eb43c793f046ed643fef3aebbed545d69e4dcf606e downloads/uarch-riscv-tests-json-logs.tar.gz diff --git a/test/UArchReset.t.sol b/test/UArchReset.t.sol index fbbcf38..b05356f 100644 --- a/test/UArchReset.t.sol +++ b/test/UArchReset.t.sol @@ -56,10 +56,14 @@ contract UArchReset_Test is AccessLogJsonParse { vm.pauseGasMetering(); // create a large buffer and reuse it bytes memory buffer = new bytes(100 * (siblingsLength + 1) * 32); + // count the fixtures replayed below, so a missing entry fails loudly + uint256 found = 0; for (uint256 i = 0; i < catalog.length; i++) { - // run the plain reset log and the rejected-input log, whose final - // root hash is the recorded revert root hash + // run the plain reset log, the rejected-input log (whose final + // root hash is the recorded revert root hash), and the + // accepted-input log (which reads the manual yield but does not + // revert, so its final root hash is the post-reset root) if ( keccak256(abi.encodePacked(catalog[i].logFilename)) != keccak256(abi.encodePacked("reset-uarch-steps.json")) @@ -67,9 +71,14 @@ contract UArchReset_Test is AccessLogJsonParse { != keccak256( abi.encodePacked("reset-uarch-rejected-steps.json") ) + && keccak256(abi.encodePacked(catalog[i].logFilename)) + != keccak256( + abi.encodePacked("reset-uarch-accepted-steps.json") + ) ) { continue; } + found++; console.log("Replaying log file %s ...", catalog[i].logFilename); string memory rj = @@ -95,8 +104,11 @@ contract UArchReset_Test is AccessLogJsonParse { "final root hash must match" ); } - - // load json log + assertEq( + found, + 3, + "catalog is missing one of the reset-uarch-{steps,rejected-steps,accepted-steps}.json entries" + ); } function loadCatalog(string memory path) From 260f837799c673ac01b6e13be7220b62ca5ccf1c Mon Sep 17 00:00:00 2001 From: Diego Nehab <1635557+diegonehab@users.noreply.github.com> Date: Thu, 25 Jun 2026 20:21:34 +0100 Subject: [PATCH 7/7] refactor: remove mark_dirty_page ECALL from the generated step The emulator dropped the mark_dirty_page ECALL (function code 3) from the microarchitecture, so the generated step code must follow. Regenerate UArchStep.sol, which removes the fn=3 dispatch branch, and regenerate EmulatorConstants.sol, which removes the UARCH_ECALL_FN_MARK_DIRTY_PAGE constant. Drop markDirtyPageECALL from EmulatorCompat.sol by hand, since that file is a generator input rather than an output. Fix the generators so they run against the current emulator. The step generator and its Makefile prerequisite now read uarch-step.hpp, which is where the step enum moved after uarch-step.h was retired. The constants generator no longer emits the removed ECALL constant. Add testRemovedMarkDirtyPageEcall, which loads the binary that issues fn=3 and asserts it now traps as an unsupported ecall. --- Makefile | 4 ++-- helper_scripts/generate_EmulatorConstants.lua | 1 - helper_scripts/generate_UArchStep.sh | 2 +- shasum-download | 4 ++-- src/EmulatorCompat.sol | 6 ------ src/EmulatorConstants.sol | 3 +-- src/UArchStep.sol | 6 ------ test/UArchInterpret.t.sol | 21 +++++++++++++++++++ 8 files changed, 27 insertions(+), 20 deletions(-) diff --git a/Makefile b/Makefile index 955e985..ef10306 100644 --- a/Makefile +++ b/Makefile @@ -4,7 +4,7 @@ DOWNLOADDIR := downloads SRC_DIR := src EMULATOR_VERSION ?= v0.21.0 -EMULATOR_TAG ?= -test2 +EMULATOR_TAG ?= -test3 SOLIDITY_VERSION ?= 0.8.30 @@ -113,7 +113,7 @@ generate-replay: generate-constants: $(EMULATOR_DIR) EMULATOR_DIR=$(EMULATOR_DIR) ./helper_scripts/generate_EmulatorConstants.sh -generate-step: $(EMULATOR_DIR)/src/uarch-step.h $(EMULATOR_DIR)/src/uarch-step.cpp +generate-step: $(EMULATOR_DIR)/src/uarch-step.hpp $(EMULATOR_DIR)/src/uarch-step.cpp EMULATOR_DIR=$(EMULATOR_DIR) ./helper_scripts/generate_UArchStep.sh generate-reset: $(EMULATOR_DIR)/src/uarch-reset-state.cpp diff --git a/helper_scripts/generate_EmulatorConstants.lua b/helper_scripts/generate_EmulatorConstants.lua index f932b82..2b90f7e 100755 --- a/helper_scripts/generate_EmulatorConstants.lua +++ b/helper_scripts/generate_EmulatorConstants.lua @@ -31,7 +31,6 @@ out:write(' uint64 constant UARCH_RAM_LENGTH = 0x' .. hex(cartesi.UARCH_RAM_L out:write(' uint64 constant UARCH_STATE_START_ADDRESS = 0x' .. hex(cartesi.UARCH_STATE_START_ADDRESS) .. ';\n') out:write(' uint64 constant UARCH_ECALL_FN_HALT = ' .. cartesi.UARCH_ECALL_FN_HALT .. ';\n') out:write(' uint64 constant UARCH_ECALL_FN_PUTCHAR = ' .. cartesi.UARCH_ECALL_FN_PUTCHAR .. ';\n') -out:write(' uint64 constant UARCH_ECALL_FN_MARK_DIRTY_PAGE = ' .. cartesi.UARCH_ECALL_FN_MARK_DIRTY_PAGE .. ';\n') out:write(' uint64 constant UARCH_ECALL_FN_WRITE_TLB = ' .. cartesi.UARCH_ECALL_FN_WRITE_TLB .. ';\n') out:write(' uint64 constant HTIF_YIELD = 0x' .. hex(cartesi.machine:get_reg_address("htif_iyield")) .. ';\n') out:write(' uint64 constant IFLAGS_Y_ADDRESS = 0x' .. hex(cartesi.machine:get_reg_address("iflags_Y")) .. ';\n') diff --git a/helper_scripts/generate_UArchStep.sh b/helper_scripts/generate_UArchStep.sh index fa1ebf2..11a18d8 100755 --- a/helper_scripts/generate_UArchStep.sh +++ b/helper_scripts/generate_UArchStep.sh @@ -5,7 +5,7 @@ set -e SED=${SED:-"sed"} EMULATOR_DIR=${EMULATOR_DIR:-"../emulator"} CPP_STEP_PATH=${EMULATOR_DIR}"/src/uarch-step.cpp" -CPP_STEP_H_PATH=${EMULATOR_DIR}"/src/uarch-step.h" +CPP_STEP_H_PATH=${EMULATOR_DIR}"/src/uarch-step.hpp" TEMPLATE_FILE="./templates/UArchStep.sol.template" TARGET_FILE="src/UArchStep.sol" diff --git a/shasum-download b/shasum-download index 6bc3a76..8777e7b 100644 --- a/shasum-download +++ b/shasum-download @@ -1,2 +1,2 @@ -fd4c5d56f5515626762602991d7e8de09f85958ac2ece813e6b9bea148dea016 downloads/machine-emulator-tests-data.deb -96843bf71e5a8cd1628ef6eb43c793f046ed643fef3aebbed545d69e4dcf606e downloads/uarch-riscv-tests-json-logs.tar.gz +28fd67825be2bf53188326065a06f9a5019f7bd07426d21f6053f34e8652fb3e downloads/machine-emulator-tests-data.deb +f7d22eacba7c9df62fa6dfa63fe75bdc387f6c7a98f20606bcfd0b01a851830c downloads/uarch-riscv-tests-json-logs.tar.gz diff --git a/src/EmulatorCompat.sol b/src/EmulatorCompat.sol index f8c1c9f..db3f0a5 100644 --- a/src/EmulatorCompat.sol +++ b/src/EmulatorCompat.sol @@ -328,12 +328,6 @@ library EmulatorCompat { function putCharECALL(AccessLogs.Context memory a, uint8 c) internal pure {} - function markDirtyPageECALL( - AccessLogs.Context memory a, - uint64 paddr, - uint64 pma_index - ) internal pure {} - function writeTlbECALL( AccessLogs.Context memory a, uint64 setIndex, diff --git a/src/EmulatorConstants.sol b/src/EmulatorConstants.sol index ce3b614..47f1993 100644 --- a/src/EmulatorConstants.sol +++ b/src/EmulatorConstants.sol @@ -25,7 +25,7 @@ library EmulatorConstants { // START OF AUTO-GENERATED CODE bytes32 constant UARCH_PRISTINE_STATE_HASH = - 0x245a715ed3343e88ea5b2810584faea872991cca0200979e8f43056cd2db59c6; + 0x42e6d54b3b07b4a1c5f2ed13aae7c8d924269c32728a5b66f54b70358b4d99e7; uint64 constant UARCH_CYCLE_ADDRESS = 0x400008; uint64 constant UARCH_CYCLE_MAX = 0x100000; uint64 constant UARCH_HALT_FLAG_ADDRESS = 0x400000; @@ -40,7 +40,6 @@ library EmulatorConstants { uint64 constant UARCH_STATE_START_ADDRESS = 0x400000; uint64 constant UARCH_ECALL_FN_HALT = 1; uint64 constant UARCH_ECALL_FN_PUTCHAR = 2; - uint64 constant UARCH_ECALL_FN_MARK_DIRTY_PAGE = 3; uint64 constant UARCH_ECALL_FN_WRITE_TLB = 4; uint64 constant HTIF_YIELD = 0x348; uint64 constant IFLAGS_Y_ADDRESS = 0x300; diff --git a/src/UArchStep.sol b/src/UArchStep.sol index c3c23b3..65e3146 100644 --- a/src/UArchStep.sol +++ b/src/UArchStep.sol @@ -1079,12 +1079,6 @@ library UArchStep { EmulatorCompat.putCharECALL(a, uint8(c)); // Can be a NOOP in Solidity return advancePc(a, pc); } - if (fn == EmulatorConstants.UARCH_ECALL_FN_MARK_DIRTY_PAGE) { - uint64 paddr = EmulatorCompat.readX(a, 10); // a0 contains physical address in page to be marked dirty - uint64 pma_index = EmulatorCompat.readX(a, 11); // a1 contains a index of PMA where page falls - EmulatorCompat.markDirtyPageECALL(a, paddr, pma_index); // This MUST be be a NOOP in Solidity - return advancePc(a, pc); - } if (fn == EmulatorConstants.UARCH_ECALL_FN_WRITE_TLB) { uint64 set_index = EmulatorCompat.readX(a, 10); // a0 contains TLB set (code, read, write) uint64 slot_index = EmulatorCompat.readX(a, 11); // a1 contains slot_index to modify diff --git a/test/UArchInterpret.t.sol b/test/UArchInterpret.t.sol index 8847e9c..5f3d243 100644 --- a/test/UArchInterpret.t.sol +++ b/test/UArchInterpret.t.sol @@ -141,6 +141,27 @@ contract UArchInterpretTest is Test { ExternalUArchInterpret.interpret(a); } + function testRemovedMarkDirtyPageEcall() public { + // function code 3 was the mark_dirty_page ecall and has been removed, so the + // binary that issues it must now revert as an unsupported ecall + AccessLogs.Context memory a = newAccessLogsContext(); + + loadBin( + a.buffer, + string.concat( + BINARIES_PATH, "rv64ui-uarch-ecall-removed-mark-page-dirty.bin" + ) + ); + + // init pc to ram start + EmulatorCompat.writePc(a, EmulatorConstants.UARCH_RAM_START_ADDRESS); + // init cycle to 0 + EmulatorCompat.writeCycle(a, 0); + + vm.expectRevert("unsupported ecall function"); + ExternalUArchInterpret.interpret(a); + } + function loadBin(Buffer.Context memory buffer, string memory path) private view