mirror of
https://github.com/tlsnotary/tlsn.git
synced 2026-01-08 21:08:04 -05:00
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:
@@ -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" }
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -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:
|
||||
//
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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"] }
|
||||
|
||||
@@ -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
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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`].
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)]
|
||||
|
||||
@@ -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 }
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user