ITADN

Improve handling of revert-root-hash

#388Opendiegonehab 创建于 2026-06-10
enhancement
D
diegonehabcommented
## Context This is part of the rollup "revert on reject" feature. `revert_root_hash` is a 32-byte field in the shadow state (at `AR_SHADOW_REVERT_ROOT_HASH_START`) that records the machine root hash as it was *before* an input was fed. When the guest later rejects an advance (a manual yield with reason `RX_REJECTED`), the dispute/verifier must prove the machine reverts to that previously-committed state rather than to the post-reject machine hash — which the verifier cannot reconstruct on its own, so the value must already live in the hashed state. There are two sides to the feature: - **Write side (this plan):** `send_cmio_response` records `revert_root_hash` as part of its (logged, verifiable) operation, so the value is committed into the merkle tree. - **Read side (future, out of scope here):** `verify_step` / `verify_reset_uarch` detect a rejected manual yield and substitute the stored `revert_root_hash` for the post-state root hash. This plan only ensures the accessors and shapes are in place so that work drops in cleanly. The trigger for this work: the revert hash currently has to be poked in by the host via a separate `write_revert_root_hash` call before `send_cmio_response`. We want the value to be a first-class, logged input of `send_cmio_response` itself, threaded through every interface layer, so it is part of the proof. ## Already done (before this plan) Verb-prefix rename `get_/set_` -> `read_/write_` for the revert hash, across all sources and tests (the `revert_root_hash` noun for fields/locals is unchanged): - `machine` / `i_machine` / `local_machine` / `jsonrpc_machine`: `read_revert_root_hash` / `write_revert_root_hash` (+ `do_` virtuals) - C API: `cm_read_revert_root_hash` / `cm_write_revert_root_hash` (`cm.h`, `cm.cpp`) - JSON-RPC: wire methods `machine.read_revert_root_hash` / `machine.write_revert_root_hash` (handlers + dispatch in `jsonrpc-remote-machine.cpp`, schema in `jsonrpc-discover.json`) - Lua methods `machine:read_revert_root_hash` / `machine:write_revert_root_hash` (`clua-i-machine.cpp`, caller in `cartesi-machine.lua`) - Tests: `tests/lua/machine-bind.lua`, `tests/misc/test-machine-c-api.cpp` Verified: no `get_/set_revert_root_hash` or `getRevertRootHash/setRevertRootHash` remain in `src/` or `tests/` (the generated `jsonrpc-discover.cpp` rebuilds from the `.json`). ## Design decisions 1. **Naming:** `read_/write_revert_root_hash` everywhere — it is stored state (a leaf), not a computed value like `get_root_hash`. 2. **All three entry points receive the hash as the first argument:** `send_cmio_response`, `log_send_cmio_response`, and `verify_send_cmio_response`. 3. **The shared core `cartesi::send_cmio_response` takes it** as its first argument (after the state accessor) and writes it as its first state mutation, *after* the `iflags.Y` precondition check (don't mutate if the precondition fails; this becomes log access #2). 4. **The write is a single full-leaf write** — `revert_root_hash` is exactly one hash-tree word (`HASH_TREE_WORD_SIZE` == 32 bytes, `HASH_TREE_LOG2_WORD_SIZE` == 5), so the access replaces the whole leaf at `AR_SHADOW_REVERT_ROOT_HASH_START`. Note this is *not* the same as the existing per-register accessors (`do_write_iflags_Y`, `write_htif_fromhost`): those write an 8-byte *machine* word and their record/replay path splices a single `uint64` into the enclosing leaf, so they cannot express a 32-byte write. The single full-leaf write logs `read_hash` = old leaf hash and `written_hash` = `keccak256(revert_root_hash)`, which is exactly Solidity's `writeLeaf(stride, keccak256(hash))` and exactly the single-leaf (`data_length == write_length == 32`, no padding) case of the existing `do_write_memory_with_padding` — that code path was hoisted into label-parameterized helpers and reused literally (see work item 1). 5. **Type convention:** the hash is `const_machine_hash_view` at every layer. That covers the C++ interfaces (`i_machine`/`local`/`jsonrpc`/`machine`, where the existing `read_/write_revert_root_hash` plumbing already uses it), the state-access accessors, and the templated core (`machine_hash` and `cm_hash`-converted values bind to the view implicitly). This means changing the seeded `machine::send_cmio_response(const machine_hash &, ...)` declaration to take `const_machine_hash_view`. The Solidity-compat dialect names the same type through the alias `bytes32` (in `uarch-solidity-compat.hpp`), so the core signature reads `bytes32 revertRootHash` and maps to the generated Solidity verbatim. Raw pointers appear only inside the record/replay accessors, which pass `hash.data()`/`hash.size()` to the generic region-write helpers. 6. **Solidity (`machine-solidity-step`) is renamed too**, to match the C++ house style (see work item 5). `EmulatorCompat.getRevertRootHash/setRevertRootHash` -> `readRevertRootHash/writeRevertRootHash`; the auto-generated `SendCmioResponse.sol` then regenerates from the C++ template once the C++ compat function is named `writeRevertRootHash`. ## Work items ### 1. Leaf-level write accessor (the delicate, security-critical part) Add `write_revert_root_hash` to the state-access layer, producing/consuming an access-log entry identical in shape to a single-leaf region write. - `src/uarch-solidity-compat.hpp`: add `using bytes32 = const_machine_hash_view;` to the Solidity type aliases, and `template <typename State> void writeRevertRootHash(State &a, bytes32 revertRootHash)` -> `a.write_revert_root_hash(revertRootHash)` (mirrors `writeHtifFromhost`). - `src/i-state-access.hpp`: add `write_revert_root_hash(const_machine_hash_view hash)` that dispatches to `derived().do_write_revert_root_hash(hash)` (place near `write_memory_with_padding`). This pulls `machine-hash.hpp` into the uarch and zk builds, which is fine (its `std::vector` part is already guarded for ZKARCHITECTURE and the uarch cross-build compiles it). - `src/state-access.hpp`: `do_write_revert_root_hash` copies the view into `m_s.shadow.revert_root_hash` (direct, like `do_write_htif_fromhost`). - `src/record-send-cmio-state-access.hpp`: `do_write_revert_root_hash` logs a full-leaf write at `AR_SHADOW_REVERT_ROOT_HASH_START` — get proof for the leaf (`read_hash` = `proof.get_target_hash()`), `written_hash` = `get_merkle_tree_hash` of the 32 supplied bytes, then `write_memory` + `update_hash_tree`. Implemented by hoisting the body of `do_write_memory_with_padding` into a label-parameterized `log_write_memory_with_padding` helper that both accessors call, specialized here to one leaf with `data_length == write_length == HASH_TREE_WORD_SIZE`. (The hoist also fixed the logged read data to be copied from the write offset within the range instead of the range start.) - `src/replay-send-cmio-state-access.hpp`: `do_write_revert_root_hash` verifies the leaf write — consume the access, check `address == AR_SHADOW_REVERT_ROOT_HASH_START` and `log2_size == HASH_TREE_LOG2_WORD_SIZE`, check `written_hash == get_hash(supplied bytes)`, verify proof against current root, `bubble_up`. Implemented by hoisting `do_write_memory_with_padding`'s body into a shared `check_write_memory_with_padding` helper, same as on the record side. (Read accessor `do_read_revert_root_hash` is intentionally deferred to the `verify_step`/`verify_reset_uarch` work; `send_cmio_response` only writes.) ### 2. Core templated function - `src/send-cmio-response.hpp` / `src/send-cmio-response.cpp`: change signature to `send_cmio_response(STATE_ACCESS a, bytes revert_root_hash, uint16 reason, bytes data, uint32 dataLength)`. After the `readIflagsY` guard, call `writeRevertRootHash(a, revert_root_hash)` before writing the rx buffer. Update the three explicit template instantiations (`state_access`, `record_send_cmio_state_access`, `replay_send_cmio_state_access`). ### 3. Thread the parameter through every interface Add `revert_root_hash` as the first argument to all three entry points and forward it: - `src/machine.hpp` / `machine.cpp`: change the seeded `send_cmio_response` first param to `const_machine_hash_view`, and give `log_send_cmio_response` and `verify_send_cmio_response` the same `const_machine_hash_view revert_root_hash` first param; each passes `revert_root_hash.data()` to `cartesi::send_cmio_response`. - `src/i-machine.hpp`: the three wrappers + their `do_` virtuals (first param `const_machine_hash_view`). - `src/local-machine.{hpp,cpp}`: forward to `machine`. - `src/jsonrpc-machine.{hpp,cpp}`: client encodes the hash to base64 and adds it as the first request arg (pattern from `do_write_revert_root_hash`). - `src/jsonrpc-remote-machine.cpp`: server handlers parse the hash first (pattern from `jsonrpc_machine_write_revert_root_hash_handler` using `parse_args<cartesi::machine_hash, ...>`). - `src/jsonrpc-discover.json`: add the `revert_root_hash` (`Base64Hash`) param to the `send_cmio_response`, `log_send_cmio_response`, and `verify_send_cmio_response` schemas. - `src/cm.h` / `cm.cpp`: add the `const cm_hash *revert_root_hash` first param to `cm_log_send_cmio_response` and `cm_verify_send_cmio_response` (`cm_send_cmio_response` already has it); convert and forward. - `src/clua-i-machine.cpp`: the three Lua bindings read the hash as the first arg (`clua_check_cm_hash` at index 2), shifting reason/data/log_type down by one. ### 4. Callers, tests, docs - `src/cartesi-machine.lua`: in `load_cmio_input` (advance) and `load_cmio_query` (inspect), pass `machine:get_root_hash()` as the first arg to `send_cmio_response` and drop the now redundant explicit `machine:write_revert_root_hash(...)` line (the send writes it). - `src/test-collect-hashes.lua`, `tests/lua/cmio-test.lua`, `tests/lua/uarch-riscv-tests.lua`, `tests/lua/spec-verify-uarch-failure.lua`, `tests/lua/machine-bind.lua`: update every `send_/log_/verify_send_cmio_response` call site to pass the hash first. - **Access-log expectations change** (adding the revert-hash write inserts one access): in `machine-bind.lua`, the happy-path and sizes tests go from 4 to 5 accesses and the zero-bytes test from 3 to 4; the "Dump of log produced by send_cmio_response should match" expected text gains the new revert-hash write line (as access #2). Update these and the access-pattern comment in `spec-verify-uarch-failure.lua`. - `doc/recipes/run-rolling-calculator.lua` and `doc/README.md.template`: replace the `write_revert_root_hash` + `send_cmio_response` two-step with the single `send_cmio_response(machine:get_root_hash(), ...)` call, and adjust the surrounding prose. Show the `README.md.template` diff in chat before applying. Generated `doc/README.md`, `README.html`, and `recipes/cache/*` are left to regenerate. ### 5. Solidity rename (`machine-solidity-step`) - `src/EmulatorCompat.sol`: rename `getRevertRootHash` -> `readRevertRootHash` and `setRevertRootHash` -> `writeRevertRootHash`. The function bodies and the `revertRootHash` / `hashOfRevertRootHash` locals are unchanged. - `helper_scripts/generate_SendCmioResponse.sh`: its sed rules are coupled to the C++ source layout. Update the signature-replacement rule for the new `bytes32 revertRootHash` parameter and broaden the explicit-instantiation cleanup rule to match the reflowed continuation lines. - `helper_scripts/generate_EmulatorConstants.lua`: the Lua lookups still used the removed `cartesi.CMIO_YIELD_*` names. Switch them to `cartesi.HTIF_YIELD_*` (the emitted Solidity constant names keep the `CMIO_` prefix for now). Regenerate `src/EmulatorConstants.sol`, which also picks up the branch's new `UARCH_PRISTINE_STATE_HASH`. - `test/SendCmioResponse.t.sol` calls `sendCmioResponse` and gains the `revertRootHash` argument. Pass the catalog's `initialRootHash`, matching what the emulator-side log generator records. - Regenerating `src/SendCmioResponse.sol` from the emulator (work item 2) emits the `EmulatorCompat.writeRevertRootHash(...)` call. - `EmulatorConstants.REVERT_ROOT_HASH_ADDRESS` is a noun/address and stays. ### 6. Out of scope here, but the design must stay compatible (the read side) This section records the conclusions of this session's design discussion so the follow-up work is not re-derived. `send_cmio_response` is only the **write** side of the feature; the **read** side lands on `step`/`uarch_step`/`reset_uarch` and is implemented later. **The shared concept (revert-on-reject).** All of the post-operation verifiers converge on one rule: > After replaying the operation, if the machine ended in a *manual-yield rejected* state, > the expected `root_hash_after` is the stored `revert_root_hash`; otherwise it is the > machine's actual root hash. This is needed because a verifier cannot reconstruct the pre-input state on its own — the value has to already be committed in the hashed state, which is precisely what `send_cmio_response` does on the write side. **The predicate** ("manual-yield rejected"): `iflags.Y != 0` (a manual yield is pending) **and** `htif.tohost` decodes to cmd == `HTIF_YIELD_CMD_MANUAL` with reason == `HTIF_YIELD_MANUAL_REASON_RX_REJECTED`. Detecting it reads `iflags.Y` and `htif.tohost`; on a hit it then reads `revert_root_hash`. **Why `send_cmio_response` is write-only.** Its last action is `writeIflagsY(a, 0)`, so its post-state is never a pending manual yield — the predicate can never fire at the end of a send. Therefore `verify_send_cmio_response` needs no substitution and `send_cmio_response` never needs the read side. The operations that *land* the machine in a rejected manual yield are `step`/`uarch_step`/`reset_uarch`, and that is where the substitution belongs. **"Touching" the registers.** For replay to make the same branch decision deterministically, the recorded operation must *read* `iflags.Y`, `htif.tohost`, and (on a hit) `revert_root_hash` at its end, so those accesses are in the log. Hence `reset_uarch` (and `step`) must touch those three at completion. Because record and replay see identical state values, they take the same conditional branch and their access sequences match. **Factoring.** A templated free function over `STATE_ACCESS`, e.g. `is_rejected_manual_yield(a)` (reads `iflags.Y` + `htif.tohost`), plus the read accessor below — callable identically under record (logs the reads) and replay (checks them). Each `verify_X`, after `a.finish()`, computes `expected = is_rejected_manual_yield(a) ? read_revert_root_hash(a) : a.get_root_hash()` and compares to `root_hash_after`; each `log_X` calls the same reads at the same point so the logs line up. Place the helper where `send-cmio-response.cpp`-style Solidity-translatable code lives so it can be shared by the uarch/step paths and their Solidity equivalents. **The read accessor (the missing half of the pair).** Add `do_read_revert_root_hash` / compat `readRevertRootHash` as a single full-leaf *read* at `AR_SHADOW_REVERT_ROOT_HASH_START` (returns the 32 bytes, verifying they hash to the leaf), mirroring Solidity `readRevertRootHash` (`readLeaf` + the `keccak256(value) == leafHash` check, formerly `getRevertRootHash`). It is the read counterpart of the `write` accessor added in work item 1; this is why this plan adds a dedicated read/write *pair* rather than reusing `write_memory_with_padding` — the read side has no padding-write analog and must be a dedicated leaf read. This work item is **not** implemented now; it is documented so work item 1's accessor and naming choices (dedicated leaf-level read/write pair) are made with it in mind. ## Verification - `make` — builds the emulator and regenerates `jsonrpc-discover.cpp` from the `.json`. - `make format` / `make lint`. - `make test-misc` — C API tests, incl. `revert_root_hash_round_trip_test` and the null-arg cases. - Lua bind tests (`tests/lua/machine-bind.lua`): `read/write_revert_root_hash` round trip; `send_cmio_response` happy path / different sizes / zero bytes (with updated access counts); the log-dump match (with the new access line); `verify_send_cmio_response` still accepts the round-tripped log and rejects a bad final hash. - `tests/lua/cmio-test.lua` for both `local` and `jsonrpc` machine types (exercises the rollup advance/inspect loop end-to-end through the new signature and wire protocol). - Confirm the new access appears as a single-leaf write at `AR_SHADOW_REVERT_ROOT_HASH_START` with `log2_size == 5`, and that record then replay (`log_send_cmio_response` -> `verify_send_cmio_response`) round-trips. - `machine-solidity-step`: regenerate `src/SendCmioResponse.sol` from the emulator template, confirm it calls `EmulatorCompat.writeRevertRootHash(...)`, regenerate the uarch json test logs from the updated emulator (`make test-generate-uarch-logs` in machine-emulator) and install them in `test/uarch-log/`, then build and run the Solidity tests (incl. `test/SendCmioResponse.t.sol`) with the CI-pinned Foundry v1.4.3 (newer Foundry rejects the duplicated `Entry` struct name in `vm.parseJsonTypeArray`).
0 条评论