Testing with Mock Services
When developing a delta domain, you'll want to test your implementation without connecting to the live network or generating expensive cryptographic proofs. The delta_domain_sdk provides several mock services that let you develop and test your domain locally.
This tutorial shows how to configure your domain client with mock services for rapid development and testing, and how to transition to production when you're ready.
Overview
The SDK provides mock implementations for three key components:
| Component | Mock Implementation | Use Case | Generic Domain Uses |
|---|---|---|---|
| Storage | In-Memory Storage | Ephemeral storage for tests | In-Memory Storage |
| Base Layer RPC | Mock RPC | Simulates base layer validator RPC without network | Defined in config file |
| Proving | Mock Proving Client | Validates logic without generating proofs | Mock Proving Client |
When you did the Run Example Domain Quickstart you were already using most of them.
Default Configuration
When you create a domain (using Runtime::from_config or Runtime::builder),
mock services are used by default:
- in-memory storage (see below for details),
- mock RPC (see below for details), and
- mock proving client (no proof generation).
use delta_domain_sdk::{config::Config, Runtime};
let config = Config::load()?;
Runtime::from_config(config)
.build()
.await?;
In-Memory Storage
The storage backend is determined by the rocksdb cargo feature:
# Without rocksdb feature - uses in-memory storage (default)
delta_domain_sdk = { version = "..." }
# With rocksdb feature - uses RocksDB persistent storage
delta_domain_sdk = { version = "...", features = ["rocksdb"] }
In-memory storage means all vault data and domain state is stored in RAM and lost when the process exits. This is ideal for testing because each test run starts with a clean slate, there's no disk I/O overhead, and you don't need to clean up database files between test runs.
For production, you'll want to enable the rocksdb feature for persistent
storage.
Mock Base Layer (RPC)
Which base layer RPC is configured for the domain is determined by the rpc_url
field in your configuration file (e.g. domain.yaml). If rpc_url is set, the
domain connects to that RPC endpoint:
shard: 123
keypair: ./path/to/key.json
rpc_url: https://rpc.delta.network
If rpc_url is omitted, commented, or the domain is created with
Runtime::builder, a mock RPC is used instead. You can also explicitly
configure the RPC on the builder by calling with_rpc() or with_mock_rpc():
// Use a real RPC endpoint
Runtime::builder(shard, keypair)
.with_rpc("https://rpc.delta.network")
.build()
.await?;
// Explicitly override config with mock RPC
Runtime::from_config(shard, keypair)
.with_mock_rpc(HashMap::new())
.build()
.await?;
The mock base layer simulates a validator RPC without requiring network connectivity. It maintains an in-memory state of vaults, SDLs, and proofs.
Pre-populating Vaults
When testing transaction flows, your domain needs access to vaults with balances to debit from. Since the mock RPC doesn't connect to a real network, you need to pre-populate vaults with the initial state your tests require:
use delta_domain_sdk::{
base::vaults::{Address, Vault},
Runtime,
};
use std::collections::HashMap;
// Create a vault with an initial balance
let user_keypair = crypto::ed25519::PrivKey::generate();
let user_address = Address::new(user_keypair.pub_key().owner(), shard.get());
let mut vault = Vault::new(shard);
vault.set_balance(1_000_000); // 1 million native token Plancks 🤑
// Configure the domain with pre-populated vaults
let runtime = Runtime::builder(shard, operator_keypair)
.with_mock_rpc(HashMap::from([
(user_address, vault),
// ... add more vaults and addresses here
]))
.build()
.await?;
Simulating Base Layer Events
When connected to a real validator network, your domain receives events from the
base layer (e.g. when proofs are finalized or the epoch changes). To test how
your domain reacts to such events, you can simulate them using
Runtime::mock_base_layer_event:
use delta_domain_sdk::base::{
events::BaseLayerEvent,
sdl::{SdlStatus, SdlUpdate},
vaults::Address,
};
// Simulate a vault migrating into your domain
let address = Address::new(some_owner_id, shard.get());
runtime.mock_base_layer_event(Ok(
BaseLayerEvent::VaultImmigrated(address)
)).await?;
// Simulate an SDL being fully proven on the base layer
runtime.mock_base_layer_event(Ok(
BaseLayerEvent::SdlUpdate(SdlUpdate {
hash: sdl_hash,
originator: shard.get(),
status: SdlStatus::FullyProven,
})
)).await?;
Mock Proving Client
The mock proving client validates your transaction logic without generating cryptographic proofs. It runs the same validation code that would run inside the zkVM but skips the expensive proof generation. This lets you catch logic errors quickly during development without waiting for real proofs.
By default, the domain uses a mock proving client that only validates global
laws. This is equivalent to calling
.with_proving_client(mock::Client::global_laws()) on the domain builder.
To also test your local laws, add them to the mock client:
use delta_domain_sdk::{proving::mock, Runtime};
// Add local laws to the mock proving client
let runtime = Runtime::builder(shard, keypair)
.with_proving_client(
mock::Client::global_laws()
.with_local_laws::<MyLocalLaws>()
)
.build()
.await?;
For more details on the mock proving client and on implementing and testing local laws, see the Implementing Local Laws guide.
Complete Example
Here's a complete example showing how to set up a fully mocked domain for testing:
use delta_domain_sdk::{
base::{
crypto::ed25519::PrivKey,
vaults::{Address, Vault},
},
execution::default_execute,
proving::mock,
Runtime,
};
use std::{collections::HashMap, num::NonZero, time::Duration};
#[tokio::test]
async fn test_transaction_flow() -> Result<(), Box<dyn std::error::Error>> {
// Setup
let shard = NonZero::new(1).unwrap();
let operator_keypair = PrivKey::generate();
let user_keypair = PrivKey::generate();
// Create a vault with initial balance
let user_address = Address::new(user_keypair.pub_key().owner(), shard.get());
let mut vault = Vault::new(shard);
vault.set_balance(100_000);
// Build runtime with all mocks
let runtime = Runtime::builder(shard, operator_keypair)
.with_mock_rpc(HashMap::from([(user_address, vault)]))
.with_proving_client(mock::Client::global_laws())
.build()
.await?;
// Run the runtime
tokio::spawn(async { runtime.run().await });
// Execute your test scenario
let messages = vec![/* your verifiables */];
runtime.apply(default_execute(messages)).await?;
// Submit and prove
if let Some(sdl_hash) = runtime.submit().await? {
runtime.prove(sdl_hash).await?;
runtime.submit_proof(sdl_hash).await?;
}
Ok(())
}
Transitioning to Production
When you're ready to move from testing to production, you'll need to replace the mock services with real implementations.
1. Replace In-Memory Storage with RocksDB
Enable the rocksdb feature in your Cargo.toml:
[dependencies]
delta_domain_sdk = { version = "...", features = ["rocksdb"] }
This already gives you a RocksDB-backed storage (without changing any code). If
you want to additionally configure your RocksDB-storage, you can use
.with_rocksdb(). For example, to configure the database path:
use delta_domain_sdk::storage::options::DbOptions;
let db_options = DbOptions::default()
.with_db_prefix_path("/var/lib/my-domain/data");
let runtime = Runtime::builder(shard, keypair)
.with_rocksdb(db_options)
.build()
.await?;
2. Replace Mock RPC with Real RPC
Set rpc_url in your config file and load Runtime::from_config.
Or manually connect to an actual base layer network:
let runtime = Runtime::builder(shard, keypair)
.with_rpc("https://rpc.delta.network") // Real RPC endpoint
.build()
.await?;
3. Replace Mock Proving with SP1
Switch to the SP1 proving client for cryptographic proofs. First, you have to activate the SP1 cargo feature:
[dependencies]
delta_domain_sdk = { version = "...", features = ["rocksdb", "sp1"] }
Then, configure your sp1::Client to use one of the supported back-ends (CPU,
GPU, remote, ...). For example, CPU-based:
use delta_domain_sdk::proving::sp1;
let runtime = Runtime::builder(shard, keypair)
.with_proving_client(sp1::Client::global_laws_cpu())
.build()
.await?;
See the API docs of the SP1 client for all options.
Bonus: Ensure Domain Agreement
Before your domain can productively run and connect to the base layer, it needs
a valid Domain Agreement. Use Runtime::ensure_domain_agreement or a similar
method to create that.
Next Steps
- Explore the domain SDK API documentation for more configuration options