Skip to content

Commit d0499a6

Browse files
feat: add cli command to run the solana-test-validator with ER setup (#567)
<!-- greptile_comment --> <h2>Greptile Overview</h2> Updated On: 2025-10-09 15:30:44 UTC <h3>Summary</h3> This PR adds a new CLI command `mb-test-validator` that provides a convenient way to run `solana-test-validator` with MagicBlock's Ephemeral Rollup (ER) setup. The implementation includes several key components: **Core Functionality**: The PR introduces `mbTestValidator.ts`, which wraps `solana-test-validator` with predefined BPF programs and accounts necessary for MagicBlock's ER infrastructure. This eliminates the need for users to manually specify numerous `--bpf-program` and `--account` flags when setting up a local test environment. **Code Refactoring**: The existing `ephemeralValidator.ts` was refactored to extract process management logic into a reusable `runWithForwardedExit` function. This promotes code reuse between the ephemeral validator and the new test validator implementations, following DRY principles while maintaining consistent process handling. **Build Integration**: The package.json was updated to include the new `mb-test-validator` binary and modified the build script to automatically fetch required program binaries and account dumps from MagicBlock's devnet RPC during compilation. **Asset Management**: A new bash script `fetch-local-dumps.sh` was added to handle downloading of Solana program binaries (.so files) and account data (.json files) from MagicBlock's devnet, ensuring the test validator has access to the correct program and account states for local development. This change integrates well with the existing codebase by following established patterns from `ephemeralValidator.ts` and maintaining consistency in CLI tool implementation. The solution addresses the practical need for developers to quickly spin up a properly configured test validator without manual setup of MagicBlock-specific infrastructure components. <h3>Important Files Changed</h3> <details> <summary>Changed Files</summary> | Filename | Score | Overview | |----------|-------|----------| | .github/packages/npm-package/ephemeralValidator.ts | 5/5 | Refactored to extract reusable process management logic into `runWithForwardedExit` function | | .github/packages/npm-package/mbTestValidator.ts | 2/5 | New CLI tool for running solana-test-validator with ER setup, but contains duplicate program entries and potential process handling issues | | .github/packages/npm-package/package.json | 4/5 | Added new CLI binary and build script modification to fetch local dumps during compilation | | .github/packages/npm-package/scripts/fetch-local-dumps.sh | 4/5 | New script to fetch program binaries and account data from MagicBlock devnet with proper error handling | </details> <h3>Confidence score: 3/5</h3> - This PR requires careful review due to several implementation issues that could cause runtime problems - Score reflects concerns about duplicate program entries, process exit handling logic, and missing error handling in critical paths - Pay close attention to mbTestValidator.ts which contains the most problematic code patterns <h3>Sequence Diagram</h3> ```mermaid sequenceDiagram participant User participant "npm/yarn" participant mbTestValidator.ts participant "fetch-local-dumps.sh" participant "Solana CLI" participant "MagicBlock RPC" participant "solana-test-validator" participant "Local Filesystem" User->>npm/yarn: "npm run build" or "yarn build" npm/yarn->>mbTestValidator.ts: "tsc" (compile TypeScript) npm/yarn->>fetch-local-dumps.sh: "bash scripts/fetch-local-dumps.sh" fetch-local-dumps.sh->>Local Filesystem: "mkdir -p local-dumps" loop For each account fetch-local-dumps.sh->>Solana CLI: "solana account <pubkey> --output json --url <RPC_URL>" Solana CLI->>MagicBlock RPC: "GET account data" MagicBlock RPC-->>Solana CLI: "Account JSON data" Solana CLI-->>fetch-local-dumps.sh: "Account data" fetch-local-dumps.sh->>Local Filesystem: "Write <pubkey>.json" end loop For each program fetch-local-dumps.sh->>Solana CLI: "solana program dump <pubkey> <output.so> --url <RPC_URL>" Solana CLI->>MagicBlock RPC: "GET program binary" MagicBlock RPC-->>Solana CLI: "Program .so binary" Solana CLI-->>fetch-local-dumps.sh: "Program binary" fetch-local-dumps.sh->>Local Filesystem: "Write <pubkey>.so" end User->>npm/yarn: "mb-test-validator [args]" npm/yarn->>mbTestValidator.ts: "Execute mb-test-validator command" mbTestValidator.ts->>Local Filesystem: "Check for required .so and .json files" Local Filesystem-->>mbTestValidator.ts: "File existence status" alt Missing files mbTestValidator.ts->>User: "Warning: missing local dumps files" end mbTestValidator.ts->>solana-test-validator: "spawn with --bpf-program and --account flags" loop Process lifecycle management solana-test-validator->>mbTestValidator.ts: "Process events (exit, signals)" mbTestValidator.ts->>User: "Forward exit codes and signals" end User->>mbTestValidator.ts: "SIGINT (Ctrl+C)" mbTestValidator.ts->>solana-test-validator: "Kill SIGINT/SIGTERM" ``` <!-- greptile_other_comments_section --> <!-- /greptile_comment -->
1 parent d259367 commit d0499a6

File tree

4 files changed

+156
-8
lines changed

4 files changed

+156
-8
lines changed

.github/packages/npm-package/ephemeralValidator.ts

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -33,10 +33,8 @@ function getExePath(): string {
3333
}
3434
}
3535

36-
function runEphemeralValidator(location: string): void {
37-
const args = process.argv.slice(2);
38-
const ephemeralValidator = spawn(location, args, { stdio: "inherit" });
39-
ephemeralValidator.on("exit", (code: number | null, signal: NodeJS.Signals | null) => {
36+
function runWithForwardedExit(child: ReturnType<typeof spawn>): void {
37+
child.on("exit", (code: number | null, signal: NodeJS.Signals | null) => {
4038
process.on("exit", () => {
4139
if (signal) {
4240
process.kill(process.pid, signal);
@@ -47,11 +45,17 @@ function runEphemeralValidator(location: string): void {
4745
});
4846

4947
process.on("SIGINT", () => {
50-
ephemeralValidator.kill("SIGINT");
51-
ephemeralValidator.kill("SIGTERM");
48+
child.kill("SIGINT");
49+
child.kill("SIGTERM");
5250
});
5351
}
5452

53+
function runEphemeralValidator(location: string): void {
54+
const args = process.argv.slice(2);
55+
const ephemeralValidator = spawn(location, args, { stdio: "inherit" });
56+
runWithForwardedExit(ephemeralValidator);
57+
}
58+
5559
function tryPackageEphemeralValidator(): boolean {
5660
try {
5761
const path = getExePath();
@@ -101,4 +105,6 @@ function trySystemEphemeralValidator(): void {
101105

102106
runEphemeralValidator(absoluteBinaryPath);
103107
}
108+
109+
// If the first argument is our special command, run the test validator and exit.
104110
tryPackageEphemeralValidator() || trySystemEphemeralValidator();
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
#!/usr/bin/env node
2+
import { spawn } from "child_process";
3+
import * as path from "path";
4+
import * as fs from "fs";
5+
6+
function runWithForwardedExit(child: ReturnType<typeof spawn>): void {
7+
child.on("exit", (code: number | null, signal: NodeJS.Signals | null) => {
8+
process.on("exit", () => {
9+
if (signal) {
10+
process.kill(process.pid, signal);
11+
} else if (code !== null) {
12+
process.exit(code);
13+
}
14+
});
15+
});
16+
17+
process.on("SIGINT", () => {
18+
child.kill("SIGINT");
19+
child.kill("SIGTERM");
20+
});
21+
}
22+
23+
function dumpsDir(): string {
24+
// Compiled js lives in lib/, source in package root. We want <package-root>/bin/local-dumps
25+
const libDir = __dirname;
26+
const root = path.resolve(libDir, "..");
27+
return path.join(root, "ephemeral-validator", "bin", "local-dumps");
28+
}
29+
30+
function runMbTestValidator(): void {
31+
const exe = "solana-test-validator";
32+
const dumps = dumpsDir();
33+
const p = (name: string) => path.join(dumps, name);
34+
35+
const args = [
36+
// programs
37+
"--bpf-program",
38+
"DELeGGvXpWV2fqJUhqcF5ZSYMS4JTLjteaAMARRSaeSh",
39+
p("DELeGGvXpWV2fqJUhqcF5ZSYMS4JTLjteaAMARRSaeSh.so"),
40+
"--bpf-program",
41+
"noopb9bkMVfRPU8AsbpTUg8AQkHtKwMYZiFUjNRtMmV",
42+
p("noopb9bkMVfRPU8AsbpTUg8AQkHtKwMYZiFUjNRtMmV.so"),
43+
"--bpf-program",
44+
"Vrf1RNUjXmQGjmQrQLvJHs9SNkvDJEsRVFPkfSQUwGz",
45+
p("Vrf1RNUjXmQGjmQrQLvJHs9SNkvDJEsRVFPkfSQUwGz.so"),
46+
"--bpf-program",
47+
"noopb9bkMVfRPU8AsbpTUg8AQkHtKwMYZiFUjNRtMmV",
48+
p("noopb9bkMVfRPU8AsbpTUg8AQkHtKwMYZiFUjNRtMmV.so"),
49+
// accounts
50+
"--account",
51+
"mAGicPQYBMvcYveUZA5F5UNNwyHvfYh5xkLS2Fr1mev",
52+
p("mAGicPQYBMvcYveUZA5F5UNNwyHvfYh5xkLS2Fr1mev.json"),
53+
"--account",
54+
"EpJnX7ueXk7fKojBymqmVuCuwyhDQsYcLVL1XMsBbvDX",
55+
p("EpJnX7ueXk7fKojBymqmVuCuwyhDQsYcLVL1XMsBbvDX.json"),
56+
"--account",
57+
"7JrkjmZPprHwtuvtuGTXp9hwfGYFAQLnLeFM52kqAgXg",
58+
p("7JrkjmZPprHwtuvtuGTXp9hwfGYFAQLnLeFM52kqAgXg.json"),
59+
"--account",
60+
"Cuj97ggrhhidhbu39TijNVqE74xvKJ69gDervRUXAxGh",
61+
p("Cuj97ggrhhidhbu39TijNVqE74xvKJ69gDervRUXAxGh.json"),
62+
"--account",
63+
"5hBR571xnXppuCPveTrctfTU7tJLSN94nq7kv7FRK5Tc",
64+
p("5hBR571xnXppuCPveTrctfTU7tJLSN94nq7kv7FRK5Tc.json"),
65+
"--account",
66+
"F72HqCR8nwYsVyeVd38pgKkjXmXFzVAM8rjZZsXWbdE",
67+
p("F72HqCR8nwYsVyeVd38pgKkjXmXFzVAM8rjZZsXWbdE.json"),
68+
];
69+
70+
const expectedFiles = [
71+
"DELeGGvXpWV2fqJUhqcF5ZSYMS4JTLjteaAMARRSaeSh.so",
72+
"noopb9bkMVfRPU8AsbpTUg8AQkHtKwMYZiFUjNRtMmV.so",
73+
"Vrf1RNUjXmQGjmQrQLvJHs9SNkvDJEsRVFPkfSQUwGz.so",
74+
"mAGicPQYBMvcYveUZA5F5UNNwyHvfYh5xkLS2Fr1mev.json",
75+
"EpJnX7ueXk7fKojBymqmVuCuwyhDQsYcLVL1XMsBbvDX.json",
76+
"7JrkjmZPprHwtuvtuGTXp9hwfGYFAQLnLeFM52kqAgXg.json",
77+
"Cuj97ggrhhidhbu39TijNVqE74xvKJ69gDervRUXAxGh.json",
78+
"5hBR571xnXppuCPveTrctfTU7tJLSN94nq7kv7FRK5Tc.json",
79+
"F72HqCR8nwYsVyeVd38pgKkjXmXFzVAM8rjZZsXWbdE.json",
80+
];
81+
const missingFiles = expectedFiles
82+
.map((f) => p(f))
83+
.filter((full) => !fs.existsSync(full));
84+
if (missingFiles.length > 0) {
85+
console.warn("Warning: missing local dumps files:\n" + missingFiles.join("\n"));
86+
}
87+
88+
const extraArgs = process.argv.slice(2);
89+
const child = spawn(exe, [...args, ...extraArgs], { stdio: "inherit" });
90+
runWithForwardedExit(child);
91+
}
92+
93+
runMbTestValidator();

.github/packages/npm-package/package.json

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,14 @@
1212
},
1313
"license": "Business Source License 1.1",
1414
"bin": {
15-
"ephemeral-validator": "ephemeralValidator.js"
15+
"ephemeral-validator": "ephemeralValidator.js",
16+
"mb-test-validator": "mbTestValidator.js"
1617
},
1718
"scripts": {
1819
"typecheck": "tsc --noEmit",
1920
"lint": "prettier */*.js \"*/**/*{.js,.ts}\" --check ",
2021
"lint:fix": "node_modules/.bin/prettier */*.js \"*/**/*{.js,.ts}\" -w",
21-
"build": "tsc",
22+
"build": "tsc && bash scripts/fetch-local-dumps.sh",
2223
"dev": "yarn build && node lib/index.js"
2324
},
2425
"devDependencies": {
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
#!/usr/bin/env bash
2+
set -euo pipefail
3+
4+
# This script fetches program .so binaries and account .json files into
5+
# <package-root>/bin/local-dumps, to be used by mb-test-validator.
6+
# It mirrors the list from the issue description and defaults to MagicBlock devnet RPC.
7+
8+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
9+
PKG_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
10+
DUMPS_DIR="$PKG_ROOT/lib/bin/local-dumps"
11+
RPC_URL="${SOLANA_RPC_URL:-https://rpc.magicblock.app/devnet}"
12+
13+
mkdir -p "$DUMPS_DIR"
14+
15+
# Dump accounts
16+
accounts=(
17+
mAGicPQYBMvcYveUZA5F5UNNwyHvfYh5xkLS2Fr1mev
18+
EpJnX7ueXk7fKojBymqmVuCuwyhDQsYcLVL1XMsBbvDX
19+
7JrkjmZPprHwtuvtuGTXp9hwfGYFAQLnLeFM52kqAgXg
20+
Cuj97ggrhhidhbu39TijNVqE74xvKJ69gDervRUXAxGh
21+
5hBR571xnXppuCPveTrctfTU7tJLSN94nq7kv7FRK5Tc
22+
F72HqCR8nwYsVyeVd38pgKkjXmXFzVAM8rjZZsXWbdE
23+
)
24+
25+
for acc in "${accounts[@]}"; do
26+
out="$DUMPS_DIR/$acc.json"
27+
echo "Dumping account $acc -> $out"
28+
if ! solana account "$acc" --output json --url "$RPC_URL" > "$out"; then
29+
echo "Warning: failed to dump account $acc" >&2
30+
fi
31+
done
32+
33+
# Dump programs
34+
programs=(
35+
DELeGGvXpWV2fqJUhqcF5ZSYMS4JTLjteaAMARRSaeSh
36+
noopb9bkMVfRPU8AsbpTUg8AQkHtKwMYZiFUjNRtMmV
37+
Vrf1RNUjXmQGjmQrQLvJHs9SNkvDJEsRVFPkfSQUwGz
38+
)
39+
40+
for prog in "${programs[@]}"; do
41+
out="$DUMPS_DIR/$prog.so"
42+
echo "Dumping program $prog -> $out"
43+
if ! solana program dump "$prog" "$out" --url "$RPC_URL"; then
44+
echo "Warning: failed to dump program $prog" >&2
45+
fi
46+
done
47+
48+
echo "local-dumps directory: $DUMPS_DIR"

0 commit comments

Comments
 (0)