Skip to content

Repository files navigation

wire-tools-ts

TypeScript test harness and end-to-end flow suites for the WIRE OPP (Outpost Protocol) cross-chain messaging system.

This repo stands up a full local cluster — the WIRE depot plus its Ethereum and Solana outposts — and drives flow-* scenarios across all three chains, verifying that OPP envelopes circulate and that on-chain state stays consistent end to end.

What it exercises

The OPP message flow spans three chains:

  • WIRE depot (nodeop + kiod) — system contracts sysio.epoch, sysio.msgch, sysio.opreg, sysio.uwrit, sysio.reserv, sysio.chalg, …
  • Ethereum outpost (anvil) — OPP.sol, OPPInbound.sol, OperatorRegistry.sol, ReserveManager.sol, StakingManager.sol (+ liqEth).
  • Solana outpost (solana-test-validator) — the opp-outpost Anchor program (+ liqsol-*).

Where this repo fits in the platform

wire-tools-ts consumes the other WIRE repos as siblings on disk — it builds nothing on-chain itself, it orchestrates the already-built artifacts of:

Sibling repo Provides Built with
wire-sysio nodeop, kiod, clio, system-contract .wasm/.abi CMake / Ninja
wire-ethereum outpost Solidity contracts + deployLocal.ts Hardhat
wire-solana opp-outpost program .so + IDL Anchor

All four are checked out together as a single workspace via Google's repo tool. For cloning and syncing the platform, follow wire-platform-manifest/README.md first — this README assumes the siblings already exist next to this repo.

For a full, from-scratch host build of every dependency (Ubuntu 24.04 / WSL2), see docs/local-setup.md.

Prerequisites

Toolchain

Tool Pinned version Why Install
Node.js >= 22 runs the harness + flow tests nodejs.org or nvm
pnpm 10.32.1 the only supported package manager corepack enable && corepack prepare pnpm@10.32.1 --activate
Rust 1.86.0 toolchain for Solana / Anchor builds see below
Foundry (anvil) >= 1.5 local Ethereum node for the ETH outpost see below
Solana CLI (solana-test-validator) 2.1.21 (Agave) local Solana validator for the SOL outpost see below
Anchor (anchor) via avm 0.31.0 builds + loads the opp-outpost program see below

The Solana / Anchor / Rust versions are pinned by wire-solana (Anchor.tomlanchor_version = "0.31.0", solana_version = "2.1.21"; rust-toolchain.tomlchannel = "1.86.0"). Match them — a mismatched validator or Anchor CLI produces program-load failures that look like flow bugs.

Install snippets

# ── Node + pnpm (via Corepack, ships with Node ≥ 16.9) ─────────────────────
corepack enable
corepack prepare pnpm@10.32.1 --activate

# ── Rust ───────────────────────────────────────────────────────────────────
# https://rustup.rs
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y
. "$HOME/.cargo/env"

# ── Foundry / anvil ──────────────────────────────────────────────────────────
# https://book.getfoundry.sh/getting-started/installation
curl -L https://foundry.paradigm.xyz | bash
"$HOME/.foundry/bin/foundryup"
export PATH="$HOME/.foundry/bin:$PATH"

# ── Solana CLI (Agave) — pin 2.1.21 to match wire-solana ─────────────────────
# https://docs.anza.xyz/cli/install
sh -c "$(curl -sSfL https://release.anza.xyz/v2.1.21/install)"
export PATH="$HOME/.local/share/solana/install/active_release/bin:$PATH"

# ── Anchor via avm (Anchor Version Manager) — pin 0.31.0 ─────────────────────
# https://www.anchor-lang.com/docs/installation
cargo install --git https://github.com/coral-xyz/anchor avm --force
avm install 0.31.0
avm use 0.31.0

Verify:

node --version        # v22+  (v24 OK)
pnpm --version        # 10.32.1
anvil --version       # 1.5.x
solana --version      # 2.1.21 ... Agave
anchor --version      # anchor-cli 0.31.0

