Skip to content

Commit e60f6eb

Browse files
committed
feat: turn send_cmio_response failures into no-ops in log and verify
The core send_cmio_response can no longer fail. If verification could fail for a reachable machine state and response, there would be states for which the honest party cannot produce a log that proves the resulting state transition. The iflags.Y and response-length checks now return without touching the state, like the advance-state yield check already did. The length check also moves ahead of the revert root hash write, where it can still prevent all state changes. iflags.Y remains the first access, so a no-op log is never empty, which replay requires. The host-facing machine::send_cmio_response still refuses misuse upfront. check_pending_cmio_request takes over the dropped checks, requiring a manual yield for every response reason and a response that fits in the rx buffer, with the same error messages as before. machine::log_send_cmio_response no longer fails for any machine state or response argument and logs a no-op instead.
1 parent 331e86e commit e60f6eb

8 files changed

Lines changed: 142 additions & 30 deletions

File tree

Makefile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ DOWNLOADDIR := downloads
44
SRC_DIR := src
55

66
EMULATOR_VERSION ?= v0.21.0
7-
EMULATOR_TAG ?= -test1
7+
EMULATOR_TAG ?= -test2
88

99
SOLIDITY_VERSION ?= 0.8.30
1010

helper_scripts/generate_EmulatorConstants.lua

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ out:write(' uint64 constant HTIF_FROMHOST_ADDRESS = 0x' ..
3939
hex(cartesi.machine:get_reg_address("htif_fromhost")) .. ';\n')
4040
out:write(' uint64 constant HTIF_TOHOST_ADDRESS = 0x' ..
4141
hex(cartesi.machine:get_reg_address("htif_tohost")) .. ';\n')
42-
out:write(' uint8 constant CMIO_YIELD_REASON_ADVANCE_STATE = 0x' ..
42+
out:write(' uint8 constant HTIF_YIELD_REASON_ADVANCE_STATE = 0x' ..
4343
hex(cartesi.HTIF_YIELD_REASON_ADVANCE_STATE) .. ';\n')
4444
out:write(' uint32 constant HASH_TREE_LOG2_WORD_SIZE = 0x' .. hex(cartesi.HASH_TREE_LOG2_WORD_SIZE) .. ';\n')
4545
out:write(' uint32 constant HASH_TREE_WORD_SIZE = uint32(1) << HASH_TREE_LOG2_WORD_SIZE;\n')

shasum-download

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
1-
b52a2b54cf18b1215108281ed5c6df93ed4ba3922a09f4155d9f945e703f9fd7 downloads/machine-emulator-tests-data.deb
2-
5bfb82f7a6665fe264d7d849d8b90f7b476de5f17d01a2743c15952bf8de0069 downloads/uarch-riscv-tests-json-logs.tar.gz
1+
7ae0df341e8d14b08461748a9f2f639d1cc23bf6c7f1a1aa31185503fef56230 downloads/machine-emulator-tests-data.deb
2+
b828dc0be96c3f46d61fd973e730a13e44afaca0a95670f7c56371cfc784d705 downloads/uarch-riscv-tests-json-logs.tar.gz

src/EmulatorCompat.sol

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -187,6 +187,16 @@ library EmulatorCompat {
187187
);
188188
}
189189

190+
function readHtifTohost(AccessLogs.Context memory a)
191+
internal
192+
pure
193+
returns (uint64)
194+
{
195+
return a.readWord(
196+
EmulatorConstants.HTIF_TOHOST_ADDRESS.toPhysicalAddress()
197+
);
198+
}
199+
190200
// Conversions and arithmetic functions
191201

192202
function int8ToUint64(int8 val) internal pure returns (uint64) {
@@ -380,4 +390,26 @@ library EmulatorCompat {
380390

381391
return n;
382392
}
393+
394+
function isYieldedManualWith(uint64 tohost, uint64 yieldReason)
395+
internal
396+
pure
397+
returns (bool)
398+
{
399+
uint64 dev = uint64ShiftRight(
400+
tohost & EmulatorConstants.HTIF_DEV_MASK,
401+
EmulatorConstants.HTIF_DEV_SHIFT
402+
);
403+
uint64 cmd = uint64ShiftRight(
404+
tohost & EmulatorConstants.HTIF_CMD_MASK,
405+
EmulatorConstants.HTIF_CMD_SHIFT
406+
);
407+
uint64 reason = uint64ShiftRight(
408+
tohost & EmulatorConstants.HTIF_REASON_MASK,
409+
EmulatorConstants.HTIF_REASON_SHIFT
410+
);
411+
return dev == EmulatorConstants.HTIF_DEV_YIELD
412+
&& cmd == EmulatorConstants.HTIF_YIELD_CMD_MANUAL
413+
&& reason == yieldReason;
414+
}
383415
}

