WIP: interactive prover in TypeScript/React

This commit is contained in:
Hendrik Eeckhaut
2024-04-05 23:24:17 +02:00
parent 1643bdb4fb
commit ec3be0d561
15 changed files with 517 additions and 11 deletions

1
interactive-demo/prover-ts/.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
package-lock.json

View File

@@ -0,0 +1,15 @@
TODO
1. Start Verifier
2. Start websocket proxy
3. Start app
Since a web browser doesn't have the ability to make TCP connection, we need to use a websocket proxy server.
To run your own websockify proxy **locally**, run:
```sh
git clone https://github.com/novnc/websockify && cd websockify
./docker/build.sh
docker run -it --rm -p 55688:80 novnc/websockify 80 swapi.dev:443
```

View File

@@ -0,0 +1,90 @@
import React, { ReactElement, useCallback, useEffect, useState } from 'react';
import { createRoot } from 'react-dom/client';
import { interactive_prove, prove, verify } from 'tlsn-js';
import { Proof } from 'tlsn-js/build/types';
import { Watch } from 'react-loader-spinner';
const container = document.getElementById('root');
const root = createRoot(container!);
root.render(<App />);
function App(): ReactElement {
const [processing, setProcessing] = useState(false);
const [result, setResult] = useState<{
time: number;
sent: string;
recv: string;
notaryUrl: string;
} | null>(null);
const [proof, setProof] = useState<Proof | null>(null);
const uri = "https://swapi.dev/api/people/1";
const id = "interactive-verifier-demo";
const onClick = useCallback(async () => {
setProcessing(true);
const p = await interactive_prove(
'ws://localhost:55688',
'ws://localhost:9816',
uri,
id);
// setProof(p);
}, [setProof, setProcessing]);
useEffect(() => {
(async () => {
if (proof) {
const r = await verify(proof);
setResult(r);
setProcessing(false);
}
})();
}, [proof, setResult]);
return (
<div>
<button onClick={!processing ? onClick : undefined} disabled={processing}>
Start demo
</button>
<div>
<b>Proof: </b>
{!processing && !proof ? (
<i>not started</i>
) : !proof ? (
<>
Proving data from swapi...
<Watch
visible={true}
height="40"
width="40"
radius="48"
color="#000000"
ariaLabel="watch-loading"
wrapperStyle={{}}
wrapperClass=""
/>
Open <i>Developer tools</i> to follow progress
</>
) : (
<>
<details>
<summary>View Proof</summary>
<pre>{JSON.stringify(proof, null, 2)}</pre>
</details>
</>
)}
</div>
<div>
<b>Verification: </b>
{!proof ? (
<i>not started</i>
) : !result ? (
<i>verifying</i>
) : (
<pre>{JSON.stringify(result, null, 2)}</pre>
)}
</div>
</div>
);
}

View File

@@ -0,0 +1,16 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>React/Typescrip Example</title>
</head>
<body>
<script>
</script>
<div id="root"></div>
</body>
</html>

View File

@@ -0,0 +1,30 @@
{
"name": "prover-ts",
"version": "1.0.0",
"description": "",
"main": "webpack.js",
"scripts": {
"dev": "webpack-dev-server --config webpack.js"
},
"author": "",
"license": "ISC",
"dependencies": {
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-loader-spinner": "^6.1.6",
"tlsn-js": "../../../tlsn-js"
},
"devDependencies": {
"@types/react": "^18.0.26",
"@types/react-dom": "^18.0.10",
"babel-loader": "^9.1.3",
"copy-webpack-plugin": "^11.0.0",
"html-webpack-plugin": "^5.5.0",
"source-map-loader": "^5.0.0",
"ts-loader": "^9.4.2",
"typescript": "^4.9.4",
"webpack": "^5.75.0",
"webpack-cli": "^4.10.0",
"webpack-dev-server": "^4.11.1"
}
}

View File

