From 7b6b45f1ec26828cc73917fe07c2f33b754dec83 Mon Sep 17 00:00:00 2001 From: Thomas Zamojski Date: Tue, 6 May 2025 18:04:20 +0200 Subject: [PATCH] ADD: uncompress commitments in FFI. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Commitments has 2 representations in bytes: uncompressed and compressed. They are both used in different parts of Verkle Tries. So far, we could go from uncompressed serialized to compressed serialized form via the compress method, but not the other way around. This commitment fills the gap with the uncompress method, and the vectorized version uncompressMany. Note that uncompressMany makes use of Montgoméry's inversion trick much like hashMany. Signed-off-by: Thomas Zamojski --- banderwagon/src/element.rs | 82 ++++++++++++++++++ .../verkle/cryptography/LibIpaMultipoint.java | 21 +++++ .../cryptography/LibIpaMultipointTest.java | 39 +++++++++ bindings/java/rust_code/src/lib.rs | 86 ++++++++++++++++++- bindings/java/rust_code/src/parsers.rs | 31 ++++++- .../verkle_cryptography_LibIpaMultipoint.h | 16 ++++ ffi_interface/src/lib.rs | 6 +- ffi_interface/src/serialization.rs | 14 +++ 8 files changed, 292 insertions(+), 3 deletions(-) diff --git a/banderwagon/src/element.rs b/banderwagon/src/element.rs index 665f4d8..7d17ae2 100644 --- a/banderwagon/src/element.rs +++ b/banderwagon/src/element.rs @@ -103,6 +103,37 @@ impl Element { Some(element) } + pub fn batch_from_bytes(bytes: &[u8]) -> Option> { + let n_elements = 1 + (bytes.len() - 1) / Self::compressed_serialized_size(); + let mut xs = Vec::with_capacity(n_elements); + for chunk in bytes.chunks(Self::compressed_serialized_size()) { + // Switch from big endian to little endian, as arkworks library uses little endian + let mut chunked_bytes = chunk.to_vec(); + chunked_bytes.reverse(); + let x: Fq = Fq::deserialize_compressed(&chunked_bytes[..]).ok()?; + xs.push(x); + } + + // Get points in the group, but possibly not in the prime subgroup + let points = Self::batch_get_point_from_x(&xs); + let mut elements = Vec::with_capacity(n_elements); + + for point in points { + let point = point?; // Short-circuits if point is None + let element = Element(EdwardsProjective::new_unchecked( + point.x, + point.y, + point.x * point.y, + Fq::one(), + )); + if !element.subgroup_check() { + return None; // Short-circuit on failed subgroup check + } + elements.push(element); + } + Some(elements) + } + pub const fn compressed_serialized_size() -> usize { 32 } @@ -125,6 +156,36 @@ impl Element { Some(EdwardsAffine::new_unchecked(x, y)) } + pub fn batch_get_point_from_x(xs: &[Fq]) -> Vec> { + let mut ys_squared = Vec::with_capacity(xs.len()); + + for x in xs { + // y^2 = dx^2 - 1 + ys_squared.push(BandersnatchConfig::COEFF_D * x.square() - Fq::one()); + } + + // y^2 = 1 / (dx^2 - 1) + batch_inversion(&mut ys_squared); + + for (x, y_squared) in xs.iter().zip(ys_squared.iter_mut()) { + *y_squared *= BandersnatchConfig::COEFF_A * x.square() - Fq::one(); + } + + let mut elements = Vec::with_capacity(xs.len()); + for (x, y_squared) in xs.iter().zip(ys_squared.iter()) { + match y_squared.sqrt() { + Some(mut y) => { + if !is_positive(y) { + y = -y; + } + elements.push(Some(EdwardsAffine::new_unchecked(*x, y))); + } + None => elements.push(None), + } + } + elements + } + fn map_to_field(&self) -> Fq { self.0.x / self.0.y } @@ -250,6 +311,27 @@ mod tests { assert_eq!(expected_i, got[i]); } } + + #[test] + fn from_batch_from_bytes() { + let mut points = Vec::new(); + for i in 0..10 { + points.push(Element::prime_subgroup_generator() * Fr::from(i)); + } + let mut compressed = [0u8; 320]; + for (i, point) in points.clone().into_iter().enumerate() { + let bytes = point.to_bytes(); + let start_index = i * 32; + let end_index = start_index + 32; + compressed[start_index..end_index].copy_from_slice(&bytes); + } + + let got = Element::batch_from_bytes(&compressed).unwrap(); + + for i in 0..10 { + assert_eq!(points[i], got[i]); + } + } } #[cfg(test)] diff --git a/bindings/java/java_code/src/main/java/verkle/cryptography/LibIpaMultipoint.java b/bindings/java/java_code/src/main/java/verkle/cryptography/LibIpaMultipoint.java index 5726162..6e2b28b 100644 --- a/bindings/java/java_code/src/main/java/verkle/cryptography/LibIpaMultipoint.java +++ b/bindings/java/java_code/src/main/java/verkle/cryptography/LibIpaMultipoint.java @@ -99,6 +99,27 @@ public static native byte[] updateSparse( */ public static native byte[] compressMany(byte[] commitments); + /** + * Uncompresses a compressed commitment. + * + *

