DevelopersP2P NetworkingTesting

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)
  • 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

  1. Error Handling: Always implement proper error handling and logging to diagnose test failures.

  2. Network Delays: Include appropriate delays for network initialization and handshakes.

  3. Verification: Thoroughly verify all job outputs against expected results.

  4. Cleanup: Use temporary directories that are automatically cleaned up after tests.

  5. 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(())
}