risc0_zkvm/claim/
work.rs

1// Copyright 2025 RISC Zero, 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
15//! [WorkClaim] and associated types and functions.
16
17use alloc::{boxed::Box, collections::VecDeque, vec::Vec};
18use core::fmt;
19
20use borsh::{BorshDeserialize, BorshSerialize};
21use risc0_binfmt::{tagged_struct, DecodeError, Digestible, PovwNonce};
22use serde::{Deserialize, Serialize};
23
24use crate::{
25    sha,
26    sha::{Digest, Sha256},
27    MaybePruned, PrunedValueError, ReceiptClaim, Unknown,
28};
29
30/// A wrapper around the underlying claim that additionally includes the amount of verifiable work
31/// completed, and the nonces used, in the process of proving the claim.
32///
33/// This type is instantiated as [`WorkClaim<ReceiptClaim>`] when PoVW is used with zkVM proving.
34#[derive(Clone, Serialize, Deserialize, BorshSerialize, BorshDeserialize)]
35pub struct WorkClaim<Claim> {
36    /// The wrapped claim (e.g. [ReceiptClaim][crate::ReceiptClaim]).
37    pub claim: MaybePruned<Claim>,
38    /// Work associated with proving the wrapped claim.
39    pub work: MaybePruned<Work>,
40}
41
42impl<Claim> WorkClaim<Claim> {
43    /// Prunes the claim, retaining its digest, and converts into a [WorkClaim] with an unknown
44    /// claim type. Can be used to get receipts of a uniform type across heterogeneous claims.
45    pub fn into_unknown(self) -> WorkClaim<Unknown>
46    where
47        Claim: Digestible,
48    {
49        WorkClaim {
50            claim: MaybePruned::Pruned(self.claim.digest::<sha::Impl>()),
51            work: self.work,
52        }
53    }
54}
55
56impl<Claim> Digestible for WorkClaim<Claim>
57where
58    Claim: Digestible,
59{
60    /// Hash the [ReceiptClaim] to get a digest of the struct.
61    fn digest<S: Sha256>(&self) -> Digest {
62        tagged_struct::<S>(
63            "risc0.WorkClaim",
64            &[self.claim.digest::<S>(), self.work.digest::<S>()],
65            &[],
66        )
67    }
68}
69
70impl<Claim> fmt::Debug for WorkClaim<Claim>
71where
72    Claim: Digestible + fmt::Debug,
73{
74    fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result {
75        fmt.debug_struct("WorkClaim")
76            .field("claim", &self.claim)
77            .field("work", &self.work)
78            .finish()
79    }
80}
81
82impl WorkClaim<ReceiptClaim> {
83    /// Encodes the work claim to a seal buffer.
84    pub fn encode_to_seal(&self, buf: &mut Vec<u32>) -> Result<(), PrunedValueError> {
85        self.claim.as_value()?.encode(buf)?;
86        self.work.as_value()?.encode_to_seal(buf);
87        Ok(())
88    }
89
90    /// Decodes a work claim from a seal buffer.
91    pub fn decode_from_seal(
92        buf: &mut VecDeque<u32>,
93    ) -> Result<Self, crate::claim::receipt::DecodeError> {
94        Ok(Self {
95            claim: ReceiptClaim::decode(buf)?.into(),
96            work: Work::decode_from_seal(buf)?.into(),
97        })
98    }
99}
100
101/// A compact representation of completed work within Proof of Verifiable Work (PoVW).
102///
103/// This struct contains a compact representation of the range of used nonces, along with the value
104/// of the work. It is used to represent the work within a single execution.
105#[derive(Clone, Debug, Serialize, Deserialize, BorshSerialize, BorshDeserialize, PartialEq, Eq)]
106pub struct Work {
107    /// Lowest nonce in the range, inclusive.
108    pub nonce_min: PovwNonce,
109    /// Highest nonce in the range, inclusive.
110    pub nonce_max: PovwNonce,
111    /// Value of work (e.g. cycles) accumulated.
112    pub value: u64,
113}
114
115/// Error returned when the
116#[derive(Debug, Clone)]
117#[non_exhaustive]
118pub enum WorkClaimError {
119    NonceRangesNotContiguous(Box<(Work, Work)>),
120    PrunedValue(PrunedValueError),
121}
122
123impl fmt::Display for WorkClaimError {
124    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
125        match self {
126            WorkClaimError::NonceRangesNotContiguous(ab) => {
127                write!(
128                    f,
129                    "work nonce ranges are not contiguous: ({:?}, {:?}) and ({:?}. {:?})",
130                    ab.0.nonce_min, ab.0.nonce_max, ab.1.nonce_min, ab.1.nonce_max
131                )
132            }
133            WorkClaimError::PrunedValue(err) => err.fmt(f),
134        }
135    }
136}
137
138impl From<PrunedValueError> for WorkClaimError {
139    fn from(err: PrunedValueError) -> Self {
140        Self::PrunedValue(err)
141    }
142}
143
144#[cfg(feature = "std")]
145impl std::error::Error for WorkClaimError {}
146
147impl Work {
148    /// Joins two work values by combining their nonce ranges and summing their values,
149    /// ensuring the nonce ranges are disjoint.
150    pub fn join(&self, other: &Self) -> Result<Self, WorkClaimError> {
151        // Check that the two nonce ranges are contiguous. This must match the implementation of
152        // the join_povw recursion program.
153        let contiguous = self
154            .nonce_max
155            .to_u256()
156            .checked_add(1u64.try_into().unwrap())
157            .map(|max| max == other.nonce_min.to_u256())
158            .unwrap_or(false);
159        if !contiguous {
160            return Err(WorkClaimError::NonceRangesNotContiguous(Box::new((
161                self.clone(),
162                other.clone(),
163            ))));
164        }
165
166        Ok(Self {
167            nonce_min: self.nonce_min,
168            nonce_max: other.nonce_max,
169            value: self.value + other.value,
170        })
171    }
172
173    pub(crate) fn encode_to_seal(&self, buf: &mut Vec<u32>) {
174        buf.extend(self.nonce_min.to_u16s().into_iter().map(u32::from));
175        buf.extend(self.nonce_max.to_u16s().into_iter().map(u32::from));
176        buf.extend(u64_to_u16s(self.value).into_iter().map(u32::from));
177    }
178
179    pub(crate) fn decode_from_seal(
180        buf: &mut VecDeque<u32>,
181    ) -> Result<Self, risc0_binfmt::DecodeError> {
182        Ok(Self {
183            nonce_min: PovwNonce::decode_from_seal(buf)?,
184            nonce_max: PovwNonce::decode_from_seal(buf)?,
185            value: decode_work_value_from_seal(buf)?,
186        })
187    }
188}
189
190impl MaybePruned<Work> {
191    /// Joins two possibly pruned work values by combining their nonce ranges and summing their
192    /// values, ensuring the nonce ranges are disjoint.
193    pub fn join(&self, other: &Self) -> Result<Self, WorkClaimError> {
194        Ok(self.as_value()?.join(other.as_value()?)?.into())
195    }
196}
197
198fn u64_to_u16s(x: u64) -> [u16; 4] {
199    let mut u16s = bytemuck::cast::<_, [u16; 4]>(x.to_le_bytes());
200    // Bytes are little-endian, so on a big-endian machine, they need to be reversed.
201    for x in u16s.iter_mut() {
202        *x = u16::from_le(*x);
203    }
204    u16s
205}
206
207fn u64_from_u16s(mut u16s: [u16; 4]) -> u64 {
208    // Bytes are little-endian, so on a big-endian machine, they need to be reversed.
209    for x in u16s.iter_mut() {
210        *x = u16::from_le(*x);
211    }
212    u64::from_le_bytes(bytemuck::cast(u16s))
213}
214
215fn decode_work_value_from_seal(buf: &mut VecDeque<u32>) -> Result<u64, risc0_binfmt::DecodeError> {
216    if buf.len() < 4 {
217        return Err(DecodeError::EndOfStream);
218    }
219    fn u16_from_u32(x: u32) -> Result<u16, risc0_binfmt::DecodeError> {
220        x.try_into()
221            .map_err(|_| risc0_binfmt::DecodeError::OutOfRange)
222    }
223    Ok(u64_from_u16s(
224        buf.drain(..4)
225            .map(u16_from_u32)
226            .collect::<Result<Vec<_>, _>>()?
227            .try_into()
228            .unwrap(),
229    ))
230}
231
232impl Digestible for Work {
233    /// Hash the [ReceiptClaim] to get a digest of the struct.
234    fn digest<S: Sha256>(&self) -> Digest {
235        let mut buf = Vec::new();
236        self.encode_to_seal(&mut buf);
237        tagged_struct::<S>("risc0.Work", &Vec::<Digest>::new(), &buf)
238    }
239}
240
241#[cfg(test)]
242mod tests {
243    use super::*;
244    use crate::{sha, ReceiptClaim};
245    use alloc::collections::VecDeque;
246    use risc0_binfmt::{ExitCode, SystemState};
247
248    #[test]
249    fn test_work_seal_encoding_round_trip() {
250        let original = Work {
251            nonce_min: rand::random(),
252            nonce_max: rand::random(),
253            value: rand::random(),
254        };
255
256        let mut buf = Vec::new();
257        original.encode_to_seal(&mut buf);
258
259        let mut decode_buf = VecDeque::from(buf);
260        let decoded = Work::decode_from_seal(&mut decode_buf).unwrap();
261
262        assert_eq!(original.nonce_min, decoded.nonce_min);
263        assert_eq!(original.nonce_max, decoded.nonce_max);
264        assert_eq!(original.value, decoded.value);
265        assert!(decode_buf.is_empty());
266    }
267
268    #[test]
269    fn test_work_claim_seal_encoding_round_trip() {
270        use crate::sha::Digest;
271
272        // Receipt claim needs an unpruned pre and post state to be encoded.
273        let claim = ReceiptClaim {
274            pre: SystemState {
275                pc: 0,
276                merkle_root: *sha::Impl::hash_bytes(b"pre"),
277            }
278            .into(),
279            post: SystemState {
280                pc: 0,
281                merkle_root: *sha::Impl::hash_bytes(b"pre"),
282            }
283            .into(),
284            output: MaybePruned::Pruned(Digest::ZERO),
285            input: MaybePruned::Pruned(Digest::ZERO),
286            exit_code: ExitCode::SystemSplit,
287        };
288
289        let original = WorkClaim {
290            claim: claim.into(),
291            work: Work {
292                nonce_min: rand::random(),
293                nonce_max: rand::random(),
294                value: rand::random(),
295            }
296            .into(),
297        };
298
299        let mut buf = Vec::new();
300        original.encode_to_seal(&mut buf).unwrap();
301
302        let mut decode_buf = VecDeque::from(buf);
303        let decoded = WorkClaim::decode_from_seal(&mut decode_buf).unwrap();
304
305        assert_eq!(
306            original.claim.as_value().unwrap(),
307            decoded.claim.as_value().unwrap()
308        );
309        assert_eq!(
310            original.work.as_value().unwrap().nonce_min,
311            decoded.work.as_value().unwrap().nonce_min
312        );
313        assert_eq!(
314            original.work.as_value().unwrap().nonce_max,
315            decoded.work.as_value().unwrap().nonce_max
316        );
317        assert_eq!(
318            original.work.as_value().unwrap().value,
319            decoded.work.as_value().unwrap().value
320        );
321        assert!(decode_buf.is_empty());
322    }
323}