Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
target/
Cargo.lock
plonkjs/dist/
transfer-wasm/dist/
node_modules/
.DS_Store
3 changes: 2 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
[workspace]
members = ["plonkwasm", "integration-tests"]
exclude = ["transfer-wasm"]
resolver = "2"

[profile.release]
opt-level = "s"
opt-level = 3
lto = true
panic = "abort"
14 changes: 13 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
@@ -1,8 +1,14 @@
.PHONY: cq build-wasm build-wasm-raw build-wasm-rayon serve-example test test-rust test-js
.PHONY: cq build-wasm build-wasm-raw build-wasm-rayon build-transfer-wasm-rayon serve-example test test-rust test-js

WASM_ARTIFACT := target/wasm32-unknown-unknown/release/examples/test_circuit_wasm.wasm
TRANSFER_WASM_ARTIFACT := target/wasm32-unknown-unknown/release/examples/transfer_wasm.wasm
WASM_RAYON_MAX_MEMORY ?= 2147483648
TRANSFER_WASM_RAYON_MAX_MEMORY ?= 4294967296
WASM_RAYON_RUSTFLAGS := -C target-feature=+atomics,+bulk-memory -C link-arg=--shared-memory -C link-arg=--import-memory -C link-arg=--max-memory=$(WASM_RAYON_MAX_MEMORY) -C link-arg=--export=__heap_base -C link-arg=--export=__wasm_init_tls -C link-arg=--export=__tls_size -C link-arg=--export=__tls_align -C link-arg=--export=__tls_base
TRANSFER_WASM_RAYON_RUSTFLAGS := -C target-feature=+atomics,+bulk-memory -C link-arg=--shared-memory -C link-arg=--import-memory -C link-arg=--max-memory=$(TRANSFER_WASM_RAYON_MAX_MEMORY) -C link-arg=--export=__heap_base -C link-arg=--export=__wasm_init_tls -C link-arg=--export=__tls_size -C link-arg=--export=__tls_align -C link-arg=--export=__tls_base
TRANSFER_WASM_FEATURES ?= wasm-rayon
TRANSFER_WASM_CARGO_ARGS ?=
TRANSFER_WASM_OPT ?= 1
EXAMPLE_PORT ?= 8000

cq:
Expand All @@ -24,6 +30,12 @@ build-wasm-rayon:
RUSTFLAGS="$(WASM_RAYON_RUSTFLAGS)" rustup run nightly cargo build -p plonk-integration-tests --release --target wasm32-unknown-unknown --example test_circuit_wasm --features wasm-rayon -Z build-std=panic_abort,std
wasm-bindgen --target web --out-dir plonkjs/dist --out-name plonkwasm --no-typescript --keep-lld-exports $(WASM_ARTIFACT)

build-transfer-wasm-rayon:
mkdir -p transfer-wasm/dist
RUSTFLAGS="$(TRANSFER_WASM_RAYON_RUSTFLAGS)" rustup run nightly cargo $(TRANSFER_WASM_CARGO_ARGS) build --manifest-path transfer-wasm/Cargo.toml --target-dir target --release --target wasm32-unknown-unknown --example transfer_wasm --features "$(TRANSFER_WASM_FEATURES)" -Z build-std=panic_abort,std
wasm-bindgen --target web --out-dir transfer-wasm/dist --out-name transfer_wasm --no-typescript --keep-lld-exports $(TRANSFER_WASM_ARTIFACT)
if [ "$(TRANSFER_WASM_OPT)" = "1" ] && command -v wasm-opt >/dev/null 2>&1; then wasm-opt -O4 --enable-threads --enable-bulk-memory transfer-wasm/dist/transfer_wasm_bg.wasm -o transfer-wasm/dist/transfer_wasm_bg.wasm; fi

serve-example: build-keys build-wasm
node integration-tests/example/server.mjs $(EXAMPLE_PORT)

Expand Down
45 changes: 45 additions & 0 deletions transfer-wasm/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
[package]
name = "plonk-transfer-wasm"
version = "0.1.0"
edition = "2021"
publish = false

[dependencies]
dusk-bytes = "0.1.7"
dusk-core = { path = "/home/hein_/projects/rusk-private/core", features = ["zk"] }
dusk-plonk = { version = "0.22.0-rc.0", default-features = false, features = ["legacy-proving", "rkyv-impl", "alloc"] }
plonkwasm = { path = "../plonkwasm" }
rand_chacha = "0.3"
rand_core = "0.6"
serde = { version = "1", features = ["derive"] }
serde_json = "1"

