Skip to content

Commit 43b413f

Browse files
author
Marcos Pernambuco Motta
committed
fix: sp1 abort reason via public values stream
1 parent 5bda6c9 commit 43b413f

4 files changed

Lines changed: 33 additions & 25 deletions

File tree

sp1/README.md

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -103,9 +103,10 @@ Sepolia gateway; nothing in this repo commits to either choice.
103103
make -C sp1 seal # prove the one-mcycle fixture to a Groth16 seal
104104
make -C sp1 test-solidity # on-chain tests against a Sepolia fork (needs seal)
105105

106-
The reject-fixture tests run the CLI as a subprocess and assert the abort
107-
reason each forged log dies with. They execute without proving, so no GPU
108-
and no artifact download is needed.
106+
The reject-fixture tests assert the abort reason each forged log dies with;
107+
the guest writes it to the public-values stream, the one channel every
108+
executor backend carries back to the host. They execute without proving, so
109+
no GPU and no artifact download is needed.
109110

110111
## The sha256 precompile ABI
111112

sp1/cpp/sp1-runtime.cpp

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
// SP1 runtime primitives from libzkevm.a
2424
extern "C" void syscall_write(uint32_t fd, const uint8_t *buf, size_t nbytes);
2525
extern "C" NO_RETURN void syscall_halt(uint8_t exit_code);
26+
extern "C" void write_output(const uint8_t *output, size_t size);
2627
// On the RV64 target the sha256 precompiles address w and state with an 8-byte
2728
// stride: 64 and 8 u64 slots each holding a u32 value (see sp1-core-executor
2829
// minimal/precompiles/sha256). A u32-packed buffer overruns.
@@ -58,8 +59,13 @@ extern "C" void zk_putchar(char character) {
5859

5960
// abort() itself comes from libzkevm.a; assert() in zk-runtime.hpp calls it.
6061

62+
// The reason goes to the public-values stream as well as stderr: the stream
63+
// travels back to the host on every executor backend, while stderr forwarding
64+
// does not (the x86-64 native executor drops it). A nonzero halt is never
65+
// proven, so the stream carries no committed meaning on this path.
6166
extern "C" NO_RETURN void zk_abort_with_msg(const char *msg) {
6267
syscall_write(2, reinterpret_cast<const uint8_t *>(msg), strlen(msg));
68+
write_output(reinterpret_cast<const uint8_t *>(msg), strlen(msg));
6369
syscall_halt(1);
6470
}
6571

sp1/rust/src/lib.rs

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -120,8 +120,10 @@ pub fn stdin_for_log(log_file_path: &str) -> Result<SP1Stdin, String> {
120120
///
121121
/// The exit code check is not optional. A structurally invalid log makes the
122122
/// guest abort via `zk_abort_with_msg`, which halts with code 1, but SP1's
123-
/// executor returns `Ok` for a nonzero halt: only the abort message reaches the
124-
/// process stderr. Without this check a forged log reads as accepted.
123+
/// executor returns `Ok` for a nonzero halt. The abort reason is read from the
124+
/// public-values stream, where the guest writes it alongside stderr: the
125+
/// stream comes back on every executor backend, while stderr forwarding does
126+
/// not (the x86-64 native executor drops it).
125127
pub async fn try_execute<P: Prover>(
126128
client: &P,
127129
elf: Elf,
@@ -133,7 +135,11 @@ pub async fn try_execute<P: Prover>(
133135
.await
134136
.map_err(|e| format!("{e}"))?;
135137
if report.exit_code != 0 {
136-
return Err(format!("guest aborted with exit code {}", report.exit_code));
138+
let reason = String::from_utf8_lossy(public_values.as_slice());
139+
return Err(format!(
140+
"guest aborted with exit code {}: {reason}",
141+
report.exit_code
142+
));
137143
}
138144
Ok((public_values, report))
139145
}

sp1/rust/tests/test_reject_fixtures.rs

Lines changed: 14 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -18,12 +18,13 @@
1818
//! log. The reject fixtures are structurally invalid step logs and the guest
1919
//! must abort on every one, each for its own reason.
2020
//!
21-
//! The abort reason only surfaces on the process stderr, so the reject
22-
//! assertions run the CLI as a subprocess and match the reason there.
21+
//! The reason arrives through the public-values stream (surfaced in
22+
//! try_execute's error), the one channel every executor backend carries —
23+
//! stderr forwarding is interpreter-only.
2324
//!
2425
//! Generate the fixtures with `make -C sp1 fixtures` (needs a built emulator).
2526
26-
use std::{fs, path::PathBuf, process::Command};
27+
use std::{fs, path::PathBuf};
2728

2829
use cartesi_sp1::try_execute;
2930
use sp1_sdk::{Elf, ProverClient};
@@ -69,8 +70,8 @@ fn guest_elf() -> Elf {
6970
Elf::Static(Box::leak(bytes.into_boxed_slice()))
7071
}
7172

72-
#[test]
73-
fn guest_rejects_forged_logs() {
73+
#[tokio::test]
74+
async fn guest_rejects_forged_logs() {
7475
let dir = fixtures_dir().join("reject-machine");
7576
assert!(
7677
dir.exists(),
@@ -80,8 +81,8 @@ fn guest_rejects_forged_logs() {
8081
let text = fs::read_to_string(dir.join("_manifest.csv"))
8182
.unwrap_or_else(|e| panic!("failed to read reject manifest: {e}"));
8283

83-
let cli = env!("CARGO_BIN_EXE_cartesi-sp1-cli");
84-
let guest = repo_root().join("sp1/cpp/guest.elf");
84+
let client = ProverClient::builder().light().build().await;
85+
let elf = guest_elf();
8586
let mut checked = 0;
8687
for line in text.lines().skip(1) {
8788
if line.is_empty() {
@@ -96,21 +97,15 @@ fn guest_rejects_forged_logs() {
9697
let (name, tag) = (cols[1], cols[2]);
9798
let path = dir.join(name);
9899

99-
let output = Command::new(cli)
100-
.args(["--guest-elf", guest.to_str().unwrap()])
101-
.args(["execute", path.to_str().unwrap()])
102-
.output()
103-
.unwrap_or_else(|e| panic!("running {cli}: {e}"));
104-
assert!(
105-
!output.status.success(),
106-
"guest ACCEPTED forged log {name} (tag {tag})"
107-
);
108-
let stderr = String::from_utf8_lossy(&output.stderr);
100+
let error = try_execute(&client, elf.clone(), path.to_str().unwrap())
101+
.await
102+
.err()
103+
.unwrap_or_else(|| panic!("guest ACCEPTED forged log {name} (tag {tag})"));
109104
let expected = expected_message(tag);
110105
assert!(
111-
stderr.contains(expected),
106+
error.contains(expected),
112107
"forged log {name} (tag {tag}) rejected for the wrong reason:\n \
113-
expected substring {expected:?}\n stderr: {stderr}"
108+
expected substring {expected:?}\n error: {error}"
114109
);
115110
checked += 1;
116111
}

0 commit comments

Comments
 (0)