feat: Playground for P2P demo using websocket and streams (#76)

This commit is contained in:
tsukino
2024-12-03 07:54:26 -05:00
committed by GitHub
parent d64361c785
commit a82add9a05
14 changed files with 4089 additions and 0 deletions

1
demo/web-to-web-p2p/.gitignore vendored Normal file
View File

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

View File

@@ -0,0 +1,24 @@
# Web-to-Web P2P Demo
This project demonstrates a peer-to-peer (P2P) communication between two web clients using TLSNotary.
The web prover will get data from <https://swapi.dev> and prove it to the web verifier.
In this demo, the two web clients run in the same browser page (`./src/app.tsx`) and communicate via a simple websocket server (`./server/index.js`)
## Run the demo
1. Run the demo:
```
npm i
npm run dev
```
2. Open <http://localhost:3456/>
3. Click the **Start Demo** button
The Prover window logs the Prover's output, the Verifier logs the Verifier's output. In the console view you can see the websocket log.
You can also open the Browser developer tools (F12) to see more TLSNotary protocol logs.
## Project Structure
- `src/`: Contains the source code for the demo.
- `server/`: Contains the WebSocket server code.

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,48 @@
{
"name": "web-to-web-p2p",
"version": "1.0.0",
"description": "",
"main": "webpack.js",
"scripts": {
"dev:server": "node ./server/index.js",
"dev:ui": "webpack-dev-server --config webpack.js",
"dev": "concurrently npm:dev:ui npm:dev:server",
"build": "webpack --config webpack.js"
},
"author": "",
"license": "ISC",
"dependencies": {
"comlink": "^4.4.1",
"concurrently": "^9.1.0",
"express": "^4.21.1",
"qs": "^6.13.0",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-loader-spinner": "^6.1.6",
"tailwindcss": "^3.4.14",
"tlsn-js": "0.1.0-alpha.7.1",
"ws": "^8.18.0"
},
"devDependencies": {
"@types/react": "^18.0.26",
"@types/react-dom": "^18.0.10",
"babel-loader": "^9.1.3",
"copy-webpack-plugin": "^11.0.0",
"crypto-browserify": "^3.12.0",
"css-loader": "^6.7.3",
"html-webpack-plugin": "^5.5.0",
"postcss-loader": "^7.3.3",
"postcss-preset-env": "^9.1.1",
"sass": "^1.57.1",
"sass-loader": "^13.2.0",
"style-loader": "^3.3.1",
"source-map-loader": "^5.0.0",
"stream-browserify": "^3.0.0",
"ts-loader": "^9.4.2",
"typescript": "^4.9.4",
"vm-browserify": "^1.1.2",
"webpack": "^5.75.0",
"webpack-cli": "^4.10.0",
"webpack-dev-server": "^4.11.1"
}
}

3439
demo/web-to-web-p2p/pnpm-lock.yaml generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,4 @@
const tailwindcss = require("tailwindcss");
module.exports = {
plugins: ["postcss-preset-env", tailwindcss],
};

View File

@@ -0,0 +1,27 @@
// This file runs a WebSocket server which enables a web prover and web verifier to connect to each other
const express = require('express');
const { createServer } = require('http');
const { WebSocketServer } = require('ws');
const qs = require('qs');
const app = express();
const port = process.env.PORT || 3001;
const server = createServer(app);
const wss = new WebSocketServer({ server });
const clients = new Map();
wss.on('connection', async (client, request) => {
const query = qs.parse((request.url || '').replace(/\/\?/g, ''));
const id = query.id;
clients.set(id, client);
client.on('message', (data) => {
const target = id === 'prover' ? 'verifier' : 'prover';
console.log(target, data.length);
clients.get(target).send(data);
});
});
server.listen(port, () => {
console.log(`ws server listening on port ${port}`);
});

View File

@@ -0,0 +1,3 @@
@tailwind base;
@tailwind components;
@tailwind utilities;

View File

@@ -0,0 +1,272 @@
import React, { ReactElement, useCallback, useEffect, useState } from 'react';
import { createRoot } from 'react-dom/client';
import { Watch } from 'react-loader-spinner';
import * as Comlink from 'comlink';
import {
Prover as TProver,
Verifier as TVerifier,
Commit,
Transcript,
} from 'tlsn-js';
import './app.scss';
import WebSocketStream from './stream';
const { init, Prover, Verifier }: any = Comlink.wrap(
new Worker(new URL('./worker.ts', import.meta.url)),
);
const container = document.getElementById('root');
const root = createRoot(container!);
root.render(<App />);
let proverLogs: string[] = [];
let verifierLogs: string[] = [];
const p2pProxyUrl = 'ws://localhost:3001';
const serverDns = 'swapi.dev';
const webSocketProxy = `wss://notary.pse.dev/proxy?token=${serverDns}`;
const requestUrl = `https://swapi.dev/api/people/1`;
function App(): ReactElement {
const [ready, setReady] = useState(false);
const [proverMessages, setProverMessages] = useState<string[]>([]);
const [verifierMessages, setVerifierMessages] = useState<string[]>([]);
const [started, setStarted] = useState(false);
// Initialize TLSNotary
useEffect(() => {
(async () => {
await init({ loggingLevel: 'Debug' });
setReady(true);
})();
}, []);
// Set up streams for prover and verifier
// This is just for demo purposes. In the future we want to pass in the stream to the
// prover instead of using the websocket url.
useEffect(() => {
(async () => {
(async () => {
const proverStream = new WebSocketStream(`${p2pProxyUrl}?id=prover`);
const reader = await proverStream.reader();
while (true) {
const { done, value } = await reader.read();
if (done) {
console.log('stream finished');
break;
}
console.log(`Received data from stream:`, await value.text());
}
})();
// Set up stream for verifier
(async () => {
const verifierStream = new WebSocketStream(
`${p2pProxyUrl}?id=verifier`,
);
const writer = await verifierStream.writer();
writer.write('Hello');
writer.write('World!');
writer.close();
})();
})();
}, []);
const addProverLog = useCallback((log: string) => {
proverLogs = proverLogs.concat(
`${new Date().toLocaleTimeString()} - ${log}`,
);
setProverMessages(proverLogs);
}, []);
const addVerifierLog = useCallback((log: string) => {
verifierLogs = verifierLogs.concat(
`${new Date().toLocaleTimeString()} - ${log}`,
);
setVerifierMessages(verifierLogs);
}, []);
const start = useCallback(async () => {
if (!ready) return;
setStarted(true);
addProverLog('Instantiate Prover class');
const prover: TProver = await new Prover({
serverDns: serverDns,
});
addProverLog('Prover class instantiated');
addVerifierLog('Instantiate Verifier class');
const verifier: TVerifier = await new Verifier({});
addVerifierLog('Verifier class instantiated');
addVerifierLog('Connect verifier to p2p proxy');
// TODO tlsn-wasm: we want to pass in the stream here instead of the websocket url
// The stream is both readable and writable (duplex)
try {
await verifier.connect(`${p2pProxyUrl}?id=verifier`);
} catch (e: any) {
addVerifierLog('Error connecting verifier to p2p proxy');
addVerifierLog(e.message);
return;
}
addVerifierLog('Verifier connected to p2p proxy');
addProverLog('Set up prover and connect to p2p proxy');
// TODO: we also want to pass in the stream here
const proverSetup = prover.setup(`${p2pProxyUrl}?id=prover`);
addProverLog('Prover connected to p2p proxy');
// Wait for prover to finish setting up websocket
// TODO: Make the setup better and avoid this wait
await new Promise((r) => setTimeout(r, 2000));
addVerifierLog('Start verifier');
// This needs to be called before we send the request
// This starts the verifier and makes it wait for the prover to send the request
const verified = verifier.verify();
await proverSetup;
addProverLog('Finished prover setup');
addProverLog('Send request');
try {
await prover.sendRequest(webSocketProxy, {
url: requestUrl,
method: 'GET',
headers: {
'Content-Type': 'application/json',
},
body: {
hello: 'world',
one: 1,
},
});
} catch (e: any) {
addProverLog(`Error sending request to ${requestUrl}`);
addProverLog(e.message);
return;
}
addProverLog('Request sent');
const transcript = await prover.transcript();
addProverLog('Response received');
addProverLog('Transcript sent');
addProverLog(transcript.sent);
addProverLog('Transcript received');
addProverLog(transcript.recv);
addProverLog('Revealing data to verifier');
// Prover only reveals parts the transcript to the verifier
const commit: Commit = {
sent: [
transcript.ranges.sent.info!,
transcript.ranges.sent.headers!['content-type'],
transcript.ranges.sent.headers!['host'],
...transcript.ranges.sent.lineBreaks,
],
recv: [
transcript.ranges.recv.info!,
transcript.ranges.recv.headers!['server'],
transcript.ranges.recv.headers!['date'],
transcript.ranges.recv.json!['name'],
transcript.ranges.recv.json!['gender'],
...transcript.ranges.recv.lineBreaks,
],
};
await prover.reveal(commit);
addProverLog('Data revealed to verifier');
const result = await verified;
addVerifierLog('Verification completed');
const t = new Transcript({
sent: result.transcript.sent,
recv: result.transcript.recv,
});
addVerifierLog('Verified data:');
addVerifierLog(`transcript.sent: ${t.sent()}`);
addVerifierLog(`transcript.recv: ${t.recv()}`);
setStarted(false);
}, [ready]);
return (
<div className="w-screen h-screen flex flex-col overflow-hidden">
<div className="w-full p-2.5 bg-slate-200 mb-5 flex-shrink-0">
<h1>Web-to-Web P2P Demo</h1>
<p>
This demo showcases peer-to-peer communication between a web prover
and a web verifier using TLSNotary. The prover fetches data from{' '}
<a href="https://swapi.dev" target="_blank" rel="noopener noreferrer">
swapi.dev
</a>{' '}
and proves it to the verifier.
</p>
</div>
<div className="grid grid-rows-2 grid-cols-2 p-2 gap-2 flex-grow">
<div className="flex flex-col items-center border border-slate-300 bg-slate-50 rounded row-span-1 col-span-1 p-4 gap-2">
<div className="font-semibold">Prover</div>
<div className="flex flex-col text-sm bg-white border border-slate-300 w-full flex-grow cursor-text py-1 overflow-y-auto">
{proverMessages.map((m, index) => (
<span key={index} className="px-2 py-1 text-slate-600 break-all">
{m}
</span>
))}
</div>
</div>
<div className="flex flex-col items-center border border-slate-300 bg-slate-100 rounded row-span-1 col-span-1 p-4 gap-2">
<div className="font-semibold">Verifier</div>
<div className="flex flex-col text-sm bg-white border border-slate-300 w-full flex-grow cursor-text py-1 overflow-y-auto">
{verifierMessages.map((m, index) => (
<span
key={index}
className="px-1 py-0.5 text-slate-600 break-all"
>
{m}
</span>
))}
</div>
</div>
<div className="flex flex-row justify-center row-span-1 col-span-2">
<Button
className="h-fit"
disabled={!ready || started}
onClick={start}
>
<div>
{ready && !started ? (
<>Start Demo</>
) : (
<Watch
visible={true}
height="40"
width="40"
radius="48"
color="#000000"
ariaLabel="watch-loading"
wrapperStyle={{}}
wrapperClass=""
/>
)}
</div>
</Button>
</div>
</div>
</div>
);
}
if ((module as any).hot) {
(module as any).hot.accept();
}
function Button(props: any) {
const { className = '', ...p } = props;
return (
<button
className={`px-4 py-2 bg-slate-300 rounded transition-colors border border-b-slate-400 border-r-slate-400 border-t-white border-l-white hover:bg-slate-200 active:bg-slate-300 active:border-t-slate-400 active:border-l-slate-400 active:border-b-white active:border-r-white disabled:opacity-50 disabled:bg-slate-200 ${className}`}
{...p}
/>
);
}

View File

@@ -0,0 +1,62 @@
export default class WebSocketStream {
client: WebSocket;
readable: Promise<ReadableStream>;
writable: Promise<WritableStream>;
constructor(url: string) {
const client = new WebSocket(url);
const deferredReadable = defer<ReadableStream>();
const deferredWritable = defer<WritableStream>();
this.client = client;
this.readable = deferredReadable.promise;
this.writable = deferredWritable.promise;
client.onopen = () => {
const readable = new ReadableStream({
start(controller) {
client.onmessage = async (event) => {
controller.enqueue(event.data);
};
},
cancel() {
client.close();
},
});
const writable = new WritableStream({
write(chunk) {
client.send(chunk);
},
close() {
client.close();
},
});
deferredReadable.resolve(readable);
deferredWritable.resolve(writable);
};
}
async reader() {
return this.readable.then((stream) => stream.getReader());
}
async writer() {
return this.writable.then((stream) => stream.getWriter());
}
}
function defer<value = any>(): {
promise: Promise<value>;
resolve: (val: value | Promise<value>) => void;
reject: (err: any) => void;
} {
let resolve: (val: value | Promise<value>) => void,
reject: (err: any) => void;
const promise: Promise<value> = new Promise((res, rej) => {
resolve = res;
reject = rej;
});
return { promise, resolve: resolve!, reject: reject! };
}

View File

@@ -0,0 +1,10 @@
import * as Comlink from 'comlink';
import init, { Prover, Attestation, Presentation, Verifier } from 'tlsn-js';
Comlink.expose({
init,
Prover,
Verifier,
Presentation,
Attestation,
});

View File

@@ -0,0 +1,12 @@
/** @type {import('tailwindcss').Config} */
module.exports = {
content: ['./src/**/*.{js,jsx,ts,tsx}'],
theme: {
extend: {
colors: {
primary: '#243f5f',
},
},
},
plugins: [],
};

View File

@@ -0,0 +1,21 @@
{
"compilerOptions": {
"target": "es2015",
"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": [
"src/app.tsx"
]
}

View File

@@ -0,0 +1,150 @@
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, 'src', '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: 'source-map-loader',
},
{
loader: require.resolve('ts-loader'),
},
],
},
{
test: /\.(js|jsx)$/,
use: [
{
loader: 'source-map-loader',
},
{
loader: require.resolve('babel-loader'),
},
],
exclude: /node_modules/,
},
{
// look for .css or .scss files
test: /\.(css|scss)$/,
// in the `web` directory
use: [
{
loader: 'style-loader',
},
{
loader: 'css-loader',
options: { importLoaders: 1 },
},
{
loader: 'postcss-loader',
},
{
loader: 'sass-loader',
options: {
sourceMap: true,
},
},
],
},
],
},
resolve: {
alias: alias,
extensions: fileExtensions
.map((extension) => '.' + extension)
.concat(['.js', '.jsx', '.ts', '.tsx', '.css']),
fallback: {
crypto: require.resolve('crypto-browserify'),
stream: require.resolve('stream-browserify'),
vm: require.resolve('vm-browserify'),
},
},
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({
process: 'process/browser',
}),
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: {
port: 3456,
host: 'localhost',
hot: true,
headers: {
'Cross-Origin-Embedder-Policy': 'require-corp',
'Cross-Origin-Opener-Policy': 'same-origin',
},
client: {
overlay: false,
},
},
};
module.exports = options;