Skip to content

Commit

Permalink
feat: add simplified Transcript trait and implementations (#123)
Browse files Browse the repository at this point in the history
# Rationale for this change

In order to write the verifier in Solidity, we should enable using a
Solidity compatibly public-coin transcript. This PR adds an API for
doing so.

# What changes are included in this PR?

* added simplified `Transcript` trait
* implemented `Transcript` trait for `merlin::Transcript`
* added Solidity compatible `Keccak256Transcript`
* added tests for `Transcript` and implementations

# Are these changes tested?

Yes
  • Loading branch information
JayWhite2357 authored Sep 3, 2024
1 parent c33f4b7 commit b9ef335
Show file tree
Hide file tree
Showing 8 changed files with 307 additions and 0 deletions.
1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ rayon = { version = "1.5" }
serde = { version = "1" }
serde_json = { version = "1" }
thiserror = { version = "1" }
tiny-keccak = { version = "2.0.2", features = [ "keccak" ] }
tokio = { version = "1.39.3" }
tracing = { version = "0.1.36" }
tracing-opentelemetry = { version = "0.22.0" }
Expand Down
1 change: 1 addition & 0 deletions crates/proof-of-sql/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ rayon = { workspace = true }
serde = { workspace = true, features = ["serde_derive"] }
serde_json = { workspace = true }
thiserror = { workspace = true }
tiny-keccak = { workspace = true }
tracing = { workspace = true, features = ["attributes"] }
zerocopy = { workspace = true }

Expand Down
55 changes: 55 additions & 0 deletions crates/proof-of-sql/src/base/proof/keccak256_transcript.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
use super::{transcript_core::TranscriptCore, Transcript};
use core::mem::replace;
use tiny_keccak::{Hasher, Keccak};

#[allow(dead_code)]
/// Public coin transcript that is easily portable to Solidity.
///
/// Leverages the keccak256 hash function, which has the lowest gas costs on Solidity.
///
/// The public coin transcript consists of alternating prover messages and verifier challenges.
/// In order to multiple verifier challenges in a row, an empty prover message must be sent.
/// In order to send multiple prover messages in a row, the verifier challenge can be discarded.
///
/// The challenges/state are computed as follows:
/// ```pseudo-code
/// challenge_(i+1) = keccak256(challenge_i, message_(i+1))
/// ```
pub struct Keccak256Transcript(Keccak);
impl TranscriptCore for Keccak256Transcript {
fn new() -> Self {
Self(Keccak::v256())
}
fn raw_append(&mut self, message: &[u8]) {
self.0.update(message)
}
fn raw_challenge(&mut self) -> [u8; 32] {
let mut result = [0; 32];

// Replace existing Hasher with a new one, and finalize the old Hasher,
// getting a hash/the desired challenge:
replace(self, Transcript::new()).0.finalize(&mut result);

// Add this challenge to the new Hasher for the next round of messages:
self.raw_append(&result);

result
}
}

#[cfg(test)]
mod tests {
use super::{super::transcript_core::test_util::*, Keccak256Transcript};
#[test]
fn we_get_equivalent_challenges_with_equivalent_keccak256_transcripts() {
we_get_equivalent_challenges_with_equivalent_transcripts::<Keccak256Transcript>()
}
#[test]
fn we_get_different_challenges_with_different_keccak256_transcripts() {
we_get_different_challenges_with_different_transcripts::<Keccak256Transcript>()
}
#[test]
fn we_get_different_nontrivial_consecutive_challenges_from_keccak256_transcript() {
we_get_different_nontrivial_consecutive_challenges_from_transcript::<Keccak256Transcript>()
}
}
11 changes: 11 additions & 0 deletions crates/proof-of-sql/src/base/proof/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,14 @@ mod transcript_protocol;
#[cfg(test)]
mod transcript_protocol_test;
pub use transcript_protocol::{MessageLabel, TranscriptProtocol};

mod transcript;
pub use transcript::Transcript;

mod transcript_core;
#[cfg(test)]
mod transcript_core_test;

mod keccak256_transcript;
#[allow(unused_imports)]
pub use keccak256_transcript::Keccak256Transcript;
30 changes: 30 additions & 0 deletions crates/proof-of-sql/src/base/proof/transcript.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
use crate::base::scalar::Scalar;
use zerocopy::{AsBytes, FromBytes};

/// A public-coin transcript.
///
/// This trait contains several method for adding prover messages and computing verifier challenges.
///
/// Implementation note: this is intended to be implemented via [super::transcript_core::TranscriptCore] rather than directly.
#[allow(dead_code)]
pub trait Transcript {
/// Creates a new transcript
fn new() -> Self;
/// Appends the provided messages by appending the reversed raw bytes (i.e. assuming the message is bigendian)
fn extend_as_be<M: FromBytes + AsBytes>(&mut self, messages: impl IntoIterator<Item = M>);
/// Appends the provided messages by appending the raw bytes (i.e. assuming the message is littleendian)
fn extend_as_le_from_refs<'a, M: AsBytes + 'a>(
&mut self,
messages: impl IntoIterator<Item = &'a M>,
);
/// Appends the provided scalars by appending the reversed raw bytes of the canonical value of the scalar (i.e. bigendian form)
fn extend_scalars_as_be<'a, S: Scalar + 'a>(
&mut self,
messages: impl IntoIterator<Item = &'a S>,
);
/// Request a scalar challenge. Assumes that the reversed raw bytes are the canonical value of the scalar (i.e. bigendian form)
fn scalar_challenge_as_be<S: Scalar>(&mut self) -> S;
#[cfg(test)]
/// Request a challenge. Returns the raw, unreversed, bytes. (i.e. littleendian form)
fn challenge_as_le(&mut self) -> [u8; 32];
}
106 changes: 106 additions & 0 deletions crates/proof-of-sql/src/base/proof/transcript_core.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
use super::Transcript;
use crate::base::{ref_into::RefInto, scalar::Scalar};
use zerocopy::{AsBytes, FromBytes};

/// A trait used to facilitate implementation of [Transcript](super::Transcript).
///
/// There is a blanket `impl<T: TranscriptCore> Transcript for T` implementation.
pub(super) trait TranscriptCore {
/// Creates a new transcript.
fn new() -> Self;
/// Appends a slice of bytes (as a message) to the transcript.
fn raw_append(&mut self, message: &[u8]);
/// Pulls a challenge from the transcript.
fn raw_challenge(&mut self) -> [u8; 32];
}

/// private method to facilitate recieving challenges and reversing them. Undefined behavior if the size_of `M` is not 32 bytes.
fn receive_challenge_as_be<M: FromBytes>(slf: &mut impl TranscriptCore) -> M {
debug_assert_eq!(32, core::mem::size_of::<M>());
let mut bytes = slf.raw_challenge();
bytes.reverse();
M::read_from(&bytes).unwrap()
}

impl<T: TranscriptCore> Transcript for T {
fn new() -> Self {
TranscriptCore::new()
}
fn extend_as_be<M: FromBytes + AsBytes>(&mut self, messages: impl IntoIterator<Item = M>) {
messages.into_iter().for_each(|mut message| {
let bytes = message.as_bytes_mut();
bytes.reverse();
self.raw_append(bytes)
})
}
fn extend_as_le_from_refs<'a, M: AsBytes + 'a>(
&mut self,
messages: impl IntoIterator<Item = &'a M>,
) {
messages
.into_iter()
.for_each(|message| self.raw_append(message.as_bytes()))
}
fn extend_scalars_as_be<'a, S: Scalar + 'a>(
&mut self,
messages: impl IntoIterator<Item = &'a S>,
) {
self.extend_as_be::<[u64; 4]>(messages.into_iter().map(RefInto::ref_into))
}
fn scalar_challenge_as_be<S: Scalar>(&mut self) -> S {
receive_challenge_as_be::<[u64; 4]>(self).into()
}
#[cfg(test)]
fn challenge_as_le(&mut self) -> [u8; 32] {
self.raw_challenge()
}
}

#[cfg(test)]
pub(super) mod test_util {
use super::TranscriptCore;
pub fn we_get_equivalent_challenges_with_equivalent_transcripts<T: TranscriptCore>() {
let mut transcript1: T = TranscriptCore::new();
transcript1.raw_append(b"message");

let mut transcript2: T = TranscriptCore::new();
transcript2.raw_append(b"message");

assert_eq!(
transcript1.raw_challenge(),
transcript2.raw_challenge(),
"challenges do not match when transcripts are the same"
);
}
pub fn we_get_different_challenges_with_different_transcripts<T: TranscriptCore>() {
let mut transcript1: T = TranscriptCore::new();
transcript1.raw_append(b"message1");

let mut transcript2: T = TranscriptCore::new();
transcript2.raw_append(b"message2");

assert_ne!(
transcript1.raw_challenge(),
transcript2.raw_challenge(),
"challenges match even though transcripts are different"
);
}
pub fn we_get_different_nontrivial_consecutive_challenges_from_transcript<T: TranscriptCore>() {
let mut transcript: T = TranscriptCore::new();
let challenge1 = transcript.raw_challenge();
let challenge2 = transcript.raw_challenge();

assert_ne!(
challenge1, [0; 32],
"first challenge in transcript is trivial"
);
assert_ne!(
challenge2, [0; 32],
"second challenge in transcript is trivial"
);
assert_ne!(
challenge1, challenge2,
"consequtive challenges match even though transcripts are different"
);
}
}
72 changes: 72 additions & 0 deletions crates/proof-of-sql/src/base/proof/transcript_core_test.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
use super::{transcript_core::TranscriptCore, Keccak256Transcript as T, Transcript};
use crate::base::scalar::Curve25519Scalar as S;
use zerocopy::AsBytes;
#[test]
fn we_can_add_values_to_the_transcript_in_big_endian_form() {
let mut transcript1: T = TranscriptCore::new();
transcript1.extend_as_be([1u16, 1000, 2]);

let mut transcript2: T = TranscriptCore::new();
transcript2.raw_append(&[0, 1]);
transcript2.raw_append(&[3, 232]);
transcript2.raw_append(&[0, 2]);

assert_eq!(transcript1.raw_challenge(), transcript2.raw_challenge());
}

#[test]
fn we_can_add_values_to_the_transcript_in_little_endian_form() {
let mut transcript1: T = TranscriptCore::new();
transcript1.extend_as_le_from_refs(&[1u16, 1000, 2]);

let mut transcript2: T = TranscriptCore::new();
transcript2.raw_append(&[1, 0]);
transcript2.raw_append(&[232, 3]);
transcript2.raw_append(&[2, 0]);

assert_eq!(transcript1.raw_challenge(), transcript2.raw_challenge());
}

#[test]
fn we_can_add_scalars_to_the_transcript_in_big_endian_form() {
let mut transcript1: T = TranscriptCore::new();
transcript1.extend_scalars_as_be(&[S::from(1), S::from(1000), S::from(2)]);

let mut transcript2: T = TranscriptCore::new();
transcript2.raw_append(&[
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 1,
]);
transcript2.raw_append(&[
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
3, 232,
]);
transcript2.raw_append(&[
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 2,
]);

assert_eq!(transcript1.raw_challenge(), transcript2.raw_challenge());
}

#[test]
fn we_can_get_challenge_as_scalar_interpreted_as_big_endian() {
let mut transcript1: T = TranscriptCore::new();
let scalar: S = transcript1.scalar_challenge_as_be();

let mut transcript2: T = TranscriptCore::new();
let mut bytes = transcript2.raw_challenge();
bytes.reverse();
let mut limbs: [u64; 4] = scalar.into();
limbs.as_bytes_mut().copy_from_slice(&bytes);

assert_eq!(scalar, limbs.into());
}

#[test]
fn we_can_get_challenge_as_little_endian() {
let mut transcript1: T = TranscriptCore::new();
let mut transcript2: T = TranscriptCore::new();

assert_eq!(transcript1.raw_challenge(), transcript2.challenge_as_le());
}
31 changes: 31 additions & 0 deletions crates/proof-of-sql/src/base/proof/transcript_protocol.rs
Original file line number Diff line number Diff line change
Expand Up @@ -185,3 +185,34 @@ impl MessageLabel {
}
}
}

impl super::transcript_core::TranscriptCore for merlin::Transcript {
fn new() -> Self {
merlin::Transcript::new(b"TranscriptCore::new")
}
fn raw_append(&mut self, message: &[u8]) {
self.append_message(b"TranscriptCore::raw_append", message)
}
fn raw_challenge(&mut self) -> [u8; 32] {
let mut result = [0u8; 32];
self.challenge_bytes(b"TranscriptCore::raw_challenge", &mut result);
result
}
}

#[cfg(test)]
mod tests {
use super::super::transcript_core::test_util::*;
#[test]
fn we_get_equivalent_challenges_with_equivalent_merlin_transcripts() {
we_get_equivalent_challenges_with_equivalent_transcripts::<merlin::Transcript>()
}
#[test]
fn we_get_different_challenges_with_different_keccak256_transcripts() {
we_get_different_challenges_with_different_transcripts::<merlin::Transcript>()
}
#[test]
fn we_get_different_nontrivial_consecutive_challenges_from_keccak256_transcript() {
we_get_different_nontrivial_consecutive_challenges_from_transcript::<merlin::Transcript>()
}
}

0 comments on commit b9ef335

Please sign in to comment.