Skip to main content

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:

ComponentMock ImplementationUse CaseGeneric Domain Uses
StorageIn-Memory StorageEphemeral storage for testsIn-Memory Storage
Base Layer RPCMock RPCSimulates base layer validator RPC without networkDefined in config file
ProvingMock Proving ClientValidates logic without generating proofsMock 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:

Cargo.toml
# 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:

domain.yaml
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