Decode shared Signed values into field elements

This commit is contained in:
Sam Tay
2024-01-26 13:14:25 -05:00
parent 843090887c
commit 4313391bea
6 changed files with 230 additions and 71 deletions

View File

@@ -19,7 +19,7 @@ use crate::{
};
use sunscreen_runtime::{
InnerPlaintext, NumCiphertexts, Plaintext, ShareWithZKP, TryFromPlaintext, TryIntoPlaintext,
InnerPlaintext, NumCiphertexts, Plaintext, TryFromPlaintext, TryIntoPlaintext,
};
use std::ops::*;
@@ -36,12 +36,12 @@ impl NumCiphertexts for Signed {
const NUM_CIPHERTEXTS: usize = 1;
}
impl ShareWithZKP for Signed {
#[cfg(feature = "linkedproofs")]
impl sunscreen_runtime::ShareWithZKP for Signed {
/// While a freshly encoded plaintext will only use up to 64 coefficients, plaintexts resulting
/// from a multiplicative circuit can result in valid Signed encodings that use more than 64
/// coefficients. A bound of 128 should work for virtually all cases.
const DEGREE_BOUND: usize = 64;
// const DEGREE_BOUND: usize = 128;
const DEGREE_BOUND: usize = 128;
}
impl FheProgramInputTrait for Signed {}

View File

@@ -5,7 +5,7 @@ use paste::paste;
use seal_fhe::Plaintext as SealPlaintext;
use sunscreen_runtime::{
InnerPlaintext, NumCiphertexts, Plaintext, ShareWithZKP, TryFromPlaintext, TryIntoPlaintext,
InnerPlaintext, NumCiphertexts, Plaintext, TryFromPlaintext, TryIntoPlaintext,
};
use crate as sunscreen;
@@ -341,12 +341,17 @@ type_synonyms! {
64, 128, 256, 512
}
impl ShareWithZKP for Unsigned64 {
const DEGREE_BOUND: usize = 128;
}
#[cfg(feature = "linkedproofs")]
mod sharing {
use sunscreen_runtime::ShareWithZKP;
impl ShareWithZKP for Unsigned128 {
const DEGREE_BOUND: usize = 255;
impl ShareWithZKP for super::Unsigned64 {
const DEGREE_BOUND: usize = 128;
}
impl ShareWithZKP for super::Unsigned128 {
const DEGREE_BOUND: usize = 255;
}
}
#[cfg(test)]

View File

@@ -0,0 +1,201 @@
use sunscreen_compiler_macros::TypeName;
use sunscreen_runtime::ShareWithZKP;
use sunscreen_zkp_backend::{BigInt, FieldSpec};
use crate::{
invoke_gadget,
types::{bfv::Signed, zkp::ProgramNode},
zkp::{with_zkp_ctx, ZkpContextOps},
};
use super::{gadgets::SignedModulus, Field, NumFieldElements, ToNativeFields};
use crate as sunscreen;
/// A BFV plaintext polynomial that has been shared to a ZKP program.
///
/// Note that `C` represents the size of the 2s complement decomposition of each coefficient in the
/// polynomial. This is unfortunately plaintext modulus dependent, and can be calculated by
/// `plaintext_modulus.ilog2() + 1`.
///
/// Similarly `N` is the degree of the _shared_ polynomial, which may be less than the full lattice
/// dimension. See [`Share::DEGREE_BOUND`](sunscreen_runtime::Share).
//
// TODO if we just commit to 64-bit coefficient bounds, we can get rid of dynamic C.
#[derive(Debug, Clone, TypeName)]
struct BfvPlaintext<F: FieldSpec, const C: usize, const N: usize> {
data: Box<[[Field<F>; C]; N]>,
}
impl<F: FieldSpec, T, const N: usize, const R: usize> From<[[T; N]; R]> for BfvPlaintext<F, N, R>
where
T: Into<Field<F>> + std::fmt::Debug,
{
fn from(x: [[T; N]; R]) -> Self {
Self {
data: Box::new(x.map(|x| x.map(|x| x.into()))),
}
}
}
impl<F: FieldSpec, const C: usize, const N: usize> NumFieldElements for BfvPlaintext<F, C, N> {
const NUM_NATIVE_FIELD_ELEMENTS: usize = C * N;
}
impl<F: FieldSpec, const C: usize, const N: usize> ToNativeFields for BfvPlaintext<F, C, N> {
fn to_native_fields(&self) -> Vec<BigInt> {
self.data.into_iter().flatten().map(|x| x.val).collect()
}
}
#[derive(Debug, Clone, TypeName)]
/// A [BFV signed integer](crate::types::bfv::Signed) that has been shared to a ZKP program.
pub struct BfvSigned<F: FieldSpec, const C: usize>(
BfvPlaintext<F, C, { <Signed as ShareWithZKP>::DEGREE_BOUND }>,
);
impl<F: FieldSpec, const C: usize> NumFieldElements for BfvSigned<F, C> {
const NUM_NATIVE_FIELD_ELEMENTS: usize = <BfvPlaintext<
F,
C,
{ <Signed as ShareWithZKP>::DEGREE_BOUND },
> as NumFieldElements>::NUM_NATIVE_FIELD_ELEMENTS;
}
impl<F: FieldSpec, const C: usize> ToNativeFields for BfvSigned<F, C> {
fn to_native_fields(&self) -> Vec<BigInt> {
self.0.to_native_fields()
}
}
/// Decode the underlying plaintext polynomial into the field.
pub trait AsFieldElement<F: FieldSpec> {
/**
* Return a structure scaled by `x`.
*/
fn into_field_elem(self) -> ProgramNode<Field<F>>;
}
impl<F: FieldSpec, const C: usize> AsFieldElement<F> for ProgramNode<BfvSigned<F, C>> {
fn into_field_elem(self) -> ProgramNode<Field<F>> {
let (plain_modulus, two, mut coeffs) = with_zkp_ctx(|ctx| {
let plain_modulus = ctx.add_constant(&BigInt::from_u32(4096));
let two = ctx.add_constant(&BigInt::from_u32(2));
// Get coeffs via 2s complement construction
let coeffs = self
.ids
.chunks(C)
.map(|xs| {
let mut c = ctx.add_constant(&BigInt::ZERO);
for (i, x) in xs.iter().enumerate() {
let pow = ctx.add_constant(&(BigInt::ONE << i));
let mul = ctx.add_multiplication(pow, *x);
if i == C - 1 {
c = ctx.add_subtraction(c, mul);
} else {
c = ctx.add_addition(c, mul);
}
}
c
})
.collect::<Vec<_>>();
(plain_modulus, two, coeffs)
});
// Translate coefficients into field modulus
let cutoff_divider = SignedModulus::new(F::FIELD_MODULUS, 1);
let divider = SignedModulus::new(F::FIELD_MODULUS, 63);
// TODO make sure this is correct, might need +1 somewhere to match the signed encoding
let neg_cutoff = invoke_gadget(cutoff_divider, &[plain_modulus, two])[0];
for c in coeffs.iter_mut() {
let is_negative = invoke_gadget(divider, &[*c, neg_cutoff])[0];
*c = with_zkp_ctx(|ctx| {
let shift = ctx.add_multiplication(plain_modulus, is_negative);
ctx.add_subtraction(*c, shift)
});
}
ProgramNode::new(&[with_zkp_ctx(|ctx| {
// Get signed value by decoding according to Signed implementation.
let mut x = ctx.add_constant(&BigInt::ZERO);
for (i, c) in coeffs.iter().enumerate() {
let pow = ctx.add_constant(&(BigInt::ONE << i));
let mul = ctx.add_multiplication(pow, *c);
x = ctx.add_addition(x, mul);
}
x
})])
}
}
#[cfg(test)]
mod tests {
use super::*;
use sunscreen_zkp_backend::bulletproofs::BulletproofsBackend;
use sunscreen_zkp_backend::FieldSpec;
use crate::types::zkp::{BulletproofsField, Field};
use crate::zkp_program;
use crate::{self as sunscreen, ZkpProgramFnExt};
#[zkp_program]
fn is_eq<F: FieldSpec>(#[private] x: BfvSigned<F, 13>, #[public] y: Field<F>) {
x.into_field_elem().constrain_eq(y);
}
// TODO accept plaintext as zkp program arg, fold into BfvSigned, and test odd pt moduli.
#[test]
fn can_decode_signed_properly() {
let is_eq_zkp = is_eq.compile::<BulletproofsBackend>().unwrap();
let runtime = is_eq.runtime::<BulletproofsBackend>().unwrap();
let plain_modulus = 4096_u64;
const LOG_P: usize = 13; // i.e. `plain_modulus.ilog2() + 1`
for val in [3i64, -3] {
// Simulate the polynomial signed encoding
let mut signed_encoding = [0; 128];
let abs_val = val.unsigned_abs();
for (i, c) in signed_encoding.iter_mut().take(64).enumerate() {
let bit = (abs_val & 0x1 << i) >> i;
*c = if val.is_negative() {
bit * (plain_modulus - bit)
} else {
bit
};
}
// Now further break down each coeff into 2s complement
// Note that these numbers will virtually always be positive in the Zq context, for all but
// pathological plaintext moduli.
let coeffs = signed_encoding.map(|c| {
let mut bits = [0; LOG_P];
for (i, b) in bits.iter_mut().enumerate() {
let bit = (c & (0x1 << i)) >> i;
*b = bit;
}
bits
});
let encoded = BfvSigned(BfvPlaintext {
data: Box::new(coeffs.map(|c| c.map(BulletproofsField::from))),
});
let proof = runtime
.proof_builder(&is_eq_zkp)
.private_input(encoded)
.public_input(BulletproofsField::from(val))
.prove()
.unwrap();
runtime
.verification_builder(&is_eq_zkp)
.proof(&proof)
.public_input(BulletproofsField::from(val))
.verify()
.unwrap();
}
}
}

View File

@@ -6,6 +6,7 @@ use crate::zkp::{invoke_gadget, with_zkp_ctx, ZkpContextOps};
use super::ToUInt;
#[derive(Clone, Copy)]
pub struct SignedModulus {
field_modulus: BigInt,
max_remainder_bits: usize,

View File

@@ -1,12 +1,13 @@
#[cfg(feature = "linkedproofs")]
mod bfv_plaintext;
mod field;
mod gadgets;
mod program_node;
mod rns_polynomial;
#[cfg(feature = "linkedproofs")]
pub use bfv_plaintext::*;
pub use field::*;
// N.B. `NodeIndex` is actually common to both FHE and ZKP, but it's really only leaked as an
// implementation detail on the ZKP side (via gadgets). So I think it makes sense to export under
// sunscreen::types::zkp.
pub use petgraph::stable_graph::NodeIndex;
pub use program_node::*;
pub use rns_polynomial::*;

View File

@@ -6,7 +6,7 @@ mod linked_tests {
use logproof::rings::ZqSeal128_1024;
use logproof::test::seal_bfv_encryption_linear_relation;
use sunscreen::types::bfv::{Signed, Unsigned, Unsigned64};
use sunscreen::types::zkp::{BulletproofsField, Mod};
use sunscreen::types::zkp::{AsFieldElement, BfvSigned, BulletproofsField, Mod};
use sunscreen::{
types::zkp::{ConstrainCmp, Field, FieldSpec, ProgramNode},
zkp_program, zkp_var, Compiler,
@@ -26,61 +26,21 @@ mod linked_tests {
};
}
/// Convert a twos complement represented signed integer into a field element.
fn from_twos_complement_field_element<F: FieldSpec>(
x: &[ProgramNode<Field<F>>],
) -> ProgramNode<Field<F>> {
let mut x_recon = zkp_var!(0);
let n = x.len();
for (i, x_i) in x.iter().enumerate().take(n - 1) {
x_recon = x_recon + (zkp_var!(sunscreen_zkp_backend::BigInt::ONE << i) * (*x_i));
}
x_recon = x_recon - zkp_var!(sunscreen_zkp_backend::BigInt::ONE << (n - 1)) * x[n - 1];
x_recon
}
// Convert a coeff into native big int
// TODO package this up for shared types
fn from_signed_encoding<F: FieldSpec>(x: &[ProgramNode<Field<F>>]) -> ProgramNode<Field<F>> {
let mut x_recon = zkp_var!(0);
let n = x.len();
let plain_modulus = zkp_var!(4096);
for (i, x_i) in x.iter().enumerate() {
// TODO this is not correct. 4095 (equiv to -1 in plaintext context) just reduces to
// native field element 4095. We need the _reverse_ of the signed_reduce function.
// Hence, this is currently broken for negative numbers.
let c = Field::signed_reduce(*x_i, plain_modulus, 15);
x_recon = x_recon + (zkp_var!(sunscreen_zkp_backend::BigInt::ONE << i) * c);
}
x_recon
}
#[zkp_program]
fn valid_transaction<F: FieldSpec>(
#[private] x: [Field<F>; 832],
// #[private] x: [Field<F>; 1664],
#[private] tx: BfvSigned<F, 13>,
#[public] balance: Field<F>,
) {
let lower_bound = zkp_var!(0);
// Reconstruct x from the bag of bits
let plain_modulus_log_2 = 4096u64.ilog2() as usize + 1;
let coeffs = x
.chunks(plain_modulus_log_2)
.map(|c| from_twos_complement_field_element(c))
.collect::<Vec<_>>();
let x_recon = from_signed_encoding(&coeffs);
// Reconstruct tx
let tx_recon = tx.into_field_elem();
// Constraint that x is less than or equal to balance
balance.constrain_ge_bounded(x_recon, 64);
balance.constrain_ge_bounded(tx_recon, 64);
// Constraint that x is greater than or equal to zero
lower_bound.constrain_le_bounded(x_recon, 64);
lower_bound.constrain_le_bounded(tx_recon, 64);
}
#[test]
@@ -145,24 +105,15 @@ mod linked_tests {
}
#[zkp_program]
fn is_eq<F: FieldSpec>(#[private] x: [Field<F>; 832], #[public] y: Field<F>) {
// Reconstruct x from the bag of bits
let plain_modulus_log_2 = 4096u64.ilog2() as usize + 1;
let coeffs = x
.chunks(plain_modulus_log_2)
.map(|c| from_twos_complement_field_element(c))
.collect::<Vec<_>>();
let x_recon = from_signed_encoding(&coeffs);
// Constraint that x is less than or equal to balance
x_recon.constrain_eq(y);
fn is_eq<F: FieldSpec>(#[private] x: BfvSigned<F, 13>, #[public] y: Field<F>) {
x.into_field_elem().constrain_eq(y);
}
#[test]
fn test_is_eq() {
let rt = FheZkpRuntime::new(&SMALL_PARAMS, &BulletproofsBackend::new()).unwrap();
let (public_key, _secret_key) = rt.generate_keys().unwrap();
let is_eq_zkp = valid_transaction.compile::<BulletproofsBackend>().unwrap();
let is_eq_zkp = is_eq.compile::<BulletproofsBackend>().unwrap();
for val in [3, -3] {
let mut proof_builder = LogProofBuilder::new(&rt);