pod2/src/middleware/basetypes.rs
Eduard S. a7a30176a7
Split Params into base and developer-defined (#458)
I thought it would be nice to have a Predicate for the typed value so that the developer can work with predicates as values comfortably.  Then I noticed that hashing a predicate required `Params` which would have been annoying for converting a `TypedValue::Predicate` to `RawValue` and this led to a small refactor over how `Params` work.

We already had some fields in the `Params` struct that determine compatibility between encoded data.  They can be seen as determining a kind of ABI compatibility.  In general it's better if those parameters don't change so that different circuit configurations can still verify proofs from each other.  So I decided to force those parameters to be constant in the code base and not allow the user of our library to change them.  Many field element serialization/deserialization functions in our code depended on those parameters, and since now they are constant many functions get rid of the `Params` argument, which simplifies the code.  This includes the serialization of a `Predicate` which was required to calculate its hash.
2026-02-02 16:23:32 +01:00

297 lines
8.9 KiB
Rust

//! This file exposes the imported backend dependent basetypes as middleware types, taking them
//! from the feature-enabled backend.
//!
//! This is done in order to avoid inconsistencies where a type or parameter is defined in the
//! middleware to have certain characteristic and later in the backend it gets used differently.
//! The idea is that those types and parameters (eg. lengths) have a single source of truth in the
//! code; and in the case of the "base types" this is determined by the backend being used under
//! the hood, not by a choice of the middleware parameters.
//!
//! The idea with this approach, is that the frontend & middleware should not need to import the
//! proving library used by the backend (eg. plonky2, plonky3, etc).
//!
//! For example, the `Hash` and `Value` types are types belonging at the middleware, and is the
//! middleware who reasons about them, but depending on the backend being used, the `Hash` and
//! `Value` types will have different sizes. So it's the backend being used who actually defines
//! their nature under the hood. For example with a plonky2 backend, these types will have a length
//! of 4 field elements, whereas with a plonky3 backend they will have a length of 8 field
//! eleements.
//!
//! Note that his approach does not introduce new traits or abstract code, just makes use of rust
//! features to define 'base types' that are being used in the middleware.
//!
//!
//! NOTE (TMP): current implementation still uses plonky2 in the middleware for u64/i64 to F
//! conversion. Eventually we will do those conversions through the approach described in this
//! file, removing the imports of plonky2 in the middleware.
use std::{
cmp::{Ord, Ordering},
fmt,
fmt::Write,
};
use hex::{FromHex, FromHexError, ToHex};
use plonky2::{
field::types::{Field, PrimeField64},
hash::poseidon::PoseidonHash,
plonk::config::Hasher,
};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use super::serialization::*;
// Plonky2 specific types.
// Value, Hash, F and other types are imported based on 'features'. For example by default we use
// theg'plonky2' feature, but it could be used a 'plonky3' feature, so then the Value, Hash and F
// types would come from the plonky3 backend.
#[cfg(feature = "backend_plonky2")]
pub use crate::backends::plonky2::basetypes::*;
#[cfg(feature = "backend_plonky2")]
pub use crate::backends::plonky2::{Error as BackendError, Result as BackendResult};
use crate::middleware::{ToFields, Value};
pub const HASH_SIZE: usize = 4;
pub const VALUE_SIZE: usize = 4;
pub const EMPTY_VALUE: RawValue = RawValue([F::ZERO, F::ZERO, F::ZERO, F::ZERO]);
pub const SELF_ID_HASH: Hash = Hash([F(0x5), F(0xe), F(0x1), F(0xf)]);
pub const EMPTY_HASH: Hash = Hash([F::ZERO, F::ZERO, F::ZERO, F::ZERO]);
#[derive(Clone, Copy, Debug, Default, Hash, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
pub struct RawValue(
#[serde(
serialize_with = "serialize_value_tuple",
deserialize_with = "deserialize_value_tuple"
)]
// We know that Serde will serialize and deserialize this as a string, so we can
// use the JsonSchema to validate the format.
#[schemars(with = "String", regex(pattern = r"^[0-9a-fA-F]{64}$"))]
pub [F; VALUE_SIZE],
);
impl ToFields for RawValue {
fn to_fields(&self) -> Vec<F> {
self.0.to_vec()
}
}
impl RawValue {
pub fn to_bytes(self) -> Vec<u8> {
self.0
.iter()
.flat_map(|e| e.to_canonical_u64().to_le_bytes())
.collect()
}
}
impl Ord for RawValue {
fn cmp(&self, other: &Self) -> Ordering {
for (lhs, rhs) in self.0.iter().zip(other.0.iter()).rev() {
let (lhs, rhs) = (lhs.to_canonical_u64(), rhs.to_canonical_u64());
match lhs.cmp(&rhs) {
Ordering::Less => return Ordering::Less,
Ordering::Greater => return Ordering::Greater,
_ => {}
}
}
Ordering::Equal
}
}
impl PartialOrd for RawValue {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
impl From<i64> for RawValue {
fn from(v: i64) -> Self {
let lo = F::from_canonical_u64((v as u64) & 0xffffffff);
let hi = F::from_canonical_u64((v as u64) >> 32);
RawValue([lo, hi, F::ZERO, F::ZERO])
}
}
impl From<Hash> for RawValue {
fn from(h: Hash) -> Self {
RawValue(h.0)
}
}
impl fmt::Display for RawValue {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
Hash(self.0).fmt(f)
}
}
impl ToHex for RawValue {
fn encode_hex<T: std::iter::FromIterator<char>>(&self) -> T {
self.0
.iter()
.rev()
.fold(String::with_capacity(64), |mut s, limb| {
write!(s, "{:016x}", limb.0).unwrap();
s
})
.chars()
.collect()
}
fn encode_hex_upper<T: std::iter::FromIterator<char>>(&self) -> T {
self.0
.iter()
.rev()
.fold(String::with_capacity(64), |mut s, limb| {
write!(s, "{:016X}", limb.0).unwrap();
s
})
.chars()
.collect()
}
}
impl FromHex for RawValue {
type Error = FromHexError;
fn from_hex<T: AsRef<[u8]>>(hex: T) -> Result<Self, Self::Error> {
Hash::from_hex(hex).map(|h| RawValue(h.0))
}
}
#[derive(Clone, Copy, Debug, Default, Hash, Eq, PartialEq, Serialize, Deserialize, JsonSchema)]
pub struct Hash(
#[serde(
serialize_with = "serialize_hash_tuple",
deserialize_with = "deserialize_hash_tuple"
)]
#[schemars(with = "String", regex(pattern = r"^[0-9a-fA-F]{64}$"))]
pub [F; HASH_SIZE],
);
impl From<Hash> for HashOut {
fn from(hash: Hash) -> HashOut {
HashOut { elements: hash.0 }
}
}
impl ToHex for Hash {
fn encode_hex<T: std::iter::FromIterator<char>>(&self) -> T {
self.0
.iter()
.rev()
.fold(String::with_capacity(64), |mut s, limb| {
write!(s, "{:016x}", limb.0).unwrap();
s
})
.chars()
.collect()
}
fn encode_hex_upper<T: std::iter::FromIterator<char>>(&self) -> T {
self.0
.iter()
.rev()
.fold(String::with_capacity(64), |mut s, limb| {
write!(s, "{:016X}", limb.0).unwrap();
s
})
.chars()
.collect()
}
}
pub fn hash_value(input: &RawValue) -> Hash {
hash_fields(&input.0)
}
pub fn hash_fields(input: &[F]) -> Hash {
Hash(PoseidonHash::hash_no_pad(input).elements)
}
pub fn hash_values(input: &[Value]) -> Hash {
hash_fields(&input.iter().flat_map(|v| v.raw().0).collect::<Vec<_>>())
}
impl From<RawValue> for Hash {
fn from(v: RawValue) -> Self {
Hash(v.0)
}
}
impl ToFields for Hash {
fn to_fields(&self) -> Vec<F> {
self.0.to_vec()
}
}
impl PartialOrd for Hash {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
impl Ord for Hash {
fn cmp(&self, other: &Self) -> Ordering {
RawValue(self.0).cmp(&RawValue(other.0))
}
}
impl fmt::Display for Hash {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
if f.alternate() {
write!(f, "0x{}", self.encode_hex::<String>())
} else {
// display the least significant field element in big endian
write!(f, "0x…")?;
let v0 = self.0[0].to_canonical_u64();
for i in (0..4).rev() {
write!(f, "{:02x}", (v0 >> (i * 8)) & 0xff)?;
}
Ok(())
}
}
}
impl FromHex for Hash {
type Error = FromHexError;
fn from_hex<T: AsRef<[u8]>>(hex: T) -> Result<Self, Self::Error> {
// The input `hex` is a big-endian hex string.
let bytes = <[u8; 32]>::from_hex(hex)?;
let mut inner = [F::ZERO; HASH_SIZE];
for i in 0..HASH_SIZE {
let start = i * 8;
let end = start + 8;
let chunk: [u8; 8] = bytes[start..end]
.try_into()
.expect("slice with incorrect length");
// We read big-endian chunks from a big-endian string,
// and place them into a little-endian limb array.
let u64_val = u64::from_be_bytes(chunk);
inner[HASH_SIZE - 1 - i] = F::from_canonical_u64(u64_val);
}
Ok(Self(inner))
}
}
pub fn hash_str(s: &str) -> Hash {
let mut input = s.as_bytes().to_vec();
input.push(1); // padding
// Merge 7 bytes into 1 field, because the field is slightly below 64 bits
let input: Vec<F> = input
.chunks(7)
.map(|bytes| {
let mut v: u64 = 0;
for b in bytes.iter().rev() {
v <<= 8;
v += *b as u64;
}
F::from_canonical_u64(v)
})
.collect();
hash_fields(&input)
}