@@ -0,0 +1,19 @@
{
"compilerOptions": {
"target": "es5",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": false,
"skipLibCheck": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"noFallthroughCasesInSwitch": true,
"module": "esnext",
"moduleResolution": "node",
"resolveJsonModule": true,
"noEmit": false,
"jsx": "react"
},
"include": ["app.tsx"]
}

View File

@@ -0,0 +1,110 @@
var webpack = require('webpack'),
path = require('path'),
CopyWebpackPlugin = require('copy-webpack-plugin'),
HtmlWebpackPlugin = require('html-webpack-plugin');
const ASSET_PATH = process.env.ASSET_PATH || '/';
var alias = {};
var fileExtensions = [
'jpg',
'jpeg',
'png',
'gif',
'eot',
'otf',
'svg',
'ttf',
'woff',
'woff2',
];
var options = {
ignoreWarnings: [
/Circular dependency between chunks with runtime/,
/ResizeObserver loop completed with undelivered notifications/,
],
mode: 'development',
entry: {
app: path.join(__dirname, 'app.tsx'),
},
output: {
filename: '[name].bundle.js',
path: path.resolve(__dirname, 'build'),
clean: true,
publicPath: ASSET_PATH,
},
module: {
rules: [
{
test: new RegExp('.(' + fileExtensions.join('|') + ')$'),
type: 'asset/resource',
exclude: /node_modules/,
},
{
test: /\.html$/,
loader: 'html-loader',
exclude: /node_modules/,
},
{
test: /\.(ts|tsx)$/,
exclude: /node_modules/,
use: [
{
loader: require.resolve('ts-loader'),
},
],
},
{
test: /\.(js|jsx)$/,
use: [
{
loader: 'source-map-loader',
},
{
loader: require.resolve('babel-loader'),
},
],
exclude: /node_modules/,
},
],
},
resolve: {
alias: alias,
extensions: fileExtensions
.map((extension) => '.' + extension)
.concat(['.js', '.jsx', '.ts', '.tsx', '.css']),
},
plugins: [
new CopyWebpackPlugin({
patterns: [
{
from: 'node_modules/tlsn-js/build',
to: path.join(__dirname, 'build'),
force: true,
},
],
}),
new HtmlWebpackPlugin({
template: path.join(__dirname, 'index.ejs'),
filename: 'index.html',
cache: false,
}),
new webpack.ProvidePlugin({
Buffer: ['buffer', 'Buffer'],
}),
].filter(Boolean),
// Required by wasm-bindgen-rayon, in order to use SharedArrayBuffer on the Web
// Ref:
// - https://github.com/GoogleChromeLabs/wasm-bindgen-rayon#setting-up
// - https://web.dev/i18n/en/coop-coep/
devServer: {
headers: {
'Cross-Origin-Embedder-Policy': 'require-corp',
'Cross-Origin-Opener-Policy': 'same-origin',
},
},
};
module.exports = options;

View File

