diff --git a/crates/core/src/index.rs b/crates/core/src/index.rs index 6d9daa7a20..38acf5d4ad 100644 --- a/crates/core/src/index.rs +++ b/crates/core/src/index.rs @@ -6,18 +6,19 @@ 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>, } impl Default for Index { @@ -60,14 +61,14 @@ 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(); 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); } Self { items, @@ -84,15 +85,15 @@ 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]) + 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]) } } 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,7 +101,7 @@ 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) }) } } @@ -114,12 +115,15 @@ mod test { #[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 +131,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 +150,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 +166,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 +180,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 0d6c073309..9143c2331d 100644 --- a/crates/core/src/transcript.rs +++ b/crates/core/src/transcript.rs @@ -40,7 +40,7 @@ mod proof; use std::{fmt, ops::Range}; use serde::{Deserialize, Serialize}; -use utils::range::{Difference, IndexRanges, RangeSet, ToRangeSet, Union}; +use utils::range::{Difference, IndexRanges, RangeSet, Subset, ToRangeSet, Union}; use crate::connection::TranscriptLength; @@ -494,6 +494,11 @@ impl Idx { self.0.len() } + /// Returns the number of ranges in the index. + pub fn len_ranges(&self) -> usize { + self.0.len_ranges() + } + /// Returns whether the index is empty. pub fn is_empty(&self) -> bool { self.0.is_empty() @@ -508,6 +513,11 @@ impl Idx { pub fn union(&self, other: &Idx) -> Idx { Idx(self.0.union(&other.0)) } + + /// Checks if this index is a subset of another. + pub fn is_subset(&self, other: &Idx) -> bool { + self.0.is_subset(&other.0) + } } /// Builder for [`Idx`]. diff --git a/crates/core/src/transcript/encoding/tree.rs b/crates/core/src/transcript/encoding/tree.rs index 3bcec07067..8ab5cc5af0 100644 --- a/crates/core/src/transcript/encoding/tree.rs +++ b/crates/core/src/transcript/encoding/tree.rs @@ -192,6 +192,11 @@ impl EncodingTree { pub fn contains(&self, idx: &(Direction, Idx)) -> bool { self.idxs.contains_right(idx) } + + /// Returns the committed transcript indices. + pub(crate) fn transcript_indices(&self) -> impl IntoIterator { + self.idxs.right_values() + } } #[cfg(test)] diff --git a/crates/core/src/transcript/proof.rs b/crates/core/src/transcript/proof.rs index ae9a68dc1e..7f4f5263cf 100644 --- a/crates/core/src/transcript/proof.rs +++ b/crates/core/src/transcript/proof.rs @@ -1,8 +1,12 @@ //! Transcript proofs. -use std::{collections::HashSet, fmt}; - use serde::{Deserialize, Serialize}; +use std::{ + cmp::Ordering, + collections::{hash_set::Iter, HashSet}, + fmt, + ops::Range, +}; use utils::range::ToRangeSet; use crate::{ @@ -135,6 +139,186 @@ impl From for TranscriptProofError { } } +/// A wrapper on [Idx]. +/// +/// This is purely used for [ProofIdxs::prune_subset]. An effective sort of +/// [Idx] is needed there to reduce the time complexity to prune subset(s) from +/// a collection of [Idx]. +/// +/// A custom ordering logic is implemented for [OrderedIdx] to achieve that (see +/// [Ord] implementation below). As a result, for each [OrderedIdx] in this +/// sorted collection, a full on subset check only needs to be done with other +/// [OrderedIdx] that are bigger than itself. This is faster than +/// conducting the check with every other [OrderedIdx] in the collection. +#[derive(Debug, Clone, Hash, PartialEq, Eq)] +struct OrderedIdx(Idx); + +impl OrderedIdx { + fn new(idx: Idx) -> Self { + Self(idx) + } + + fn inner(&self) -> &Idx { + &self.0 + } + + fn into_inner(self) -> Idx { + self.0 + } + + fn is_subset(&self, other: &OrderedIdx) -> bool { + self.0.is_subset(other.inner()) + } + + fn iter_ranges(&self) -> impl Iterator> + '_ { + self.0.iter_ranges() + } +} + +/// Custom ordering for [OrderedIdx]. +/// +/// [OrderedIdx] is ordered in terms of its likelihood to be a subset of other, +/// where [OrderedIdx] with higher likelihood to be a subset of other, is +/// smaller. For example, +/// +/// (a) [4..5, 7..9] is smaller than [1..5, 6..10]. +/// (b) [7..9, 10..13] is smaller than [7..10, 12..15]. +/// (c) [1..3, 5..9] is smaller than [1..3, 4..10]. +/// +/// Instead of a full on subset check, a cheaper 'subset likelihood' check is +/// used for this. Note that this doesn't guarantee that the smaller +/// [OrderedIdx] is always a subset, e.g. example (b). +impl Ord for OrderedIdx { + fn cmp(&self, other: &Self) -> Ordering { + let mut self_ranges_iter = self.iter_ranges(); + let mut other_ranges_iter = other.iter_ranges(); + + let mut self_range_option = self_ranges_iter.next(); + let mut other_range_option = other_ranges_iter.next(); + + // Iterate from the respective first range of [self] and [other], return + // immediately if their start or end are different. + while self_range_option.is_some() && other_range_option.is_some() { + let self_range = self_range_option.unwrap(); + let other_range = other_range_option.unwrap(); + + // Compare the start of the range (start..end) between the nth range of [self] + // and [other], where bigger start = higher likelihood to be a + // subset. For example, 4..x is likely to be a subset of 1..y, but + // 1..y cannot be a subset of 4..x. + if self_range.start != other_range.start { + // reverse() as bigger start = more likely to be subset = smaller [OrderedIdx]. + return self_range.start.cmp(&other_range.start).reverse(); + } + + // If start is the same, then the end of the range (start..end) is compared. + // Smaller end = higher likelihood to be a subset. For example, x..4 is likely + // to be a subset of x..5, but x..5 cannot be a subset of x..4. + if self_range.end != other_range.end { + return self_range.end.cmp(&other_range.end); + } + + self_range_option = self_ranges_iter.next(); + other_range_option = other_ranges_iter.next(); + } + + match (self_range_option, other_range_option) { + // All N ranges of [other] are the same as the first N ranges of [self], + // i.e. [other] is a subset of [self]. + (Some(_), None) => Ordering::Greater, + // All N ranges of [self] are the same as the first N ranges of [other], + // i.e. [self] is a subset of [other]. + (None, Some(_)) => Ordering::Less, + // All ranges of [self] are the same as all ranges of [other]. + (None, None) => Ordering::Equal, + (Some(_), Some(_)) => { + unreachable!("self_range_option and other_range_option cannot be both some") + } + } + } +} + +impl PartialOrd for OrderedIdx { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +/// A wrapper to store and process ([Direction], [Idx]) to be revealed. +#[derive(Debug)] +struct ProofIdxs(HashSet<(Direction, Idx)>); + +impl ProofIdxs { + fn new() -> Self { + Self(HashSet::default()) + } + + fn insert(&mut self, dir_idx: (Direction, Idx)) -> bool { + self.0.insert(dir_idx) + } + + fn is_empty(&self) -> bool { + self.0.is_empty() + } + + fn iter(&self) -> Iter<'_, (Direction, Idx)> { + self.0.iter() + } + + /// Prune any rangeset [Idx] that is a subset of another rangeset before + /// constructing [EncodingProof] or [PlaintextHashProof] to reduce + /// unnecessary proof size, and proving + verifying time later. + /// + /// This is because any subset rangeset would also be revealed by its + /// superset rangeset. + fn prune_subset(&mut self) { + self.prune_subset_with_direction(Direction::Sent); + self.prune_subset_with_direction(Direction::Received); + } + + fn prune_subset_with_direction(&mut self, direction: Direction) { + // Given a specific [Direction], collect relevant [Idx] and build [OrderedIdx]. + let mut idxs_to_prune = self + .iter() + .filter(|(dir, _)| *dir == direction) + .map(|(_, idx)| OrderedIdx::new(idx.clone())) + .collect::>(); + + // Sort the collection according to the custom ordering described in [Ord] + // implementation of [OrderedIdx] — which reduces subset search + // time complexity. + idxs_to_prune.sort_unstable(); + + // Reverse the order so that subset check of each [OrderedIdx] is done with the + // most likely superset (the biggest) first. + idxs_to_prune.reverse(); + + // [OrderedIdx] to be pruned and excluded. + let mut pruned_idxs = HashSet::new(); + + // Start from the second [OrderedIdx], as the first is guaranteed to not be a + // subset of any. + for i in 1..idxs_to_prune.len() { + let idx = &idxs_to_prune[i]; + + // Perform subset check of [idx] with [other_idx] that are bigger, as [idx] + // is only likely to be their subset. + for other_idx in idxs_to_prune.iter().take(i) { + // Remove [idx] if it's a subset — this check contains an inner loop, which is + // why the custom-ordering sort is used to minimise the no. of iteration of + // the current loop. + if !pruned_idxs.contains(other_idx) && idx.is_subset(other_idx) { + pruned_idxs.insert(idx); + } + } + } + + pruned_idxs.into_iter().for_each(|idx| { + self.0.remove(&(direction, idx.clone().into_inner())); + }); + } +} + /// Builder for [`TranscriptProof`]. #[derive(Debug)] pub struct TranscriptProofBuilder<'a> { @@ -142,8 +326,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_proof_idxs: ProofIdxs, + hash_proof_idxs: ProofIdxs, } impl<'a> TranscriptProofBuilder<'a> { @@ -158,8 +342,8 @@ impl<'a> TranscriptProofBuilder<'a> { transcript, encoding_tree, plaintext_hashes, - encoding_proof_idxs: HashSet::default(), - hash_proofs: Vec::new(), + encoding_proof_idxs: ProofIdxs::new(), + hash_proof_idxs: ProofIdxs::new(), } } @@ -197,6 +381,8 @@ impl<'a> TranscriptProofBuilder<'a> { )); } + let dir_idx = (direction, idx); + match kind { TranscriptCommitmentKind::Encoding => { let Some(encoding_tree) = self.encoding_tree else { @@ -206,50 +392,108 @@ impl<'a> TranscriptProofBuilder<'a> { )); }; - let dir_idx = (direction, idx); - - if !encoding_tree.contains(&dir_idx) { - return Err(TranscriptProofBuilderError::new( - BuilderErrorKind::MissingCommitment, - format!( - "encoding commitment is missing for ranges in {} transcript", - direction - ), - )); + // Insert the rangeset [dir_idx] if it's in the encoding tree, i.e. it's + // committed. + if encoding_tree.contains(&dir_idx) { + self.encoding_proof_idxs.insert(dir_idx); + } else { + // Check if there is any committed rangeset in the encoding tree that is a + // subset of [dir_idx] — if yes, stage them into a temporary collection + // first. + let mut staged_subsets = Vec::new(); + for committed_dir_idx in encoding_tree + .transcript_indices() + .into_iter() + .filter(|(dir, _)| *dir == dir_idx.0) + { + if committed_dir_idx.1.is_subset(&dir_idx.1) { + staged_subsets.push(committed_dir_idx); + } + } + + // Form a union of all the staged subsets. + let mut union_subsets = Idx::empty(); + for &(_, idx) in staged_subsets.iter() { + union_subsets = union_subsets.union(idx); + } + + // Check if the union equals to [dir_idx] — if yes, that means [dir_idx] + // can be revealed as it is fully covered by the staged committed subsets. + if union_subsets == dir_idx.1 { + staged_subsets.into_iter().for_each(|commited_dir_idx| { + self.encoding_proof_idxs.insert(commited_dir_idx.clone()); + }); + // If no, that means either + // (1) No subset found. + // (2) There are ranges in [dir_idx] that are not covered by + // the staged subsets, hence + // [dir_idx] cannot be revealed. + } else { + return Err(TranscriptProofBuilderError::new( + BuilderErrorKind::MissingCommitment, + format!( + "encoding commitment is missing for ranges in {} transcript", + direction + ), + )); + } } - - self.encoding_proof_idxs.insert(dir_idx); } TranscriptCommitmentKind::Hash { .. } => { - let Some(PlaintextHashSecret { - direction, - commitment, - blinder, - .. - }) = self.plaintext_hashes.get_by_transcript_idx(&idx) - else { - return Err(TranscriptProofBuilderError::new( - BuilderErrorKind::MissingCommitment, - format!( - "hash commitment is missing for ranges in {} transcript", - direction - ), - )); - }; - - 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, - )); + // Insert the rangeset [dir_idx] if it's in [plaintext_hashes], i.e. it's + // committed. + if self + .plaintext_hashes + .get_by_transcript_idx(&dir_idx) + .is_some() + { + self.hash_proof_idxs.insert(dir_idx); + } else { + // Check if there is any committed rangeset in [plaintext_hashes] that is a + // subset of [dir_idx] — if yes, stage them into a temporary collection + // first. + let mut staged_subsets = Vec::new(); + for committed_secret in self + .plaintext_hashes + .iter() + .filter(|secret| secret.direction == dir_idx.0) + { + if committed_secret.idx.is_subset(&dir_idx.1) { + staged_subsets + .push((committed_secret.direction, &committed_secret.idx)); + } + } + + // Form a union of all the staged subsets. + let mut union_subsets = Idx::empty(); + for &(_, idx) in staged_subsets.iter() { + union_subsets = union_subsets.union(idx); + } + + // Check if the union equals to [dir_idx] — if yes, that means [dir_idx] + // can be revealed as it is fully covered by the staged committed subsets. + if union_subsets == dir_idx.1 { + staged_subsets.into_iter().for_each(|commited_dir_idx| { + self.hash_proof_idxs + .insert((commited_dir_idx.0, commited_dir_idx.1.clone())); + }); + // If no, that means either + // (1) No subset found. + // (2) There are ranges in [dir_idx] that are not covered by + // the staged subsets, hence + // [dir_idx] cannot be revealed. + } else { + return Err(TranscriptProofBuilderError::new( + BuilderErrorKind::MissingCommitment, + format!( + "hash commitment is missing for ranges in {} transcript", + direction + ), + )); + } + } } } - Ok(self) } @@ -295,20 +539,51 @@ impl<'a> TranscriptProofBuilder<'a> { } /// Builds the transcript proof. - pub fn build(self) -> Result { + pub fn build(mut self) -> Result { let encoding_proof = if !self.encoding_proof_idxs.is_empty() { + self.encoding_proof_idxs.prune_subset(); + let encoding_tree = self.encoding_tree.expect("encoding tree is present"); + let proof = encoding_tree .proof(self.transcript, self.encoding_proof_idxs.iter()) .expect("subsequences were checked to be in tree"); + Some(proof) } else { None }; + let mut hash_proofs = Vec::new(); + if !self.hash_proof_idxs.is_empty() { + self.hash_proof_idxs.prune_subset(); + + for dir_idx in self.hash_proof_idxs.iter() { + let PlaintextHashSecret { + commitment, + blinder, + .. + } = self + .plaintext_hashes + .get_by_transcript_idx(dir_idx) + .expect("idx was checked to be committed"); + + let (_, data) = self + .transcript + .get(dir_idx.0, &dir_idx.1) + .expect("subsequence was checked to be in transcript") + .into_parts(); + + hash_proofs.push(PlaintextHashProof::new( + Blinded::new_with_blinder(data, blinder.clone()), + *commitment, + )); + } + } + Ok(TranscriptProof { encoding_proof, - hash_proofs: self.hash_proofs, + hash_proofs, }) } } @@ -355,22 +630,65 @@ impl fmt::Display for TranscriptProofBuilderError { } } +#[allow(clippy::single_range_in_vec_init)] #[cfg(test)] mod tests { + use rstest::rstest; use tlsn_data_fixtures::http::{request::GET_WITH_HEADER, response::OK_JSON}; + use utils::range::RangeSet; use crate::{ + attestation::FieldId, fixtures::{ attestation_fixture, encoder_seed, encoding_provider, request_fixture, ConnectionFixture, RequestFixture, }, - hash::Blake3, + hash::{Blake3, HashAlgId}, signing::SignatureAlgId, + transcript::TranscriptCommitConfigBuilder, }; use super::*; - #[test] + #[rstest] + fn test_verify_missing_encoding_commitment_root() { + let transcript = Transcript::new(GET_WITH_HEADER, OK_JSON); + let connection = ConnectionFixture::tlsnotary(transcript.length()); + + let RequestFixture { + mut request, + encoding_tree, + } = request_fixture( + transcript.clone(), + encoding_provider(GET_WITH_HEADER, OK_JSON), + connection.clone(), + Blake3::default(), + ); + + let index = Index::default(); + let mut builder = TranscriptProofBuilder::new(&transcript, Some(&encoding_tree), &index); + + builder.reveal_recv(&(0..transcript.len().1)).unwrap(); + + let transcript_proof = builder.build().unwrap(); + + request.encoding_commitment_root = None; + let attestation = attestation_fixture( + request, + connection, + SignatureAlgId::SECP256K1, + encoder_seed().to_vec(), + ); + + let provider = CryptoProvider::default(); + let err = transcript_proof + .verify_with_provider(&provider, &attestation.body) + .err() + .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], @@ -389,7 +707,7 @@ mod tests { assert!(matches!(err.kind, BuilderErrorKind::Index)); } - #[test] + #[rstest] fn test_reveal_missing_encoding_tree() { let transcript = Transcript::new( [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11], @@ -402,7 +720,7 @@ mod tests { assert!(matches!(err.kind, BuilderErrorKind::MissingCommitment)); } - #[test] + #[rstest] fn test_reveal_missing_encoding_commitment_range() { let transcript = Transcript::new(GET_WITH_HEADER, OK_JSON); let connection = ConnectionFixture::tlsnotary(transcript.length()); @@ -421,41 +739,216 @@ mod tests { assert!(matches!(err.kind, BuilderErrorKind::MissingCommitment)); } - #[test] - fn test_verify_missing_encoding_commitment() { + #[rstest] + fn test_reveal_missing_hash_commitment_range() { let transcript = Transcript::new(GET_WITH_HEADER, OK_JSON); - let connection = ConnectionFixture::tlsnotary(transcript.length()); - let RequestFixture { - mut request, - encoding_tree, - } = request_fixture( - transcript.clone(), - encoding_provider(GET_WITH_HEADER, OK_JSON), - connection.clone(), - Blake3::default(), - ); + let mut transcript_commitment_builder = TranscriptCommitConfigBuilder::new(&transcript); + transcript_commitment_builder.default_kind(TranscriptCommitmentKind::Hash { + alg: HashAlgId::SHA256, + }); + transcript_commitment_builder + .commit_recv(&(0..transcript.len().1)) + .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, + }); + + let err = builder.reveal_recv(&(0..11)).err().unwrap(); + assert!(matches!(err.kind, BuilderErrorKind::MissingCommitment)); + } + + #[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( + 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); - builder.reveal_recv(&(0..transcript.len().1)).unwrap(); + if success { + assert!(builder.reveal_recv(&reveal_recv_rangeset).is_ok()); + } else { + let err = builder.reveal_recv(&reveal_recv_rangeset).err().unwrap(); + assert!(matches!(err.kind, BuilderErrorKind::MissingCommitment)); + } - let transcript_proof = builder.build().unwrap(); + // 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).err().unwrap(); + assert!(matches!(err.kind, BuilderErrorKind::MissingCommitment)); + } + } - request.encoding_commitment_root = None; - let attestation = attestation_fixture( - request, - connection, - SignatureAlgId::SECP256K1, - encoder_seed().to_vec(), - ); + #[rstest] + #[case::no_subset_range( + vec![RangeSet::from([0..5]), RangeSet::from([4..40, 99..145]), RangeSet::from([40..59]), RangeSet::from([59..109])], + None + )] + #[case::contains_single_subset_range( + vec![RangeSet::from([0..40, 99..145]), RangeSet::from([11..20]), RangeSet::from([40..99])], + Some(vec![RangeSet::from([11..20])]) + )] + #[case::contains_multiple_subset_ranges( + vec![RangeSet::from([0..51, 69..145]), RangeSet::from([0..30, 99..145]), RangeSet::from([51..69])], + Some(vec![RangeSet::from([0..30, 99..145])]) + )] + // Also tests [Ord] implementation for [Idx]. + #[case::contains_multiple_subset_rangesets( + vec![RangeSet::from([0..7, 13..31, 49..145]), RangeSet::from([7..13, 31..49]), RangeSet::from([0..7, 14..30]), RangeSet::from([0..7, 13..29]), RangeSet::from([0..7, 13..31, 50..70]), RangeSet::from([0..7, 13..31])], + Some(vec![RangeSet::from([0..7, 14..30]), RangeSet::from([0..7, 13..29]), RangeSet::from([0..7, 13..31, 50..70]), RangeSet::from([0..7, 13..31])]) + )] + fn test_prune_proof_idxs_direction( + #[case] commit_recv_rangesets: Vec>, + #[case] expected_pruned_subsets: Option>>, + ) { + let transcript = Transcript::new(GET_WITH_HEADER, OK_JSON); - let provider = CryptoProvider::default(); - let err = transcript_proof - .verify_with_provider(&provider, &attestation.body) - .err() - .unwrap(); - assert!(matches!(err.kind, ErrorKind::Encoding)); + let mut transcript_commitment_builder = TranscriptCommitConfigBuilder::new(&transcript); + // Commit the rangesets. + for rangeset in &commit_recv_rangesets { + 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); + + // Reveal all committed rangesets via a superset rangeset. + builder.reveal_recv(&RangeSet::from([0..145])).unwrap(); + + let commit_recv_rangesets: HashSet<_> = commit_recv_rangesets.into_iter().collect(); + let initial_proof_idxs = builder + .encoding_proof_idxs + .iter() + .filter(|(dir, _)| *dir == Direction::Received) + .map(|(_, idx)| idx.clone().0) + .collect::>(); + + // Since all committed rangesets are revealed, before pruning, + // proof_idxs should contain all committed rangesets. + assert_eq!(commit_recv_rangesets, initial_proof_idxs); + + // Prune any subset. + builder.encoding_proof_idxs.prune_subset(); + + let pruned_proof_idxs = builder + .encoding_proof_idxs + .iter() + .filter(|(dir, _)| *dir == Direction::Received) + .map(|(_, idx)| idx.clone().0) + .collect::>(); + + if let Some(subsets) = expected_pruned_subsets { + let pruned_subsets: HashSet<_> = commit_recv_rangesets + .difference(&pruned_proof_idxs) + .collect(); + assert_eq!(pruned_subsets, subsets.iter().collect()); + } else { + assert_eq!(commit_recv_rangesets, pruned_proof_idxs); + }; } } diff --git a/crates/examples/attestation/present.rs b/crates/examples/attestation/present.rs index a798f593b8..e5878795de 100644 --- a/crates/examples/attestation/present.rs +++ b/crates/examples/attestation/present.rs @@ -8,6 +8,7 @@ use tlsn_examples::ExampleType; use tlsn_formats::http::HttpTranscript; use clap::Parser; +use utils::range::{ToRangeSet, Union}; #[derive(Parser, Debug)] #[command(version, about, long_about = None)] @@ -40,12 +41,12 @@ async fn create_presentation(example_type: &ExampleType) -> Result<(), Box Result<(), Box. + // end: . + builder.reveal_recv( + &(response + .headers + .first() + .unwrap() + .to_range_set() + .min() + .unwrap() + ..response + .headers + .last() + .unwrap() + .to_range_set() + .end() + .unwrap()), + )?; + match content { tlsn_formats::http::BodyContent::Json(json) => { - // For experimentation, reveal the entire response or just a selection + // For experimentation, reveal the entire committed response or just a selection + // of committed parts. let reveal_all = false; if reveal_all { builder.reveal_recv(response)?; diff --git a/crates/examples/attestation/prove.rs b/crates/examples/attestation/prove.rs index 36966f8bf2..aa217446f7 100644 --- a/crates/examples/attestation/prove.rs +++ b/crates/examples/attestation/prove.rs @@ -183,6 +183,9 @@ async fn notarize( // Commit to the transcript. let mut builder = TranscriptCommitConfig::builder(prover.transcript()); + // This commits to various parts of the transcript separately (e.g. request + // headers, response headers, response body and more). See https://docs.tlsnotary.org//protocol/commit_strategy.html + // for other strategies that can be used to generate commitments. DefaultHttpCommitter::default().commit_transcript(&mut builder, &transcript)?; prover.transcript_commit(builder.build()?);