openvm_continuations/
commit_bytes.rs

1use std::{array::from_fn, fmt};
2
3use num_bigint::BigUint;
4use openvm_stark_backend::codec::{Decode, Encode};
5use openvm_stark_sdk::config::baby_bear_poseidon2::{DIGEST_SIZE, F};
6use openvm_verify_stark_host::pvs::VkCommit;
7use p3_field::{PrimeCharacteristicRing, PrimeField32};
8use serde::{Deserialize, Deserializer, Serialize, Serializer};
9
10pub const COMMIT_NUM_BYTES: usize = 32;
11
12/// Wrapper for the canonical big-endian byte representation of a BabyBear digest interpreted as
13/// an unsigned integer in base `F::MODULUS`. Each commit can be converted to a Bn254 using the
14/// trivial identification as natural numbers or into a `u32` digest by decomposing the big integer
15/// base-`F::MODULUS`.
16#[derive(Copy, Clone, Debug, PartialEq, Eq, Encode)]
17pub struct CommitBytes([u8; COMMIT_NUM_BYTES]);
18
19#[derive(Copy, Clone, Debug, PartialEq, Eq, thiserror::Error)]
20pub enum CommitBytesError {
21    #[error("non-canonical CommitBytes for BabyBear digest")]
22    NonCanonical,
23}
24
25impl CommitBytes {
26    /// Use this rather than [`CommitBytes::new`] for bytes from an untrusted source.
27    pub fn try_new(bytes: [u8; COMMIT_NUM_BYTES]) -> Result<Self, CommitBytesError> {
28        if u32_digest_to_bytes(&bytes_to_u32_digest(&bytes)) == bytes {
29            Ok(Self(bytes))
30        } else {
31            Err(CommitBytesError::NonCanonical)
32        }
33    }
34
35    /// Panics if `bytes` is non-canonical.
36    pub fn new(bytes: [u8; COMMIT_NUM_BYTES]) -> Self {
37        Self::try_new(bytes).expect("non-canonical CommitBytes for BabyBear digest")
38    }
39
40    pub fn as_slice(&self) -> &[u8; COMMIT_NUM_BYTES] {
41        &self.0
42    }
43
44    pub fn to_field_le_bytes(self) -> [u8; COMMIT_NUM_BYTES] {
45        let digest: [F; DIGEST_SIZE] = self.into();
46        let mut bytes = [0u8; COMMIT_NUM_BYTES];
47        for (i, limb) in digest.into_iter().enumerate() {
48            bytes[4 * i..4 * (i + 1)].copy_from_slice(&limb.to_unique_u32().to_le_bytes());
49        }
50        bytes
51    }
52}
53
54#[derive(Copy, Clone, Debug, PartialEq, Eq)]
55pub struct VkCommitBytes {
56    pub cached_commit: CommitBytes,
57    pub vk_pre_hash: CommitBytes,
58}
59
60impl<F: PrimeCharacteristicRing> From<VkCommitBytes> for VkCommit<F> {
61    fn from(value: VkCommitBytes) -> Self {
62        VkCommit {
63            cached_commit: value.cached_commit.into(),
64            vk_pre_hash: value.vk_pre_hash.into(),
65        }
66    }
67}
68
69impl From<[F; DIGEST_SIZE]> for CommitBytes {
70    fn from(value: [F; DIGEST_SIZE]) -> Self {
71        Self::from(value.map(|x| x.as_canonical_u32()))
72    }
73}
74
75impl From<[u32; DIGEST_SIZE]> for CommitBytes {
76    fn from(value: [u32; DIGEST_SIZE]) -> Self {
77        assert!(
78            value.iter().all(|&digit| digit < F::ORDER_U32),
79            "non-canonical BabyBear digest limb"
80        );
81        Self(u32_digest_to_bytes(&value))
82    }
83}
84
85impl<F: PrimeCharacteristicRing> From<CommitBytes> for [F; DIGEST_SIZE] {
86    fn from(value: CommitBytes) -> Self {
87        assert!(
88            u32_digest_to_bytes(&bytes_to_u32_digest(&value.0)) == value.0,
89            "non-canonical CommitBytes for BabyBear digest"
90        );
91        bytes_to_u32_digest(&value.0).map(F::from_u32)
92    }
93}
94
95fn bytes_to_biguint(bytes: &[u8; COMMIT_NUM_BYTES]) -> BigUint {
96    let mut bigint = BigUint::ZERO;
97    for byte in bytes.iter() {
98        bigint <<= 8;
99        bigint += BigUint::from(*byte);
100    }
101    bigint
102}
103
104fn biguint_to_u32_digest(mut bigint: BigUint) -> [u32; DIGEST_SIZE] {
105    let order = F::ORDER_U32;
106    from_fn(|_| {
107        let bigint_digit = bigint.clone() % order;
108        let digit = if bigint_digit == BigUint::ZERO {
109            0u32
110        } else {
111            bigint_digit.to_u32_digits()[0]
112        };
113        bigint /= order;
114        digit
115    })
116}
117
118fn u32_digest_to_biguint(digest: &[u32; DIGEST_SIZE]) -> BigUint {
119    let mut bigint = BigUint::ZERO;
120    let mut base = BigUint::from(1u32);
121    let order = BigUint::from(F::ORDER_U32);
122    for digit in digest {
123        bigint += &base * BigUint::from(*digit);
124        base *= &order;
125    }
126    bigint
127}
128
129fn bytes_to_u32_digest(bytes: &[u8; COMMIT_NUM_BYTES]) -> [u32; DIGEST_SIZE] {
130    biguint_to_u32_digest(bytes_to_biguint(bytes))
131}
132
133fn u32_digest_to_bytes(digest: &[u32; DIGEST_SIZE]) -> [u8; COMMIT_NUM_BYTES] {
134    let mut ret = [0u8; COMMIT_NUM_BYTES];
135    let bytes = u32_digest_to_biguint(digest).to_bytes_be();
136    let start = COMMIT_NUM_BYTES - bytes.len();
137    ret[start..].copy_from_slice(&bytes);
138    ret
139}
140
141impl fmt::Display for CommitBytes {
142    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
143        write!(f, "0x{}", hex::encode(self.as_slice()))
144    }
145}
146
147impl Serialize for CommitBytes {
148    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
149        self.to_string().serialize(serializer)
150    }
151}
152
153impl<'de> Deserialize<'de> for CommitBytes {
154    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
155        let hex_str = String::deserialize(deserializer)?;
156        let hex_str = hex_str.strip_prefix("0x").unwrap_or(&hex_str);
157        let bytes: [u8; COMMIT_NUM_BYTES] = hex::decode(hex_str)
158            .map_err(serde::de::Error::custom)?
159            .try_into()
160            .map_err(|_| serde::de::Error::custom("expected 32 bytes"))?;
161        CommitBytes::try_new(bytes).map_err(serde::de::Error::custom)
162    }
163}
164
165// Hand-written so that decoding goes through `try_new` instead of bypassing the canonicity check.
166impl Decode for CommitBytes {
167    fn decode<R: std::io::Read>(reader: &mut R) -> std::io::Result<Self> {
168        let bytes = <[u8; COMMIT_NUM_BYTES]>::decode(reader)?;
169        CommitBytes::try_new(bytes).map_err(std::io::Error::other)
170    }
171}
172
173#[cfg(feature = "root-prover")]
174mod bn254 {
175    use p3_bn254::Bn254;
176    use p3_field::PrimeField;
177
178    use super::*;
179
180    impl From<Bn254> for CommitBytes {
181        fn from(value: Bn254) -> Self {
182            Self::new(bn254_to_bytes(value))
183        }
184    }
185
186    impl From<[Bn254; 1]> for CommitBytes {
187        fn from(value: [Bn254; 1]) -> Self {
188            CommitBytes::from(value[0])
189        }
190    }
191
192    impl From<CommitBytes> for Bn254 {
193        fn from(value: CommitBytes) -> Self {
194            bytes_to_bn254(&value.0)
195        }
196    }
197
198    fn bytes_to_bn254(bytes: &[u8; COMMIT_NUM_BYTES]) -> Bn254 {
199        let order = Bn254::from_u32(1 << 8);
200        let mut ret = Bn254::ZERO;
201        let mut base = Bn254::ONE;
202        for byte in bytes.iter().rev() {
203            ret += base * Bn254::from_u8(*byte);
204            base *= order;
205        }
206        ret
207    }
208
209    fn bn254_to_bytes(bn254: Bn254) -> [u8; COMMIT_NUM_BYTES] {
210        let mut ret = [0u8; COMMIT_NUM_BYTES];
211        let bytes = bn254.as_canonical_biguint().to_bytes_be();
212        let start = COMMIT_NUM_BYTES - bytes.len();
213        ret[start..].copy_from_slice(&bytes);
214        ret
215    }
216}
217
218/// Untrusted input paths must reject non-canonical bytes as errors rather than panicking.
219#[cfg(test)]
220mod tests {
221    use serde::de::IntoDeserializer;
222
223    use super::*;
224
225    /// Largest canonical digest, i.e. `F::MODULUS^8 - 1`.
226    fn max_canonical() -> [u8; COMMIT_NUM_BYTES] {
227        u32_digest_to_bytes(&[F::ORDER_U32 - 1; DIGEST_SIZE])
228    }
229
230    #[test]
231    fn try_new_canonicity() {
232        assert!(CommitBytes::try_new([0u8; COMMIT_NUM_BYTES]).is_ok());
233        assert!(CommitBytes::try_new(max_canonical()).is_ok());
234        assert_eq!(
235            CommitBytes::try_new([0xff; COMMIT_NUM_BYTES]),
236            Err(CommitBytesError::NonCanonical)
237        );
238    }
239
240    #[test]
241    fn deserialize_rejects_non_canonical() {
242        let hex = format!("0x{}", hex::encode([0xffu8; COMMIT_NUM_BYTES]));
243        let de: serde::de::value::StrDeserializer<serde::de::value::Error> =
244            hex.as_str().into_deserializer();
245        assert!(CommitBytes::deserialize(de).is_err());
246    }
247
248    #[test]
249    fn decode_rejects_non_canonical() {
250        let encoded = [0xffu8; COMMIT_NUM_BYTES];
251        assert!(CommitBytes::decode(&mut encoded.as_slice()).is_err());
252    }
253
254    #[test]
255    fn encode_decode_roundtrip() {
256        let commit = CommitBytes::try_new(max_canonical()).unwrap();
257        let bytes = commit.encode_to_vec().unwrap();
258        assert_eq!(CommitBytes::decode_from_bytes(&bytes).unwrap(), commit);
259    }
260}