diff --git a/Cargo.lock b/Cargo.lock index 4b9ca86cc..c50b3dbfc 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-cli/src/lib.rs b/crates/boundless-cli/src/lib.rs index 5a3db7e55..9d948afe6 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. diff --git a/crates/boundless-market/Cargo.toml b/crates/boundless-market/Cargo.toml index 62af513b9..9a4bf80a9 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 cbc18a783..c813eaec5 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() {