Livy TLSNotary Docs
Examples

tlsn

Main tee_dev example for WebSocket notarization with TDX attestation.

Main Example

The main runnable example is crates/examples/tee/ws.rs on the tee_dev branch.

It creates a WebSocket notarization session with tee_attestation: Some(true), runs a TLSN request against a configurable HTTPS target, receives the normal TLSN attestation and secrets, then receives and verifies the extra TDX attestation payload.

The underlying TDX session path is documented in the TDX Notary runbook. For the broader Livy verifiable-compute model, refer to the Livy example model.

/notarize Example: tee_ws

  • Purpose: run the WebSocket notarization flow with TEE attestation against the deployed public notary
  • Flow: /session + /notarize WebSocket notarization flow
  • Repo path: crates/examples/tee/ws.rs
  • Branch: tee_dev
  • Prerequisites:
    • Rust toolchain
    • cloned livylabs/tlsn repo
    • public notary reachable at https://tlsn.livylabs.xyz
    • public CA trust available locally, which is why USE_FIXTURE_CA=false

Public Notary Run

NOTARY_SCHEME=https \
NOTARY_HOST=tlsn.livylabs.xyz \
NOTARY_PORT=443 \
TARGET_HOST=api.weather.gov \
TARGET_PORT=443 \
TARGET_SERVER_NAME=api.weather.gov \
TARGET_URI=/points/37.7749,-122.4194 \
USE_FIXTURE_CA=false \
MAX_SENT_DATA=4096 \
MAX_RECV_DATA=16384 \
cargo run -p tlsn-examples --example tee_ws

What It Does

  • creates a notary session over HTTPS
  • sets teeAttestation: true on the session request
  • upgrades to WebSocket notarization
  • performs an MPC-TLS fetch against api.weather.gov
  • commits the full sent and received transcript
  • calls prover.notarize_with_tee(...)
  • receives a TDX attestation payload after the TLSN attestation
  • decodes the TDX quote JSON
  • extracts the TDX report data
  • verifies the TDX quote

Configuration

Env varDefaultPurpose
NOTARY_SCHEMEhttpNotary scheme, either http or https.
NOTARY_HOST127.0.0.1Notary host.
NOTARY_PORT7047Notary port.
TARGET_HOST127.0.0.1Target HTTPS server host.
TARGET_PORT4000Target HTTPS server port.
TARGET_SERVER_NAMEtest-server.ioTLS server name used by the prover.
TARGET_URI/formats/jsonRequest URI sent to the target server.
MAX_SENT_DATA8192TLSN maximum sent data.
MAX_RECV_DATA16384TLSN maximum received data.
USE_FIXTURE_CAtrueUse the repository fixture CA for local target testing.

Expected Output

  • HTTP status line from the target request
  • TLSN attestation received from notary
  • pretty-printed TDX attestation JSON
  • TDX attestation verified successfully
  • extracted TDX report data
  • written artifacts:
    • ws-test.attestation.tlsn
    • ws-test.secrets.tlsn

Notes

  • The example sends teeAttestation: true to /session; the server stores that flag and emits the extra TDX attestation during /notarize.
  • The example targets the public HTTPS deployment, not a fixture CA or local test server.

Source

Source: crates/examples/tee/ws.rs

use std::env;

use async_tungstenite::{
    tokio::{connect_async, connect_async_with_tls_connector_and_config},
    tungstenite::protocol::WebSocketConfig,
};
use attestation_verifier::{
    decode_tdx_quote_json, extract_report_data_hex, verify_tdx_quote_json,
};
use futures::{AsyncRead, AsyncWrite};
use http_body_util::{BodyExt as _, Empty, Full};
use hyper::{body::Bytes, Request, StatusCode};
use hyper_tls::HttpsConnector;
use hyper_util::{
    client::legacy::{connect::HttpConnector, Builder},
    rt::{TokioExecutor, TokioIo},
};
use notary_common::{ClientType, NotarizationSessionRequest, NotarizationSessionResponse};
use tls_core::verify::WebPkiVerifier;
use tls_server_fixture::CA_CERT_DER;
use tlsn_core::{request::RequestConfig, CryptoProvider};
use tlsn_prover::{Prover, ProverConfig};
use tokio_native_tls::native_tls::TlsConnector as NativeTlsConnector;
use tokio_util::compat::{FuturesAsyncReadCompatExt, TokioAsyncReadCompatExt};
use tracing::info;
use ws_stream_tungstenite::WsStream;

