Skip to main content

Post-Quantum Signing

This guide shows how to run Silent Network on one server, create a wallet from a React frontend, register an ML-DSA verifier key, and submit an ML-DSA verification transaction to the ZKNOX verifier contract on Base Sepolia.

The flow uses Silent Network for authentication, key generation, and signing:

  • Passkey or Auth0 authenticates the user.
  • Wallet creation generates secp256k1, ed25519, and mldsa44 keys.
  • The ML-DSA flow signs raw message bytes and sends Base Sepolia transactions to the ZKNOX smart contract: first setKey registers the converted public key, then verify checks the message and signature.

Prerequisites

  • Access to the Silence Laboratories GitHub Container Registry packages.
  • Docker and Docker Compose on the server that will run Silent Network.
  • Node.js 22+ for the React frontend.
  • A browser that supports Passkeys if you use Passkey authentication.

For production deployments, use Deploying on Confidential Space. The local Docker Compose setup below is intended for development environments.

1. Authenticate Docker to GHCR

Log in with a GitHub account or token that can pull the Silent Network images:

echo "$GITHUB_TOKEN" | docker login ghcr.io -u "$GITHUB_USERNAME" --password-stdin

2. Start Silent Network

This section gives the shortest path to run the nodes and backend locally. For a deeper explanation of the node setup, backend setup, request flow, and service code breakdown, see Setup nodes and Setup backend.

Clone the deployment repository on the server:

git clone https://github.com/silence-laboratories/silent-network-deploy
cd silent-network-deploy

Configure the image registry and start the auth-enabled local network:

export IMG_URI=ghcr.io/silence-laboratories/el-services/
export IMG_TAG=v4.0.3-policy-with-auth
export GH_TOKEN="$GITHUB_TOKEN"
export DOCKER_DEFAULT_PLATFORM=linux/amd64 # (Optional) required for ARM64 machine

docker compose -f ./local/docker-compose.yaml up

The local deployment exposes:

  • WPBE: localhost:8090
  • MPC Node 1: localhost:8081
  • MPC Node 2: localhost:8082
  • MPC Node 3: localhost:8083
  • MPC Node 4: localhost:8084
  • MPC Node 5: localhost:8085

Verify the services:

curl http://localhost:8090/v2/version
{"docker_image_tag":"v4.0.3-policy-with-auth","build_hash":"v3.1.0-silent-pay-demo-6-gc781050d","profile":"release","api_key":"wpbe1","public_key":"759d0c9b07d2d0bbc4979789e97fa1d5ef502fa0f253c93e38aa824e218cb719","features":"default, eoa_auth, eph_auth, gcp_verifier, passkey_auth, policy_engine, user_auth","orig_id":"not set"}
curl --insecure https://localhost:8081/v2/version
{"docker_image_tag":"v4.0.3-policy-with-auth","build_hash":"v3.1.0-silent-pay-demo-6-gc781050d","profile":"release","api_key":"wpbe2","public_key":"02d9f040ba87b0c9d85edcdfd056dcc48cc2ff078c9fc97cf97fe457a05ffabe6d","features":"default, eoa_auth, eph_auth, gcp_verifier, passkey_auth, policy_engine, user_auth","orig_id":"not set"}

3. Start the React wallet frontend

This section shows how to run the frontend locally using the Wallet Provider Client SDK. For the full SDK walkthrough, including authentication, wallet creation, transaction signing, see Setup frontend.

Clone the examples repository:

git clone https://github.com/silence-laboratories/pq-demo-react.git
cd pq-demo-react

Run the remaining frontend commands from your React wallet application directory.

Create the environment file:

cp .env.example .env

For a local Silent Network, set these values in .env:

VITE_WALLET_PROVIDER_URL=ws://localhost:8090
VITE_API_VERSION=v1

VITE_RP_ID=localhost
VITE_RP_NAME=http://localhost:5173

VITE_MPC_THRESHOLD=2
VITE_MPC_PARTIES=3

For ML-DSA, Silent Network currently supports these threshold/party combinations:

Threshold tParties n
22
23
33

The .env values above use t=2, n=3.

Set the Base Sepolia and ZKNOX verifier values:

VITE_SEPOLIA_RPC_URL=https://sepolia.base.org
VITE_SEPOLIA_CHAIN_ID=84532
VITE_SEPOLIA_CHAIN_NAME=Base Sepolia
VITE_EXPLORER_URL=https://sepolia.basescan.org
VITE_ZKNOX_CONTRACT_ADDRESS=0x092c5d82069de997E34Ce2505CA7D5042f3721ef

Install dependencies and start the frontend:

bun install
bun run dev

The frontend is now running at http://localhost:5173.

4. Create wallet and submit an ML-DSA verification transaction

From the frontend:

  1. Select Sign with passkey.
  2. Click Register passkey. Approve the passkey creation.
  3. Click New Wallet. Enter Wallet Name. Continue.
  4. Select Wallet Security ECDSA + ML-DSA-44 (ZKNOX). Continue.
  5. Click Create Wallet. Authenticate with created passkey.
  6. Wait for Base Sepolia ETH funding.
  7. Register the ZKNOX key with setKey.
  8. Click Send to start signing and sending a verification transaction to Base Sepolia.
  9. Enter Message to sign. Click Verify, then authenticate passkey to approve the signing.
  10. Once passkey verification is done, the signed transaction will call verify on Base Sepolia.

The frontend asks Silent Network to sign raw bytes with mldsa44, converts the public key and signature into the ZKNOX verifier input format, then sends transactions to the Base Sepolia smart contract:

function setKey(bytes memory pubkey) external returns (bytes memory);

function verify(bytes memory pk, bytes memory m, bytes memory signature, bytes memory ctx)
external
view
returns (bool);

The transaction to address is the ZKNOX smart contract. The setKey calldata registers the encoded public key and returns the pointer used by verify. The verify calldata includes that pointer, the message hash, the ML-DSA signature, and an empty context value.

5. ZKNOX verifier parameters

The ZKNOX NIST verifier expects ML-DSA-44 inputs in the verifier contract shape, not the packed SDK response shape.

The conversion uses:

  • ML-DSA-44 public key size: 1312 bytes.
  • ML-DSA-44 signature size: 2420 bytes.
  • Raw SDK message: message.
  • Message hash for signing and verification: keccak256(message).
  • Context: 0x for this demo.
  • Signature split: cTilde (32 bytes), z (2304 bytes), and h (84 bytes).
  • Public key conversion: recover A_hat from rho, compute tr, decode t1, scale by 2^13, run NTT, compact each polynomial, then encode the result as the setKey public key blob.

The setKey call is:

function setKey(bytes memory pubkey) external returns (bytes memory);

setKey stores the encoded ML-DSA public key and returns a pointer. The frontend calls setKey after the ZKNOX wallet receives Base Sepolia ETH, waits for the registration transaction, and saves the returned pointer for later verification calls.

The verifier call is:

function verify(bytes memory pk, bytes memory m, bytes memory signature, bytes memory ctx)
external
view
returns (bool);

verify checks the stored public key pointer, message hash, ML-DSA signature, and context. The frontend first calls verify as a read-only check. If the result is true, it can optionally send a transaction that calls verify (note: as a view function this does not update contract state; it only records the call itself on-chain).

Use the ZKNOX conversion utility in walletprovider-sdk as the reference implementation for converting SDK ML-DSA outputs into the verifier input format.