src/EmulatorConstants.sol

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ library EmulatorConstants {
4646
uint64 constant IFLAGS_Y_ADDRESS = 0x300;
4747
uint64 constant HTIF_FROMHOST_ADDRESS = 0x330;
4848
uint64 constant HTIF_TOHOST_ADDRESS = 0x328;
49-
uint8 constant CMIO_YIELD_REASON_ADVANCE_STATE = 0x0;
49+
uint8 constant HTIF_YIELD_REASON_ADVANCE_STATE = 0x0;
5050
uint32 constant HASH_TREE_LOG2_WORD_SIZE = 0x5;
5151
uint32 constant HASH_TREE_WORD_SIZE = uint32(1) << HASH_TREE_LOG2_WORD_SIZE;
5252
uint32 constant HTIF_DEV_SHIFT = 0x38;

src/SendCmioResponse.sol

Lines changed: 24 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -36,15 +36,29 @@ library SendCmioResponse {
3636
bytes32 dataHash,
3737
uint32 dataLength
3838
) internal pure {
39+
// This function cannot fail. When a failure is detected, the operation is a no-op instead,
40+
// so the honest party can always log and prove the resulting state transition.
41+
// A response to a machine that is not waiting on a manual yield is a no-op.
3942
if (!EmulatorCompat.readIflagsY(a)) {
40-
EmulatorCompat.throwRuntimeError(a, "iflags.Y is not set");
43+
return;
44+
}
45+
if (reason == EmulatorConstants.HTIF_YIELD_REASON_ADVANCE_STATE) {
46+
// Advance-state responses are the input boundary of the rollups flow. They only apply to a
47+
// machine waiting for an input on an rx-accepted manual yield. Sending one to a machine that
48+
// yielded manual with any other reason (e.g., rejected an input or threw an exception) is a no-op.
49+
uint64 tohost = EmulatorCompat.readHtifTohost(a);
50+
if (!EmulatorCompat.isYieldedManualWith(
51+
tohost,
52+
EmulatorConstants.HTIF_YIELD_MANUAL_REASON_RX_ACCEPTED
53+
)) {
54+
return;
55+
}
4156
}
42-
// Record the machine root hash to revert to in case the response is eventually rejected
43-
EmulatorCompat.writeRevertRootHash(a, revertRootHash);
4457
// A zero length data is a valid response. We just skip writing to the rx buffer.
58+
uint32 writeLengthLog2Size = 0;
4559
if (dataLength > 0) {
4660
// Find the write length: the smallest power of 2 that is >= dataLength and >= tree leaf size
47-
uint32 writeLengthLog2Size = EmulatorCompat.uint32Log2(dataLength);
61+
writeLengthLog2Size = EmulatorCompat.uint32Log2(dataLength);
4862
if (
4963
writeLengthLog2Size < EmulatorConstants.HASH_TREE_LOG2_WORD_SIZE
5064
) {
@@ -56,14 +70,17 @@ library SendCmioResponse {
5670
) {
5771
writeLengthLog2Size += 1;
5872
}
73+
// A response with data that does not fit in the rx buffer is a no-op
5974
if (
6075
writeLengthLog2Size
6176
> EmulatorConstants.AR_CMIO_RX_BUFFER_LOG2_SIZE
6277
) {
63-
EmulatorCompat.throwRuntimeError(
64-
a, "CMIO response data is too large"
65-
);
78+
return;
6679
}
80+
}
81+
// Record the machine root hash to revert to in case the response is eventually rejected
82+
EmulatorCompat.writeRevertRootHash(a, revertRootHash);
83+
if (dataLength > 0) {
6784
a.writeRegion(
6885
Memory.regionFromPhysicalAddress(
6986
EmulatorConstants.AR_CMIO_RX_BUFFER_START

src/UArchReset.sol

Lines changed: 4 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -36,24 +36,10 @@ library UArchReset {
3636
uint64 tohost = EmulatorCompat.readWord(
3737
a, EmulatorConstants.HTIF_TOHOST_ADDRESS
3838
);
39-
uint64 dev = EmulatorCompat.uint64ShiftRight(
40-
tohost & EmulatorConstants.HTIF_DEV_MASK,
41-
EmulatorConstants.HTIF_DEV_SHIFT
42-
);
43-
uint64 cmd = EmulatorCompat.uint64ShiftRight(
44-
tohost & EmulatorConstants.HTIF_CMD_MASK,
45-
EmulatorConstants.HTIF_CMD_SHIFT
46-
);
47-
uint64 reason = EmulatorCompat.uint64ShiftRight(
48-
tohost & EmulatorConstants.HTIF_REASON_MASK,
49-
EmulatorConstants.HTIF_REASON_SHIFT
50-
);
51-
if (
52-
dev == EmulatorConstants.HTIF_DEV_YIELD
53-
&& cmd == EmulatorConstants.HTIF_YIELD_CMD_MANUAL
54-
&& reason
55-
== EmulatorConstants.HTIF_YIELD_MANUAL_REASON_RX_REJECTED
56-
) {
39+
if (EmulatorCompat.isYieldedManualWith(
40+
tohost,
41+
EmulatorConstants.HTIF_YIELD_MANUAL_REASON_RX_REJECTED
42+
)) {
5743
EmulatorCompat.revertState(a);
5844
}
5945
}

test/SendCmioResponse.t.sol

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,8 @@ contract SendCmioResponse_Test is AccessLogJsonParse {
3434
string constant JSON_PATH = "./test/uarch-log/";
3535
string constant CATALOG_PATH = "catalog.json";
3636
string constant SEND_CMIO_RESPONSE_PATH = "send-cmio-response-steps.json";
37+
string constant SEND_CMIO_RESPONSE_NOOP_PATH =
38+
"send-cmio-response-noop-steps.json";
3739

3840
uint256 constant siblingsLength = 59;
3941

@@ -115,6 +117,81 @@ contract SendCmioResponse_Test is AccessLogJsonParse {
115117
}
116118
}
117119

120+
function testSendCmioResponseNoop() public {
121+
Entry[] memory catalog =
122+
loadCatalog(string.concat(JSON_PATH, CATALOG_PATH));
123+
string memory noopLog =
124+
string.concat(JSON_PATH, SEND_CMIO_RESPONSE_NOOP_PATH);
125+
126+
// all tests combined can easily run out of gas, stop metering
127+
// also raise memory_limit in foundry.toml per https://github.com/foundry-rs/foundry/issues/3971
128+
vm.pauseGasMetering();
129+
// create a large buffer and reuse it
130+
bytes memory buffer = new bytes(100 * (siblingsLength + 1) * 32);
131+
bool found = false;
132+
133+
for (uint256 i = 0; i < catalog.length; i++) {
134+
if (
135+
keccak256(abi.encodePacked(catalog[i].logFilename))
136+
!= keccak256(
137+
abi.encodePacked("send-cmio-response-noop-steps.json")
138+
)
139+
) {
140+
continue;
141+
}
142+
found = true;
143+
console.log("Replaying log file %s ...", catalog[i].logFilename);
144+
145+
string memory rj = loadJsonLog(noopLog);
146+
147+
bytes32 initialRootHash = vm.parseBytes32(
148+
string.concat("0x", catalog[i].initialRootHash)
149+
);
150+
bytes32 finalRootHash =
151+
vm.parseBytes32(string.concat("0x", catalog[i].finalRootHash));
152+
// the log was taken from a machine that yielded manual with reason
153+
// rx-rejected, so the advance-state response must be a no-op
154+
assertEq(
155+
initialRootHash,
156+
finalRootHash,
157+
"initial and final root hashes must match"
158+
);
159+
160+
loadBufferFromRawJson(buffer, rj);
161+
162+
AccessLogs.Context memory accessLogs =
163+
AccessLogs.Context(initialRootHash, Buffer.Context(buffer, 0));
164+
165+
// Prepare arguments for sendCmioResponse
166+
// These values are hard-coded in order to match the values used when generating the test log file
167+
uint16 reason = EmulatorConstants.HTIF_YIELD_REASON_ADVANCE_STATE;
168+
bytes memory response = bytes("This is a test cmio response");
169+
bytes memory paddedResponse = new bytes(32);
170+
for (uint256 j = 0; j < response.length; j++) {
171+
paddedResponse[j] = response[j];
172+
}
173+
bytes32 paddedResponseHash = keccak256(paddedResponse);
174+
// call sendCmioResponse
175+
// the test log file was generated passing the initial root hash as the revert root hash
176+
SendCmioResponse.sendCmioResponse(
177+
accessLogs,
178+
initialRootHash,
179+
reason,
180+
paddedResponseHash,
181+
uint32(response.length)
182+
);
183+
// ensure that the final root hash matches the expected value
184+
assertEq(
185+
accessLogs.currentRootHash,
186+
finalRootHash,
187+
"final root hash must match"
188+
);
189+
}
190+
assertTrue(
191+
found, "catalog has no send-cmio-response-noop-steps.json entry"
192+
);
193+
}
194+
118195
function loadCatalog(string memory path)
119196
private
120197
view

0 commit comments

Comments
 (0)