[target.'cfg(target_arch = "wasm32")'.dependencies]
getrandom = { version = "0.2", features = ["js"], optional = true }
wasm-bindgen = "=0.2.121"
wasm-bindgen-rayon = { version = "1.3", features = ["no-bundler"], optional = true }

[features]
default = []
wasm-rayon = ["dusk-core/std", "dusk-plonk/std", "dep:getrandom", "dep:wasm-bindgen-rayon"]

[[example]]
name = "transfer_wasm"
path = "example/transfer_wasm.rs"
crate-type = ["cdylib"]
test = false

[profile.release]
opt-level = 3
lto = true
panic = "abort"

[patch.crates-io]
dusk-plonk = { git = "ssh://git@github.com/dusk-network/plonk-private.git", branch = "browser-wasm-transfer-bench" }
bls12_381-bls = { git = "https://github.com/dusk-network/bls12_381-bls-private.git", rev = "2bbf457d31d83df92f3afeca8e9fc72c3d33545b" }
dusk-jubjub = { git = "https://github.com/dusk-network/jubjub-private.git", rev = "6e2b63f59edac8afa4f81a28b12d88529af37cdb" }
jubjub-elgamal = { git = "https://github.com/dusk-network/jubjub-elgamal-private.git", rev = "1f0d481fb29282c69fdc0063a0404cb89a53bd10" }
phoenix-core = { git = "https://github.com/dusk-network/phoenix-private.git", rev = "3d5b9838d7e824b441c93de9aaa38737263b97bd" }
phoenix-circuits = { git = "https://github.com/dusk-network/phoenix-private.git", rev = "3d5b9838d7e824b441c93de9aaa38737263b97bd" }
piecrust = { git = "https://github.com/dusk-network/piecrust-private.git", rev = "66ddb31693ffb0fd5a4faac3c80930e6228e0836" }
piecrust-uplink = { git = "https://github.com/dusk-network/piecrust-private.git", rev = "66ddb31693ffb0fd5a4faac3c80930e6228e0836" }
50 changes: 50 additions & 0 deletions transfer-wasm/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
# Dusk transfer WASM benchmark

This crate builds a real Phoenix transfer prover for browser WASM. It exposes
the standard `plonkweb` circuit ABI (`plonkweb_init`, `plonkweb_prove`, and
`plonkweb_verify`) so it can be loaded by `plonkjs` like the integration-test
artifact.

Current fixture defaults:

- circuit: `TxCircuitFourTwo`
- input notes: 4
- circuit bytes: `/home/hein_/projects/rusk-private-boreas-full-blst/rusk-prover/tests/tx_circuit_vec.hex`
- prover key: `/home/hein_/.dusk/rusk/keys/58929845e2fa0ba344dc378af9ef2c9edbe1346439a4c786b80b79ab3e07d6cf.pk`
- verifier key: same path with `.vd`

Build the Dusk BLS backend currently patched in this crate. The benchmark crate
is excluded from the root workspace so normal `plonkweb` CI does not need local
Rusk paths or private benchmark dependencies. Its manifest patch points at
`dusk-network/plonk-private` branch `browser-wasm-transfer-bench` for the
prover hot-path and trusted unchecked key loading changes.

```sh
make build-transfer-wasm-rayon
```

`plonkweb_init` uses `Prover::try_from_trusted_unchecked_bytes`, which validates
the serialized container lengths but skips commit-key point validation. It is
only appropriate when the `.pk` is a local/generated trusted artifact, not user
supplied bytes. For `TxCircuitFourTwo` this reduced browser prover decode from
about 32 seconds to about 3.5 seconds without changing artifact size or
steady-state memory.

The `plonkweb_prove` JSON request accepts the normal `seed_hex` field plus a
Phoenix transfer fixture in `tx_circuit_vec_hex`. `plonkweb_verify` accepts the
standard `proof_hex` and `public_inputs_hex` fields.

To test another `dusk-plonk` worktree without editing the root manifest, pass
Cargo config through the Makefile:

```sh
make build-transfer-wasm-rayon \
TRANSFER_WASM_CARGO_ARGS='--config patch.crates-io.dusk-plonk.path="/home/hein_/projects/plonk-private-boreas-full-blst"'
```

For the BLST branch, also add `blst-backend` to the `dusk-plonk` dependency
features in `transfer-wasm/Cargo.toml`; the current Dusk-BLS branch does not
declare that feature, so it cannot be made a shared workspace feature.

The browser harness used during investigation is intentionally not included in
this branch.
229 changes: 229 additions & 0 deletions transfer-wasm/example/transfer_wasm.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,229 @@
use std::sync::{Mutex, OnceLock};

