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 <themighty1@users.noreply.github.com>

* 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 <themighty1@users.noreply.github.com>
Co-authored-by: sinu <65924192+sinui0@users.noreply.github.com>
This commit is contained in:
yuroitaki
2025-03-20 22:55:13 +08:00
committed by GitHub
parent c1b3d64d5d
commit 4cdd1395e8
12 changed files with 402 additions and 122 deletions

View File

@@ -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" }

View File

@@ -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 }

View File

@@ -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:
//

View File

@@ -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 }

View File

@@ -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;

View File

@@ -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"] }

View File

@@ -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<T> {
items: Vec<T>,
// Lookup by field id.
field_ids: HashMap<FieldId, usize>,
// Lookup by transcript index.
transcript_idxs: HashMap<Idx, usize>,
// 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<T> Default for Index<T> {
@@ -26,6 +31,8 @@ impl<T> Default for Index<T> {
items: Default::default(),
field_ids: Default::default(),
transcript_idxs: Default::default(),
sent: Default::default(),
recv: Default::default(),
}
}
}
@@ -60,19 +67,28 @@ impl<T> From<Index<T>> for Vec<T> {
impl<T> Index<T> {
pub(crate) fn new<F>(items: Vec<T>, 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<T> Index<T> {
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<Item = &(Direction, Idx)> {
self.transcript_idxs.keys()
}
}
impl From<Vec<Field<PlaintextHash>>> for Index<Field<PlaintextHash>> {
fn from(items: Vec<Field<PlaintextHash>>) -> Self {
Self::new(items, |field: &Field<PlaintextHash>| {
(&field.id, &field.data.idx)
(&field.id, field.data.direction, &field.data.idx)
})
}
}
@@ -100,26 +129,29 @@ impl From<Vec<Field<PlaintextHash>>> for Index<Field<PlaintextHash>> {
impl From<Vec<PlaintextHashSecret>> for Index<PlaintextHashSecret> {
fn from(items: Vec<PlaintextHashSecret>) -> 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<Vec<Stub>> for Index<Stub> {
fn from(items: Vec<Stub>) -> 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
);
}
}

View File

@@ -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<usize> {
&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`].

View File

@@ -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 {

View File

@@ -61,6 +61,10 @@ pub struct EncodingTree {
/// Mapping between the index of a leaf and the transcript index it
/// corresponds to.
idxs: BiMap<usize, (Direction, Idx)>,
/// 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<Item = &(Direction, Idx)> {
self.idxs.right_values()
}
}
#[cfg(test)]

View File

@@ -39,6 +39,7 @@ pub(crate) struct PlaintextHashProof {
}
impl PlaintextHashProof {
#[allow(unused)]
pub(crate) fn new(data: Blinded<Vec<u8>>, commitment: FieldId) -> Self {
Self { data, commitment }
}

View File

@@ -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<PlaintextHashProofError> 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<PlaintextHashSecret>,
encoding_proof_idxs: HashSet<(Direction, Idx)>,
hash_proofs: Vec<PlaintextHashProof>,
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<TranscriptProof, TranscriptProofBuilderError> {
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<RangeSet<usize>>,
#[case] reveal_recv_rangeset: RangeSet<usize>,
#[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<PlaintextHashSecret> = transcripts_commitment_config
.iter_hash()
.map(|(&(direction, ref idx), _)| PlaintextHashSecret {
direction,
idx: idx.clone(),
commitment: FieldId::default(),
blinder: rand::random(),
})
.collect::<Vec<_>>()
.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));
}
}
}