diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 5e33b9327..b03a0d513 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -190,6 +190,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_stepinside 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: | diff --git a/CHANGELOG.md b/CHANGELOG.md index 00158e821..175efa284 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 581a094fe..6e6724328 100644 --- a/src/Makefile +++ b/src/Makefile @@ -363,7 +363,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 83b7d471e..d24747bad 100755 --- a/src/cartesi-machine.lua +++ b/src/cartesi-machine.lua @@ -434,6 +434,9 @@ where options are: this option implies --initial-hash and --final-hash. (default: none) +--log-steps=, + log and save a step of mcycles to . + --log-uarch-step advance one micro step and print access log. @@ -610,6 +613,8 @@ local load_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, { @@ -1239,6 +1244,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%-uarch%-step$", function(all) @@ -2299,6 +2313,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_uarch_step 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 68b5eae15..f2c9a3624 100644 --- a/src/clua-i-virtual-machine.cpp +++ b/src/clua-i-virtual-machine.cpp @@ -332,6 +332,14 @@ 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; + TRY_EXECUTE(cm_log_step(m.get(), luaL_checkinteger(L, 2), luaL_checkstring(L, 3), &break_reason, err_msg)); + 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) { @@ -718,6 +726,7 @@ static const auto machine_obj_index = cartesi::clua_make_luaL_Reg_array({ {"log_uarch_reset", machine_obj_index_log_uarch_reset}, {"send_cmio_response", machine_obj_index_send_cmio_response}, {"log_send_cmio_response", machine_obj_index_log_send_cmio_response}, + {"log_step", machine_obj_index_log_step}, }); /// \brief This is the machine __close metamethod implementation. diff --git a/src/clua-jsonrpc-machine.cpp b/src/clua-jsonrpc-machine.cpp index a0e84d5f6..a7ab9ee9d 100644 --- a/src/clua-jsonrpc-machine.cpp +++ b/src/clua-jsonrpc-machine.cpp @@ -224,6 +224,24 @@ static int jsonrpc_machine_class_verify_send_cmio_response_state_transition(lua_ return 1; } +/// \brief This is the machine.verify_step() static method implementation. +static int jsonrpc_machine_class_verify_step(lua_State *L) { + const int stubidx = lua_upvalueindex(1); + const int ctxidx = lua_upvalueindex(2); + lua_settop(L, 5); + auto &managed_jsonrpc_mgr = clua_check>(L, stubidx, ctxidx); + cm_hash root_hash_before{}; + clua_check_cm_hash(L, 1, &root_hash_before); + const auto *filename = luaL_checkstring(L, 2); + const uint64_t mcycle_count = luaL_optinteger(L, 3, UINT64_MAX); + cm_hash root_hash_after{}; + clua_check_cm_hash(L, 4, &root_hash_after); + TRY_EXECUTE(cm_jsonrpc_verify_step(managed_jsonrpc_mgr.get(), &root_hash_before, filename, mcycle_count, + &root_hash_after, err_msg)); + lua_pop(L, 2); + return 0; +} + /// \brief Contents of the machine class metatable __index table. static const auto jsonrpc_machine_static_methods = cartesi::clua_make_luaL_Reg_array({ {"get_default_config", jsonrpc_machine_class_get_default_config}, @@ -237,6 +255,7 @@ static const auto jsonrpc_machine_static_methods = cartesi::clua_make_luaL_Reg_a {"get_csr_address", jsonrpc_machine_class_get_csr_address}, {"verify_send_cmio_response_log", jsonrpc_machine_class_verify_send_cmio_response_log}, {"verify_send_cmio_response_state_transition", jsonrpc_machine_class_verify_send_cmio_response_state_transition}, + {"verify_step", jsonrpc_machine_class_verify_step}, }); /// \brief Prints a JSONRPC machine class diff --git a/src/clua-machine.cpp b/src/clua-machine.cpp index 45c7e20ac..b6e1dc3ea 100644 --- a/src/clua-machine.cpp +++ b/src/clua-machine.cpp @@ -148,6 +148,19 @@ static int machine_class_index_verify_send_cmio_response_log(lua_State *L) { return 1; } +/// \brief This is the machine.verify_step() method implementation. +static int machine_class_index_verify_step(lua_State *L) { + lua_settop(L, 4); + cm_hash root_hash_before{}; + clua_check_cm_hash(L, 1, &root_hash_before); + cm_hash root_hash_after{}; + clua_check_cm_hash(L, 4, &root_hash_after); + + TRY_EXECUTE( + cm_verify_step(&root_hash_before, luaL_checkstring(L, 2), luaL_checkinteger(L, 3), &root_hash_after, err_msg)); + return 1; +} + /// \brief This is the machine.verify_send_cmio_response_state_transition() method implementation. static int machine_class_index_verify_send_cmio_response_state_transition(lua_State *L) { lua_settop(L, 6); @@ -183,6 +196,7 @@ static const auto machine_class_index = cartesi::clua_make_luaL_Reg_array({ {"get_csr_address", machine_class_index_get_csr_address}, {"verify_send_cmio_response_log", machine_class_index_verify_send_cmio_response_log}, {"verify_send_cmio_response_state_transition", machine_class_index_verify_send_cmio_response_state_transition}, + {"verify_step", machine_class_index_verify_step}, }); /// \brief This is the cartesi.machine() constructor implementation. diff --git a/src/i-virtual-machine.h b/src/i-virtual-machine.h index 96cbbd6ac..bf0a8592a 100644 --- a/src/i-virtual-machine.h +++ b/src/i-virtual-machine.h @@ -56,6 +56,11 @@ class i_virtual_machine { return do_run(mcycle_end); } + /// \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 Serialize entire state to directory void store(const std::string &dir) { do_store(dir); @@ -698,6 +703,7 @@ class i_virtual_machine { private: virtual interpreter_break_reason do_run(uint64_t mcycle_end) = 0; + virtual interpreter_break_reason do_log_step(uint64_t mcycle_count, const std::string &filename) = 0; virtual void do_store(const std::string &dir) = 0; virtual access_log do_log_uarch_step(const access_log::type &log_type, bool one_based = false) = 0; virtual machine_merkle_tree::proof_type do_get_proof(uint64_t address, int log2_size) const = 0; diff --git a/src/interpret.cpp b/src/interpret.cpp index 217f8c7bb..c320da7e2 100644 --- a/src/interpret.cpp +++ b/src/interpret.cpp @@ -21,6 +21,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" #endif #include "machine-statistics.h" @@ -5677,6 +5679,13 @@ 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_access32 +template interpreter_break_reason interpret(replay_step_state_access &a, uint64_t mcycle_end); + #endif // MICROARCHITECTURE } // namespace cartesi diff --git a/src/interpret.h b/src/interpret.h index b9af6a90a..a28db95c9 100644 --- a/src/interpret.h +++ b/src/interpret.h @@ -63,11 +63,19 @@ extern template interpreter_break_reason interpret(uarch_machine_state_access &a #else // Forward declarations class state_access; +class record_step_state_access; +class replay_step_state_access; class machine; // Declaration of explicit instantiation in module interpret.cpp extern template interpreter_break_reason interpret(state_access &a, uint64_t mcycle_end); +// Declaration of explicit instantiation +extern template interpreter_break_reason interpret(record_step_state_access &a, uint64_t mcycle_end); + +// Declaration of explicit instantiation +extern template interpreter_break_reason interpret(replay_step_state_access &a, uint64_t mcycle_end); + #endif // MICROARCHITECTURE } // namespace cartesi diff --git a/src/jsonrpc-machine-c-api.cpp b/src/jsonrpc-machine-c-api.cpp index 74d284cf2..200cb8e9f 100644 --- a/src/jsonrpc-machine-c-api.cpp +++ b/src/jsonrpc-machine-c-api.cpp @@ -264,6 +264,18 @@ int cm_jsonrpc_verify_send_cmio_response_log(const cm_jsonrpc_mgr *mgr, uint16_t return cm_result_failure(err_msg); } +int cm_jsonrpc_verify_step(const cm_jsonrpc_mgr *mgr, const cm_hash *root_hash_before, const char *filename, + uint16_t mcycle_count, const cm_hash *root_hash_after, char **err_msg) try { + const auto *cpp_mgr = convert_from_c(mgr); + 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); + cartesi::jsonrpc_virtual_machine::verify_step(*cpp_mgr, cpp_root_hash_before, filename, mcycle_count, + cpp_root_hash_after); + return cm_result_success(err_msg); +} catch (...) { + return cm_result_failure(err_msg); +} + int cm_jsonrpc_verify_send_cmio_response_state_transition(const cm_jsonrpc_mgr *mgr, uint16_t reason, const unsigned char *data, size_t length, const cm_hash *root_hash_before, const cm_access_log *log, const cm_hash *root_hash_after, const cm_machine_runtime_config *runtime_config, bool one_based, diff --git a/src/jsonrpc-machine-c-api.h b/src/jsonrpc-machine-c-api.h index ad0775037..ec3e1fa80 100644 --- a/src/jsonrpc-machine-c-api.h +++ b/src/jsonrpc-machine-c-api.h @@ -234,6 +234,16 @@ CM_API int cm_jsonrpc_verify_send_cmio_response_log(const cm_jsonrpc_mgr *mgr, u const unsigned char *data, size_t length, const cm_access_log *log, const cm_machine_runtime_config *runtime_config, bool one_based, char **err_msg); +/// \brief Checks the validity of a step log file. +/// \param mgr Cartesi jsonrpc connection manager. Must be pointer to valid object +/// \param root_hash_before Machine root hash before step +/// \param filename Path to the step log file +/// \param mcycle_count Number of mcycles in the step +/// \param root_hash_after Machine root hash after step +/// \param err_msg Receives the error message if function execution fails +CM_API int cm_jsonrpc_verify_step(const cm_jsonrpc_mgr *mgr, const cm_hash *root_hash_before, const char *filename, + uint16_t mcycle_count, const cm_hash *root_hash_after, char **err_msg); + /// \brief Checks the validity of a state transition caused by send_cmio_response /// \param mgr Cartesi jsonrpc connection manager. Must be pointer to valid object /// \param reason Reason for sending response. diff --git a/src/jsonrpc-remote-machine.cpp b/src/jsonrpc-remote-machine.cpp index 0521f963b..ae8e39fc8 100644 --- a/src/jsonrpc-remote-machine.cpp +++ b/src/jsonrpc-remote-machine.cpp @@ -924,6 +924,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 @@ -1698,6 +1712,25 @@ static json jsonrpc_machine_send_cmio_response_handler(const json &j, const std: return jsonrpc_response_ok(j); } +/// \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); + switch (count_args(args)) { + case 4: + cartesi::machine::verify_step(std::get<0>(args), std::get<1>(args), std::get<2>(args), std::get<3>(args)); + break; + default: + throw std::runtime_error{"error detecting number of arguments"}; + } + return jsonrpc_response_ok(j); +} + static json jsonrpc_machine_log_send_cmio_response_handler(const json &j, const std::shared_ptr &session) { if (!session->handler->machine) { @@ -1911,6 +1944,8 @@ 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 d32c720bd..be4eccfc5 100644 --- a/src/jsonrpc-virtual-machine.cpp +++ b/src/jsonrpc-virtual-machine.cpp @@ -440,6 +440,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_mgr->get_stream(), m_mgr->get_remote_address(), "machine.log_step", + std::tie(mcycle_count, filename), result); + return result; +} + void jsonrpc_virtual_machine::do_store(const std::string &directory) { bool result = false; jsonrpc_request(m_mgr->get_stream(), m_mgr->get_remote_address(), "machine.store", std::tie(directory), result); @@ -1057,6 +1064,15 @@ void jsonrpc_virtual_machine::do_send_cmio_response(uint16_t reason, const unsig std::tie(reason, b64), result); } +void jsonrpc_virtual_machine::verify_step(const jsonrpc_mgr_ptr &mgr, const hash_type &root_hash_before, + const char *filename, uint16_t mcycle_count, const hash_type &root_hash_after) { + 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(mgr->get_stream(), mgr->get_remote_address(), "machine.verify_step", + std::tie(b64_root_hash_before, filename, mcycle_count, b64_root_hash_after), result); +} + access_log jsonrpc_virtual_machine::do_log_send_cmio_response(uint16_t reason, const unsigned char *data, size_t length, const access_log::type &log_type, bool one_based) { not_default_constructible result; diff --git a/src/jsonrpc-virtual-machine.h b/src/jsonrpc-virtual-machine.h index 2d757c0fe..9e5062a20 100644 --- a/src/jsonrpc-virtual-machine.h +++ b/src/jsonrpc-virtual-machine.h @@ -73,6 +73,9 @@ class jsonrpc_virtual_machine final : public i_virtual_machine { const unsigned char *data, size_t length, const hash_type &root_hash_before, const access_log &log, const hash_type &root_hash_after, const machine_runtime_config &r = {}, bool one_based = false); + static void verify_step(const jsonrpc_mgr_ptr &mgr, const hash_type &root_hash_before, const char *filename, + uint16_t mcycle_count, const hash_type &root_hash_after); + static std::string fork(const jsonrpc_mgr_ptr &mgr); static void rebind(const jsonrpc_mgr_ptr &mgr, const std::string &address); static uint64_t get_x_address(const jsonrpc_mgr_ptr &mgr, int i); @@ -84,6 +87,7 @@ class jsonrpc_virtual_machine final : public i_virtual_machine { machine_config do_get_initial_config(void) const 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) override; uint64_t do_read_csr(csr r) const override; void do_write_csr(csr w, uint64_t val) override; diff --git a/src/machine-c-api.cpp b/src/machine-c-api.cpp index 7ed8848e1..a3704235d 100644 --- a/src/machine-c-api.cpp +++ b/src/machine-c-api.cpp @@ -982,6 +982,28 @@ int cm_machine_run(cm_machine *m, uint64_t mcycle_end, CM_BREAK_REASON *break_re return cm_result_failure(err_msg); } +int cm_log_step(cm_machine *m, uint64_t mcycle_count, const char *log_filename, CM_BREAK_REASON *break_reason_result, + char **err_msg) try { + auto *cpp_machine = convert_from_c(m); + cartesi::interpreter_break_reason break_reason = cpp_machine->log_step(mcycle_count, null_to_empty(log_filename)); + if (break_reason_result) { + *break_reason_result = static_cast(break_reason); + } + return cm_result_success(err_msg); +} catch (...) { + return cm_result_failure(err_msg); +} + +int cm_verify_step(const cm_hash *root_hash_before, const char *log_filename, uint64_t mcycle_count, + const cm_hash *root_hash_after, char **err_msg) try { + 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); + cartesi::machine::verify_step(cpp_root_hash_before, null_to_empty(log_filename), mcycle_count, cpp_root_hash_after); + return cm_result_success(err_msg); +} catch (...) { + return cm_result_failure(err_msg); +} + int cm_read_uarch_x(const cm_machine *m, int i, uint64_t *val, char **err_msg) try { if (val == nullptr) { throw std::invalid_argument("invalid val output"); diff --git a/src/machine-c-api.h b/src/machine-c-api.h index 91c41bbd5..fd05b5bf3 100644 --- a/src/machine-c-api.h +++ b/src/machine-c-api.h @@ -539,6 +539,28 @@ CM_API void cm_delete_machine(cm_machine *m); /// \returns 0 for success, non zero code for error CM_API int cm_machine_run(cm_machine *m, uint64_t mcycle_end, CM_BREAK_REASON *break_reason_result, char **err_msg); +/// \brief Runs the machine for the given mcycle count and generates a log of accessed pages and proof data. +/// \param m Pointer to valid 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 machine run interruption when not NULL +/// \param err_msg Receives the error message if function execution fails +CM_API int cm_log_step(cm_machine *m, uint64_t mcycle_count, const char *log_filename, + CM_BREAK_REASON *break_reason_result, char **err_msg); + +/// \brief Checks the validity of a step log file. +/// \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 +/// \param err_msg Receives the error message if function execution fails +/// or NULL in case of successful function execution. In case of failure error_msg +/// must be deleted by the function caller using cm_delete_cstring. +/// err_msg can be NULL, meaning the error message won't be received. +/// \returns 0 for success, non zero code for error +CM_API int cm_verify_step(const cm_hash *root_hash_before, const char *log_filename, uint64_t mcycle_count, + const cm_hash *root_hash_after, char **err_msg); + /// \brief Runs the machine for one micro cycle logging all accesses to the state. /// \param m Pointer to valid machine instance /// \param log_type Type of access log to generate. diff --git a/src/machine-merkle-tree.cpp b/src/machine-merkle-tree.cpp index 78916c49f..54bc48bc2 100644 --- a/src/machine-merkle-tree.cpp +++ b/src/machine-merkle-tree.cpp @@ -425,6 +425,34 @@ 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)) { + 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 && log2_node_size > log2_target_size) { + const int log2_child_size = log2_node_size - 1; + const int path_bit = (target_address & (UINT64_C(1) << (log2_child_size))) != 0; + node = node->child[path_bit]; + log2_node_size = log2_child_size; + } + if (!node) { + // We hit a pristine node along the path to the target node + return get_pristine_hash(log2_target_size); + } + assert(node); + 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 8f26c158e..28b96016c 100644 --- a/src/machine-merkle-tree.h +++ b/src/machine-merkle-tree.h @@ -347,6 +347,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 3365a9e46..26da5e817 100644 --- a/src/machine.cpp +++ b/src/machine.cpp @@ -29,7 +29,9 @@ #include "interpret.h" #include "plic-factory.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" @@ -1737,6 +1739,18 @@ machine_merkle_tree::proof_type machine::get_proof(uint64_t address, int log2_si return get_proof(address, log2_size, skip_merkle_tree_update); } +machine::hash_type machine::get_merkle_tree_node_hash(uint64_t address, int log2_size, + skip_merkle_tree_update_t) 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); +} + void machine::read_memory(uint64_t address, unsigned char *data, uint64_t length) const { if (length == 0) { return; @@ -2148,6 +2162,35 @@ 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{}; + (void) __builtin_add_overflow(a.read_mcycle(), mcycle_count, &mcycle_end); + 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{}; + (void) __builtin_add_overflow(a.read_mcycle(), mcycle_count, &mcycle_end); + 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_mcycle()) { throw std::invalid_argument{"mcycle is past"}; diff --git a/src/machine.h b/src/machine.h index 17e28be42..96e85e141 100644 --- a/src/machine.h +++ b/src/machine.h @@ -202,6 +202,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 @@ -336,6 +350,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) const; + /// \brief Verifies integrity of Merkle tree. /// \returns True if tree is self-consistent, false otherwise. bool verify_merkle_tree(void) const; diff --git a/src/os.cpp b/src/os.cpp index cb1a48750..ecc74fd19 100644 --- a/src/os.cpp +++ b/src/os.cpp @@ -786,4 +786,24 @@ void os_sleep_us(uint64_t timeout_us) { Sleep(timeout_us / 1000); #endif } + +long 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 c8178e525..90107746f 100644 --- a/src/os.h +++ b/src/os.h @@ -147,6 +147,12 @@ void os_disable_sigpipe(); /// \brief Sleep until timeout_us microseconds elapsed void os_sleep_us(uint64_t timeout_us); +/// \brief Get the length of a file +long 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..4ff3b0230 --- /dev/null +++ b/src/record-step-state-access.h @@ -0,0 +1,840 @@ +// 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(void) const { + // get phys address of pc in dhadow + touch_page(shadow_state_get_csr_abs_addr(shadow_state_csr::pc)); + return m_m.get_state().pc; + } + + void do_write_pc(uint64_t val) { + touch_page(shadow_state_get_csr_abs_addr(shadow_state_csr::pc)); + m_m.get_state().pc = val; + } + + uint64_t do_read_fcsr(void) const { + touch_page(shadow_state_get_csr_abs_addr(shadow_state_csr::fcsr)); + return m_m.get_state().fcsr; + } + + void do_write_fcsr(uint64_t val) { + touch_page(shadow_state_get_csr_abs_addr(shadow_state_csr::fcsr)); + m_m.get_state().fcsr = val; + } + + uint64_t do_read_icycleinstret(void) const { + touch_page(shadow_state_get_csr_abs_addr(shadow_state_csr::icycleinstret)); + return m_m.get_state().icycleinstret; + } + + void do_write_icycleinstret(uint64_t val) { + touch_page(shadow_state_get_csr_abs_addr(shadow_state_csr::icycleinstret)); + m_m.get_state().icycleinstret = val; + } + + uint64_t do_read_mvendorid(void) const { // NOLINT(readability-convert-member-functions-to-static) + touch_page(shadow_state_get_csr_abs_addr(shadow_state_csr::mvendorid)); + return MVENDORID_INIT; + } + + uint64_t do_read_marchid(void) const { // NOLINT(readability-convert-member-functions-to-static) + touch_page(shadow_state_get_csr_abs_addr(shadow_state_csr::marchid)); + return MARCHID_INIT; + } + + uint64_t do_read_mimpid(void) const { // NOLINT(readability-convert-member-functions-to-static) + touch_page(shadow_state_get_csr_abs_addr(shadow_state_csr::mimpid)); + return MIMPID_INIT; + } + + uint64_t do_read_mcycle(void) const { + touch_page(shadow_state_get_csr_abs_addr(shadow_state_csr::mcycle)); + return m_m.get_state().mcycle; + } + + void do_write_mcycle(uint64_t val) { + touch_page(shadow_state_get_csr_abs_addr(shadow_state_csr::mcycle)); + m_m.get_state().mcycle = val; + } + + uint64_t do_read_mstatus(void) const { + touch_page(shadow_state_get_csr_abs_addr(shadow_state_csr::mstatus)); + return m_m.get_state().mstatus; + } + + void do_write_mstatus(uint64_t val) { + touch_page(shadow_state_get_csr_abs_addr(shadow_state_csr::mstatus)); + m_m.get_state().mstatus = val; + } + + uint64_t do_read_menvcfg(void) const { + touch_page(shadow_state_get_csr_abs_addr(shadow_state_csr::menvcfg)); + return m_m.get_state().menvcfg; + } + + void do_write_menvcfg(uint64_t val) { + touch_page(shadow_state_get_csr_abs_addr(shadow_state_csr::menvcfg)); + m_m.get_state().menvcfg = val; + } + + uint64_t do_read_mtvec(void) const { + touch_page(shadow_state_get_csr_abs_addr(shadow_state_csr::mtvec)); + return m_m.get_state().mtvec; + } + + void do_write_mtvec(uint64_t val) { + touch_page(shadow_state_get_csr_abs_addr(shadow_state_csr::mtvec)); + m_m.get_state().mtvec = val; + } + + uint64_t do_read_mscratch(void) const { + touch_page(shadow_state_get_csr_abs_addr(shadow_state_csr::mscratch)); + return m_m.get_state().mscratch; + } + + void do_write_mscratch(uint64_t val) { + touch_page(shadow_state_get_csr_abs_addr(shadow_state_csr::mscratch)); + m_m.get_state().mscratch = val; + } + + uint64_t do_read_mepc(void) const { + touch_page(shadow_state_get_csr_abs_addr(shadow_state_csr::mepc)); + return m_m.get_state().mepc; + } + + void do_write_mepc(uint64_t val) { + touch_page(shadow_state_get_csr_abs_addr(shadow_state_csr::mepc)); + m_m.get_state().mepc = val; + } + + uint64_t do_read_mcause(void) const { + touch_page(shadow_state_get_csr_abs_addr(shadow_state_csr::mcause)); + return m_m.get_state().mcause; + } + + void do_write_mcause(uint64_t val) { + touch_page(shadow_state_get_csr_abs_addr(shadow_state_csr::mcause)); + m_m.get_state().mcause = val; + } + + uint64_t do_read_mtval(void) const { + touch_page(shadow_state_get_csr_abs_addr(shadow_state_csr::mtval)); + return m_m.get_state().mtval; + } + + void do_write_mtval(uint64_t val) { + touch_page(shadow_state_get_csr_abs_addr(shadow_state_csr::mtval)); + m_m.get_state().mtval = val; + } + + uint64_t do_read_misa(void) const { + touch_page(shadow_state_get_csr_abs_addr(shadow_state_csr::misa)); + return m_m.get_state().misa; + } + + void do_write_misa(uint64_t val) { + touch_page(shadow_state_get_csr_abs_addr(shadow_state_csr::misa)); + m_m.get_state().misa = val; + } + + uint64_t do_read_mie(void) const { + touch_page(shadow_state_get_csr_abs_addr(shadow_state_csr::mie)); + return m_m.get_state().mie; + } + + void do_write_mie(uint64_t val) { + touch_page(shadow_state_get_csr_abs_addr(shadow_state_csr::mie)); + m_m.get_state().mie = val; + } + + uint64_t do_read_mip(void) const { + touch_page(shadow_state_get_csr_abs_addr(shadow_state_csr::mip)); + return m_m.get_state().mip; + } + + void do_write_mip(uint64_t val) { + touch_page(shadow_state_get_csr_abs_addr(shadow_state_csr::mip)); + m_m.get_state().mip = val; + } + + uint64_t do_read_medeleg(void) const { + touch_page(shadow_state_get_csr_abs_addr(shadow_state_csr::medeleg)); + return m_m.get_state().medeleg; + } + + void do_write_medeleg(uint64_t val) { + touch_page(shadow_state_get_csr_abs_addr(shadow_state_csr::medeleg)); + m_m.get_state().medeleg = val; + } + + uint64_t do_read_mideleg(void) const { + touch_page(shadow_state_get_csr_abs_addr(shadow_state_csr::mideleg)); + return m_m.get_state().mideleg; + } + + void do_write_mideleg(uint64_t val) { + touch_page(shadow_state_get_csr_abs_addr(shadow_state_csr::mideleg)); + m_m.get_state().mideleg = val; + } + + uint64_t do_read_mcounteren(void) const { + touch_page(shadow_state_get_csr_abs_addr(shadow_state_csr::mcounteren)); + return m_m.get_state().mcounteren; + } + + void do_write_mcounteren(uint64_t val) { + touch_page(shadow_state_get_csr_abs_addr(shadow_state_csr::mcounteren)); + m_m.get_state().mcounteren = val; + } + + uint64_t do_read_senvcfg(void) const { + touch_page(shadow_state_get_csr_abs_addr(shadow_state_csr::senvcfg)); + return m_m.get_state().senvcfg; + } + + void do_write_senvcfg(uint64_t val) { + touch_page(shadow_state_get_csr_abs_addr(shadow_state_csr::senvcfg)); + m_m.get_state().senvcfg = val; + } + + uint64_t do_read_stvec(void) const { + touch_page(shadow_state_get_csr_abs_addr(shadow_state_csr::stvec)); + return m_m.get_state().stvec; + } + + void do_write_stvec(uint64_t val) { + touch_page(shadow_state_get_csr_abs_addr(shadow_state_csr::stvec)); + m_m.get_state().stvec = val; + } + + uint64_t do_read_sscratch(void) const { + touch_page(shadow_state_get_csr_abs_addr(shadow_state_csr::sscratch)); + return m_m.get_state().sscratch; + } + + void do_write_sscratch(uint64_t val) { + touch_page(shadow_state_get_csr_abs_addr(shadow_state_csr::sscratch)); + m_m.get_state().sscratch = val; + } + + uint64_t do_read_sepc(void) const { + touch_page(shadow_state_get_csr_abs_addr(shadow_state_csr::sepc)); + return m_m.get_state().sepc; + } + + void do_write_sepc(uint64_t val) { + touch_page(shadow_state_get_csr_abs_addr(shadow_state_csr::sepc)); + m_m.get_state().sepc = val; + } + + uint64_t do_read_scause(void) const { + touch_page(shadow_state_get_csr_abs_addr(shadow_state_csr::scause)); + return m_m.get_state().scause; + } + + void do_write_scause(uint64_t val) { + touch_page(shadow_state_get_csr_abs_addr(shadow_state_csr::scause)); + m_m.get_state().scause = val; + } + + uint64_t do_read_stval(void) const { + touch_page(shadow_state_get_csr_abs_addr(shadow_state_csr::stval)); + return m_m.get_state().stval; + } + + void do_write_stval(uint64_t val) { + touch_page(shadow_state_get_csr_abs_addr(shadow_state_csr::stval)); + m_m.get_state().stval = val; + } + + uint64_t do_read_satp(void) const { + touch_page(shadow_state_get_csr_abs_addr(shadow_state_csr::satp)); + return m_m.get_state().satp; + } + + void do_write_satp(uint64_t val) { + touch_page(shadow_state_get_csr_abs_addr(shadow_state_csr::satp)); + m_m.get_state().satp = val; + } + + uint64_t do_read_scounteren(void) const { + touch_page(shadow_state_get_csr_abs_addr(shadow_state_csr::scounteren)); + return m_m.get_state().scounteren; + } + + void do_write_scounteren(uint64_t val) { + touch_page(shadow_state_get_csr_abs_addr(shadow_state_csr::scounteren)); + m_m.get_state().scounteren = val; + } + + uint64_t do_read_ilrsc(void) const { + touch_page(shadow_state_get_csr_abs_addr(shadow_state_csr::ilrsc)); + return m_m.get_state().ilrsc; + } + + void do_write_ilrsc(uint64_t val) { + touch_page(shadow_state_get_csr_abs_addr(shadow_state_csr::ilrsc)); + m_m.get_state().ilrsc = val; + } + + void do_set_iflags_H(void) { + touch_page(shadow_state_get_csr_abs_addr(shadow_state_csr::iflags)); + m_m.get_state().iflags.H = true; + } + + bool do_read_iflags_H(void) const { + touch_page(shadow_state_get_csr_abs_addr(shadow_state_csr::iflags)); + return m_m.get_state().iflags.H; + } + + void do_set_iflags_X(void) { + touch_page(shadow_state_get_csr_abs_addr(shadow_state_csr::iflags)); + m_m.get_state().iflags.X = true; + } + + void do_reset_iflags_X(void) { + touch_page(shadow_state_get_csr_abs_addr(shadow_state_csr::iflags)); + m_m.get_state().iflags.X = false; + } + + bool do_read_iflags_X(void) const { + touch_page(shadow_state_get_csr_abs_addr(shadow_state_csr::iflags)); + return m_m.get_state().iflags.X; + } + + void do_set_iflags_Y(void) { + touch_page(shadow_state_get_csr_abs_addr(shadow_state_csr::iflags)); + m_m.get_state().iflags.Y = true; + } + + void do_reset_iflags_Y(void) { + touch_page(shadow_state_get_csr_abs_addr(shadow_state_csr::iflags)); + m_m.get_state().iflags.Y = false; + } + + bool do_read_iflags_Y(void) const { + touch_page(shadow_state_get_csr_abs_addr(shadow_state_csr::iflags)); + return m_m.get_state().iflags.Y; + } + + uint8_t do_read_iflags_PRV(void) const { + touch_page(shadow_state_get_csr_abs_addr(shadow_state_csr::iflags)); + return m_m.get_state().iflags.PRV; + } + + void do_write_iflags_PRV(uint8_t val) { + touch_page(shadow_state_get_csr_abs_addr(shadow_state_csr::iflags)); + m_m.get_state().iflags.PRV = val; + } + + uint64_t do_read_iunrep(void) const { + touch_page(shadow_state_get_csr_abs_addr(shadow_state_csr::iunrep)); + return m_m.get_state().iunrep; + } + + void do_write_iunrep(uint64_t val) { + touch_page(shadow_state_get_csr_abs_addr(shadow_state_csr::iunrep)); + m_m.get_state().iunrep = val; + } + + uint64_t do_read_clint_mtimecmp(void) const { + touch_page(shadow_state_get_csr_abs_addr(shadow_state_csr::clint_mtimecmp)); + return m_m.get_state().clint.mtimecmp; + } + + void do_write_clint_mtimecmp(uint64_t val) { + touch_page(shadow_state_get_csr_abs_addr(shadow_state_csr::clint_mtimecmp)); + m_m.get_state().clint.mtimecmp = val; + } + + uint64_t do_read_plic_girqpend(void) const { + touch_page(shadow_state_get_csr_abs_addr(shadow_state_csr::plic_girqpend)); + return m_m.get_state().plic.girqpend; + } + + void do_write_plic_girqpend(uint64_t val) { + touch_page(shadow_state_get_csr_abs_addr(shadow_state_csr::plic_girqpend)); + m_m.get_state().plic.girqpend = val; + } + + uint64_t do_read_plic_girqsrvd(void) const { + touch_page(shadow_state_get_csr_abs_addr(shadow_state_csr::plic_girqsrvd)); + return m_m.get_state().plic.girqsrvd; + } + + void do_write_plic_girqsrvd(uint64_t val) { + touch_page(shadow_state_get_csr_abs_addr(shadow_state_csr::plic_girqsrvd)); + m_m.get_state().plic.girqsrvd = val; + } + + uint64_t do_read_htif_fromhost(void) const { + touch_page(shadow_state_get_csr_abs_addr(shadow_state_csr::htif_fromhost)); + return m_m.get_state().htif.fromhost; + } + + void do_write_htif_fromhost(uint64_t val) { + touch_page(shadow_state_get_csr_abs_addr(shadow_state_csr::htif_fromhost)); + m_m.get_state().htif.fromhost = val; + } + + uint64_t do_read_htif_tohost(void) const { + touch_page(shadow_state_get_csr_abs_addr(shadow_state_csr::htif_tohost)); + return m_m.get_state().htif.tohost; + } + + void do_write_htif_tohost(uint64_t val) { + touch_page(shadow_state_get_csr_abs_addr(shadow_state_csr::htif_tohost)); + m_m.get_state().htif.tohost = val; + } + + uint64_t do_read_htif_ihalt(void) const { + touch_page(shadow_state_get_csr_abs_addr(shadow_state_csr::htif_ihalt)); + return m_m.get_state().htif.ihalt; + } + + uint64_t do_read_htif_iconsole(void) const { + touch_page(shadow_state_get_csr_abs_addr(shadow_state_csr::htif_iconsole)); + return m_m.get_state().htif.iconsole; + } + + uint64_t do_read_htif_iyield(void) const { + touch_page(shadow_state_get_csr_abs_addr(shadow_state_csr::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)) { + touch_page(paddr); + 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(void) { + touch_page(shadow_state_get_csr_abs_addr(shadow_state_csr::iflags)); + return m_m.get_state().read_iflags(); + } + + void do_write_iflags(uint64_t val) { + touch_page(shadow_state_get_csr_abs_addr(shadow_state_csr::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 + inline 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 + inline 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 + inline 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..a33be0dd9 --- /dev/null +++ b/src/replay-step-state-access-interop.cpp @@ -0,0 +1,46 @@ +// 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) +} + +extern "C" void interop_print(const char *msg) { + printf("%s\n", msg); +} diff --git a/src/replay-step-state-access-interop.h b/src/replay-step-state-access-interop.h new file mode 100644 index 000000000..a527c2694 --- /dev/null +++ b/src/replay-step-state-access-interop.h @@ -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 . +// + +#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_print(const char *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..8e2d35355 --- /dev/null +++ b/src/replay-step-state-access.h @@ -0,0 +1,1097 @@ +// 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(void) : mock_pma_entry(-1, 0, 0, {false, false, true /* empty */}) { + ; + } + + int get_index(void) const { + return m_pma_index; + } + + flags get_flags(void) const { + return m_flags; + } + + uint64_t get_start(void) const { + return m_start; + } + + uint64_t get_length(void) const { + return m_length; + } + + bool get_istart_M(void) const { + return m_flags.M; + } + + bool get_istart_IO(void) const { + return m_flags.IO; + } + + bool get_istart_E(void) const { + return m_flags.E; + } + + bool get_istart_R(void) const { + return m_flags.R; + } + + bool get_istart_W(void) const { + return m_flags.W; + } + + bool get_istart_X(void) const { + return m_flags.X; + } + + bool get_istart_IR(void) const { + return m_flags.IR; + } + + const pma_driver *get_device_driver() { + return m_device_driver; + } + + void *get_device_context() { + return m_device_context; + } + + void mark_dirty_page(uint64_t address_in_range) { + // Dummy implementation. + } +}; + +// \brief checks if the buffer size is large enough to hold the data +// \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 offset of the next data +// \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]; +#pragma pack(push, 1) + struct page_type { + address_type index; + data_type data; + hash_type hash; + }; +#pragma pack(pop) + uint64_t m_page_count; ///< Number of pages in the step log + page_type *m_pages; ///< Array of page data + uint64_t m_sibling_count; ///< Number of sibling hashes in the step log + hash_type *m_sibling_hashes; ///< 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) : + m_page_count{0}, + m_pages{nullptr}, + m_sibling_count{0}, + m_sibling_hashes{nullptr} { + // 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]; + } else 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) { + 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 const_cast(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++]; + } else 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; + } else 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(void) { + return raw_read_memory(shadow_state_get_csr_abs_addr(shadow_state_csr::pc)); + } + + void do_write_pc(uint64_t val) { + raw_write_memory(shadow_state_get_csr_abs_addr(shadow_state_csr::pc), val); + } + + uint64_t do_read_fcsr(void) { + return raw_read_memory(shadow_state_get_csr_abs_addr(shadow_state_csr::fcsr)); + } + + void do_write_fcsr(uint64_t val) { + raw_write_memory(shadow_state_get_csr_abs_addr(shadow_state_csr::fcsr), val); + } + + uint64_t do_read_icycleinstret(void) { + return raw_read_memory(shadow_state_get_csr_abs_addr(shadow_state_csr::icycleinstret)); + } + + void do_write_icycleinstret(uint64_t val) { + raw_write_memory(shadow_state_get_csr_abs_addr(shadow_state_csr::icycleinstret), val); + } + + uint64_t do_read_mvendorid(void) { + return raw_read_memory(shadow_state_get_csr_abs_addr(shadow_state_csr::mvendorid)); + } + + uint64_t do_read_marchid(void) { + return raw_read_memory(shadow_state_get_csr_abs_addr(shadow_state_csr::marchid)); + } + + uint64_t do_read_mimpid(void) { + return raw_read_memory(shadow_state_get_csr_abs_addr(shadow_state_csr::mimpid)); + } + + uint64_t do_read_mcycle(void) { + return raw_read_memory(shadow_state_get_csr_abs_addr(shadow_state_csr::mcycle)); + } + + void do_write_mcycle(uint64_t val) { + raw_write_memory(shadow_state_get_csr_abs_addr(shadow_state_csr::mcycle), val); + } + + uint64_t do_read_mstatus(void) { + return raw_read_memory(shadow_state_get_csr_abs_addr(shadow_state_csr::mstatus)); + } + + void do_write_mstatus(uint64_t val) { + raw_write_memory(shadow_state_get_csr_abs_addr(shadow_state_csr::mstatus), val); + } + + uint64_t do_read_mtvec(void) { + return raw_read_memory(shadow_state_get_csr_abs_addr(shadow_state_csr::mtvec)); + } + + void do_write_mtvec(uint64_t val) { + raw_write_memory(shadow_state_get_csr_abs_addr(shadow_state_csr::mtvec), val); + } + + uint64_t do_read_mscratch(void) { + return raw_read_memory(shadow_state_get_csr_abs_addr(shadow_state_csr::mscratch)); + } + + void do_write_mscratch(uint64_t val) { + raw_write_memory(shadow_state_get_csr_abs_addr(shadow_state_csr::mscratch), val); + } + + uint64_t do_read_mepc(void) { + return raw_read_memory(shadow_state_get_csr_abs_addr(shadow_state_csr::mepc)); + } + + void do_write_mepc(uint64_t val) { + raw_write_memory(shadow_state_get_csr_abs_addr(shadow_state_csr::mepc), val); + } + + uint64_t do_read_mcause(void) { + return raw_read_memory(shadow_state_get_csr_abs_addr(shadow_state_csr::mcause)); + } + + void do_write_mcause(uint64_t val) { + raw_write_memory(shadow_state_get_csr_abs_addr(shadow_state_csr::mcause), val); + } + + uint64_t do_read_mtval(void) { + return raw_read_memory(shadow_state_get_csr_abs_addr(shadow_state_csr::mtval)); + } + + void do_write_mtval(uint64_t val) { + raw_write_memory(shadow_state_get_csr_abs_addr(shadow_state_csr::mtval), val); + } + + uint64_t do_read_misa(void) { + return raw_read_memory(shadow_state_get_csr_abs_addr(shadow_state_csr::misa)); + } + + void do_write_misa(uint64_t val) { + raw_write_memory(shadow_state_get_csr_abs_addr(shadow_state_csr::misa), val); + } + + uint64_t do_read_mie(void) { + return raw_read_memory(shadow_state_get_csr_abs_addr(shadow_state_csr::mie)); + } + + void do_write_mie(uint64_t val) { + raw_write_memory(shadow_state_get_csr_abs_addr(shadow_state_csr::mie), val); + } + + uint64_t do_read_mip(void) { + return raw_read_memory(shadow_state_get_csr_abs_addr(shadow_state_csr::mip)); + } + + void do_write_mip(uint64_t val) { + raw_write_memory(shadow_state_get_csr_abs_addr(shadow_state_csr::mip), val); + } + + uint64_t do_read_medeleg(void) { + return raw_read_memory(shadow_state_get_csr_abs_addr(shadow_state_csr::medeleg)); + } + + void do_write_medeleg(uint64_t val) { + raw_write_memory(shadow_state_get_csr_abs_addr(shadow_state_csr::medeleg), val); + } + + uint64_t do_read_mideleg(void) { + return raw_read_memory(shadow_state_get_csr_abs_addr(shadow_state_csr::mideleg)); + } + + void do_write_mideleg(uint64_t val) { + raw_write_memory(shadow_state_get_csr_abs_addr(shadow_state_csr::mideleg), val); + } + + uint64_t do_read_mcounteren(void) { + return raw_read_memory(shadow_state_get_csr_abs_addr(shadow_state_csr::mcounteren)); + } + + void do_write_mcounteren(uint64_t val) { + raw_write_memory(shadow_state_get_csr_abs_addr(shadow_state_csr::mcounteren), val); + } + + uint64_t do_read_senvcfg(void) const { + return raw_read_memory(shadow_state_get_csr_abs_addr(shadow_state_csr::senvcfg)); + } + + void do_write_senvcfg(uint64_t val) { + raw_write_memory(shadow_state_get_csr_abs_addr(shadow_state_csr::senvcfg), val); + } + + uint64_t do_read_menvcfg(void) const { + return raw_read_memory(shadow_state_get_csr_abs_addr(shadow_state_csr::menvcfg)); + } + + void do_write_menvcfg(uint64_t val) { + raw_write_memory(shadow_state_get_csr_abs_addr(shadow_state_csr::menvcfg), val); + } + + uint64_t do_read_stvec(void) { + return raw_read_memory(shadow_state_get_csr_abs_addr(shadow_state_csr::stvec)); + } + + void do_write_stvec(uint64_t val) { + raw_write_memory(shadow_state_get_csr_abs_addr(shadow_state_csr::stvec), val); + } + + uint64_t do_read_sscratch(void) { + return raw_read_memory(shadow_state_get_csr_abs_addr(shadow_state_csr::sscratch)); + } + + void do_write_sscratch(uint64_t val) { + raw_write_memory(shadow_state_get_csr_abs_addr(shadow_state_csr::sscratch), val); + } + + uint64_t do_read_sepc(void) { + return raw_read_memory(shadow_state_get_csr_abs_addr(shadow_state_csr::sepc)); + } + + void do_write_sepc(uint64_t val) { + raw_write_memory(shadow_state_get_csr_abs_addr(shadow_state_csr::sepc), val); + } + + uint64_t do_read_scause(void) { + return raw_read_memory(shadow_state_get_csr_abs_addr(shadow_state_csr::scause)); + } + + void do_write_scause(uint64_t val) { + raw_write_memory(shadow_state_get_csr_abs_addr(shadow_state_csr::scause), val); + } + + uint64_t do_read_stval(void) { + return raw_read_memory(shadow_state_get_csr_abs_addr(shadow_state_csr::stval)); + } + + void do_write_stval(uint64_t val) { + raw_write_memory(shadow_state_get_csr_abs_addr(shadow_state_csr::stval), val); + } + + uint64_t do_read_satp(void) { + return raw_read_memory(shadow_state_get_csr_abs_addr(shadow_state_csr::satp)); + } + + void do_write_satp(uint64_t val) { + raw_write_memory(shadow_state_get_csr_abs_addr(shadow_state_csr::satp), val); + } + + uint64_t do_read_scounteren(void) { + return raw_read_memory(shadow_state_get_csr_abs_addr(shadow_state_csr::scounteren)); + } + + void do_write_scounteren(uint64_t val) { + raw_write_memory(shadow_state_get_csr_abs_addr(shadow_state_csr::scounteren), val); + } + + uint64_t do_read_ilrsc(void) { + return raw_read_memory(shadow_state_get_csr_abs_addr(shadow_state_csr::ilrsc)); + } + + void do_write_ilrsc(uint64_t val) { + raw_write_memory(shadow_state_get_csr_abs_addr(shadow_state_csr::ilrsc), val); + } + + uint64_t do_read_iflags(void) { + return raw_read_memory(shadow_state_get_csr_abs_addr(shadow_state_csr::iflags)); + } + + void do_write_iflags(uint64_t val) { + raw_write_memory(shadow_state_get_csr_abs_addr(shadow_state_csr::iflags), val); + } + + void do_set_iflags_H(void) { + auto old_iflags = read_iflags(); + auto new_iflags = old_iflags | IFLAGS_H_MASK; + write_iflags(new_iflags); + } + + bool do_read_iflags_H(void) { + auto iflags = read_iflags(); + return (iflags & IFLAGS_H_MASK) != 0; + } + + void do_set_iflags_X(void) { + auto old_iflags = read_iflags(); + auto new_iflags = old_iflags | IFLAGS_X_MASK; + write_iflags(new_iflags); + } + + void do_reset_iflags_X(void) { + auto old_iflags = read_iflags(); + auto new_iflags = old_iflags & (~IFLAGS_X_MASK); + write_iflags(new_iflags); + } + + bool do_read_iflags_X(void) { + auto iflags = read_iflags(); + return (iflags & IFLAGS_X_MASK) != 0; + } + + void do_set_iflags_Y(void) { + auto old_iflags = read_iflags(); + auto new_iflags = old_iflags | IFLAGS_Y_MASK; + write_iflags(new_iflags); + } + + void do_reset_iflags_Y(void) { + auto old_iflags = read_iflags(); + auto new_iflags = old_iflags & (~IFLAGS_Y_MASK); + write_iflags(new_iflags); + } + + bool do_read_iflags_Y(void) { + auto iflags = read_iflags(); + return (iflags & IFLAGS_Y_MASK) != 0; + } + + uint8_t do_read_iflags_PRV(void) { + 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(void) { + return raw_read_memory(shadow_state_get_csr_abs_addr(shadow_state_csr::iunrep)); + } + + void do_write_iunrep(uint64_t val) { + raw_write_memory(shadow_state_get_csr_abs_addr(shadow_state_csr::iunrep), val); + } + + uint64_t do_read_clint_mtimecmp(void) { + return raw_read_memory(shadow_state_get_csr_abs_addr(shadow_state_csr::clint_mtimecmp)); + } + + void do_write_clint_mtimecmp(uint64_t val) { + raw_write_memory(shadow_state_get_csr_abs_addr(shadow_state_csr::clint_mtimecmp), val); + } + + uint64_t do_read_plic_girqpend(void) { + return raw_read_memory(shadow_state_get_csr_abs_addr(shadow_state_csr::plic_girqpend)); + } + + void do_write_plic_girqpend(uint64_t val) { + raw_write_memory(shadow_state_get_csr_abs_addr(shadow_state_csr::plic_girqpend), val); + } + + uint64_t do_read_plic_girqsrvd(void) { + return raw_read_memory(shadow_state_get_csr_abs_addr(shadow_state_csr::plic_girqsrvd)); + } + + void do_write_plic_girqsrvd(uint64_t val) { + raw_write_memory(shadow_state_get_csr_abs_addr(shadow_state_csr::plic_girqsrvd), val); + } + + uint64_t do_read_htif_fromhost(void) { + return raw_read_memory(shadow_state_get_csr_abs_addr(shadow_state_csr::htif_fromhost)); + } + + void do_write_htif_fromhost(uint64_t val) { + raw_write_memory(shadow_state_get_csr_abs_addr(shadow_state_csr::htif_fromhost), val); + } + + uint64_t do_read_htif_tohost(void) { + return raw_read_memory(shadow_state_get_csr_abs_addr(shadow_state_csr::htif_tohost)); + } + + void do_write_htif_tohost(uint64_t val) { + raw_write_memory(shadow_state_get_csr_abs_addr(shadow_state_csr::htif_tohost), val); + } + + uint64_t do_read_htif_ihalt(void) { + return raw_read_memory(shadow_state_get_csr_abs_addr(shadow_state_csr::htif_ihalt)); + } + + uint64_t do_read_htif_iconsole(void) { + return raw_read_memory(shadow_state_get_csr_abs_addr(shadow_state_csr::htif_iconsole)); + } + + uint64_t do_read_htif_iyield(void) { + return raw_read_memory(shadow_state_get_csr_abs_addr(shadow_state_csr::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 (int i = 0; i < m_pmas.size(); i++) { + auto &pma = get_pma_entry(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; + f.IO = (istart & PMA_ISTART_IO_MASK) >> PMA_ISTART_IO_SHIFT; + f.E = (istart & PMA_ISTART_E_MASK) >> PMA_ISTART_E_SHIFT; + f.R = (istart & PMA_ISTART_R_MASK) >> PMA_ISTART_R_SHIFT; + f.W = (istart & PMA_ISTART_W_MASK) >> PMA_ISTART_W_SHIFT; + f.X = (istart & PMA_ISTART_X_MASK) >> PMA_ISTART_X_SHIFT; + f.IR = (istart & PMA_ISTART_IR_MASK) >> PMA_ISTART_IR_SHIFT; + f.IW = (istart & PMA_ISTART_IW_MASK) >> PMA_ISTART_IW_SHIFT; + 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 94b8df48b..aae964cf2 100644 --- a/src/virtual-machine.cpp +++ b/src/virtual-machine.cpp @@ -36,6 +36,10 @@ interpreter_break_reason virtual_machine::do_run(uint64_t mcycle_end) { return m_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_uarch_step(const access_log::type &log_type, bool one_based) { return m_machine->log_uarch_step(log_type, one_based); } diff --git a/src/virtual-machine.h b/src/virtual-machine.h index f6fc0c943..ec4d65a13 100644 --- a/src/virtual-machine.h +++ b/src/virtual-machine.h @@ -41,6 +41,7 @@ class virtual_machine : public i_virtual_machine { private: void do_store(const std::string &dir) 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; access_log do_log_uarch_step(const access_log::type &log_type, bool one_based = false) override; machine_merkle_tree::proof_type do_get_proof(uint64_t address, int log2_size) const override; void do_get_root_hash(hash_type &hash) const override; diff --git a/tests/Makefile b/tests/Makefile index f691ef4a2..f420c75af 100644 --- a/tests/Makefile +++ b/tests/Makefile @@ -185,6 +185,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 @@ -255,7 +258,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 74479f79f..75e2eab1a 100755 --- a/tests/lua/cartesi-machine-tests.lua +++ b/tests/lua/cartesi-machine-tests.lua @@ -355,6 +355,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 @@ -607,6 +610,22 @@ local function run_machine(machine, ctx, max_mcycle, advance_machine_fn) ctx.read_htif_tohost_data = machine:read_htif_tohost_data() end +local function run_machine_step(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() + 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) + ctx.read_htif_tohost_data = machine:read_htif_tohost_data() +end + local function advance_machine_with_uarch(machine) if machine:run_uarch() == cartesi.UARCH_BREAK_REASON_UARCH_HALTED then machine:reset_uarch() @@ -980,6 +999,13 @@ elseif command == "run" then check_and_print_result(machine, row) machine:destroy() end) +elseif command == "run_step" then + failures = parallel.run(contexts, jobs, function(row) + local machine = build_machine(row.ram_image) + run_machine_step(machine, row, row.expected_cycles) + check_and_print_result(machine, row) + machine:destroy() + 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 2f56d3d56..9ad1b939c 100755 --- a/tests/lua/machine-bind.lua +++ b/tests/lua/machine-bind.lua @@ -1653,4 +1653,213 @@ 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 = io.open(filename, "rb") + local page_count = string.unpack(" = 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("