Built dependency chains

The cluster loads compiled artifacts from the sibling repos. Build them before running anything here (each repo's own README/CLAUDE.md is authoritative):

# 1. wire-sysio — produces bin/{nodeop,kiod,clio} + system-contract wasm/abi
#    (build/release or build/debug). See wire-sysio/CLAUDE.md for the CMake invocation.
ls ../wire-sysio/build/release/bin/nodeop      # sanity check

# 2. wire-ethereum — compile the outpost contracts
cd ../wire-ethereum && pnpm install && pnpm build      # npx hardhat compile

# 3. wire-solana — build the opp-outpost program (.so + IDL)
cd ../wire-solana && anchor build

Install & build this repo

pnpm install      # also auto-links sibling @wireio/* packages via .pnpmfile.cjs
pnpm build        # tsc -b across all packages

Packages

pnpm workspace (no nx/turbo/lerna); everything lives under packages/.

Package Name Purpose
cluster-tool @wireio/cluster-tool Core harness: process managers, chain clients, bootstrap, wire-cluster-tool CLI
flow-operator-collateral-deposit @wireio/test-flow-operator-collateral-deposit Node-operator collateral deposit + withdraw remit
flow-swap-with-underwriting @wireio/test-flow-swap-with-underwriting Bidirectional SWAP (ETH ↔ SOL) with underwriting
flow-swap-non-native-tokens @wireio/test-flow-swap-non-native-tokens SWAP of non-native tokens (USDC / USDT / LIQ)
flow-swap-variance-revert @wireio/test-flow-swap-variance-revert Swap variance-tolerance revert
flow-batch-operator-termination @wireio/test-flow-batch-operator-termination Batch-operator termination via delivery underperformance
flow-yield-distribution @wireio/test-flow-yield-distribution STAKING_REWARDsysio.dclaim::onrewardfundclaim
flow-emissions-soak @wireio/test-flow-emissions-soak Multi-hour emissions + sysio.dclaim payout soak
debugging-* / test-app-server @wireio/debugging-* OPP debugging server, client tooling, TUI, shared types

Flow packages depend on the harness via workspace:*.

Running flows

Every flow needs three paths into the sibling repos, supplied as env vars (or as flags to the helper script below):

Env var Points to
WIRE_BUILD_PATH wire-sysio build dir (must contain bin/nodeop), e.g. ../wire-sysio/build/release
WIRE_ETH_PATH wire-ethereum repo root (must contain hardhat.config.ts)
WIRE_SOLANA_PATH wire-solana repo root (built opp-outpost)
WIRE_CLUSTER_PATH (optional) cluster data dir; the harness generates a fresh temp dir per run when unset

Option A — the run-flow.mjs helper (THE canonical way)

scripts/run-flow.mjs discovers the flow packages dynamically, lets you pick one by name / regex (or interactively), validates the sibling-repo paths, wires the env vars, and drives the matching package's test script. This is the canonical flow runner — sessions/automation MUST use it (per wire-platform-manifest/.claude/rules/run-flows-via-canonical-scripts.md), and every live run is paired with the heartbeat monitor (see Monitoring a live flow run below).

# Usage: ./scripts/run-flow.mjs [name-or-pattern] [options]

# Interactive picker over every packages/flow-* (no argument):
./scripts/run-flow.mjs \
  --wire-build-path ../wire-sysio/build/release \
  --ethereum-path   ../wire-ethereum \
  --solana-path     ../wire-solana

# Exact name (full or short form):
./scripts/run-flow.mjs flow-swap-with-underwriting --wire-build-path … --ethereum-path … --solana-path …
./scripts/run-flow.mjs swap-with-underwriting       --wire-build-path … --ethereum-path … --solana-path …

# Regex — 1 match runs it, multiple matches drop into a scoped picker:
./scripts/run-flow.mjs swap --wire-build-path … --ethereum-path … --solana-path …

Each --wire-build-path / --ethereum-path / --solana-path flag falls back to its env var (WIRE_BUILD_PATH / WIRE_ETH_PATH / WIRE_SOLANA_PATH); one of the two is required. --cluster-path (env WIRE_CLUSTER_PATH) is optional — omit it and the harness picks a fresh temp dir.

Option B — pnpm --filter directly (what Option A runs under the hood)

For a human at an interactive terminal only — sessions/automation always go through Option A, which adds path validation and correct stdio handling:

export WIRE_BUILD_PATH=../wire-sysio/build/release
export WIRE_ETH_PATH=../wire-ethereum
export WIRE_SOLANA_PATH=../wire-solana

pnpm --filter @wireio/test-flow-operator-collateral-deposit test
pnpm --filter @wireio/test-flow-swap-with-underwriting       test

# Every flow at once (long — builds first):
pnpm test

Monitoring a live flow run

Every live run is watched by the canonical six-probe heartbeat monitor, scripts/flow-heartbeat-monitor.mjsone instance per flow run, started right after the flow and left running for the run's entire life:

node scripts/flow-heartbeat-monitor.mjs --cluster-path <cluster-dir>

Every 90s it probes chain liveness, operators, epoch state, msgch envelopes, data/opp-debugging/ per-direction artifact counts, and the aggregate cluster log (FATAL vs NOISE classified), printing one HB … line per cycle — and it bails loudly on the fatal conditions (epoch stall, zero-delta opp-debugging, plugin-layer failures) instead of letting a dead run burn its full pollUntil deadline. It self-concludes when the flow exits. --interval-seconds / --epoch-duration-seconds are the only tuning knobs; everything else derives from the cluster's cluster-config.json.

Rules of the road (binding for sessions/automation; see wire-platform-manifest/.claude/rules/run-flows-via-canonical-scripts.md and …/cluster-state-active-probing.md):

  • Never run a flow without its monitor, and never watch one with a hand-rolled tail | grep instead of the script.
  • Never wrap either script in session-local launcher/watcher scripts — if a run needs a new probe, bail, or launch behavior, extend flow-heartbeat-monitor.mjs / run-flow.mjs themselves.
  • Parallel runs need disjoint --cluster-path dirs; port allocation is collision-proof via BindConfig, so parallelism is bounded only by host resources.

Launching a persistent test cluster

For interactive work — poking the chains with clio or the debugging TUI — drive the wire-cluster-tool CLI directly (alias: wtc). Its commands: create, run, destroy, package, and create-external-config.

Two ways to start a daemon — and how they relate

After create, there are two launchers, and they are not alternatives to pick between at random:

wire-cluster-tool run a daemon's start.sh
Starts the WHOLE cluster, in dependency order ONE daemon
Needs this repo installed + built only that daemon's binary
Ports/paths re-read from cluster-config.json at launch frozen from the config at emit time
Registers with the bind registry yes (validate + registerResolved) no

run is the local development path. start.sh exists for the published artifact: a consumer who unpacks wire-platform-cluster-external-data-<version>.tgz has the tree but not this repo, and the argv otherwise lives only inside wire-cluster-tool.

Every daemon directory gets one — <cluster>/data/<daemon>/start.sh — carrying the same argv run would spawn, with three properties worth knowing:

  • Relocatable. No build-host path is frozen. $NODE_DIR and $CLUSTER_DIR derive from the script's own location (override with WIRE_CLUSTER_DIR). The wire-sysio install prefix is WIRE_PREFIX_PATH, RESOLVED rather than demanded: an explicit WIRE_PREFIX_PATH wins, else the parent of a nodeop found on PATH (so a host with wire-sysio installed needs no configuration at all), else WIRE_BUILD_PATH for a tree still pointed at a build dir — and it fails loudly naming all three if none resolves. WIRE_ETH_PATH / WIRE_SOLANA_PATH are asserted outright where referenced, since nothing on PATH can imply them. PATH-resolved binaries indirect through WIRE_ANVIL_BIN / WIRE_SOLANA_TEST_VALIDATOR_BIN / WIRE_NODE_BIN, falling back to command -v. The WIRE_ prefix is deliberate — unprefixed names collide with ambient ones (NODE_BIN is set to a bin DIRECTORY by common node version managers, so the fallback is never reached and the script execs a directory).
  • Run semantics. Nodes are relaunch-form (no one-shot genesis flags); anvil carries interval mining. Conditions that depended on the build host — anvil's --load-state, nodeop's --trace-no-abis — render as shell tests the script evaluates itself.
  • Not bind-registry aware. The ports were issued by the registry at create time, but a start.sh-launched daemon does not re-claim them, so it is invisible to a concurrently-resolving cluster on the same host. Intended for a dedicated deploy host; use run when other clusters share the machine.

Invoke as ./start.sh — the emitted scripts are mode 0755.

Under the default KEY signature provider these scripts contain an inline private key for every producer/operator node, exactly as the daemon's argv does — the script header says so. Published GHA artifacts are unaffected: that workflow is SSM-only and refuses any other provider, so its scripts carry SSM:<id> references instead.

wire-cluster-tool create \
  --cluster-path=/tmp/wire-cluster-tool-001 \
  --force \
  --build-path=/data/shared/code/wire-platform/wire-sysio/build/release \
  --producer-count=5 \
  --node-count=1 \
  --batch-operator-count=3 \
  --underwriter-count=1 \
  --epoch-duration-sec=60 \
  --ethereum-path=/data/shared/code/wire-platform/wire-ethereum \
  --solana-path=/data/shared/code/wire-platform/wire-solana \
&& wire-cluster-tool run --cluster-path=/tmp/wire-cluster-tool-001

create writes a cluster-config.json under --cluster-path and bootstraps the chains; run starts the cluster from that saved config and blocks until you Ctrl+C (which triggers a clean shutdown). Tear it down with:

wire-cluster-tool destroy --cluster-path=/tmp/wire-cluster-tool-001

Common options (every command)

Options follow their command (wire-cluster-tool <command> --flag … — the command comes first).

Option Alias Description
--cluster-path -d (required) directory for cluster data + cluster-config.json

run and destroy take only --cluster-path.

create options

Option Alias Default Description
--build-path (required) wire-sysio build dir (with bin/nodeop)
--ethereum-path (required) wire-ethereum repo root; bootstraps anvil + outpost deploy
--solana-path (required) wire-solana repo root; bootstraps solana-test-validator + opp-outpost
--force false overwrite an existing cluster directory
--node-count -n 1 producer node processes to launch
--producer-count -p 1 producer accounts to register on-chain
--batch-operator-count -b 3 batch operators
--underwriter-count -u 1 underwriters
--epoch-duration-sec 60 minimum epoch duration in seconds (the depot floor — sysio.epoch::setconfig rejects lower)
--warmup-epochs 1 epochs before an operator goes WARMUPACTIVE
--cooldown-epochs 1 epochs before an operator can deregister after COOLDOWN
--terminate-max-consecutive-misses consecutive missed-delivery termination threshold
--terminate-max-percent-misses24h 24h missed-delivery percentage termination threshold
--terminate-window-ms termination evaluation window in ms
--bind-all false bind every daemon to 0.0.0.0 instead of loopback
--enable-mock-reserves false seed the 8 mock (chain, token) PRIMARY reserves at bootstrap
--bind-* auto per-daemon address/port pins (--bind-anvil-port, --bind-nodeop-ports-bios-http, …); unpinned ports are auto-assigned collision-free
--bind-config a BindConfig JSON file: a complete config is used verbatim (no port probing — remote addresses stay put), a partial one is merged over the resolved defaults (CLI --bind-* > file > defaults)
--external-outpost-config an ExternalOutpostConfig JSON file: bootstrap the depot against already-deployed REMOTE ETH+SOL outposts (skips the local anvil/validator + outpost deploys)
--logging-levels-console / --logging-levels-file info / debug per-sink levels for the HARNESS's own logger. console additionally sets the level of every nodeop logger (net_plugin_impl, producer_plugin, …): libfc filters at the logger, not the sink, so one level necessarily drives both of nodeop's sinks and the console is the binding one — it is the stream the harness captures. Raising it to debug on a large cluster produces GBs of nodeop output per minute; --logging-levels-file does NOT bound that, as it never touches nodeop's logging.json.
--logging-file-format jsonl log file format: text or jsonl
--report-path / --report-basename <cluster>/reports, cluster-build Report output location

Cluster signature providers

create controls how the cluster's own signing keys are handled via --signature-provider-type (default KEY):

  • KEY — keys are generated locally and embedded inline in each node's --signature-provider spec (KEY:<privateKey>). The default; byte-identical to the historical bootstrap.

  • SSM — keys are published to AWS SSM Parameter Store and referenced by a REGION-LESS SSM:<secretId> spec (the depot's ssm plugin resolves the region from the AWS environment chain). Requires --signature-provider-ssm carrying the secret-id pattern, either inline JSON (leading {) or a file path, plus an awsClusterNodeConfig (the AWS account + the regions every secret is replicated to — there is no primary region):

    wire-cluster-tool create … --signature-provider-type SSM \
      --signature-provider-ssm '{"awsSecretIdPattern":"/wire/{cluster}/{account}/{keyType}"}'

    Pattern placeholders: {cluster} (the AWS account — dev / test / prod), {account} (the operator's durable handle or a node name), {keyType}, and the optional {version}; an unknown or unfilled placeholder fails fast. Publishing runs at create time, to EVERY region, and needs AWS credentials.

  • KIOD — material-less; the key lives in the local kiod wallet and specs render KIOD:<kiod-url>.

Node packaging

package archives each node's full config tree (config.ini, logging.json, data dirs) plus the cluster genesis.json into <cluster>/packages/<node>.<ext> — one self-contained archive per node, the hand-off artifact for a multihost environment with distinct compute and storage (for example S3/EC2 — equally GCS or any other provider; deliberately loosely coupled to the target). Runs only on a successfully-created, STOPPED cluster:

wire-cluster-tool -d <cluster-dir> package --package-type zip   # case-insensitive

The format is a required --package-type choice (zip today; the ClusterPackageType enum + its per-type backend is the extension seam). Under the default KEY provider each node's config.ini embeds its signing-key specs, so archives are sensitive; cluster-keys.json is NEVER included.

External outpost clusters

Point a cluster at Ethereum + Solana outposts that already run on real chains instead of the local anvil / solana-test-validator: pass --external-outpost-config <file> at create (the depot bootstraps normally but no local outpost is started or deployed), together with a --bind-config whose anvil / solana addresses are the REMOTE RPC endpoints. The ExternalOutpostConfig is fully self-described (RPC endpoints come from the bind config; the Solana program id is parsed from the IDL):

{
  "ethereum": {
    "addressFile": "/opt/wire/eth/outpost-addrs.json",
    "abiFiles": ["/opt/wire/eth/eth-abis/OPP.json", "/opt/wire/eth/eth-abis/OPPInbound.json"],
    "chainId": 11155111
  },
  "solana": { "idlFile": "/opt/wire/sol/liqsol_core.json" }
}

External mode ALSO requires an EXPLICIT --underwriter-count 0. The flag defaults to 1, so omitting it asks for one underwriter — and an external cluster has no local outpost for an underwriter to bond collateral on, so create fails fast naming whichever cause applies ("you asked for N" vs "you omitted it and got the default").

At create the harness verifies the external endpoints are reachable (eth_chainId matches the configured chainId; Solana getVersion responds) and gates bootstrap success on the depot's head block advancing AND on an outbound envelope being queued for every registered outpost — NOT on epoch distribution (there is no local chain to advance). A LOCAL cluster instead gates on sysio.epoch::current_epoch_index passing the bootstrap epoch. A remote anvil/solana bind address WITHOUT --external-outpost-config fails fast.

Exporting a deployable external config

create-external-config clones a CREATED, STOPPED local cluster into a fresh, deployable directory with a different (typically remote) BindConfig merged in, and emits a self-described external-cluster-config.json:

wire-cluster-tool create-external-config \
  --local-cluster-path    /opt/wire/testnet-local \
  --external-cluster-path /opt/wire/testnet \
  --external-bind-config  ~/testnet-bind-config.json

The five stages run as Report steps: Validate (the external BindConfig is topology-compatible — one bind entry per node/role, every operator account present, no duplicate ports, sane solana dynamic range; fails fast before any write), Clone (copy the tree, excluding *.pid / logs/ / reports/, preserving cluster-keys.json's 0600), Rebind (re-render cluster-config.json, genesis.json, every node's config.ini / logging.json, and cluster-state.json from the merged, external-rooted model — never text-patched), Emit (external-cluster-config.json), and Verify (scan the tree for any stale local port + round-trip the emitted JSON). The emitted config carries the external bindings, each operator account's key providers matching the source cluster's provider type (KEY inline for a KEY cluster, SSM awsSecretId references — no plaintext — for an SSM cluster, material-less KIOD for a KIOD cluster), the depot epochDurationSec + genesis path, and the ethereum/solana outpost references — fully self-described, so the external directory can then be packaged and deployed on another host (createcreate-external-configpackage).

End-to-end: an SSM-keyed cluster → deployable external config

A single cluster's lifecycle — create with keys in AWS SSM, then create-external-config on that same cluster:

# 1. Create a cluster whose signing keys are PUBLISHED to AWS SSM (not inline).
#    Publishing runs at create time and needs AWS credentials in the environment.
wire-cluster-tool create \
  --cluster-path  /opt/wire/testnet-local \
  --build-path    /opt/wire-sysio/build/release \
  --ethereum-path /opt/wire-ethereum \
  --solana-path   /opt/wire-solana \
  --signature-provider-type SSM \
  --signature-provider-ssm '{"awsSecretIdPattern":"/wire/{cluster}/{account}/{keyType}"}'

# Each generated key is PutParameter'd to SSM under the rendered id, in EVERY
# region of awsClusterNodeConfig.regions — e.g.
#   /wire/test/batchop.a/K1   ({cluster} = awsClusterNodeConfig.account)
# — and node/daemon --signature-provider specs render a region-less SSM:<id>.

# 2. Stop the cluster, then clone it into a deployable external directory with a
#    remote BindConfig merged in, emitting its self-described external config.
wire-cluster-tool create-external-config \
  --local-cluster-path    /opt/wire/testnet-local \
  --external-cluster-path /opt/wire/testnet \
  --external-bind-config  ~/testnet-bind-config.json

# Because the source cluster used SSM, external-cluster-config.json carries SSM
# providers ({awsRegions, awsSecretId} — the SAME ids create published,
# reconstructed from the pattern; awsRegions is informational, for operators and
# disaster recovery) with NO plaintext keys. (A KEY cluster emits
# inline KEY providers; a KIOD cluster, material-less KIOD providers.)

Environment variables

Variable Used by Description
WIRE_BUILD_PATH flows wire-sysio build dir (with bin/nodeop)
WIRE_ETH_PATH flows wire-ethereum repo root
WIRE_SOLANA_PATH flows wire-solana repo root
WIRE_CLUSTER_PATH flows cluster data dir (optional; temp dir if unset)
LOG_LEVEL everything harness log level: debug | info | warn | error (default info)

Architecture

The harness manages chain child processes with child_process.spawn + tree-kill (not pm2). On startup it pkills any stray nodeop / kiod / anvil / solana-test-validator, registers exit handlers for cleanup, and (when a cluster dir is set) tees per-process and combined logs to <cluster-path>/logs/.

  • processes/ProcessManager, WIREChainManager (nodeop+kiod), AnvilManager, SolanaValidatorManager.
  • cluster/ClusterManager orchestrates the create → bootstrap → start lifecycle.
  • clients/Clio, WIREClient, ETHClient (ethers), SOLClient (@solana/web3.js).
  • bootstrap/ + cluster/ETHBootstrapper.ts / bootstrap/SOLBootstrap.ts — chain initialization and test-cluster custody seeding.

OPP debugging artifacts (the raw envelope bytes each side saw) land under <cluster-path>/data/opp-debugging/.

Code style & conventions

See CLAUDE.md and STYLE.md. Highlights:

  • Prettier: no semicolons, no trailing commas, double quotes, 2-space indent, arrow parens avoid.
  • Every new/modified exported symbol ships unit tests in the same change.
  • No src/ in import/export specifiers — use package aliases or barrels.

Appendix

Useful Info

Parallel Process Caveats

Running two or more flows (or clusters) concurrently on one host is supported, but only because each of the following shared-state hazards is explicitly handled. Keep them in mind when touching any of these areas:

  • Port selection vs port binding. BindConfigProvider.resolve picks free ports, but a picked port is not bound until its daemon spawns (possibly minutes later). A second process resolving in that window would happily re-pick it. Every resolving process therefore registers its full resolved BindConfig in /tmp/wire-platform-bind-config/<pid>.bind-config.json before releasing the host-global port lock; later resolvers read every LIVE registration into their exclusion set (dead-pid files are reaped — kill(pid, 0) + a /proc/<pid>/cmdline recycled-pid guard). Files are removed on process exit; findAvailable reads the registry but never writes it.
  • Hardhat deploy shares the repo's compile cache. The Ethereum outpost deploy (npx hardhat run src/scripts/deployLocal.ts) compiles-if-stale into <wire-ethereum>/artifacts/ + <wire-ethereum>/cache/, which are checkout-wide. Hardhat has no cross-process build lock, and two concurrent compiles corrupt those dirs for every later run. The harness serializes the hardhat invocation with a host-global file lock (EthereumOutpostBootstrapper.HardhatDeployLockPath, long-hold retry options). When artifacts are already fresh the hold is just the deploy (~30–60s); a colliding pair serializes its two deploys.
  • Ethereum deploy state is per-cluster, never repo-shared. Deploy configs and address files (outpost-addrs.json, liqeth-addrs.json, …) live under <cluster>/data/ethereum-deployments/ (ClusterConfigProvider.ethereumDeploymentsPath), and deployLocal.ts is pointed there via WIRE_ETH_DEPLOYMENTS_PATH. The pre-rewrite location — <wire-ethereum>/.local/deployments/, shared by every run — let one run's deploy wipe another's configs and address files mid-deploy (2026-07-02 pair-1 incident: the "stale artifact" clear of run B deleted the address file run A was about to read). Never read outpost addresses from the repo path; go through ClusterConfigProvider.ethereumDeploymentsPath / EthereumCollateralTool.loadOutpostAddresses(deploymentsPath).
  • Hardhat ABI artifacts are read-shared. ABIs load from <wire-ethereum>/artifacts/ (read-only after compile, which the deploy lock serializes) — that is fine to share; only deploy state had to move.
  • Solana state is already per-cluster. The SOL bootstrap persists its keypair + mock-mints under <cluster>/data/, and programs load per validator via --bpf-program — no shared writes.
  • Process cleanup is pid-targeted, never name-targeted. ProcessManager sweeps only pids recorded in THIS cluster's pidfiles and signals only its own registered pids on exit (with a /proc recycled-pid guard). A host-wide pkill nodeop from any tooling would kill every parallel run's nodes — see the incident note in ProcessManager.ts.

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages