diff --git a/crates/proof-of-sql/src/evm_compatibility/dyn_proof_plan_serializer/constants.rs b/crates/proof-of-sql/src/evm_compatibility/dyn_proof_plan_serializer/constants.rs new file mode 100644 index 000000000..af7ebf76c --- /dev/null +++ b/crates/proof-of-sql/src/evm_compatibility/dyn_proof_plan_serializer/constants.rs @@ -0,0 +1,7 @@ +pub const FILTER_EXEC_NUM: u8 = 0; + +pub const COLUMN_EXPR_NUM: u8 = 0; +pub const EQUALS_EXPR_NUM: u8 = 1; +pub const LITERAL_EXPR_NUM: u8 = 2; + +pub const BIGINT_TYPE_NUM: u8 = 0; diff --git a/crates/proof-of-sql/src/evm_compatibility/dyn_proof_plan_serializer/error.rs b/crates/proof-of-sql/src/evm_compatibility/dyn_proof_plan_serializer/error.rs new file mode 100644 index 000000000..61e0c1208 --- /dev/null +++ b/crates/proof-of-sql/src/evm_compatibility/dyn_proof_plan_serializer/error.rs @@ -0,0 +1,25 @@ +use snafu::Snafu; + +/// Errors that can occur during proof plan serialization. +#[derive(Debug, Snafu)] +#[snafu(visibility(pub(super)))] +pub enum ProofPlanSerializationError { + /// Error indicating that the operation is not supported. + #[snafu(display("Not supported"))] + NotSupported, + /// Error indicating that there are more than 255 results in the filter. + #[snafu(display("More than 255 results in filter."))] + TooManyResults, + /// Error indicating that there are more than 255 tables referenced in the plan. + #[snafu(display("More than 255 tables referenced in the plan."))] + TooManyTables, + /// Error indicating that there are more than 255 columns referenced in the plan. + #[snafu(display("More than 255 columns referenced in the plan."))] + TooManyColumns, + /// Error indicating that the table was not found. + #[snafu(display("Table not found"))] + TableNotFound, + /// Error indicating that the column was not found. + #[snafu(display("Column not found"))] + ColumnNotFound, +} diff --git a/crates/proof-of-sql/src/evm_compatibility/dyn_proof_plan_serializer/mod.rs b/crates/proof-of-sql/src/evm_compatibility/dyn_proof_plan_serializer/mod.rs new file mode 100644 index 000000000..b4e5be1f2 --- /dev/null +++ b/crates/proof-of-sql/src/evm_compatibility/dyn_proof_plan_serializer/mod.rs @@ -0,0 +1,17 @@ +/// This module contains constants used in the proof serialization process. +pub(super) mod constants; + +/// This module defines errors that can occur during proof plan serialization. +mod error; + +/// This module handles the serialization of proof expressions. +mod serialize_proof_expr; + +/// This module handles the serialization of proof plans. +mod serialize_proof_plan; + +/// This module provides the main serializer for proof plans. +mod serializer; + +pub use error::ProofPlanSerializationError; +pub use serializer::DynProofPlanSerializer; diff --git a/crates/proof-of-sql/src/evm_compatibility/dyn_proof_plan_serializer/serialize_proof_expr.rs b/crates/proof-of-sql/src/evm_compatibility/dyn_proof_plan_serializer/serialize_proof_expr.rs new file mode 100644 index 000000000..067112a79 --- /dev/null +++ b/crates/proof-of-sql/src/evm_compatibility/dyn_proof_plan_serializer/serialize_proof_expr.rs @@ -0,0 +1,246 @@ +use super::{ + constants::{BIGINT_TYPE_NUM, COLUMN_EXPR_NUM, EQUALS_EXPR_NUM, LITERAL_EXPR_NUM}, + error::{ColumnNotFoundSnafu, NotSupportedSnafu}, + DynProofPlanSerializer, ProofPlanSerializationError, +}; +use crate::{ + base::{database::LiteralValue, scalar::Scalar}, + evm_compatibility::primitive_serialize_ext::PrimitiveSerializeExt, + sql::proof_exprs::{ColumnExpr, DynProofExpr, EqualsExpr, LiteralExpr}, +}; +use snafu::OptionExt; + +impl DynProofPlanSerializer { + pub(super) fn serialize_dyn_proof_expr( + self, + expr: &DynProofExpr, + ) -> Result { + match expr { + DynProofExpr::Column(column_expr) => self + .serialize_u8(COLUMN_EXPR_NUM) + .serialize_column_expr(column_expr), + DynProofExpr::Literal(literal_expr) => self + .serialize_u8(LITERAL_EXPR_NUM) + .serialize_literal_expr(literal_expr), + DynProofExpr::Equals(equals_expr) => self + .serialize_u8(EQUALS_EXPR_NUM) + .serialize_equals_expr(equals_expr), + _ => NotSupportedSnafu.fail(), + } + } + + fn serialize_column_expr( + self, + column_expr: &ColumnExpr, + ) -> Result { + let column_number = self + .column_refs + .get(&column_expr.column_ref) + .copied() + .context(ColumnNotFoundSnafu)?; + Ok(self.serialize_u8(column_number)) + } + + fn serialize_literal_expr( + self, + literal_expr: &LiteralExpr, + ) -> Result { + match literal_expr.value { + LiteralValue::BigInt(value) => Ok(self + .serialize_u8(BIGINT_TYPE_NUM) + .serialize_scalar(value.into())), + _ => NotSupportedSnafu.fail(), + } + } + + fn serialize_equals_expr( + self, + equals_expr: &EqualsExpr, + ) -> Result { + self.serialize_dyn_proof_expr(equals_expr.lhs.as_ref())? + .serialize_dyn_proof_expr(equals_expr.rhs.as_ref()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::{ + base::{ + database::{ColumnRef, ColumnType, TableRef}, + map::indexset, + scalar::test_scalar::TestScalar, + }, + sql::proof_exprs::AndExpr, + }; + use core::iter; + use itertools::Itertools; + + #[test] + fn we_can_serialize_a_column_expr() { + let table_ref: TableRef = "namespace.table".parse().unwrap(); + let column_0_ref: ColumnRef = + ColumnRef::new(table_ref, "column_0".parse().unwrap(), ColumnType::BigInt); + let column_1_ref: ColumnRef = + ColumnRef::new(table_ref, "column_1".parse().unwrap(), ColumnType::BigInt); + let column_2_ref: ColumnRef = + ColumnRef::new(table_ref, "column_2".parse().unwrap(), ColumnType::BigInt); + let serializer = DynProofPlanSerializer::::try_new( + indexset! {}, + indexset! { column_0_ref, column_1_ref }, + ) + .unwrap(); + + // Serialization of column 0 should result in a single byte with value 0. + let column_0_expr = ColumnExpr::new(column_0_ref); + let bytes_0 = serializer + .clone() + .serialize_column_expr(&column_0_expr) + .unwrap() + .into_bytes(); + assert_eq!(bytes_0, vec![0]); + + // Serialization of column 1 should result in a single byte with value 1. + let column_1_expr = ColumnExpr::new(column_1_ref); + let bytes_1 = serializer + .clone() + .serialize_column_expr(&column_1_expr) + .unwrap() + .into_bytes(); + assert_eq!(bytes_1, vec![1]); + + // Wrapping the column expression in a `DynProofExpr` should result in the same serialization, + // but with the column expression number prepended. + let wrapped_column_1_expr = DynProofExpr::Column(column_1_expr); + let wrapped_bytes_1 = serializer + .clone() + .serialize_dyn_proof_expr(&wrapped_column_1_expr) + .unwrap() + .into_bytes(); + assert_eq!(wrapped_bytes_1, vec![COLUMN_EXPR_NUM, 1]); + + // Serialization of column 2 should result in an error because there are only two columns. + let column_2_expr = ColumnExpr::new(column_2_ref); + let result = serializer.clone().serialize_column_expr(&column_2_expr); + assert!(matches!( + result, + Err(ProofPlanSerializationError::ColumnNotFound) + )); + } + + #[test] + fn we_can_serialize_a_literal_expr() { + let serializer = + DynProofPlanSerializer::::try_new(indexset! {}, indexset! {}).unwrap(); + + // Serialization of a big int literal should result in a byte with the big int type number, + // followed by the big int value in big-endian form, padded with leading zeros to 32 bytes. + let literal_bigint_expr = LiteralExpr::new(LiteralValue::BigInt(4200)); + let bigint_bytes = serializer + .clone() + .serialize_literal_expr(&literal_bigint_expr) + .unwrap() + .into_bytes(); + let expected_bigint_bytes = iter::empty::() + .chain([BIGINT_TYPE_NUM]) + .chain([0; 30]) + .chain([16, 104]) + .collect_vec(); + assert_eq!(bigint_bytes, expected_bigint_bytes); + + // Wrapping the literal expression in a `DynProofExpr` should result in the same serialization, + // but with the literal expression number prepended. + let wrapped_literal_expr = DynProofExpr::Literal(literal_bigint_expr); + let wrapped_bytes = serializer + .clone() + .serialize_dyn_proof_expr(&wrapped_literal_expr) + .unwrap() + .into_bytes(); + let expected_wrapped_bytes = iter::empty::() + .chain([LITERAL_EXPR_NUM]) + .chain(expected_bigint_bytes) + .collect_vec(); + assert_eq!(wrapped_bytes, expected_wrapped_bytes); + + // Serialization of a small int literal should result in an error + // because only big int literals are supported so far + let literal_smallint_expr = LiteralExpr::new(LiteralValue::SmallInt(4200)); + let result = serializer + .clone() + .serialize_literal_expr(&literal_smallint_expr); + assert!(matches!( + result, + Err(ProofPlanSerializationError::NotSupported) + )); + } + + #[test] + fn we_can_serialize_an_equals_expr() { + let table_ref: TableRef = "namespace.table".parse().unwrap(); + let column_0_ref: ColumnRef = + ColumnRef::new(table_ref, "column_0".parse().unwrap(), ColumnType::BigInt); + let serializer = + DynProofPlanSerializer::::try_new(indexset! {}, indexset! { column_0_ref }) + .unwrap(); + + let lhs = DynProofExpr::Column(ColumnExpr::new(column_0_ref)); + let rhs = DynProofExpr::Literal(LiteralExpr::new(LiteralValue::BigInt(4200))); + let lhs_bytes = serializer + .clone() + .serialize_dyn_proof_expr(&lhs) + .unwrap() + .into_bytes(); + let rhs_bytes = serializer + .clone() + .serialize_dyn_proof_expr(&rhs) + .unwrap() + .into_bytes(); + + // Serialization of an equals expression should result in the serialization of the left-hand side, + // followed by the serialization of the right-hand side. + let equals_expr = EqualsExpr::new(Box::new(lhs.clone()), Box::new(rhs.clone())); + let bytes = serializer + .clone() + .serialize_equals_expr(&equals_expr) + .unwrap() + .into_bytes(); + let expected_bytes = iter::empty::() + .chain(lhs_bytes.clone()) + .chain(rhs_bytes.clone()) + .collect_vec(); + assert_eq!(bytes, expected_bytes); + + // Wrapping the equals expression in a `DynProofExpr` should result in the same serialization, + // but with the equals expression number prepended. + let wrapped_equals_expr = DynProofExpr::Equals(equals_expr); + let wrapped_bytes = serializer + .clone() + .serialize_dyn_proof_expr(&wrapped_equals_expr) + .unwrap() + .into_bytes(); + let expected_wrapped_bytes = iter::empty::() + .chain([EQUALS_EXPR_NUM]) + .chain(expected_bytes) + .collect_vec(); + assert_eq!(wrapped_bytes, expected_wrapped_bytes); + } + + #[test] + fn we_cannot_serialize_an_unsupported_expr() { + let table_ref: TableRef = "namespace.table".parse().unwrap(); + let column_0_ref: ColumnRef = + ColumnRef::new(table_ref, "column_0".parse().unwrap(), ColumnType::BigInt); + let serializer = + DynProofPlanSerializer::::try_new(indexset! {}, indexset! { column_0_ref }) + .unwrap(); + + let lhs = DynProofExpr::Column(ColumnExpr::new(column_0_ref)); + let rhs = DynProofExpr::Literal(LiteralExpr::new(LiteralValue::BigInt(4200))); + let expr = DynProofExpr::And(AndExpr::new(Box::new(lhs.clone()), Box::new(rhs.clone()))); + let result = serializer.clone().serialize_dyn_proof_expr(&expr); + assert!(matches!( + result, + Err(ProofPlanSerializationError::NotSupported) + )); + } +} diff --git a/crates/proof-of-sql/src/evm_compatibility/dyn_proof_plan_serializer/serialize_proof_plan.rs b/crates/proof-of-sql/src/evm_compatibility/dyn_proof_plan_serializer/serialize_proof_plan.rs new file mode 100644 index 000000000..cfb418e83 --- /dev/null +++ b/crates/proof-of-sql/src/evm_compatibility/dyn_proof_plan_serializer/serialize_proof_plan.rs @@ -0,0 +1,218 @@ +use super::{ + constants::FILTER_EXEC_NUM, + error::{NotSupportedSnafu, TableNotFoundSnafu, TooManyResultsSnafu}, + DynProofPlanSerializer, ProofPlanSerializationError, +}; +use crate::{ + base::scalar::Scalar, + evm_compatibility::primitive_serialize_ext::PrimitiveSerializeExt, + sql::{ + proof_exprs::{AliasedDynProofExpr, TableExpr}, + proof_plans::{DynProofPlan, FilterExec}, + }, +}; +use snafu::OptionExt; + +impl DynProofPlanSerializer { + pub fn serialize_dyn_proof_plan( + self, + plan: &DynProofPlan, + ) -> Result { + match plan { + DynProofPlan::Filter(filter_exec) => self + .serialize_u8(FILTER_EXEC_NUM) + .serialize_filter_exec(filter_exec), + _ => NotSupportedSnafu.fail(), + } + } + + fn serialize_filter_exec( + self, + filter_exec: &FilterExec, + ) -> Result { + let result_count = u8::try_from(filter_exec.aliased_results.len()) + .ok() + .context(TooManyResultsSnafu)?; + + filter_exec + .aliased_results + .iter() + .try_fold( + self.serialize_table_expr(&filter_exec.table)? + .serialize_u8(result_count), + Self::serialize_aliased_dyn_proof_expr, + )? + .serialize_dyn_proof_expr(&filter_exec.where_clause) + } + + fn serialize_table_expr( + self, + table_expr: &TableExpr, + ) -> Result { + let table_number = self + .table_refs + .get(&table_expr.table_ref) + .copied() + .context(TableNotFoundSnafu)?; + Ok(self.serialize_u8(table_number)) + } + + fn serialize_aliased_dyn_proof_expr( + self, + aliased_expr: &AliasedDynProofExpr, + ) -> Result { + self.serialize_dyn_proof_expr(&aliased_expr.expr) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::{ + base::{database::LiteralValue, map::indexset, scalar::test_scalar::TestScalar}, + sql::proof_exprs::{DynProofExpr, LiteralExpr}, + }; + use core::iter; + use itertools::Itertools; + + #[test] + fn we_can_serialize_an_aliased_dyn_proof_expr() { + let serializer = + DynProofPlanSerializer::::try_new(indexset! {}, indexset! {}).unwrap(); + + let expr = DynProofExpr::Literal(LiteralExpr::new(LiteralValue::BigInt(4200))); + let expr_bytes = serializer + .clone() + .serialize_dyn_proof_expr(&expr) + .unwrap() + .into_bytes(); + + let aliased_expr = AliasedDynProofExpr { + expr: expr.clone(), + alias: "alias".parse().unwrap(), + }; + let bytes = serializer + .clone() + .serialize_aliased_dyn_proof_expr(&aliased_expr) + .unwrap() + .into_bytes(); + assert_eq!(bytes, expr_bytes); + } + + #[test] + fn we_can_serialize_a_table_expr() { + let table_0_ref = "namespace.table_0".parse().unwrap(); + let table_1_ref = "namespace.table_1".parse().unwrap(); + let table_2_ref = "namespace.table_2".parse().unwrap(); + let serializer = DynProofPlanSerializer::::try_new( + indexset! { table_0_ref, table_1_ref }, + indexset! {}, + ) + .unwrap(); + + // Serialization of table 0 should result in a single byte with value 0. + let table_0_expr = TableExpr { + table_ref: table_0_ref, + }; + let bytes_0 = serializer + .clone() + .serialize_table_expr(&table_0_expr) + .unwrap() + .into_bytes(); + assert_eq!(bytes_0, vec![0]); + + // Serialization of table 1 should result in a single byte with value 1. + let table_1_expr = TableExpr { + table_ref: table_1_ref, + }; + let bytes_1 = serializer + .clone() + .serialize_table_expr(&table_1_expr) + .unwrap() + .into_bytes(); + assert_eq!(bytes_1, vec![1]); + + // Serialization of table 2 should result in an error because it is not in the serializer's set. + let table_2_expr = TableExpr { + table_ref: table_2_ref, + }; + let result = serializer.clone().serialize_table_expr(&table_2_expr); + assert!(matches!( + result, + Err(ProofPlanSerializationError::TableNotFound) + )); + } + + #[test] + fn we_can_serialize_a_filter_exec() { + let table_ref = "namespace.table".parse().unwrap(); + let serializer = + DynProofPlanSerializer::::try_new(indexset! { table_ref }, indexset! {}) + .unwrap(); + + let expr_a = DynProofExpr::Literal(LiteralExpr::new(LiteralValue::BigInt(4200))); + let expr_b = DynProofExpr::Literal(LiteralExpr::new(LiteralValue::BigInt(4200))); + let expr_c = DynProofExpr::Literal(LiteralExpr::new(LiteralValue::BigInt(4200))); + let aliased_expr_0 = AliasedDynProofExpr { + expr: expr_a.clone(), + alias: "alias_0".parse().unwrap(), + }; + let aliased_expr_1 = AliasedDynProofExpr { + expr: expr_b.clone(), + alias: "alias_1".parse().unwrap(), + }; + let table_expr = TableExpr { table_ref }; + + let expr_c_bytes = serializer + .clone() + .serialize_dyn_proof_expr(&expr_c) + .unwrap() + .into_bytes(); + let aliased_expr_0_bytes = serializer + .clone() + .serialize_aliased_dyn_proof_expr(&aliased_expr_0) + .unwrap() + .into_bytes(); + let aliased_expr_1_bytes = serializer + .clone() + .serialize_aliased_dyn_proof_expr(&aliased_expr_1) + .unwrap() + .into_bytes(); + let table_expr_bytes = serializer + .clone() + .serialize_table_expr(&table_expr) + .unwrap() + .into_bytes(); + + // Serialization of a filter exec should result in the table number, the number of results, + // the serialized aliased expressions, and the serialized where clause. + let filter_exec = FilterExec::new(vec![aliased_expr_0, aliased_expr_1], table_expr, expr_c); + let expected_bytes = iter::empty::() + .chain(table_expr_bytes) + .chain([2]) + .chain(aliased_expr_0_bytes) + .chain(aliased_expr_1_bytes) + .chain(expr_c_bytes) + .collect_vec(); + let bytes = serializer + .clone() + .serialize_filter_exec(&filter_exec) + .unwrap() + .into_bytes(); + assert_eq!(bytes, expected_bytes); + + // Serialization of a filter DynProofPlan should result in the + // filter exec number and the serialized filter exec. + let wrapped_filter_exec = DynProofPlan::Filter(filter_exec); + let expected_wrapped_bytes = iter::empty::() + .chain([FILTER_EXEC_NUM]) + .chain(expected_bytes) + .collect_vec(); + let wrapped_bytes = serializer + .clone() + .serialize_dyn_proof_plan(&wrapped_filter_exec) + .unwrap() + .into_bytes(); + assert_eq!(wrapped_bytes, expected_wrapped_bytes); + } +} diff --git a/crates/proof-of-sql/src/evm_compatibility/dyn_proof_plan_serializer/serializer.rs b/crates/proof-of-sql/src/evm_compatibility/dyn_proof_plan_serializer/serializer.rs new file mode 100644 index 000000000..c6110c3ec --- /dev/null +++ b/crates/proof-of-sql/src/evm_compatibility/dyn_proof_plan_serializer/serializer.rs @@ -0,0 +1,140 @@ +use super::{ + error::{TooManyColumnsSnafu, TooManyTablesSnafu}, + ProofPlanSerializationError, +}; +use crate::{ + base::{ + database::{ColumnRef, TableRef}, + map::{IndexMap, IndexSet}, + scalar::Scalar, + }, + evm_compatibility::primitive_serialize_ext::PrimitiveSerializeExt, +}; +use alloc::vec::Vec; +use core::marker::PhantomData; + +/// A serializer for a `DynProofPlan`. +#[derive(Debug, Clone)] +pub struct DynProofPlanSerializer { + pub(super) table_refs: IndexMap, + pub(super) column_refs: IndexMap, + data: Vec, + _phantom: PhantomData, +} + +impl PrimitiveSerializeExt for DynProofPlanSerializer { + fn serialize_slice(mut self, value: &[u8]) -> Self { + self.data.extend_from_slice(value); + self + } +} + +impl DynProofPlanSerializer { + /// Converts the serialized plan into a byte vector. + /// + /// # Returns + /// + /// * `Vec` - The serialized byte vector. + #[must_use] + pub fn into_bytes(self) -> Vec { + self.data + } + + /// Creates a new serializer with the given table and column references. + /// + /// # Arguments + /// + /// * `table_refs` - A set of table references. + /// * `column_refs` - A set of column references. + /// + /// # Returns + /// + /// * `Result` - The new serializer or an error if there are too many tables or columns. + pub fn try_new( + table_refs: IndexSet, + column_refs: IndexSet, + ) -> Result { + if u8::try_from(table_refs.len()).is_err() { + TooManyTablesSnafu.fail()?; + } + if u8::try_from(column_refs.len()).is_err() { + TooManyColumnsSnafu.fail()?; + } + Ok(Self { + table_refs: table_refs.into_iter().zip(0..).collect(), + column_refs: column_refs.into_iter().zip(0..).collect(), + data: Vec::new(), + _phantom: PhantomData, + }) + } +} + +#[cfg(test)] +mod tests { + use crate::{ + base::{ + database::{ColumnRef, ColumnType, TableRef}, + map::{indexmap, indexset}, + scalar::test_scalar::TestScalar, + }, + evm_compatibility::dyn_proof_plan_serializer::{ + DynProofPlanSerializer, ProofPlanSerializationError, + }, + }; + + #[test] + fn we_can_create_dyn_proof_plan_serializer() { + let table_ref_1: TableRef = "namespace.table1".parse().unwrap(); + let table_ref_2: TableRef = "namespace.table2".parse().unwrap(); + let column_ref_1: ColumnRef = + ColumnRef::new(table_ref_1, "column1".parse().unwrap(), ColumnType::BigInt); + let column_ref_2: ColumnRef = + ColumnRef::new(table_ref_2, "column2".parse().unwrap(), ColumnType::BigInt); + + let table_refs = indexset! { table_ref_1, table_ref_2 }; + let column_refs = indexset! { column_ref_1, column_ref_2 }; + let serializer = + DynProofPlanSerializer::::try_new(table_refs, column_refs).unwrap(); + assert_eq!( + serializer.table_refs, + indexmap! { table_ref_1 => 0, table_ref_2 => 1 } + ); + assert_eq!( + serializer.column_refs, + indexmap! { column_ref_1 => 0, column_ref_2 => 1 } + ); + } + + #[test] + fn we_cannot_create_dyn_proof_plan_serializer_with_too_many_tables() { + let table_refs = (0..=u8::MAX as usize) + .map(|i| format!("namespace.table{i}").parse().unwrap()) + .collect(); + + let result = DynProofPlanSerializer::::try_new(table_refs, indexset! {}); + assert!(matches!( + result, + Err(ProofPlanSerializationError::TooManyTables) + )); + } + + #[test] + fn we_cannot_create_dyn_proof_plan_serializer_with_too_many_columns() { + let table_ref: TableRef = "namespace.table".parse().unwrap(); + let column_refs = (0..=u8::MAX as usize) + .map(|i| { + ColumnRef::new( + table_ref, + format!("column{i}").parse().unwrap(), + ColumnType::BigInt, + ) + }) + .collect(); + + let result = DynProofPlanSerializer::::try_new(indexset! {}, column_refs); + assert!(matches!( + result, + Err(ProofPlanSerializationError::TooManyColumns) + )); + } +} diff --git a/crates/proof-of-sql/src/evm_compatibility/mod.rs b/crates/proof-of-sql/src/evm_compatibility/mod.rs new file mode 100644 index 000000000..58562bf1c --- /dev/null +++ b/crates/proof-of-sql/src/evm_compatibility/mod.rs @@ -0,0 +1,4 @@ +mod dyn_proof_plan_serializer; +mod primitive_serialize_ext; +mod serialize_query_expr; +pub use serialize_query_expr::serialize_query_expr; diff --git a/crates/proof-of-sql/src/evm_compatibility/primitive_serialize_ext.rs b/crates/proof-of-sql/src/evm_compatibility/primitive_serialize_ext.rs new file mode 100644 index 000000000..38dceb44c --- /dev/null +++ b/crates/proof-of-sql/src/evm_compatibility/primitive_serialize_ext.rs @@ -0,0 +1,105 @@ +use crate::base::scalar::Scalar; +use zerocopy::AsBytes; + +/// Trait for serializing primitive types in a way that is compatible with efficient proof verification on the EVM. +pub trait PrimitiveSerializeExt: Sized { + /// Serializes a slice of bytes. + /// + /// # Arguments + /// + /// * `value` - A slice of bytes to be serialized. + /// + /// # Returns + /// + /// * `Self` - The serialized result. + fn serialize_slice(self, value: &[u8]) -> Self; + + /// Serializes a single byte. + /// + /// # Arguments + /// + /// * `value` - A byte to be serialized. + /// + /// # Returns + /// + /// * `Self` - The serialized result. + fn serialize_u8(self, value: u8) -> Self { + self.serialize_slice(&[value]) + } + + /// Serializes a scalar value. The scalar is serialized as a 256-bit, bytwise-big-endian integer. + /// This is the format used by the EVM for representing integers. + /// + /// # Arguments + /// + /// * `value` - A scalar value to be serialized. + /// + /// # Returns + /// + /// * `Self` - The serialized result. + fn serialize_scalar(self, value: S) -> Self { + let mut limbs: [u64; 4] = value.into(); + limbs.as_bytes_mut().reverse(); + self.serialize_slice(limbs.as_bytes()) + } +} + +#[cfg(test)] +mod tests { + use super::PrimitiveSerializeExt; + use crate::base::scalar::{test_scalar::TestScalar, Scalar}; + use core::{iter, marker::PhantomData}; + use itertools::Itertools; + struct MockSerializer(Vec, PhantomData); + impl MockSerializer { + fn new() -> Self { + MockSerializer(Vec::new(), PhantomData) + } + fn into_inner(self) -> Vec { + self.0 + } + } + impl PrimitiveSerializeExt for MockSerializer { + fn serialize_slice(mut self, value: &[u8]) -> Self { + self.0.extend_from_slice(value); + self + } + } + + #[test] + fn we_can_serialize_u8() { + let serializer = MockSerializer::::new(); + let result = serializer.serialize_u8(123).into_inner(); + assert_eq!(result, vec![123]); + } + + #[test] + fn we_can_serialize_scalar_that_requires_one_bytes() { + let serializer = MockSerializer::::new(); + let bytes = serializer + .serialize_scalar(TestScalar::from(123)) + .into_inner(); + assert_eq!( + bytes, + iter::empty::() + .chain([0; 31]) + .chain([123]) + .collect_vec() + ); + } + + #[test] + fn we_can_serialize_scalar_that_requires_two_bytes() { + let serializer = MockSerializer::::new(); + let bytes = serializer + .serialize_scalar(TestScalar::from(123 + (45 << 8))) + .into_inner(); + assert_eq!( + bytes, + iter::empty::() + .chain([0; 30]) + .chain([45, 123]) + .collect_vec() + ); + } +} diff --git a/crates/proof-of-sql/src/evm_compatibility/serialize_query_expr.rs b/crates/proof-of-sql/src/evm_compatibility/serialize_query_expr.rs new file mode 100644 index 000000000..cddac175c --- /dev/null +++ b/crates/proof-of-sql/src/evm_compatibility/serialize_query_expr.rs @@ -0,0 +1,158 @@ +use super::dyn_proof_plan_serializer::{DynProofPlanSerializer, ProofPlanSerializationError}; +use crate::{ + base::scalar::Scalar, + sql::{parse::QueryExpr, proof::ProofPlan}, +}; +use alloc::vec::Vec; + +/// Serializes a `QueryExpr` into a vector of bytes. +/// +/// This function takes a `QueryExpr` and attempts to serialize it into a vector of bytes. +/// The serialization is done in a manner that is compatible with efficient proof verification +/// on the EVM. +/// +/// # Arguments +/// +/// * `query_expr` - A reference to the `QueryExpr` to be serialized. +/// +/// # Returns +/// +/// * `Ok(Vec)` - A vector of bytes representing the serialized query expression. +/// * `Err(ProofPlanSerializationError)` - An error indicating why the serialization failed. +/// +/// # Errors +/// +/// This function returns a `ProofPlanSerializationError::NotSupported` error if the query +/// expression contains postprocessing steps or if the proof plan cannot be serialized. +pub fn serialize_query_expr( + query_expr: &QueryExpr, +) -> Result, ProofPlanSerializationError> { + let plan = query_expr + .postprocessing() + .is_empty() + .then(|| query_expr.proof_expr()) + .ok_or(ProofPlanSerializationError::NotSupported)?; + let bytes = DynProofPlanSerializer::::try_new( + plan.get_table_references(), + plan.get_column_references(), + )? + .serialize_dyn_proof_plan(plan)? + .into_bytes(); + Ok(bytes) +} + +#[cfg(test)] +mod tests { + use crate::{ + base::{ + database::{ColumnRef, ColumnType, LiteralValue}, + map::indexset, + scalar::test_scalar::TestScalar, + }, + evm_compatibility::{ + dyn_proof_plan_serializer::{ + constants::*, DynProofPlanSerializer, ProofPlanSerializationError, + }, + serialize_query_expr, + }, + sql::{ + parse::QueryExpr, + postprocessing::{OwnedTablePostprocessing, SlicePostprocessing}, + proof_exprs::{ + AliasedDynProofExpr, ColumnExpr, DynProofExpr, EqualsExpr, LiteralExpr, TableExpr, + }, + proof_plans::{DynProofPlan, EmptyExec, FilterExec}, + }, + }; + use core::iter; + use itertools::Itertools; + + #[test] + fn we_can_generate_serialized_proof_plan_for_query_expr() { + let table_ref = "namespace.table".parse().unwrap(); + let identifier_alias = "alias".parse().unwrap(); + + let plan = DynProofPlan::Filter(FilterExec::new( + vec![AliasedDynProofExpr { + expr: DynProofExpr::Literal(LiteralExpr::new(LiteralValue::BigInt(1001))), + alias: identifier_alias, + }], + TableExpr { table_ref }, + DynProofExpr::Literal(LiteralExpr::new(LiteralValue::BigInt(1002))), + )); + + // Serializing a query expression without postprocessing steps should succeed and + // return the serialization of the proof plan. + let query_expr = QueryExpr::new(plan.clone(), vec![]); + let expected_bytes = + DynProofPlanSerializer::::try_new(indexset! { table_ref }, indexset! {}) + .unwrap() + .serialize_dyn_proof_plan(&plan) + .unwrap() + .into_bytes(); + + let bytes = serialize_query_expr::(&query_expr).unwrap(); + assert_eq!(bytes, expected_bytes); + + // Serializing a query expression with postprocessing steps should fail. + let post_processing_query_expr = QueryExpr::new( + plan, + vec![OwnedTablePostprocessing::Slice(SlicePostprocessing::new( + None, None, + ))], + ); + let result = serialize_query_expr::(&post_processing_query_expr); + assert!(matches!( + result, + Err(ProofPlanSerializationError::NotSupported) + )); + } + + #[test] + fn we_cannot_generate_serialized_proof_plan_for_unsupported_plan() { + let plan = DynProofPlan::Empty(EmptyExec::new()); + let result = serialize_query_expr::(&QueryExpr::new(plan, vec![])); + assert!(matches!( + result, + Err(ProofPlanSerializationError::NotSupported) + )); + } + + #[test] + fn we_can_generate_serialized_proof_plan_for_simple_filter() { + let table_ref = "namespace.table".parse().unwrap(); + let identifier_a = "a".parse().unwrap(); + let identifier_b = "b".parse().unwrap(); + let identifier_alias = "alias".parse().unwrap(); + + let column_ref_a = ColumnRef::new(table_ref, identifier_a, ColumnType::BigInt); + let column_ref_b = ColumnRef::new(table_ref, identifier_b, ColumnType::BigInt); + + let plan = DynProofPlan::Filter(FilterExec::new( + vec![AliasedDynProofExpr { + expr: DynProofExpr::Column(ColumnExpr::new(column_ref_b)), + alias: identifier_alias, + }], + TableExpr { table_ref }, + DynProofExpr::Equals(EqualsExpr::new( + Box::new(DynProofExpr::Column(ColumnExpr::new(column_ref_a))), + Box::new(DynProofExpr::Literal(LiteralExpr::new( + LiteralValue::BigInt(5), + ))), + )), + )); + + let query_expr = QueryExpr::new(plan, vec![]); + let bytes = serialize_query_expr::(&query_expr).unwrap(); + let expected_bytes = iter::empty::() + .chain([FILTER_EXEC_NUM, 0, 1]) // filter expr, table number, result count + .chain([COLUMN_EXPR_NUM, 0]) // column expr, column b (#0) + .chain([EQUALS_EXPR_NUM]) // equals expr + .chain([COLUMN_EXPR_NUM, 1]) // column expr, column a (#1) + .chain([LITERAL_EXPR_NUM, BIGINT_TYPE_NUM]) // literal expr, literal type + .chain([0; 31]) // leading 0s of literal value + .chain([5]) // literal value + .collect_vec(); + assert_eq!(bytes, expected_bytes); + } +} diff --git a/crates/proof-of-sql/src/lib.rs b/crates/proof-of-sql/src/lib.rs index 6595f74a2..4a04f6ff8 100644 --- a/crates/proof-of-sql/src/lib.rs +++ b/crates/proof-of-sql/src/lib.rs @@ -13,3 +13,6 @@ pub mod utils; #[cfg(test)] mod tests; + +/// Module for converting Proof of SQL components to EVM compatible format +pub mod evm_compatibility; diff --git a/crates/proof-of-sql/src/sql/proof/proof_plan.rs b/crates/proof-of-sql/src/sql/proof/proof_plan.rs index 67547cde8..a84dcdab7 100644 --- a/crates/proof-of-sql/src/sql/proof/proof_plan.rs +++ b/crates/proof-of-sql/src/sql/proof/proof_plan.rs @@ -61,6 +61,6 @@ pub trait ProverEvaluate { pub trait ProverHonestyMarker: Debug + Send + Sync + PartialEq + 'static {} /// [`ProverHonestyMarker`] for generic [`ProofPlan`] types whose implementation is canonical/honest. -#[derive(Debug, PartialEq)] +#[derive(Debug, PartialEq, Clone)] pub struct HonestProver; impl ProverHonestyMarker for HonestProver {} diff --git a/crates/proof-of-sql/src/sql/proof_exprs/column_expr.rs b/crates/proof-of-sql/src/sql/proof_exprs/column_expr.rs index 58493099e..3f25d6dcb 100644 --- a/crates/proof-of-sql/src/sql/proof_exprs/column_expr.rs +++ b/crates/proof-of-sql/src/sql/proof_exprs/column_expr.rs @@ -16,7 +16,7 @@ use serde::{Deserialize, Serialize}; /// Note: this is currently limited to named column expressions. #[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize)] pub struct ColumnExpr { - column_ref: ColumnRef, + pub(crate) column_ref: ColumnRef, } impl ColumnExpr { diff --git a/crates/proof-of-sql/src/sql/proof_exprs/equals_expr.rs b/crates/proof-of-sql/src/sql/proof_exprs/equals_expr.rs index 37ef02b9b..c88b10aff 100644 --- a/crates/proof-of-sql/src/sql/proof_exprs/equals_expr.rs +++ b/crates/proof-of-sql/src/sql/proof_exprs/equals_expr.rs @@ -17,8 +17,8 @@ use serde::{Deserialize, Serialize}; /// Provable AST expression for an equals expression #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] pub struct EqualsExpr { - lhs: Box, - rhs: Box, + pub(crate) lhs: Box, + pub(crate) rhs: Box, } impl EqualsExpr { diff --git a/crates/proof-of-sql/src/sql/proof_exprs/literal_expr.rs b/crates/proof-of-sql/src/sql/proof_exprs/literal_expr.rs index 21f5b7cb3..e4cd8cf0e 100644 --- a/crates/proof-of-sql/src/sql/proof_exprs/literal_expr.rs +++ b/crates/proof-of-sql/src/sql/proof_exprs/literal_expr.rs @@ -25,7 +25,7 @@ use serde::{Deserialize, Serialize}; /// changes, and the performance is sufficient for present. #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] pub struct LiteralExpr { - value: LiteralValue, + pub(crate) value: LiteralValue, } impl LiteralExpr { diff --git a/crates/proof-of-sql/src/sql/proof_exprs/mod.rs b/crates/proof-of-sql/src/sql/proof_exprs/mod.rs index 5143559c0..cb5c7303a 100644 --- a/crates/proof-of-sql/src/sql/proof_exprs/mod.rs +++ b/crates/proof-of-sql/src/sql/proof_exprs/mod.rs @@ -34,7 +34,7 @@ pub(crate) use literal_expr::LiteralExpr; mod literal_expr_test; mod and_expr; -use and_expr::AndExpr; +pub(crate) use and_expr::AndExpr; #[cfg(all(test, feature = "blitzar"))] mod and_expr_test; @@ -62,9 +62,9 @@ pub(crate) use numerical_util::{ }; mod equals_expr; +pub(crate) use equals_expr::EqualsExpr; use equals_expr::{ prover_evaluate_equals_zero, result_evaluate_equals_zero, verifier_evaluate_equals_zero, - EqualsExpr, }; #[cfg(all(test, feature = "blitzar"))] mod equals_expr_test; diff --git a/crates/proof-of-sql/src/sql/proof_exprs/table_expr.rs b/crates/proof-of-sql/src/sql/proof_exprs/table_expr.rs index b6ba40e96..56e259a8b 100644 --- a/crates/proof-of-sql/src/sql/proof_exprs/table_expr.rs +++ b/crates/proof-of-sql/src/sql/proof_exprs/table_expr.rs @@ -2,7 +2,7 @@ use crate::base::database::TableRef; use serde::{Deserialize, Serialize}; /// Expression for an SQL table -#[derive(Debug, PartialEq, Eq, Serialize, Deserialize)] +#[derive(Debug, PartialEq, Eq, Serialize, Deserialize, Clone)] pub struct TableExpr { pub table_ref: TableRef, } diff --git a/crates/proof-of-sql/src/sql/proof_plans/dyn_proof_plan.rs b/crates/proof-of-sql/src/sql/proof_plans/dyn_proof_plan.rs index 22e69c2b2..7e332d159 100644 --- a/crates/proof-of-sql/src/sql/proof_plans/dyn_proof_plan.rs +++ b/crates/proof-of-sql/src/sql/proof_plans/dyn_proof_plan.rs @@ -15,7 +15,7 @@ use bumpalo::Bump; use serde::{Deserialize, Serialize}; /// The query plan for proving a query -#[derive(Debug, PartialEq, Serialize, Deserialize)] +#[derive(Debug, PartialEq, Serialize, Deserialize, Clone)] #[enum_dispatch::enum_dispatch] pub enum DynProofPlan { /// Source [`ProofPlan`] for (sub)queries without table source such as `SELECT "No table here" as msg;` diff --git a/crates/proof-of-sql/src/sql/proof_plans/empty_exec.rs b/crates/proof-of-sql/src/sql/proof_plans/empty_exec.rs index 91943113c..9f3cf0815 100644 --- a/crates/proof-of-sql/src/sql/proof_plans/empty_exec.rs +++ b/crates/proof-of-sql/src/sql/proof_plans/empty_exec.rs @@ -18,7 +18,7 @@ use serde::{Deserialize, Serialize}; /// Source [`ProofPlan`] for (sub)queries without table source such as `SELECT "No table here" as msg;` /// Inspired by [`DataFusion EmptyExec`](https://docs.rs/datafusion/latest/datafusion/physical_plan/empty/struct.EmptyExec.html) -#[derive(Debug, PartialEq, Serialize, Deserialize)] +#[derive(Debug, PartialEq, Serialize, Deserialize, Clone)] pub struct EmptyExec {} impl Default for EmptyExec { diff --git a/crates/proof-of-sql/src/sql/proof_plans/filter_exec.rs b/crates/proof-of-sql/src/sql/proof_plans/filter_exec.rs index 5778f47db..0bfece895 100644 --- a/crates/proof-of-sql/src/sql/proof_plans/filter_exec.rs +++ b/crates/proof-of-sql/src/sql/proof_plans/filter_exec.rs @@ -31,10 +31,10 @@ use serde::{Deserialize, Serialize}; /// ``` /// /// This differs from the [`FilterExec`] in that the result is not a sparse table. -#[derive(Debug, PartialEq, Serialize, Deserialize)] +#[derive(Debug, PartialEq, Serialize, Deserialize, Clone)] pub struct OstensibleFilterExec { - pub(super) aliased_results: Vec, - pub(super) table: TableExpr, + pub(crate) aliased_results: Vec, + pub(crate) table: TableExpr, /// TODO: add docs pub(crate) where_clause: DynProofExpr, phantom: PhantomData, diff --git a/crates/proof-of-sql/src/sql/proof_plans/group_by_exec.rs b/crates/proof-of-sql/src/sql/proof_plans/group_by_exec.rs index e935fcb88..4a9270985 100644 --- a/crates/proof-of-sql/src/sql/proof_plans/group_by_exec.rs +++ b/crates/proof-of-sql/src/sql/proof_plans/group_by_exec.rs @@ -39,7 +39,7 @@ use serde::{Deserialize, Serialize}; /// ``` /// /// Note: if `group_by_exprs` is empty, then the query is equivalent to removing the `GROUP BY` clause. -#[derive(Debug, PartialEq, Serialize, Deserialize)] +#[derive(Debug, PartialEq, Serialize, Deserialize, Clone)] pub struct GroupByExec { pub(super) group_by_exprs: Vec, pub(super) sum_expr: Vec, diff --git a/crates/proof-of-sql/src/sql/proof_plans/projection_exec.rs b/crates/proof-of-sql/src/sql/proof_plans/projection_exec.rs index 881734691..df10790d7 100644 --- a/crates/proof-of-sql/src/sql/proof_plans/projection_exec.rs +++ b/crates/proof-of-sql/src/sql/proof_plans/projection_exec.rs @@ -23,7 +23,7 @@ use serde::{Deserialize, Serialize}; /// ```ignore /// SELECT , ..., FROM /// ``` -#[derive(Debug, PartialEq, Serialize, Deserialize)] +#[derive(Debug, PartialEq, Serialize, Deserialize, Clone)] pub struct ProjectionExec { pub(super) aliased_results: Vec, pub(super) table: TableExpr, diff --git a/crates/proof-of-sql/src/sql/proof_plans/slice_exec.rs b/crates/proof-of-sql/src/sql/proof_plans/slice_exec.rs index c5af72444..aca38aaf1 100644 --- a/crates/proof-of-sql/src/sql/proof_plans/slice_exec.rs +++ b/crates/proof-of-sql/src/sql/proof_plans/slice_exec.rs @@ -27,7 +27,7 @@ use serde::{Deserialize, Serialize}; /// ```ignore /// LIMIT [OFFSET ] /// ``` -#[derive(Debug, PartialEq, Serialize, Deserialize)] +#[derive(Debug, PartialEq, Serialize, Deserialize, Clone)] pub struct SliceExec { pub(super) input: Box, pub(super) skip: usize, diff --git a/crates/proof-of-sql/src/sql/proof_plans/table_exec.rs b/crates/proof-of-sql/src/sql/proof_plans/table_exec.rs index 8220191d1..0e4b62fba 100644 --- a/crates/proof-of-sql/src/sql/proof_plans/table_exec.rs +++ b/crates/proof-of-sql/src/sql/proof_plans/table_exec.rs @@ -17,7 +17,7 @@ use serde::{Deserialize, Serialize}; /// Source [`ProofPlan`] for (sub)queries with table source such as `SELECT col from tab;` /// Inspired by `DataFusion` data source [`ExecutionPlan`]s such as [`ArrowExec`] and [`CsvExec`]. /// Note that we only need to load the columns we use. -#[derive(Debug, PartialEq, Serialize, Deserialize)] +#[derive(Debug, PartialEq, Serialize, Deserialize, Clone)] pub struct TableExec { /// Table reference pub table_ref: TableRef, diff --git a/crates/proof-of-sql/src/sql/proof_plans/union_exec.rs b/crates/proof-of-sql/src/sql/proof_plans/union_exec.rs index 0fca988a0..48a273398 100644 --- a/crates/proof-of-sql/src/sql/proof_plans/union_exec.rs +++ b/crates/proof-of-sql/src/sql/proof_plans/union_exec.rs @@ -30,7 +30,7 @@ use serde::{Deserialize, Serialize}; /// UNION ALL /// /// ``` -#[derive(Debug, PartialEq, Serialize, Deserialize)] +#[derive(Debug, PartialEq, Serialize, Deserialize, Clone)] pub struct UnionExec { pub(super) inputs: Vec, pub(super) schema: Vec,