boundless_market/input.rs
1// Copyright 2026 Boundless Foundation, Inc.
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7// http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15use bytemuck::Pod;
16use risc0_zkvm::serde::to_vec;
17use risc0_zkvm::ExecutorEnv;
18use rmp_serde;
19use serde::{Deserialize, Serialize};
20
21use crate::contracts::RequestInput;
22
23// Input version.
24#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
25#[repr(u8)]
26#[non_exhaustive]
27enum Version {
28 // Raw version with no encoding.
29 V0 = 0,
30 // MessagePack encoded version based on [InputV1].
31 #[default]
32 V1 = 1,
33}
34
35impl From<Version> for u8 {
36 fn from(v: Version) -> Self {
37 v as u8
38 }
39}
40
41impl TryFrom<u8> for Version {
42 type Error = Error;
43
44 fn try_from(v: u8) -> Result<Version, Self::Error> {
45 match v {
46 v if v == Version::V0 as u8 => Ok(Version::V0),
47 v if v == Version::V1 as u8 => Ok(Version::V1),
48 _ => Err(Error::UnsupportedVersion(v as u64)),
49 }
50 }
51}
52
53#[derive(Debug, thiserror::Error)]
54#[non_exhaustive]
55/// Input error.
56pub enum Error {
57 /// MessagePack serde encoding error
58 #[error("MessagePack serde encoding error: {0}")]
59 MessagePackSerdeEncode(#[from] rmp_serde::encode::Error),
60 /// MessagePack serde decoding error
61 #[error("MessagePack serde decoding error: {0}")]
62 MessagePackSerdeDecode(#[from] rmp_serde::decode::Error),
63 /// risc0-zkvm Serde error
64 #[error("risc0-zkvm Serde error: {0}")]
65 ZkvmSerde(#[from] risc0_zkvm::serde::Error),
66 /// Unsupported version
67 #[error("Unsupported version: {0}")]
68 UnsupportedVersion(u64),
69 /// Encoded input buffer is empty, which is an invalid encoding.
70 #[error("Cannot decode empty buffer as input")]
71 EmptyEncodedInput,
72}
73
74/// Structured input used by the Boundless prover to execute the guest for the proof request.
75///
76/// This struct is related to the [ExecutorEnv] in that both represent the environments provided to
77/// the guest by the host that is executing and proving the execution. In contrast to the
78/// [ExecutorEnv] provided by [risc0_zkvm], this struct contains only the options that are
79/// supported by Boundless.
80#[derive(Clone, Default, Debug, Serialize, Deserialize)]
81#[cfg_attr(test, derive(PartialEq, Eq))]
82#[non_exhaustive]
83pub struct GuestEnv {
84 /// Input data to be provided to the guest as stdin.
85 ///
86 /// The data here will be provided to the guest without further encoding (e.g. the bytes will
87 /// be provided directly). When the guest calls `env::read_slice` these are the bytes that will
88 /// be read. If the guest uses `env::read`, this should be encoded using the default RISC Zero
89 /// codec. [GuestEnvBuilder::write] will encode the data given using the default codec.
90 pub stdin: Vec<u8>,
91}
92
93impl GuestEnv {
94 /// Create a new [GuestEnvBuilder]
95 pub fn builder() -> GuestEnvBuilder {
96 Default::default()
97 }
98
99 /// Parse an encoded [GuestEnv] with version support.
100 pub fn decode(bytes: &[u8]) -> Result<Self, Error> {
101 if bytes.is_empty() {
102 return Err(Error::EmptyEncodedInput);
103 }
104 match Version::try_from(bytes[0])? {
105 Version::V0 => Ok(Self { stdin: bytes[1..].to_vec() }),
106 Version::V1 => Ok(rmp_serde::from_read(&bytes[1..])?),
107 }
108 }
109
110 /// Encode the [GuestEnv] for inclusion in a proof request.
111 pub fn encode(&self) -> Result<Vec<u8>, Error> {
112 let mut encoded = Vec::<u8>::new();
113 // Push the version as the first byte to indicate the message version.
114 encoded.push(Version::V1.into());
115 encoded.extend_from_slice(&rmp_serde::to_vec_named(&self)?);
116 Ok(encoded)
117 }
118
119 /// Create a [GuestEnv] with `stdin` set to the contents of the given `bytes`.
120 pub fn from_stdin(bytes: impl Into<Vec<u8>>) -> Self {
121 GuestEnv { stdin: bytes.into() }
122 }
123}
124
125impl TryFrom<GuestEnv> for ExecutorEnv<'_> {
126 type Error = anyhow::Error;
127
128 /// Create an [ExecutorEnv], which can be used for execution and proving through the
129 /// [risc0_zkvm] [Prover][risc0_zkvm::Prover] and [Executor][risc0_zkvm::Executor] traits, from
130 /// the given [GuestEnv].
131 fn try_from(env: GuestEnv) -> Result<Self, Self::Error> {
132 ExecutorEnv::builder().write_slice(&env.stdin).build()
133 }
134}
135
136impl From<GuestEnvBuilder> for GuestEnv {
137 fn from(builder: GuestEnvBuilder) -> Self {
138 builder.build_env()
139 }
140}
141
142/// Input builder, used to build the structured input (i.e. env) for execution and proving.
143///
144/// Boundless provers decode the input provided in a proving request as a [GuestEnv]. This
145/// [GuestEnvBuilder] provides methods for constructing and encoding the guest environment.
146#[derive(Clone, Default, Debug)]
147#[non_exhaustive]
148pub struct GuestEnvBuilder {
149 /// Input data to be provided to the guest as stdin.
150 ///
151 /// See [GuestEnv::stdin]
152 pub stdin: Vec<u8>,
153}
154
155impl GuestEnvBuilder {
156 /// Create a new input builder.
157 pub fn new() -> Self {
158 Self { stdin: Vec::new() }
159 }
160
161 /// Build the [GuestEnv] for inclusion in a proof request.
162 pub fn build_env(self) -> GuestEnv {
163 GuestEnv { stdin: self.stdin }
164 }
165
166 /// Build the and encode [GuestEnv] for inclusion in a proof request.
167 pub fn build_vec(self) -> Result<Vec<u8>, Error> {
168 self.build_env().encode()
169 }
170
171 /// Build and encode the [GuestEnv] into an inline [RequestInput] for inclusion in a proof request.
172 pub fn build_inline(self) -> Result<RequestInput, Error> {
173 Ok(RequestInput::inline(self.build_env().encode()?))
174 }
175
176 /// Write input data.
177 ///
178 /// This function will serialize `data` using the RISC Zero default codec that
179 /// can be deserialized in the guest with a corresponding `risc0_zkvm::env::read` with
180 /// the same data type.
181 ///
182 /// # Example
183 ///
184 /// ```
185 /// use boundless_market::GuestEnv;
186 /// use serde::Serialize;
187 ///
188 /// #[derive(Serialize)]
189 /// struct Input {
190 /// a: u32,
191 /// b: u32,
192 /// }
193 ///
194 /// let input1 = Input{ a: 1, b: 2 };
195 /// let input2 = Input{ a: 3, b: 4 };
196 /// let input = GuestEnv::builder()
197 /// .write(&input1).unwrap()
198 /// .write(&input2).unwrap();
199 /// ```
200 pub fn write<T: Serialize>(self, data: &T) -> Result<Self, Error> {
201 Ok(self.write_slice(&to_vec(data)?))
202 }
203
204 /// Write input data.
205 ///
206 /// This function writes a slice directly to the underlying buffer. A
207 /// corresponding `risc0_zkvm::env::read_slice` can be used within
208 /// the guest to read the data.
209 ///
210 /// # Example
211 ///
212 /// ```
213 /// use boundless_market::GuestEnv;
214 ///
215 /// let slice1 = [0, 1, 2, 3];
216 /// let slice2 = [3, 2, 1, 0];
217 /// let input = GuestEnv::builder()
218 /// .write_slice(&slice1)
219 /// .write_slice(&slice2);
220 /// ```
221 pub fn write_slice<T: Pod>(self, slice: &[T]) -> Self {
222 let mut input = self.stdin;
223 input.extend_from_slice(bytemuck::cast_slice(slice));
224 Self { stdin: input, ..self }
225 }
226
227 /// Write a frame.
228 ///
229 /// A frame contains a length header along with the payload. Reading a frame can be more
230 /// efficient than streaming deserialization of a message. Streaming deserialization
231 /// deserialization can cause many syscalls, whereas a frame will only have two.
232 pub fn write_frame(self, payload: &[u8]) -> Self {
233 let len = payload.len() as u32;
234 let mut input = self.stdin;
235 input.extend_from_slice(&len.to_le_bytes());
236 input.extend_from_slice(payload);
237 Self { stdin: input, ..self }
238 }
239}
240
241#[cfg(test)]
242mod tests {
243 use super::*;
244
245 #[test]
246 fn test_version_parsing() -> Result<(), Error> {
247 // Test V1
248 let v1 = GuestEnv::builder().write_slice(&[1u8, 2, 3]);
249 let bytes = v1.build_vec()?;
250 let parsed = GuestEnv::decode(&bytes)?;
251 assert_eq!(parsed.stdin, vec![1, 2, 3]);
252
253 // Test V0
254 let bytes = vec![0u8, 1, 2, 3];
255 let parsed = GuestEnv::decode(&bytes)?;
256 assert_eq!(parsed.stdin, vec![1, 2, 3]);
257
258 // Test unsupported version
259 let bytes = vec![2u8, 1, 2, 3];
260 let parsed = GuestEnv::decode(&bytes);
261 assert!(parsed.is_err());
262
263 Ok(())
264 }
265
266 #[test]
267 fn test_encode_decode_env() -> Result<(), Error> {
268 let timestamp = format! {"{:?}", std::time::SystemTime::now()};
269 let env = GuestEnv::builder().write_slice(timestamp.as_bytes()).build_env();
270
271 let decoded_env = GuestEnv::decode(&env.encode()?)?;
272 assert_eq!(env, decoded_env);
273 Ok(())
274 }
275}