Introduction
ZombieNet SDK is a Rust testing framework for Polkadot SDK-based blockchains. It spawns ephemeral blockchain networks programmatically for testing and development.
The SDK succeeds the original ZombieNet (TypeScript), offering a type-safe, composable Rust API.
Migrating from TypeScript ZombieNet? The TOML configuration format is largely compatible. See Network Definition Spec for the full reference.
Key Features
- Programmatic Network Spawning - Define and spawn relay chains and parachains from Rust code
- Fluent Builder API - Compose networks using a chainable builder pattern
- Multiple Providers - Run on Kubernetes, Podman, Docker, or natively
- Metrics & Assertions - Query Prometheus metrics and validate network behavior
- Subxt Integration - Interact with networks using subxt
Use Cases
- Integration Testing - Test cross-chain functionality with complete networks
- CI/CD Pipelines - Automate network deployment and testing
- Development - Spin up local networks for development and debugging
- Parachain Testing - Test registration, block production etc.
Next
See Getting Started to spawn your first network.
Getting Started
This guide walks you through setting up ZombieNet SDK and spawning your first network.
- Setup - Prerequisites and installation
- Your First Network - Spawn a relay chain and parachain
Next
- Network Definition Spec - Configuration reference
- Providers - Native, Docker, Kubernetes
- CLI Usage - CLI reference
- Examples - More configurations and use cases
Setup
Prerequisites
- Rust - Install via rustup
- A provider (at least one):
- Native: Binaries used by your network in PATH (or absolute paths in config), e.g.,
polkadot,polkadot-parachain - Docker/Podman: Container runtime installed and running
- Kubernetes: Cluster access with
kubectlconfigured
- Native: Binaries used by your network in PATH (or absolute paths in config), e.g.,
Installation
CLI
Download pre-built binaries from GitHub Releases, or install via Cargo:
cargo install zombie-cli
Or build from source:
git clone https://github.com/paritytech/zombienet-sdk
cd zombienet-sdk
cargo build --release -p zombie-cli
# Binary will be at target/release/zombie-cli
As a Dependency
Add to your Cargo.toml:
[dependencies]
zombienet-sdk = "0.4"
Your First Network
Using TOML
Create network.toml:
[relaychain]
chain = "rococo-local"
default_command = "polkadot"
[[relaychain.nodes]]
name = "alice"
[[relaychain.nodes]]
name = "bob"
Spawn with the CLI:
zombie-cli spawn network.toml --provider native
# or: --provider docker
Programmatically
The same network in Rust:
use zombienet_sdk::{NetworkConfigBuilder, NetworkConfigExt}; #[tokio::main] async fn main() -> Result<(), Box<dyn std::error::Error>> { let network = NetworkConfigBuilder::new() .with_relaychain(|r| { r.with_chain("rococo-local") .with_default_command("polkadot") .with_validator(|v| v.with_name("alice")) .with_validator(|v| v.with_name("bob")) }) .build()? .spawn_native() .await?; let alice = network.get_node("alice")?; println!("Alice WS: {}", alice.ws_uri()); tokio::signal::ctrl_c().await?; Ok(()) }
Adding a Parachain
TOML
Extend network.toml:
[relaychain]
chain = "rococo-local"
default_command = "polkadot"
[[relaychain.nodes]]
name = "alice"
[[relaychain.nodes]]
name = "bob"
[[parachains]]
id = 1000
[[parachains.collators]]
name = "collator01"
command = "polkadot-parachain"
Programmatically
use zombienet_sdk::{NetworkConfigBuilder, NetworkConfigExt}; #[tokio::main] async fn main() -> Result<(), Box<dyn std::error::Error>> { let network = NetworkConfigBuilder::new() .with_relaychain(|r| { r.with_chain("rococo-local") .with_default_command("polkadot") .with_validator(|v| v.with_name("alice")) .with_validator(|v| v.with_name("bob")) }) .with_parachain(|p| { p.with_id(1000) .with_collator(|c| { c.with_name("collator01") .with_command("polkadot-parachain") }) }) .build()? .spawn_native() .await?; let collator = network.get_node("collator01")?; println!("Collator WS: {}", collator.ws_uri()); tokio::signal::ctrl_c().await?; Ok(()) }
How It Works
This document covers the two core aspects of what ZombieNet does under the hood: chain spec customization and command-line argument generation.
Chain Spec Customization
When spawning a network, ZombieNet modifies chain specifications to configure the test environment. These customizations happen automatically based on your network configuration.
Relay Chain Customizations
The framework applies these modifications to relay chain specs:
| Customization | Description |
|---|---|
| Runtime Genesis Patch | Merges user-provided genesis_overrides JSON (applied first): objects merge, any other value replaces |
| Genesis Patch | Applies the genesis_patch RFC 6902 operations after the overrides; add at /- appends to a list. Needs a plain chain spec. A replace path, or the parent of an add path, must exist once genesis is merged. The steps below run after the patch, so what they rewrite (authorities, stakers, invulnerables, validatorCount, node balances) wins over it |
| Clear Authorities | Removes existing authorities from session/keys, aura/authorities, grandpa/authorities, collatorSelection/invulnerables, and staking/invulnerables + staking/stakers (relay only). Also sets validatorCount to 0 unless devStakers is configured. |
| Add Balances | Adds balances for each node's sr and sr_stash accounts. Balance is max(initial_balance, staking_min * 2). Only nodes with initial_balance > 0 receive balances. |
| Add Zombie Account | Adds //Zombie account with 1000 units (in chain denomination, i.e., 1000 * 10^token_decimals planck) for internal operations |
| Add Staking | Configures staking for validators with minimum stake |
| Add Session Authorities | Adds validators as session authorities (if session pallet exists) |
| Add Aura/Grandpa Authorities | For chains without session pallet |
| Add HRMP Channels | Configures cross-chain messaging channels |
| Register Parachains | Adds InGenesis parachains to genesis |
Parachain Customizations
For cumulus-based parachains:
| Customization | Description |
|---|---|
| Override para_id/paraId | Sets correct parachain ID in chain spec root (both para_id and paraId variants) |
| Override relay_chain | Sets relay chain ID in chain spec root |
| Apply Genesis Overrides | Merges user-provided JSON overrides |
| Genesis Patch | Applies the genesis_patch RFC 6902 operations after the overrides, before the authorities and balances below are rewritten |
| Clear/Add Authorities | Clears existing authorities from session/keys, aura/authorities, grandpa/authorities, collatorSelection/invulnerables. Then adds collator session keys (if session pallet exists) or aura authorities. |
| Add Collator Selection | Adds invulnerable collators to collatorSelection/invulnerables |
| Override parachainInfo | Sets correct parachainId in /parachainInfo/parachainId |
| Add Balances | Extracts accounts from assets pallet metadata and ensures they have native token balances. Adds balances for collator sr accounts. |
For EVM-based parachains (is_evm_based = true), Ethereum session keys are generated and used instead of standard keys.
Chain Spec Key Types
ZombieNet supports customizing session key types and their cryptographic schemes via the chain_spec_key_types configuration option.
Cryptographic Schemes
| Scheme | Suffix | Description |
|---|---|---|
| SR25519 | _sr | Schnorr signatures, used by most runtime keys |
| ED25519 | _ed | Edwards curve signatures, used by grandpa |
| ECDSA | _ec | Elliptic curve, used by beefy and Ethereum compatibility |
Predefined Key Types
These key types have predefined default schemes:
| Key Type | Default Scheme | Notes |
|---|---|---|
aura | SR25519 | ED25519 on asset-hub-polkadot |
babe | SR25519 | |
grandpa | ED25519 | |
beefy | ECDSA | |
im_online | SR25519 | |
authority_discovery | SR25519 | |
para_validator | SR25519 | |
para_assignment | SR25519 | |
parachain_validator | SR25519 | |
nimbus | SR25519 | |
vrf | SR25519 |
Syntax
Two formats are supported:
- Short form:
aura— uses the predefined default scheme - Long form:
aura_ed— explicitly specifies the scheme
Unknown key types default to SR25519 when using short form.
Example
[relaychain]
chain_spec_key_types = ["aura", "grandpa", "beefy"] # Uses defaults
# Or with explicit schemes:
chain_spec_key_types = ["aura_ed", "grandpa_sr", "custom_key_ec"]
Optional Overrides
Two additional overrides can be applied after the raw chain spec is generated:
- WASM Override (
wasm_override): Replaces runtime code (:codestorage key) with custom WASM - Raw Spec Override (
raw_spec_override): Applies JSON patch merge to override any part of the raw spec
Command-Line Arguments
ZombieNet generates command-line arguments for each spawned node. Understanding what gets added helps debug issues and customize node behavior.
Framework-Managed Arguments
These arguments are always set by the framework and filtered out if provided by user configuration:
| Argument | Value |
|---|---|
--chain | Generated chain spec path |
--name | Node name from config |
--rpc-cors | all |
--rpc-methods | unsafe |
--parachain-id | Parachain ID (relay chain nodes only, when para_id is specified; NOT used for cumulus collators) |
--node-key | Deterministic key derived from node name |
Additionally, these flags are managed by the framework:
| Flag | When Added |
|---|---|
--no-telemetry | Relay chain nodes only (not cumulus collators) |
--collator | Cumulus collators with is_validator: true |
--execution wasm | Embedded relay chain full node (after -- separator) |
Arguments Added Conditionally
| Argument | Condition |
|---|---|
--validator | Relay chain nodes with is_validator: true |
--insecure-validator-i-know-what-i-do | Validators, if the binary supports it |
--prometheus-external | Unless already in user args |
--unsafe-rpc-external | Only for Docker/Kubernetes providers (not native) |
--bootnodes | When bootnode addresses are available |
Port Arguments
The framework injects port arguments based on assigned ports:
| Argument | Description |
|---|---|
--prometheus-port | Prometheus metrics port |
--rpc-port | RPC/WebSocket port |
--listen-addr | P2P listen address (format: /ip4/0.0.0.0/tcp/{port}/ws) |
--base-path | Data directory path |
Cumulus Collator Arguments
Cumulus-based collators receive two sets of arguments separated by --:
Before -- (collator arguments):
--chain(parachain spec)--collator--prometheus-port,--rpc-port,--listen-addr--base-path- User-provided collator args
After -- (embedded relay chain full node):
--base-path(separate relay data directory)--chain(relay chain spec)--execution wasm(hardcoded, always WASM execution)--port(full node P2P port, injected by framework when assigned port differs from default 30333)--prometheus-port(full node metrics port, injected by framework when assigned port differs from default 9616)- User-provided full node args
Removing Framework Arguments
Use the -: prefix to remove arguments that the framework adds:
[[relaychain.nodes]]
name = "bob"
args = ["-:--insecure-validator-i-know-what-i-do"]
This removes --insecure-validator-i-know-what-i-do from bob's command line.
The removal syntax:
-:--flag-nameremoves--flag-name-:flag-namealso removes--flag-name(normalized automatically)- Works for both flags and options (the option's value is also removed)
Example: Generated Relay Chain Command
polkadot \
--chain /cfg/rococo-local.json \
--name alice \
--rpc-cors all \
--rpc-methods unsafe \
--unsafe-rpc-external \ # omitted for native provider
--node-key <deterministic_key> \
--prometheus-external \
--validator \
--insecure-validator-i-know-what-i-do \ # if binary supports it
--bootnodes <bootnode_multiaddrs> \
--prometheus-port 9615 \
--rpc-port 9944 \
--listen-addr /ip4/0.0.0.0/tcp/30333/ws \
--base-path /data \
--no-telemetry \
[user_args...]
Example: Generated Cumulus Collator Command
polkadot-parachain \
--chain /cfg/1000.json \
--name collator01 \
--rpc-cors all \
--rpc-methods unsafe \
--unsafe-rpc-external \ # omitted for native provider
--node-key <deterministic_key> \
--prometheus-external \
--collator \ # if is_validator: true
--bootnodes <para_bootnode_multiaddrs> \
--prometheus-port 9615 \
--rpc-port 9944 \
--listen-addr /ip4/0.0.0.0/tcp/30333/ws \
--base-path /data \
[user_collator_args...] \
-- \
--base-path /relay-data \
--chain /cfg/rococo-local.json \
--execution wasm \
--port 30334 \ # if assigned port differs from default
--prometheus-port 9616 \ # if assigned port differs from default
[user_full_node_args...]
Network Definition Spec
Configuration via TOML files or the builder API.
- Global Settings - Timeouts, concurrency, base directory
- Relaychain - Relay chain configuration
- Parachain - Parachain and collator configuration
- Node - Node, node groups, and resources
- HRMP Channels - Cross-chain messaging
- Custom Processes - Custom process to spawn
- Chain Spec - Chain spec generation options
- Examples - Complete configuration examples
Global Settings
TOML
[settings]
timeout = 3600
node_spawn_timeout = 600
local_ip = "127.0.0.1"
base_dir = "/tmp/zombienet"
spawn_concurrency = 4
tear_down_on_failure = true
bootnodes_addresses = ["/ip4/127.0.0.1/tcp/30333/p2p/12D3KooW..."]
Builder
#![allow(unused)] fn main() { use zombienet_sdk::NetworkConfigBuilder; let config = NetworkConfigBuilder::new() .with_relaychain(|r| { r.with_chain("rococo-local") .with_default_command("polkadot") .with_validator(|v| v.with_name("alice")) }) .with_global_settings(|gs| { gs.with_network_spawn_timeout(3600) .with_node_spawn_timeout(600) .with_local_ip("127.0.0.1") .with_base_dir("/tmp/zombienet") .with_spawn_concurrency(4) }) .build() .unwrap(); }
Reference
| Option | Type | Default | Description |
|---|---|---|---|
timeout | Number | 3600 | Global timeout (seconds) for network spawn |
node_spawn_timeout | Number | 600 | Individual node spawn timeout (seconds). Override with ZOMBIE_NODE_SPAWN_TIMEOUT_SECONDS env var |
local_ip | String | 127.0.0.1 | Local IP for exposed services |
base_dir | String | Random temp dir | Base directory for network artifacts |
spawn_concurrency | Number | 20 | How many nodes are spawned at a time (one whenever a node or chain spec has {{ZOMBIE:..}} placeholders), and how many custom processes. Override with ZOMBIE_SPAWN_CONCURRENCY env var |
tear_down_on_failure | Boolean | true | Tear down network if nodes become unresponsive |
bootnodes_addresses | Array | — | External bootnode multiaddresses |
Relaychain
TOML
[relaychain]
chain = "rococo-local"
default_command = "polkadot"
default_image = "parity/polkadot:latest"
default_args = ["-lruntime=debug"]
# Optional: Use a pre-existing chain spec
# chain_spec_path = "/path/to/chain-spec.json"
# Optional: Generate chain spec with a command
# chain_spec_command = "polkadot build-spec --chain {chain}"
# Optional: Runtime for chain spec generation
# chain_spec_runtime.src = "/path/to/runtime.wasm"
# chain_spec_runtime.preset = "development"
# Genesis configuration
random_nominators_count = 4
max_nominations = 16
# Genesis patch (applied to chain spec)
[relaychain.genesis.balances]
balances = [["5GrwvaEF...", 1000000000000]]
# Genesis patch as RFC 6902 operations, applied after `genesis`; paths are
# relative to the runtime genesis, and `add` at `/-` appends to a list
[[relaychain.genesis_patch]]
op = "add"
path = "/balances/balances/-"
value = ["5FHneW46...", 1000000000000]
[[relaychain.nodes]]
name = "alice"
invulnerable = true
[[relaychain.nodes]]
name = "bob"
args = ["-lparachain=trace"]
Builder
#![allow(unused)] fn main() { use zombienet_sdk::NetworkConfigBuilder; let config = NetworkConfigBuilder::new() .with_relaychain(|r| { r.with_chain("rococo-local") .with_default_command("polkadot") .with_default_image("parity/polkadot:latest") .with_validator(|v| v.with_name("alice").invulnerable(true)) .with_validator(|v| v.with_name("bob")) }) .build() .unwrap(); }
Reference
| Option | Type | Default | Description |
|---|---|---|---|
chain | String | rococo-local | Chain name (e.g., polkadot, rococo-local, westend) |
default_command | String | polkadot | Default binary command for nodes |
default_image | String | — | Default container image (Docker/Podman/K8s) |
default_args | Array | — | Default CLI arguments for all nodes |
default_resources | Object | — | Default resource limits (see Resources) |
default_db_snapshot | String | — | Default database snapshot path/URL |
chain_spec_path | String | — | Path or URL to pre-existing chain spec |
chain_spec_command | String | — | Command template to generate chain spec |
chain_spec_command_is_local | Boolean | false | Run chain spec command locally |
chain_spec_command_output_path | String | /dev/stdout | Output path for chain spec command |
chain_spec_runtime | Object | — | Runtime WASM for chain spec generation (see Chain Spec) |
random_nominators_count | Number | — | Number of random nominators for staking |
max_nominations | Number | — | Maximum nominations per nominator |
genesis | Object | — | Genesis overrides as JSON (alias: runtime_genesis_patch) |
genesis_patch | Array | — | RFC 6902 operations applied to the runtime genesis after genesis (see How it works) |
wasm_override | String | — | WASM runtime override path/URL |
raw_spec_override | String/Object | — | Raw chain spec override (inline JSON or file path) |
Parachain
TOML
[[parachains]]
id = 1000
chain = "asset-hub-rococo-local"
onboard_as_parachain = true
default_command = "polkadot-parachain"
balance = 2000000000000
# Registration strategy (pick one):
# add_to_genesis = true # InGenesis (default, fastest)
# register_para = true # UsingExtrinsic (via extrinsic after spawn)
# (neither = Manual, no automatic registration)
# Genesis configuration
# genesis_wasm_path = "/path/to/para-wasm"
# genesis_state_path = "/path/to/para-genesis-state"
# Or generate genesis artifacts
# genesis_wasm_generator = "polkadot-parachain export-genesis-wasm"
# genesis_state_generator = ["polkadot-parachain", "export-genesis-state"]
# Genesis patch
[parachains.genesis.balances]
balances = [["5GrwvaEF...", 1000000000000]]
[[parachains.collators]]
name = "collator01"
command = "polkadot-parachain"
[[parachains.collators]]
name = "collator02"
args = ["-lparachain=trace"]
Builder
#![allow(unused)] fn main() { use zombienet_sdk::{NetworkConfigBuilder, RegistrationStrategy}; let config = NetworkConfigBuilder::new() .with_relaychain(|r| { r.with_chain("rococo-local") .with_default_command("polkadot") .with_validator(|v| v.with_name("alice")) .with_validator(|v| v.with_name("bob")) }) .with_parachain(|p| { p.with_id(1000) .with_collator(|c| c.with_name("collator01").with_command("polkadot-parachain")) }) .build() .unwrap(); }
Reference
| Option | Type | Default | Description |
|---|---|---|---|
id | Number | — | Required. Unique parachain ID |
chain | String | — | Chain name for the parachain |
cumulus_based | Boolean | true | Whether parachain uses Cumulus |
evm_based | Boolean | false | Whether parachain is EVM-based (e.g., Frontier) |
onboard_as_parachain | Boolean | true | Register as parachain (vs parathread) |
add_to_genesis | Boolean | — | Register in genesis (InGenesis strategy). Mutually exclusive with register_para |
register_para | Boolean | — | Register via extrinsic (UsingExtrinsic strategy). Mutually exclusive with add_to_genesis |
balance | Number | 2000000000000 | Initial balance for parachain account |
default_command | String | — | Default collator command |
default_image | String | — | Default container image |
default_args | Array | — | Default CLI arguments |
default_resources | Object | — | Default resource limits (see Resources) |
default_db_snapshot | String | — | Default database snapshot path/URL |
genesis_wasm_path | String | — | Path/URL to genesis WASM blob |
genesis_wasm_generator | String | — | Command to generate genesis WASM |
genesis_state_path | String | — | Path/URL to genesis state |
genesis_state_generator | String/Array | — | Command with args to generate genesis state |
genesis | Object | — | Genesis overrides as JSON (alias: runtime_genesis_patch) |
genesis_patch | Array | — | RFC 6902 operations applied to the runtime genesis after genesis (see How it works) |
chain_spec_path | String | — | Path/URL to parachain chain spec |
chain_spec_command | String | — | Command template to generate chain spec |
chain_spec_command_is_local | Boolean | false | Run chain spec command locally |
chain_spec_command_output_path | String | /dev/stdout | Output path for chain spec command |
chain_spec_runtime | Object | — | Runtime WASM for chain spec generation |
wasm_override | String | — | WASM runtime override path/URL |
raw_spec_override | String/Object | — | Raw chain spec override (inline JSON or file path) |
bootnodes_addresses | Array | — | Bootnode multiaddresses |
no_default_bootnodes | Boolean | false | Don't auto-assign bootnode role if none specified |
Registration Strategies
- InGenesis - In relay chain genesis (fastest)
- UsingExtrinsic - Via extrinsic after spawn
- Manual - No automatic registration
Collator Groups
[[parachains.collator_groups]]
name = "collator"
count = 3
command = "polkadot-parachain"
#![allow(unused)] fn main() { .with_parachain(|p| { p.with_id(1000) .with_collator_group(|g| { g.with_count(3) .with_base_node(|n| n.with_name("collator").with_command("polkadot-parachain")) }) }) }
Node
Validators, full nodes, and collators share this configuration.
TOML
[[relaychain.nodes]]
name = "alice"
command = "polkadot"
# subcommand = "node" # Optional: for nested CLI commands
image = "parity/polkadot:latest"
invulnerable = true
bootnode = false
initial_balance = 5000000000000
args = ["-lruntime=debug"]
env = [
{ name = "RUST_LOG", value = "info" }
]
# Networking
rpc_port = 9933
prometheus_port = 9615
p2p_port = 30333
# p2p_cert_hash = "..." # Optional: libp2p WebRTC cert hash
bootnodes_addresses = ["/ip4/127.0.0.1/tcp/30333/p2p/12D3KooW..."]
# Storage & Keys
db_snapshot = "/path/to/snapshot.tar.gz"
keystore_path = "/path/to/keystore"
keystore_key_types = ["aura", "gran"]
# override_eth_key = "0x..." # Optional: override EVM session key
node_log_path = "/tmp/alice.log"
# Resources (for container providers)
[relaychain.nodes.resources]
request_memory = "1Gi"
request_cpu = "500m"
limit_memory = "2Gi"
limit_cpu = "1000m"
Builder
#![allow(unused)] fn main() { .with_validator(|node| { node.with_name("alice") .with_command("polkadot") .with_image("parity/polkadot:latest") .invulnerable(true) .with_args(vec!["-lruntime=debug".into()]) .with_env(vec![("RUST_LOG".into(), "info".into())]) .with_resources(|r| { r.with_request_memory("1Gi").with_limit_memory("2Gi") }) }) }
Reference
| Option | Type | Default | Description |
|---|---|---|---|
name | String | — | Required. Unique node name |
command | String | Inherited | Binary command to execute |
subcommand | String | — | Optional subcommand (e.g., for nested CLI) |
image | String | Inherited | Container image |
args | Array | — | CLI arguments |
env | Array | — | Environment variables as {name, value} pairs |
validator | Boolean | true | Whether node is a validator/authority |
invulnerable | Boolean | true | Add to invulnerables set in genesis |
bootnode | Boolean | false | Whether node acts as a bootnode |
initial_balance | Number | 2000000000000 | Initial account balance (alias: balance) |
rpc_port | Number | Auto | RPC port |
prometheus_port | Number | Auto | Prometheus metrics port |
p2p_port | Number | Auto | P2P networking port |
p2p_cert_hash | String | — | libp2p WebRTC certificate hash |
bootnodes_addresses | Array | — | Additional bootnode addresses |
db_snapshot | String | — | Database snapshot path/URL |
keystore_path | String | — | Custom keystore directory |
keystore_key_types | Array | — | Key types to generate in keystore (e.g., aura, gran, babe) |
chain_spec_key_types | Array | — | Session key types to inject into chain spec genesis |
override_eth_key | String | — | Override auto-generated EVM session key |
node_log_path | String | — | Path for node log file |
resources | Object | — | Resource limits (see below) |
Node Groups
Define multiple nodes with the same configuration. Creates validator-0, validator-1, etc.
TOML
[[relaychain.node_groups]]
name = "validator"
count = 5
command = "polkadot"
Builder
#![allow(unused)] fn main() { .with_relaychain(|r| { r.with_chain("rococo-local") .with_default_command("polkadot") .with_node_group(|g| { g.with_base_node(|node| { node.with_name("validator") .with_args(vec!["-lruntime=debug".into()]) }) .with_count(5) }) }) }
Resources
For container providers (Docker, Podman, Kubernetes).
[relaychain.default_resources]
request_memory = "1Gi"
request_cpu = "500m"
limit_memory = "2Gi"
limit_cpu = "1000m"
Reference
| Option | Type | Description |
|---|---|---|
request_memory | String | Requested memory (e.g., 512Mi, 1Gi) |
request_cpu | String | Requested CPU (e.g., 250m, 1) |
limit_memory | String | Memory limit |
limit_cpu | String | CPU limit |
HRMP Channels
Cross-chain messaging between parachains.
TOML
[[hrmp_channels]]
sender = 1000
recipient = 2000
max_capacity = 8
max_message_size = 512
[[hrmp_channels]]
sender = 2000
recipient = 1000
max_capacity = 8
max_message_size = 512
Builder
#![allow(unused)] fn main() { let config = NetworkConfigBuilder::new() .with_relaychain(|r| { /* ... */ }) .with_parachain(|p| p.with_id(1000).with_collator(|c| c.with_name("col1"))) .with_parachain(|p| p.with_id(2000).with_collator(|c| c.with_name("col2"))) .with_hrmp_channel(|h| h.with_sender(1000).with_recipient(2000)) .with_hrmp_channel(|h| h.with_sender(2000).with_recipient(1000)) .build() .unwrap(); }
Reference
| Option | Type | Default | Description |
|---|---|---|---|
sender | Number | — | Required. Sending parachain ID |
recipient | Number | — | Required. Receiving parachain ID |
max_capacity | Number | 8 | Maximum messages in channel |
max_message_size | Number | 512 | Maximum message size in bytes |
Custom Processes
Allow to set custom processes to spawn after the network is up: services that run next to the nodes, such as an RPC adapter, an IPFS daemon or a database another process needs.
A custom process is a member of the network like a node: it is tracked on the Network, written to zombie.json, attached to again from it and destroyed with the namespace. Look it up with network.get_custom_process("name") or list them with network.custom_processes(). A process may not share its name with a node, since they share one registry: in TOML the process is rejected; in the builder a node declared after a process with its name is renamed, as it would be after another node, so declare processes last.
Unlike nodes, a custom process runs its command directly, not through zombienet's wrapper script: most of them are off-the-shelf images, which may not have bash. On docker and kubernetes the pause/resume/restart controls go through that wrapper, so on a custom process they return an error; on native they signal the process and work as for nodes.
A process must listen on all interfaces (0.0.0.0) on docker and kubernetes, where it is reached through a published port or a Service; one bound to 127.0.0.1 is reachable from nowhere. Most daemons need a flag for that (--rpc-external for eth-rpc below, --host 0.0.0.0 and the like elsewhere).
A process gets the same writable /data directory a node gets, on every provider; that is where kubo above keeps its repo (IPFS_PATH) and where anything that must survive a restart of the process belongs.
command replaces the image's entrypoint, on every provider. An image whose entrypoint does setup work (kubo's runs ipfs init and binds the API and gateway to all interfaces; the postgres image's runs initdb) needs that entrypoint named as the command, with the image's usual arguments as args.
TOML
[[custom_processes]]
name = "eth-rpc"
command = "eth-rpc"
args = [ "--node-rpc-url", "{{ZOMBIE:alice:internal_ws_uri}}", "--rpc-port", "{{ZOMBIE:eth-rpc:port_http}}", "--rpc-external" ]
env = [
{ name = "RUST_LOG", value = "info" }
]
ports = [ { name = "http", port = 0 } ]
[[custom_processes]]
name = "ipfs"
image = "docker.io/ipfs/kubo:v0.39.0"
command = "/usr/local/bin/start_ipfs"
args = [ "daemon" ]
ports = [ { name = "api", port = 5001 }, { name = "gateway", port = 8080 } ]
[custom_processes.resources]
limits = { cpu = "500m", memory = "512Mi" }
Builder
#![allow(unused)] fn main() { let config = NetworkConfigBuilder::new() .with_relaychain(|r| { /* ... */ }) .with_custom_process(|c| c.with_name("eth-rpc").with_command("eth-rpc")) }
#![allow(unused)] fn main() { let cp = CustomProcessBuilder::new() .with_name("ipfs") .with_command("/usr/local/bin/start_ipfs") .with_image("docker.io/ipfs/kubo:v0.39.0") .with_args(vec!["daemon".into()]) .with_env(vec![("IPFS_PATH", "/data/ipfs")]) .with_named_port("api", 5001) .with_named_port("gateway", 8080) .with_resources(|r| r.with_limit_cpu("500m").with_limit_memory("512Mi")) .with_ready_check(ReadyCheck::tcp("api")) .with_timeout(60) .with_dependency("ipfs-init") .build() .unwrap(); }
Placeholders
The args and env values of a process may refer to the nodes, which are all running by then, with the same {{ZOMBIE:<node-name>:<field>}} placeholders node args use: ws_uri, internal_ws_uri (the address other members of the network use, which is what a process wants), multiaddr, prometheus_uri for a substrate node, rpc_uri and peer_addr for a JAM node. On docker a node's addresses are host addresses, so a process in a container reaches a node only through the host (host.docker.internal on Docker Desktop). They may also refer to any custom process's ports, their own or another's, as {{ZOMBIE:<process-name>:port_<port-name>}}, since every port is picked before anything starts (see Lifecycle). A placeholder that resolves to nothing (a node or port that does not exist) means the process is never started, which fails the spawn unless the process is allowed to fail (see Lifecycle).
Ports
Each declared port is TCP and exposed by name. A port declared as 0 is picked free by zombienet when the process is spawned. Whatever the number, the process learns it through the placeholder above; eth-rpc is told --rpc-port <picked> that way.
On kubernetes the declared ports are the pod's Service ports, so other pods reach the process as <process-name>:<port>, and a port-forward is opened from the host running zombienet for each (unless running in CI, where the pod's own address is used). The pod's readiness probe is the process's check (see Lifecycle), so the Service routes to the pod once that passes on the pod address, which a process bound to 127.0.0.1 never does (see above). On docker each port is published on a free host port picked at spawn, so two networks can share a host; on native the process listens on them as declared, and a fixed port that is busy fails the spawn.
The running process reports where every port is reachable:
#![allow(unused)] fn main() { let ipfs = network.get_custom_process("ipfs")?; println!("{}", ipfs.get_uri_for_name("api").unwrap()); // from here, e.g. 127.0.0.1:53421 on k8s let api = ipfs.port("api").unwrap(); println!("{}", api.external); // the same address println!("{}", api.internal); // from inside the network, e.g. ipfs:5001 on k8s }
On native external and internal are the same address. On docker external is the published host port and internal the container's own address. On kubernetes external is a port-forward that lives as long as the zombienet process; re-attaching opens new ones, except for a one-shot, which has finished. A port-forward, like docker's published port, accepts connections whether or not the process is listening, so is_responsive() on kubernetes and docker reports the forward, not the process; only on native and on kubernetes in CI does it reach the process itself. Re-attaching takes the processes as recorded in zombie.json, with the same caveat.
Lifecycle
A process is ready when its ready_check passes: tcp = "<port name>", the port accepts a connection, or http = { port = "<port name>", path = "/health" }, a GET answers 2xx, every interval seconds (1 by default). The process's timeout, in seconds, bounds the wait (the global node_spawn_timeout by default); a one-shot is waited on to exit within it, and has failed when it does not. Without a check, a process with ports is ready when its first port accepts a TCP connection, and one without ports is ready once started. On kubernetes the check is the pod's readiness probe, so the kubelet does the checking from inside the cluster and the Service routes only once it passes; on docker and native zombienet probes from where it runs. On docker a published port answers for docker's proxy whether or not the process listens, so a tcp check there, including the default on the first port, passes at once and gives no real ordering; zombienet warns about it at spawn, and http is the check to use there.
A one-shot process (one_shot = true) runs to completion within its timeout: exit 0 is success, anything else a failure. It has no readiness, so no ready_check. On kubernetes a custom process's pod is never restarted by the kubelet, one-shot or not: a process that exits is done, by success or by failure, and its pod says so.
depends_on lists the processes this one starts after, with a condition as docker-compose names them: service_started, service_healthy (passed its check), service_completed_successfully (a one-shot that exited 0). A bare name means the target's natural condition: completed for a one-shot, healthy when it has a check or a port, started otherwise. Every process starts as soon as its dependencies are met, in parallel with whatever else can start; one with no dependencies starts once the nodes are up. A dependency must name a custom process, its condition must fit the target, and there may be no cycle; all three are checked when the config loads.
[[custom_processes]]
name = "migrate"
image = "docker.io/library/postgres:16"
command = "psql"
args = [ "-h", "db", "-f", "/data/schema.sql" ]
one_shot = true
depends_on = ["db"]
[[custom_processes]]
name = "api"
image = "example/api:1.0"
command = "api"
ports = [ { name = "http", port = 8080 } ]
ready_check = { http = { port = "http", path = "/health" } }
timeout = 120
depends_on = [ "migrate", { name = "cache", condition = "service_started" } ]
Every process is required: one that does not meet its condition within its timeout, or is never started (its ports could not be reserved, a dependency did not meet its condition, a placeholder did not resolve), fails the spawn, as a node that never comes up does; the error names the process and the reason, and the network is cleaned up when its provider is dropped (on native the logs stay in base_dir). On a spawned network process.state() is Ready or Completed, written to zombie.json. wait_ready() on a process waits for its condition again and reports Failed { reason } when it is no longer met; the network-wide wait_until_is_up() leaves processes out.
Processes start at most spawn_concurrency at a time, the value as set, where nodes drop to one at a time whenever a node or chain spec has {{ZOMBIE:..}} placeholders. Placeholders in a process do not limit the nodes.
A process may name another's port the way it names its own, {{ZOMBIE:<process>:port_<name>}}, since every port is picked before anything starts. The host part of the address is the other process's name on kubernetes and the host itself on native; on docker the containers have no names for each other, so the placeholder is of little use there.
Reference
| Option | Type | Default | Description |
|---|---|---|---|
name | String | — | Required. Name of the process; also its pod/container name and the key it is looked up by. Lowercase letters, digits and -, starting with a letter, at most 63 characters, unique among nodes and processes |
command | String | — | Required. Command to execute; replaces the image's entrypoint |
image | String | — | Container image |
args | Array | — | CLI arguments; may use the placeholders above |
env | Array | — | Environment variables as {name, value} pairs; values may use the same placeholders |
ports | Array | — | TCP ports the process listens on, as {name, port} pairs. Names are 1 to 15 lowercase letters, digits or -, with a letter, unique per process; port: 0 is picked at spawn |
resources | Table | — | CPU/memory requests and limits (kubernetes only; a warning is logged where ignored), as for nodes |
ready_check | Table | first port over TCP | { tcp = "<port>" } or { http = { port = "<port>", path = "..." } }, plus interval in seconds |
depends_on | Array | — | Names, or { name, condition } with service_started, service_healthy or service_completed_successfully |
one_shot | Bool | false | Runs to completion; exit 0 is success. No ready_check |
timeout | Integer | node_spawn_timeout | Seconds to wait for the ready check to pass, or the one-shot to exit |
Chain Spec Generation
Priority: chain_spec_path > chain_spec_runtime > chain_spec_command > auto-generation.
Pre-existing
chain_spec_path = "/path/to/chain-spec.json" # or URL
Via Command
chain_spec_command = "polkadot build-spec --chain {chain} --disable-default-bootnode"
Via Runtime
[relaychain.chain_spec_runtime]
src = "/path/to/runtime.wasm"
preset = "development" # optional
Complete Example
Relay chain with two parachains and HRMP channels.
TOML
[settings]
timeout = 600
[relaychain]
chain = "rococo-local"
default_command = "polkadot"
default_args = ["-lruntime=debug"]
[[relaychain.nodes]]
name = "alice"
[[relaychain.nodes]]
name = "bob"
[[parachains]]
id = 1000
[[parachains.collators]]
name = "collator-1000"
command = "polkadot-parachain"
[[parachains]]
id = 2000
[[parachains.collators]]
name = "collator-2000"
command = "polkadot-parachain"
[[hrmp_channels]]
sender = 1000
recipient = 2000
[[hrmp_channels]]
sender = 2000
recipient = 1000
Builder
use zombienet_sdk::{NetworkConfigBuilder, NetworkConfigExt}; #[tokio::main] async fn main() -> Result<(), Box<dyn std::error::Error>> { let network = NetworkConfigBuilder::new() .with_relaychain(|r| { r.with_chain("rococo-local") .with_default_command("polkadot") .with_validator(|v| v.with_name("alice")) .with_validator(|v| v.with_name("bob")) }) .with_parachain(|p| { p.with_id(1000) .with_collator(|c| c.with_name("collator-1000").with_command("polkadot-parachain")) }) .with_parachain(|p| { p.with_id(2000) .with_collator(|c| c.with_name("collator-2000").with_command("polkadot-parachain")) }) .with_hrmp_channel(|h| h.with_sender(1000).with_recipient(2000)) .with_hrmp_channel(|h| h.with_sender(2000).with_recipient(1000)) .build() .unwrap() .spawn_native() .await?; Ok(()) }
Providers
Providers determine how ZombieNet spawns and manages nodes. Choose based on your environment and requirements.
Quick Comparison
| Provider | Runs As | Image Required | Resource Limits |
|---|---|---|---|
| Native | Local processes | No | No |
| Docker | Containers | Yes | No |
| Kubernetes | K8s pods | Yes | Yes |
Provider Selection
Set the default provider via environment variable:
export ZOMBIE_PROVIDER=native # or: docker, k8s
Or specify per-command with --provider flag. Default is docker.
Native
Runs nodes directly on your machine as local processes.
Requirements
- Node binaries in PATH (e.g.,
polkadot,polkadot-parachain) - Or specify absolute paths in configuration
Usage
CLI:
zombie-cli spawn network.toml --provider native
Programmatically:
#![allow(unused)] fn main() { let network = config.spawn_native().await?; }
Notes
- Fastest startup, no container overhead
- No container images needed
- Ports are assigned randomly to avoid conflicts
- Files stored in a temp directory (or
base_dirif configured)
Docker
Runs nodes in Docker containers.
Requirements
- Docker or Podman installed and running
- Container images with node binaries
Usage
CLI:
zombie-cli spawn network.toml --provider docker
Programmatically:
#![allow(unused)] fn main() { let network = config.spawn_docker().await?; }
Default Images
Override via environment variables:
| Variable | Default | Description |
|---|---|---|
POLKADOT_IMAGE | docker.io/parity/polkadot:latest | Relay chain nodes |
CUMULUS_IMAGE | docker.io/parity/polkadot-parachain:latest | Parachain collators |
MALUS_IMAGE | docker.io/paritypr/malus:latest | Malus (malicious node) |
COL_IMAGE | docker.io/paritypr/colander:latest | Colander |
Labels
Every node container of a network, its scripts container and both of its volumes carry zombienet.namespace (the namespace name), zombienet.kind (node, collator, custom-process, temp and the other zombie-role values for containers, helper for the scripts container, volume for volumes) and zombienet.name. The short-lived --rm containers that set a namespace up are not labelled. Teardown removes the containers that carry the namespace label or the namespace name prefix, then tries to remove its volumes. To list a network's containers: docker ps --filter label=zombienet.namespace=<namespace>.
Kubernetes
Deploys nodes as Kubernetes pods.
Requirements
- Kubernetes cluster access
kubectlconfigured with valid kubeconfig- Container images accessible to the cluster
Usage
CLI:
zombie-cli spawn network.toml --provider k8s
Programmatically:
#![allow(unused)] fn main() { let network = config.spawn_k8s().await?; }
Pod Labels
Node pods carry a zombie-role label with the same values as zombienet v1: node (relay chain nodes, validators or not), collator, cumulus-collator, bootnode (dedicated bootnodes) and temp (short-lived helper pods). Custom processes carry custom-process. The file server has no zombie-role.
Attaching to Running Networks
Reconnect to a previously spawned network, currently running network using the zombie.json state file:
#![allow(unused)] fn main() { use zombienet_sdk::{AttachToLive, AttachToLiveNetwork}; // Native let network = AttachToLiveNetwork::attach_native("/tmp/zombie-1/zombie.json".into()).await?; // Docker let network = AttachToLiveNetwork::attach_docker("/tmp/zombie-1/zombie.json".into()).await?; // Kubernetes let network = AttachToLiveNetwork::attach_k8s("/tmp/zombie-1/zombie.json".into()).await?; }
The zombie.json file is written to the network's base directory after spawn completes.
CLI Usage
The zombie-cli command-line tool spawns and manages ZombieNet networks.
Installation
Pre-built binaries:
Download from GitHub Releases.
From source:
cargo install zombie-cli
spawn
Spawn a network from a configuration file.
zombie-cli spawn <CONFIG> [OPTIONS]
Arguments
| Argument | Required | Description |
|---|---|---|
CONFIG | Yes | Path to TOML configuration file |
Options
| Option | Short | Default | Description |
|---|---|---|---|
--provider | -p | docker | Provider: native, docker, or k8s |
--dir | -d | Random temp | Base directory for network files |
--spawn-concurrency | -c | 20 | Concurrent node spawn processes |
--node-verifier | -v | metric | Readiness check: metric or none |
Examples
# Spawn with native provider
zombie-cli spawn network.toml --provider native
# Spawn with custom directory
zombie-cli spawn network.toml --dir /tmp/my-network
# Spawn with Docker (default)
zombie-cli spawn network.toml
# Spawn on Kubernetes
zombie-cli spawn network.toml --provider k8s
reproduce
Reproduce CI test runs locally in Docker. Downloads nextest archives from GitHub Actions and runs them in a containerized environment matching CI.
zombie-cli reproduce <REPO> <RUN_ID> [OPTIONS]
zombie-cli reproduce --archive <PATH> [OPTIONS]
Arguments
| Argument | Required | Description |
|---|---|---|
REPO | Yes* | Repository name (e.g., zombienet-sdk) |
RUN_ID | Yes* | GitHub Actions run ID (find in the URL of a workflow run) |
*Not required when using --archive.
Options
| Option | Short | Description |
|---|---|---|
--archive | -a | Path to local nextest archive (.tar.zst) |
--artifact-pattern | -p | Filter artifact downloads by pattern (default: *zombienet-artifacts*) |
--skip | -s | Skip tests matching pattern (repeatable) |
[TEST_FILTER] | Additional arguments passed to nextest (use -- separator) |
Requirements
- Docker installed and running
- GitHub CLI (
gh) installed and authenticated (for downloading artifacts) POLKADOT_SDK_PATHenvironment variable pointing to your polkadot-sdk workspace
How It Works
- Downloads nextest test archives (
.tar.zst) from GitHub Actions - Optionally downloads binary artifacts matching the pattern
- Runs tests inside a Docker container with:
ZOMBIE_PROVIDER=native(native provider inside container)RUST_LOG=debugfor verbose output- Mounted polkadot-sdk workspace
- Extracts binaries to
/tmp/binariesinside the container
Examples
# Reproduce from GitHub Actions (requires gh CLI authentication)
POLKADOT_SDK_PATH=/path/to/polkadot-sdk \
zombie-cli reproduce zombienet-sdk 123456789
# Reproduce from local archive (no gh CLI needed)
POLKADOT_SDK_PATH=/path/to/polkadot-sdk \
zombie-cli reproduce --archive ./my-archive.tar.zst
# Download only artifacts matching "ray" pattern
zombie-cli reproduce zombienet-sdk 123456789 -p ray
# Skip specific slow tests
zombie-cli reproduce zombienet-sdk 123456789 --skip "slow_test"
# Pass filters to nextest
zombie-cli reproduce zombienet-sdk 123456789 -- --test-filter my_test
Nextest Archive Format
The .tar.zst archive is a nextest archive containing compiled test binaries and metadata. These are produced by CI runs using cargo nextest archive.
Environment Variables
| Variable | Command | Description |
|---|---|---|
RUST_LOG | All | Logging level (e.g., debug, info) |
ZOMBIE_SPAWN_CONCURRENCY | spawn | Override spawn concurrency |
POLKADOT_SDK_PATH | reproduce | Path to polkadot-sdk workspace |
POLKADOT_IMAGE | spawn | Override Polkadot container image |
CUMULUS_IMAGE | spawn | Override Cumulus container image |
Examples
All examples are in crates/examples/.
Run Rust examples with cargo run --example <example_name>.
Spawn from config files with zombie-cli spawn -p <provider> <config-file>.
Basic Network Setup
Start here to understand network configuration patterns.
| Example | Description |
|---|---|
simple_network_example | Minimal relay chain with two validators using TOML config |
small_network_with_default | Override default command and image for all nodes |
small_network_with_base_dir | Custom base directory for node data |
small_network_config | Minimal programmatic configuration |
Parachain Lifecycle
Configure, register, and manage parachains.
| Example | Description |
|---|---|
small_network_with_para | Basic relay + parachain topology |
register_para | Register parachain via extrinsic |
add_para | Add parachain to running network |
para_upgrade | Runtime upgrade on running parachain |
two_paras_same_id | Handling duplicate parachain IDs |
Node Groups
Scale networks with grouped nodes.
| Example | Description |
|---|---|
big_network_with_group_nodes | Programmatic network with grouped nodes |
network_example_with_group_nodes | TOML config with group nodes |
Advanced Configuration
| Example | Description |
|---|---|
resource_limits | CPU and memory limits for containers |
wasm-override | Custom WASM runtime override |
arg_removal | Remove default CLI arguments |
db_snapshot | Database snapshots for faster init |
docker_db_snapshot | DB snapshots in Docker environments |
raw_spec_override | Override raw chain spec fields |
genesis_patch | Append to and replace runtime genesis values with an RFC 6902 patch |
Chain Spec Generation
| Example | Description |
|---|---|
chain_spec_generation | Dynamic chain spec generation |
chain_spec_runtime_kusama | Kusama with custom runtime WASM |
polkadot_people_wasm_runtime | Polkadot + People parachain with custom runtimes |
genesis_state_generator_example | Custom genesis state generator |
Keys and Security
| Example | Description |
|---|---|
keystore_key_types | Keystore directories and key type validation |
chain_spec_key_types | Chain spec session key configuration |
evm_parachain_session_key | EVM parachain session keys |
Network Utilities
| Example | Description |
|---|---|
from_live | Attach to running network via zombie.json |
test_run_script | Run scripts on nodes |
custom_process_ports | A custom process with a declared port, reached through the network |
custom_process_lifecycle | A one-shot, a server with an http ready check that depends on it, a client that waits for the server |
custom_process_positional_args | A custom process script told where a node is |
Config Files
Example TOML configs are in crates/examples/examples/configs/:
| Config | Description |
|---|---|
simple.toml | Basic two-validator relay chain |
simple-group-nodes.toml | Relay chain with node groups |
resource_limits.toml | Container resource limits |
wasm-override.toml | WASM runtime override |
arg-removal.toml | CLI argument removal |
polkadot-ah-chain-spec-runtime.toml | Polkadot + Asset Hub with custom runtimes |
custom_process_ports.toml | Custom process with a declared port |
custom_process_lifecycle.toml | Custom processes with one_shot, ready_check and depends_on |
custom_process_positional_args.toml | Custom processes with positional arguments |
Environment Variables
ZombieNet SDK uses environment variables for configuration overrides, container images, and CI integration.
Container Images
Override default images for Docker and Kubernetes providers:
| Variable | Default | Description |
|---|---|---|
POLKADOT_IMAGE | docker.io/parity/polkadot:latest | Relay chain node image |
CUMULUS_IMAGE | docker.io/parity/polkadot-parachain:latest | Parachain collator image |
MALUS_IMAGE | docker.io/paritypr/malus:latest | Malus (malicious node) image |
COL_IMAGE | docker.io/paritypr/colander:latest | Colander image |
Example:
POLKADOT_IMAGE=docker.io/parity/polkadot:v1.5.0 zombie-cli spawn network.toml
Provider Selection
| Variable | Values | Default | Description |
|---|---|---|---|
ZOMBIE_PROVIDER | native, k8s, docker | docker | Default provider when not specified |
Example:
ZOMBIE_PROVIDER=native zombie-cli spawn network.toml
Spawn Control
| Variable | Description |
|---|---|
ZOMBIE_SPAWN_CONCURRENCY | Override spawn concurrency (default: 20). Set to lower values for resource-constrained environments. |
ZOMBIE_NODE_SPAWN_TIMEOUT_SECONDS | Override per-node spawn timeout in seconds |
Example:
ZOMBIE_SPAWN_CONCURRENCY=10 zombie-cli spawn network.toml
CLI-Specific
| Variable | Command | Description |
|---|---|---|
POLKADOT_SDK_PATH | reproduce | Path to local polkadot-sdk workspace (required for reproduce) |
RUST_LOG | All | Logging level (debug, info, warn, error) |
Other
| Variable | Description |
|---|---|
ZOMBIE_RM_TGZ_AFTER_EXTRACT | Remove tgz archives after extraction (used for db snapshot extraction) |
TOML Template Replacement
Environment variables can be referenced in TOML configuration files using {{VAR_NAME}} syntax:
[relaychain]
default_image = "{{POLKADOT_IMAGE}}"
[[relaychain.nodes]]
name = "alice"
image = "{{CUSTOM_NODE_IMAGE}}"
Any environment variable can be substituted. If the variable is not set, the placeholder remains unchanged.
Runtime Token Replacement
For dynamic values from running nodes, use {{ZOMBIE:node_name:field}} syntax in node arguments:
[[relaychain.nodes]]
name = "bob"
args = ["--sync-target", "{{ZOMBIE:alice:multiaddr}}"]
Available fields:
| Field | Aliases | Description |
|---|---|---|
multiaddr | multiAddress | Node's libp2p multiaddress |
ws_uri | wsUri | WebSocket RPC endpoint |
prometheus_uri | prometheusUri | Prometheus metrics endpoint |
When using token replacement, spawn concurrency is automatically set to 1 (serial) to ensure dependencies are resolved correctly.