This repo is an Artillery-based load testing suite to measure limits and identify bottlenecks in DataHaven and StorageHub.
- Node.js >= 22
pnpm
pnpm installRequired:
NETWORK(testnet,stagenetorlocal)
Per-test required:
TEST_MNEMONIC(required by tests that derive accounts and do SIWE)FILE_KEY(required by the download test)
Network URLs/IDs are intentionally hardcoded in src/networks.ts:
- Testnet: MSP
https://deo-dh-backend.testnet.datahaven-infra.network - Stagenet: MSP
https://deo-dh-backend.stagenet.datahaven-infra.network - Local: MSP
http://127.0.0.1:8080, RPChttp://127.0.0.1:9888
Local notes:
NETWORK=localmatches the “normal” local StorageHub defaults when you boot a local network from the StorageHub repo with:pnpm docker:start:solochain-evm:initialised- See StorageHub docs: Spawning solochain-evm-initialised fullnet
- It assumes:
- MSP at
http://127.0.0.1:8080 - EVM/Substrate RPC at
http://127.0.0.1:9888/ws://127.0.0.1:9888
- MSP at
- SIWE domain/uri: these should be provided by the dApp doing SIWE (they are not “network” properties). For local testing, a dApp often runs on
localhost:3000orlocalhost:3001.localhost:3001/http://localhost:3001is what the StorageHub repo’sdemo-app(SDK examples) commonly uses.
pnpm fmt— check formattingpnpm fmt:fix— apply formattingpnpm lint— check lint rulespnpm lint:fix— apply safe lint fixespnpm typecheck— TypeScript typecheck (no emit)pnpm build— compile to./distpnpm with-logs -- <command>— run<command>with per-run JSONL logs + a local index allocatorpnpm run:with-logs -- <command>— build, then run<command>with logs + index allocatorpnpm test:run <scenario.yml>— run any Artillery scenario (via the logs/index wrapper)pnpm test:unauth— unauthenticated MSP endpoints scenariopnpm test:examples.getProfile— SIWE +getProfilescenariopnpm test:examples.createBucket— derive account + create a bucket scenariopnpm test:download— SIWE + download file scenariopnpm util:fund-accounts— fund derived test accounts
pnpm util:fund-accounts is a small helper to print or fund EVM accounts derived from a mnemonic.
Print first 10 derived addresses:
TEST_MNEMONIC="..." pnpm util:fund-accountsFund first 10 derived addresses (0.01 native token each):
NETWORK=local TEST_MNEMONIC="..." pnpm util:fund-accounts -- --privateKey 0x... --amount 0.01 --start 0 --count 10Notes:
--batchSizecontrols parallelism per batch (default: 10). IfbatchSize > count, onlycounttransfers are sent.
List available scenarios:
ls scenariosRun one:
NETWORK=stagenet pnpm test:run scenarios/<file>.ymlExamples (replace the scenario file with anything from ls scenarios):
NETWORK=local pnpm test:run scenarios/msp.unauth.ymlLOG_LEVEL=info LOG_CONSOLE=true \
NETWORK=stagenet \
TEST_MNEMONIC="test test test test test test test test test test test junk" \
pnpm test:run scenarios/examples.getProfile.ymlLOG_LEVEL=info LOG_CONSOLE=true \
NETWORK=stagenet \
TEST_MNEMONIC="test test test test test test test test test test test junk" \
FILE_KEY="<your-file-key>" \
pnpm test:run scenarios/download.ymlLogging is handled by Pino.
Env vars:
LOG_LEVEL:fatal|error|warn|info|debug|trace|silent(default:error)LOG_CONSOLE:true|false(default:true)LOG_FILE_ENABLED:true|false(default:true). When enabled andLOG_FILEis not set, logs are written to./logs/run-<date_time>-pid<PID>.jsonl.LOG_FILE: path to a log file (optional). When set, logs are appended to that file in addition to console (unlessLOG_CONSOLE=false).
Examples:
LOG_LEVEL=debug NETWORK=testnet pnpm test:run scenarios/msp.unauth.ymlLOG_LEVEL=info LOG_FILE=./artillery.log NETWORK=testnet pnpm test:run scenarios/msp.unauth.ymlThis test randomly calls:
GET /health(client.info.getHealth())GET /info(client.info.getInfo())
It uses NETWORK=testnet|stagenet and the MSP base URL from src/networks.ts.
Run:
NETWORK=stagenet pnpm test:run scenarios/msp.unauth.ymlKnobs (optional):
VU_SLEEP_MIN_MS=50/VU_SLEEP_MAX_MS=250(jitter per request loop)MSP_TIMEOUT_MS=60000(override HTTP timeout)
Metrics emitted (counters + histograms):
msp.health.ok,msp.health.msmsp.info.ok,msp.info.msmsp.req.err(total request errors)
This test performs init steps (derive + SIWE) and downloads a file from the MSP, measuring throughput and latency.
Required env vars:
NETWORK(testnet,stagenetorlocal)TEST_MNEMONICFILE_KEY(the file key/hash to download)
Run:
NETWORK=stagenet FILE_KEY=<your-file-key> pnpm test:run scenarios/download.ymlKnobs (optional):
LOG_LEVEL=info(see Logging section)
Metrics emitted:
download.file.ok,download.file.ms(file download)download.bytes(total bytes downloaded per request)download.file.err(error counter)auth.siwe.err(only if SIWE fails; init steps are muted so only errors surface)
Most scenarios follow the same pattern:
- Init (muted):
deriveAccount→SIWE - Actions (not muted): call one or more action processors (e.g.
actionGetProfile,downloadFile)
The muting is controlled by two processor steps:
muteMetrics: while muted, the metrics helper will only emit*.errcounters; it drops ok counters + histograms.unmuteMetrics: restores normal metric emission for the action phase.
This keeps summaries focused on action timings while still surfacing setup/auth failures.
- Picks a unique account index (via the local index allocator started by
scripts/run-scenario.ts) - Derives an account from
TEST_MNEMONIC - Persists
privateKey(and derivation metadata) into Artillery vars for later steps
- Reads the derived
privateKey - Calls the SDK SIWE auth (
mspClient.auth.SIWE(...)) - Persists the resulting
__siweSessioninto Artillery vars
-
Create a scenario file under
scenarios/(for examplescenarios/myTest.yml). -
Use the standard template:
config.processor: "../dist/src/processors/index.js"- Init steps (muted):
muteMetrics→deriveAccount→SIWE→unmuteMetrics - Then call your action processor(s)
- Run it via the generic runner:
NETWORK=stagenet pnpm test:run scenarios/myTest.yml(Optional) If you want a shortcut alias, add test:myTest: "pnpm test:run scenarios/myTest.yml".
Metrics depend on the scenario and processor functions used. Common ones:
msp.health.ok,msp.health.ms,msp.info.ok,msp.info.ms,msp.req.erraction.getProfile.ok,action.getProfile.ms,action.getProfile.errdownload.file.ok,download.file.ms,download.bytes,download.file.err
When init steps are wrapped with muteMetrics/unmuteMetrics, only *.err counters from init will appear in the summary (ok + histograms are muted).