Improve handling of revert-root-hash
enhancement
## 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 条评论