const USER_AGENT: &str = "TLSNotary-WS-TDX-Test/1.0";
const DEFAULT_MAX_SENT_DATA: usize = 1 << 13;
const DEFAULT_MAX_RECV_DATA: usize = 1 << 14;

trait NotaryIo: AsyncRead + AsyncWrite + Send + Unpin {}

impl<T> NotaryIo for T where T: AsyncRead + AsyncWrite + Send + Unpin {}

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    tracing_subscriber::fmt::init();

    let notary_scheme = env::var("NOTARY_SCHEME").unwrap_or("http".into());
    let notary_host: String = env::var("NOTARY_HOST").unwrap_or("127.0.0.1".into());
    let notary_port: u16 = env::var("NOTARY_PORT")
        .ok()
        .and_then(|port| port.parse().ok())
        .unwrap_or(7047);

    let target_host: String = env::var("TARGET_HOST").unwrap_or("127.0.0.1".into());
    let target_port: u16 = env::var("TARGET_PORT")
        .ok()
        .and_then(|port| port.parse().ok())
        .unwrap_or(4000);
    let target_server_name: String =
        env::var("TARGET_SERVER_NAME").unwrap_or("test-server.io".into());
    let target_uri: String = env::var("TARGET_URI").unwrap_or("/formats/json".into());
    let max_sent_data: usize = env::var("MAX_SENT_DATA")
        .ok()
        .and_then(|value| value.parse().ok())
        .unwrap_or(DEFAULT_MAX_SENT_DATA);
    let max_recv_data: usize = env::var("MAX_RECV_DATA")
        .ok()
        .and_then(|value| value.parse().ok())
        .unwrap_or(DEFAULT_MAX_RECV_DATA);
    let use_fixture_ca = env::var("USE_FIXTURE_CA")
        .map(|value| value == "true" || value == "1")
        .unwrap_or(true);

    let session_id =
        create_websocket_session(&notary_scheme, &notary_host, notary_port, max_sent_data, max_recv_data)
            .await?;
    info!(%session_id, "Created websocket notarization session");

    let notary_ws_socket =
        connect_notary_websocket(&notary_scheme, &notary_host, notary_port, &session_id).await?;

    let crypto_provider = build_target_crypto_provider(use_fixture_ca)?;

    let prover_config = ProverConfig::builder()
        .server_name(target_server_name.as_str())
        .protocol_config(
            tlsn_common::config::ProtocolConfig::builder()
                .max_sent_data(max_sent_data)
                .max_recv_data(max_recv_data)
                .build()?,
        )
        .crypto_provider(crypto_provider)
        .build()?;

    let prover = Prover::new(prover_config).setup(notary_ws_socket).await?;

    let client_socket = tokio::net::TcpStream::connect((target_host.as_str(), target_port)).await?;
    let (mpc_tls_connection, prover_fut) = prover.connect(client_socket.compat()).await?;
    let mpc_tls_connection = TokioIo::new(mpc_tls_connection.compat());

    let prover_task = tokio::spawn(prover_fut);

    let (mut request_sender, connection) =
        hyper::client::conn::http1::handshake(mpc_tls_connection).await?;
    tokio::spawn(connection);

    let request = Request::builder()
        .uri(target_uri)
        .header("Host", target_server_name.as_str())
        .header("Accept", "*/*")
        .header("Accept-Encoding", "identity")
        .header("Connection", "close")
        .header("User-Agent", USER_AGENT)
        .body(Empty::<Bytes>::new())?;

    let response = request_sender.send_request(request).await?;
    println!("Application server response: {}", response.status());
    assert_eq!(response.status(), StatusCode::OK);
    let _ = response.into_body().collect().await?;

    let mut prover = prover_task.await??;

    let (sent_len, recv_len) = prover.transcript().len();
    let mut builder = tlsn_core::transcript::TranscriptCommitConfig::builder(prover.transcript());
    builder.commit_sent(&(0..sent_len))?;
    builder.commit_recv(&(0..recv_len))?;

    let mut request_builder = RequestConfig::builder();
    request_builder.transcript_commit(builder.build()?);
    let request_config = request_builder.build()?;

    #[allow(deprecated)]
    let (attestation, secrets, tee_attestation) = prover.notarize_with_tee(&request_config).await?;

    println!("TLSN attestation received from notary");
    println!(
        "TDX attestation packet:\n{}",
        serde_json::to_string_pretty(&tee_attestation)?
    );

    let tdx_attestation_json = serde_json::to_string(&tee_attestation.tdx_attestation)?;
    let quote = decode_tdx_quote_json(&tdx_attestation_json)?;
    let report_data_hex = extract_report_data_hex(&quote)?;
    verify_tdx_quote_json(&tdx_attestation_json).await?;
    println!("TDX attestation verified successfully");
    println!("Extracted TDX report data: {report_data_hex}");

    tokio::fs::write("ws-test.attestation.tlsn", bincode::serialize(&attestation)?).await?;
    tokio::fs::write("ws-test.secrets.tlsn", bincode::serialize(&secrets)?).await?;

    prover.close().await?;

    Ok(())
}

