Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Select an option

  • Save rsoury/7cfa8ffb06809c8e8d0df4f8ca074191 to your computer and use it in GitHub Desktop.

Select an option

Save rsoury/7cfa8ffb06809c8e8d0df4f8ca074191 to your computer and use it in GitHub Desktop.
TLSN & Verity Benchmarks for zkTLS Demo Day
// This example demonstrates how to use the Prover to acquire an attestation for
// an HTTP request sent to jsonplaceholder.typicode.com. The attestation and secrets are
// saved to disk.
use http_body_util::Empty;
use hyper::{Request, StatusCode, body::Bytes};
use hyper_util::rt::TokioIo;
use tokio::{
io::{AsyncRead, AsyncWrite},
sync::oneshot::{self, Receiver, Sender},
time::{sleep, Duration},
};
use tokio_util::compat::{FuturesAsyncReadCompatExt, TokioAsyncReadCompatExt};
use tracing::info;
use std::sync::{Arc, Mutex};
use tlsn::{
attestation::{
Attestation, AttestationConfig, CryptoProvider, Secrets,
request::{Request as AttestationRequest, RequestConfig},
signing::Secp256k1Signer,
},
config::{ProtocolConfig, ProtocolConfigValidator},
connection::{ConnectionInfo, HandshakeData, ServerName, TranscriptLength},
prover::{ProveConfig, Prover, ProverConfig, ProverOutput, TlsConfig, state::Committed},
transcript::{ContentType, TranscriptCommitConfig},
verifier::{Verifier, VerifierConfig, VerifierOutput, VerifyConfig},
};
use tlsn_formats::http::{DefaultHttpCommitter, HttpCommit, HttpTranscript};
pub const MAX_SENT_DATA: usize = 1024;
pub const MAX_RECV_DATA: usize = 4096;
const SERVER_DOMAIN: &str = "jsonplaceholder.typicode.com";
// Setting of the application server.
const USER_AGENT: &str = "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36";
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
tracing_subscriber::fmt::init();
let uri: &str = "/todos/1";
// Shared state for tracking running average
let stats = Arc::new(Mutex::new(Stats {
total_proofs: 0,
total_time: 0.0,
}));
// Run batches of 4 concurrent proofs every 5 seconds, staggered 1 second apart
let mut batch_number = 0;
let mut next_batch_start = std::time::Instant::now();
loop {
batch_number += 1;
let batch_start = next_batch_start;
println!("\n=== Starting batch {} ===", batch_number);
let mut handles = Vec::new();
// Start 4 proofs, staggered by 1 second each
for i in 0..4 {
let stats_clone = Arc::clone(&stats);
let uri = uri.to_string();
let proof_id = (batch_number * 4 + i + 1) as usize;
let handle = tokio::spawn(async move {
// Calculate when this proof should start relative to batch start
let delay = Duration::from_secs(i as u64);
sleep(delay).await;
// Run the proof
match run_single_proof(&uri, stats_clone, proof_id).await {
Ok(_) => {}
Err(e) => eprintln!("Proof {} failed: {}", proof_id, e),
}
});
handles.push(handle);
}
// Wait for all proofs in this batch to complete
for handle in handles {
let _ = handle.await;
}
// Calculate next batch start time (5 seconds after this batch started)
next_batch_start = batch_start + Duration::from_secs(5);
let now = std::time::Instant::now();
// Wait until it's time for the next batch
if next_batch_start > now {
sleep(next_batch_start - now).await;
}
}
}
struct Stats {
total_proofs: usize,
total_time: f64,
}
async fn run_single_proof(
uri: &str,
stats: Arc<Mutex<Stats>>,
proof_id: usize,
) -> Result<(), Box<dyn std::error::Error>> {
// Create a new notary connection for this proof
let (notary_socket, prover_socket) = tokio::io::duplex(1 << 23);
let (request_tx, request_rx) = oneshot::channel();
let (attestation_tx, attestation_rx) = oneshot::channel();
tokio::spawn(async move {
notary(notary_socket, request_rx, attestation_tx)
.await
.unwrap()
});
let server_host: &str = "jsonplaceholder.typicode.com";
let server_port: u16 = 443;
let tls_config = TlsConfig::builder().build().unwrap();
// Set up protocol configuration for prover.
let mut prover_config_builder = ProverConfig::builder();
prover_config_builder
.server_name(ServerName::Dns(SERVER_DOMAIN.try_into().unwrap()))
.tls_config(tls_config)
.protocol_config(
ProtocolConfig::builder()
// We must configure the amount of data we expect to exchange beforehand, which will
// be preprocessed prior to the connection. Reducing these limits will improve
// performance.
.max_sent_data(MAX_SENT_DATA)
.max_recv_data(MAX_RECV_DATA)
.build()?,
);
let prover_config = prover_config_builder.build()?;
// Create a new prover and perform necessary setup.
let prover = Prover::new(prover_config).setup(prover_socket.compat()).await?;
// ? Start timing benchmark before connection to server
let start_time = std::time::Instant::now();
// Open a TCP connection to the server.
let client_socket = tokio::net::TcpStream::connect((server_host, server_port)).await?;
// Bind the prover to the server connection.
// The returned `mpc_tls_connection` is an MPC TLS connection to the server: all
// data written to/read from it will be encrypted/decrypted using MPC with
// the notary.
let (mpc_tls_connection, prover_fut) = prover.connect(client_socket.compat()).await?;
let mpc_tls_connection = TokioIo::new(mpc_tls_connection.compat());
// Spawn the prover task to be run concurrently in the background.
let prover_task = tokio::spawn(prover_fut);
// Attach the hyper HTTP client to the connection.
let (mut request_sender, connection) =
hyper::client::conn::http1::handshake(mpc_tls_connection).await?;
// Spawn the HTTP task to be run concurrently in the background.
tokio::spawn(connection);
// Build a simple HTTP request with common headers.
let request = Request::builder()
.version(hyper::Version::HTTP_10) // Attempt to let the server know we don't want a chunked response
.uri(uri)
.header("Host", SERVER_DOMAIN)
.header("Accept", "*/*")
// Using "identity" instructs the Server not to use compression for its HTTP response.
// TLSNotary tooling does not support compression.
.header("Accept-Encoding", "identity")
.header("Connection", "close")
.header("User-Agent", USER_AGENT)
.body(Empty::<Bytes>::new())?;
info!("[Proof {}] Starting an MPC TLS connection with the server", proof_id);
// Send the request to the server and wait for the response.
let response = request_sender.send_request(request).await?;
info!("[Proof {}] Got a response from the server: {}", proof_id, response.status());
assert!(response.status() == StatusCode::OK);
// The prover task should be done now, so we can await it.
let prover = prover_task.await??;
// Parse the HTTP transcript.
let transcript = HttpTranscript::parse(prover.transcript())?;
// 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)?;
let transcript_commit = builder.build()?;
// Build an attestation request.
let mut builder = RequestConfig::builder();
builder.transcript_commit(transcript_commit);
let request_config = builder.build()?;
let (_attestation, _secrets) = notarize(prover, &request_config, request_tx, attestation_rx).await?;
// End timing benchmark
let elapsed = start_time.elapsed();
let elapsed_secs = elapsed.as_secs_f64();
// Update running statistics
let mut stats_guard = stats.lock().unwrap();
stats_guard.total_proofs += 1;
stats_guard.total_time += elapsed_secs;
let avg_time = stats_guard.total_time / stats_guard.total_proofs as f64;
let total_proofs = stats_guard.total_proofs;
drop(stats_guard);
println!(
"[Proof {}] Completed in {:.2}ms ({:.3}s) | Running average: {:.2}ms ({:.3}s) | Total proofs: {}",
proof_id,
elapsed_secs * 1000.0,
elapsed_secs,
avg_time * 1000.0,
avg_time,
total_proofs
);
Ok(())
}
async fn notarize(
mut prover: Prover<Committed>,
config: &RequestConfig,
request_tx: Sender<AttestationRequest>,
attestation_rx: Receiver<Attestation>,
) -> Result<(Attestation, Secrets), Box<dyn std::error::Error>> {
let mut builder = ProveConfig::builder(prover.transcript());
if let Some(config) = config.transcript_commit() {
builder.transcript_commit(config.clone());
}
let disclosure_config = builder.build()?;
let ProverOutput {
transcript_commitments,
transcript_secrets,
..
} = prover.prove(&disclosure_config).await?;
// Build an attestation request.
let mut builder = AttestationRequest::builder(config);
let transcript = prover.transcript().clone();
let tls_transcript = prover.tls_transcript().clone();
prover.close().await?;
builder
.server_name(ServerName::Dns(SERVER_DOMAIN.try_into().unwrap()))
.handshake_data(HandshakeData {
certs: tls_transcript
.server_cert_chain()
.expect("server cert chain is present")
.to_vec(),
sig: tls_transcript
.server_signature()
.expect("server signature is present")
.clone(),
binding: tls_transcript.certificate_binding().clone(),
})
.transcript(transcript)
.transcript_commitments(transcript_secrets, transcript_commitments);
let (request, secrets) = builder.build(&CryptoProvider::default())?;
// Send attestation request to notary.
request_tx
.send(request.clone())
.map_err(|_| "notary is not receiving attestation request".to_string())?;
// Receive attestation from notary.
let attestation = attestation_rx
.await
.map_err(|err| format!("notary did not respond with attestation: {err}"))?;
// Check the attestation is consistent with the Prover's view.
request.validate(&attestation)?;
Ok((attestation, secrets))
}
async fn notary<S: AsyncWrite + AsyncRead + Send + Sync + Unpin + 'static>(
socket: S,
request_rx: Receiver<AttestationRequest>,
attestation_tx: Sender<Attestation>,
) -> Result<(), Box<dyn std::error::Error>> {
// Set up Verifier.
let config_validator = ProtocolConfigValidator::builder()
.max_sent_data(MAX_SENT_DATA)
.max_recv_data(MAX_RECV_DATA)
.build()
.unwrap();
// Create a root certificate store with the server-fixture's self-signed
// certificate. This is only required for offline testing with the
// server-fixture.
let verifier_config = VerifierConfig::builder()
.protocol_config_validator(config_validator)
.build()
.unwrap();
let mut verifier = Verifier::new(verifier_config)
.setup(socket.compat())
.await?
.run()
.await?;
let VerifierOutput {
transcript_commitments,
..
} = verifier.verify(&VerifyConfig::default()).await?;
let tls_transcript = verifier.tls_transcript().clone();
verifier.close().await?;
let sent_len = tls_transcript
.sent()
.iter()
.filter_map(|record| {
if let ContentType::ApplicationData = record.typ {
Some(record.ciphertext.len())
} else {
None
}
})
.sum::<usize>();
let recv_len = tls_transcript
.recv()
.iter()
.filter_map(|record| {
if let ContentType::ApplicationData = record.typ {
Some(record.ciphertext.len())
} else {
None
}
})
.sum::<usize>();
// Receive attestation request from prover.
let request = request_rx.await?;
// Load a dummy signing key.
let signing_key = k256::ecdsa::SigningKey::from_bytes(&[1u8; 32].into())?;
let signer = Box::new(Secp256k1Signer::new(&signing_key.to_bytes())?);
let mut provider = CryptoProvider::default();
provider.signer.set_signer(signer);
// Build an attestation.
let mut att_config_builder = AttestationConfig::builder();
att_config_builder.supported_signature_algs(Vec::from_iter(provider.signer.supported_algs()));
let att_config = att_config_builder.build()?;
let mut builder = Attestation::builder(&att_config).accept_request(request)?;
builder
.connection_info(ConnectionInfo {
time: tls_transcript.time(),
version: (*tls_transcript.version()),
transcript_length: TranscriptLength {
sent: sent_len as u32,
received: recv_len as u32,
},
})
.server_ephemeral_key(tls_transcript.server_ephemeral_key().clone())
.transcript_commitments(transcript_commitments);
let attestation = builder.build(&provider)?;
// Send attestation to prover.
attestation_tx
.send(attestation)
.map_err(|_| "prover is not receiving attestation".to_string())?;
Ok(())
}
use std::sync::{Arc, Mutex};
use tokio::time::{sleep, Duration};
use verity_client::client::{VerityClient, VerityClientConfig};
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
tracing_subscriber::fmt::init();
let uri: &str = "https://jsonplaceholder.typicode.com/todos/1";
// Shared state for tracking running average
let stats = Arc::new(Mutex::new(Stats {
total_proofs: 0,
total_time: 0.0,
}));
// Run batches of 4 concurrent proofs every 5 seconds, staggered 1 second apart
let mut batch_number = 0;
let mut next_batch_start = std::time::Instant::now();
loop {
batch_number += 1;
let batch_start = next_batch_start;
println!("\n=== Starting batch {} ===", batch_number);
let mut handles = Vec::new();
// Start 4 proofs, staggered by 1 second each
for i in 0..4 {
let stats_clone = Arc::clone(&stats);
let uri = uri.to_string();
let proof_id = (batch_number * 4 + i + 1) as usize;
let handle = tokio::spawn(async move {
// Calculate when this proof should start relative to batch start
let delay = Duration::from_secs(i as u64);
sleep(delay).await;
// Run the proof
match run_single_proof(&uri, stats_clone, proof_id).await {
Ok(_) => {}
Err(e) => eprintln!("Proof {} failed: {}", proof_id, e),
}
});
handles.push(handle);
}
// Wait for all proofs in this batch to complete
for handle in handles {
let _ = handle.await;
}
// Calculate next batch start time (5 seconds after this batch started)
next_batch_start = batch_start + Duration::from_secs(5);
let now = std::time::Instant::now();
// Wait until it's time for the next batch
if next_batch_start > now {
sleep(next_batch_start - now).await;
}
}
}
struct Stats {
total_proofs: usize,
total_time: f64,
}
async fn run_single_proof(
uri: &str,
stats: Arc<Mutex<Stats>>,
proof_id: usize,
) -> Result<(), Box<dyn std::error::Error>> {
let config = VerityClientConfig {
prover_url: String::from("http://127.0.0.1:8080"),
proof_timeout: Some(std::time::Duration::from_millis(30000)),
};
let verity_client = VerityClient::new(config);
// Start timing benchmark before request via VerityClient
let start_time = std::time::Instant::now();
// Make request and get proof
let response = verity_client
.get(uri)
.send()
.await?;
// End timing benchmark after proof is acquired
let elapsed = start_time.elapsed();
let elapsed_secs = elapsed.as_secs_f64();
// Save the proof to disk (same destination pattern as original example)
let proof_path = format!("example-jsonplaceholder-proof-{}.json", proof_id);
tokio::fs::write(&proof_path, &response.proof).await?;
// Update running statistics
let mut stats_guard = stats.lock().unwrap();
stats_guard.total_proofs += 1;
stats_guard.total_time += elapsed_secs;
let avg_time = stats_guard.total_time / stats_guard.total_proofs as f64;
let total_proofs = stats_guard.total_proofs;
drop(stats_guard);
println!(
"[Proof {}] Completed in {:.2}ms ({:.3}s) | Running average: {:.2}ms ({:.3}s) | Total proofs: {} | Proof saved to {}",
proof_id,
elapsed_secs * 1000.0,
elapsed_secs,
avg_time * 1000.0,
avg_time,
total_proofs,
proof_path
);
Ok(())
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment