Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
489ddda
fix(scanner): OP_FALSE (0x00) is parsed as PushBytes, not Op
TaprootFreak May 6, 2026
f5fc85e
Add crash-safe persistence and improve error handling
TaprootFreak May 6, 2026
f78f72e
Add replay protection for received coins
TaprootFreak May 6, 2026
8496689
Update docs: add IS_MAINNET, NETWORK_NAME, PUBLISHER_KEY to env vars …
TaprootFreak May 6, 2026
c65c5fe
Add Schnorr signature verification for send endpoint
TaprootFreak May 6, 2026
5cc2035
Update docs: fix API endpoints, electrs URLs, bind address, open tasks
TaprootFreak May 6, 2026
18e82bf
Fix docs: API response format, SP1 options, remove unused env vars, D…
TaprootFreak May 6, 2026
d059b44
Fix non-deterministic address derivation (breaks wallet recovery)
TaprootFreak May 6, 2026
68475df
Fix docs: complete API reference table in CONTRIBUTING.md
TaprootFreak May 6, 2026
1c0363b
Fix minting account to use deterministic address derivation
TaprootFreak May 6, 2026
4ab45ef
Clean up docs: remove completed tasks, deduplicate API table
TaprootFreak May 6, 2026
d823210
fix(server): serialize Commitment, not Option<Commitment> for inscrip…
TaprootFreak May 6, 2026
9b675d3
Fix project structure tree connector in README.md
TaprootFreak May 6, 2026
4766ba0
Fix destructive account state on failed send_coins
TaprootFreak May 6, 2026
522c95e
fix(server): use previous commitment pubkey for SMT lookup in recursi…
TaprootFreak May 6, 2026
fc9c00f
fix(server): handle missing commitment in send handler gracefully
TaprootFreak May 7, 2026
333fdb6
Fix critical: proof verification, panic on empty proofs, silent errors
TaprootFreak May 7, 2026
0661efb
Run cargo fmt on all crates
TaprootFreak May 7, 2026
385faed
Add CI workflow with fmt, clippy, build, and tests
TaprootFreak May 7, 2026
10dba48
Fix clippy warnings: remove unused imports, allow dead code
TaprootFreak May 7, 2026
0eee1fc
docs: add two-phase send flow, /api/commit endpoint, mock prover stage
TaprootFreak May 7, 2026
0778804
Add Axum integration tests for all API endpoints
TaprootFreak May 7, 2026
917e0c6
Return error on UTXO exhaustion instead of silent success
TaprootFreak May 7, 2026
108d61b
feat(server): persist ProofStore to disk for crash recovery
TaprootFreak May 7, 2026
24ee813
fix(server): persist proof before accounts in send handler
TaprootFreak May 7, 2026
743610c
Add username system and LNURL-pay endpoint (Phase 1)
TaprootFreak May 7, 2026
5babbae
Default address identifier from hex prefix, no claim needed
TaprootFreak May 7, 2026
7cab3d2
Fix formatting in publisher.rs
TaprootFreak May 7, 2026
c4f6426
Fix username route conflict: /username/claim and /username/resolve/{id}
TaprootFreak May 7, 2026
f63a6a8
Fix LNURL routing and add comprehensive integration tests
TaprootFreak May 7, 2026
370060c
Remove unused HeaderValue import
TaprootFreak May 7, 2026
cf74c39
Add scanner parsing tests and commit retry tests
TaprootFreak May 7, 2026
6fccc57
Fix commit_invalid_signature test for CI (no Bitcoin node)
TaprootFreak May 7, 2026
79a8867
Add signature verification and persistence tests
TaprootFreak May 7, 2026
4dc0147
Add claim signature, receive edge case, and username tests
TaprootFreak May 7, 2026
7753a24
Redact sensitive data from logs, use safe path construction for proofs
TaprootFreak May 9, 2026
8157534
Address remaining CodeQL alerts: remove test prints, validate proof p…
TaprootFreak May 10, 2026
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
79 changes: 79 additions & 0 deletions .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
name: CI

on:
push:
branches: [develop]
pull_request:
branches: [develop, main]

permissions:
contents: read

env:
CARGO_TERM_COLOR: always

jobs:
lint-and-build:
name: Lint & Build
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4

- name: Install Rust 1.81.0
uses: dtolnay/rust-toolchain@master
with:
toolchain: "1.81.0"
components: rustfmt, clippy

- name: Cache cargo registry and build
uses: actions/cache@v4
with:
path: |
~/.cargo/registry
~/.cargo/git
target
key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}
restore-keys: |
${{ runner.os }}-cargo-

- name: Check formatting
run: cargo fmt --all --check

- name: Run clippy (server + shared)
run: cargo clippy -p server -p shared -- -D warnings

- name: Run clippy (program lib)
run: cargo clippy -p zkcoins-program --lib -- -D warnings

- name: Build server
run: cargo build -p server

tests:
name: Tests
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4

- name: Install Rust 1.81.0
uses: dtolnay/rust-toolchain@master
with:
toolchain: "1.81.0"

- name: Cache cargo registry and build
uses: actions/cache@v4
with:
path: |
~/.cargo/registry
~/.cargo/git
target
key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}
restore-keys: |
${{ runner.os }}-cargo-

- name: Run tests (server + shared, skip slow SP1 prover tests)
run: cargo test -p server -p shared -- --skip account_server::tests

- name: Run tests (program lib)
run: cargo test -p zkcoins-program --lib
26 changes: 9 additions & 17 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ This guide covers everything you need to develop, test, and deploy the zkCoins b
git clone https://github.com/zk-coins/server.git
cd server
SP1_PROVER=mock cargo run -p server
# Server starts on http://127.0.0.1:4242
# Server starts on http://0.0.0.0:4242
```

## Prerequisites
Expand Down Expand Up @@ -169,10 +169,11 @@ The `zkvm` feature gates the SP1 entrypoint and all `sp1_zkvm::` calls.

| Variable | Default | Description |
|---|---|---|
| `SP1_PROVER` | `mock` | `mock` (stub proofs) or `local` (real SP1) |
| `ESPLORA_URL` | `https://mutinynet.com/api` | Bitcoin node API |
| `BITCOIN_RPC_USER` | — | Bitcoin Core RPC username |
| `BITCOIN_RPC_PASSWORD` | — | Bitcoin Core RPC password |
| `SP1_PROVER` | `mock` | `mock` (no proof), `cpu`, `cuda`, or `network` |
| `ESPLORA_URL` | `https://mutinynet.com/api` | Esplora API endpoint (electrs or public) |
| `IS_MAINNET` | `false` | `true` for Bitcoin Mainnet, `false` for Mutinynet/Signet |
| `NETWORK_NAME` | `Mutinynet` | Human-readable network name (returned by `/api/info`) |
| `PUBLISHER_KEY` | test key | 32-byte hex private key for inscription publishing. **Required on mainnet** |
| `RUST_LOG` | `info` | Log level (`debug`, `info`, `warn`, `error`) |

## Docker
Expand All @@ -182,15 +183,15 @@ docker build -t zkcoin/server .
docker run -p 4242:4242 \
--network bitcoin \
-e SP1_PROVER=mock \
-e ESPLORA_URL=http://bitcoind-mainnet:8332 \
-e ESPLORA_URL=http://electrs-mainnet:3000 \
zkcoin/server
```

The Dockerfile removes the `script` crate from the workspace (via `sed`) to avoid requiring the SP1 toolchain. The stub prover in `script/src/lib.rs` provides the same API surface with mock proofs.
The pre-built ELF (`elf/zkcoins-program`) is committed to the repo, so Docker builds do not require the Succinct toolchain — only standard Rust.

### Bitcoin Node

The server needs a Bitcoin node. In production, it connects via the shared Docker network `bitcoin` to `bitcoind-mainnet:8332`. Requirements:
The server needs a Bitcoin node with an Esplora-compatible indexer (electrs). In production, it connects via the shared Docker network `bitcoin` to `electrs-mainnet:3000` (DEV: `electrs-mutinynet:3000`). The underlying bitcoind requires:
- `txindex=1`
- `rest=1`
- `server=1`
Expand All @@ -207,15 +208,6 @@ See [docs.zkcoins.app/infrastructure/backend](https://docs.zkcoins.app/infrastru

Build time is ~5 minutes (Rust compilation on ARM64).

## API Reference

| Endpoint | Method | Description | Success |
|---|---|---|---|
| `/api/mint` | POST | Mint coins from minting account | `{ proof_id }` |
| `/api/send` | POST | Transfer coins between accounts | `{ proof_id }` |
| `/api/balance?address=<hex>` | GET | Query account balance | `{ balance }` |
| `/api/proof/:id` | GET | Download coin proof (binary) | Binary data |

## Related Repos

- [zk-coins/app](https://github.com/zk-coins/app) — Web application (frontend)
Expand Down
3 changes: 3 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

37 changes: 25 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ Rust/Axum backend for [zkcoins.app](https://zkcoins.app) — account management,
| ZK Proofs | SP1 zkVM | Write proofs in standard Rust, no DSL |
| Data structures | SMT + MMR | Non-inclusion proofs + append-only history |
| Bitcoin | Taproot Inscriptions | 64-byte nullifiers, Esplora API scanning |
| Bitcoin node | bitcoind-mainnet | Shared Docker network `bitcoin`, port 8332 |
| Bitcoin index | electrs (Esplora) | Esplora REST API via shared Docker network `bitcoin` |

Full rationale: [docs.zkcoins.app/tech-decisions](https://docs.zkcoins.app/tech-decisions)

Expand All @@ -36,11 +36,25 @@ SP1_PROVER=mock cargo run -p server
| Endpoint | Method | Description | Response |
|---|---|---|---|
| `/health` | GET | Health check | `ok` (200) |
| `/api/mint` | POST | Mint coins (faucet) | `{ proof_id }` |
| `/api/send` | POST | Transfer coins | `{ proof_id }` |
| `/api/info` | GET | Network info | `{ network }` |
| `/api/mint` | POST | Mint coins (faucet) | `{ success, proof_id }` |
| `/api/send` | POST | Transfer coins (phase 1) | `{ success, proof_id, account_state_hash, output_coins_root }` |
| `/api/commit` | POST | Submit signed commitment (phase 2) | `{ success, proof_id }` |
| `/api/balance?address=<hex>` | GET | Query balance | `{ balance }` |
| `/api/address` | GET | List all addresses | `{ addresses }` |
| `/api/receive` | POST | Receive coins from sender | `{ success }` |
| `/api/proof/:id` | GET | Download coin proof | Binary |

### Two-Phase Send Flow

User sends require a two-phase flow because the server doesn't hold sender private keys:

1. **`POST /api/send`** — server generates ZK proof, returns `proof_id` + `account_state_hash` + `output_coins_root`
2. **Client signs commitment** — `Schnorr(hash_concat(account_state_hash, output_coins_root))` with BIP-32 key at `numPubkeys`
3. **`POST /api/commit`** — server verifies commitment, broadcasts Taproot inscription, delivers coin to recipient via `receive_coin`

Mint uses a single-phase flow (server holds the minting account key).

## Project Structure

```
Expand All @@ -54,7 +68,7 @@ server/ # Axum REST API
│ └── publisher.rs # Taproot Inscription broadcaster (commit/reveal)
shared/ # Shared types (Commitment, Invoice, ClientAccount)
program/ # SP1 zkVM circuit types (AccountState, Coin, ProofData)
│ └── src/merkle/ # SMT + MMR implementations
├── src/merkle/ # SMT + MMR implementations
script/ # Prover (real SP1 zkVM — create_account, update_account)
```

Expand All @@ -63,9 +77,10 @@ script/ # Prover (real SP1 zkVM — create_account, update_accoun
| Variable | Default | Description |
|---|---|---|
| `SP1_PROVER` | `mock` | `mock` (no proof), `cpu`, `cuda`, or `network` |
| `ESPLORA_URL` | `https://mutinynet.com/api` | Bitcoin node API |
| `BITCOIN_RPC_USER` | — | Bitcoin Core RPC username |
| `BITCOIN_RPC_PASSWORD` | — | Bitcoin Core RPC password |
| `ESPLORA_URL` | `https://mutinynet.com/api` | Esplora API endpoint (electrs or public) |
| `IS_MAINNET` | `false` | `true` for Bitcoin Mainnet, `false` for Mutinynet/Signet |
| `NETWORK_NAME` | `Mutinynet` | Human-readable network name (returned by `/api/info`) |
| `PUBLISHER_KEY` | test key | 32-byte hex private key for inscription publishing. **Required on mainnet** — server panics if default test key is used |
| `RUST_LOG` | `info` | Log level |

## Docker
Expand All @@ -75,7 +90,7 @@ docker build -t zkcoin/server .
docker run -p 4242:4242 \
--network bitcoin \
-e SP1_PROVER=mock \
-e ESPLORA_URL=http://bitcoind-mainnet:8332 \
-e ESPLORA_URL=http://electrs-mainnet:3000 \
zkcoin/server
```

Expand All @@ -97,19 +112,17 @@ Staged scaling for the SP1 prover:

| Stage | When to move | Configuration |
|---|---|---|
| **1. CPU (current)** | Baseline | `SP1_PROVER=cpu` running on Mac Studio M3 Ultra, 96 GB unified memory. Measure `update_account` / `create_account` latency under real load before scaling further. |
| **0. Mock (DEV)** | Development & testing | `SP1_PROVER=mock` — no real proofs, instant responses. Required on DEV because CPU prover causes OOM (SP1 `update_account` exceeds available memory). |
| **1. CPU (PRD)** | Production baseline | `SP1_PROVER=cpu` running on Mac Studio M3 Ultra, 96 GB unified memory. `create_account` works, `update_account` needs memory tuning. |
| **2. Succinct Prover Network** | CPU latency becomes a bottleneck | `SP1_PROVER=network` — no hardware commitment, requires PROVE token deposit and accepts token-price exposure. See [docs.succinct.xyz](https://docs.succinct.xyz/docs/sp1/prover-network/quickstart). |
| **3. Self-hosted CUDA** | Network volume too costly or PROVE exposure undesirable | `SP1_PROVER=cuda` on x86 Linux with NVIDIA GPU (Compute Capability ≥ 8.6, ≥ 24 GB VRAM — RTX 4090 / 5090 / RTX 6000 Ada). Apple Silicon is not supported. |

Skip stages only with concrete latency or cost data, not assumptions.

## Open Tasks

- [x] CORS headers (allow frontend to call API directly)
- [x] Real SP1 proofs (CPU prover live on DEV/PRD)
- [ ] GPU acceleration (`SP1_PROVER=cuda`) or Succinct Prover Network
- [ ] Explorer endpoints (`/api/stats`, `/api/nullifiers`)
- [ ] Publisher key from environment variable (currently hardcoded)
- [ ] Light client support

## Related
Expand Down
Binary file modified elf/zkcoins-program
Binary file not shown.
21 changes: 9 additions & 12 deletions program/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
use merkle::{hash_concat, merkle_mountain_range::MMRProof};
use rand::Rng;
use serde::{Deserialize, Serialize};
use sha2::{Digest, Sha256};

use derive_builder::Builder;
use merkle::{
sparse_merkle_tree::{InclusionProof, NonInclusionProof, DEFAULT_HASHES}, HashDigest
sparse_merkle_tree::{InclusionProof, NonInclusionProof, DEFAULT_HASHES},
HashDigest,
};

pub type Amount = u64;
Expand Down Expand Up @@ -67,7 +67,10 @@ impl CommitmentMerkleProofs {
}
}

pub const MINTING_ADDRESS: HashDigest = [44, 153, 26, 227, 141, 88, 195, 127, 88, 144, 228, 143, 121, 49, 51, 158, 111, 205, 183, 53, 133, 35, 183, 240, 183, 165, 104, 116, 66, 228, 94, 242];
pub const MINTING_ADDRESS: HashDigest = [
175, 83, 161, 5, 16, 78, 44, 44, 237, 20, 140, 19, 48, 116, 86, 210, 247, 116, 223, 190, 106,
191, 59, 198, 226, 248, 55, 102, 143, 24, 155, 216,
];

pub fn hash(data: &[u8]) -> HashDigest {
Sha256::digest(data).into()
Expand Down Expand Up @@ -139,13 +142,7 @@ pub struct AccountState {

impl AccountState {
pub fn new(initial_public_key: PublicKey) -> Self {
// TODO: The randomness here is annoying why do we not hash the public key directly and
// skip the first one in the commitments?
// We add random bytes to the public key as a blinding factor for the address.
// This ensures that the on-chain commited public keys can not be linked to the address.
let mut rng = rand::thread_rng();
let random_bytes: [u8; 32] = rng.gen();
let address = hash(&[initial_public_key.clone(), random_bytes.to_vec()].concat());
let address = hash(&initial_public_key);
AccountState {
owner: address,
balance: 0,
Expand All @@ -160,7 +157,7 @@ impl AccountState {

self.balance = match self.balance.checked_add(coin.amount) {
Some(balance) => balance,
None => return Err("Receiving coin causes an overflow")
None => return Err("Receiving coin causes an overflow"),
};
Ok(self)
}
Expand All @@ -187,7 +184,7 @@ impl AccountState {
// Apply coin.
self.balance = match self.balance.checked_sub(coin.amount) {
Some(balance) => balance,
None => return Err("Balance too small to create Coin.")
None => return Err("Balance too small to create Coin."),
};
}

Expand Down
Loading
Loading