use dusk_bytes::{DeserializableSlice, Serializable};
use dusk_core::transfer::phoenix::{TxCircuit, TxCircuitVec, NOTES_TREE_DEPTH};
use dusk_plonk::prelude::{PlonkVersion, Proof, Prover, Verifier};
use rand_chacha::ChaCha20Rng;
use rand_core::SeedableRng;
use serde::{Deserialize, Serialize};

#[cfg(all(target_arch = "wasm32", feature = "wasm-rayon"))]
pub use wasm_bindgen_rayon::init_thread_pool;

static PROVER: OnceLock<Mutex<Option<Prover>>> = OnceLock::new();
static VERIFIER: OnceLock<Mutex<Option<Verifier>>> = OnceLock::new();

#[derive(Debug, Serialize)]
struct InitResponse {
ready: bool,
}

#[derive(Debug, Deserialize)]
struct ProveRequest {
seed_hex: String,
tx_circuit_vec_hex: String,
}

#[derive(Debug, Serialize)]
struct ProveResponse {
proof_hex: String,
public_inputs_hex: String,
input_notes: usize,
}

#[derive(Debug, Deserialize)]
struct VerifyRequest {
proof_hex: String,
public_inputs_hex: String,
}

#[derive(Debug, Serialize)]
struct VerifyResponse {
verified: bool,
}

#[no_mangle]
pub extern "C" fn plonkweb_alloc(len: usize) -> *mut u8 {
plonkwasm::wasm::alloc(len)
}

#[no_mangle]
/// # Safety
///
/// `ptr` and `len` must describe a buffer previously returned by
/// `plonkweb_alloc` and not already freed.
pub unsafe extern "C" fn plonkweb_free(ptr: *mut u8, len: usize) {
unsafe { plonkwasm::wasm::free(ptr, len) };
}

#[no_mangle]
/// Loads the transfer prover and verifier keys into wasm memory.
///
/// The prover key uses `Prover::try_from_trusted_unchecked_bytes`, which skips
/// commit-key point validation. This artifact is for trusted local benchmark
/// keys generated by Rusk, not user-supplied prover-key bytes.
///
/// # Safety
///
/// Pointer and length pairs must describe valid readable key buffers for the
/// duration of this call.
pub unsafe extern "C" fn plonkweb_init(
prover_key_ptr: *const u8,
prover_key_len: usize,
verifier_key_ptr: *const u8,
verifier_key_len: usize,
) -> u64 {
let prover_key = unsafe { core::slice::from_raw_parts(prover_key_ptr, prover_key_len) };
let verifier_key = unsafe { core::slice::from_raw_parts(verifier_key_ptr, verifier_key_len) };

plonkwasm::wasm::respond(init(prover_key, verifier_key))
}

#[no_mangle]
/// Computes a Phoenix transfer proof from a JSON request.
///
/// The request must contain `seed_hex` and `tx_circuit_vec_hex`; the circuit
/// shape is selected from the serialized `TxCircuitVec` input-note count.
///
/// # Safety
///
/// `request_ptr` and `request_len` must describe a valid readable request
/// buffer for the duration of this call.
pub unsafe extern "C" fn plonkweb_prove(request_ptr: *const u8, request_len: usize) -> u64 {
unsafe { plonkwasm::wasm::respond_from_request(request_ptr, request_len, prove_transfer) }
}

#[no_mangle]
/// Verifies a Phoenix transfer proof from a JSON request.
///
/// # Safety
///
/// `request_ptr` and `request_len` must describe a valid readable request
/// buffer for the duration of this call.
pub unsafe extern "C" fn plonkweb_verify(request_ptr: *const u8, request_len: usize) -> u64 {
unsafe { plonkwasm::wasm::respond_from_request(request_ptr, request_len, verify_transfer) }
}

fn init(prover_key: &[u8], verifier_key: &[u8]) -> Result<InitResponse, String> {
let prover =
Prover::try_from_trusted_unchecked_bytes(prover_key).map_err(|err| format!("{err:?}"))?;
let verifier = Verifier::try_from_bytes(verifier_key).map_err(|err| format!("{err:?}"))?;

let prover_storage = PROVER.get_or_init(|| Mutex::new(None));
*prover_storage
.lock()
.map_err(|_| "cached prover mutex is poisoned".to_string())? = Some(prover);

let verifier_storage = VERIFIER.get_or_init(|| Mutex::new(None));
*verifier_storage
.lock()
.map_err(|_| "cached verifier mutex is poisoned".to_string())? = Some(verifier);

Ok(InitResponse { ready: true })
}

