From efb46bf93e9935f03b70465e6780fe1aa1e3227b Mon Sep 17 00:00:00 2001 From: jonastheis <4181434+jonastheis@users.noreply.github.com> Date: Tue, 7 Jul 2026 17:15:21 +0800 Subject: [PATCH 1/2] fix(market): restore risc0 Digest wire format for deployed-guest inputs The SDK decoupling (#2039) replaced risc0_zkvm::sha::Digest ([u32; 8], postcard varint words) with a byte-backed Digest ([u8; 32], raw bytes), silently changing the postcard encoding of every digest embedded in guest inputs. Hosts feeding the deployed assessor guest (image 0x6c5a03c0...) then produce undecodable AssessorInput bytes: the guest panics with DeserializeUnexpectedEnd. CI cannot catch this drift because the in-repo guest rebuilds with the same type, so both sides move together; only the boundary to already-deployed guests breaks. Pin Digest's serde to risc0's word-wise layout (eight little-endian u32 words) and add a byte-exact regression test. Verified against the deployed assessor guest: the encoding is byte-identical to v2.0.2's and the live guest ELF executes past input decode. --- Cargo.lock | 1 + crates/boundless-market/Cargo.toml | 1 + crates/boundless-market/src/digest.rs | 41 +++++++++++++++++++++++++-- 3 files changed, 41 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 4b9ca86cc0..c50b3dbfc7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2895,6 +2895,7 @@ dependencies = [ "httpmock", "hyper-util", "moka", + "postcard", "rand 0.9.2", "reqwest 0.13.1", "risc0-aggregation", diff --git a/crates/boundless-market/Cargo.toml b/crates/boundless-market/Cargo.toml index 62af513b9c..9a4bf80a97 100644 --- a/crates/boundless-market/Cargo.toml +++ b/crates/boundless-market/Cargo.toml @@ -106,6 +106,7 @@ serde_json = { workspace = true } alloy = { workspace = true, features = ["json-rpc"] } boundless-test-utils = { workspace = true } guest-util = { workspace = true } +postcard = { workspace = true, features = ["alloc"] } temp-env = { version = "0.3.6", features = ["async_closure"] } tokio = { workspace = true, features = ["test-util"] } tracing-subscriber = { workspace = true, features = ["env-filter"] } diff --git a/crates/boundless-market/src/digest.rs b/crates/boundless-market/src/digest.rs index cbc18a783e..c813eaec55 100644 --- a/crates/boundless-market/src/digest.rs +++ b/crates/boundless-market/src/digest.rs @@ -13,15 +13,41 @@ // limitations under the License. use alloy_primitives::B256; -use serde::{Deserialize, Serialize}; +use serde::{Deserialize, Deserializer, Serialize, Serializer}; #[cfg(not(target_os = "zkvm"))] use std::fmt; /// A 32-byte hash digest representing an image ID or journal digest. -#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, Default, Serialize, Deserialize)] +#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, Default)] pub struct Digest([u8; 32]); +/// Serde encodes as eight little-endian u32 words, wire-identical to +/// `risc0_zkvm::sha::Digest`. Deployed guests (e.g. the on-chain assessor) +/// decode their postcard inputs against that layout, so the encoding is a +/// compatibility contract, not an implementation detail: a byte-array +/// encoding here would silently break every host-to-deployed-guest boundary +/// that embeds a digest. +impl Serialize for Digest { + fn serialize(&self, serializer: S) -> Result { + let words: [u32; 8] = core::array::from_fn(|i| { + u32::from_le_bytes(self.0[i * 4..i * 4 + 4].try_into().expect("4-byte chunk")) + }); + words.serialize(serializer) + } +} + +impl<'de> Deserialize<'de> for Digest { + fn deserialize>(deserializer: D) -> Result { + let words = <[u32; 8]>::deserialize(deserializer)?; + let mut bytes = [0u8; 32]; + for (i, word) in words.iter().enumerate() { + bytes[i * 4..i * 4 + 4].copy_from_slice(&word.to_le_bytes()); + } + Ok(Self(bytes)) + } +} + impl Digest { /// The zero digest. pub const ZERO: Self = Self([0u8; 32]); @@ -101,6 +127,17 @@ mod tests { assert_eq!(<[u8; 32]>::from(d), bytes); } + #[test] + fn postcard_wire_format_matches_risc0_digest() { + // risc0's Digest is [u32; 8]; postcard varint-encodes each LE word. + // 0x42424242 -> varint c2 84 89 92 04, repeated for all 8 words. + let d = Digest::from_bytes([0x42; 32]); + let encoded = postcard::to_allocvec(&d).unwrap(); + assert_eq!(encoded, [0xc2, 0x84, 0x89, 0x92, 0x04].repeat(8)); + let decoded: Digest = postcard::from_bytes(&encoded).unwrap(); + assert_eq!(decoded, d); + } + #[cfg(not(target_os = "zkvm"))] #[test] fn roundtrip_hex() { From a5806b1aeb157f5fd292314f53469de4eacc5416 Mon Sep 17 00:00:00 2001 From: jonastheis <4181434+jonastheis@users.noreply.github.com> Date: Tue, 7 Jul 2026 17:15:49 +0800 Subject: [PATCH 2/2] feat(cli): allow overriding the assessor guest image URL via env ASSESSOR_IMAGE_URL overrides the fulfiller's default assessor guest source (any downloader scheme, e.g. file://). Useful for proving a locally built assessor guest against a router adapter that pins its image id, e.g. in test deployments. --- crates/boundless-cli/src/lib.rs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/crates/boundless-cli/src/lib.rs b/crates/boundless-cli/src/lib.rs index 5a3db7e551..9d948afe69 100644 --- a/crates/boundless-cli/src/lib.rs +++ b/crates/boundless-cli/src/lib.rs @@ -284,7 +284,12 @@ impl OrderFulfiller { )?) }; - Self::initialize(prover, client, assessor_selector, ASSESSOR_DEFAULT_IMAGE_URL).await + // ASSESSOR_IMAGE_URL overrides the default assessor guest source (any downloader + // scheme, e.g. file://). The proven guest's image id must match the id pinned by the + // router assessor adapter that `assessor_selector` dispatches to. + let assessor_image_url = std::env::var("ASSESSOR_IMAGE_URL") + .unwrap_or_else(|_| ASSESSOR_DEFAULT_IMAGE_URL.to_string()); + Self::initialize(prover, client, assessor_selector, &assessor_image_url).await } /// Initialize an OrderFulfiller from a provided Prover instance.