@@ -149,12 +149,12 @@ fn redact_and_reveal_received_data(prover: &mut Prover<Prove>) {
// Get the homeworld from the received data.
let received_string = String::from_utf8(prover.recv_transcript().data().to_vec()).unwrap();
let re = Regex::new(r#""homeworld"\s?:\s?"(.*?)""#).unwrap();
let commit_hash_match = re.captures(&received_string).unwrap().get(1).unwrap();
let homeworld_match = re.captures(&received_string).unwrap().get(1).unwrap();
// Reveal everything except for the homeworld.
_ = prover.reveal(0..commit_hash_match.start(), Direction::Received);
_ = prover.reveal(0..homeworld_match.start(), Direction::Received);
_ = prover.reveal(
commit_hash_match.end()..recv_transcript_len,
homeworld_match.end()..recv_transcript_len,
Direction::Received,
);
}

View File

@@ -124,12 +124,14 @@ async fn verifier<T: AsyncWrite + AsyncRead + Send + Unpin + 'static>(
let verifier = Verifier::new(verifier_config);
// Verify MPC-TLS and wait for (redacted) data.
debug!("Starting MPC-TLS verification...");
let (sent, received, session_info) = verifier
.verify(socket.compat())
.await
.map_err(|err| eyre!("Verification failed: {err}"))?;
// Check send data: check host.
// Check sent data: check host.
debug!("Starting sent data verification...");
let sent_data = String::from_utf8(sent.data().to_vec())
.map_err(|err| eyre!("Failed to parse sent data: {err}"))?;
sent_data
@@ -137,6 +139,7 @@ async fn verifier<T: AsyncWrite + AsyncRead + Send + Unpin + 'static>(
.ok_or_else(|| eyre!("Verification failed: Expected host {}", server_domain))?;
// Check received data: check json and version number.
debug!("Starting received data verification...");
let response = String::from_utf8(received.data().to_vec())
.map_err(|err| eyre!("Failed to parse received data: {err}"))?;
debug!("Received data: {:?}", response);

View File

@@ -16,7 +16,7 @@
"serve:test": "serve --config ../serve.json ./test-build -l 3001",
"build:src": "webpack --config webpack.build.config.js",
"build:types": "tsc --project tsconfig.compile.json",
"build": "NODE_ENV=production concurrently npm:build:src npm:build:types",
"build": "npm run build:wasm && NODE_ENV=production concurrently npm:build:src npm:build:types",
"update:wasm": "sh utils/check-wasm.sh -f",
"test:wasm": "cd wasm/prover; wasm-pack test --firefox --release --headless",
"build:wasm": "wasm-pack build --target web wasm/prover",
@@ -38,14 +38,12 @@
"@types/expect": "^24.3.0",
"@types/mocha": "^10.0.6",
"@types/serve-handler": "^6.1.4",
"@typescript-eslint/eslint-plugin": "^6.15.0",
"@typescript-eslint/parser": "7.0.2",
"browserify": "^17.0.0",
"concurrently": "^5.1.0",
"constants-browserify": "^1.0.0",
"copy-webpack-plugin": "^5.0.5",
"crypto-browserify": "^3.12.0",
"eslint": "^8.47.0",
"eslint": "^8.57.0",
"eslint-config-prettier": "^9.0.0",
"eslint-plugin-prettier": "^5.0.0",
"file-loader": "^5.0.2",
@@ -64,7 +62,8 @@
"ts-loader": "^6.2.1",
"ts-mocha": "^10.0.0",
"ts-node": "^10.9.2",
"typescript": "^4.9.3",
"typescript": "^4.9.5",
"typescript-eslint": "^7.4.0",
"webpack": "^5.75.0",
"webpack-cli": "^5.0.0",
"webpack-dev-server": "^4.11.1",
@@ -76,4 +75,4 @@
"engines": {
"node": ">= 16.20.2"
}
}
}

View File

