Benchmarking the cost and speed of different strategies for making private batch payments on Aleo using USDCx (compliant stablecoin).
Four strategies are compared, each sending USDCx to N recipients using transfer_private_with_creds:
One transaction at a time, chaining the change record through each transfer.
Lane 1: [tx1] → [tx2] → [tx3] → ... → [txN]
↑ each tx = 1 transfer, records chain through
- Records needed: 1 sender, 1 fee, 1 credentials
- Transactions: N
- Tradeoff: Simplest, cheapest per-recipient, but slowest
Recipients are divided across c concurrent lanes. Each lane processes its share sequentially, but all lanes run simultaneously.
Lane 1: [tx1] → [tx2] → [tx3] → [tx4] ← ceil(N/c) recipients
Lane 2: [tx1] → [tx2] → [tx3] → [tx4]
Lane 3: [tx1] → [tx2] → [tx3] → [tx4]
Lane 4: [tx1] → [tx2] → [tx3] → [tx4]
↑ all lanes run concurrently
- Records needed: c senders, c fees, c credentials
- Transactions: N (same as sequential)
- Tradeoff: Faster wall clock, same total fees, needs more records
A custom Leo program batches multiple transfer_private_with_creds calls into a single transaction using batch density b.
Lane 1: [batch_b] → [batch_b] → ... → [batch_b]
↑ each tx = b transfers in one transaction
ceil(N/b) txs total
- Records needed: 1 sender, 1 fee, 1 credentials
- Transactions: ceil(N/b)
- Tradeoff: Fewer transactions, but each tx has higher proving cost and fee
Combines batching and parallelism. c concurrent lanes each process batched transactions.
Lane 1: [batch_b] → [batch_b] ← ceil(N/c) recipients, batched
Lane 2: [batch_b] → [batch_b]
Lane 3: [batch_b] → [batch_b]
Lane 4: [batch_b] → [batch_b]
↑ all lanes concurrent, each tx = b transfers
- Records needed: c senders, c fees, c credentials
- Transactions: ceil(N/b) total across all lanes
- Tradeoff: Fastest wall clock, but highest per-tx cost
| Parameter | Values |
|---|---|
| Recipients (N) | 16, 64, 128, 256 |
| Concurrency (c) | 4, 8, 16, 32, unlimited |
| Batch Density (b) | 4, 8, 16 |
Not all combinations are tested — redundant and impractical configs are filtered out.
src/
shared/ # Shared infrastructure
types.ts # Interfaces
config.ts # Parameters, experiment grid
utils.ts # Proving, broadcasting, retries
setup-strategy.ts # Record splitting, credential provisioning
run-strategy.ts # Benchmark execution
generate-strategy-accounts.ts # Account creation and funding
strategies/
sequential/ # Each strategy is self-contained
strategy.ts # Strategy implementation
generate-accounts.ts # Account management
setup.ts # Record/credential provisioning
run.ts # Benchmark entry point
parallel/
batch/
hybrid/
programs/ # Leo programs (deployed to testnet)
batch_credits/ # Batched credits transfers (batch_4/8/16)
batch_usdcx/ # Batched USDCx transfers (batch_4/8/10)
big_batch_usdcx/ # batch_16 (separate due to variable limit)
split_credits/ # Tree-based credits record splitting
split_usdcx/ # Tree-based USDCx token splitting
batch_usdcx_credentials/ # Batched credential minting
burn_usdcx_credentials/ # Credential burning utility
results/ # Benchmark results (JSONL + analysis notebook)
sequential.jsonl
parallel.jsonl
batch.jsonl
hybrid.jsonl
summary.json
analysis.ipynb
report.ts # CLI report generator
verify-funding.ts # Verify account balances
verify-setup.ts # Verify setup records on-chain
show-funding.ts # Display funding requirements
- Node.js v22+
- Provable SDK (
@provablehq/sdk) - Leo v3.5+ (for compiling Leo programs)
- dotenv
- TypeScript v5+
- tsx (TypeScript execution)
npm install
pip install pandas matplotlib jupyter # for analysisCopy .env.example to .env and fill in:
MASTER_PRIVATE_KEY=APrivateKey1... # Master account for funding
RECIPIENT_ADDRESS=aleo1... # Target address for all transfers
PROVABLE_API_KEY=your_api_key # Provable API credentials
PROVABLE_CONSUMER_ID=your_consumer_id
ENDPOINT=https://api.provable.com/v2
The following programs must be deployed to Aleo testnet:
test_batch_credits.aleotest_batch_usdcx.aleotest_big_batch_usdcx.aleotest_split_credits_v2.aleotest_split_usdcx_v2.aleotest_batch_usdcx_credentials_v2.aleo
Build each with leo build in its src/programs/ directory.
Each strategy follows the same workflow:
npx tsx src/strategies/sequential/generate-accounts.ts generate# Fund all
npx tsx src/strategies/sequential/generate-accounts.ts fund
# Fund specific experiment
npx tsx src/strategies/parallel/generate-accounts.ts fund 16 4npx tsx src/verify-funding.ts src/strategies/sequential/accounts.json
# Specific experiment
npx tsx src/verify-funding.ts src/strategies/parallel/accounts.json 64 8# All configs for a strategy
npx tsx src/strategies/sequential/setup.ts
# Specific config
npx tsx src/strategies/parallel/setup.ts 16 4
npx tsx src/strategies/batch/setup.ts 64 8
npx tsx src/strategies/hybrid/setup.ts 128 8 4npx tsx src/verify-setup.ts sequential
npx tsx src/verify-setup.ts parallel 64 8# All configs for a strategy
npm run benchmark:sequential
npm run benchmark:parallel
npm run benchmark:batch
npm run benchmark:hybrid
# Specific config
npm run benchmark:sequential -- 16
npm run benchmark:parallel -- 64 8
npm run benchmark:batch -- 128 4
npm run benchmark:hybrid -- 256 8 4# CLI summary table
npm run report
# Jupyter analysis notebook
jupyter notebook src/results/analysis.ipynbRun npm run report to see the full results table, or open src/results/analysis.ipynb for visualizations.
Key metrics per experiment:
- Wall Clock Time — total elapsed time from first tx to last confirmation
- Total Fee — sum of all transaction fees in credits
- Cost/Recipient — fee per recipient
- Sec/Recipient — throughput (lower is better)
Discovered during development:
| Constraint | Value |
|---|---|
| Max transitions per transaction | 32 (31 usable + 1 fee) |
| Max deployment variables | 2,097,152 |
| Max async block captures | 16 (limits USDCx batch to 16) |
| credits.aleo/split remainder deduction | 10,000 microcredits per split |
| Batch program fees | ~26k (b=4), ~84k (b=8), ~296k (b=16) microcredits |
| Single transfer fee | ~3,747 microcredits |