Converts a serialised commitment from compressed to uncompressed form. + * + * @param commitment compressed serialised commitment. + * @return uncompressed serialised commitment. + */ + public static native byte[] uncompress(byte[] commitment); + + /** + * UnCompresses many compressed commitments. + * + *

Converts serialised commitment from compressed to uncompressed form. The vectorised version + * is highly optimised, making use of Montgoméry's batch inversion trick. + * + * @param commitments uncompressed serialised commitments. + * @return compressed serialised commitments. + */ + public static native byte[] uncompressMany(byte[] commitments); + /** * Convert a commitment to its corresponding scalar. * diff --git a/bindings/java/java_code/src/test/java/verkle/cryptography/LibIpaMultipointTest.java b/bindings/java/java_code/src/test/java/verkle/cryptography/LibIpaMultipointTest.java index 0a718e0..5a321ee 100644 --- a/bindings/java/java_code/src/test/java/verkle/cryptography/LibIpaMultipointTest.java +++ b/bindings/java/java_code/src/test/java/verkle/cryptography/LibIpaMultipointTest.java @@ -79,6 +79,45 @@ public void testCallLibraryWithMaxElements() { assertThat(result).isEqualTo(expected); } + @Test + public void testCompressRoundTrip() { + // uncompress(compress(x)) might not give the same representation as the original x. + // So to test, we compress again. We could have hashed it too. + Bytes input = + Bytes.fromHexString( + "0x0128b513cfb016d3d836b5fa4a8a1260395d4ca831d65027aa74b832d92e0d6d9beb8d5e42b78b99e4eb233e7eca6276c6f4bd235b35c091546e2a2119bc1455"); + Bytes expected = Bytes.wrap(LibIpaMultipoint.compress(input.toArray())); + Bytes result = + Bytes.wrap(LibIpaMultipoint.compress(LibIpaMultipoint.uncompress(expected.toArray()))); + assertThat(result).isEqualTo(expected); + } + + @Test + public void testUncompressRoundTrip() { + Bytes32 expected = + Bytes32.fromHexString("0x3337896554fd3960bef9a4d0ff658ee8ee470cf9ca88a3c807cbe128536c5c05"); + Bytes32 result = + Bytes32.wrap(LibIpaMultipoint.compress(LibIpaMultipoint.uncompress(expected.toArray()))); + assertThat(result).isEqualTo(expected); + } + + @Test + public void testCompressManyRoundTrip() { + Bytes first = Bytes.fromHexString( + "0x0c7f8df856f6860c9f2c6cb0f86c10228e511cca1c4a08263189d629940cb189706cbaa63c436901b6355e10a524337d97688fa5b0cf6b2b91b98e654547f728").reverse(); + Bytes input = Bytes.concatenate( + Bytes.fromHexString( + "0x0c7f8df856f6860c9f2c6cb0f86c10228e511cca1c4a08263189d629940cb189706cbaa63c436901b6355e10a524337d97688fa5b0cf6b2b91b98e654547f728").reverse(), + Bytes.fromHexString( + "0x0128b513cfb016d3d836b5fa4a8a1260395d4ca831d65027aa74b832d92e0d6d9beb8d5e42b78b99e4eb233e7eca6276c6f4bd235b35c091546e2a2119bc1455"), + Bytes.fromHexString( + "0x0128b513cfb016d3d836b5fa4a8a1260395d4ca831d65027aa74b832d92e0d6d9beb8d5e42b78b99e4eb233e7eca6276c6f4bd235b35c091546e2a2119bc1455")); + byte[] compressed = LibIpaMultipoint.compressMany(input.toArray()); + Bytes result = Bytes.wrap(LibIpaMultipoint.compressMany(LibIpaMultipoint.uncompressMany(compressed))); + + assertThat(result).isEqualTo(Bytes.wrap(compressed)); + } + @Test public void testUpdateCommitmentSparseIdentityCommitment() { // Numbers and result is taken from: diff --git a/bindings/java/rust_code/src/lib.rs b/bindings/java/rust_code/src/lib.rs index 6efd5d2..7734677 100644 --- a/bindings/java/rust_code/src/lib.rs +++ b/bindings/java/rust_code/src/lib.rs @@ -11,7 +11,10 @@ * SPDX-License-Identifier: Apache-2.0 */ mod parsers; -use parsers::{parse_commitment, parse_commitments, parse_indices, parse_scalars}; +use parsers::{ + parse_commitment, parse_commitments, parse_compressed_commitment, parse_compressed_commitments, + parse_indices, parse_scalars, +}; mod utils; use utils::{ @@ -216,6 +219,47 @@ pub extern "system" fn Java_verkle_cryptography_LibIpaMultipoint_compress<'local result } +#[no_mangle] +pub extern "system" fn Java_verkle_cryptography_LibIpaMultipoint_uncompress<'local>( + mut env: JNIEnv<'local>, + _class: JClass<'_>, + commitment: JByteArray, +) -> JByteArray<'local> { + let compressed = match parse_compressed_commitment(&env, commitment) { + Ok(v) => v, + Err(e) => { + env.throw_new("java/lang/IllegalArgumentException", e) + .expect("Failed to throw exception for commit inputs."); + return JByteArray::default(); + } + }; + let commitment = match ffi_interface::deserialize_commitment(compressed) { + Ok(v) => v, + Err(e) => { + let error_message = format!( + "Invalid compressed commitment input. Couldn't convert to a correct subgroup element: {:?}", + e + ); + env.throw_new("java/lang/IllegalArgumentException", error_message) + .expect("Failed to throw exception for compressed commitment input."); + return JByteArray::default(); + } + }; + let result = match env.byte_array_from_slice(&commitment) { + Ok(s) => s, + Err(e) => { + let error_message = format!( + "Invalid commitment output. Couldn't convert to byte array: {:?}", + e + ); + env.throw_new("java/lang/IllegalArgumentException", &error_message) + .expect("Couldn't convert to byte array"); + return JByteArray::default(); + } + }; + result +} + #[no_mangle] pub extern "system" fn Java_verkle_cryptography_LibIpaMultipoint_compressMany<'local>( mut env: JNIEnv<'local>, @@ -249,6 +293,46 @@ pub extern "system" fn Java_verkle_cryptography_LibIpaMultipoint_compressMany<'l result } +#[no_mangle] +pub extern "system" fn Java_verkle_cryptography_LibIpaMultipoint_uncompressMany<'local>( + mut env: JNIEnv<'local>, + _class: JClass<'_>, + commitments: JByteArray, +) -> JByteArray<'local> { + let compressed = match parse_compressed_commitments(&env, commitments) { + Ok(v) => v, + Err(e) => { + env.throw_new("java/lang/IllegalArgumentException", e) + .expect("Failed to throw exception for commit inputs."); + return JByteArray::default(); + } + }; + let commitments: Vec = match ffi_interface::batch_deserialize_commitment(&compressed) { + Ok(v) => v.into_iter().flat_map(|array| array.into_iter()).collect(), + Err(_e) => { + env.throw_new( + "java/lang/IllegalArgumentException", + "Could not deserialize commitments", + ) + .expect("Failed to throw exception for commit inputs."); + return JByteArray::default(); + } + }; + let result = match env.byte_array_from_slice(&commitments) { + Ok(s) => s, + Err(e) => { + let error_message = format!( + "Invalid commitment output. Couldn't convert to byte array: {:?}", + e + ); + env.throw_new("java/lang/IllegalArgumentException", &error_message) + .expect("Couldn't convert to byte array"); + return JByteArray::default(); + } + }; + result +} + #[no_mangle] pub extern "system" fn Java_verkle_cryptography_LibIpaMultipoint_hash<'local>( mut env: JNIEnv<'local>, diff --git a/bindings/java/rust_code/src/parsers.rs b/bindings/java/rust_code/src/parsers.rs index 3875a34..48c2713 100644 --- a/bindings/java/rust_code/src/parsers.rs +++ b/bindings/java/rust_code/src/parsers.rs @@ -1,4 +1,4 @@ -use ffi_interface::CommitmentBytes; +use ffi_interface::{CommitmentBytes, CommitmentCompressedBytes}; use jni::{objects::JByteArray, JNIEnv}; use std::convert::TryFrom; @@ -47,3 +47,32 @@ pub fn parse_commitments<'a>( Ok(commitment_bytes) } + +pub fn parse_compressed_commitment( + env: &JNIEnv, + commitment: JByteArray<'_>, +) -> Result { + let commitment_bytes = env + .convert_byte_array(commitment) + .map_err(|_| "cannot convert byte vector to vector")?; + + let result: CommitmentCompressedBytes = + CommitmentCompressedBytes::try_from(commitment_bytes) + .map_err(|_| "Wrong commitment size: should be 32 bytes".to_string())?; + Ok(result) +} + +pub fn parse_compressed_commitments<'a>( + env: &JNIEnv<'a>, + commitment: JByteArray<'a>, +) -> Result, String> { + let commitment_bytes = env + .convert_byte_array(commitment) + .map_err(|_| "cannot convert byte vector to vector")?; + + if commitment_bytes.len() % 32 != 0 { + return Err("Wrong input size: should be a multiple of 32 bytes".to_string()); + }; + + Ok(commitment_bytes) +} diff --git a/bindings/java/rust_code/verkle_cryptography_LibIpaMultipoint.h b/bindings/java/rust_code/verkle_cryptography_LibIpaMultipoint.h index 17aa80e..689350e 100644 --- a/bindings/java/rust_code/verkle_cryptography_LibIpaMultipoint.h +++ b/bindings/java/rust_code/verkle_cryptography_LibIpaMultipoint.h @@ -55,6 +55,22 @@ JNIEXPORT jbyteArray JNICALL Java_verkle_cryptography_LibIpaMultipoint_compress JNIEXPORT jbyteArray JNICALL Java_verkle_cryptography_LibIpaMultipoint_compressMany (JNIEnv *, jclass, jbyteArray); +/* + * Class: verkle_cryptography_LibIpaMultipoint + * Method: uncompress + * Signature: ([B)[B + */ +JNIEXPORT jbyteArray JNICALL Java_verkle_cryptography_LibIpaMultipoint_uncompress + (JNIEnv *, jclass, jbyteArray); + +/* + * Class: verkle_cryptography_LibIpaMultipoint + * Method: uncompressMany + * Signature: ([B)[B + */ +JNIEXPORT jbyteArray JNICALL Java_verkle_cryptography_LibIpaMultipoint_uncompressMany + (JNIEnv *, jclass, jbyteArray); + /* * Class: verkle_cryptography_LibIpaMultipoint * Method: hash diff --git a/ffi_interface/src/lib.rs b/ffi_interface/src/lib.rs index fbecabf..edef49c 100644 --- a/ffi_interface/src/lib.rs +++ b/ffi_interface/src/lib.rs @@ -4,7 +4,8 @@ pub mod serialization; // TODO: we ideally don't want to export these. // - deserialize_update_commitment_sparse should not be exported and is an abstraction leak pub use serialization::{ - deserialize_commitment, deserialize_update_commitment_sparse, serialize_commitment, + batch_deserialize_commitment, deserialize_commitment, deserialize_update_commitment_sparse, + serialize_commitment, }; use banderwagon::Element; @@ -60,6 +61,9 @@ impl Context { /// A serialized uncompressed group element pub type CommitmentBytes = [u8; 64]; +/// A serialized compressed group element +pub type CommitmentCompressedBytes = [u8; 32]; + /// A serialized scalar field element pub type ScalarBytes = [u8; 32]; diff --git a/ffi_interface/src/serialization.rs b/ffi_interface/src/serialization.rs index 0087bd8..797183c 100644 --- a/ffi_interface/src/serialization.rs +++ b/ffi_interface/src/serialization.rs @@ -80,6 +80,20 @@ pub fn deserialize_commitment(serialized_commitment: [u8; 32]) -> Result Result, Error> { + let elements = Element::batch_from_bytes(&serialized_commitments).ok_or_else(|| { + Error::CouldNotDeserializeCommitment { + bytes: serialized_commitments.to_vec(), + } + })?; + let commitments = elements + .into_iter() + .map(|element| element.to_bytes_uncompressed()) + .collect::>(); + Ok(commitments) +} #[must_use] pub fn deserialize_proof_query(bytes: &[u8]) -> ProverQuery {