A batch processing arithmetic demo demonstrating the vApp Architecture
Based off the template for creating an end-to-end SP1 project that can generate Zero-Knowledge proofs for batched transactions with continuous balance tracking.
This project demonstrates batch processing with Zero-Knowledge proofs for continuous balance tracking:
- Continuous Balance: An internal counter is continuously updated by user transactions
- Batch Processing: Transactions are grouped into batches (FIFO) and proven together
- Zero-Knowledge Privacy: Individual transaction amounts remain private in batches
- Authenticated Data Structure: Merkle roots link counter states to cryptographic commitments
Initial balance: 10
User submits: +5, +7 β Batched together
ZK Proof: "Balance went from 10 to 22" (without revealing +5, +7)
New balance: 22
script/- Local SP1 unit testing for batch proofs (cargo runfor fast development)cli/- Batch processing client for submitting transactions and managing batchesapi/- Production server with batch creation and ZK proof generation via Sindridb/- PostgreSQL with batch processing tables and Merkle tree state managementlib/- Pure computation logic for processing transaction batches (zkVM compatible)program/- RISC-V program for proving batch transitions:initial_balance + [tx1, tx2, ...] = final_balance
π‘ Pro Tip: Use the included Makefile for even simpler commands:
make setup # Install dependencies + copy .env + initialize database
# Update .env file with needed env vars
make deploy # Deploy circuit to Sindri
make up # Start servicesDatabase Initialization: If you just need to set up the database for offline development:
make initDB # Start DB, run migrations, generate SQLx cache, then stop DBmake setup
# This calls ./install-dependencies.shcp .env.example .env
# Edit .env and add your Sindri API key for proof generation and circuit deployment:
# Get your API key from https://sindri.app
# SINDRI_API_KEY=your_sindri_api_key_here# Set the SINDRI_CIRCUIT_TAG in the .env file
make deploy
# This calls ./deploy-circuit.shNote: This step is required for proof generation. Without deploying the circuit, you can still run the server and submit transactions, but proof generation will fail.
# Start database + API server (uses pre-built image from GitHub Container Registry)
make up
# Verify services are running
docker-compose ps
# Check server health
curl http://localhost:8080/api/v2/healthFor Local Development: If you're actively developing and want to build the Docker image locally for faster iteration:
# Option 1: Use the development compose file, this will re-build the server dockerfile
make up-dev
# Option 2: Run API server locally (requires PostgreSQL running)
docker-compose up postgres -d
cargo run --bin server --releaseOption A: Direct HTTP API
# Submit individual transactions to batch queue
curl -X POST http://localhost:8080/api/v2/transactions \
-H 'Content-Type: application/json' \
-d '{"amount": 5}'
curl -X POST http://localhost:8080/api/v2/transactions \
-H 'Content-Type: application/json' \
-d '{"amount": 7}'
# View pending (unbatched) transactions
curl http://localhost:8080/api/v2/transactions/pending
# Trigger batch creation and get contract data (public/private split)
curl -X POST http://localhost:8080/api/v2/batches \
-H 'Content-Type: application/json' \
-d '{}'
# Get current counter state
curl http://localhost:8080/api/v2/state/currentOption B: CLI Client (Recommended)
π‘ Note: The CLI now supports the complete batch processing workflow with ZK proof verification.
# Check API server health
cargo run --bin cli -- health-check
# Submit transactions to the batch queue
cargo run --bin cli -- submit-transaction --amount 5
cargo run --bin cli -- submit-transaction --amount 7
# View all pending transactions
cargo run --bin cli -- view-pending
# Trigger batch creation with verbose contract data
cargo run --bin cli -- trigger-batch --verbose
# Get current counter state and Merkle root
cargo run --bin cli -- get-current-state
# List all historical batches
cargo run --bin cli -- list-batches
# Get specific batch details
cargo run --bin cli -- get-batch --batch-id 1
# Download proof data for local verification (when ready)
cargo run --bin cli -- download-proof --batch-id 1
# Verify proof locally using the downloaded JSON file
cargo run --bin cli -- verify-proof \
--proof-file proof_batch_1.json \
--expected-initial-balance 0 \
--expected-final-balance 12For fast local SP1 unit testing during zkVM program development:
# Quick SP1 batch processing test from root directory
cargo run --release
# This tests: initial_balance=10 + [5, 7] β final_balance=22
# Generates Core proof in ~8 seconds without database dependenciesThis provides a fast feedback loop for SP1 development, testing that a batch of transactions [5, 7] correctly transitions the balance from 10 to 22 while keeping individual amounts private.
That's it! π You now have a running zero-knowledge arithmetic server with multiple interaction methods.
Note for Linux users:
- After running the install script, you may need to log out and back in (or restart your terminal) for Docker group membership to take effect. You can verify Docker is working by running
docker --versionanddocker compose version. - The script installs OpenSSL development libraries (
libssl-dev) required for Rust crates compilation. - If you encounter OpenSSL-related compilation errors, ensure you have the latest packages:
sudo apt-get update && sudo apt-get install -y libssl-dev pkg-config
Installed Tools: The script installs all necessary development tools including Rust toolchain, SP1, Foundry, Docker, Node.js, PostgreSQL client tools, sqlx-cli for database migrations, and other utilities.
For developers who want to run cargo check or work on non-database code without starting PostgreSQL, this project supports SQLx offline mode:
# Set offline mode and run cargo check
export SQLX_OFFLINE=true
cargo check --workspace
# This works without any database connection!The project includes pre-generated SQLx query cache files (.sqlx/ directory) that contain compile-time query metadata. This allows SQLx macros to validate SQL queries during compilation without connecting to a live database.
You need to regenerate the SQLx cache when:
- Database schema changes (new tables, columns, migrations)
- SQL queries in the code are modified
- You encounter compilation errors about missing query metadata
# Recommended way: Use the make command (fully automated)
make initDB
# Manual way:
docker-compose up postgres -d
cd db && sqlx migrate run && cd ..
cargo sqlx prepare --workspaceThe cache files in .sqlx/ are committed to version control, so most developers won't need to regenerate them unless they're working on database-related changes.
- β
Fast compilation - No database connection required for
cargo check - β CI/CD friendly - Works in environments without PostgreSQL
- β Developer productivity - Work on application logic without database setup
- β Offline development - Code and compile anywhere
The project includes a comprehensive Makefile with convenient shortcuts for common tasks:
make help- Show all available commands with descriptionsmake install- Install all dependencies via./install-dependencies.shmake env- Copy.env.exampleto.envmake setup- Complete setup: install dependencies, copy .env, initialize databasemake initDB- Initialize database (start, migrate, generate SQLx cache, stop)
make run- Local SP1 unit testing (~8s Core proofs, no database needed)make cli ARGS="..."- Run CLI client (e.g.,make cli ARGS="health-check")make server- Start API server locally (requires database)make test- Run all testsmake forge-build- Build smart contractsmake forge-test- Run smart contract tests
make up- Start services using pre-built Docker imagemake up-dev- Start services with local Docker buildmake down- Stop all servicesmake deploy- Deploy circuit to Sindrimake deploy-contract- Deploy Arithmetic smart contractmake deploy-contract-help- Show smart contract deployment help
make docker-build- Build Docker image locallymake docker-push- Build and push image to GitHub registry
make clean-docker- Clean up Docker resourcesmake clean-sqlx- Remove.sqlx/cache directorymake clean-builds- Removetarget/,build/,ADS/directoriesmake clean- Clean up all resources (docker, sqlx, builds)
# Complete setup from scratch
make setup
# Quick SP1 test
make run
# Start production services
make up
# CLI examples
make cli ARGS="submit-transaction --amount 5"
make cli ARGS="list-batches"
make cli ARGS="health-check"For full batch processing with database and ZK proof generation:
# Start database + API server
make up
# Verify services
curl http://localhost:8080/api/v2/health# Submit individual transactions
cargo run --bin cli -- submit-transaction --amount 5
cargo run --bin cli -- submit-transaction --amount 7
# View pending transactions
cargo run --bin cli -- view-pending
# Create batch and trigger ZK proof generation
cargo run --bin cli -- trigger-batch --verbose
# Monitor batch status
cargo run --bin cli -- list-batches
cargo run --bin cli -- get-batch --batch-id 1# Download proof data when ready
cargo run --bin cli -- download-proof --batch-id 1
# Verify locally (no network dependencies)
cargo run --bin cli -- verify-proof \
--proof-file proof_batch_1.json \
--expected-initial-balance 0 \
--expected-final-balance 12- Privacy: Individual transaction amounts
[5, 7]remain hidden - Correctness: Balance transition cryptographically verified
- Trustless: External parties can verify without database access
- Offline: Works without network once proof data is downloaded
The project features automated smart contract posting for proven batches. After batches are created and proven, a background process automatically detects proven batches and posts state roots to the Ethereum smart contract, providing a fully automated pipeline from batch creation to on-chain state updates.
π Complete End-to-End Automation:
- Submit Transactions: Use CLI to submit individual transactions
- Create Batch: Trigger batch creation from pending transactions
- ZK Proof Generation: Background process generates proof via Sindri
- Smart Contract Posting: Background process automatically posts state roots to contract
- Status Tracking: Database tracks posting status and timestamps
The CLI workflow is the same as described in Production Batch Processing. The background process automatically handles:
- β ZK proof generation via Sindri
- β Smart contract posting when proof is ready
- β Database status updates with timestamps
For automated smart contract posting, configure these environment variables:
# Required for smart contract integration
export ETHEREUM_RPC_URL=https://eth-mainnet.alchemyapi.io/v2/demo
export ETHEREUM_CONTRACT_ADDRESS=0x1234567890123456789012345678901234567890
export ETHEREUM_WALLET_PRIVATE_KEY=your_private_key_without_0x_prefix
export ETHEREUM_DEPLOYER_ADDRESS=0x1234567890123456789012345678901234567890
export SINDRI_API_KEY=your_sindri_api_key_here- Automatic Detection: Scans for proven batches every 30 seconds
- Smart Contract Submission: Posts state roots using ethereum-client
- Random State Roots: Uses 32-byte hashes (temporary until ADS integration)
- Error Handling: Graceful fallback if Ethereum client not configured
- Rate Limiting: Controlled submission rate to avoid network congestion
- Audit Trail: Complete database tracking of posting status and timestamps
New tracking columns in proof_batches:
posted_to_contract BOOLEAN DEFAULT FALSE -- Tracks if batch posted to contract
posted_to_contract_at TIMESTAMP -- Timestamp of successful posting- Zero Manual Intervention: Fully automated pipeline from CLI to blockchain
- Fault Tolerance: Background process handles retries and error recovery
- Audit Trail: Complete database tracking of batch lifecycle
- Scalable: Handles multiple batches concurrently with rate limiting
- Production Ready: Comprehensive error handling and monitoring
Deploy the Arithmetic smart contract to Ethereum-compatible networks using the included Makefile commands.
Before deploying, you'll need:
- An Ethereum RPC endpoint (e.g., Alchemy, Infura, or local node)
- A wallet private key for deployment
- The SP1 verifier contract address for your target network
- Your program verification key (generated during circuit compilation)
# 1. Set required environment variables
export ETHEREUM_RPC_URL="https://eth-mainnet.g.alchemy.com/v2/your-api-key"
export ETHEREUM_WALLET_PRIVATE_KEY="your-private-key-without-0x-prefix"
export VERIFIER_CONTRACT_ADDRESS="0x1234567890123456789012345678901234567890"
export PROGRAM_VKEY="0xabcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890ab"
# 2. Deploy the contract
make deploy-contractAlternative using .env file:
# Add the variables to your .env file, then:
export $(cat .env | grep -v '^#' | xargs) && make deploy-contractGet help with deployment:
make deploy-contract-help# Build smart contracts
make forge-build
# Run smart contract tests
make forge-testWhen deployment succeeds, you'll see output similar to:
β
All environment variables are set
π Deploying contract...
π‘ RPC URL: https://eth-mainnet.g.alchemy.com/v2/your-api-key
π Verifier: 0x1234567890123456789012345678901234567890
ποΈ Program VKey: 0xabcdef...
[β ] Compiling...
[β ] Deploying Arithmetic on eth...
β
Hash: 0xabc123...
Contract Address: 0xdef456...
β
Contract deployed successfully!
Save the contract address for use in your application's environment configuration.
This project integrates with Sindri for serverless zero-knowledge proof generation, providing a scalable alternative to local SP1 proving.
For Sindri API key setup, see the Environment Variables section in Quick Start. The proof generation process:
- Creates SP1 inputs and serializes them for Sindri
- Generates EVM-compatible proofs (Groth16 by default)
- Submits proof request to Sindri using the
demo-vappcircuit - Stores proof metadata in PostgreSQL
- Returns proof ID for external verification
The batch proof generation process:
- Collects pending transactions into a batch (FIFO order)
- Creates SP1 inputs:
initial_balanceandtransactions: Vec<i32> - Generates EVM-compatible batch proofs (Groth16 by default) via Sindri
- Submits proof request to Sindri using the
demo-vappcircuit - Stores batch metadata and proof ID in PostgreSQL
- Associates Merkle root with the proven state transition
- Returns batch ID and contract submission data (public/private split)
For Smart Contract Integration environment variables, see the Environment Setup section under Smart Contract Integration.
The project includes a comprehensive GitHub Actions workflow (.github/workflows/sindri.yml) that:
- Lints the circuit using Sindri CLI
- Builds the SP1 program with the current branch/PR
- Deploys the circuit to Sindri with a unique tag
- Generates a zero-knowledge proof (7 + 13 = 20)
- Verifies the proof using external verification (no database required)
Branch Tagging Strategy:
- Main branch:
main-<commit-sha> - Pull requests:
pr-<number>-<branch-name>
This ensures each deployment is uniquely tagged and traceable to the source code.
The zero-knowledge batch proofs demonstrate that:
- You know the individual transaction amounts in a batch (e.g.,
[5, 7]) - The batch correctly transitions the balance (e.g.,
10 β 22) - The computation was performed correctly according to the batching rules
- Privacy: Individual transaction amounts remain hidden from public view
- Integrity: The balance transition is cryptographically proven without revealing private data
- Authenticity: No one can forge this proof without knowing the actual transaction batch
Public: "Balance went from 10 to 22" + ZK Proof + Merkle Root
Private: Individual amounts [5, 7] (never revealed on-chain)
Understanding the verification flow through analogy:
1. In Digital Signing:
- Private key: Can only sign messages
- Public key: Can only verify signatures
- The only "computation" being proven is "I signed this message"
2. In ZK Proving:
- Proving key: Can only generate proofs for a specific compiled program (circuit) with specific public inputs and some private witness
- Verification key: Can only verify proofs for that exact program, using the same public inputs
- The "computation" being proven is whatever the compiled program defines β e.g., "I took oldRoot and a private batch of transactions, applied the rules, and got newRoot"
3. Key Difference from Normal Signatures:
- In signatures, the message can be arbitrary; the private key doesn't "know" or "care" about what's inside, it just signs bytes
- In ZK, the PK/VK pair encodes the program itself β the rules for what constitutes a valid computation
- Change the program β you must regenerate both PK and VK
4. Why Both PK and VK Contain the "Same Compiled Program Steps": When you do the "setup" for a circuit (trusted or transparent), the compiler:
- Turns your high-level program into a low-level constraint system (R1CS, AIR, etc.)
- Generates a proving key containing all the extra metadata needed to construct a proof from a witness
- Generates a verification key containing the compressed commitments needed to check that a proof corresponds to that exact constraint system
- Because they are derived from the same constraints, PK and VK are inseparable as a pair β a VK from one circuit can't verify proofs from another
5. In Your vApp Case:
- PK = off-chain, owned by your prover (Arda sequencer/prover cluster)
- VK = on-chain, baked into the global settlement contract for that namespace
- Proof = ephemeral artifact generated per batch, posted with public inputs
- Verification = anyone with VK + proof + public inputs can check correctness β no need for the PK or the private data
- Serverless Proving: No need to set up SP1 proving infrastructure
- Scalable: Generate multiple proofs in parallel
- Optimized: Sindri's infrastructure is optimized for proof generation
- Verified: Server-side verification ensures proof correctness
- Production Ready: Suitable for production ZK applications
The API server will start on http://localhost:8080 by default.
Transaction Operations:
POST /api/v2/transactions- Submit individual transactions to batch processing queueGET /api/v2/transactions/pending- View all pending (unbatched) transactions
Batch Operations:
POST /api/v2/batches- Create batch from pending transactions and get contract dataGET /api/v2/batches- List all historical batchesGET /api/v2/batches/{batch_id}- Get specific batch detailsPOST /api/v2/batches/{batch_id}/proof- Update batch with ZK proof from Sindri
State Operations:
GET /api/v2/state/current- Get current counter state and Merkle root statusGET /api/v2/state/{batch_id}/contract- Get contract submission data (public/private split)
System Operations:
GET /api/v2/health- Health check and service statusGET /api/v2/info- API information and capabilities
For complete usage examples with curl commands, see the Quick Start section's "Option A: Direct HTTP API".
The system provides a clean separation between batch proof generation (via the API server) and proof verification (done locally):
Use the CLI or HTTP API as described in the Quick Start section to submit transactions and create batches. The response includes batch_id for later verification.
# Get raw proof data for local verification (when proof is ready)
cargo run --bin cli -- download-proof --batch-id 1
# Downloads proof_batch_1.json containing:
# - proof_data: hex-encoded SP1 proof
# - public_values: hex-encoded public values
# - verifying_key: hex-encoded verification key
# - initial_balance and final_balance for verification# Run local verification tool with downloaded batch proof
cargo run --bin cli -- verify-proof \
--proof-file proof_batch_1.json \
--expected-initial-balance 0 \
--expected-final-balance 12 \
--verbose
# Output:
# β
Balance validation PASSED (0 β 12)
# β
Structure validation PASSED
# π Batch proof structure successfully verified!
# β’ Privacy: Individual transaction amounts [5, 7] remain hidden
# β’ Correctness: Balance transition verified- No Docker Dependencies: Verification runs on any machine with Rust
- Trustless: No need to trust the API server for verification
- Privacy: All verification happens locally
- Offline: Works without network access once proof data is downloaded
- Portable: Verification can be done on any machine or integrated into other systems
This enables trustless verification where external parties can cryptographically verify computation results without seeing private inputs, requiring database access, or trusting external services.