From 4cdd1395e8e966d42008b60eeb534c9fa11376ba Mon Sep 17 00:00:00 2001 From: yuroitaki <25913766+yuroitaki@users.noreply.github.com> Date: Thu, 20 Mar 2025 22:55:13 +0800 Subject: [PATCH] feat(core): find set cover solution for user in TranscriptProofBuilder (#664) * Add reveal groups of ranges. * Reveal committed ranges given a rangeset. * Fix test and wordings. * Fix wordings. * Add reveal feature for hash commitments. * Formatting. * Fix wording. * Add subset check. * Add subset check. * Add clippy allow. * Fix missing direction in transcript index lookup. * Fix prune subset. * Refactor proof_idxs. * Throw error if only one subset detected. * Fix superset reveal. * Fmt. * Refactored Ord for Idx. * Update crates/core/src/transcript/proof.rs Co-authored-by: dan * Adjust example and comments. * Adjust comments. * Remove comment. * Change comment style. * Change comment. * Add comments. * Change to lazily check set cover. * use rangeset and simplify * restore examples * fix import * rustfmt * clippy --------- Co-authored-by: yuroitaki <> Co-authored-by: dan Co-authored-by: sinu <65924192+sinui0@users.noreply.github.com> --- Cargo.toml | 9 +- crates/common/Cargo.toml | 2 +- crates/common/src/transcript.rs | 4 +- crates/components/deap/Cargo.toml | 2 +- crates/components/deap/src/lib.rs | 2 +- crates/core/Cargo.toml | 1 + crates/core/src/index.rs | 69 +++- crates/core/src/transcript.rs | 38 +- crates/core/src/transcript/commit.rs | 11 +- crates/core/src/transcript/encoding/tree.rs | 22 ++ crates/core/src/transcript/hash.rs | 1 + crates/core/src/transcript/proof.rs | 363 +++++++++++++++----- 12 files changed, 402 insertions(+), 122 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index cce0cf278..0ff6451a3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -62,8 +62,8 @@ tlsn-tls-backend = { path = "crates/tls/backend" } tlsn-tls-client = { path = "crates/tls/client" } tlsn-tls-client-async = { path = "crates/tls/client-async" } tlsn-tls-core = { path = "crates/tls/core" } -tlsn-utils = { git = "https://github.com/tlsnotary/tlsn-utils", rev = "425614e" } -tlsn-utils-aio = { git = "https://github.com/tlsnotary/tlsn-utils", rev = "425614e" } +tlsn-utils = { git = "https://github.com/tlsnotary/tlsn-utils", rev = "6650a95" } +tlsn-utils-aio = { git = "https://github.com/tlsnotary/tlsn-utils", rev = "6650a95" } tlsn-verifier = { path = "crates/verifier" } mpz-circuits = { git = "https://github.com/privacy-scaling-explorations/mpz", tag = "v0.1.0-alpha.2" } @@ -79,10 +79,11 @@ mpz-share-conversion = { git = "https://github.com/privacy-scaling-explorations/ mpz-fields = { git = "https://github.com/privacy-scaling-explorations/mpz", tag = "v0.1.0-alpha.2" } mpz-zk = { git = "https://github.com/privacy-scaling-explorations/mpz", tag = "v0.1.0-alpha.2" } +rangeset = { version = "0.1" } serio = { version = "0.2" } -spansy = { git = "https://github.com/tlsnotary/tlsn-utils", rev = "425614e" } +spansy = { git = "https://github.com/tlsnotary/tlsn-utils", rev = "6650a95" } uid-mux = { version = "0.2" } -websocket-relay = { git = "https://github.com/tlsnotary/tlsn-utils", rev = "425614e" } +websocket-relay = { git = "https://github.com/tlsnotary/tlsn-utils", rev = "6650a95" } aes = { version = "0.8" } aes-gcm = { version = "0.9" } diff --git a/crates/common/Cargo.toml b/crates/common/Cargo.toml index 7ae0325d3..c72ecfa79 100644 --- a/crates/common/Cargo.toml +++ b/crates/common/Cargo.toml @@ -11,7 +11,6 @@ default = [] tlsn-core = { workspace = true } tlsn-tls-core = { workspace = true } tlsn-cipher = { workspace = true } -tlsn-utils = { workspace = true } mpz-core = { workspace = true } mpz-common = { workspace = true } mpz-memory-core = { workspace = true } @@ -23,6 +22,7 @@ derive_builder = { workspace = true } futures = { workspace = true } once_cell = { workspace = true } opaque-debug = { workspace = true } +rangeset = { workspace = true } serio = { workspace = true, features = ["codec", "bincode"] } thiserror = { workspace = true } tracing = { workspace = true } diff --git a/crates/common/src/transcript.rs b/crates/common/src/transcript.rs index 9cfba655b..6bda1e633 100644 --- a/crates/common/src/transcript.rs +++ b/crates/common/src/transcript.rs @@ -1,9 +1,9 @@ //! TLS transcript. use mpz_memory_core::{binary::U8, Vector}; +use rangeset::Intersection; use tls_core::msgs::enums::ContentType; use tlsn_core::transcript::{Direction, Idx, Transcript}; -use utils::range::Intersection; /// A transcript of sent and received TLS records. #[derive(Debug, Default, Clone)] @@ -172,9 +172,9 @@ pub struct IncompleteTranscript {} mod tests { use super::TranscriptRefs; use mpz_memory_core::{binary::U8, FromRaw, Slice, Vector}; + use rangeset::RangeSet; use std::ops::Range; use tlsn_core::transcript::{Direction, Idx}; - use utils::range::RangeSet; // TRANSCRIPT_REFS: // diff --git a/crates/components/deap/Cargo.toml b/crates/components/deap/Cargo.toml index d17e89762..55636bcc8 100644 --- a/crates/components/deap/Cargo.toml +++ b/crates/components/deap/Cargo.toml @@ -7,7 +7,7 @@ edition = "2021" mpz-core = { workspace = true } mpz-common = { workspace = true } mpz-vm-core = { workspace = true } -tlsn-utils = { workspace = true } +rangeset = { workspace = true } thiserror = { workspace = true } serde = { workspace = true, features = ["derive"] } serio = { workspace = true } diff --git a/crates/components/deap/src/lib.rs b/crates/components/deap/src/lib.rs index 46100e039..d85df19d8 100644 --- a/crates/components/deap/src/lib.rs +++ b/crates/components/deap/src/lib.rs @@ -19,8 +19,8 @@ use mpz_vm_core::{ memory::{binary::Binary, DecodeFuture, Memory, Slice, View}, Call, Callable, Execute, Vm, VmError, }; +use rangeset::{Difference, RangeSet, UnionMut}; use tokio::sync::{Mutex, MutexGuard, OwnedMutexGuard}; -use utils::range::{Difference, RangeSet, UnionMut}; type Error = DeapError; diff --git a/crates/core/Cargo.toml b/crates/core/Cargo.toml index 918ae7d30..4e2db52b9 100644 --- a/crates/core/Cargo.toml +++ b/crates/core/Cargo.toml @@ -16,6 +16,7 @@ fixtures = ["dep:hex", "dep:tlsn-data-fixtures"] tlsn-data-fixtures = { workspace = true, optional = true } tlsn-tls-core = { workspace = true, features = ["serde"] } tlsn-utils = { workspace = true } +rangeset = { workspace = true, features = ["serde"] } bcs = { workspace = true } bimap = { version = "0.6", features = ["serde"] } diff --git a/crates/core/src/index.rs b/crates/core/src/index.rs index 6d9daa7a2..1f40759ab 100644 --- a/crates/core/src/index.rs +++ b/crates/core/src/index.rs @@ -6,18 +6,23 @@ use crate::{ attestation::{Field, FieldId}, transcript::{ hash::{PlaintextHash, PlaintextHashSecret}, - Idx, + Direction, Idx, }, }; -/// Index for items which can be looked up by transcript index or field id. +/// Index for items which can be looked up by transcript's (direction and index) +/// or field id. #[derive(Debug, Clone)] pub(crate) struct Index { items: Vec, // Lookup by field id. field_ids: HashMap, - // Lookup by transcript index. - transcript_idxs: HashMap, + // Lookup by transcript direction and index. + transcript_idxs: HashMap<(Direction, Idx), usize>, + /// Union of all sent indices. + sent: Idx, + /// Union of all received indices. + recv: Idx, } impl Default for Index { @@ -26,6 +31,8 @@ impl Default for Index { items: Default::default(), field_ids: Default::default(), transcript_idxs: Default::default(), + sent: Default::default(), + recv: Default::default(), } } } @@ -60,19 +67,28 @@ impl From> for Vec { impl Index { pub(crate) fn new(items: Vec, f: F) -> Self where - F: Fn(&T) -> (&FieldId, &Idx), + F: Fn(&T) -> (&FieldId, Direction, &Idx), { let mut field_ids = HashMap::new(); let mut transcript_idxs = HashMap::new(); + let mut sent = Idx::default(); + let mut recv = Idx::default(); for (i, item) in items.iter().enumerate() { - let (id, idx) = f(item); + let (id, dir, idx) = f(item); field_ids.insert(*id, i); - transcript_idxs.insert(idx.clone(), i); + transcript_idxs.insert((dir, idx.clone()), i); + match dir { + Direction::Sent => sent.union_mut(idx), + Direction::Received => recv.union_mut(idx), + } } + Self { items, field_ids, transcript_idxs, + sent, + recv, } } @@ -84,15 +100,28 @@ impl Index { self.field_ids.get(id).map(|i| &self.items[*i]) } - pub(crate) fn get_by_transcript_idx(&self, idx: &Idx) -> Option<&T> { - self.transcript_idxs.get(idx).map(|i| &self.items[*i]) + #[allow(unused)] + pub(crate) fn get_by_transcript_idx(&self, dir_idx: &(Direction, Idx)) -> Option<&T> { + self.transcript_idxs.get(dir_idx).map(|i| &self.items[*i]) + } + + pub(crate) fn idx(&self, direction: Direction) -> &Idx { + match direction { + Direction::Sent => &self.sent, + Direction::Received => &self.recv, + } + } + + #[allow(unused)] + pub(crate) fn iter_idxs(&self) -> impl Iterator { + self.transcript_idxs.keys() } } impl From>> for Index> { fn from(items: Vec>) -> Self { Self::new(items, |field: &Field| { - (&field.id, &field.data.idx) + (&field.id, field.data.direction, &field.data.idx) }) } } @@ -100,26 +129,29 @@ impl From>> for Index> { impl From> for Index { fn from(items: Vec) -> Self { Self::new(items, |item: &PlaintextHashSecret| { - (&item.commitment, &item.idx) + (&item.commitment, item.direction, &item.idx) }) } } #[cfg(test)] mod test { - use utils::range::RangeSet; + use rangeset::RangeSet; use super::*; #[derive(PartialEq, Debug, Clone)] struct Stub { field_index: FieldId, + direction: Direction, index: Idx, } impl From> for Index { fn from(items: Vec) -> Self { - Self::new(items, |item: &Stub| (&item.field_index, &item.index)) + Self::new(items, |item: &Stub| { + (&item.field_index, item.direction, &item.index) + }) } } @@ -127,10 +159,12 @@ mod test { vec![ Stub { field_index: FieldId(1), + direction: Direction::Sent, index: Idx::new(RangeSet::from([0..1, 18..21])), }, Stub { field_index: FieldId(2), + direction: Direction::Received, index: Idx::new(RangeSet::from([1..5, 8..11])), }, ] @@ -144,10 +178,12 @@ mod test { let stubs = vec![ Stub { field_index: FieldId(1), + direction: Direction::Sent, index: stub_a_index.clone(), }, Stub { field_index: stub_b_field_index, + direction: Direction::Received, index: Idx::new(RangeSet::from([1..5, 8..11])), }, ]; @@ -158,7 +194,7 @@ mod test { Some(&stubs[1]) ); assert_eq!( - stubs_index.get_by_transcript_idx(&stub_a_index), + stubs_index.get_by_transcript_idx(&(Direction::Sent, stub_a_index)), Some(&stubs[0]) ); } @@ -172,6 +208,9 @@ mod test { let wrong_field_index = FieldId(200); assert_eq!(stubs_index.get_by_field_id(&wrong_field_index), None); - assert_eq!(stubs_index.get_by_transcript_idx(&wrong_index), None); + assert_eq!( + stubs_index.get_by_transcript_idx(&(Direction::Sent, wrong_index)), + None + ); } } diff --git a/crates/core/src/transcript.rs b/crates/core/src/transcript.rs index 55074ad71..5a6ab0606 100644 --- a/crates/core/src/transcript.rs +++ b/crates/core/src/transcript.rs @@ -39,8 +39,8 @@ mod proof; use std::{fmt, ops::Range}; +use rangeset::{Difference, IndexRanges, RangeSet, Subset, ToRangeSet, Union, UnionMut}; use serde::{Deserialize, Serialize}; -use utils::range::{Difference, IndexRanges, RangeSet, ToRangeSet, Union}; use crate::connection::TranscriptLength; @@ -499,10 +499,44 @@ impl Idx { self.0.len_ranges() } + pub(crate) fn as_range_set(&self) -> &RangeSet { + &self.0 + } + /// Returns the union of this index with another. - pub fn union(&self, other: &Idx) -> Idx { + pub(crate) fn union(&self, other: &Idx) -> Idx { Idx(self.0.union(&other.0)) } + + /// Unions this index with another. + pub(crate) fn union_mut(&mut self, other: &Idx) { + self.0.union_mut(&other.0); + } + + /// Returns the difference between `self` and `other`. + pub(crate) fn difference(&self, other: &Idx) -> Idx { + Idx(self.0.difference(&other.0)) + } + + /// Returns `true` if `self` is a subset of `other`. + pub(crate) fn is_subset(&self, other: &Idx) -> bool { + self.0.is_subset(&other.0) + } +} + +impl std::fmt::Display for Idx { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str("Idx([")?; + let count = self.0.len_ranges(); + for (i, range) in self.0.iter_ranges().enumerate() { + write!(f, "{}..{}", range.start, range.end)?; + if i < count - 1 { + write!(f, ", ")?; + } + } + f.write_str("])")?; + Ok(()) + } } /// Builder for [`Idx`]. diff --git a/crates/core/src/transcript/commit.rs b/crates/core/src/transcript/commit.rs index 3106d3996..e7f959578 100644 --- a/crates/core/src/transcript/commit.rs +++ b/crates/core/src/transcript/commit.rs @@ -2,8 +2,8 @@ use std::{collections::HashSet, fmt}; +use rangeset::ToRangeSet; use serde::{Deserialize, Serialize}; -use utils::range::ToRangeSet; use crate::{ hash::HashAlgId, @@ -22,6 +22,15 @@ pub enum TranscriptCommitmentKind { }, } +impl fmt::Display for TranscriptCommitmentKind { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::Encoding => f.write_str("encoding"), + Self::Hash { alg } => write!(f, "hash ({alg})"), + } + } +} + /// Configuration for transcript commitments. #[derive(Debug, Clone)] pub struct TranscriptCommitConfig { diff --git a/crates/core/src/transcript/encoding/tree.rs b/crates/core/src/transcript/encoding/tree.rs index cff9c0b9c..b315a8f38 100644 --- a/crates/core/src/transcript/encoding/tree.rs +++ b/crates/core/src/transcript/encoding/tree.rs @@ -61,6 +61,10 @@ pub struct EncodingTree { /// Mapping between the index of a leaf and the transcript index it /// corresponds to. idxs: BiMap, + /// Union of all transcript indices in the sent direction. + sent_idx: Idx, + /// Union of all transcript indices in the received direction. + received_idx: Idx, } opaque_debug::implement!(EncodingTree); @@ -84,6 +88,8 @@ impl EncodingTree { tree: MerkleTree::new(hasher.id()), nonces: Vec::new(), idxs: BiMap::new(), + sent_idx: Idx::empty(), + received_idx: Idx::empty(), }; let mut leaves = Vec::new(); @@ -122,6 +128,10 @@ impl EncodingTree { leaves.push(hasher.hash(&CanonicalSerialize::serialize(&leaf))); this.nonces.push(leaf.into_parts().1); this.idxs.insert(this.idxs.len(), dir_idx.clone()); + match direction { + Direction::Sent => this.sent_idx = this.sent_idx.union(idx), + Direction::Received => this.received_idx = this.received_idx.union(idx), + } } this.tree.insert(hasher, leaves); @@ -192,6 +202,18 @@ impl EncodingTree { pub fn contains(&self, idx: &(Direction, Idx)) -> bool { self.idxs.contains_right(idx) } + + pub(crate) fn idx(&self, direction: Direction) -> &Idx { + match direction { + Direction::Sent => &self.sent_idx, + Direction::Received => &self.received_idx, + } + } + + /// Returns the committed transcript indices. + pub(crate) fn transcript_indices(&self) -> impl Iterator { + self.idxs.right_values() + } } #[cfg(test)] diff --git a/crates/core/src/transcript/hash.rs b/crates/core/src/transcript/hash.rs index 3bb05df02..9083545c7 100644 --- a/crates/core/src/transcript/hash.rs +++ b/crates/core/src/transcript/hash.rs @@ -39,6 +39,7 @@ pub(crate) struct PlaintextHashProof { } impl PlaintextHashProof { + #[allow(unused)] pub(crate) fn new(data: Blinded>, commitment: FieldId) -> Self { Self { data, commitment } } diff --git a/crates/core/src/transcript/proof.rs b/crates/core/src/transcript/proof.rs index c3411ddfe..aa525fbe9 100644 --- a/crates/core/src/transcript/proof.rs +++ b/crates/core/src/transcript/proof.rs @@ -1,13 +1,11 @@ //! Transcript proofs. -use std::{collections::HashSet, fmt}; - +use rangeset::{Cover, ToRangeSet}; use serde::{Deserialize, Serialize}; -use utils::range::ToRangeSet; +use std::fmt; use crate::{ attestation::Body, - hash::Blinded, index::Index, transcript::{ commit::TranscriptCommitmentKind, @@ -135,6 +133,32 @@ impl From for TranscriptProofError { } } +#[derive(Debug)] +struct QueryIdx { + sent: Idx, + recv: Idx, +} + +impl QueryIdx { + fn new() -> Self { + Self { + sent: Idx::empty(), + recv: Idx::empty(), + } + } + + fn is_empty(&self) -> bool { + self.sent.is_empty() && self.recv.is_empty() + } + + fn union(&mut self, direction: &Direction, other: &Idx) { + match direction { + Direction::Sent => self.sent.union_mut(other), + Direction::Received => self.recv.union_mut(other), + } + } +} + /// Builder for [`TranscriptProof`]. #[derive(Debug)] pub struct TranscriptProofBuilder<'a> { @@ -142,8 +166,8 @@ pub struct TranscriptProofBuilder<'a> { transcript: &'a Transcript, encoding_tree: Option<&'a EncodingTree>, plaintext_hashes: &'a Index, - encoding_proof_idxs: HashSet<(Direction, Idx)>, - hash_proofs: Vec, + encoding_query_idx: QueryIdx, + hash_query_idx: QueryIdx, } impl<'a> TranscriptProofBuilder<'a> { @@ -158,8 +182,8 @@ impl<'a> TranscriptProofBuilder<'a> { transcript, encoding_tree, plaintext_hashes, - encoding_proof_idxs: HashSet::default(), - hash_proofs: Vec::new(), + encoding_query_idx: QueryIdx::new(), + hash_query_idx: QueryIdx::new(), } } @@ -206,50 +230,32 @@ impl<'a> TranscriptProofBuilder<'a> { )); }; - let dir_idx = (direction, idx); - - if !encoding_tree.contains(&dir_idx) { + if idx.is_subset(encoding_tree.idx(direction)) { + self.encoding_query_idx.union(&direction, &idx); + } else { + let missing = idx.difference(encoding_tree.idx(direction)); return Err(TranscriptProofBuilderError::new( BuilderErrorKind::MissingCommitment, format!( - "encoding commitment is missing for ranges in {} transcript", - direction + "encoding commitment is missing for ranges in {direction} transcript: {missing}" ), )); } - - self.encoding_proof_idxs.insert(dir_idx); } TranscriptCommitmentKind::Hash { .. } => { - let Some(PlaintextHashSecret { - direction, - commitment, - blinder, - .. - }) = self.plaintext_hashes.get_by_transcript_idx(&idx) - else { + if idx.is_subset(self.plaintext_hashes.idx(direction)) { + self.hash_query_idx.union(&direction, &idx); + } else { + let missing = idx.difference(self.plaintext_hashes.idx(direction)); return Err(TranscriptProofBuilderError::new( BuilderErrorKind::MissingCommitment, format!( - "hash commitment is missing for ranges in {} transcript", - direction + "hash commitment is missing for ranges in {direction} transcript: {missing}" ), )); - }; - - let (_, data) = self - .transcript - .get(*direction, &idx) - .expect("subsequence was checked to be in transcript") - .into_parts(); - - self.hash_proofs.push(PlaintextHashProof::new( - Blinded::new_with_blinder(data, blinder.clone()), - *commitment, - )); + } } } - Ok(self) } @@ -296,19 +302,52 @@ impl<'a> TranscriptProofBuilder<'a> { /// Builds the transcript proof. pub fn build(self) -> Result { - let encoding_proof = if !self.encoding_proof_idxs.is_empty() { + let encoding_proof = if !self.encoding_query_idx.is_empty() { let encoding_tree = self.encoding_tree.expect("encoding tree is present"); + + let Some(sent_idxs) = self.encoding_query_idx.sent.as_range_set().cover_by( + encoding_tree + .transcript_indices() + .filter(|(dir, _)| *dir == Direction::Sent), + |(_, idx)| &idx.0, + ) else { + return Err(TranscriptProofBuilderError::cover( + Direction::Sent, + TranscriptCommitmentKind::Encoding, + )); + }; + + let Some(recv_idxs) = self.encoding_query_idx.recv.as_range_set().cover_by( + encoding_tree + .transcript_indices() + .filter(|(dir, _)| *dir == Direction::Received), + |(_, idx)| &idx.0, + ) else { + return Err(TranscriptProofBuilderError::cover( + Direction::Received, + TranscriptCommitmentKind::Encoding, + )); + }; + let proof = encoding_tree - .proof(self.transcript, self.encoding_proof_idxs.iter()) + .proof(self.transcript, sent_idxs.into_iter().chain(recv_idxs)) .expect("subsequences were checked to be in tree"); + Some(proof) } else { None }; + if !self.hash_query_idx.is_empty() { + return Err(TranscriptProofBuilderError::new( + BuilderErrorKind::NotSupported, + "opening transcript hash commitments is not yet supported", + )); + } + Ok(TranscriptProof { encoding_proof, - hash_proofs: self.hash_proofs, + hash_proofs: Vec::new(), }) } } @@ -330,12 +369,24 @@ impl TranscriptProofBuilderError { source: Some(source.into()), } } + + fn cover(direction: Direction, kind: TranscriptCommitmentKind) -> Self { + Self { + kind: BuilderErrorKind::Cover { direction, kind }, + source: None, + } + } } -#[derive(Debug)] +#[derive(Debug, PartialEq)] enum BuilderErrorKind { Index, MissingCommitment, + Cover { + direction: Direction, + kind: TranscriptCommitmentKind, + }, + NotSupported, } impl fmt::Display for TranscriptProofBuilderError { @@ -345,6 +396,10 @@ impl fmt::Display for TranscriptProofBuilderError { match self.kind { BuilderErrorKind::Index => f.write_str("index error")?, BuilderErrorKind::MissingCommitment => f.write_str("commitment error")?, + BuilderErrorKind::Cover { direction, kind } => f.write_str(&format!( + "unable to cover ranges in {direction} transcript using available {kind} commitments" + ))?, + BuilderErrorKind::NotSupported => f.write_str("not supported")?, } if let Some(source) = &self.source { @@ -355,74 +410,28 @@ impl fmt::Display for TranscriptProofBuilderError { } } +#[allow(clippy::single_range_in_vec_init)] #[cfg(test)] mod tests { + use rangeset::RangeSet; + use rstest::rstest; use tlsn_data_fixtures::http::{request::GET_WITH_HEADER, response::OK_JSON}; use crate::{ + attestation::FieldId, fixtures::{ attestation_fixture, encoder_secret, encoding_provider, request_fixture, ConnectionFixture, RequestFixture, }, - hash::Blake3, + hash::{Blake3, HashAlgId}, signing::SignatureAlgId, + transcript::TranscriptCommitConfigBuilder, }; use super::*; - #[test] - fn test_reveal_range_out_of_bounds() { - let transcript = Transcript::new( - [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11], - [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11], - ); - let index = Index::default(); - let mut builder = TranscriptProofBuilder::new(&transcript, None, &index); - - let err = builder.reveal(&(10..15), Direction::Sent).err().unwrap(); - assert!(matches!(err.kind, BuilderErrorKind::Index)); - - let err = builder - .reveal(&(10..15), Direction::Received) - .err() - .unwrap(); - assert!(matches!(err.kind, BuilderErrorKind::Index)); - } - - #[test] - fn test_reveal_missing_encoding_tree() { - let transcript = Transcript::new( - [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11], - [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11], - ); - let index = Index::default(); - let mut builder = TranscriptProofBuilder::new(&transcript, None, &index); - - let err = builder.reveal_recv(&(9..11)).err().unwrap(); - assert!(matches!(err.kind, BuilderErrorKind::MissingCommitment)); - } - - #[test] - fn test_reveal_missing_encoding_commitment_range() { - let transcript = Transcript::new(GET_WITH_HEADER, OK_JSON); - let connection = ConnectionFixture::tlsnotary(transcript.length()); - - let RequestFixture { encoding_tree, .. } = request_fixture( - transcript.clone(), - encoding_provider(GET_WITH_HEADER, OK_JSON), - connection, - Blake3::default(), - ); - - let index = Index::default(); - let mut builder = TranscriptProofBuilder::new(&transcript, Some(&encoding_tree), &index); - - let err = builder.reveal_recv(&(0..11)).err().unwrap(); - assert!(matches!(err.kind, BuilderErrorKind::MissingCommitment)); - } - - #[test] - fn test_verify_missing_encoding_commitment() { + #[rstest] + fn test_verify_missing_encoding_commitment_root() { let transcript = Transcript::new(GET_WITH_HEADER, OK_JSON); let connection = ConnectionFixture::tlsnotary(transcript.length()); @@ -458,4 +467,168 @@ mod tests { .unwrap(); assert!(matches!(err.kind, ErrorKind::Encoding)); } + + #[rstest] + fn test_reveal_range_out_of_bounds() { + let transcript = Transcript::new( + [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11], + [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11], + ); + let index = Index::default(); + let mut builder = TranscriptProofBuilder::new(&transcript, None, &index); + + let err = builder.reveal(&(10..15), Direction::Sent).unwrap_err(); + assert!(matches!(err.kind, BuilderErrorKind::Index)); + + let err = builder + .reveal(&(10..15), Direction::Received) + .err() + .unwrap(); + assert!(matches!(err.kind, BuilderErrorKind::Index)); + } + + #[rstest] + fn test_reveal_missing_encoding_tree() { + let transcript = Transcript::new( + [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11], + [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11], + ); + let index = Index::default(); + let mut builder = TranscriptProofBuilder::new(&transcript, None, &index); + + let err = builder.reveal_recv(&(9..11)).unwrap_err(); + assert!(matches!(err.kind, BuilderErrorKind::MissingCommitment)); + } + + #[rstest] + fn test_reveal_incomplete_encoding_commitment_range() { + let transcript = Transcript::new(GET_WITH_HEADER, OK_JSON); + let connection = ConnectionFixture::tlsnotary(transcript.length()); + + let RequestFixture { encoding_tree, .. } = request_fixture( + transcript.clone(), + encoding_provider(GET_WITH_HEADER, OK_JSON), + connection, + Blake3::default(), + ); + + let index = Index::default(); + let mut builder = TranscriptProofBuilder::new(&transcript, Some(&encoding_tree), &index); + + // We only have a commitment for the entire received transcript. We won't be + // able to cover this range alone. + builder.reveal_recv(&(0..11)).unwrap(); + + let err = builder.build().unwrap_err(); + assert!(matches!( + err.kind, + BuilderErrorKind::Cover { + direction: Direction::Received, + kind: TranscriptCommitmentKind::Encoding + } + )); + } + + #[rstest] + #[case::reveal_all_rangesets_with_exact_set( + vec![RangeSet::from([0..10]), RangeSet::from([12..30]), RangeSet::from([0..5, 15..30]), RangeSet::from([70..75, 85..100])], + RangeSet::from([0..10, 12..30]), + true, + )] + #[case::reveal_all_rangesets_with_superset_ranges( + vec![RangeSet::from([0..1]), RangeSet::from([1..2, 8..9]), RangeSet::from([2..4, 6..8]), RangeSet::from([2..3, 6..7]), RangeSet::from([9..12])], + RangeSet::from([0..4, 6..9]), + true, + )] + #[case::reveal_all_rangesets_with_superset_range( + vec![RangeSet::from([0..1, 2..4]), RangeSet::from([1..3]), RangeSet::from([1..9]), RangeSet::from([2..3])], + RangeSet::from([0..4]), + true, + )] + #[case::failed_to_reveal_with_superset_range_missing_within( + vec![RangeSet::from([0..20, 45..56]), RangeSet::from([80..120]), RangeSet::from([50..53])], + RangeSet::from([0..120]), + false, + )] + #[case::failed_to_reveal_with_superset_range_missing_outside( + vec![RangeSet::from([2..20, 45..116]), RangeSet::from([20..45]), RangeSet::from([50..53])], + RangeSet::from([0..120]), + false, + )] + #[case::failed_to_reveal_with_superset_ranges_missing_outside( + vec![RangeSet::from([1..10]), RangeSet::from([1..20]), RangeSet::from([15..20, 75..110])], + RangeSet::from([0..41, 74..100]), + false, + )] + #[case::failed_to_reveal_as_no_subset_range( + vec![RangeSet::from([2..4]), RangeSet::from([1..2]), RangeSet::from([1..9]), RangeSet::from([2..3])], + RangeSet::from([0..1]), + false, + )] + #[allow(clippy::single_range_in_vec_init)] + fn test_reveal_mutliple_rangesets_with_one_rangeset( + #[case] commit_recv_rangesets: Vec>, + #[case] reveal_recv_rangeset: RangeSet, + #[case] success: bool, + ) { + let transcript = Transcript::new(GET_WITH_HEADER, OK_JSON); + + // Encoding commitment kind + let mut transcript_commitment_builder = TranscriptCommitConfigBuilder::new(&transcript); + for rangeset in commit_recv_rangesets.iter() { + transcript_commitment_builder.commit_recv(rangeset).unwrap(); + } + + let transcripts_commitment_config = transcript_commitment_builder.build().unwrap(); + + let encoding_tree = EncodingTree::new( + &Blake3::default(), + transcripts_commitment_config.iter_encoding(), + &encoding_provider(GET_WITH_HEADER, OK_JSON), + &transcript.length(), + ) + .unwrap(); + + let index = Index::default(); + let mut builder = TranscriptProofBuilder::new(&transcript, Some(&encoding_tree), &index); + + if success { + assert!(builder.reveal_recv(&reveal_recv_rangeset).is_ok()); + } else { + let err = builder.reveal_recv(&reveal_recv_rangeset).unwrap_err(); + assert!(matches!(err.kind, BuilderErrorKind::MissingCommitment)); + } + + // Hash commitment kind + let mut transcript_commitment_builder = TranscriptCommitConfigBuilder::new(&transcript); + transcript_commitment_builder.default_kind(TranscriptCommitmentKind::Hash { + alg: HashAlgId::SHA256, + }); + for rangeset in commit_recv_rangesets.iter() { + transcript_commitment_builder.commit_recv(rangeset).unwrap(); + } + let transcripts_commitment_config = transcript_commitment_builder.build().unwrap(); + + let plaintext_hash_secrets: Index = transcripts_commitment_config + .iter_hash() + .map(|(&(direction, ref idx), _)| PlaintextHashSecret { + direction, + idx: idx.clone(), + commitment: FieldId::default(), + blinder: rand::random(), + }) + .collect::>() + .into(); + let mut builder = TranscriptProofBuilder::new(&transcript, None, &plaintext_hash_secrets); + builder.default_kind(TranscriptCommitmentKind::Hash { + alg: HashAlgId::SHA256, + }); + + if success { + assert!(builder.reveal_recv(&reveal_recv_rangeset).is_ok()); + } else { + let err = builder.reveal_recv(&reveal_recv_rangeset).unwrap_err(); + assert!(matches!(err.kind, BuilderErrorKind::MissingCommitment)); + } + } }