fn build_target_crypto_provider(
    use_fixture_ca: bool,
) -> Result<CryptoProvider, Box<dyn std::error::Error>> {
    if !use_fixture_ca {
        return Ok(CryptoProvider::default());
    }

    let mut root_store = tls_core::anchors::RootCertStore::empty();
    root_store.add(&tls_core::key::Certificate(CA_CERT_DER.to_vec()))?;

    Ok(CryptoProvider {
        cert: WebPkiVerifier::new(root_store, None),
        ..Default::default()
    })
}

async fn connect_notary_websocket(
    notary_scheme: &str,
    notary_host: &str,
    notary_port: u16,
    session_id: &str,
) -> Result<Box<dyn NotaryIo>, Box<dyn std::error::Error>> {
    let ws_scheme = websocket_scheme(notary_scheme)?;
    let ws_url = format!(
        "{ws_scheme}://{notary_host}:{notary_port}/notarize?sessionId={session_id}"
    );

    if notary_scheme == "https" {
        let tls_connector = NativeTlsConnector::builder().build()?;
        let (notary_ws_stream, _) = connect_async_with_tls_connector_and_config(
            ws_url,
            Some(tokio_native_tls::TlsConnector::from(tls_connector)),
            Some(WebSocketConfig::default()),
        )
        .await?;

        Ok(Box::new(WsStream::new(notary_ws_stream)))
    } else {
        let (notary_ws_stream, _) = connect_async(ws_url).await?;
        Ok(Box::new(WsStream::new(notary_ws_stream)))
    }
}

async fn create_websocket_session(
    notary_scheme: &str,
    notary_host: &str,
    notary_port: u16,
    max_sent_data: usize,
    max_recv_data: usize,
) -> Result<String, Box<dyn std::error::Error>> {
    let payload = serde_json::to_vec(&NotarizationSessionRequest {
        client_type: ClientType::Websocket,
        max_sent_data: Some(max_sent_data),
        max_recv_data: Some(max_recv_data),
        tee_attestation: Some(true),
    })?;
    let session_uri = format!("{notary_scheme}://{notary_host}:{notary_port}/session");

    let response = if notary_scheme == "https" {
        let tls_connector = NativeTlsConnector::builder().build()?;
        let mut http_connector = HttpConnector::new();
        http_connector.enforce_http(false);
        let https_connector =
            HttpsConnector::from((http_connector, tokio_native_tls::TlsConnector::from(tls_connector)));
        let client = Builder::new(TokioExecutor::new()).build(https_connector);

        client
            .request(
                Request::builder()
                    .uri(session_uri)
                    .method("POST")
                    .header("Host", notary_host)
                    .header("Content-Type", "application/json")
                    .body(Full::new(Bytes::from(payload)))?,
            )
            .await?
    } else {
        let notary_socket = tokio::net::TcpStream::connect((notary_host, notary_port)).await?;
        let (mut request_sender, connection) =
            hyper::client::conn::http1::handshake(TokioIo::new(notary_socket)).await?;
        tokio::spawn(async move {
            let _ = connection.await;
        });

        request_sender
            .send_request(
                Request::builder()
                    .uri(session_uri)
                    .method("POST")
                    .header("Host", notary_host)
                    .header("Content-Type", "application/json")
                    .body(Full::new(Bytes::from(payload)))?,
            )
            .await?
    };

    if response.status() != StatusCode::OK {
        return Err(format!("Session creation failed with status {}", response.status()).into());
    }

    let payload = response.into_body().collect().await?.to_bytes();
    let session = serde_json::from_slice::<NotarizationSessionResponse>(&payload)?;

    Ok(session.session_id)
}

