Skip to content

Commit 0a794df

Browse files
committed
Merge remote-tracking branch 'origin/main' into ft/accounts_generation
2 parents 0eb2303 + 3098b18 commit 0a794df

File tree

4 files changed

+173
-3
lines changed

4 files changed

+173
-3
lines changed

README.md

Lines changed: 58 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,10 +29,14 @@ Network URLs/IDs are intentionally **hardcoded** in `src/networks.ts` and are co
2929
- **Local**: MSP `http://127.0.0.1:8080`, RPC `http://127.0.0.1:9888`
3030

3131
Local notes:
32-
- `NETWORK=local` expects:
32+
- `NETWORK=local` matches the “normal” local StorageHub defaults when you boot a local network from the StorageHub repo with:
33+
- `pnpm docker:start:solochain-evm:initialised`
34+
- See StorageHub docs: [Spawning solochain-evm-initialised fullnet](https://github.com/Moonsong-Labs/storage-hub/tree/main/test#spawning-solochain-evm-initialised-fullnet)
35+
- It assumes:
3336
- MSP at `http://127.0.0.1:8080`
3437
- EVM/Substrate RPC at `http://127.0.0.1:9888` / `ws://127.0.0.1:9888`
35-
- SIWE domain/uri: `localhost:3001` / `http://localhost:3001`
38+
- **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:3000` or `localhost:3001`.
39+
- `localhost:3001` / `http://localhost:3001` is what the StorageHub repo’s `demo-app` (SDK examples) commonly uses.
3640

3741
## Commands
3842

@@ -42,6 +46,17 @@ Local notes:
4246
- `pnpm lint:fix` — apply safe lint fixes
4347
- `pnpm test` — build -> artillery
4448
- `pnpm test:msp-unauth` — standalone unauth MSP load test (no SIWE, no keys)
49+
- `pnpm test:download` — file download load test (requires SIWE auth + FILE_KEY)
50+
51+
Examples (local):
52+
53+
```bash
54+
NETWORK=local pnpm test:msp-unauth
55+
```
56+
57+
```bash
58+
NETWORK=local STORAGEHUB_PRIVATE_KEY=0x... pnpm test
59+
```
4560

4661
Examples (local):
4762

@@ -97,6 +112,47 @@ Metrics emitted (counters + histograms):
97112
- `msp.info.ok`, `msp.info.ms`
98113
- `msp.req.err` (total request errors)
99114

115+
## Download load test
116+
117+
This test authenticates via SIWE and downloads a file from the MSP, measuring throughput and latency.
118+
119+
Required env vars:
120+
- `NETWORK` (`testnet` or `stagenet`)
121+
- `FILE_KEY` (the file key/hash to download)
122+
123+
Run:
124+
125+
```bash
126+
NETWORK=stagenet FILE_KEY=<your-file-key> pnpm test:download
127+
```
128+
129+
Knobs (optional):
130+
- `ARTILLERY_WORKERS=4` (parallel local processes)
131+
- `LOG_LEVEL=info` (see Logging section)
132+
133+
Metrics emitted:
134+
- `download.siwe.ok`, `download.siwe.ms` (SIWE auth)
135+
- `download.file.ok`, `download.file.ms` (file download)
136+
- `download.bytes` (total bytes downloaded per request)
137+
- `download.siwe.err`, `download.file.err` (error counters)
138+
139+
## Per-VU private keys (Artillery payload)
140+
141+
This test expects a per-VU `privateKey` variable from `config.payload` in `scenarios/artillery.yml`.
142+
143+
1) Create `data/private_keys.csv` (ignored by git), based on the example:
144+
- `data/private_keys.example.csv`
145+
146+
Notes:
147+
- `pnpm preflight` will use `STORAGEHUB_PRIVATE_KEY` **if set**, otherwise it will use the **first key** in `data/private_keys.csv`.
148+
- If Artillery does not inject `privateKey` into `context.vars` (depends on engine/runtime), the scenario will fall back to reading keys directly from `data/private_keys.csv` (round-robin).
149+
150+
2) Run:
151+
152+
```bash
153+
NETWORK=stagenet LOG_LEVEL=info pnpm test
154+
```
155+
100156
## Scenario output
101157

