ZUL://DOCSPRIVACY LAYER 2 — SETTLED ON SOLANA
INDEX
INTRODUCTION — /docs/quickstart

Run it locally

The repository is a monorepo: a Rust workspace for the chain under chain/, Anchor programs under programs/, circom circuits under circuits/, and a pnpm workspace for the TypeScript SDK, indexer, and explorer.

Prerequisites

  • Rustvia rustup, plus the usual native build dependencies (a C toolchain, pkg-config, OpenSSL headers, protobuf compiler, clang). Budget ≥ 8 GB RAM for compiling Agave-derived crates.
  • Node.js + pnpm for the web workspace.
  • PostgreSQL if you want the indexer + explorer reading real chain data.

Run the node

SHELL — chain/
cd chain
cargo test                 # unit + integration tests
cargo run -p zul-node -- --config ../config/node.example.toml

The example config produces a block every 500 ms, serves JSON-RPC on 127.0.0.1:8899 and WebSocket on 127.0.0.1:8900, and enables the built-in faucet (airdrops are real System transfers signed by a faucet key). L1 settlement is disabled until program IDs and keys are configured.

CONFIG — node.example.toml (excerpt)
[node]
slot_duration_ms = 500

[rpc]
http_addr = "127.0.0.1:8899"
ws_addr   = "127.0.0.1:8900"
faucet_enabled = true
faucet_max_lamports = 10000000000   # 10 ZUL per request

[l1]
enabled = false                     # on once programs + keys exist
batch_interval_slots = 120

Talk to it

The RPC speaks the Solana protocol, so the standard web3 client works unchanged — point a connection at the L2 and airdrop yourself gas:

TYPESCRIPT
import { Connection, Keypair, LAMPORTS_PER_SOL } from "@solana/web3.js";

const conn = new Connection("http://127.0.0.1:8899", "confirmed");
const me = Keypair.generate();

// 1 ZUL — the chain's lamport unit is ZUL, not SOL
await conn.requestAirdrop(me.publicKey, 1 * LAMPORTS_PER_SOL);
console.log(await conn.getBalance(me.publicKey));
console.log(await conn.getSlot());

The Solana CLI also works: solana balance -u http://127.0.0.1:8899 <pubkey>.

Run the web stack

SHELL — repo root
pnpm install
pnpm -r build

# indexer: copy its .env.example, set DATABASE_URL + node RPC, then run it
# explorer: same — it reads the indexer's Postgres via Prisma
KEYS

Sequencer, faucet, and bridge keys are intentionally absent from the repository. Copy the example config files and generate keys locally; nothing that signs should ever be committed.

From here: how blocks work, or jump straight to shield / transfer / unshield.