Created
November 14, 2025 14:45
-
-
Save rsoury/7cfa8ffb06809c8e8d0df4f8ca074191 to your computer and use it in GitHub Desktop.
TLSN & Verity Benchmarks for zkTLS Demo Day
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // 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(()) | |
| } | |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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