From 80e8942930406adc51fdd19433e3dcd590c08b0f Mon Sep 17 00:00:00 2001 From: Marcos Pernambuco Motta <1091485+mpernambuco@users.noreply.github.com> Date: Tue, 17 Dec 2024 08:28:02 -0300 Subject: [PATCH] feat!: add log_step and verify_step --- .github/workflows/build.yml | 3 + CHANGELOG.md | 3 + src/Makefile | 3 +- src/cartesi-machine.lua | 20 + src/clua-i-virtual-machine.cpp | 27 + src/i-virtual-machine.h | 14 + src/interpret.cpp | 6 + src/jsonrpc-discover.json | 74 ++ src/jsonrpc-remote-machine.cpp | 30 + src/jsonrpc-virtual-machine.cpp | 16 + src/jsonrpc-virtual-machine.h | 3 + src/machine-c-api.cpp | 33 + src/machine-c-api.h | 19 + src/machine-merkle-tree.cpp | 27 + src/machine-merkle-tree.h | 6 + src/machine.cpp | 47 + src/machine.h | 26 + src/os.cpp | 19 + src/os.h | 6 + src/record-step-state-access.h | 839 +++++++++++++++++ src/replay-step-state-access-interop.cpp | 42 + src/replay-step-state-access-interop.h | 40 + src/replay-step-state-access.h | 1099 ++++++++++++++++++++++ src/virtual-machine.cpp | 9 + src/virtual-machine.h | 3 + tests/Makefile | 5 +- tests/lua/cartesi-machine-tests.lua | 37 + tests/lua/machine-bind.lua | 206 +++- 28 files changed, 2658 insertions(+), 4 deletions(-) create mode 100644 src/record-step-state-access.h create mode 100644 src/replay-step-state-access-interop.cpp create mode 100644 src/replay-step-state-access-interop.h create mode 100644 src/replay-step-state-access.h diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 5e33b9327..252b475a0 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -191,6 +191,9 @@ jobs: - name: Run test suite inside the docker image run: docker run --rm -t ${{ github.repository_owner }}/machine-emulator:tests /usr/bin/cartesi-machine-tests run + - name: Run test suite with log_step inside the docker image + run: docker run --rm -t ${{ github.repository_owner }}/machine-emulator:tests /usr/bin/cartesi-machine-tests run_step + - name: Save and Load run: | docker run --rm -t ${{ github.repository_owner }}/machine-emulator:tests /usr/share/cartesi-machine/tests/scripts/test-save-and-load.sh diff --git a/CHANGELOG.md b/CHANGELOG.md index 778227c19..d109b477c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] ### Added +- Added the "log_step" method +- Added the "verify_step" method +- Added the "--log-step" option to "cartesi-machine.lua" ### Changed - Added a "--jobs" option to "uarch-riscv-tests.lua" test diff --git a/src/Makefile b/src/Makefile index 8834659b4..5f66b62d7 100644 --- a/src/Makefile +++ b/src/Makefile @@ -374,7 +374,8 @@ LIBCARTESI_OBJS:= \ uarch-pristine-ram.o \ uarch-pristine-state-hash.o \ uarch-pristine-hash.o \ - send-cmio-response.o + send-cmio-response.o \ + replay-step-state-access-interop.o CARTESI_CLUA_OBJS:= \ clua.o \ diff --git a/src/cartesi-machine.lua b/src/cartesi-machine.lua index 6eeb8123e..c1b9b9f39 100755 --- a/src/cartesi-machine.lua +++ b/src/cartesi-machine.lua @@ -441,6 +441,9 @@ where options are: this option implies --initial-hash and --final-hash. (default: none) + --log-step=, + log and save a step of mcycles to . + --log-step-uarch advance one micro step and print access log. @@ -622,6 +625,8 @@ local load_json_config = false local gdb_address local exec_arguments = {} local assert_rolling_template = false +local log_step_mcycle_count +local log_step_filename local function parse_memory_range(opts, what, all) local f = util.parse_options(opts, { @@ -1251,6 +1256,15 @@ local options = { return true end, }, + { + "^%-%-log%-step%=(.*),(.*)$", + function(count, filename) + if (not count) or not filename then return false end + log_step_mcycle_count = assert(util.parse_number(count), "invalid steps " .. count) + log_step_filename = filename + return true + end, + }, { "^%-%-log%-step%-uarch$", function(all) @@ -2185,6 +2199,12 @@ if max_uarch_cycle > 0 then end end if gdb_stub then gdb_stub:close() end +if log_step_mcycle_count then + stderr(string.format("Logging step of %d cycles to %s\n", log_step_mcycle_count, log_step_filename)) + print_root_hash(machine, stderr_unsilenceable) + machine:log_step(log_step_mcycle_count, log_step_filename) + print_root_hash(machine, stderr_unsilenceable) +end if log_step_uarch then assert(config.processor.iunrep == 0, "micro step proof is meaningless in unreproducible mode") stderr("Gathering micro step log: please wait\n") diff --git a/src/clua-i-virtual-machine.cpp b/src/clua-i-virtual-machine.cpp index d74849e5a..708c6372f 100644 --- a/src/clua-i-virtual-machine.cpp +++ b/src/clua-i-virtual-machine.cpp @@ -733,6 +733,16 @@ static int machine_obj_index_run(lua_State *L) { return 1; } +static int machine_obj_index_log_step(lua_State *L) { + auto &m = clua_check>(L, 1); + cm_break_reason break_reason = CM_BREAK_REASON_FAILED; + if (cm_log_step(m.get(), luaL_checkinteger(L, 2), luaL_checkstring(L, 3), &break_reason) != 0) { + return luaL_error(L, "%s", cm_get_last_error_message()); + } + lua_pushinteger(L, static_cast(break_reason)); + return 1; +} + /// \brief This is the machine:read_uarch_halt_flag() method implementation. /// \param L Lua state. static int machine_obj_index_read_uarch_halt_flag(lua_State *L) { @@ -1048,6 +1058,21 @@ static int machine_obj_index_get_reg_address(lua_State *L) { return 1; } +/// \brief This is the machine.verify_step() method implementation. +static int machine_obj_index_verify_step(lua_State *L) { + lua_settop(L, 5); + auto &m = clua_check>(L, 1); + cm_hash root_hash_before{}; + clua_check_cm_hash(L, 2, &root_hash_before); + cm_hash root_hash_after{}; + clua_check_cm_hash(L, 5, &root_hash_after); + if (cm_verify_step(m.get(), &root_hash_before, luaL_checkstring(L, 3), luaL_checkinteger(L, 4), &root_hash_after) != + 0) { + return luaL_error(L, "%s", cm_get_last_error_message()); + } + return 1; +} + /// \brief This is the machine:verify_step_uarch() method implementation. /// \param L Lua state. static int machine_obj_index_verify_step_uarch(lua_State *L) { @@ -1131,6 +1156,7 @@ static const auto machine_obj_index = cartesi::clua_make_luaL_Reg_array({ {"read_virtual_memory", machine_obj_index_read_virtual_memory}, {"read_word", machine_obj_index_read_word}, {"run", machine_obj_index_run}, + {"log_step", machine_obj_index_log_step}, {"run_uarch", machine_obj_index_run_uarch}, {"log_step_uarch", machine_obj_index_log_step_uarch}, {"store", machine_obj_index_store}, @@ -1152,6 +1178,7 @@ static const auto machine_obj_index = cartesi::clua_make_luaL_Reg_array({ {"log_send_cmio_response", machine_obj_index_log_send_cmio_response}, {"get_default_config", machine_obj_index_get_default_config}, {"get_reg_address", machine_obj_index_get_reg_address}, + {"verify_step", machine_obj_index_verify_step}, {"verify_step_uarch", machine_obj_index_verify_step_uarch}, {"verify_reset_uarch", machine_obj_index_verify_reset_uarch}, {"verify_send_cmio_response", machine_obj_index_verify_send_cmio_response}, diff --git a/src/i-virtual-machine.h b/src/i-virtual-machine.h index e3202ac03..9f09e919e 100644 --- a/src/i-virtual-machine.h +++ b/src/i-virtual-machine.h @@ -88,6 +88,11 @@ class i_virtual_machine { do_store(dir); } + /// \brief Runs the machine for the given mcycle count and generates a log file of accessed pages and proof data. + interpreter_break_reason log_step(uint64_t mcycle_count, const std::string &filename) { + return do_log_step(mcycle_count, filename); + } + /// \brief Runs the machine for one micro cycle logging all accesses to the state. access_log log_step_uarch(const access_log::type &log_type) { return do_log_step_uarch(log_type); @@ -220,6 +225,12 @@ class i_virtual_machine { return do_get_default_config(); } + /// \brief Checks the validity of a state transition caused by log_step. + void verify_step(const hash_type &root_hash_before, const std::string &log_filename, uint64_t mcycle_count, + const hash_type &root_hash_after) const { + do_verify_step(root_hash_before, log_filename, mcycle_count, root_hash_after); + } + /// \brief Checks the validity of a state transition caused by log_step_uarch. void verify_step_uarch(const hash_type &root_hash_before, const access_log &log, const hash_type &root_hash_after) const { @@ -250,6 +261,7 @@ class i_virtual_machine { virtual void do_load(const std::string &directory, const machine_runtime_config &runtime) = 0; virtual interpreter_break_reason do_run(uint64_t mcycle_end) = 0; virtual void do_store(const std::string &dir) const = 0; + virtual interpreter_break_reason do_log_step(uint64_t mcycle_count, const std::string &filename) = 0; virtual access_log do_log_step_uarch(const access_log::type &log_type) = 0; virtual machine_merkle_tree::proof_type do_get_proof(uint64_t address, int log2_size) const = 0; virtual void do_get_root_hash(hash_type &hash) const = 0; @@ -277,6 +289,8 @@ class i_virtual_machine { const access_log::type &log_type) = 0; virtual uint64_t do_get_reg_address(reg r) const = 0; virtual machine_config do_get_default_config() const = 0; + virtual void do_verify_step(const hash_type &root_hash_before, const std::string &log_filename, + uint64_t mcycle_count, const hash_type &root_hash_after) const = 0; virtual void do_verify_step_uarch(const hash_type &root_hash_before, const access_log &log, const hash_type &root_hash_after) const = 0; virtual void do_verify_reset_uarch(const hash_type &root_hash_before, const access_log &log, diff --git a/src/interpret.cpp b/src/interpret.cpp index 1df0a93c4..572d317e3 100644 --- a/src/interpret.cpp +++ b/src/interpret.cpp @@ -24,6 +24,8 @@ #include "uarch-machine-state-access.h" #include "uarch-runtime.h" #else +#include "record-step-state-access.h" +#include "replay-step-state-access.h" #include "state-access.h" #include #endif // MICROARCHITECTURE @@ -5647,6 +5649,10 @@ template interpreter_break_reason interpret(uarch_machine_state_access &a, uint6 #else // Explicit instantiation for state_access template interpreter_break_reason interpret(state_access &a, uint64_t mcycle_end); +// Explicit instantiation for record_step_state_access +template interpreter_break_reason interpret(record_step_state_access &a, uint64_t mcycle_end); +// Explicit instantiation for replay_step_state_access +template interpreter_break_reason interpret(replay_step_state_access &a, uint64_t mcycle_end); #endif // MICROARCHITECTURE } // namespace cartesi diff --git a/src/jsonrpc-discover.json b/src/jsonrpc-discover.json index fabd8510e..2f8a2cbaf 100644 --- a/src/jsonrpc-discover.json +++ b/src/jsonrpc-discover.json @@ -841,6 +841,80 @@ "type": "boolean" } } + }, + { + "name": "machine.log_step", + "summary": "Logs a step of a given number of cycles", + "params": [ + { + "name": "mcycle_count", + "description": "Number of cycles to log", + "required": true, + "schema": { + "$ref": "#/components/schemas/UnsignedInteger" + } + }, + { + "name": "filename", + "description": "Filename to store the log", + "required": true, + "schema": { + "type": "string" + } + } + ], + "result": { + "name": "reason", + "description": "Reason call returned", + "schema": { + "$ref": "#/components/schemas/InterpreterBreakReason" + } + } + }, + { + "name": "machine.verify_step", + "summary": "Checks the validity of a step log file.", + "params": [ + { + "name": "root_hash_before", + "description": "State hash before step", + "required": true, + "schema": { + "$ref": "#/components/schemas/Base64Hash" + } + }, + { + "name": "filename", + "description": "Filename containing the log of the step", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "mcycle_count", + "description": "Number of cycles in step", + "required": true, + "schema": { + "$ref": "#/components/schemas/UnsignedInteger" + } + }, + { + "name": "root_hash_after", + "description": "State hash after step", + "required": true, + "schema": { + "$ref": "#/components/schemas/Base64Hash" + } + } + ], + "result": { + "name": "status", + "description": "True when operation succeeded", + "schema": { + "type": "boolean" + } + } } ], "components": { diff --git a/src/jsonrpc-remote-machine.cpp b/src/jsonrpc-remote-machine.cpp index c6fe30093..bba2bbd36 100644 --- a/src/jsonrpc-remote-machine.cpp +++ b/src/jsonrpc-remote-machine.cpp @@ -953,6 +953,20 @@ static json jsonrpc_machine_run_handler(const json &j, const std::shared_ptr &session) { + if (!session->handler->machine) { + return jsonrpc_response_invalid_request(j, "no machine"); + } + static const char *param_name[] = {"mcycle_count", "filename"}; + auto args = parse_args(j, param_name); + auto reason = session->handler->machine->log_step(std::get<0>(args), std::get<1>(args)); + return jsonrpc_response_ok(j, interpreter_break_reason_name(reason)); +} + /// \brief Translate an uarch_interpret_break_reason value to string /// \param reason uarch_interpret_break_reason value to translate /// \returns String representation of value @@ -1009,6 +1023,20 @@ static json jsonrpc_machine_log_reset_uarch_handler(const json &j, const std::sh return jsonrpc_response_ok(j, session->handler->machine->log_reset_uarch(std::get<0>(args).value())); } +/// \brief JSONRPC handler for the machine.verify_send_cmio_response method +/// \param j JSON request object +/// \param session HTTP session +/// \returns JSON response object +static json jsonrpc_machine_verify_step_handler(const json &j, const std::shared_ptr &session) { + (void) session; + static const char *param_name[] = {"root_hash_before", "filename", "mcycle_count", "root_hash_after"}; + auto args = parse_args(j, param_name); + // NOLINTNEXTLINE(bugprone-unchecked-optional-access) + cartesi::machine::verify_step(std::get<0>(args), std::get<1>(args), std::get<2>(args), std::get<3>(args)); + return jsonrpc_response_ok(j); +} + /// \brief JSONRPC handler for the machine.verify_step_uarch method /// \param j JSON request object /// \param session HTTP session @@ -1428,6 +1456,7 @@ static json jsonrpc_dispatch_method(const json &j, const std::shared_ptr(); SLOG(debug) << session->handler->local_endpoint << " handling \"" << method << "\" method"; diff --git a/src/jsonrpc-virtual-machine.cpp b/src/jsonrpc-virtual-machine.cpp index cbd92aa8e..3aa0ef001 100644 --- a/src/jsonrpc-virtual-machine.cpp +++ b/src/jsonrpc-virtual-machine.cpp @@ -601,6 +601,13 @@ interpreter_break_reason jsonrpc_virtual_machine::do_run(uint64_t mcycle_end) { return result; } +interpreter_break_reason jsonrpc_virtual_machine::do_log_step(uint64_t mcycle_count, const std::string &filename) { + interpreter_break_reason result = interpreter_break_reason::failed; + jsonrpc_request(m_ioc, m_stream, m_address, "machine.log_step", std::tie(mcycle_count, filename), result, + m_timeout); + return result; +} + void jsonrpc_virtual_machine::do_store(const std::string &directory) const { bool result = false; jsonrpc_request(m_ioc, m_stream, m_address, "machine.store", std::tie(directory), result, m_timeout); @@ -782,6 +789,15 @@ machine_config jsonrpc_virtual_machine::do_get_default_config() const { return result; } +void jsonrpc_virtual_machine::do_verify_step(const hash_type &root_hash_before, const std::string &log_filename, + uint64_t mcycle_count, const hash_type &root_hash_after) const { + bool result = false; + auto b64_root_hash_before = encode_base64(root_hash_before); + auto b64_root_hash_after = encode_base64(root_hash_after); + jsonrpc_request(m_ioc, m_stream, m_address, "machine.verify_step", + std::tie(b64_root_hash_before, log_filename, mcycle_count, b64_root_hash_after), result, m_timeout); +} + void jsonrpc_virtual_machine::do_verify_step_uarch(const hash_type &root_hash_before, const access_log &log, const hash_type &root_hash_after) const { bool result = false; diff --git a/src/jsonrpc-virtual-machine.h b/src/jsonrpc-virtual-machine.h index add6cb852..6b448f741 100644 --- a/src/jsonrpc-virtual-machine.h +++ b/src/jsonrpc-virtual-machine.h @@ -101,6 +101,7 @@ class jsonrpc_virtual_machine final : public i_virtual_machine { void do_create(const machine_config &config, const machine_runtime_config &runtime) override; void do_load(const std::string &directory, const machine_runtime_config &runtime) override; interpreter_break_reason do_run(uint64_t mcycle_end) override; + interpreter_break_reason do_log_step(uint64_t mcycle_count, const std::string &filename) override; void do_store(const std::string &dir) const override; uint64_t do_read_reg(reg r) const override; void do_write_reg(reg w, uint64_t val) override; @@ -128,6 +129,8 @@ class jsonrpc_virtual_machine final : public i_virtual_machine { const access_log::type &log_type) override; uint64_t do_get_reg_address(reg r) const override; machine_config do_get_default_config() const override; + void do_verify_step(const hash_type &root_hash_before, const std::string &log_filename, uint64_t mcycle_count, + const hash_type &root_hash_after) const override; void do_verify_step_uarch(const hash_type &root_hash_before, const access_log &log, const hash_type &root_hash_after) const override; void do_verify_reset_uarch(const hash_type &root_hash_before, const access_log &log, diff --git a/src/machine-c-api.cpp b/src/machine-c-api.cpp index 22f514265..a17c29fe6 100644 --- a/src/machine-c-api.cpp +++ b/src/machine-c-api.cpp @@ -336,6 +336,21 @@ cm_error cm_run_uarch(cm_machine *m, uint64_t uarch_cycle_end, cm_uarch_break_re return cm_result_failure(); } +CM_API cm_error cm_log_step(cm_machine *m, uint64_t mcycle_count, const char *log_filename, + cm_break_reason *break_reason) try { + if (log_filename == nullptr) { + throw std::invalid_argument("invalid log_filename"); + } + auto *cpp_m = convert_from_c(m); + const auto status = cpp_m->log_step(mcycle_count, log_filename); + if (break_reason != nullptr) { + *break_reason = static_cast(status); + } + return cm_result_success(); +} catch (...) { + return cm_result_failure(); +} + cm_error cm_log_step_uarch(cm_machine *m, int32_t log_type, const char **log) try { if (log == nullptr) { throw std::invalid_argument("invalid access log output"); @@ -352,6 +367,24 @@ cm_error cm_log_step_uarch(cm_machine *m, int32_t log_type, const char **log) tr return cm_result_failure(); } +cm_error cm_verify_step(const cm_machine *m, const cm_hash *root_hash_before, const char *log_filename, + uint64_t mcycle_count, const cm_hash *root_hash_after) try { + if (log_filename == nullptr) { + throw std::invalid_argument("invalid log_filename"); + } + const cartesi::machine::hash_type cpp_root_hash_before = convert_from_c(root_hash_before); + const cartesi::machine::hash_type cpp_root_hash_after = convert_from_c(root_hash_after); + if (m != nullptr) { + const auto *cpp_m = convert_from_c(m); + cpp_m->verify_step(cpp_root_hash_before, log_filename, mcycle_count, cpp_root_hash_after); + } else { + cartesi::machine::verify_step(cpp_root_hash_before, log_filename, mcycle_count, cpp_root_hash_after); + } + return cm_result_success(); +} catch (...) { + return cm_result_failure(); +} + cm_error cm_verify_step_uarch(const cm_machine *m, const cm_hash *root_hash_before, const char *log, const cm_hash *root_hash_after) try { if (log == nullptr) { diff --git a/src/machine-c-api.h b/src/machine-c-api.h index 22968cab6..3e9243b6d 100644 --- a/src/machine-c-api.h +++ b/src/machine-c-api.h @@ -672,6 +672,15 @@ CM_API cm_error cm_send_cmio_response(cm_machine *m, uint16_t reason, const uint // Logging // ------------------------------------ +/// \brief Runs the machine for the given mcycle count and generates a log of accessed pages and proof data. +/// \param m Pointer to a non-empty machine object (holds a machine instance). +/// \param mcycle_count Number of mcycles to run +/// \param log_filename Name of the log file to be generated +/// \param break_reason Receives reason for returning (can be NULL). +/// \returns 0 for success, non zero code for error. +CM_API cm_error cm_log_step(cm_machine *m, uint64_t mcycle_count, const char *log_filename, + cm_break_reason *break_reason_result); + /// \brief Runs the machine in the microarchitecture for one micro cycle logging all accesses to the state. /// \param m Pointer to a non-empty machine object (holds a machine instance). /// \param log_type Type of access log to generate. @@ -704,6 +713,16 @@ CM_API cm_error cm_log_send_cmio_response(cm_machine *m, uint16_t reason, const // Verifying // ------------------------------------ +/// \brief Checks the validity of a step log file. +/// \param m Pointer to a non-empty machine object (holds a machine instance). +/// \param root_hash_before State hash before step +/// \param log_filename Path to the step log file to be verified +/// \param mcycle_count Number of mcycles in the step +/// \param root_hash_after State hash after step +/// \returns 0 for success, non zero code for error +CM_API cm_error cm_verify_step(const cm_machine *m, const cm_hash *root_hash_before, const char *log_filename, + uint64_t mcycle_count, const cm_hash *root_hash_after); + /// \brief Checks the validity of a state transition produced by cm_log_step_uarch. /// \param m Pointer to a machine object. Can be NULL (for local machines). /// \param root_hash_before State hash before step. diff --git a/src/machine-merkle-tree.cpp b/src/machine-merkle-tree.cpp index 5dd88e84e..a0c23bc57 100644 --- a/src/machine-merkle-tree.cpp +++ b/src/machine-merkle-tree.cpp @@ -431,6 +431,33 @@ machine_merkle_tree::proof_type machine_merkle_tree::get_proof(address_type targ return proof; } +machine_merkle_tree::hash_type machine_merkle_tree::get_node_hash(address_type target_address, + int log2_target_size) const { + if (log2_target_size > get_log2_root_size() || log2_target_size < get_log2_word_size()) { + throw std::runtime_error{"log2_target_size is out of bounds"}; + } + if (log2_target_size < get_log2_page_size()) { + throw std::runtime_error{"log2_target_size is smaller than page size"}; + } + if ((target_address & ((static_cast(1) << log2_target_size) - 1)) != 0) { + throw std::invalid_argument{"address is not page-aligned"}; + } + int log2_node_size = get_log2_root_size(); + const tree_node *node = m_root; + // walk down the tree until we reach the target node + while (node != nullptr && log2_node_size > log2_target_size) { + const int log2_child_size = log2_node_size - 1; + const int path_bit = static_cast((target_address & (UINT64_C(1) << (log2_child_size))) != 0); + node = node->child[path_bit]; + log2_node_size = log2_child_size; + } + if (node == nullptr) { + // We hit a pristine node along the path to the target node + return get_pristine_hash(log2_target_size); + } + return node->hash; +} + std::ostream &operator<<(std::ostream &out, const machine_merkle_tree::hash_type &hash) { auto f = out.flags(); for (const unsigned b : hash) { diff --git a/src/machine-merkle-tree.h b/src/machine-merkle-tree.h index d65ea209a..71293fed5 100644 --- a/src/machine-merkle-tree.h +++ b/src/machine-merkle-tree.h @@ -349,6 +349,12 @@ class machine_merkle_tree final { /// \param hash Receives the hash. void get_page_node_hash(address_type page_index, hash_type &hash) const; + /// \brief Get the hash of a node in the Merkle tree. + /// \param target_address Address of target node. + /// \param log2_target_size log2 of the node size. + /// \return Hash of the node. + hash_type get_node_hash(address_type target_address, int log2_target_size) const; + /// \brief Returns the hash for a log2_size pristine node. /// \param log2_size log2 of size subintended by node. /// \return Reference to precomputed hash. diff --git a/src/machine.cpp b/src/machine.cpp index 0e5ad1761..a94ad3e12 100644 --- a/src/machine.cpp +++ b/src/machine.cpp @@ -50,7 +50,9 @@ #include "pma-defines.h" #include "pma.h" #include "record-state-access.h" +#include "record-step-state-access.h" #include "replay-state-access.h" +#include "replay-step-state-access.h" #include "riscv-constants.h" #include "send-cmio-response.h" #include "shadow-pmas-factory.h" @@ -2112,6 +2114,18 @@ void machine::get_root_hash(hash_type &hash) const { m_t.get_root_hash(hash); } +machine::hash_type machine::get_merkle_tree_node_hash(uint64_t address, int log2_size, + skip_merkle_tree_update_t /* unused */) const { + return m_t.get_node_hash(address, log2_size); +} + +machine::hash_type machine::get_merkle_tree_node_hash(uint64_t address, int log2_size) const { + if (!update_merkle_tree()) { + throw std::runtime_error{"error updating Merkle tree"}; + } + return get_merkle_tree_node_hash(address, log2_size, skip_merkle_tree_update); +} + bool machine::verify_merkle_tree() const { return m_t.verify_tree(); } @@ -2482,6 +2496,39 @@ uarch_interpreter_break_reason machine::run_uarch(uint64_t uarch_cycle_end) { return uarch_interpret(a, uarch_cycle_end); } +interpreter_break_reason machine::log_step(uint64_t mcycle_count, const std::string &filename) { + if (!update_merkle_tree()) { + throw std::runtime_error{"error updating Merkle tree"}; + } + hash_type root_hash_before; + get_root_hash(root_hash_before); + record_step_state_access a(*this, filename); + uint64_t mcycle_end{}; + if (__builtin_add_overflow(a.read_mcycle(), mcycle_count, &mcycle_end)) { + mcycle_end = UINT64_MAX; + } + auto break_reason = interpret(a, mcycle_end); + a.finish(); + hash_type root_hash_after; + get_root_hash(root_hash_after); + verify_step(root_hash_before, filename, mcycle_count, root_hash_after); + return break_reason; +} + +void machine::verify_step(const hash_type &root_hash_before, const std::string &filename, uint64_t mcycle_count, + const hash_type &root_hash_after) { + auto data_length = os_get_file_length(filename.c_str(), "step log file"); + auto *data = os_map_file(filename.c_str(), data_length, false /* not shared */); + replay_step_state_access a(data, data_length, root_hash_before); + uint64_t mcycle_end{}; + if (__builtin_add_overflow(a.read_mcycle(), mcycle_count, &mcycle_end)) { + mcycle_end = UINT64_MAX; + } + interpret(a, mcycle_end); + a.finish(root_hash_after); + os_unmap_file(data, data_length); +} + interpreter_break_reason machine::run(uint64_t mcycle_end) { if (mcycle_end < read_reg(reg::mcycle)) { throw std::invalid_argument{"mcycle is past"}; diff --git a/src/machine.h b/src/machine.h index 786810000..2e9ce2299 100644 --- a/src/machine.h +++ b/src/machine.h @@ -323,6 +323,20 @@ class machine final { /// frequent scenario is when the program executes a WFI instruction. Another example is when the machine halts. interpreter_break_reason run(uint64_t mcycle_end); + /// \brief Runs the machine for the given mcycle count and generates a log of accessed pages and proof data. + /// \param mcycle_count Number of mcycles to run the machine for. + /// \param filename Name of the file to store the log. + /// \returns The reason the machine was interrupted. + interpreter_break_reason log_step(uint64_t mcycle_count, const std::string &filename); + + /// \brief Checks the validity of a step log file. + /// \param root_hash_before Hash of the state before the step. + /// \param log_filename Name of the file containing the log. + /// \param mcycle_count Number of mcycles the machine was run for. + /// \param root_hash_after Hash of the state after the step. + static void verify_step(const hash_type &root_hash_before, const std::string &log_filename, uint64_t mcycle_count, + const hash_type &root_hash_after); + /// \brief Runs the machine in the microarchitecture until the mcycles advances by one unit or the micro cycle /// counter (uarch_cycle) reaches uarch_cycle_end /// \param uarch_cycle_end uarch_cycle limit @@ -439,6 +453,18 @@ class machine final { /// \param hash Receives the hash. void get_root_hash(hash_type &hash) const; + /// \brief Obtains the hash of a node in the Merkle tree. + /// \param address Address of target node. Must be aligned to a 2log2_size boundary. + /// \param log2_size log2 of size subintended by target node. + /// \returns The hash of the target node. + hash_type get_merkle_tree_node_hash(uint64_t address, int log2_size) const; + + /// \brief Obtains the hash of a node in the Merkle tree without making any modifications to the tree. + /// \param address Address of target node. Must be aligned to a 2log2_size boundary. + /// \param log2_size log2 of size subintended by target node. + /// \returns The hash of the target node. + hash_type get_merkle_tree_node_hash(uint64_t address, int log2_size, skip_merkle_tree_update_t /*unused*/) const; + /// \brief Verifies integrity of Merkle tree. /// \returns True if tree is self-consistent, false otherwise. bool verify_merkle_tree() const; diff --git a/src/os.cpp b/src/os.cpp index ab543f2a9..e05316995 100644 --- a/src/os.cpp +++ b/src/os.cpp @@ -956,4 +956,23 @@ int os_double_fork([[maybe_unused]] bool emancipate, [[maybe_unused]] const char #endif } +int64_t os_get_file_length(const char *filename, const char *text) { + auto fp = unique_fopen(filename, "rb"); + if (fseek(fp.get(), 0, SEEK_END) != 0) { + throw std::system_error{errno, std::generic_category(), + "unable to obtain length of file '"s + filename + "' "s + text}; + } + const auto length = ftell(fp.get()); + if (length < 0) { + throw std::system_error{errno, std::generic_category(), + "unable to obtain length of file '"s + filename + "' "s + text}; + } + return length; +} + +bool os_file_exists(const char *filename) { + struct stat buffer {}; + return (stat(filename, &buffer) == 0); +} + } // namespace cartesi diff --git a/src/os.h b/src/os.h index a821c060f..ffb41ea21 100644 --- a/src/os.h +++ b/src/os.h @@ -163,6 +163,12 @@ int os_double_fork_or_throw(bool emancipate); // In case of error, parent returns -1 and there is no grand-child. int os_double_fork(bool emancipate, const char **err_msg); +/// \brief Get the length of a file +int64_t os_get_file_length(const char *filename, const char *text = ""); + +/// \brief Check if a file exists +bool os_file_exists(const char *filename); + } // namespace cartesi #endif diff --git a/src/record-step-state-access.h b/src/record-step-state-access.h new file mode 100644 index 000000000..323b0f1ce --- /dev/null +++ b/src/record-step-state-access.h @@ -0,0 +1,839 @@ +// Copyright Cartesi and individual authors (see AUTHORS) +// SPDX-License-Identifier: LGPL-3.0-or-later +// +// This program is free software: you can redistribute it and/or modify it under +// the terms of the GNU Lesser General Public License as published by the Free +// Software Foundation, either version 3 of the License, or (at your option) any +// later version. +// +// This program is distributed in the hope that it will be useful, but WITHOUT ANY +// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A +// PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License along +// with this program (see COPYING). If not, see . +// + +#ifndef RECORD_STEP_STATE_ACCESS_H +#define RECORD_STEP_STATE_ACCESS_H + +#include "compiler-defines.h" +#include "machine.h" + +#include "device-state-access.h" +#include "i-state-access.h" +#include "shadow-pmas.h" +#include "unique-c-ptr.h" +#include +#include +#include + +namespace cartesi { + +/// \class record_step_state_access +/// \brief Records machine state access into a step log file +class record_step_state_access : public i_state_access { + constexpr static int LOG2_ROOT_SIZE = machine_merkle_tree::get_log2_root_size(); + constexpr static int LOG2_PAGE_SIZE = machine_merkle_tree::get_log2_page_size(); + constexpr static uint64_t PAGE_SIZE = UINT64_C(1) << LOG2_PAGE_SIZE; + + using address_type = machine_merkle_tree::address_type; + using page_data_type = std::array; + using pages_type = std::map; + using hash_type = machine_merkle_tree::hash_type; + using sibling_hashes_type = std::vector; + using page_indices_type = std::vector; + + // NOLINTNEXTLINE(cppcoreguidelines-avoid-const-or-ref-data-members) + machine &m_m; ///< reference to machine + std::string m_filename; ///< where to save the log + mutable pages_type m_touched_pages; ///< copy of all pages touched during execution + +public: + /// \brief Constructor + /// \param m reference to machine + /// \param filename where to save the log + /// \details The log file is saved when finish() is called + record_step_state_access(machine &m, const std::string &filename) : m_m(m), m_filename(filename) { + if (os_file_exists(filename.c_str())) { + throw std::runtime_error("file already exists"); + } + } + record_step_state_access(const record_step_state_access &) = delete; + record_step_state_access(record_step_state_access &&) = delete; + record_step_state_access &operator=(const record_step_state_access &) = delete; + record_step_state_access &operator=(record_step_state_access &&) = delete; + ~record_step_state_access() = default; + + /// \brief Finish recording and save the log file + void finish() { + // get sibling hashes of all touched pages + auto sibling_hashes = get_sibling_hashes(); + uint64_t page_count = m_touched_pages.size(); + uint64_t sibling_count = sibling_hashes.size(); + + // Write log file. + // The log format is as follows: + // page_count, [(page_index, data, scratch_area), ...], sibling_count, [sibling_hash, ...] + // We store the page index, instead of the page address. + // Scratch area is used by the replay to store page hashes, which change during replay + // This is to work around the lack of dynamic memory allocation when replaying the log in microarchitectures + auto fp = unique_fopen(m_filename.c_str(), "wb"); + if (fwrite(&page_count, sizeof(page_count), 1, fp.get()) != 1) { + throw std::runtime_error("Could not write page count to log file"); + } + for (auto &[address, data] : m_touched_pages) { + const auto page_index = address >> LOG2_PAGE_SIZE; + if (fwrite(&page_index, sizeof(page_index), 1, fp.get()) != 1) { + throw std::runtime_error("Could not write page index to log file"); + } + if (fwrite(data.data(), data.size(), 1, fp.get()) != 1) { + throw std::runtime_error("Could not write page data to log file"); + } + static const hash_type all_zeros{}; + if (fwrite(all_zeros.data(), sizeof(all_zeros), 1, fp.get()) != 1) { + throw std::runtime_error("Could not write page hash scratch to log file"); + } + } + if (fwrite(&sibling_count, sizeof(sibling_count), 1, fp.get()) != 1) { + throw std::runtime_error("Could not write sibling count to log file"); + } + for (auto &hash : sibling_hashes) { + if (fwrite(hash.data(), sizeof(hash), 1, fp.get()) != 1) { + throw std::runtime_error("Could not write sibling hash to log file"); + } + } + } + +private: + friend i_state_access; + + /// \brief Mark a page as touched and save its contents + /// \param address address of the page + void touch_page(address_type address) const { + auto page = address & ~(PAGE_SIZE - 1); + if (m_touched_pages.find(page) != m_touched_pages.end()) { + return; // already saved + } + auto [it, _] = m_touched_pages.emplace(page, page_data_type()); + m_m.read_memory(page, it->second.data(), it->second.size()); + } + + /// \brief Get the sibling hashes of all touched pages + sibling_hashes_type get_sibling_hashes() { + sibling_hashes_type sibling_hashes{}; + // page address are converted to page indices, in order to avoid overflows + page_indices_type page_indices{}; + // iterate in ascending order of page addresses (the container is ordered by key) + for (const auto &[address, _] : m_touched_pages) { + page_indices.push_back(address >> LOG2_PAGE_SIZE); + } + auto next_page_index = page_indices.cbegin(); + get_sibling_hashes_impl(0, LOG2_ROOT_SIZE - LOG2_PAGE_SIZE, page_indices, next_page_index, sibling_hashes); + if (next_page_index != page_indices.cend()) { + throw std::runtime_error("get_sibling_hashes failed to consume all pages"); + } + return sibling_hashes; + } + + /// \brief Recursively get the sibling hashes of all touched pages + /// \param page_index index of 1st page in range + /// \param page_count_log2_size log2 of the number of pages in range + /// \param page_indices indices of all pages + /// \param next_page_index smallest page index not visited yet + /// \param sibling_hashes stores the collected sibling hashes during the recursion + void get_sibling_hashes_impl(address_type page_index, int page_count_log2_size, page_indices_type &page_indices, + page_indices_type::const_iterator &next_page_index, sibling_hashes_type &sibling_hashes) { + auto page_count = UINT64_C(1) << page_count_log2_size; + if (next_page_index == page_indices.cend() || page_index + page_count <= *next_page_index) { + // we can skip the merkle tree update, because a full update was done before the recording started + sibling_hashes.push_back(m_m.get_merkle_tree_node_hash(page_index << LOG2_PAGE_SIZE, + page_count_log2_size + LOG2_PAGE_SIZE, skip_merkle_tree_update)); + } else if (page_count_log2_size > 0) { + get_sibling_hashes_impl(page_index, page_count_log2_size - 1, page_indices, next_page_index, + sibling_hashes); + get_sibling_hashes_impl(page_index + (UINT64_C(1) << (page_count_log2_size - 1)), page_count_log2_size - 1, + page_indices, next_page_index, sibling_hashes); + } else { + ++next_page_index; + } + } + + // NOLINTNEXTLINE(readability-convert-member-functions-to-static) + void do_push_bracket(bracket_type type, const char *text) { + (void) type; + (void) text; + } + + int do_make_scoped_note(const char *text) { // NOLINT(readability-convert-member-functions-to-static) + (void) text; + return 0; + } + + uint64_t do_read_x(int reg) const { + touch_page(shadow_state_get_x_abs_addr(reg)); + return m_m.get_state().x[reg]; + } + + void do_write_x(int reg, uint64_t val) { + assert(reg != 0); + touch_page(shadow_state_get_x_abs_addr(reg)); + m_m.get_state().x[reg] = val; + } + + uint64_t do_read_f(int reg) const { + touch_page(shadow_state_get_f_abs_addr(reg)); + return m_m.get_state().f[reg]; + } + + void do_write_f(int reg, uint64_t val) { + touch_page(shadow_state_get_f_abs_addr(reg)); + m_m.get_state().f[reg] = val; + } + + uint64_t do_read_pc() const { + // get phys address of pc in dhadow + touch_page(shadow_state_get_reg_abs_addr(shadow_state_reg::pc)); + return m_m.get_state().pc; + } + + void do_write_pc(uint64_t val) { + touch_page(shadow_state_get_reg_abs_addr(shadow_state_reg::pc)); + m_m.get_state().pc = val; + } + + uint64_t do_read_fcsr() const { + touch_page(shadow_state_get_reg_abs_addr(shadow_state_reg::fcsr)); + return m_m.get_state().fcsr; + } + + void do_write_fcsr(uint64_t val) { + touch_page(shadow_state_get_reg_abs_addr(shadow_state_reg::fcsr)); + m_m.get_state().fcsr = val; + } + + uint64_t do_read_icycleinstret() const { + touch_page(shadow_state_get_reg_abs_addr(shadow_state_reg::icycleinstret)); + return m_m.get_state().icycleinstret; + } + + void do_write_icycleinstret(uint64_t val) { + touch_page(shadow_state_get_reg_abs_addr(shadow_state_reg::icycleinstret)); + m_m.get_state().icycleinstret = val; + } + + uint64_t do_read_mvendorid() const { // NOLINT(readability-convert-member-functions-to-static) + touch_page(shadow_state_get_reg_abs_addr(shadow_state_reg::mvendorid)); + return MVENDORID_INIT; + } + + uint64_t do_read_marchid() const { // NOLINT(readability-convert-member-functions-to-static) + touch_page(shadow_state_get_reg_abs_addr(shadow_state_reg::marchid)); + return MARCHID_INIT; + } + + uint64_t do_read_mimpid() const { // NOLINT(readability-convert-member-functions-to-static) + touch_page(shadow_state_get_reg_abs_addr(shadow_state_reg::mimpid)); + return MIMPID_INIT; + } + + uint64_t do_read_mcycle() const { + touch_page(shadow_state_get_reg_abs_addr(shadow_state_reg::mcycle)); + return m_m.get_state().mcycle; + } + + void do_write_mcycle(uint64_t val) { + touch_page(shadow_state_get_reg_abs_addr(shadow_state_reg::mcycle)); + m_m.get_state().mcycle = val; + } + + uint64_t do_read_mstatus() const { + touch_page(shadow_state_get_reg_abs_addr(shadow_state_reg::mstatus)); + return m_m.get_state().mstatus; + } + + void do_write_mstatus(uint64_t val) { + touch_page(shadow_state_get_reg_abs_addr(shadow_state_reg::mstatus)); + m_m.get_state().mstatus = val; + } + + uint64_t do_read_menvcfg() const { + touch_page(shadow_state_get_reg_abs_addr(shadow_state_reg::menvcfg)); + return m_m.get_state().menvcfg; + } + + void do_write_menvcfg(uint64_t val) { + touch_page(shadow_state_get_reg_abs_addr(shadow_state_reg::menvcfg)); + m_m.get_state().menvcfg = val; + } + + uint64_t do_read_mtvec() const { + touch_page(shadow_state_get_reg_abs_addr(shadow_state_reg::mtvec)); + return m_m.get_state().mtvec; + } + + void do_write_mtvec(uint64_t val) { + touch_page(shadow_state_get_reg_abs_addr(shadow_state_reg::mtvec)); + m_m.get_state().mtvec = val; + } + + uint64_t do_read_mscratch() const { + touch_page(shadow_state_get_reg_abs_addr(shadow_state_reg::mscratch)); + return m_m.get_state().mscratch; + } + + void do_write_mscratch(uint64_t val) { + touch_page(shadow_state_get_reg_abs_addr(shadow_state_reg::mscratch)); + m_m.get_state().mscratch = val; + } + + uint64_t do_read_mepc() const { + touch_page(shadow_state_get_reg_abs_addr(shadow_state_reg::mepc)); + return m_m.get_state().mepc; + } + + void do_write_mepc(uint64_t val) { + touch_page(shadow_state_get_reg_abs_addr(shadow_state_reg::mepc)); + m_m.get_state().mepc = val; + } + + uint64_t do_read_mcause() const { + touch_page(shadow_state_get_reg_abs_addr(shadow_state_reg::mcause)); + return m_m.get_state().mcause; + } + + void do_write_mcause(uint64_t val) { + touch_page(shadow_state_get_reg_abs_addr(shadow_state_reg::mcause)); + m_m.get_state().mcause = val; + } + + uint64_t do_read_mtval() const { + touch_page(shadow_state_get_reg_abs_addr(shadow_state_reg::mtval)); + return m_m.get_state().mtval; + } + + void do_write_mtval(uint64_t val) { + touch_page(shadow_state_get_reg_abs_addr(shadow_state_reg::mtval)); + m_m.get_state().mtval = val; + } + + uint64_t do_read_misa() const { + touch_page(shadow_state_get_reg_abs_addr(shadow_state_reg::misa)); + return m_m.get_state().misa; + } + + void do_write_misa(uint64_t val) { + touch_page(shadow_state_get_reg_abs_addr(shadow_state_reg::misa)); + m_m.get_state().misa = val; + } + + uint64_t do_read_mie() const { + touch_page(shadow_state_get_reg_abs_addr(shadow_state_reg::mie)); + return m_m.get_state().mie; + } + + void do_write_mie(uint64_t val) { + touch_page(shadow_state_get_reg_abs_addr(shadow_state_reg::mie)); + m_m.get_state().mie = val; + } + + uint64_t do_read_mip() const { + touch_page(shadow_state_get_reg_abs_addr(shadow_state_reg::mip)); + return m_m.get_state().mip; + } + + void do_write_mip(uint64_t val) { + touch_page(shadow_state_get_reg_abs_addr(shadow_state_reg::mip)); + m_m.get_state().mip = val; + } + + uint64_t do_read_medeleg() const { + touch_page(shadow_state_get_reg_abs_addr(shadow_state_reg::medeleg)); + return m_m.get_state().medeleg; + } + + void do_write_medeleg(uint64_t val) { + touch_page(shadow_state_get_reg_abs_addr(shadow_state_reg::medeleg)); + m_m.get_state().medeleg = val; + } + + uint64_t do_read_mideleg() const { + touch_page(shadow_state_get_reg_abs_addr(shadow_state_reg::mideleg)); + return m_m.get_state().mideleg; + } + + void do_write_mideleg(uint64_t val) { + touch_page(shadow_state_get_reg_abs_addr(shadow_state_reg::mideleg)); + m_m.get_state().mideleg = val; + } + + uint64_t do_read_mcounteren() const { + touch_page(shadow_state_get_reg_abs_addr(shadow_state_reg::mcounteren)); + return m_m.get_state().mcounteren; + } + + void do_write_mcounteren(uint64_t val) { + touch_page(shadow_state_get_reg_abs_addr(shadow_state_reg::mcounteren)); + m_m.get_state().mcounteren = val; + } + + uint64_t do_read_senvcfg() const { + touch_page(shadow_state_get_reg_abs_addr(shadow_state_reg::senvcfg)); + return m_m.get_state().senvcfg; + } + + void do_write_senvcfg(uint64_t val) { + touch_page(shadow_state_get_reg_abs_addr(shadow_state_reg::senvcfg)); + m_m.get_state().senvcfg = val; + } + + uint64_t do_read_stvec() const { + touch_page(shadow_state_get_reg_abs_addr(shadow_state_reg::stvec)); + return m_m.get_state().stvec; + } + + void do_write_stvec(uint64_t val) { + touch_page(shadow_state_get_reg_abs_addr(shadow_state_reg::stvec)); + m_m.get_state().stvec = val; + } + + uint64_t do_read_sscratch() const { + touch_page(shadow_state_get_reg_abs_addr(shadow_state_reg::sscratch)); + return m_m.get_state().sscratch; + } + + void do_write_sscratch(uint64_t val) { + touch_page(shadow_state_get_reg_abs_addr(shadow_state_reg::sscratch)); + m_m.get_state().sscratch = val; + } + + uint64_t do_read_sepc() const { + touch_page(shadow_state_get_reg_abs_addr(shadow_state_reg::sepc)); + return m_m.get_state().sepc; + } + + void do_write_sepc(uint64_t val) { + touch_page(shadow_state_get_reg_abs_addr(shadow_state_reg::sepc)); + m_m.get_state().sepc = val; + } + + uint64_t do_read_scause() const { + touch_page(shadow_state_get_reg_abs_addr(shadow_state_reg::scause)); + return m_m.get_state().scause; + } + + void do_write_scause(uint64_t val) { + touch_page(shadow_state_get_reg_abs_addr(shadow_state_reg::scause)); + m_m.get_state().scause = val; + } + + uint64_t do_read_stval() const { + touch_page(shadow_state_get_reg_abs_addr(shadow_state_reg::stval)); + return m_m.get_state().stval; + } + + void do_write_stval(uint64_t val) { + touch_page(shadow_state_get_reg_abs_addr(shadow_state_reg::stval)); + m_m.get_state().stval = val; + } + + uint64_t do_read_satp() const { + touch_page(shadow_state_get_reg_abs_addr(shadow_state_reg::satp)); + return m_m.get_state().satp; + } + + void do_write_satp(uint64_t val) { + touch_page(shadow_state_get_reg_abs_addr(shadow_state_reg::satp)); + m_m.get_state().satp = val; + } + + uint64_t do_read_scounteren() const { + touch_page(shadow_state_get_reg_abs_addr(shadow_state_reg::scounteren)); + return m_m.get_state().scounteren; + } + + void do_write_scounteren(uint64_t val) { + touch_page(shadow_state_get_reg_abs_addr(shadow_state_reg::scounteren)); + m_m.get_state().scounteren = val; + } + + uint64_t do_read_ilrsc() const { + touch_page(shadow_state_get_reg_abs_addr(shadow_state_reg::ilrsc)); + return m_m.get_state().ilrsc; + } + + void do_write_ilrsc(uint64_t val) { + touch_page(shadow_state_get_reg_abs_addr(shadow_state_reg::ilrsc)); + m_m.get_state().ilrsc = val; + } + + void do_set_iflags_H() { + touch_page(shadow_state_get_reg_abs_addr(shadow_state_reg::iflags)); + m_m.get_state().iflags.H = true; + } + + bool do_read_iflags_H() const { + touch_page(shadow_state_get_reg_abs_addr(shadow_state_reg::iflags)); + return m_m.get_state().iflags.H; + } + + void do_set_iflags_X() { + touch_page(shadow_state_get_reg_abs_addr(shadow_state_reg::iflags)); + m_m.get_state().iflags.X = true; + } + + void do_reset_iflags_X() { + touch_page(shadow_state_get_reg_abs_addr(shadow_state_reg::iflags)); + m_m.get_state().iflags.X = false; + } + + bool do_read_iflags_X() const { + touch_page(shadow_state_get_reg_abs_addr(shadow_state_reg::iflags)); + return m_m.get_state().iflags.X; + } + + void do_set_iflags_Y() { + touch_page(shadow_state_get_reg_abs_addr(shadow_state_reg::iflags)); + m_m.get_state().iflags.Y = true; + } + + void do_reset_iflags_Y() { + touch_page(shadow_state_get_reg_abs_addr(shadow_state_reg::iflags)); + m_m.get_state().iflags.Y = false; + } + + bool do_read_iflags_Y() const { + touch_page(shadow_state_get_reg_abs_addr(shadow_state_reg::iflags)); + return m_m.get_state().iflags.Y; + } + + uint8_t do_read_iflags_PRV() const { + touch_page(shadow_state_get_reg_abs_addr(shadow_state_reg::iflags)); + return m_m.get_state().iflags.PRV; + } + + void do_write_iflags_PRV(uint8_t val) { + touch_page(shadow_state_get_reg_abs_addr(shadow_state_reg::iflags)); + m_m.get_state().iflags.PRV = val; + } + + uint64_t do_read_iunrep() const { + touch_page(shadow_state_get_reg_abs_addr(shadow_state_reg::iunrep)); + return m_m.get_state().iunrep; + } + + void do_write_iunrep(uint64_t val) { + touch_page(shadow_state_get_reg_abs_addr(shadow_state_reg::iunrep)); + m_m.get_state().iunrep = val; + } + + uint64_t do_read_clint_mtimecmp() const { + touch_page(shadow_state_get_reg_abs_addr(shadow_state_reg::clint_mtimecmp)); + return m_m.get_state().clint.mtimecmp; + } + + void do_write_clint_mtimecmp(uint64_t val) { + touch_page(shadow_state_get_reg_abs_addr(shadow_state_reg::clint_mtimecmp)); + m_m.get_state().clint.mtimecmp = val; + } + + uint64_t do_read_plic_girqpend() const { + touch_page(shadow_state_get_reg_abs_addr(shadow_state_reg::plic_girqpend)); + return m_m.get_state().plic.girqpend; + } + + void do_write_plic_girqpend(uint64_t val) { + touch_page(shadow_state_get_reg_abs_addr(shadow_state_reg::plic_girqpend)); + m_m.get_state().plic.girqpend = val; + } + + uint64_t do_read_plic_girqsrvd() const { + touch_page(shadow_state_get_reg_abs_addr(shadow_state_reg::plic_girqsrvd)); + return m_m.get_state().plic.girqsrvd; + } + + void do_write_plic_girqsrvd(uint64_t val) { + touch_page(shadow_state_get_reg_abs_addr(shadow_state_reg::plic_girqsrvd)); + m_m.get_state().plic.girqsrvd = val; + } + + uint64_t do_read_htif_fromhost() const { + touch_page(shadow_state_get_reg_abs_addr(shadow_state_reg::htif_fromhost)); + return m_m.get_state().htif.fromhost; + } + + void do_write_htif_fromhost(uint64_t val) { + touch_page(shadow_state_get_reg_abs_addr(shadow_state_reg::htif_fromhost)); + m_m.get_state().htif.fromhost = val; + } + + uint64_t do_read_htif_tohost() const { + touch_page(shadow_state_get_reg_abs_addr(shadow_state_reg::htif_tohost)); + return m_m.get_state().htif.tohost; + } + + void do_write_htif_tohost(uint64_t val) { + touch_page(shadow_state_get_reg_abs_addr(shadow_state_reg::htif_tohost)); + m_m.get_state().htif.tohost = val; + } + + uint64_t do_read_htif_ihalt() const { + touch_page(shadow_state_get_reg_abs_addr(shadow_state_reg::htif_ihalt)); + return m_m.get_state().htif.ihalt; + } + + uint64_t do_read_htif_iconsole() const { + touch_page(shadow_state_get_reg_abs_addr(shadow_state_reg::htif_iconsole)); + return m_m.get_state().htif.iconsole; + } + + uint64_t do_read_htif_iyield() const { + touch_page(shadow_state_get_reg_abs_addr(shadow_state_reg::htif_iyield)); + return m_m.get_state().htif.iyield; + } + + // NOLINTNEXTLINE(readability-convert-member-functions-to-static) + NO_INLINE std::pair do_poll_external_interrupts(uint64_t mcycle, uint64_t mcycle_max) { + (void) mcycle_max; + return {mcycle, false}; + } + + uint64_t do_read_pma_istart(int i) const { + assert(i >= 0 && i < (int) PMA_MAX); + touch_page(shadow_pmas_get_pma_abs_addr(i)); + const auto &pmas = m_m.get_pmas(); + uint64_t istart = 0; + if (i >= 0 && i < static_cast(pmas.size())) { + istart = pmas[i].get_istart(); + } + return istart; + } + + uint64_t do_read_pma_ilength(int i) const { + assert(i >= 0 && i < (int) PMA_MAX); + touch_page(shadow_pmas_get_pma_abs_addr(i)); + const auto &pmas = m_m.get_pmas(); + uint64_t ilength = 0; + if (i >= 0 && i < static_cast(pmas.size())) { + ilength = pmas[i].get_ilength(); + } + return ilength; + } + + template + void do_read_memory_word(uint64_t paddr, const unsigned char *hpage, uint64_t hoffset, T *pval) const { + (void) paddr; + touch_page(paddr); + *pval = cartesi::aliased_aligned_read(hpage + hoffset); + } + + template + void do_write_memory_word(uint64_t paddr, unsigned char *hpage, uint64_t hoffset, T val) { + (void) paddr; + touch_page(paddr); + aliased_aligned_write(hpage + hoffset, val); + } + + // NOLINTNEXTLINE(readability-convert-member-functions-to-static) + bool do_read_memory(uint64_t paddr, const unsigned char *data, uint64_t length) const { + (void) paddr; + (void) data; + (void) length; + throw std::runtime_error("Unexpected call to do_read_memory"); + } + + // NOLINTNEXTLINE(readability-convert-member-functions-to-static) + bool do_write_memory(uint64_t paddr, const unsigned char *data, uint64_t length) { + + (void) paddr; + (void) data; + (void) length; + throw std::runtime_error("Unexpected call to do_write_memory"); + } + + template + pma_entry &do_find_pma_entry(uint64_t paddr) { + int i = 0; + while (true) { + touch_page(shadow_pmas_get_pma_abs_addr(i)); + auto &pma = m_m.get_state().pmas[i]; + // The pmas array always contain a sentinel. It is an entry with + // zero length. If we hit it, return it + if (pma.get_length() == 0) { + return pma; + } + // Otherwise, if we found an entry where the access fits, return it + // Note the "strange" order of arithmetic operations. + // This is to ensure there is no overflow. + // Since we know paddr >= start, there is no chance of overflow + // in the first subtraction. + // Since length is at least 4096 (an entire page), there is no + // chance of overflow in the second subtraction. + if (paddr >= pma.get_start() && paddr - pma.get_start() <= pma.get_length() - sizeof(T)) { + return pma; + } + i++; + } + } + + static unsigned char *do_get_host_memory(pma_entry &pma) { + return pma.get_memory_noexcept().get_host_memory(); + } + + pma_entry &do_get_pma_entry(int index) { + auto &pmas = m_m.get_state().pmas; + const auto last_pma_index = static_cast(pmas.size()) - 1; + if (index >= last_pma_index) { + touch_page(shadow_pmas_get_pma_abs_addr(last_pma_index)); + return pmas[last_pma_index]; + } + touch_page(shadow_pmas_get_pma_abs_addr(index)); + return pmas[index]; + } + + uint64_t do_read_iflags() { + touch_page(shadow_state_get_reg_abs_addr(shadow_state_reg::iflags)); + return m_m.get_state().read_iflags(); + } + + void do_write_iflags(uint64_t val) { + touch_page(shadow_state_get_reg_abs_addr(shadow_state_reg::iflags)); + m_m.get_state().write_iflags(val); + } + + bool do_read_device(pma_entry &pma, uint64_t mcycle, uint64_t offset, uint64_t *pval, int log2_size) { + device_state_access da(*this, mcycle); + return pma.get_device_noexcept().get_driver()->read(pma.get_device_noexcept().get_context(), &da, offset, pval, + log2_size); + } + + execute_status do_write_device(pma_entry &pma, uint64_t mcycle, uint64_t offset, uint64_t val, int log2_size) { + device_state_access da(*this, mcycle); + return pma.get_device_noexcept().get_driver()->write(pma.get_device_noexcept().get_context(), &da, offset, val, + log2_size); + } + + template + bool do_translate_vaddr_via_tlb(uint64_t vaddr, unsigned char **phptr) { + const uint64_t eidx = tlb_get_entry_index(vaddr); + touch_page(tlb_get_entry_hot_abs_addr(eidx)); + touch_page(tlb_get_entry_cold_abs_addr(eidx)); + const tlb_hot_entry &tlbhe = m_m.get_state().tlb.hot[ETYPE][eidx]; + if (unlikely(!tlb_is_hit(tlbhe.vaddr_page, vaddr))) { + return false; + } + *phptr = cast_addr_to_ptr(tlbhe.vh_offset + vaddr); + const tlb_cold_entry &tlbce = m_m.get_state().tlb.cold[ETYPE][eidx]; + touch_page(tlbce.paddr_page); + return true; + } + + template + bool do_read_memory_word_via_tlb(uint64_t vaddr, T *pval) { + const uint64_t eidx = tlb_get_entry_index(vaddr); + const tlb_hot_entry &tlbhe = m_m.get_state().tlb.hot[ETYPE][eidx]; + const tlb_cold_entry &tlbce = m_m.get_state().tlb.cold[ETYPE][eidx]; + touch_page(tlb_get_entry_hot_abs_addr(eidx)); + touch_page(tlb_get_entry_cold_abs_addr( + eidx)); // save cold entry to allow reconstruction of paddr during playback + if (unlikely(!tlb_is_hit(tlbhe.vaddr_page, vaddr))) { + return false; + } + const auto *h = cast_addr_to_ptr(tlbhe.vh_offset + vaddr); + *pval = cartesi::aliased_aligned_read(h); + touch_page(tlbce.paddr_page); + + return true; + } + + template + bool do_write_memory_word_via_tlb(uint64_t vaddr, T val) { + const uint64_t eidx = tlb_get_entry_index(vaddr); + const tlb_hot_entry &tlbhe = m_m.get_state().tlb.hot[ETYPE][eidx]; + touch_page(tlb_get_entry_hot_abs_addr(eidx)); + touch_page(tlb_get_entry_cold_abs_addr(eidx)); + if (unlikely(!tlb_is_hit(tlbhe.vaddr_page, vaddr))) { + return false; + } + touch_page(tlb_get_entry_cold_abs_addr( + eidx)); // save cold entry to allow reconstruction of paddr during playback + const tlb_cold_entry &tlbce = m_m.get_state().tlb.cold[ETYPE][eidx]; + touch_page(tlbce.paddr_page); + + auto *h = cast_addr_to_ptr(tlbhe.vh_offset + vaddr); + aliased_aligned_write(h, val); + return true; + } + + template + unsigned char *do_replace_tlb_entry(uint64_t vaddr, uint64_t paddr, pma_entry &pma) { + const uint64_t eidx = tlb_get_entry_index(vaddr); + touch_page(tlb_get_entry_hot_abs_addr(eidx)); + touch_page(tlb_get_entry_cold_abs_addr( + eidx)); // save cold entry to allow reconstruction of paddr during playback + tlb_hot_entry &tlbhe = m_m.get_state().tlb.hot[ETYPE][eidx]; + tlb_cold_entry &tlbce = m_m.get_state().tlb.cold[ETYPE][eidx]; + // Mark page that was on TLB as dirty so we know to update the Merkle tree + if constexpr (ETYPE == TLB_WRITE) { + if (tlbhe.vaddr_page != TLB_INVALID_PAGE) { + pma_entry &pma = do_get_pma_entry(static_cast(tlbce.pma_index)); + pma.mark_dirty_page(tlbce.paddr_page - pma.get_start()); + } + } + const uint64_t vaddr_page = vaddr & ~PAGE_OFFSET_MASK; + const uint64_t paddr_page = paddr & ~PAGE_OFFSET_MASK; + unsigned char *hpage = pma.get_memory_noexcept().get_host_memory() + (paddr_page - pma.get_start()); + tlbhe.vaddr_page = vaddr_page; + tlbhe.vh_offset = cast_ptr_to_addr(hpage) - vaddr_page; + tlbce.paddr_page = paddr_page; + tlbce.pma_index = static_cast(pma.get_index()); + touch_page(tlbce.paddr_page); + return hpage; + } + + template + void do_flush_tlb_entry(uint64_t eidx) { + touch_page(tlb_get_entry_hot_abs_addr(eidx)); + touch_page(tlb_get_entry_cold_abs_addr( + eidx)); // save cold entry to allow reconstruction of paddr during playback + tlb_hot_entry &tlbhe = m_m.get_state().tlb.hot[ETYPE][eidx]; + // Mark page that was on TLB as dirty so we know to update the Merkle tree + if constexpr (ETYPE == TLB_WRITE) { + if (tlbhe.vaddr_page != TLB_INVALID_PAGE) { + tlbhe.vaddr_page = TLB_INVALID_PAGE; + const tlb_cold_entry &tlbce = m_m.get_state().tlb.cold[ETYPE][eidx]; + pma_entry &pma = do_get_pma_entry(static_cast(tlbce.pma_index)); + pma.mark_dirty_page(tlbce.paddr_page - pma.get_start()); + } else { + tlbhe.vaddr_page = TLB_INVALID_PAGE; + } + } else { + tlbhe.vaddr_page = TLB_INVALID_PAGE; + } + } + + template + void do_flush_tlb_type() { + for (uint64_t i = 0; i < PMA_TLB_SIZE; ++i) { + do_flush_tlb_entry(i); + } + } + + void do_flush_tlb_vaddr(uint64_t vaddr) { + (void) vaddr; + // We can't flush just one TLB entry for that specific virtual address, + // because megapages/gigapages may be in use while this TLB implementation ignores it, + // so we have to flush all addresses. + do_flush_tlb_type(); + do_flush_tlb_type(); + do_flush_tlb_type(); + } + + bool do_get_soft_yield() { + return m_m.get_state().soft_yield; + } +}; + +} // namespace cartesi + +#endif diff --git a/src/replay-step-state-access-interop.cpp b/src/replay-step-state-access-interop.cpp new file mode 100644 index 000000000..bcd300b3d --- /dev/null +++ b/src/replay-step-state-access-interop.cpp @@ -0,0 +1,42 @@ +// Copyright Cartesi and individual authors (see AUTHORS) +// SPDX-License-Identifier: LGPL-3.0-or-later +// +// This program is free software: you can redistribute it and/or modify it under +// the terms of the GNU Lesser General Public License as published by the Free +// Software Foundation, either version 3 of the License, or (at your option) any +// later version. +// +// This program is distributed in the hope that it will be useful, but WITHOUT ANY +// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A +// PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License along +// with this program (see COPYING). If not, see . +// + +#include "replay-step-state-access-interop.h" +#include "machine-merkle-tree.h" + +using namespace cartesi; + +static_assert(interop_log2_root_size == machine_merkle_tree::get_log2_root_size(), + "interop_log2_root_size must match machine_merkle_tree::get_log2_root_size()"); +static_assert(sizeof(cartesi::machine_merkle_tree::hash_type) == sizeof(std::remove_pointer_t), + "hash_type size mismatch"); + +extern "C" void interop_merkle_tree_hash(const unsigned char *data, size_t size, interop_hash_type hash) { + machine_merkle_tree::hasher_type hasher{}; + get_merkle_tree_hash(hasher, data, size, machine_merkle_tree::get_word_size(), + // NOLINTNEXTLINE(cppcoreguidelines-pro-type-reinterpret-cast) + *reinterpret_cast(hash)); +} + +extern "C" void interop_concat_hash(interop_const_hash_type left, interop_const_hash_type right, + interop_hash_type result) { + machine_merkle_tree::hasher_type hasher{}; + // NOLINTBEGIN(cppcoreguidelines-pro-type-reinterpret-cast) + get_concat_hash(hasher, *reinterpret_cast(left), + *reinterpret_cast(right), + *reinterpret_cast(result)); + // NOLINTEND(cppcoreguidelines-pro-type-reinterpret-cast) +} diff --git a/src/replay-step-state-access-interop.h b/src/replay-step-state-access-interop.h new file mode 100644 index 000000000..cb15b8f14 --- /dev/null +++ b/src/replay-step-state-access-interop.h @@ -0,0 +1,40 @@ +// Copyright Cartesi and individual authors (see AUTHORS) +// SPDX-License-Identifier: LGPL-3.0-or-later +// +// This program is free software: you can redistribute it and/or modify it under +// the terms of the GNU Lesser General Public License as published by the Free +// Software Foundation, either version 3 of the License, or (at your option) any +// later version. +// +// This program is distributed in the hope that it will be useful, but WITHOUT ANY +// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A +// PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License along +// with this program (see COPYING). If not, see . +// + +#ifndef REPLAY_STEP_STATE_ACCESS_INTEROP_H +#define REPLAY_STEP_STATE_ACCESS_INTEROP_H + +#include "compiler-defines.h" +#include +#include +#include + +const static uint64_t interop_log2_root_size = 64; +constexpr size_t interop_machine_hash_byte_size = 32; + +using interop_hash_type = unsigned char (*)[interop_machine_hash_byte_size]; +using interop_const_hash_type = const unsigned char (*)[interop_machine_hash_byte_size]; + +NO_RETURN inline void interop_throw_runtime_error(const char *msg) { + throw std::runtime_error(msg); +} + +extern "C" void interop_merkle_tree_hash(const unsigned char *data, size_t size, interop_hash_type hash); + +extern "C" void interop_concat_hash(interop_const_hash_type left, interop_const_hash_type right, + interop_hash_type result); + +#endif \ No newline at end of file diff --git a/src/replay-step-state-access.h b/src/replay-step-state-access.h new file mode 100644 index 000000000..eba94c37f --- /dev/null +++ b/src/replay-step-state-access.h @@ -0,0 +1,1099 @@ +// Copyright Cartesi and individual authors (see AUTHORS) +// SPDX-License-Identifier: LGPL-3.0-or-later +// +// This program is free software: you can redistribute it and/or modify it under +// the terms of the GNU Lesser General Public License as published by the Free +// Software Foundation, either version 3 of the License, or (at your option) any +// later version. +// +// This program is distributed in the hope that it will be useful, but WITHOUT ANY +// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A +// PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License along +// with this program (see COPYING). If not, see . +// + +#ifndef REPLAY_STEP_STATE_ACCESS_H +#define REPLAY_STEP_STATE_ACCESS_H + +#include +#include + +#include "clint.h" +#include "compiler-defines.h" +#include "device-state-access.h" +#include "htif.h" +#include "i-state-access.h" +#include "plic.h" +#include "pma-constants.h" +#include "replay-step-state-access-interop.h" +#include "riscv-constants.h" +#include "shadow-pmas.h" +#include "shadow-state.h" +#include "shadow-tlb.h" +#include "shadow-uarch-state.h" +#include "strict-aliasing.h" +#include "uarch-constants.h" +#include "uarch-defines.h" + +namespace cartesi { + +// \file this code is designed to be compiled for a free-standing environment. +// Environment-specific functions have the prefix "interop_" and are declared in "replay-step-state-access-interop.h" + +class mock_pma_entry final { +public: + struct flags { + bool M; + bool IO; + bool E; + bool R; + bool W; + bool X; + bool IR; + bool IW; + PMA_ISTART_DID DID; + }; + +private: + int m_pma_index; + uint64_t m_start; + uint64_t m_length; + flags m_flags; + const pma_driver *m_device_driver; + void *m_device_context; + +public: + mock_pma_entry(int pma_index, uint64_t start, uint64_t length, flags flags, const pma_driver *pma_driver = nullptr, + void *device_context = nullptr) : + m_pma_index{pma_index}, + m_start{start}, + m_length{length}, + m_flags{flags}, + m_device_driver{pma_driver}, + m_device_context{device_context} {} + + mock_pma_entry() : + mock_pma_entry(-1, 0, 0, {false, false, true, false, false, false, false, false, PMA_ISTART_DID{}}) { + ; + } + + int get_index() const { + return m_pma_index; + } + + flags get_flags() const { + return m_flags; + } + + uint64_t get_start() const { + return m_start; + } + + uint64_t get_length() const { + return m_length; + } + + bool get_istart_M() const { + return m_flags.M; + } + + bool get_istart_IO() const { + return m_flags.IO; + } + + bool get_istart_E() const { + return m_flags.E; + } + + bool get_istart_R() const { + return m_flags.R; + } + + bool get_istart_W() const { + return m_flags.W; + } + + bool get_istart_X() const { + return m_flags.X; + } + + bool get_istart_IR() const { + return m_flags.IR; + } + + const pma_driver *get_device_driver() { + return m_device_driver; + } + + void *get_device_context() { + return m_device_context; + } + + // NOLINTNEXTLINE(readability-convert-member-functions-to-static) + void mark_dirty_page(uint64_t address_in_range) { + (void) address_in_range; + // Dummy implementation. + } +}; + +// \brief checks if a buffer is large enough to hold a data block of N elements of size S starting at a given offset +// \param max The maximum offset allowed +// \param current The current offset +// \param elsize The size of each element +// \param elcount The number of elements +// \param next Receives the start offset of the next data block +// \return true if the buffer is large enough and data doesn't overflow, false otherwise +static inline bool validate_and_advance_offset(uint64_t max, uint64_t current, uint64_t elsize, uint64_t elcount, + uint64_t *next) { + uint64_t size{}; + if (__builtin_mul_overflow(elsize, elcount, &size)) { + return false; + } + if (__builtin_add_overflow(current, size, next)) { + return false; + } + return *next <= max; +} + +static_assert(sizeof(shadow_tlb_state::hot[0]) == PMA_PAGE_SIZE, "size of hot tlb cache must be PM_PAGE_SIZE bytes"); +static_assert(sizeof(shadow_tlb_state::cold[0]) == PMA_PAGE_SIZE, "size of cold tlb cache must be PM_PAGE_SIZE bytes"); +static_assert(sizeof(shadow_tlb_state::hot) + sizeof(shadow_tlb_state::cold) == sizeof(shadow_tlb_state), + "size of shadow tlb state"); + +// \brief Provides machine state from a step log file +class replay_step_state_access : public i_state_access { +public: + using hash_type = std::array; + static_assert(sizeof(hash_type) == interop_machine_hash_byte_size); + +private: + using address_type = uint64_t; + using data_type = unsigned char[PMA_PAGE_SIZE]; + + struct PACKED page_type { + address_type index; + data_type data; + hash_type hash; + }; + + uint64_t m_page_count{0}; ///< Number of pages in the step log + page_type *m_pages{nullptr}; ///< Array of page data + uint64_t m_sibling_count{0}; ///< Number of sibling hashes in the step log + hash_type *m_sibling_hashes{nullptr}; ///< Array of sibling hashes + std::array, PMA_MAX> m_pmas{}; ///< Array of PMA entries + +public: + // \brief Construct a replay_step_state_access object from a log image and expected initial root hash + // \param log_image Image of the step log file + // \param log_size The size of the log data + // \param root_hash_before The expected machine root hash before the replay + // \throw runtime_error if the initial root hash does not match or the log data is invalid + replay_step_state_access(unsigned char *log_image, uint64_t log_size, const hash_type &root_hash_before) { + // relevant offsets in the log data + uint64_t first_page_offset{}; + uint64_t first_siblng_offset{}; + uint64_t sibling_count_offset{}; + uint64_t end_offset{}; // end of the log data + + // ensure that log_step + size does not overflow + if (__builtin_add_overflow(cast_ptr_to_addr(log_image), log_size, &end_offset)) { + interop_throw_runtime_error("step log size overflow"); + } + + // set page count + if (!validate_and_advance_offset(log_size, 0, sizeof(m_page_count), 1, &first_page_offset)) { + interop_throw_runtime_error("page count past end of step log"); + } + memcpy(&m_page_count, log_image, sizeof(m_page_count)); + if (m_page_count == 0) { + interop_throw_runtime_error("page count is zero"); + } + + // set page data + if (!validate_and_advance_offset(log_size, first_page_offset, sizeof(page_type), m_page_count, + &sibling_count_offset)) { + interop_throw_runtime_error("page data past end of step log"); + } + // NOLINTNEXTLINE(cppcoreguidelines-pro-type-reinterpret-cast) + m_pages = reinterpret_cast(log_image + first_page_offset); + + // set sibling count and hashes + if (!validate_and_advance_offset(log_size, sibling_count_offset, sizeof(m_sibling_count), 1, + &first_siblng_offset)) { + interop_throw_runtime_error("sibling count past end of step log"); + } + memcpy(&m_sibling_count, log_image + sibling_count_offset, sizeof(m_sibling_count)); + + // set sibling hashes + if (!validate_and_advance_offset(log_size, first_siblng_offset, sizeof(hash_type), m_sibling_count, + &end_offset)) { + interop_throw_runtime_error("sibling hashes past end of step log"); + } + // NOLINTNEXTLINE(cppcoreguidelines-pro-type-reinterpret-cast) + m_sibling_hashes = reinterpret_cast(log_image + first_siblng_offset); + + // ensure that we read exactly the expected log size + if (end_offset != log_size) { + interop_throw_runtime_error("extra data at end of step log"); + } + + // ensure that the page indexes are in increasing order + // and that the scratch hash area is all zeros + static const hash_type all_zeros{}; + for (uint64_t i = 0; i < m_page_count; i++) { + if (i > 0 && m_pages[i - 1].index >= m_pages[i].index) { + interop_throw_runtime_error("invalid log format: page index is not in increasing order"); + } + if (m_pages[i].hash != all_zeros) { + interop_throw_runtime_error("invalid log format: page scratch hash area is not zero"); + } + } + + // compute and check the machine root hash before the replay + auto computed_root_hash_before = compute_root_hash(); + if (computed_root_hash_before != root_hash_before) { + interop_throw_runtime_error("initial root hash mismatch"); + } + // relocate all tlb vh offsets into the logged page data + relocate_all_tlb_vh_offset(); + relocate_all_tlb_vh_offset(); + relocate_all_tlb_vh_offset(); + } + + replay_step_state_access(const replay_step_state_access &) = delete; + replay_step_state_access(replay_step_state_access &&) = delete; + replay_step_state_access &operator=(const replay_step_state_access &) = delete; + replay_step_state_access &operator=(replay_step_state_access &&) = delete; + ~replay_step_state_access() = default; + + // \brief Finish the replay and check the final machine root hash + // \param final_root_hash The expected final machine root hash + // \throw runtime_error if the final root hash does not match + void finish(const hash_type &root_hash_after) { + // reset all tlb vh offsets to zero + // this is to mimic peek behavior of tlb pma device + reset_all_tlb_vh_offset(); + reset_all_tlb_vh_offset(); + reset_all_tlb_vh_offset(); + // compute and check machine root hash after the replay + auto computed_final_root_hash = compute_root_hash(); + if (computed_final_root_hash != root_hash_after) { + interop_throw_runtime_error("final root hash mismatch"); + } + } + +private: + friend i_state_access; + + // \brief Relocate all TLB virtual to host offsets + // \details Points the vh_offset relative to the logged page data + template + void relocate_all_tlb_vh_offset() { + // NOLINTBEGIN(cppcoreguidelines-pro-type-reinterpret-cast)) + for (size_t i = 0; i < PMA_TLB_SIZE; ++i) { + const auto he_address = tlb_get_entry_hot_abs_addr(i); + const auto he_page = he_address & ~(PMA_PAGE_SIZE - 1); + const auto he_offset = he_address - he_page; + auto *he_log = try_find_page(he_page); + // if the page containing the tlb hot entries is present in the log + if (he_log) { + volatile tlb_hot_entry *tlbhe = reinterpret_cast(he_log->data + he_offset); + if (tlbhe->vh_offset != 0) { + interop_throw_runtime_error("expected vh_offset to be zero"); + } + // find the logged cold entry page + const auto ce_addr = tlb_get_entry_cold_abs_addr(i); + const auto ce_page = ce_addr & ~(PMA_PAGE_SIZE - 1); + const auto ce_offset = ce_addr - ce_page; + auto *ce_log = find_page(ce_page); + volatile tlb_cold_entry *tlbce = reinterpret_cast(ce_log->data + ce_offset); + // find the logged page pointed by the cold entry + auto *log = try_find_page(tlbce->paddr_page); + if (log) { + // point vh_offset to the logged page data + tlbhe->vh_offset = cast_ptr_to_addr(log->data) - tlbhe->vaddr_page; + } + } + } + // NOLINTEND(cppcoreguidelines-pro-type-reinterpret-cast)) + } + + // \brief Reset all TLB virtual to host offsets + // \details Points the vh_offset to zero, replicating the behavior of the tlb pma device + template + void reset_all_tlb_vh_offset() { + // NOLINTBEGIN(cppcoreguidelines-pro-type-reinterpret-cast)) + for (size_t i = 0; i < PMA_TLB_SIZE; ++i) { + const auto addr = tlb_get_entry_hot_abs_addr(i); + const auto page = addr & ~(PMA_PAGE_SIZE - 1); + const auto offset = addr - page; + auto *p = try_find_page(page); + if (p) { + volatile tlb_hot_entry *tlbhe = reinterpret_cast(p->data + offset); + tlbhe->vh_offset = 0; + } + } + // NOLINTEND(cppcoreguidelines-pro-type-reinterpret-cast)) + } + + /// \brief Try to find a page in the logged data + /// \param address The physical address of the page + /// \return A pointer to the page_type structure if found, nullptr otherwise + page_type *try_find_page(uint64_t address) const { + const auto page_index = address >> PMA_PAGE_SIZE_LOG2; + uint64_t min{0}; + uint64_t max{m_page_count}; + while (min < max) { + auto mid = (min + max) >> 1; + if (m_pages[mid].index == page_index) { + return &m_pages[mid]; + } + if (m_pages[mid].index < page_index) { + min = mid + 1; + } else { + max = mid; + } + } + return nullptr; + } + + /// \brief Find a page in the logged data + /// \param address The physical address of the page + /// \return A pointer to the page_type structure + page_type *find_page(uint64_t address) const { + auto *page = try_find_page(address); + if (page == nullptr) { + interop_throw_runtime_error("find_page: page not found"); + } + return page; + } + + /// \brief Find a page in the logged data + /// \param address The physical address of the page + /// \return A pointer to the page_type structure + page_type *find_page(uint64_t address) { + // NOLINTNEXTLINE(cppcoreguidelines-pro-type-const-cast): remove const to reuse code + return static_cast(this)->find_page(address); + } + + /// \brief Get the raw memory pointer for a given physical address + /// \param paddr The physical address + /// \param size The size of the memory region + /// \return A pointer to the raw memory + void *get_raw_memory_pointer(uint64_t paddr, size_t size) const { + auto page = paddr & ~(PMA_PAGE_SIZE - 1); + const auto offset = paddr - page; + auto end_page = (paddr + size - 1) & ~(PMA_PAGE_SIZE - 1); + if (end_page != page) { + interop_throw_runtime_error("get_raw_memory_pointer: paddr crosses page boundary"); + } + auto *data = find_page(page); + auto *p = data->data + offset; + return p; + } + + /// \brief Read a value from raw memory + /// \tparam T The type of the value to read + /// \param paddr The physical address + /// \return The value read + template + T raw_read_memory(uint64_t paddr) const { + auto size = sizeof(T); + // NOLINTNEXTLINE(cppcoreguidelines-pro-type-reinterpret-cast) + volatile T *ptr = reinterpret_cast(get_raw_memory_pointer(paddr, size)); + return *ptr; + } + + /// \brief Write a value to raw memory + /// \tparam T The type of the value to write + /// \param paddr The physical address + /// \param val The value to write + template + void raw_write_memory(uint64_t paddr, T val) { + auto size = sizeof(T); + // NOLINTNEXTLINE(cppcoreguidelines-pro-type-reinterpret-cast) + volatile T *ptr = reinterpret_cast(get_raw_memory_pointer(paddr, size)); + *ptr = val; + } + + // \brief Compute the current machine root hash + hash_type compute_root_hash() { + for (uint64_t i = 0; i < m_page_count; i++) { + interop_merkle_tree_hash(m_pages[i].data, PMA_PAGE_SIZE, + // NOLINTNEXTLINE(cppcoreguidelines-pro-type-reinterpret-cast) + reinterpret_cast(&m_pages[i].hash)); + } + + size_t next_page = 0; + size_t next_sibling = 0; + auto root_hash = + compute_root_hash_impl(0, interop_log2_root_size - PMA_PAGE_SIZE_LOG2, next_page, next_sibling); + if (next_page != m_page_count) { + interop_throw_runtime_error("compute_root_hash: next_page != m_page_count"); + } + if (next_sibling != m_sibling_count) { + interop_throw_runtime_error("compute_root_hash: sibling hashes not totally consumed"); + } + return root_hash; + } + + // \brief Compute the root hash of a memory range recursively + // \param page_index Index of the first page in the range + // \param page_count_log2_size Log2 of the size of number of pages in the range + // \param next_page Index of the next page to be visited + // \param next_sibling Index of the next sibling hash to be visited + // \return Resulting root hash of the range + hash_type compute_root_hash_impl(address_type page_index, int page_count_log2_size, size_t &next_page, + size_t &next_sibling) { + // NOLINTBEGIN(cppcoreguidelines-pro-type-reinterpret-cast)) + auto page_count = UINT64_C(1) << page_count_log2_size; + if (next_page >= m_page_count || page_index + page_count <= m_pages[next_page].index) { + if (next_sibling >= m_sibling_count) { + interop_throw_runtime_error( + "compute_root_hash_impl: trying to access beyond sibling count while skipping range"); + } + return m_sibling_hashes[next_sibling++]; + } + if (page_count_log2_size > 0) { + auto left = compute_root_hash_impl(page_index, page_count_log2_size - 1, next_page, next_sibling); + auto right = compute_root_hash_impl(page_index + (UINT64_C(1) << (page_count_log2_size - 1)), + page_count_log2_size - 1, next_page, next_sibling); + hash_type hash{}; + interop_concat_hash(reinterpret_cast(&left), + reinterpret_cast(&right), reinterpret_cast(&hash)); + return hash; + } + if (m_pages[next_page].index == page_index) { + return m_pages[next_page++].hash; + } + if (next_sibling >= m_sibling_count) { + interop_throw_runtime_error("compute_root_hash_impl: trying to access beyond sibling count"); + } + return m_sibling_hashes[next_sibling++]; + // NOLINTEND(cppcoreguidelines-pro-type-reinterpret-cast)) + } + + // NOLINTNEXTLINE(readability-convert-member-functions-to-static) + void do_push_bracket(bracket_type type, const char *text) { + (void) type; + (void) text; + } + + int do_make_scoped_note(const char *text) { // NOLINT(readability-convert-member-functions-to-static) + (void) text; + return 0; + } + + uint64_t do_read_x(int reg) { + return raw_read_memory(shadow_state_get_x_abs_addr(reg)); + } + + void do_write_x(int reg, uint64_t val) { + raw_write_memory(shadow_state_get_x_abs_addr(reg), val); + } + + uint64_t do_read_f(int reg) { + return raw_read_memory(shadow_state_get_f_abs_addr(reg)); + } + + void do_write_f(int reg, uint64_t val) { + raw_write_memory(shadow_state_get_f_abs_addr(reg), val); + } + + uint64_t do_read_pc() { + return raw_read_memory(shadow_state_get_reg_abs_addr(shadow_state_reg::pc)); + } + + void do_write_pc(uint64_t val) { + raw_write_memory(shadow_state_get_reg_abs_addr(shadow_state_reg::pc), val); + } + + uint64_t do_read_fcsr() { + return raw_read_memory(shadow_state_get_reg_abs_addr(shadow_state_reg::fcsr)); + } + + void do_write_fcsr(uint64_t val) { + raw_write_memory(shadow_state_get_reg_abs_addr(shadow_state_reg::fcsr), val); + } + + uint64_t do_read_icycleinstret() { + return raw_read_memory(shadow_state_get_reg_abs_addr(shadow_state_reg::icycleinstret)); + } + + void do_write_icycleinstret(uint64_t val) { + raw_write_memory(shadow_state_get_reg_abs_addr(shadow_state_reg::icycleinstret), val); + } + + uint64_t do_read_mvendorid() { + return raw_read_memory(shadow_state_get_reg_abs_addr(shadow_state_reg::mvendorid)); + } + + uint64_t do_read_marchid() { + return raw_read_memory(shadow_state_get_reg_abs_addr(shadow_state_reg::marchid)); + } + + uint64_t do_read_mimpid() { + return raw_read_memory(shadow_state_get_reg_abs_addr(shadow_state_reg::mimpid)); + } + + uint64_t do_read_mcycle() { + return raw_read_memory(shadow_state_get_reg_abs_addr(shadow_state_reg::mcycle)); + } + + void do_write_mcycle(uint64_t val) { + raw_write_memory(shadow_state_get_reg_abs_addr(shadow_state_reg::mcycle), val); + } + + uint64_t do_read_mstatus() { + return raw_read_memory(shadow_state_get_reg_abs_addr(shadow_state_reg::mstatus)); + } + + void do_write_mstatus(uint64_t val) { + raw_write_memory(shadow_state_get_reg_abs_addr(shadow_state_reg::mstatus), val); + } + + uint64_t do_read_mtvec() { + return raw_read_memory(shadow_state_get_reg_abs_addr(shadow_state_reg::mtvec)); + } + + void do_write_mtvec(uint64_t val) { + raw_write_memory(shadow_state_get_reg_abs_addr(shadow_state_reg::mtvec), val); + } + + uint64_t do_read_mscratch() { + return raw_read_memory(shadow_state_get_reg_abs_addr(shadow_state_reg::mscratch)); + } + + void do_write_mscratch(uint64_t val) { + raw_write_memory(shadow_state_get_reg_abs_addr(shadow_state_reg::mscratch), val); + } + + uint64_t do_read_mepc() { + return raw_read_memory(shadow_state_get_reg_abs_addr(shadow_state_reg::mepc)); + } + + void do_write_mepc(uint64_t val) { + raw_write_memory(shadow_state_get_reg_abs_addr(shadow_state_reg::mepc), val); + } + + uint64_t do_read_mcause() { + return raw_read_memory(shadow_state_get_reg_abs_addr(shadow_state_reg::mcause)); + } + + void do_write_mcause(uint64_t val) { + raw_write_memory(shadow_state_get_reg_abs_addr(shadow_state_reg::mcause), val); + } + + uint64_t do_read_mtval() { + return raw_read_memory(shadow_state_get_reg_abs_addr(shadow_state_reg::mtval)); + } + + void do_write_mtval(uint64_t val) { + raw_write_memory(shadow_state_get_reg_abs_addr(shadow_state_reg::mtval), val); + } + + uint64_t do_read_misa() { + return raw_read_memory(shadow_state_get_reg_abs_addr(shadow_state_reg::misa)); + } + + void do_write_misa(uint64_t val) { + raw_write_memory(shadow_state_get_reg_abs_addr(shadow_state_reg::misa), val); + } + + uint64_t do_read_mie() { + return raw_read_memory(shadow_state_get_reg_abs_addr(shadow_state_reg::mie)); + } + + void do_write_mie(uint64_t val) { + raw_write_memory(shadow_state_get_reg_abs_addr(shadow_state_reg::mie), val); + } + + uint64_t do_read_mip() { + return raw_read_memory(shadow_state_get_reg_abs_addr(shadow_state_reg::mip)); + } + + void do_write_mip(uint64_t val) { + raw_write_memory(shadow_state_get_reg_abs_addr(shadow_state_reg::mip), val); + } + + uint64_t do_read_medeleg() { + return raw_read_memory(shadow_state_get_reg_abs_addr(shadow_state_reg::medeleg)); + } + + void do_write_medeleg(uint64_t val) { + raw_write_memory(shadow_state_get_reg_abs_addr(shadow_state_reg::medeleg), val); + } + + uint64_t do_read_mideleg() { + return raw_read_memory(shadow_state_get_reg_abs_addr(shadow_state_reg::mideleg)); + } + + void do_write_mideleg(uint64_t val) { + raw_write_memory(shadow_state_get_reg_abs_addr(shadow_state_reg::mideleg), val); + } + + uint64_t do_read_mcounteren() { + return raw_read_memory(shadow_state_get_reg_abs_addr(shadow_state_reg::mcounteren)); + } + + void do_write_mcounteren(uint64_t val) { + raw_write_memory(shadow_state_get_reg_abs_addr(shadow_state_reg::mcounteren), val); + } + + uint64_t do_read_senvcfg() const { + return raw_read_memory(shadow_state_get_reg_abs_addr(shadow_state_reg::senvcfg)); + } + + void do_write_senvcfg(uint64_t val) { + raw_write_memory(shadow_state_get_reg_abs_addr(shadow_state_reg::senvcfg), val); + } + + uint64_t do_read_menvcfg() const { + return raw_read_memory(shadow_state_get_reg_abs_addr(shadow_state_reg::menvcfg)); + } + + void do_write_menvcfg(uint64_t val) { + raw_write_memory(shadow_state_get_reg_abs_addr(shadow_state_reg::menvcfg), val); + } + + uint64_t do_read_stvec() { + return raw_read_memory(shadow_state_get_reg_abs_addr(shadow_state_reg::stvec)); + } + + void do_write_stvec(uint64_t val) { + raw_write_memory(shadow_state_get_reg_abs_addr(shadow_state_reg::stvec), val); + } + + uint64_t do_read_sscratch() { + return raw_read_memory(shadow_state_get_reg_abs_addr(shadow_state_reg::sscratch)); + } + + void do_write_sscratch(uint64_t val) { + raw_write_memory(shadow_state_get_reg_abs_addr(shadow_state_reg::sscratch), val); + } + + uint64_t do_read_sepc() { + return raw_read_memory(shadow_state_get_reg_abs_addr(shadow_state_reg::sepc)); + } + + void do_write_sepc(uint64_t val) { + raw_write_memory(shadow_state_get_reg_abs_addr(shadow_state_reg::sepc), val); + } + + uint64_t do_read_scause() { + return raw_read_memory(shadow_state_get_reg_abs_addr(shadow_state_reg::scause)); + } + + void do_write_scause(uint64_t val) { + raw_write_memory(shadow_state_get_reg_abs_addr(shadow_state_reg::scause), val); + } + + uint64_t do_read_stval() { + return raw_read_memory(shadow_state_get_reg_abs_addr(shadow_state_reg::stval)); + } + + void do_write_stval(uint64_t val) { + raw_write_memory(shadow_state_get_reg_abs_addr(shadow_state_reg::stval), val); + } + + uint64_t do_read_satp() { + return raw_read_memory(shadow_state_get_reg_abs_addr(shadow_state_reg::satp)); + } + + void do_write_satp(uint64_t val) { + raw_write_memory(shadow_state_get_reg_abs_addr(shadow_state_reg::satp), val); + } + + uint64_t do_read_scounteren() { + return raw_read_memory(shadow_state_get_reg_abs_addr(shadow_state_reg::scounteren)); + } + + void do_write_scounteren(uint64_t val) { + raw_write_memory(shadow_state_get_reg_abs_addr(shadow_state_reg::scounteren), val); + } + + uint64_t do_read_ilrsc() { + return raw_read_memory(shadow_state_get_reg_abs_addr(shadow_state_reg::ilrsc)); + } + + void do_write_ilrsc(uint64_t val) { + raw_write_memory(shadow_state_get_reg_abs_addr(shadow_state_reg::ilrsc), val); + } + + uint64_t do_read_iflags() { + return raw_read_memory(shadow_state_get_reg_abs_addr(shadow_state_reg::iflags)); + } + + void do_write_iflags(uint64_t val) { + raw_write_memory(shadow_state_get_reg_abs_addr(shadow_state_reg::iflags), val); + } + + void do_set_iflags_H() { + auto old_iflags = read_iflags(); + auto new_iflags = old_iflags | IFLAGS_H_MASK; + write_iflags(new_iflags); + } + + bool do_read_iflags_H() { + auto iflags = read_iflags(); + return (iflags & IFLAGS_H_MASK) != 0; + } + + void do_set_iflags_X() { + auto old_iflags = read_iflags(); + auto new_iflags = old_iflags | IFLAGS_X_MASK; + write_iflags(new_iflags); + } + + void do_reset_iflags_X() { + auto old_iflags = read_iflags(); + auto new_iflags = old_iflags & (~IFLAGS_X_MASK); + write_iflags(new_iflags); + } + + bool do_read_iflags_X() { + auto iflags = read_iflags(); + return (iflags & IFLAGS_X_MASK) != 0; + } + + void do_set_iflags_Y() { + auto old_iflags = read_iflags(); + auto new_iflags = old_iflags | IFLAGS_Y_MASK; + write_iflags(new_iflags); + } + + void do_reset_iflags_Y() { + auto old_iflags = read_iflags(); + auto new_iflags = old_iflags & (~IFLAGS_Y_MASK); + write_iflags(new_iflags); + } + + bool do_read_iflags_Y() { + auto iflags = read_iflags(); + return (iflags & IFLAGS_Y_MASK) != 0; + } + + uint8_t do_read_iflags_PRV() { + auto iflags = read_iflags(); + return (iflags & IFLAGS_PRV_MASK) >> IFLAGS_PRV_SHIFT; + } + + void do_write_iflags_PRV(uint8_t val) { + auto old_iflags = read_iflags(); + auto new_iflags = + (old_iflags & (~IFLAGS_PRV_MASK)) | ((static_cast(val) << IFLAGS_PRV_SHIFT) & IFLAGS_PRV_MASK); + write_iflags(new_iflags); + } + + uint64_t do_read_iunrep() { + return raw_read_memory(shadow_state_get_reg_abs_addr(shadow_state_reg::iunrep)); + } + + void do_write_iunrep(uint64_t val) { + raw_write_memory(shadow_state_get_reg_abs_addr(shadow_state_reg::iunrep), val); + } + + uint64_t do_read_clint_mtimecmp() { + return raw_read_memory(shadow_state_get_reg_abs_addr(shadow_state_reg::clint_mtimecmp)); + } + + void do_write_clint_mtimecmp(uint64_t val) { + raw_write_memory(shadow_state_get_reg_abs_addr(shadow_state_reg::clint_mtimecmp), val); + } + + uint64_t do_read_plic_girqpend() { + return raw_read_memory(shadow_state_get_reg_abs_addr(shadow_state_reg::plic_girqpend)); + } + + void do_write_plic_girqpend(uint64_t val) { + raw_write_memory(shadow_state_get_reg_abs_addr(shadow_state_reg::plic_girqpend), val); + } + + uint64_t do_read_plic_girqsrvd() { + return raw_read_memory(shadow_state_get_reg_abs_addr(shadow_state_reg::plic_girqsrvd)); + } + + void do_write_plic_girqsrvd(uint64_t val) { + raw_write_memory(shadow_state_get_reg_abs_addr(shadow_state_reg::plic_girqsrvd), val); + } + + uint64_t do_read_htif_fromhost() { + return raw_read_memory(shadow_state_get_reg_abs_addr(shadow_state_reg::htif_fromhost)); + } + + void do_write_htif_fromhost(uint64_t val) { + raw_write_memory(shadow_state_get_reg_abs_addr(shadow_state_reg::htif_fromhost), val); + } + + uint64_t do_read_htif_tohost() { + return raw_read_memory(shadow_state_get_reg_abs_addr(shadow_state_reg::htif_tohost)); + } + + void do_write_htif_tohost(uint64_t val) { + raw_write_memory(shadow_state_get_reg_abs_addr(shadow_state_reg::htif_tohost), val); + } + + uint64_t do_read_htif_ihalt() { + return raw_read_memory(shadow_state_get_reg_abs_addr(shadow_state_reg::htif_ihalt)); + } + + uint64_t do_read_htif_iconsole() { + return raw_read_memory(shadow_state_get_reg_abs_addr(shadow_state_reg::htif_iconsole)); + } + + uint64_t do_read_htif_iyield() { + return raw_read_memory(shadow_state_get_reg_abs_addr(shadow_state_reg::htif_iyield)); + } + + // NOLINTNEXTLINE(readability-convert-member-functions-to-static) + std::pair do_poll_external_interrupts(uint64_t mcycle, uint64_t mcycle_max) { + (void) mcycle_max; + return {mcycle, false}; + } + + uint64_t do_read_pma_istart(int i) { + return raw_read_memory(shadow_pmas_get_pma_abs_addr(i)); + } + + uint64_t do_read_pma_ilength(int i) { + return raw_read_memory(shadow_pmas_get_pma_abs_addr(i) + sizeof(uint64_t)); + } + + template + void do_read_memory_word(uint64_t paddr, const unsigned char *hpage, uint64_t hoffset, T *pval) { + (void) hpage; + (void) hoffset; + *pval = raw_read_memory(paddr); + } + + // NOLINTNEXTLINE(readability-convert-member-functions-to-static) + bool do_read_memory(uint64_t paddr, const unsigned char *data, uint64_t length) { + (void) paddr; + (void) data; + (void) length; + return false; + } + + // NOLINTNEXTLINE(readability-convert-member-functions-to-static) + bool do_write_memory(uint64_t paddr, const unsigned char *data, uint64_t length) { + (void) paddr; + (void) data; + (void) length; + return false; + } + + template + void do_write_memory_word(uint64_t paddr, const unsigned char *hpage, uint64_t hoffset, T val) { + (void) hpage; + (void) hoffset; + raw_write_memory(paddr, val); + } + + template + mock_pma_entry &do_find_pma_entry(uint64_t paddr) { + for (size_t i = 0; i < m_pmas.size(); i++) { + auto &pma = get_pma_entry(static_cast(i)); + if (pma.get_istart_E()) { + return pma; + } + if (paddr >= pma.get_start() && paddr - pma.get_start() <= pma.get_length() - sizeof(T)) { + return pma; + } + } + interop_throw_runtime_error("do_find_pma_entry failed to find address"); + } + + mock_pma_entry &do_get_pma_entry(int index) { + const uint64_t istart = read_pma_istart(index); + const uint64_t ilength = read_pma_ilength(index); + if (!m_pmas[index]) { + m_pmas[index] = build_mock_pma_entry(index, istart, ilength); + } + // NOLINTNEXTLINE(bugprone-unchecked-optional-access) + return m_pmas[index].value(); + } + + unsigned char *do_get_host_memory(mock_pma_entry &pma) { // NOLINT(readability-convert-member-functions-to-static) + (void) pma; + return nullptr; + } + + bool do_read_device(mock_pma_entry &pma, uint64_t mcycle, uint64_t offset, uint64_t *pval, int log2_size) { + device_state_access da(*this, mcycle); + return pma.get_device_driver()->read(pma.get_device_context(), &da, offset, pval, log2_size); + } + + execute_status do_write_device(mock_pma_entry &pma, uint64_t mcycle, uint64_t offset, uint64_t val, int log2_size) { + device_state_access da(*this, mcycle); + return pma.get_device_driver()->write(pma.get_device_context(), &da, offset, val, log2_size); + } + + // NOLINTNEXTLINE(readability-convert-member-functions-to-static) + mock_pma_entry build_mock_pma_entry(int index, uint64_t istart, uint64_t ilength) { + uint64_t start{}; + mock_pma_entry::flags flags{}; + split_istart(istart, start, flags); + const pma_driver *driver = nullptr; + void *device_ctx = nullptr; + if (flags.IO) { + switch (flags.DID) { + case PMA_ISTART_DID::shadow_state: + driver = &shadow_state_driver; + break; + case PMA_ISTART_DID::shadow_pmas: + driver = &shadow_pmas_driver; + break; + case PMA_ISTART_DID::shadow_TLB: + driver = &shadow_tlb_driver; + break; + case PMA_ISTART_DID::CLINT: + driver = &clint_driver; + break; + case PMA_ISTART_DID::PLIC: + driver = &plic_driver; + break; + case PMA_ISTART_DID::HTIF: + driver = &htif_driver; + break; + default: + interop_throw_runtime_error("Unsupported device in build_mock_pma_entry"); + break; + } + } + return mock_pma_entry{index, start, ilength, flags, driver, device_ctx}; + } + + static constexpr void split_istart(uint64_t istart, uint64_t &start, mock_pma_entry::flags &f) { + f.M = ((istart & PMA_ISTART_M_MASK) >> PMA_ISTART_M_SHIFT) != 0; + f.IO = ((istart & PMA_ISTART_IO_MASK) >> PMA_ISTART_IO_SHIFT) != 0; + f.E = ((istart & PMA_ISTART_E_MASK) >> PMA_ISTART_E_SHIFT) != 0; + f.R = ((istart & PMA_ISTART_R_MASK) >> PMA_ISTART_R_SHIFT) != 0; + f.W = ((istart & PMA_ISTART_W_MASK) >> PMA_ISTART_W_SHIFT) != 0; + f.X = ((istart & PMA_ISTART_X_MASK) >> PMA_ISTART_X_SHIFT) != 0; + f.IR = ((istart & PMA_ISTART_IR_MASK) >> PMA_ISTART_IR_SHIFT) != 0; + f.IW = ((istart & PMA_ISTART_IW_MASK) >> PMA_ISTART_IW_SHIFT) != 0; + f.DID = static_cast((istart & PMA_ISTART_DID_MASK) >> PMA_ISTART_DID_SHIFT); + start = istart & PMA_ISTART_START_MASK; + } + + template + volatile tlb_hot_entry &do_get_tlb_hot_entry(uint64_t eidx) { + auto addr = tlb_get_entry_hot_abs_addr(eidx); + auto size = sizeof(tlb_hot_entry); + // NOLINTNEXTLINE(cppcoreguidelines-pro-type-reinterpret-cast) + volatile tlb_hot_entry *tlbe = reinterpret_cast(get_raw_memory_pointer(addr, size)); + return *tlbe; + } + + template + volatile tlb_cold_entry &do_get_tlb_entry_cold(uint64_t eidx) { + auto addr = tlb_get_entry_cold_abs_addr(eidx); + auto size = sizeof(tlb_cold_entry); + // NOLINTNEXTLINE(cppcoreguidelines-pro-type-reinterpret-cast) + volatile tlb_cold_entry *tlbe = reinterpret_cast(get_raw_memory_pointer(addr, size)); + return *tlbe; + } + + template + bool do_translate_vaddr_via_tlb(uint64_t vaddr, unsigned char **phptr) { + const uint64_t eidx = tlb_get_entry_index(vaddr); + const volatile tlb_hot_entry &tlbhe = do_get_tlb_hot_entry(eidx); + if (tlb_is_hit(tlbhe.vaddr_page, vaddr)) { + *phptr = cast_addr_to_ptr(tlbhe.vh_offset + vaddr); + return true; + } + return false; + } + + template + bool do_read_memory_word_via_tlb(uint64_t vaddr, T *pval) { + const uint64_t eidx = tlb_get_entry_index(vaddr); + const volatile tlb_hot_entry &tlbhe = do_get_tlb_hot_entry(eidx); + if (tlb_is_hit(tlbhe.vaddr_page, vaddr)) { + const uint64_t poffset = vaddr & PAGE_OFFSET_MASK; + const volatile tlb_cold_entry &tlbce = do_get_tlb_entry_cold(eidx); + *pval = raw_read_memory(tlbce.paddr_page + poffset); + return true; + } + return false; + } + + template + bool do_write_memory_word_via_tlb(uint64_t vaddr, T val) { + const uint64_t eidx = tlb_get_entry_index(vaddr); + const volatile tlb_hot_entry &tlbhe = do_get_tlb_hot_entry(eidx); + if (tlb_is_hit(tlbhe.vaddr_page, vaddr)) { + const uint64_t poffset = vaddr & PAGE_OFFSET_MASK; + const volatile tlb_cold_entry &tlbce = do_get_tlb_entry_cold(eidx); + raw_write_memory(tlbce.paddr_page + poffset, val); + return true; + } + return false; + } + + template + unsigned char *do_replace_tlb_entry(uint64_t vaddr, uint64_t paddr, mock_pma_entry &pma) { + const uint64_t eidx = tlb_get_entry_index(vaddr); + volatile tlb_hot_entry &tlbhe = do_get_tlb_hot_entry(eidx); + volatile tlb_cold_entry &tlbce = do_get_tlb_entry_cold(eidx); + if constexpr (ETYPE == TLB_WRITE) { + if (tlbhe.vaddr_page != TLB_INVALID_PAGE) { + mock_pma_entry &pma = do_get_pma_entry(static_cast(tlbce.pma_index)); + pma.mark_dirty_page(tlbce.paddr_page - pma.get_start()); + } + } + const uint64_t vaddr_page = vaddr & ~PAGE_OFFSET_MASK; + const uint64_t paddr_page = paddr & ~PAGE_OFFSET_MASK; + + auto *page_type = find_page(paddr_page); + auto *hpage = page_type->data; + + tlbhe.vaddr_page = vaddr_page; + tlbhe.vh_offset = cast_ptr_to_addr(hpage) - vaddr_page; + tlbce.paddr_page = paddr_page; + tlbce.pma_index = static_cast(pma.get_index()); + return hpage; + } + + template + void do_flush_tlb_entry(uint64_t eidx) { + volatile tlb_hot_entry &tlbhe = do_get_tlb_hot_entry(eidx); + // Mark page that was on TLB as dirty so we know to update the Merkle tree + if constexpr (ETYPE == TLB_WRITE) { + if (tlbhe.vaddr_page != TLB_INVALID_PAGE) { + tlbhe.vaddr_page = TLB_INVALID_PAGE; + const volatile tlb_cold_entry &tlbce = do_get_tlb_entry_cold(eidx); + mock_pma_entry &pma = do_get_pma_entry(static_cast(tlbce.pma_index)); + pma.mark_dirty_page(tlbce.paddr_page - pma.get_start()); + } else { + tlbhe.vaddr_page = TLB_INVALID_PAGE; + } + } else { + tlbhe.vaddr_page = TLB_INVALID_PAGE; + } + } + + template + void do_flush_tlb_type() { + for (uint64_t i = 0; i < PMA_TLB_SIZE; ++i) { + do_flush_tlb_entry(i); + } + } + + void do_flush_tlb_vaddr(uint64_t vaddr) { + (void) vaddr; + do_flush_tlb_type(); + do_flush_tlb_type(); + do_flush_tlb_type(); + } + + bool do_get_soft_yield() { // NOLINT(readability-convert-member-functions-to-static) + return false; + } +}; + +} // namespace cartesi + +#endif diff --git a/src/virtual-machine.cpp b/src/virtual-machine.cpp index 9653a0484..91a32bb60 100644 --- a/src/virtual-machine.cpp +++ b/src/virtual-machine.cpp @@ -73,6 +73,10 @@ interpreter_break_reason virtual_machine::do_run(uint64_t mcycle_end) { return get_machine()->run(mcycle_end); } +interpreter_break_reason virtual_machine::do_log_step(uint64_t mcycle_count, const std::string &filename) { + return m_machine->log_step(mcycle_count, filename); +} + access_log virtual_machine::do_log_step_uarch(const access_log::type &log_type) { return get_machine()->log_step_uarch(log_type); } @@ -179,6 +183,11 @@ machine_config virtual_machine::do_get_default_config() const { return machine::get_default_config(); } +void virtual_machine::do_verify_step(const hash_type &root_hash_before, const std::string &log_filename, + uint64_t mcycle_count, const hash_type &root_hash_after) const { + machine::verify_step(root_hash_before, log_filename, mcycle_count, root_hash_after); +} + void virtual_machine::do_verify_step_uarch(const hash_type &root_hash_before, const access_log &log, const hash_type &root_hash_after) const { machine::verify_step_uarch(root_hash_before, log, root_hash_after); diff --git a/src/virtual-machine.h b/src/virtual-machine.h index b1e36ed5a..a228c5595 100644 --- a/src/virtual-machine.h +++ b/src/virtual-machine.h @@ -49,6 +49,7 @@ class virtual_machine : public i_virtual_machine { void do_create(const machine_config &config, const machine_runtime_config &runtime) override; void do_load(const std::string &directory, const machine_runtime_config &runtime) override; interpreter_break_reason do_run(uint64_t mcycle_end) override; + interpreter_break_reason do_log_step(uint64_t mcycle_count, const std::string &filename) override; void do_store(const std::string &directory) const override; access_log do_log_step_uarch(const access_log::type &log_type) override; machine_merkle_tree::proof_type do_get_proof(uint64_t address, int log2_size) const override; @@ -77,6 +78,8 @@ class virtual_machine : public i_virtual_machine { const access_log::type &log_type) override; uint64_t do_get_reg_address(reg r) const override; machine_config do_get_default_config() const override; + void do_verify_step(const hash_type &root_hash_before, const std::string &log_filename, uint64_t mcycle_count, + const hash_type &root_hash_after) const override; void do_verify_step_uarch(const hash_type &root_hash_before, const access_log &log, const hash_type &root_hash_after) const override; void do_verify_reset_uarch(const hash_type &root_hash_before, const access_log &log, diff --git a/tests/Makefile b/tests/Makefile index 77cbf4b72..b1b49575c 100644 --- a/tests/Makefile +++ b/tests/Makefile @@ -184,6 +184,9 @@ test-cmio: | $(CARTESI_CMIO_PATH) test-machine: $(LUA) ./lua/cartesi-machine-tests.lua --jobs=$(NUM_JOBS) run +test-machine-with-log-step: + $(LUA) ./lua/cartesi-machine-tests.lua --jobs=$(NUM_JOBS) run_step + test-uarch: $(LUA) ./lua/cartesi-machine-tests.lua --jobs=$(NUM_JOBS) run_uarch @@ -254,7 +257,7 @@ coverage-report: $(COVERAGE_OUTPUT_DIR) export LLVM_PROFILE_FILE=coverage-%p.profraw endif -test: test-save-and-load test-yield-and-save test-machine test-uarch test-uarch-rv64ui test-uarch-interpreter test-lua test-jsonrpc test-c-api test-hash test-cmio +test: test-save-and-load test-yield-and-save test-machine test-uarch test-uarch-rv64ui test-uarch-interpreter test-lua test-jsonrpc test-c-api test-hash test-cmio test-machine-with-log-step lint format check-format: @$(MAKE) -C misc $@ diff --git a/tests/lua/cartesi-machine-tests.lua b/tests/lua/cartesi-machine-tests.lua index 5d3ed4ecb..7f7df6606 100755 --- a/tests/lua/cartesi-machine-tests.lua +++ b/tests/lua/cartesi-machine-tests.lua @@ -352,6 +352,9 @@ and command can be: run run test and report if payload and cycles match expected + run_step + run all tests by recording and verifying each test execution into a step log file + run_uarch run test in the microarchitecture and report if payload and cycles match expected @@ -919,6 +922,33 @@ local function run_host_and_uarch_machines(host_machine, uarch_machine, ctx, max return host_cycles end +local function run_machine_step(machine, reference_machine, ctx, mcycle_count) + local log_filename = os.tmpname() + local deleter = {} + setmetatable(deleter, { + __gc = function() + os.remove(log_filename) + end, + }) + os.remove(log_filename) + local root_hash_before = machine:get_root_hash() + local reference_hash = reference_machine:get_root_hash() + if root_hash_before ~= reference_hash then + fatal("%s: failed. Initial hash does not match reference machine\n", ctx.ram_image) + return + end + machine:log_step(mcycle_count, log_filename) + local root_hash_after = machine:get_root_hash() + cartesi.machine:verify_step(root_hash_before, log_filename, mcycle_count, root_hash_after) + -- run the reference machine normally and check final hashes + reference_machine:run(mcycle_count) + reference_hash = reference_machine:get_root_hash() + if root_hash_after ~= reference_hash then + fatal("%s: failed. Final hash does not match reference machine\n", ctx.ram_image) + end + ctx.read_htif_tohost_data = machine:read_reg("htif_tohost_data") +end + local failures = nil local contexts = tabular.expand({ "ram_image", "expected_cycles", "expected_halt_payload" }, selected_tests) @@ -930,6 +960,13 @@ elseif command == "run" then run_machine(machine, row, 2 * row.expected_cycles) check_and_print_result(machine, row) end) +elseif command == "run_step" then + failures = parallel.run(contexts, jobs, function(row) + local machine = build_machine(row.ram_image) + local reference_machine = build_machine(row.ram_image) + run_machine_step(machine, reference_machine, row, row.expected_cycles) + check_and_print_result(machine, row) + end) elseif command == "run_uarch" then failures = parallel.run(contexts, jobs, function(row) local machine = build_machine(row.ram_image) diff --git a/tests/lua/machine-bind.lua b/tests/lua/machine-bind.lua index 2ff1ee290..02f13f855 100755 --- a/tests/lua/machine-bind.lua +++ b/tests/lua/machine-bind.lua @@ -597,14 +597,14 @@ print("\n\n run machine to 1000 mcycle") do_test("mcycle value should be 1000 after execution", function(machine) -- Run machine machine:write_reg("mcycle", 0) - assert(machine:read_reg("mcycle") == 0) + assert(machine:read_mcycle() == 0) local test = machine:read_mcycle() while test < 1000 do machine:run(1000) test = machine:read_mcycle() end - assert(machine:read_reg("mcycle") == 1000) + assert(machine:read_mcycle() == 1000) end) print("\n\n check reading and writing htif registers") @@ -1497,4 +1497,206 @@ test_util.make_do_test(build_machine, machine_type, { assert(log.accesses[7].written == leaf_data) end) +-- helper function to load a step log file into a table +local function read_step_log_file(filename) + local file = assert(io.open(filename, "rb")) + local page_count = string.unpack(" = assert(io.open(filename, "wb")) + local page_count = #logdata.pages + if logdata.override_page_count then + page_count = logdata.override_page_count + end + file:write(string.pack("