fn prove_transfer(request: &[u8]) -> Result<ProveResponse, String> {
let request: ProveRequest =
serde_json::from_slice(request).map_err(|err| format!("invalid prove request: {err}"))?;
let seed = plonkwasm::wasm::decode_seed(&request.seed_hex)?;
let tx_circuit_vec = plonkwasm::wasm::decode_hex(&request.tx_circuit_vec_hex)?;

prove_transfer_bytes(&tx_circuit_vec, seed)
}

fn prove_transfer_bytes(
tx_circuit_vec_bytes: &[u8],
seed: [u8; 32],
) -> Result<ProveResponse, String> {
let tx_circuit_vec =
TxCircuitVec::from_slice(tx_circuit_vec_bytes).map_err(|err| format!("{err:?}"))?;
let input_notes = tx_circuit_vec.input_notes_info.len();
let mut rng = ChaCha20Rng::from_seed(seed);

let prover_storage = PROVER
.get()
.ok_or_else(|| "plonkweb init must be called before prove".to_string())?;
let prover_guard = prover_storage
.lock()
.map_err(|_| "cached prover mutex is poisoned".to_string())?;
let prover = prover_guard
.as_ref()
.ok_or_else(|| "plonkweb init must be called before prove".to_string())?;

let (proof, public_inputs) = match input_notes {
1 => {
let circuit = create_circuit::<1>(tx_circuit_vec)?;
prover.prove_with_version(&mut rng, &circuit, PlonkVersion::V3)
}
2 => {
let circuit = create_circuit::<2>(tx_circuit_vec)?;
prover.prove_with_version(&mut rng, &circuit, PlonkVersion::V3)
}
3 => {
let circuit = create_circuit::<3>(tx_circuit_vec)?;
prover.prove_with_version(&mut rng, &circuit, PlonkVersion::V3)
}
4 => {
let circuit = create_circuit::<4>(tx_circuit_vec)?;
prover.prove_with_version(&mut rng, &circuit, PlonkVersion::V3)
}
_ => return Err(format!("unsupported input note count: {input_notes}")),
}
.map_err(|err| format!("{err:?}"))?;

Ok(ProveResponse {
proof_hex: plonkwasm::wasm::encode_hex(&proof.to_bytes()),
public_inputs_hex: plonkwasm::wasm::encode_hex(&plonkwasm::serialize_public_inputs(
&public_inputs,
)),
input_notes,
})
}

fn verify_transfer(request: &[u8]) -> Result<VerifyResponse, String> {
let request: VerifyRequest =
serde_json::from_slice(request).map_err(|err| format!("invalid verify request: {err}"))?;
let proof = plonkwasm::wasm::decode_hex(&request.proof_hex)?;
let public_inputs = plonkwasm::wasm::decode_hex(&request.public_inputs_hex)?;

verify_transfer_bytes(&proof, &public_inputs)
}

fn verify_transfer_bytes(proof: &[u8], public_inputs: &[u8]) -> Result<VerifyResponse, String> {
let proof = Proof::from_slice(proof).map_err(|err| format!("invalid proof bytes: {err:?}"))?;
let public_inputs = plonkwasm::deserialize_public_inputs(public_inputs)?;

let verifier_storage = VERIFIER
.get()
.ok_or_else(|| "plonkweb init must be called before verify".to_string())?;
let verifier_guard = verifier_storage
.lock()
.map_err(|_| "cached verifier mutex is poisoned".to_string())?;
let verifier = verifier_guard
.as_ref()
.ok_or_else(|| "plonkweb init must be called before verify".to_string())?;

verifier
.verify_with_version(&proof, &public_inputs, PlonkVersion::V3)
.map_err(|err| format!("{err:?}"))?;

Ok(VerifyResponse { verified: true })
}

fn create_circuit<const I: usize>(
tx_circuit_vec: TxCircuitVec,
) -> Result<TxCircuit<NOTES_TREE_DEPTH, I>, String> {
Ok(TxCircuit {
input_notes_info: tx_circuit_vec
.input_notes_info
.try_into()
.map_err(|err| format!("{err:?}"))?,
output_notes_info: tx_circuit_vec.output_notes_info,
payload_hash: tx_circuit_vec.payload_hash,
root: tx_circuit_vec.root,
deposit: tx_circuit_vec.deposit,
max_fee: tx_circuit_vec.max_fee,
sender_pk: tx_circuit_vec.sender_pk,
signatures: tx_circuit_vec.signatures,
})
}
1 change: 1 addition & 0 deletions transfer-wasm/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
// This crate only hosts wasm examples.
Loading