fn websocket_scheme(notary_scheme: &str) -> Result<&'static str, Box<dyn std::error::Error>> {
    match notary_scheme {
        "http" => Ok("ws"),
        "https" => Ok("wss"),
        _ => Err(format!("Unsupported NOTARY_SCHEME: {notary_scheme}").into()),
    }
}

Local Fixture Run

Run a local HTTPS target fixture in another terminal:

PORT=4000 cargo run --release --bin tlsn-server-fixture

Then run the WebSocket TDX example against that fixture:

NOTARY_SCHEME=http \
NOTARY_HOST=127.0.0.1 \
NOTARY_PORT=7047 \
TARGET_HOST=127.0.0.1 \
TARGET_PORT=4000 \
TARGET_SERVER_NAME=test-server.io \
TARGET_URI=/formats/json \
USE_FIXTURE_CA=true \
cargo run --release --example tee_ws

For another HTTPS target, keep the Notary settings and replace the target settings:

NOTARY_SCHEME=http \
NOTARY_HOST=<notary-host> \
NOTARY_PORT=7047 \
TARGET_HOST=<target-host> \
TARGET_PORT=<target-port> \
TARGET_SERVER_NAME=<tls-server-name> \
TARGET_URI=/formats/json \
USE_FIXTURE_CA=false \
cargo run --release --example tee_ws

The example writes:

  • ws-test.attestation.tlsn
  • ws-test.secrets.tlsn

It also prints the TDX attestation JSON, verifies the quote, and prints the extracted report data.

/prove Example Surface

The deployed Livy /api/v1/prove endpoint starts a proof job through the notary-tee proxy. The proxy runs the TLSN prover internally, talks to the configured upstream Notary, fetches the requested HTTPS URL, stores the TLSN artifacts, and returns a job_id.

Source:

  • API implementation: crates/notary/server/notary-tee/proxy.rs
  • Runnable wrapper: crates/notary/server/notary-tee/examples/proxy.rs

Start A Proof Job

curl -sS -X POST "https://tlsn.livylabs.xyz/api/v1/prove" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://data-api.binance.vision/api/v3/exchangeInfo?symbol=ETHUSDC"
  }'

Example response shape:

{
  "job_id": "3fc3e18cccfb167f6ff9bafddbc6ccd4",
  "attestation_hash_hex": "...",
  "secrets_hash_hex": "...",
  "tdx_attestation": {
    "...": "..."
  }
}

Download The TLSN Artifacts

Use the returned job_id to download the generated TLSN attestation and secrets:

JOB_ID="3fc3e18cccfb167f6ff9bafddbc6ccd4"

curl -fL -sS "https://tlsn.livylabs.xyz/api/v1/jobs/${JOB_ID}/attestation" \
  --output "attestation-${JOB_ID}.tlsn"

curl -fL -sS "https://tlsn.livylabs.xyz/api/v1/jobs/${JOB_ID}/secrets" \
  --output "secrets-${JOB_ID}.tlsn"

One-Line Downloads

curl -fL -sS "https://tlsn.livylabs.xyz/api/v1/jobs/3fc3e18cccfb167f6ff9bafddbc6ccd4/attestation" --output "attestation-3fc3e18cccfb167f6ff9bafddbc6ccd4.tlsn"
curl -fL -sS "https://tlsn.livylabs.xyz/api/v1/jobs/3fc3e18cccfb167f6ff9bafddbc6ccd4/secrets" --output "secrets-3fc3e18cccfb167f6ff9bafddbc6ccd4.tlsn"

Notes

  • The url must be HTTPS.
  • POST /api/v1/prove is exposed by the deployed Livy notary-tee proxy.
  • This is different from the lower-level tee_ws example, which uses /session and /notarize directly.
  • The artifact endpoints return raw .tlsn files.

On this page