Updated 2026-08-20 after #1821. That PR rewrote docs/pages/tutorials/write-e3-program.mdx
against Risc0ComputeProof and added the input-root invariants to agent/INVARIANTS.md, which
resolved four of the five items this issue originally listed under "Stale copies". What follows is
what still reproduces, on main at 3c7172c8. #1821 also widened the remaining item and added a
second one, because the guest and the ComputeProvider trait both changed.
The guest commits ComputeJournal: nine 32-byte values, in the order chain ID, Interfold address,
E3 ID, encryption scheme ID, committee public-key hash, ciphertext-output hash, SAFE commitment,
parameter hash, and input root (crates/support/types/src/lib.rs:69-79). The encoding is RISC Zero
serde rather than ABI: one length word plus 32 byte words per value, which gives 132 bytes each and
1,188 in total. Risc0ComputeProof.sol:13-14 mirrors it with FIELD_SIZE = 132 and
FIELD_COUNT = 9.
The guest example commits the wrong thing
docs/pages/putting-it-together.mdx:25-48 shows a guest that reads FHEInputs and commits the
processor's raw output:
let fhe_inputs: FHEInputs = env::read();
let result = fhe_processor(&fhe_inputs);
env::commit(&result);
The guest in the repository reads a ComputeGuestInput from stdin, runs the Secure Process under
the user program's policy, and commits the journal built from the result
(crates/support/methods/guest/src/bin/program.rs:18-28):
let input: ComputeGuestInput = deserialize(&decode_input(&input_slice).unwrap()).unwrap();
let result = input.input.process(fhe_processor, policy()).unwrap();
let journal = ComputeJournal::new(input.domain, result).unwrap();
env::commit(&journal);
A guest copied from the page emits a journal holding the raw ciphertext bytes.
Risc0BfvCiphertextVerifier.verify rebuilds the digest from chain state plus the envelope's input
root and passes it to the RISC Zero verifier (Risc0BfvCiphertextVerifier.sol:38-65), so that
receipt does not verify. No incorrect result is accepted; the cost is a developer who cannot ship.
The page also predates InputPolicy. ComputeInput::process now takes one, and the page's call has
no place to put it.
The provider example on the same page has the same drift
docs/pages/putting-it-together.mdx:57-90 declares:
fn prove(&self, input: &ComputeInput) -> Self::Output
#1821 gave the trait method a second parameter, policy: InputPolicy
(crates/compute-provider/src/ciphertext_output.rs:21), and both implementations in the repository
carry it (crates/support/host/src/lib.rs:69, :403). An impl copied from the page does not
satisfy the trait.
The layout is written out in more places than it is checked
crates/support/host/src/lib.rs:535-581 pins the length and a SHA-256 digest on the Rust side, in
compute_result_journal_matches_crisp_layout. That test passes today, and the digest it pins,
4403934eb9404372…, appears nowhere under packages, examples, or templates. The TypeScript
side has its own encodeVec32 and concatenates the fields independently
(packages/interfold-contracts/test/Risc0BfvCiphertextVerifier.spec.ts:19-23, :56), and no shared
fixture links the two. Each side can change its implementation and its own expectation together and
still pass while disagreeing across the layer boundary.
Happy to open a PR for the page if it is useful.
The guest commits
ComputeJournal: nine 32-byte values, in the order chain ID, Interfold address,E3 ID, encryption scheme ID, committee public-key hash, ciphertext-output hash, SAFE commitment,
parameter hash, and input root (
crates/support/types/src/lib.rs:69-79). The encoding is RISC Zeroserde rather than ABI: one length word plus 32 byte words per value, which gives 132 bytes each and
1,188 in total.
Risc0ComputeProof.sol:13-14mirrors it withFIELD_SIZE = 132andFIELD_COUNT = 9.The guest example commits the wrong thing
docs/pages/putting-it-together.mdx:25-48shows a guest that readsFHEInputsand commits theprocessor's raw output:
The guest in the repository reads a
ComputeGuestInputfrom stdin, runs the Secure Process underthe user program's policy, and commits the journal built from the result
(
crates/support/methods/guest/src/bin/program.rs:18-28):A guest copied from the page emits a journal holding the raw ciphertext bytes.
Risc0BfvCiphertextVerifier.verifyrebuilds the digest from chain state plus the envelope's inputroot and passes it to the RISC Zero verifier (
Risc0BfvCiphertextVerifier.sol:38-65), so thatreceipt does not verify. No incorrect result is accepted; the cost is a developer who cannot ship.
The page also predates
InputPolicy.ComputeInput::processnow takes one, and the page's call hasno place to put it.
The provider example on the same page has the same drift
docs/pages/putting-it-together.mdx:57-90declares:#1821 gave the trait method a second parameter,
policy: InputPolicy(
crates/compute-provider/src/ciphertext_output.rs:21), and both implementations in the repositorycarry it (
crates/support/host/src/lib.rs:69,:403). Animplcopied from the page does notsatisfy the trait.
The layout is written out in more places than it is checked
crates/support/host/src/lib.rs:535-581pins the length and a SHA-256 digest on the Rust side, incompute_result_journal_matches_crisp_layout. That test passes today, and the digest it pins,4403934eb9404372…, appears nowhere underpackages,examples, ortemplates. The TypeScriptside has its own
encodeVec32and concatenates the fields independently(
packages/interfold-contracts/test/Risc0BfvCiphertextVerifier.spec.ts:19-23,:56), and no sharedfixture links the two. Each side can change its implementation and its own expectation together and
still pass while disagreeing across the layer boundary.
Happy to open a PR for the page if it is useful.