Testing Multi-node Blueprints
This guide covers practical testing patterns for blueprints that require multiple nodes (threshold cryptography, aggregation, consensus-style coordination).
What “multi-node” means in practice
You generally need two layers in tests:
- A deterministic chain seeded with Tangle contracts (so addresses, IDs, and event shapes match production).
- Multiple runner instances (processes or tasks) configured with distinct keystores and networking ports.
Deterministic local chain (Tangle)
The SDK ships an Anvil harness that replays tnt-core/script/v2/LocalTestnet.s.sol broadcast artifacts so the local chain matches the canonical devnet deployment.
use blueprint_sdk::testing::utils::anvil::{
harness_builder_from_env, LOCAL_BLUEPRINT_ID, LOCAL_SERVICE_ID,
};
#[tokio::test]
async fn spins_up_seeded_tangle_evm() -> anyhow::Result<()> {
let harness = harness_builder_from_env().spawn().await?;
// The seeded contracts are exposed on the harness.
let tangle = harness.tangle_contract;
let restaking = harness.restaking_contract;
let status_registry = harness.status_registry_contract;
// Seeded IDs baked into LocalTestnet.
let blueprint_id = LOCAL_BLUEPRINT_ID;
let service_id = LOCAL_SERVICE_ID;
Ok(())
}Running your blueprint logic in-process
For unit/integration tests that don’t need full operator processes, use the TestRunner utility to build a BlueprintRunner with a router and context.
use blueprint_sdk::runner::config::BlueprintEnvironment;
use blueprint_sdk::testing::utils::runner::TestRunner;
#[tokio::test]
async fn runs_a_blueprint_router() -> anyhow::Result<()> {
let env = BlueprintEnvironment::default();
let config = /* your BlueprintConfig (tangle-evm, eigenlayer, etc.) */;
let mut runner = TestRunner::new(config, env);
runner.add_job(my_job);
runner.add_background_service(my_background_service);
runner.run(my_context).await?;
Ok(())
}Full multi-node testing
For protocols that require real peer-to-peer behavior:
- Run N runner processes, each with:
- a unique
keystore_uri - a unique
network_bind_port(if using P2P networking) - the same chain RPC endpoints (from the Anvil harness)
- a unique
- Use your test to:
- create (or reuse) a service on-chain
- wait for heartbeats / peer discovery to stabilize
- submit jobs and assert results
If you need an end-to-end reference, see the SDK’s deterministic harness implementation in crates/testing-utils/anvil/src/tangle_evm.rs (it documents what is seeded and how).
Best Practices
-
Error Handling: Always implement proper error handling and logging to diagnose test failures.
-
Network Delays: Include appropriate delays for network initialization and handshakes.
-
Verification: Thoroughly verify all job outputs against expected results.
-
Cleanup: Use temporary directories that are automatically cleaned up after tests.
-
Logging: Implement comprehensive logging to track test progress and debug issues.
Example: Complete Test Structure
Here’s a complete example showing how to structure a multi-node test:
#[tokio::test(flavor = "multi_thread")]
async fn test_blueprint() -> color_eyre::Result<()> {
logging::setup_log();
let tmp_dir = tempfile::TempDir::new()?;
let harness = TangleTestHarness::setup(tmp_dir).await?;
// Initialize nodes
let (mut test_env, service_id, ) = harness.setup_services::<3>(false).await?;
test_env.initialize().await?;
// Configure nodes
let handles = test_env.node_handles().await;
for handle in handles {
// Add handlers
// ...
}
// Wait for network setup
tokio::time::sleep(std::time::Duration::from_secs(10)).await;
test_env.start().await?;
// Run test jobs
let job = harness.submit_job(service_id, JOB_ID, vec![/ args /]).await?;
let results = harness.wait_for_job_execution(service_id, job).await?;
// Verify results
assert_eq!(results.service_id, service_id);
// Additional verification...
Ok(())
}