@@ -56,6 +56,25 @@ export const prove = async (
};
};
export const interactive_prove = async (
websocket_proxy_url: string,
verifier_proxy_url: string,
uri: string,
id: string
): Promise<string> => {
const tlsn = await getTLSN();
const proof = await tlsn.interactive_prove(
websocket_proxy_url,
verifier_proxy_url,
uri,
id);
return proof
};
export const verify = async (
proof: Proof,
publicKeyOverride?: string,

View File

@@ -1,6 +1,7 @@
import init, {
initThreadPool,
prover,
interactive_prover,
verify,
} from '../wasm/prover/pkg/tlsn_extension_rs';
@@ -73,6 +74,31 @@ export default class TLSN {
return resJSON;
}
async interactive_prove(
websocket_proxy_url: string,
verifier_proxy_url: string,
uri: string,
id: string,
) {
await this.waitForStart();
const resProver = await interactive_prover(
websocket_proxy_url,
verifier_proxy_url,
uri,
id
);
const resJSON = JSON.parse(resProver);
// console.log('!@# resProver,resJSON=', { resProver, resJSON });
// console.log('!@# resAfter.memory=', resJSON.memory);
// 1105920000 ~= 1.03 gb
// console.log(
// '!@# resAfter.memory.buffer.length=',
// resJSON.memory?.buffer?.byteLength,
// );
return resJSON;
}
async verify(proof: any, pubkey: string) {
await this.waitForStart();
const raw = await verify(JSON.stringify(proof), pubkey);

View File

@@ -31,7 +31,7 @@ pin-project-lite = "0.2.4"
http-body-util = "0.1"
hyper = { version = "1.1", features = ["client", "http1"] }
hyper-util = { version = "0.1", features = ["http1"] }
hyper-util = { version = "0.1.3" }
tracing-subscriber = { version = "0.3", features = ["time"] }
tracing-web = "0.1.2"
@@ -81,6 +81,7 @@ console_error_panic_hook = { version = "0.1.7" }
strum = { version = "0.26.1" }
strum_macros = "0.26.1"
regex = "1.10.4"
[dev-dependencies]
wasm-bindgen-test = "0.3.34"
@@ -88,6 +89,7 @@ wasm-bindgen-test = "0.3.34"
[profile.release]
lto = true # Enable Link Time Optimization
opt-level = "z" # Optimize for size
debug = true
[package.metadata.wasm-pack.profile.release]
wasm-opt = true

View File

@@ -0,0 +1,173 @@
use crate::hyper_io::FuturesIo;
use crate::request_opt::RequestOptions;
pub use crate::request_opt::VerifyResult;
use crate::requests::{ClientType, NotarizationSessionRequest, NotarizationSessionResponse};
use crate::{fetch_as_json_string, setup_tracing_web};
use futures::channel::oneshot;
use futures::AsyncWriteExt;
use http_body_util::{BodyExt, Empty, Full};
use hyper::{body::Bytes, Request, StatusCode, Uri};
use js_sys::Array;
use regex::Regex;
use std::ops::Range;
use strum::EnumMessage;
use tlsn_core::proof::TlsProof;
use tlsn_core::{proof::SessionInfo, Direction, RedactedTranscript};
use tlsn_prover::tls::{state::Prove, Prover, ProverConfig};
use tracing::instrument;
use url::Url;
use wasm_bindgen::prelude::*;
use wasm_bindgen_futures::spawn_local;
pub use wasm_bindgen_rayon::init_thread_pool;
use web_sys::{Headers, RequestInit, RequestMode};
use web_time::Instant;
use ws_stream_wasm::*;
use tracing::{debug, info};
const SECRET: &str = "TLSNotary's private key 🤡";
#[tracing::instrument]
#[wasm_bindgen]
pub async fn interactive_prover(
websocket_proxy_url: String,
verifier_proxy_url: String,
uri: String,
id: String,
) -> Result<String, JsValue> {
let uri = uri.parse::<Uri>().unwrap();
assert_eq!(uri.scheme().unwrap().as_str(), "https");
let server_domain = uri.authority().unwrap().host();
info!(
"Interactive proof: {}, {}, {} ,{} ",
websocket_proxy_url, verifier_proxy_url, uri, id
);
let test = format!("{}/verify", verifier_proxy_url);
let (_, verifier_ws_stream) = WsMeta::connect(test, None)
.await
.expect_throw("assume the verifier ws connection succeeds");
let verifier_ws_stream_into = verifier_ws_stream.into_io();
// Create prover and connect to verifier.
let prover = Prover::new(
ProverConfig::builder()
.id(id)
.server_dns(server_domain)
.build()
.unwrap(),
)
.setup(verifier_ws_stream_into)
.await
.unwrap();
// Connect to TLS Server.
debug!("Connect to websocket proxy {}", websocket_proxy_url);
let (_, client_ws_stream) = WsMeta::connect(websocket_proxy_url, None)
.await
.expect_throw("assume the client ws connection succeeds");
let (mpc_tls_connection, prover_fut) =
prover.connect(client_ws_stream.into_io()).await.unwrap();
let mpc_tls_connection = unsafe { FuturesIo::new(mpc_tls_connection) };
let (prover_sender, prover_receiver) = oneshot::channel();
let handled_prover_fut = async {
let result = prover_fut.await;
let _ = prover_sender.send(result);
};
spawn_local(handled_prover_fut);
// Attach the hyper HTTP client to the TLS connection
let (mut request_sender, connection) =
hyper::client::conn::http1::handshake(mpc_tls_connection)
.await
.map_err(|e| JsValue::from_str(&format!("Could not handshake: {:?}", e)))?;
// Spawn the HTTP task to be run concurrently
let (connection_sender, connection_receiver) = oneshot::channel();
let connection_fut = connection.without_shutdown();
let handled_connection_fut = async {
let result = connection_fut.await;
let _ = connection_sender.send(result);
};
spawn_local(handled_connection_fut);
// MPC-TLS: Send Request and wait for Response.
let request = Request::builder()
.uri(uri.clone())
.header("Host", server_domain)
.header("Connection", "close")
.header("Secret", SECRET)
.method("GET")
.body(Empty::<Bytes>::new())
.unwrap();
let response = request_sender.send_request(request).await.unwrap();
assert!(response.status() == StatusCode::OK);
// Close TLS Connection.
// let mut client_socket = connection_receiver
// .await
// .map_err(|e| {
// JsValue::from_str(&format!(
// "Could not receive from connection_receiver: {:?}",
// e
// ))
// })?
// .map_err(|e| JsValue::from_str(&format!("Could not get TlsConnection: {:?}", e)))?
// .io
// .into_inner();
// client_socket
// .close()
// .await
// .map_err(|e| JsValue::from_str(&format!("Could not close socket: {:?}", e)))?;
// Create proof for the Verifier.
let prover = prover_receiver
.await
.map_err(|e| {
JsValue::from_str(&format!("Could not receive from prover_receiver: {:?}", e))
})?
.map_err(|e| JsValue::from_str(&format!("Could not get Prover: {:?}", e)))?;
let mut prover = prover.start_prove();
redact_and_reveal_received_data(&mut prover);
redact_and_reveal_sent_data(&mut prover);
prover.prove().await.unwrap();
// Finalize.
let _ = prover.finalize().await;
Ok(r#"{"result": "success"}"#.to_string())
}
/// Redacts and reveals received data to the verifier.
fn redact_and_reveal_received_data(prover: &mut Prover<Prove>) {
let recv_transcript_len = prover.recv_transcript().data().len();
// Get the homeworld from the received data.
let received_string = String::from_utf8(prover.recv_transcript().data().to_vec()).unwrap();
let re = Regex::new(r#""homeworld"\s?:\s?"(.*?)""#).unwrap();
let commit_hash_match = re.captures(&received_string).unwrap().get(1).unwrap();
// Reveal everything except for the commit hash.
_ = prover.reveal(0..commit_hash_match.start(), Direction::Received);
_ = prover.reveal(
commit_hash_match.end()..recv_transcript_len,
Direction::Received,
);
}
/// Redacts and reveals sent data to the verifier.
fn redact_and_reveal_sent_data(prover: &mut Prover<Prove>) {
let sent_transcript_len = prover.sent_transcript().data().len();
let sent_string = String::from_utf8(prover.sent_transcript().data().to_vec()).unwrap();
let secret_start = sent_string.find(SECRET).unwrap();
// Reveal everything except for the SECRET.
_ = prover.reveal(0..secret_start, Direction::Sent);
_ = prover.reveal(
secret_start + SECRET.len()..sent_transcript_len,
Direction::Sent,
);
}

View File

@@ -5,6 +5,9 @@ mod requests;
pub mod prover;
pub use prover::prover;
pub mod interactive_prover;
pub use interactive_prover::interactive_prover;
pub mod verify;
use tracing::error;
pub use verify::verify;