From 4229f7770ff8c79aefc72c0a4221932cdef24cd4 Mon Sep 17 00:00:00 2001 From: jowparks Date: Thu, 24 Oct 2024 13:01:02 -0700 Subject: [PATCH] Circuit struct serialization (#5565) * move proof generation key extension to ironfish-zkp * circuit struct serialization * fix review comments, cleanup --- ironfish-zkp/src/circuits/mint_asset.rs | 81 ++++++ ironfish-zkp/src/circuits/output.rs | 185 +++++++++++++- ironfish-zkp/src/circuits/spend.rs | 230 +++++++++++++++++- ironfish-zkp/src/circuits/util.rs | 33 +++ .../src/primitives/proof_generation_key.rs | 7 + .../src/primitives/value_commitment.rs | 59 ++++- 6 files changed, 591 insertions(+), 4 deletions(-) diff --git a/ironfish-zkp/src/circuits/mint_asset.rs b/ironfish-zkp/src/circuits/mint_asset.rs index f9f9de6fc3..b32a0ef276 100644 --- a/ironfish-zkp/src/circuits/mint_asset.rs +++ b/ironfish-zkp/src/circuits/mint_asset.rs @@ -3,6 +3,7 @@ use bellperson::{ Circuit, }; use ff::PrimeField; +use std::io::{Read, Write}; use zcash_proofs::{ circuit::ecc, constants::{PROOF_GENERATION_KEY_GENERATOR, SPENDING_KEY_GENERATOR}, @@ -12,6 +13,9 @@ use crate::{ constants::{proof::PUBLIC_KEY_GENERATOR, CRH_IVK_PERSONALIZATION}, ProofGenerationKey, }; +use byteorder::{ReadBytesExt, WriteBytesExt}; + +use super::util::FromBytes; pub struct MintAsset { /// Key required to construct proofs for a particular spending key @@ -22,6 +26,39 @@ pub struct MintAsset { pub public_key_randomness: Option, } +impl MintAsset { + pub fn write(&self, mut writer: W) -> std::io::Result<()> { + if let Some(ref proof_generation_key) = self.proof_generation_key { + writer.write_u8(1)?; + writer.write_all(proof_generation_key.to_bytes().as_ref())?; + } else { + writer.write_u8(0)?; + } + if let Some(ref public_key_randomness) = self.public_key_randomness { + writer.write_u8(1)?; + writer.write_all(public_key_randomness.to_bytes().as_ref())?; + } else { + writer.write_u8(0)?; + } + Ok(()) + } + + pub fn read(mut reader: R) -> std::io::Result { + let mut proof_generation_key = None; + if reader.read_u8()? == 1 { + proof_generation_key = Some(ProofGenerationKey::read(&mut reader)?); + } + let mut public_key_randomness = None; + if reader.read_u8()? == 1 { + public_key_randomness = Some(jubjub::Fr::read(&mut reader)?); + } + Ok(MintAsset { + proof_generation_key, + public_key_randomness, + }) + } +} + impl Circuit for MintAsset { fn synthesize>( self, @@ -181,4 +218,48 @@ mod test { // Sanity check assert!(cs.verify(&public_inputs)); } + + #[test] + fn test_mint_asset_read_write() { + // Seed a fixed RNG for determinism in the test + let mut rng = StdRng::seed_from_u64(0); + + // Create a MintAsset instance with random data + let proof_generation_key = ProofGenerationKey::new( + jubjub::SubgroupPoint::random(&mut rng), + jubjub::Fr::random(&mut rng), + ); + let public_key_randomness = jubjub::Fr::random(&mut rng); + + let mint_asset = MintAsset { + proof_generation_key: Some(proof_generation_key.clone()), + public_key_randomness: Some(public_key_randomness), + }; + + let mut buffer = vec![]; + mint_asset.write(&mut buffer).unwrap(); + + let deserialized_mint_asset = MintAsset::read(&buffer[..]).unwrap(); + + assert_eq!( + mint_asset.proof_generation_key.clone().unwrap().ak, + deserialized_mint_asset + .proof_generation_key + .clone() + .unwrap() + .ak + ); + assert_eq!( + mint_asset.proof_generation_key.clone().unwrap().nsk, + deserialized_mint_asset + .proof_generation_key + .clone() + .unwrap() + .nsk + ); + assert_eq!( + mint_asset.public_key_randomness.unwrap(), + deserialized_mint_asset.public_key_randomness.unwrap() + ); + } } diff --git a/ironfish-zkp/src/circuits/output.rs b/ironfish-zkp/src/circuits/output.rs index d7bb97760a..25f7c3fdc5 100644 --- a/ironfish-zkp/src/circuits/output.rs +++ b/ironfish-zkp/src/circuits/output.rs @@ -1,8 +1,11 @@ +use std::io::{Read, Write}; + +use byteorder::{ReadBytesExt, WriteBytesExt}; use ff::PrimeField; use bellperson::{gadgets::blake2s, Circuit, ConstraintSystem, SynthesisError}; -use group::Curve; +use group::{Curve, GroupEncoding}; use jubjub::SubgroupPoint; use zcash_proofs::{ @@ -20,7 +23,7 @@ use crate::{ ProofGenerationKey, }; -use super::util::expose_value_commitment; +use super::util::{expose_value_commitment, FromBytes}; use bellperson::gadgets::boolean; /// This is a circuit instance inspired from ZCash's `Output` circuit in the Sapling protocol @@ -49,6 +52,87 @@ pub struct Output { pub ar: Option, } +impl Output { + pub fn write(&self, mut writer: W) -> std::io::Result<()> { + if let Some(ref value_commitment) = self.value_commitment { + writer.write_u8(1)?; + writer.write_all(value_commitment.to_bytes().as_ref())?; + } else { + writer.write_u8(0)?; + } + writer.write_all(&self.asset_id)?; + if let Some(ref payment_address) = self.payment_address { + writer.write_u8(1)?; + writer.write_all(payment_address.to_bytes().as_ref())?; + } else { + writer.write_u8(0)?; + } + if let Some(ref commitment_randomness) = self.commitment_randomness { + writer.write_u8(1)?; + writer.write_all(commitment_randomness.to_bytes().as_ref())?; + } else { + writer.write_u8(0)?; + } + if let Some(ref esk) = self.esk { + writer.write_u8(1)?; + writer.write_all(esk.to_bytes().as_ref())?; + } else { + writer.write_u8(0)?; + } + if let Some(ref proof_generation_key) = self.proof_generation_key { + writer.write_u8(1)?; + writer.write_all(proof_generation_key.to_bytes().as_ref())?; + } else { + writer.write_u8(0)?; + } + if let Some(ref ar) = self.ar { + writer.write_u8(1)?; + writer.write_all(ar.to_bytes().as_ref())?; + } else { + writer.write_u8(0)?; + } + Ok(()) + } + + pub fn read(mut reader: R) -> std::io::Result { + let mut value_commitment = None; + if reader.read_u8()? == 1 { + value_commitment = Some(ValueCommitment::read(&mut reader)?); + } + let mut asset_id = [0u8; ASSET_ID_LENGTH]; + reader.read_exact(&mut asset_id)?; + let mut payment_address = None; + if reader.read_u8()? == 1 { + payment_address = Some(SubgroupPoint::read(&mut reader)?); + } + let mut commitment_randomness = None; + if reader.read_u8()? == 1 { + commitment_randomness = Some(jubjub::Fr::read(&mut reader)?); + } + let mut esk = None; + if reader.read_u8()? == 1 { + esk = Some(jubjub::Fr::read(&mut reader)?); + } + let mut proof_generation_key = None; + if reader.read_u8()? == 1 { + proof_generation_key = Some(ProofGenerationKey::read(&mut reader)?); + } + let mut ar = None; + if reader.read_u8()? == 1 { + ar = Some(jubjub::Fr::read(&mut reader)?); + } + Ok(Output { + value_commitment, + asset_id, + payment_address, + commitment_randomness, + esk, + proof_generation_key, + ar, + }) + } +} + impl Circuit for Output { fn synthesize>( self, @@ -318,6 +402,10 @@ mod test { ar: Some(ar), }; + let mut writer = vec![]; + instance.write(&mut writer).unwrap(); + let _output = Output::read(&writer[..]).unwrap(); + instance.synthesize(&mut cs).unwrap(); assert!(cs.is_satisfied()); @@ -366,4 +454,97 @@ mod test { } } } + + #[test] + fn test_output_read_write() { + let mut rng = StdRng::seed_from_u64(0); + + for _ in 0..5 { + let mut asset_id = [0u8; 32]; + let asset_generator = loop { + rng.fill(&mut asset_id[..]); + + if let Some(point) = asset_hash_to_point(&asset_id) { + break point; + } + }; + + let value_commitment_randomness = jubjub::Fr::random(&mut rng); + let note_commitment_randomness = jubjub::Fr::random(&mut rng); + let value_commitment = ValueCommitment { + value: rng.next_u64(), + randomness: value_commitment_randomness, + asset_generator, + }; + + let nsk = jubjub::Fr::random(&mut rng); + let ak = jubjub::SubgroupPoint::random(&mut rng); + let esk = jubjub::Fr::random(&mut rng); + let ar = jubjub::Fr::random(&mut rng); + + let proof_generation_key = ProofGenerationKey::new(ak, nsk); + + let viewing_key = proof_generation_key.to_viewing_key(); + + let payment_address = *PUBLIC_KEY_GENERATOR * viewing_key.ivk().0; + + let output = Output { + value_commitment: Some(value_commitment.clone()), + payment_address: Some(payment_address), + commitment_randomness: Some(note_commitment_randomness), + esk: Some(esk), + asset_id, + proof_generation_key: Some(proof_generation_key.clone()), + ar: Some(ar), + }; + + // Ser/de + let mut writer = vec![]; + output.write(&mut writer).unwrap(); + let deserialized_output: Output = Output::read(&writer[..]).unwrap(); + assert_eq!( + output.value_commitment.clone().unwrap().value, + deserialized_output.value_commitment.clone().unwrap().value + ); + assert_eq!( + output.value_commitment.clone().unwrap().randomness, + deserialized_output + .value_commitment + .clone() + .unwrap() + .randomness + ); + assert_eq!( + output.value_commitment.clone().unwrap().asset_generator, + deserialized_output + .value_commitment + .clone() + .unwrap() + .asset_generator + ); + + assert_eq!(output.asset_id, deserialized_output.asset_id); + assert_eq!(output.payment_address, deserialized_output.payment_address); + assert_eq!( + output.commitment_randomness, + deserialized_output.commitment_randomness + ); + assert_eq!(output.esk, deserialized_output.esk); + + assert_eq!( + output.proof_generation_key.clone().unwrap().ak, + deserialized_output.proof_generation_key.clone().unwrap().ak + ); + assert_eq!( + output.proof_generation_key.clone().unwrap().nsk, + deserialized_output + .proof_generation_key + .clone() + .unwrap() + .nsk + ); + + assert_eq!(output.ar, deserialized_output.ar); + } + } } diff --git a/ironfish-zkp/src/circuits/spend.rs b/ironfish-zkp/src/circuits/spend.rs index 82065ecec6..e339f1999f 100644 --- a/ironfish-zkp/src/circuits/spend.rs +++ b/ironfish-zkp/src/circuits/spend.rs @@ -1,12 +1,16 @@ +use std::io::{Read, Write}; + use bellperson::{Circuit, ConstraintSystem, SynthesisError}; +use byteorder::{LittleEndian, ReadBytesExt, WriteBytesExt}; use ff::{Field, PrimeField}; +use group::GroupEncoding; use jubjub::SubgroupPoint; use crate::constants::{CRH_IVK_PERSONALIZATION, PRF_NF_PERSONALIZATION}; use crate::ProofGenerationKey; use crate::{constants::proof::PUBLIC_KEY_GENERATOR, primitives::ValueCommitment}; -use super::util::expose_value_commitment; +use super::util::{expose_value_commitment, FromBytes}; use bellperson::gadgets::blake2s; use bellperson::gadgets::boolean; use bellperson::gadgets::multipack; @@ -50,6 +54,117 @@ pub struct Spend { pub sender_address: Option, } +impl Spend { + pub fn write(&self, mut writer: W) -> std::io::Result<()> { + if let Some(ref value_commitment) = self.value_commitment { + writer.write_u8(1)?; + writer.write_all(&value_commitment.to_bytes())?; + } else { + writer.write_u8(0)?; + } + if let Some(ref proof_generation_key) = self.proof_generation_key { + writer.write_u8(1)?; + writer.write_all(proof_generation_key.to_bytes().as_ref())?; + } else { + writer.write_u8(0)?; + } + if let Some(ref payment_address) = self.payment_address { + writer.write_u8(1)?; + writer.write_all(payment_address.to_bytes().as_ref())?; + } else { + writer.write_u8(0)?; + } + if let Some(ref commitment_randomness) = self.commitment_randomness { + writer.write_u8(1)?; + writer.write_all(commitment_randomness.to_bytes().as_ref())?; + } else { + writer.write_u8(0)?; + } + if let Some(ref ar) = self.ar { + writer.write_u8(1)?; + writer.write_all(ar.to_bytes().as_ref())?; + } else { + writer.write_u8(0)?; + } + writer.write_all((self.auth_path.len() as u64).to_le_bytes().as_ref())?; + for auth_path in &self.auth_path { + match auth_path { + Some((val, flag)) => { + writer.write_u8(1)?; + writer.write_all(&val.to_bytes_le())?; + writer.write_u8(*flag as u8)?; + } + None => writer.write_u8(0)?, + } + } + if let Some(anchor) = &self.anchor { + writer.write_u8(1)?; + writer.write_all(anchor.to_bytes_le().as_ref())?; + } else { + writer.write_u8(0)?; + } + if let Some(ref sender_address) = self.sender_address { + writer.write_u8(1)?; + writer.write_all(sender_address.to_bytes().as_ref())?; + } else { + writer.write_u8(0)?; + } + Ok(()) + } + + pub fn read(mut reader: R) -> std::io::Result { + let mut value_commitment = None; + if reader.read_u8()? == 1 { + value_commitment = Some(ValueCommitment::read(&mut reader)?); + } + let mut proof_generation_key = None; + if reader.read_u8()? == 1 { + proof_generation_key = Some(ProofGenerationKey::read(&mut reader)?); + } + let mut payment_address = None; + if reader.read_u8()? == 1 { + payment_address = Some(SubgroupPoint::read(&mut reader)?); + } + let mut commitment_randomness = None; + if reader.read_u8()? == 1 { + commitment_randomness = Some(jubjub::Fr::read(&mut reader)?); + } + let mut ar = None; + if reader.read_u8()? == 1 { + ar = Some(jubjub::Fr::read(&mut reader)?); + } + let len = reader.read_u64::().unwrap(); + let mut auth_path = vec![]; + for _ in 0..len { + if reader.read_u8()? == 1 { + let val = blstrs::Scalar::read(&mut reader)?; + let flag = reader.read_u8()? == 1; + auth_path.push(Some((val, flag))); + } else { + auth_path.push(None); + } + } + let mut anchor = None; + if reader.read_u8()? == 1 { + anchor = Some(blstrs::Scalar::read(&mut reader)?); + } + let mut sender_address = None; + if reader.read_u8()? == 1 { + sender_address = Some(SubgroupPoint::read(&mut reader)?); + } + Ok(Spend { + value_commitment, + proof_generation_key, + payment_address, + commitment_randomness, + ar, + auth_path, + anchor, + sender_address, + }) + } +} + impl Circuit for Spend { fn synthesize>( self, @@ -655,4 +770,117 @@ mod test { } } } + + #[test] + fn test_spend_read_write() { + let mut rng = StdRng::seed_from_u64(0); + + let value_commitment = ValueCommitment { + value: rng.next_u64(), + randomness: jubjub::Fr::random(&mut rng), + asset_generator: (*VALUE_COMMITMENT_VALUE_GENERATOR).into(), + }; + + let proof_generation_key = ProofGenerationKey::new( + jubjub::SubgroupPoint::random(&mut rng), + jubjub::Fr::random(&mut rng), + ); + + let viewing_key = proof_generation_key.to_viewing_key(); + + let payment_address = *PUBLIC_KEY_GENERATOR * viewing_key.ivk().0; + + let commitment_randomness = jubjub::Fr::random(&mut rng); + let auth_path = vec![Some((blstrs::Scalar::random(&mut rng), rng.next_u32() % 2 != 0)); 32]; + let ar = jubjub::Fr::random(&mut rng); + + let sender_address = jubjub::SubgroupPoint::random(&mut rng); + + let anchor = blstrs::Scalar::random(&mut rng); + + let spend = Spend { + value_commitment: Some(value_commitment.clone()), + proof_generation_key: Some(proof_generation_key.clone()), + payment_address: Some(payment_address), + commitment_randomness: Some(commitment_randomness), + ar: Some(ar), + auth_path: auth_path.clone(), + anchor: Some(anchor), + sender_address: Some(sender_address), + }; + + let mut buffer = vec![]; + spend.write(&mut buffer).unwrap(); + + let deserialized_spend = Spend::read(&buffer[..]).unwrap(); + assert_eq!( + spend.value_commitment.clone().unwrap().value, + deserialized_spend.value_commitment.clone().unwrap().value + ); + assert_eq!( + spend.value_commitment.clone().unwrap().randomness, + deserialized_spend + .value_commitment + .clone() + .unwrap() + .randomness + ); + assert_eq!( + spend + .value_commitment + .clone() + .unwrap() + .asset_generator + .to_bytes(), + deserialized_spend + .value_commitment + .clone() + .unwrap() + .asset_generator + .to_bytes() + ); + + assert_eq!( + spend.proof_generation_key.clone().unwrap().ak.to_bytes(), + deserialized_spend + .proof_generation_key + .clone() + .unwrap() + .ak + .to_bytes() + ); + assert_eq!( + spend.proof_generation_key.clone().unwrap().nsk, + deserialized_spend.proof_generation_key.clone().unwrap().nsk + ); + + assert_eq!( + spend.payment_address.unwrap().to_bytes(), + deserialized_spend.payment_address.unwrap().to_bytes() + ); + + assert_eq!( + spend.commitment_randomness, + deserialized_spend.commitment_randomness + ); + + assert_eq!(spend.ar, deserialized_spend.ar); + + assert_eq!(spend.auth_path.len(), deserialized_spend.auth_path.len()); + for (ap1, ap2) in spend + .auth_path + .iter() + .zip(deserialized_spend.auth_path.iter()) + { + assert_eq!((*ap1).unwrap().0, (*ap2).unwrap().0); + assert_eq!((*ap1).unwrap().1, (*ap2).unwrap().1); + } + + assert_eq!(spend.anchor, deserialized_spend.anchor); + + assert_eq!( + spend.sender_address.unwrap().to_bytes(), + deserialized_spend.sender_address.unwrap().to_bytes() + ); + } } diff --git a/ironfish-zkp/src/circuits/util.rs b/ironfish-zkp/src/circuits/util.rs index 29d9be4f0a..7cba93e53d 100644 --- a/ironfish-zkp/src/circuits/util.rs +++ b/ironfish-zkp/src/circuits/util.rs @@ -6,6 +6,7 @@ use bellperson::{ ConstraintSystem, SynthesisError, }; use ff::PrimeField; +use group::GroupEncoding; use zcash_proofs::{ circuit::ecc::{self, EdwardsPoint}, constants::VALUE_COMMITMENT_RANDOMNESS_GENERATOR, @@ -143,3 +144,35 @@ pub fn assert_valid_asset_generator(reader: R) -> Result; +} + +impl FromBytes for jubjub::SubgroupPoint { + fn read(mut reader: R) -> Result { + let mut bytes = [0u8; 32]; + reader.read_exact(&mut bytes)?; + Option::from(jubjub::SubgroupPoint::from_bytes(&bytes)) + .ok_or_else(|| std::io::Error::new(std::io::ErrorKind::InvalidData, "Invalid point")) + } +} + +impl FromBytes for jubjub::Fr { + fn read(mut reader: R) -> Result { + let mut bytes = [0u8; 32]; + reader.read_exact(&mut bytes)?; + Option::from(jubjub::Fr::from_bytes(&bytes)).ok_or_else(|| { + std::io::Error::new(std::io::ErrorKind::InvalidData, "Invalid field element") + }) + } +} + +impl FromBytes for blstrs::Scalar { + fn read(mut reader: R) -> Result { + let mut bytes = [0u8; 32]; + reader.read_exact(&mut bytes)?; + Option::from(blstrs::Scalar::from_bytes_le(&bytes)) + .ok_or_else(|| std::io::Error::new(std::io::ErrorKind::InvalidData, "Invalid scalar")) + } +} diff --git a/ironfish-zkp/src/primitives/proof_generation_key.rs b/ironfish-zkp/src/primitives/proof_generation_key.rs index 6df8350751..d00c3ee1b2 100644 --- a/ironfish-zkp/src/primitives/proof_generation_key.rs +++ b/ironfish-zkp/src/primitives/proof_generation_key.rs @@ -68,6 +68,13 @@ impl ProofGenerationKey { Ok(ProofGenerationKey(ZcashProofGenerationKey { ak, nsk })) } + pub fn read(mut reader: R) -> Result { + let mut proof_generation_key_bytes: [u8; 64] = [0; 64]; + reader.read_exact(&mut proof_generation_key_bytes)?; + ProofGenerationKey::from_bytes(&proof_generation_key_bytes) + .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e)) + } + pub fn hex_key(&self) -> String { bytes_to_hex(&self.to_bytes()) } diff --git a/ironfish-zkp/src/primitives/value_commitment.rs b/ironfish-zkp/src/primitives/value_commitment.rs index cc38db855c..5937a461e6 100644 --- a/ironfish-zkp/src/primitives/value_commitment.rs +++ b/ironfish-zkp/src/primitives/value_commitment.rs @@ -1,5 +1,8 @@ +use byteorder::{LittleEndian, ReadBytesExt}; use ff::Field; -use group::cofactor::CofactorGroup; +use group::{cofactor::CofactorGroup, GroupEncoding}; + +use jubjub::ExtendedPoint; use rand::thread_rng; use crate::constants::VALUE_COMMITMENT_RANDOMNESS_GENERATOR; @@ -26,6 +29,34 @@ impl ValueCommitment { (self.asset_generator.clear_cofactor() * jubjub::Fr::from(self.value)) + (*VALUE_COMMITMENT_RANDOMNESS_GENERATOR * self.randomness) } + + pub fn to_bytes(&self) -> [u8; 72] { + let mut res = [0u8; 72]; + res[0..8].copy_from_slice(&self.value.to_le_bytes()); + res[8..40].copy_from_slice(&self.randomness.to_bytes()); + res[40..72].copy_from_slice(&self.asset_generator.to_bytes()); + res + } + + pub fn write(&self, mut writer: W) -> Result<(), std::io::Error> { + writer.write_all(&self.to_bytes())?; + Ok(()) + } + + pub fn read(mut reader: R) -> Result { + let value = reader.read_u64::()?; + let mut randomness_bytes = [0u8; 32]; + reader.read_exact(&mut randomness_bytes)?; + let randomness = jubjub::Fr::from_bytes(&randomness_bytes).unwrap(); + let mut asset_generator = [0u8; 32]; + reader.read_exact(&mut asset_generator)?; + let asset_generator = ExtendedPoint::from_bytes(&asset_generator).unwrap(); + Ok(Self { + value, + randomness, + asset_generator, + }) + } } #[cfg(test)] @@ -187,4 +218,30 @@ mod test { value_commitment_two.randomness ); } + + #[test] + fn test_value_commitment_read_write() { + // Seed a fixed rng for determinism in the test + let mut rng = StdRng::seed_from_u64(0); + + let value_commitment = ValueCommitment { + value: 5, + randomness: jubjub::Fr::random(&mut rng), + asset_generator: jubjub::ExtendedPoint::random(&mut rng), + }; + + // Serialize to bytes + let serialized = value_commitment.to_bytes(); + + // Deserialize from bytes + let deserialized = ValueCommitment::read(&serialized[..]).unwrap(); + + // Assert equality + assert_eq!(value_commitment.value, deserialized.value); + assert_eq!(value_commitment.randomness, deserialized.randomness); + assert_eq!( + value_commitment.asset_generator, + deserialized.asset_generator + ); + } }