Ian Goldberg 1 месяц назад
Сommit
1b5169385b

+ 10 - 0
LICENSE

@@ -0,0 +1,10 @@
+[Note: this copyright notice and LICENSE file applies to the sigma-rs-artifact repository itself; the sigma-rs code repositories that this repository clones are covered by their own copyrights and licenses.]
+
+Copyright 2026 Ian Goldberg, Lindsey Tulloch
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+

+ 100 - 0
README.md

@@ -0,0 +1,100 @@
+# Code artifact for `sigma-rs`
+
+This repository contains the code artifact for `sigma-rs`, a Rust
+software stack for implementing protocols based on keyed-verification
+anonymous credentials (KVAC).
+
+## Artifact structure
+
+The directories in this repository are as follows:
+
+  - [`Scripts`](Scripts/)
+    : Useful scripts for building a docker image for this artifact, and running tests therein
+  - [`patches`](patches/)
+    : Patches to the Cargo.toml file for our `sigma-rs` collection of
+    crates, to force them to use the local copies of each other as
+    dependencies, rather than using the published versions from crates.io.
+    These patches are applied automatically by the `build-docker` script.
+
+## Host requirements
+
+This document will assume your host is Ubuntu 24.04, but any similar
+system should be fine.  You will need installed on the host:
+
+  - `git`
+  - Either `docker` or `podman`.  The scripts will auto-detect which of
+    `docker` or `podman` you have.  The _names_ of the scripts contain
+    the word "`docker`", even if they end up using `podman` instead.
+  - A wasm-capable web browser, which is pretty much any modern browser.
+    You cannot use Tor browser, since you'll be connecting to a
+    localhost web server, which you cannot do over the Tor network.
+  - If your host CPU has AVX2 or AVX512 support, the runtimes will be
+    noticeably faster, but the artifact will still work if you don't
+    have them.
+
+To execute the OONI iOS benchmarks (the "iOS" column in Figure 3 in the
+paper), you will need a Mac host with Xcode installed, and an iOS device
+on which you can install apps you compile yourself.
+
+## Building the artifact
+
+After downloading or cloning this repository, build a docker image with:
+```bash
+  ./Scripts/build-docker
+```
+
+On a recentish laptop, this image should take around 10 minutes to build.  The resulting image is about 9 GB.
+
+## Running the unit tests
+
+To ensure everything has built properly, you should run the unit tests within the docker with:
+
+```bash
+  Scripts/run-docker Scripts/run_all_tests
+```
+
+This should take less than 30 seconds to run.
+
+## Running the Lox native and wasm benchmarks
+
+To run the Lox native and wasm benchmarks (Tables 2 and 5 of the paper):
+
+```bash
+  Scripts/run-docker Scripts/run_lox_benches
+```
+
+When the wasm benchmarks are run (twice: once for the new `sigma-rs`
+version of Lox, and once for the original `zkp` version), the script
+will prompt you with a URL to load in a wasm-capable web browser.  Do so
+when prompted each of the two times.
+
+These benchmarks should take a couple of minutes to run.
+
+The output of the script will end with the data tables corresponding to
+Tables 2 and 5 in the paper.  The values in Table 2 are times in
+milliseconds (with stddevs in parens).  The values in Table 5 are sizes
+in bytes.
+
+## Running the OONI native benchmarks
+
+To run the OONI native benchmarks (the "native" columns in Table 3):
+
+```bash
+  Scripts/run-docker Scripts/run_ooni_benches
+```
+
+These benchmarks should take less than 30 seconds to run.
+
+The output of the script will be the data tables corresponding to
+the "native" columns of Table 3 in the paper.  The values are times in
+milliseconds (means and stddevs).
+
+## Running the OONI iOS benchmarks
+
+To run the OONI iOS benchmarks (the "iOS" column in Table 3), build the iOS app using the instructions in [the `ios/README.md` file in the OONI repository](https://github.com/ooni/userauth/blob/artifact-v0.4/ios/README.md).
+You would typically run these instructions to build the iOS app on a Mac host with Xcode installed, not in a docker.
+
+## Using `sigma-rs` in your own code
+
+For instructions on using the `sigma-rs` stack to implement your own
+KVAC protocols, see [`README.md` in the `cmz` repository](https://git-crysp.uwaterloo.ca/SigmaProtocol/cmz/src/0.2.1/README.md).

+ 62 - 0
Scripts/Dockerfile

@@ -0,0 +1,62 @@
+FROM ubuntu:24.04
+RUN apt update && apt install -y build-essential rustup python3-pip libssl-dev pkg-config git curl make iproute2
+
+# rdsys (one of the dependencies for the Lox benchmarks) is written in Go
+RUN curl -OL https://golang.org/dl/go1.25.6.linux-amd64.tar.gz \
+    && tar -C /usr/local -xzf go1.25.6.linux-amd64.tar.gz \
+    && rm go1.25.6.linux-amd64.tar.gz
+
+# Set up Go environment
+ENV PATH=$PATH:/usr/local/go/bin
+
+# Verify installation
+RUN go version
+
+# Install a recent Rust compiler
+RUN install -D /usr/bin/rustup /root/.cargo/bin/rustup && /root/.cargo/bin/rustup install 1.92.0
+
+COPY . /root/sigma-rs-artifact/
+
+# Build all the tests, binaries, benchmarks
+WORKDIR /root/sigma-rs-artifact/spongefish
+RUN cargo build --release --all-targets
+RUN cargo build --release --all-targets -p spongefish
+WORKDIR /root/sigma-rs-artifact/sigma-proofs
+RUN cargo build --release --all-targets
+WORKDIR /root/sigma-rs-artifact/sigma-compiler
+RUN cargo build --release --all-targets
+WORKDIR /root/sigma-rs-artifact/sigma-compiler/sigma-compiler-core
+RUN cargo build --release --all-targets
+WORKDIR /root/sigma-rs-artifact/sigma-compiler/sigma-compiler-derive
+RUN cargo build --release --all-targets
+WORKDIR /root/sigma-rs-artifact/cmz
+RUN cargo build --release --all-targets
+WORKDIR /root/sigma-rs-artifact/cmz/cmz-core
+RUN cargo build --release --all-targets
+WORKDIR /root/sigma-rs-artifact/cmz/cmz-derive
+RUN cargo build --release --all-targets
+WORKDIR /root/sigma-rs-artifact/application-ooni
+RUN cargo build --release --all-targets
+RUN cargo build --release --all-targets -p ooniauth-core
+WORKDIR /root/sigma-rs-artifact/application-lox/crates/lox-extensions
+RUN cargo build --release --features=bridgeauth,test --all-targets
+WORKDIR /root/sigma-rs-artifact/application-lox/crates/lox-distributor
+RUN cargo build --release --features=test-branch --all-targets
+WORKDIR /root/sigma-rs-artifact/application-lox-zkp/crates/lox-library
+RUN cargo build --release --features=bridgeauth --all-targets
+WORKDIR /root/sigma-rs-artifact/application-lox-zkp/crates/lox-distributor
+RUN cargo build --release --features=test-branch --all-targets
+
+# Build the wasm testing environment
+WORKDIR /root/sigma-rs-artifact
+RUN git clone https://gitlab.torproject.org/tpo/anti-censorship/rdsys.git
+WORKDIR /root/sigma-rs-artifact/rdsys
+RUN make build
+RUN make descriptors
+WORKDIR /root/sigma-rs-artifact/application-lox/crates/lox-wasm
+RUN cargo install wasm-pack
+RUN /root/.cargo/bin/wasm-pack build --release --target web
+WORKDIR /root/sigma-rs-artifact/application-lox-zkp/crates/lox-wasm
+RUN /root/.cargo/bin/wasm-pack build --release --target web
+
+WORKDIR /root/sigma-rs-artifact/

+ 20 - 0
Scripts/build-docker

@@ -0,0 +1,20 @@
+#!/bin/bash
+
+# cd into the directory containing this script (from the bash faq 028)
+if [[ $BASH_SOURCE = */* ]]; then
+  cd -- "${BASH_SOURCE%/*}/" || exit
+fi
+
+if [ "`type -t docker`" != "" ]; then
+    docker="docker"
+elif [ "`type -t podman`" != "" ]; then
+    docker="podman"
+else
+    echo "Cannot find either docker or podman" >&2
+    exit 1
+fi
+
+./clone-repos || exit 1
+
+cd ..
+$docker build $* -t ${SIGMA_RS_DOCKER_PREFIX}sigma-rs -f Scripts/Dockerfile .

+ 44 - 0
Scripts/clone-repos

@@ -0,0 +1,44 @@
+#!/bin/bash
+
+# cd into the directory containing this script (from the bash faq 028)
+if [[ $BASH_SOURCE = */* ]]; then
+  cd -- "${BASH_SOURCE%/*}/" || exit
+fi
+
+cd ..
+
+fetch_repo() {
+    repo_url="$1"
+    commit_id="$2"
+    dir_name="$3"
+    update_crate="$4"
+
+    if [ -d "$dir_name" ]; then
+        ( cd "$dir_name" && git fetch origin --tags && \
+            git checkout "$commit_id" )
+    else
+        git clone "$repo_url" "$dir_name" && \
+        ( cd "$dir_name" && git checkout "$commit_id" && \
+            # Patch the Cargo.toml files to point to the local copies
+            # of our own sigma-rs dependencies
+            if [ -e "../patches/${dir_name}.diff" ]; then
+                patch -p1 < ../patches/${dir_name}.diff || exit 1
+            fi )
+    fi
+    # Update the Cargo.lock file to catch the now-local sigma-rs dependencies
+    if [ "$update_crate" != "" ]; then
+        ( cd "$dir_name" && cargo update "$update_crate" ) || exit 1
+    fi
+}
+
+fetch_repos() {
+    fetch_repo https://github.com/arkworks-rs/spongefish v0.6.1 spongefish && \
+    fetch_repo https://github.com/mmaker/sigma-rs v0.3.1 sigma-proofs spongefish && \
+    fetch_repo https://git-crysp.uwaterloo.ca/SigmaProtocol/sigma-compiler 0.2.2 sigma-compiler sigma-proofs && \
+    fetch_repo https://git-crysp.uwaterloo.ca/SigmaProtocol/cmz 0.2.1 cmz sigma-compiler && \
+    fetch_repo https://gitlab.torproject.org/onyinyang/lox.git/ lox-artifact application-lox cmz && \
+    fetch_repo https://gitlab.torproject.org/onyinyang/lox.git/ lox-artifact-zkp application-lox-zkp && \
+    fetch_repo https://github.com/ooni/userauth artifact-v0.4 application-ooni cmz
+}
+
+fetch_repos

+ 11 - 0
Scripts/remove-repos

@@ -0,0 +1,11 @@
+#!/bin/bash
+
+# cd into the directory containing this script (from the bash faq 028)
+if [[ $BASH_SOURCE = */* ]]; then
+  cd -- "${BASH_SOURCE%/*}/" || exit
+fi
+
+cd ..
+
+rm -rf spongefish sigma-proofs sigma-compiler cmz application-lox \
+    application-lox-zkp application-ooni

+ 19 - 0
Scripts/run-docker

@@ -0,0 +1,19 @@
+#!/bin/bash
+
+# cd into the directory containing this script (from the bash faq 028)
+if [[ $BASH_SOURCE = */* ]]; then
+  cd -- "${BASH_SOURCE%/*}/" || exit
+fi
+
+if [ "`type -t docker`" != "" ]; then
+    docker="docker"
+elif [ "`type -t podman`" != "" ]; then
+    docker="podman"
+else
+    echo "Cannot find either docker or podman" >&2
+    exit 1
+fi
+
+cd ..
+
+$docker run --rm -it -p 127.0.0.1:8000-8001:8000-8001 sigma-rs $*

+ 23 - 0
Scripts/run_all_tests

@@ -0,0 +1,23 @@
+#!/bin/bash
+
+# cd into the directory containing this script (from the bash faq 028)
+if [[ $BASH_SOURCE = */* ]]; then
+  cd -- "${BASH_SOURCE%/*}/" || exit
+fi
+
+# All of the crates and workspaces
+CRATES="spongefish spongefish/spongefish sigma-proofs
+    sigma-compiler sigma-compiler/sigma-compiler-derive
+    sigma-compiler/sigma-compiler-core cmz cmz/cmz-derive cmz/cmz-core
+    application-ooni"
+
+cd ..
+for c in $CRATES; do
+    (cd $c && cargo test --release) || exit 1
+done
+
+# The Lox tests need extra feature flags
+(cd application-lox/crates/lox-distributor && cargo test --release --features=test-branch) || exit 1
+(cd application-lox-zkp/crates/lox-distributor && cargo test --release --features=test-branch) || exit 1
+(cd application-lox/crates/lox-extensions && cargo test --release --features=bridgeauth,test) || exit 1
+(cd application-lox-zkp/crates/lox-library && cargo test --release --features=bridgeauth) || exit 1

+ 116 - 0
Scripts/run_lox_benches

@@ -0,0 +1,116 @@
+#!/usr/bin/env python3
+
+import os
+import re
+import subprocess
+import sys
+
+def run_bench(cmd):
+    """Run the benchmark with the given command (passed as an array of
+    arguments).  The benchmark is expected to output on stdout one or more
+    csv tables, of the form:
+
+    === Table 5 ===
+
+    protocol,request size,response size
+    Open Invitation,249,543
+    Trust Promotion,1677,2285
+    Trust Migration,845,241
+    Level Up,2253,241
+    Issue Invite,1101,401
+    Redeem Invite,1037,241
+    Check Blockage,685,325
+    Blockage Migration,973,241
+    Update Invite,557,209
+    Update Credential,749,209
+    
+    This function will return a dictionary with keys being the name of the
+    table ("Table 5"), and the value being a list of csv rows, where each row
+    is a dict with keys being the name of the column, and the value being the
+    entry in that column."""
+    proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, text=True)
+    table_name = None
+    seen_nonempty_line = False
+    retval = {}
+    table = []
+    headers = []
+    for line in proc.stdout:
+        print(line,end='')
+        line = line.rstrip()
+        if matched := re.match(r'=== (.*) ===', line):
+            if table_name is not None:
+                # Store the previous table
+                retval[table_name] = table
+            table_name = matched.group(1)
+            seen_nonempty_line = False
+            table = []
+            headers = []
+            continue
+        if line == "" and seen_nonempty_line == True:
+            if table_name is not None:
+                # The table is finished; store it
+                retval[table_name] = table
+                table_name = None
+                seen_nonempty_line = False
+                table = []
+                headers = []
+            continue
+        if line == "":
+            continue
+        # At this point, we know the line is nonempty
+        if len(headers) == 0:
+            # This should be the header line
+            headers = line.split(',')
+            seen_nonempty_line = True
+        else:
+            values = line.split(',')
+            row = dict(zip(headers, values))
+            table.append(row)
+
+    # If stdout ends, and there's a table in progress, store it
+    if table_name is not None:
+        retval[table_name] = table
+
+    return retval
+
+os.chdir(os.path.dirname(os.path.realpath(sys.argv[0])))
+os.environ['PYTHONUNBUFFERED'] = '1'
+if len(sys.argv) > 1:
+    niters = sys.argv[1]
+else:
+    niters = "10"
+
+lox_sigma_native = run_bench(["./run_native_bench", niters])
+lox_zkp_native = run_bench(["./run_native_bench", "-z", niters])
+lox_sigma_wasm = run_bench(["./run_wasm_bench", niters])
+lox_zkp_wasm = run_bench(["./run_wasm_bench", "-z", niters])
+
+print("\n*** Complete Table 2 ***\n")
+print("protocol,zkp client native ms,zkp client wasm ms,zkp server native ms,sigma-rs client native ms,sigma-rs client wasm ms,sigma-rs server native ms")
+for r in range(len(lox_sigma_native['Table 2'])):
+    proto = lox_sigma_native['Table 2'][r]['protocol']
+    assert proto == lox_zkp_native['Table 2'][r]['protocol']
+    assert proto == lox_sigma_wasm['Table 2'][r]['protocol']
+    assert proto == lox_zkp_wasm['Table 2'][r]['protocol']
+    print(','.join([
+        proto,
+        lox_zkp_native['Table 2'][r]['client native ms'],
+        lox_zkp_wasm['Table 2'][r]['client wasm ms'],
+        lox_zkp_native['Table 2'][r]['server native ms'],
+        lox_sigma_native['Table 2'][r]['client native ms'],
+        lox_sigma_wasm['Table 2'][r]['client wasm ms'],
+        lox_sigma_native['Table 2'][r]['server native ms'],
+    ]))
+
+print("\n*** Complete Table 5 ***\n")
+print("protocol,zkp request bytes,zkp response bytes,sigma-rs request bytes,sigma-rs response bytes")
+for r in range(len(lox_sigma_native['Table 5'])):
+    proto = lox_sigma_native['Table 5'][r]['protocol']
+    assert proto == lox_zkp_native['Table 5'][r]['protocol']
+    print(','.join([
+        proto,
+        lox_zkp_native['Table 5'][r]['request size'],
+        lox_zkp_native['Table 5'][r]['response size'],
+        lox_sigma_native['Table 5'][r]['request size'],
+        lox_sigma_native['Table 5'][r]['response size'],
+    ]))

+ 87 - 0
Scripts/run_native_bench

@@ -0,0 +1,87 @@
+#!/usr/bin/env python3
+
+# Usage: ./run_native_bench [-z] [niters]
+# Use -z to benchmark the legacy zkp code; otherwise benchmark our sigma-rs code
+# niters defaults to 10
+
+import os
+import re
+import subprocess
+import sys
+
+progname = sys.argv.pop(0)
+if len(sys.argv) > 0 and sys.argv[0] == "-z":
+    sys.argv.pop(0)
+    loxdir = "../application-lox-zkp/crates/lox-library"
+    cargo_features = "--features=bridgeauth"
+else:
+    loxdir = "../application-lox/crates/lox-extensions"
+    cargo_features = "--features=bridgeauth,test"
+
+if len(sys.argv) > 0:
+    niters = sys.argv[0]
+else:
+    niters = "10"
+
+os.environ['LOX_BENCH_NITERS'] = niters
+os.chdir(os.path.dirname(os.path.realpath(progname)))
+os.chdir(loxdir)
+
+proc = subprocess.Popen(["cargo", "test", "--release", "bench_",
+    cargo_features, "--", "--nocapture", "--test-threads", "1"],
+    stdout=subprocess.PIPE, text=True)
+protocol = None
+protocol_map = {
+    'BLOCKAGE-MIGRATION': 'Blockage Migration',
+    'CHECK-BLOCKAGE': 'Check Blockage',
+    'ISSUE-INVITATION': 'Issue Invite',
+    'LEVEL-UP-2: 44 days': 'Level Up',
+    'TRUST-MIGRATION-0: 30 days': 'Trust Migration',
+    'OPEN-INVITATION': 'Open Invitation',
+    'REDEEM-INVITATION': 'Redeem Invite',
+    'TRUST-PROMOTION-1: 30 days': 'Trust Promotion',
+    'UPDATE-CRED': 'Update Credential',
+    'UPDATE-INVITE': 'Update Invite',
+}
+req_size = {}
+resp_size = {}
+client_time = {}
+server_time = {}
+for line in proc.stdout:
+    print(line,end='')
+    if matched := re.match(r'----(.*)----', line):
+        protocol = protocol_map[matched.group(1)]
+        continue
+    if matched := re.match(r'Request bytes range: \[\d+, (\d+)\]', line):
+        req_size[protocol] = matched.group(1)
+    elif matched := re.match(r'Response bytes range: \[\d+, (\d+)\]', line):
+        resp_size[protocol] = matched.group(1)
+    elif matched := re.match(r'Total client ms: ([\.\d]+) ± ([\.\d]+)', line):
+        client_time[protocol] = (matched.group(1), matched.group(2))
+    elif matched := re.match(r'Response ms: ([\.\d]+) ± ([\.\d]+)', line):
+        server_time[protocol] = (matched.group(1), matched.group(2))
+
+# The order in which we output the protocols
+protocol_list = [
+    'Open Invitation',
+    'Trust Promotion',
+    'Trust Migration',
+    'Level Up',
+    'Issue Invite',
+    'Redeem Invite',
+    'Check Blockage',
+    'Blockage Migration',
+    'Update Invite',
+    'Update Credential',
+]
+
+print("\n=== Table 2 ===\n")
+print("protocol,client native ms,server native ms")
+for p in protocol_list:
+    print(f"{p},{client_time[p][0]} ({client_time[p][1]}),{server_time[p][0]} ({server_time[p][1]})")
+
+print("\n=== Table 5 ===\n")
+print("protocol,request size,response size")
+for p in protocol_list:
+    print(f"{p},{req_size[p]},{resp_size[p]}")
+print()

+ 9 - 0
Scripts/run_ooni_benches

@@ -0,0 +1,9 @@
+#!/bin/bash
+
+# cd into the directory containing this script (from the bash faq 028)
+if [[ $BASH_SOURCE = */* ]]; then
+  cd -- "${BASH_SOURCE%/*}/" || exit
+fi
+
+cd ../application-ooni
+cargo run --release --example benchmark_table

+ 62 - 0
Scripts/run_wasm_bench

@@ -0,0 +1,62 @@
+#!/bin/bash
+
+# Usage: ./run_wasm_bench [-z] [niters]
+# Use -z to benchmark the legacy zkp code; otherwise benchmark our sigma-rs code
+# niters defaults to 10
+
+# cd into the directory containing this script (from the bash faq 028)
+if [[ $BASH_SOURCE = */* ]]; then
+  cd -- "${BASH_SOURCE%/*}/" || exit
+fi
+
+# See if we're benchmarking the legacy (zkp) code or our (sigma-rs) code
+if [ "$1" = "-z" ]; then
+    shift
+    loxdir="../application-lox-zkp"
+else
+    loxdir="../application-lox"
+fi
+
+if [ "$1" = "" ]; then
+    niters=10
+else
+    niters="$1"
+fi
+
+cleanup() {
+    echo "Cleaning up..."
+    if [ "$rdsyspid" != "" ]; then
+	kill $rdsyspid
+    fi
+    if [ "$webserverpid" != "" ]; then
+	kill $webserverpid
+    fi
+    echo "Done"
+}
+
+trap cleanup EXIT SIGINT SIGTERM
+
+# Start the rdsys backend
+cd ../rdsys
+./backend -config conf/config.json &
+rdsyspid=$!
+
+cd "$loxdir/crates/lox-wasm"
+# Touch the wasm files so that the zkp ones aren't used for the sigma-rs
+# benches or vice versa
+touch index.js index.html pkg/*
+# Start the web server, which will itself manage the lox distributor
+../../../Scripts/wasm_server $niters &
+webserverpid=$!
+
+sleep 2
+echo
+echo "*******************"
+echo "Please visit http://127.0.0.1:8000/ in a wasm-capable web browser"
+echo "*******************"
+echo
+
+wait $webserverpid
+webserverpid=""
+
+../../../Scripts/wasm_parser

+ 74 - 0
Scripts/wasm_parser

@@ -0,0 +1,74 @@
+#!/usr/bin/env python3
+
+# Usage: ./wasm_parser
+# Be sure that the uploaded_log file is in the current directory
+
+import os
+import re
+import subprocess
+import statistics
+import sys
+
+protocol = None
+protocol_map = {
+    'blockage-migration': 'Blockage Migration',
+    'check-blockage': 'Check Blockage',
+    'issue-invite': 'Issue Invite',
+    'level-up': 'Level Up',
+    'migration': 'Trust Migration',
+    'open-invite': 'Open Invitation',
+    'redeem-invite': 'Redeem Invite',
+    'trust-promo': 'Trust Promotion',
+    'update-cred': 'Update Credential',
+    'update-invite': 'Update Invite',
+}
+req_time = {}
+handle_time = {}
+inv = {}
+time = {}
+std = {}
+
+
+def parse_files(log):
+    for line in log:
+        if matched := re.match(r'Dump buffer:\s+\".*?(\S+) client (request|handle reply) time ([\d\.]+)\s*ms', line):
+            protocol = protocol_map[matched.group(1)]
+            val = matched.group(3)
+            if matched.group(2) == "request":
+                if protocol in req_time:
+                    req_time[protocol].append(float(val))
+                else:
+                    req_time.setdefault(protocol, []).append(float(val))
+            else:
+                if protocol in handle_time:
+                    handle_time[protocol].append(float(val))
+                else:
+                    handle_time.setdefault(protocol, []).append(float(val))
+    for protocol in req_time:
+        inv[protocol] = list(map(lambda x, y: x + y, handle_time[protocol], req_time[protocol]))
+        time[protocol] = sum(inv[protocol])/len(req_time[protocol])
+        std[protocol] = statistics.stdev(inv[protocol])
+
+with open("uploaded_log", "r") as uploaded:
+        parse_files(uploaded)
+
+# The order in which we output the protocols
+protocol_list = [
+    'Open Invitation',
+    'Trust Promotion',
+    'Trust Migration',
+    'Level Up',
+    'Issue Invite',
+    'Redeem Invite',
+    'Check Blockage',
+    'Blockage Migration',
+    'Update Invite',
+    'Update Credential',
+]
+
+print("\n=== Table 2 ===\n")
+print("protocol,client wasm ms")
+for p in protocol_list:
+    print(f"{p},{time[p]:.3f} ({std[p]:.3f})")
+
+

+ 78 - 0
Scripts/wasm_server

@@ -0,0 +1,78 @@
+#!/usr/bin/env python3
+
+# Portions cribbed from https://www.w3reference.com/blog/python3-http-server-post-example/
+
+from http.server import SimpleHTTPRequestHandler, HTTPServer
+import json
+import subprocess
+import sys
+import threading
+import time
+
+num_uploads = 0
+niters = 3
+httpd = None
+lox_distributor = None
+log_file = open("uploaded_log", "w")
+
+def shutdown_servers():
+    if lox_distributor is not None:
+        lox_distributor.terminate()
+    time.sleep(2)
+    httpd.shutdown()
+
+def restart_lox_distributor():
+    global lox_distributor
+    if lox_distributor is not None:
+        lox_distributor.terminate()
+    subprocess.run(["/bin/rm", "-rf", "lox_db"], cwd="../lox-distributor")
+    lox_distributor = subprocess.Popen(["/usr/bin/cargo", "run", "--release", "--features",
+                                        "test-branch"], cwd="../lox-distributor")
+    time.sleep(2)
+
+
+class PostHandler(SimpleHTTPRequestHandler):
+    def do_POST(self):
+        global num_uploads
+
+        path = self.path
+        if path != "/log":
+            self.send_response(404)
+            self.send_header('Content-Type', 'text/plain; charset=utf-8')
+            self.end_headers()
+            self.wfile.write("Bad URL for POST\n".encode('utf-8'))
+            return
+
+        content_length = int(self.headers.get('Content-Length', 0))
+        
+        post_body_bytes = self.rfile.read(content_length)
+        post_body_json_str = post_body_bytes.decode('utf-8')
+        post_body_str = json.loads(post_body_json_str)
+
+        log_file.write(post_body_str)
+        
+        restart_lox_distributor()
+
+        num_uploads += 1
+
+        if num_uploads < niters:
+            response = "0"
+        else:
+            response = "1"
+        
+        self.send_response(200)
+        self.send_header('Content-Type', 'text/plain; charset=utf-8')
+        self.end_headers()
+        self.wfile.write(response.encode('utf-8'))
+
+        if num_uploads == niters:
+            # We're done
+            threading.Thread(target=shutdown_servers).start()
+ 
+if __name__ == '__main__':
+    if len(sys.argv) > 1:
+        niters = int(sys.argv[1])
+    restart_lox_distributor()
+    server_address = ('', 8000)
+    httpd = HTTPServer(server_address, PostHandler)
+    httpd.serve_forever()

+ 3 - 0
patches/README

@@ -0,0 +1,3 @@
+The patches in this directory are just to force our crates to use the
+local copies of each other as their dependencies, instead of fetching
+them from crates.io.

+ 18 - 0
patches/application-lox.diff

@@ -0,0 +1,18 @@
+diff --git a/Cargo.toml b/Cargo.toml
+index 4cff30a..382aaac 100644
+--- a/Cargo.toml
++++ b/Cargo.toml
+@@ -43,3 +43,13 @@ lto = "thin"
+ codegen-units = 16
+ opt-level = 2
+ incremental = true
++
++[patch.crates-io]
++cmz = { path = "../cmz" }
++cmz-derive = { path = "../cmz/cmz-derive" }
++cmz-core = { path = "../cmz/cmz-core" }
++sigma-compiler = { path = "../sigma-compiler" }
++sigma-compiler-derive = { path = "../sigma-compiler/sigma-compiler-derive" }
++sigma-compiler-core = { path = "../sigma-compiler/sigma-compiler-core" }
++sigma-proofs = { path = "../sigma-proofs" }
++spongefish = { path = "../spongefish/spongefish" }

+ 18 - 0
patches/application-ooni.diff

@@ -0,0 +1,18 @@
+diff --git a/Cargo.toml b/Cargo.toml
+index dae361a..b20660c 100644
+--- a/Cargo.toml
++++ b/Cargo.toml
+@@ -14,3 +14,13 @@ bincode = "1"
+ cmz = "0.2"
+ serde = "1.0.219"
+ thiserror = "2.0.12"
++
++[patch.crates-io]
++cmz = { path = "../cmz" }
++cmz-derive = { path = "../cmz/cmz-derive" }
++cmz-core = { path = "../cmz/cmz-core" }
++sigma-compiler = { path = "../sigma-compiler" }
++sigma-compiler-derive = { path = "../sigma-compiler/sigma-compiler-derive" }
++sigma-compiler-core = { path = "../sigma-compiler/sigma-compiler-core" }
++sigma-proofs = { path = "../sigma-proofs" }
++spongefish = { path = "../spongefish/spongefish" }

+ 12 - 0
patches/sigma-compiler.diff

@@ -0,0 +1,12 @@
+diff --git a/Cargo.toml b/Cargo.toml
+index b2592dc..a61769a 100644
+--- a/Cargo.toml
++++ b/Cargo.toml
+@@ -26,5 +26,5 @@ dump = [ "sigma-compiler-derive/dump" ]
+ [patch.crates-io]
+ sigma-compiler-derive = { path = "sigma-compiler-derive" }
+ sigma-compiler-core = { path = "sigma-compiler-core" }
+-# sigma-proofs = { path = "../sigma-proofs" }
+-# spongefish = { path = "../spongefish/spongefish" }
++sigma-proofs = { path = "../sigma-proofs" }
++spongefish = { path = "../spongefish/spongefish" }

+ 11 - 0
patches/sigma-proofs.diff

@@ -0,0 +1,11 @@
+diff --git a/Cargo.toml b/Cargo.toml
+index cf91501..e3b2b73 100644
+--- a/Cargo.toml
++++ b/Cargo.toml
+@@ -123,3 +123,6 @@ required-features = ["std"]
+ [profile.dev]
+ # Makes tests run much faster at the cost of slightly longer builds and worse debug info.
+ opt-level = 1
++
++[patch.crates-io]
++spongefish = { path = "../spongefish/spongefish" }