102158
Counters:

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,8 @@
1616
"test:siwe:byIndex": "pnpm build && pnpm exec artillery run scenarios/siwe.byIndex.yml",
1717
"test:siwe:sequential": "pnpm build && pnpm exec artillery run scenarios/siwe.sequential.yml",
1818
"test:siwe:random": "pnpm build && pnpm exec artillery run scenarios/siwe.random.yml",
19-
"test:siwe:auth:sequential": "pnpm build && pnpm exec artillery run scenarios/siwe.auth.sequential.yml"
19+
"test:siwe:auth:sequential": "pnpm build && pnpm exec artillery run scenarios/siwe.auth.sequential.yml",
20+
"test:download": "pnpm build && pnpm exec tsx scripts/run-artillery-parallel.ts scenarios/artillery.download.yml"
2021
},
2122
"dependencies": {
2223
"@storagehub-sdk/core": "0.4.0",

scenarios/artillery.download.yml

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
config:
2+
target: "http://localhost"
3+
processor: "../dist/src/processors/download.js"
4+
5+
phases:
6+
- name: warmup
7+
duration: 10
8+
arrivalRate: 1
9+
10+
- name: ramp
11+
duration: 20
12+
arrivalRate: 1
13+
rampTo: 10
14+
15+
- name: steady
16+
duration: 40
17+
arrivalRate: 10
18+
19+
- name: cooldown
20+
duration: 10
21+
arrivalRate: 2
22+
rampTo: 1
23+
24+
scenarios:
25+
- name: download_file
26+
flow:
27+
- function: downloadFile
28+

src/processors/download.ts

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
import { Readable } from "node:stream";
2+
import { readEnv } from "../config.js";
3+
import { getLogger } from "../log.js";
4+
import { authenticateWithSiwe, connectMsp } from "../sdk/msp.js";
5+
import { initWalletFromPrivateKey, to0xPrivateKey } from "../sdk/wallet.js";
6+
import { NETWORKS } from "../networks.js";
7+
import { nextPrivateKey } from "../privateKeys.js";
8+
9+
type ArtilleryEvents = Readonly<{
10+
emit: (type: string, name: string, value: number) => void;
11+
}>;
12+
13+
type ArtilleryContext = Readonly<{
14+
vars?: Record<string, unknown>;
15+
}>;
16+
17+
function getFileKey(): string {
18+
const key = process.env.FILE_KEY;
19+
if (!key) {
20+
throw new Error("FILE_KEY env var is required");
21+
}
22+
return key;
23+
}
24+
25+
export async function downloadFile(
26+
context: ArtilleryContext,
27+
events: ArtilleryEvents
28+
): Promise<void> {
29+
const logger = getLogger();
30+
const env = readEnv();
31+
const network = NETWORKS[env.network];
32+
const fileKey = getFileKey();
33+
34+
// Get private key (from Artillery vars or fallback)
35+
const privateKeyRaw =
36+
typeof context.vars?.privateKey === "string" &&
37+
context.vars.privateKey.length > 0
38+
? context.vars.privateKey
39+
: nextPrivateKey().privateKey;
40+
41+
const privateKey = to0xPrivateKey(privateKeyRaw);
42+
const { walletClient } = initWalletFromPrivateKey(network, privateKey);
43+
44+
// Connect and authenticate
45+
const conn = await connectMsp(env, logger);
46+
47+
const siweStart = Date.now();
48+
try {
49+
await authenticateWithSiwe(conn, env, walletClient, logger);
50+
events.emit("counter", "download.siwe.ok", 1);
51+
events.emit("histogram", "download.siwe.ms", Date.now() - siweStart);
52+
} catch (err) {
53+
events.emit("counter", "download.siwe.err", 1);
54+
logger.error({ err }, "siwe failed");
55+
throw err;
56+
}
57+
58+
// Download file
59+
const dlStart = Date.now();
60+
try {
61+
const file = await conn.client.files.downloadFile(fileKey);
62+
if (!file?.stream) {
63+
throw new Error("downloadFile returned no stream");
64+
}
65+
66+
// Consume stream, count bytes (no disk write)
67+
// Type assertion needed: SDK returns web ReadableStream, Node expects its own variant
68+
const nodeReadable = Readable.fromWeb(
69+
file.stream as unknown as import("stream/web").ReadableStream
70+
);
71+
let totalBytes = 0;
72+
for await (const chunk of nodeReadable) {
73+
totalBytes += (chunk as Buffer).length;
74+
}
75+
76+
events.emit("counter", "download.file.ok", 1);
77+
events.emit("histogram", "download.file.ms", Date.now() - dlStart);
78+
events.emit("histogram", "download.bytes", totalBytes);
79+
logger.info({ fileKey, totalBytes }, "download complete");
80+
} catch (err) {
81+
events.emit("counter", "download.file.err", 1);
82+
logger.error({ err, fileKey }, "download failed");
83+
throw err;
84+
}
85+
}

0 commit comments

Comments
 (0)