Batch Processing
Batch processing enables atomic execution of multiple settlements in a single transaction. The batch processor ensures all-or-nothing semantics: either all settlements in a batch succeed, or they all fail and state is rolled back. This is critical for complex payment flows involving multiple parties, escrow releases, and fee distributions.
Overview
The batch processor provides transactional guarantees for multi-party settlements. Use cases include:
- Marketplace escrow release — Simultaneous payment to seller, platform fee to treasury, and arbiter fee
- Multi-model inference — Parallel payment to multiple AI providers for ensemble predictions
- Reward distribution — Epoch-based validator rewards paid atomically to all validators
- Cross-chain settlement — Coordinated settlement across multiple chains via bridge adapters
- Agent swarm payments — Payment to multiple agents for collaborative task completion
Key Guarantees:
- Atomicity: All settlements succeed or all fail
- Consistency: Balance snapshots ensure rollback on failure
- Isolation: Batch execution locks affected accounts
- Durability: Successful batches synced to RocksDB with fsync
Batch Creation
A batch is a collection of SettlementRequest objects grouped under a unique batch ID. Each request in the batch can be a different settlement type (immediate, escrow, micropayment channel) and can involve different assets (TNZO, USDC, ETH).
use tenzro_settlement::{BatchProcessor, SettlementRequest, SettlementBatch};
use tenzro_types::{Address, Hash};
// Initialize batch processor
let processor = BatchProcessor::new(
settlement_engine.clone(),
fee_collector.clone(),
);
// Create batch of settlements
let batch_id = Hash::from_hex("0xbatch123...")?;
let requests = vec![
// Payment to primary model provider
SettlementRequest {
settlement_id: Hash::from_hex("0xsettlement1...")?,
from: Address::from_hex("0xpayer...")?,
to: Address::from_hex("0xprovider1...")?,
amount: 800_000_000_000_000_000, // 0.8 TNZO
asset: "TNZO".to_string(),
proof: Some(ServiceProof::TeeAttestation { /* ... */ }),
metadata: serde_json::json!({
"model": "gpt-4",
"inference_id": "inf_abc"
}),
},
// Payment to secondary model provider
SettlementRequest {
settlement_id: Hash::from_hex("0xsettlement2...")?,
from: Address::from_hex("0xpayer...")?,
to: Address::from_hex("0xprovider2...")?,
amount: 200_000_000_000_000_000, // 0.2 TNZO
asset: "TNZO".to_string(),
proof: Some(ServiceProof::TeeAttestation { /* ... */ }),
metadata: serde_json::json!({
"model": "claude-3.5-sonnet",
"inference_id": "inf_xyz"
}),
},
];
let batch = SettlementBatch {
batch_id: batch_id.clone(),
requests,
status: BatchStatus::Pending,
};
// Submit batch for atomic execution
processor.process_batch(batch).await?;Atomic Execution
The batch processor guarantees atomic execution through a three-phase protocol:
Phase 1: Snapshot
Before processing any settlements, the processor creates a snapshot of all account balances that will be modified. This enables full rollback if any settlement fails.
// Snapshot phase (internal to BatchProcessor)
let mut balance_snapshots = HashMap::new();
for request in &batch.requests {
// Snapshot payer balance
let payer_balance = account_store
.get_balance(&request.from, &request.asset)
.await?;
balance_snapshots.insert(
(request.from.clone(), request.asset.clone()),
payer_balance
);
// Snapshot payee balance
let payee_balance = account_store
.get_balance(&request.to, &request.asset)
.await?;
balance_snapshots.insert(
(request.to.clone(), request.asset.clone()),
payee_balance
);
}Phase 2: Execute
Each settlement in the batch is executed sequentially. If any settlement fails (insufficient balance, invalid proof, validation error), execution stops immediately and proceeds to rollback phase.
// Execute phase
let mut receipts = Vec::new();
for request in &batch.requests {
match settlement_engine.settle(request).await {
Ok(receipt) => {
receipts.push(receipt);
}
Err(e) => {
// Settlement failed - trigger rollback
error!("Settlement {} failed: {}", request.settlement_id.to_hex(), e);
// Restore all balances from snapshot
for ((address, asset), balance) in balance_snapshots {
account_store.set_balance(&address, &asset, balance).await?;
}
// Clear all receipts
receipts.clear();
// Mark batch as failed
batch.status = BatchStatus::Failed {
error: e.to_string(),
};
return Err(e);
}
}
}Phase 3: Finalize
If all settlements succeed, the batch is finalized by:
- Collecting network fees from all settlements
- Persisting settlement receipts to RocksDB
- Syncing balance changes with fsync for durability
- Updating batch status to
Completed - Broadcasting batch completion event to gossipsub
// Finalize phase
if receipts.len() == batch.requests.len() {
// All settlements succeeded
// Collect fees
for receipt in &receipts {
let fee = fee_collector.calculate_fee(receipt.amount);
fee_collector.collect(
&receipt.to,
&receipt.asset,
fee
).await?;
}
// Persist receipts to storage
for receipt in &receipts {
storage.store_receipt(receipt).await?;
}
// Sync to disk with fsync
storage.sync().await?;
// Update batch status
batch.status = BatchStatus::Completed {
receipts: receipts.clone(),
};
// Broadcast completion event
gossip.publish("tenzro/settlements/1.0.0", &batch).await?;
}Implementation:
The batch processor correctly snapshots balances before processing, stops on first failure, restores all balance changes on rollback, clears receipts, and updates batch status to Failed with error message. Atomicity is ensured.
All-or-Nothing Semantics
The batch processor guarantees that either all settlements in a batch complete successfully, or none of them do. This prevents partial execution states that could lead to fund loss or inconsistent accounting.
Example: Marketplace Escrow Release
Consider a marketplace where a buyer purchases a digital asset from a seller through an escrow. When the seller delivers the asset, the escrow is released in a batch containing three settlements:
let batch_requests = vec![
// 1. Payment to seller (85% of escrow)
SettlementRequest {
settlement_id: Hash::from_hex("0xpay_seller...")?,
from: Address::from_hex("0xescrow...")?,
to: Address::from_hex("0xseller...")?,
amount: 850_000_000_000_000_000, // 0.85 TNZO
asset: "TNZO".to_string(),
proof: Some(ServiceProof::MultiParty {
signers: vec![buyer_addr, arbiter_addr],
signatures: vec![buyer_sig, arbiter_sig],
threshold: 2,
}),
metadata: serde_json::json!({
"type": "seller_payment",
"asset_id": "nft_12345"
}),
},
// 2. Platform fee to marketplace (10% of escrow)
SettlementRequest {
settlement_id: Hash::from_hex("0xplatform_fee...")?,
from: Address::from_hex("0xescrow...")?,
to: Address::from_hex("0xmarketplace...")?,
amount: 100_000_000_000_000_000, // 0.1 TNZO
asset: "TNZO".to_string(),
proof: Some(ServiceProof::Cryptographic {
algorithm: "ed25519".to_string(),
signature: platform_sig,
public_key: platform_pubkey,
}),
metadata: serde_json::json!({
"type": "platform_fee",
"fee_percentage": 10
}),
},
// 3. Arbiter fee (5% of escrow)
SettlementRequest {
settlement_id: Hash::from_hex("0xarbiter_fee...")?,
from: Address::from_hex("0xescrow...")?,
to: Address::from_hex("0xarbiter...")?,
amount: 50_000_000_000_000_000, // 0.05 TNZO
asset: "TNZO".to_string(),
proof: Some(ServiceProof::Cryptographic {
algorithm: "ed25519".to_string(),
signature: arbiter_sig,
public_key: arbiter_pubkey,
}),
metadata: serde_json::json!({
"type": "arbiter_fee"
}),
},
];
let batch = SettlementBatch {
batch_id: Hash::from_hex("0xmarketplace_batch...")?,
requests: batch_requests,
status: BatchStatus::Pending,
};
// If any of the three payments fail (e.g., arbiter address invalid),
// all three are rolled back - seller doesn't get paid either
processor.process_batch(batch).await?;Without atomic batch processing, a failure in payment 3 (arbiter fee) could leave payments 1 and 2 (seller and platform) in a completed state, with no way to reverse them. The batch processor prevents this by rolling back the entire batch.
FeeCollector
The FeeCollector is responsible for calculating, collecting, and routing network fees to the treasury. All settlements (individual and batch) incur a network fee of 0.5% (50 basis points) by default.
Fee Calculation
use tenzro_settlement::FeeCollector;
let collector = FeeCollector::new(
treasury_addr,
50, // 0.5% = 50 basis points
);
// Calculate fee for settlement
let settlement_amount = 1_000_000_000_000_000_000; // 1 TNZO
let fee = collector.calculate_fee(settlement_amount);
// fee = 5_000_000_000_000_000 (0.005 TNZO)
let net_amount = settlement_amount - fee;
// net_amount = 995_000_000_000_000_000 (0.995 TNZO)Fee Collection
Fees are deducted from the settlement amount before crediting the recipient. The FeeCollector maintains per-asset fee accumulators and periodically flushes them to the network treasury.
// Fee collection (internal to SettlementEngine)
pub async fn settle(&self, request: &SettlementRequest) -> Result<Receipt> {
// Calculate network fee
let fee = self.fee_collector.calculate_fee(request.amount);
let net_amount = request.amount - fee;
// Debit full amount from payer
self.account_store.debit(
&request.from,
&request.asset,
request.amount
).await?;
// Credit net amount to payee
self.account_store.credit(
&request.to,
&request.asset,
net_amount
).await?;
// Collect fee to treasury
self.fee_collector.collect(
&request.to, // Fee paid by recipient
&request.asset,
fee
).await?;
Ok(Receipt {
settlement_id: request.settlement_id.clone(),
from: request.from.clone(),
to: request.to.clone(),
amount: net_amount,
fee,
asset: request.asset.clone(),
timestamp: SystemTime::now(),
})
}Treasury Routing
Collected fees are routed to the NetworkTreasury contract, which manages multi-asset reserves and multisig withdrawals. Fees fund validator rewards, protocol development, ecosystem grants, and network operations.
use tenzro_token::NetworkTreasury;
// Initialize treasury with multisig (3-of-5 threshold)
let treasury = NetworkTreasury::new(
vec![
Address::from_hex("0xkeeper1...")?,
Address::from_hex("0xkeeper2...")?,
Address::from_hex("0xkeeper3...")?,
Address::from_hex("0xkeeper4...")?,
Address::from_hex("0xkeeper5...")?,
],
3, // Threshold
);
// FeeCollector periodically flushes fees to treasury
pub async fn flush_to_treasury(&self) -> Result<()> {
let accumulated_fees = self.accumulated_fees.lock().await;
for (asset, amount) in accumulated_fees.iter() {
if *amount > 0 {
// Transfer fees to treasury
self.account_store.transfer(
&self.treasury_addr,
asset,
*amount
).await?;
info!(
"Flushed {} {} in fees to treasury",
amount,
asset
);
}
}
// Clear accumulators
accumulated_fees.clear();
Ok(())
}Network Fee (0.5%)
The default network fee of 0.5% (50 basis points) applies to all settlement types:
- Immediate settlements — Fee deducted before crediting recipient
- Escrow releases — Fee calculated on escrowed amount, paid on release
- Micropayment channels — Fee calculated on channel close, deducted from final settlement
- Cross-chain transfers — Fee on both source and destination chains (0.5% each = 1% total)
Fee Examples:
- 1 TNZO settlement → 0.005 TNZO fee (0.5%)
- 100 TNZO settlement → 0.5 TNZO fee (0.5%)
- 10,000 TNZO settlement → 50 TNZO fee (0.5%)
Fee Governance
The network fee percentage is a governable parameter that can be adjusted through on-chain proposals. Validators vote on fee changes, which take effect after a time delay to allow users to adjust.
use tenzro_token::GovernanceEngine;
// Submit proposal to change network fee
let governance = GovernanceEngine::new();
let proposal = governance.submit_proposal(
"Reduce Network Fee to 0.3%",
"Proposal to reduce settlement fee from 0.5% to 0.3%",
serde_json::json!({
"type": "parameter_change",
"parameter": "network_fee_bps",
"current_value": 50,
"proposed_value": 30,
"activation_delay": 604800, // 7 days
}),
).await?;
// Validators vote
for validator in validators {
governance.vote(proposal.id, validator.address, true).await?;
}
// After quorum reached and delay passed, fee updates automaticallyBatch Size Limits
To prevent denial-of-service attacks and ensure reasonable execution time, batches are subject to size limits:
- Max settlements per batch: 1,000
- Max total value per batch: 10,000,000 TNZO
- Max unique accounts per batch: 10,000
- Max batch execution time: 30 seconds
// Batch validation
pub async fn process_batch(&self, mut batch: SettlementBatch) -> Result<()> {
// Check batch size limit
if batch.requests.len() > 1000 {
return Err(SettlementError::BatchTooLarge {
size: batch.requests.len(),
max: 1000,
});
}
// Check total value limit
let total_value: u64 = batch.requests
.iter()
.filter(|r| r.asset == "TNZO")
.map(|r| r.amount)
.sum();
if total_value > 10_000_000 * 10u64.pow(18) {
return Err(SettlementError::BatchValueTooHigh {
value: total_value,
max: 10_000_000 * 10u64.pow(18),
});
}
// Set execution timeout
let timeout = Duration::from_secs(30);
tokio::time::timeout(timeout, self.execute_batch(&mut batch)).await??;
Ok(())
}Error Handling
Batch processing can fail for several reasons. The BatchProcessor provides detailed error information and partial execution state for debugging.
Common Failure Modes
match processor.process_batch(batch).await {
Ok(_) => {
info!("Batch completed successfully");
}
Err(SettlementError::InsufficientBalance { address, asset, required, available }) => {
error!(
"Settlement failed: {} needs {} {} but only has {}",
address.to_hex(),
required,
asset,
available
);
// All settlements in batch rolled back
}
Err(SettlementError::ProofVerificationFailed { settlement_id, reason }) => {
error!(
"Proof verification failed for {}: {}",
settlement_id.to_hex(),
reason
);
// All settlements in batch rolled back
}
Err(SettlementError::BatchTooLarge { size, max }) => {
error!("Batch contains {} settlements, max is {}", size, max);
// Batch rejected before execution
}
Err(SettlementError::PartialExecution { completed, failed_at, error }) => {
error!(
"Batch failed at settlement {} (completed {}): {}",
failed_at,
completed,
error
);
// All settlements rolled back via snapshot restoration
}
Err(e) => {
error!("Batch processing failed: {}", e);
}
}Performance Optimization
For large batches, the batch processor employs several optimizations:
- Balance caching — Account balances loaded once and cached during batch execution
- Parallel proof verification — ZK proofs and TEE attestations verified in parallel via tokio spawn
- Write batching — All storage writes buffered and committed in a single RocksDB write batch
- Lock coarsening — Account locks acquired once per account, not per settlement
// Parallel proof verification
let proof_futures: Vec<_> = batch.requests
.iter()
.filter_map(|r| r.proof.as_ref())
.map(|proof| {
let verifier = self.proof_verifier.clone();
tokio::spawn(async move {
verifier.verify(proof).await
})
})
.collect();
// Wait for all proofs to verify
let results = futures::future::join_all(proof_futures).await;
for result in results {
result??; // Fail batch if any proof invalid
}Integration with Settlement Engine
The batch processor wraps the settlement engine and delegates individual settlement execution. This ensures that all settlement logic (proof verification, balance checks, fee collection) is reused for both single and batch settlements.
pub struct BatchProcessor {
settlement_engine: Arc<SettlementEngine>,
fee_collector: Arc<FeeCollector>,
account_store: Arc<dyn AccountStore>,
storage: Arc<dyn SettlementStore>,
}
impl BatchProcessor {
pub async fn process_batch(&self, mut batch: SettlementBatch) -> Result<()> {
// Validate batch
self.validate_batch(&batch)?;
// Snapshot balances
let snapshots = self.snapshot_balances(&batch).await?;
// Execute settlements sequentially
let mut receipts = Vec::new();
for (index, request) in batch.requests.iter().enumerate() {
match self.settlement_engine.settle(request).await {
Ok(receipt) => receipts.push(receipt),
Err(e) => {
// Rollback all changes
self.restore_snapshots(snapshots).await?;
batch.status = BatchStatus::Failed {
error: e.to_string(),
failed_at: index,
};
return Err(e);
}
}
}
// Finalize batch
batch.status = BatchStatus::Completed { receipts };
self.storage.store_batch(&batch).await?;
Ok(())
}
}Next Steps
Now that you understand batch processing and fee collection, explore:
- Trusted Execution Environments — Hardware-backed computation for secure AI inference
- Zero-Knowledge Proofs — Privacy-preserving verification of inference results
- Governance — Participate in network parameter decisions
- Treasury — Multi-asset fee accumulation and multisig withdrawals