Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
FROM node:25-bookworm-slim
FROM --platform=linux/amd64 node:25-bookworm-slim

Check warning on line 1 in Dockerfile

View workflow job for this annotation

GitHub Actions / docker-build

FROM --platform flag should not use a constant value

FromPlatformFlagConstDisallowed: FROM --platform flag should not use constant value "linux/amd64" More info: https://docs.docker.com/go/dockerfile/rule/from-platform-flag-const-disallowed/

RUN useradd -d /app -m typeberry

Expand Down
31 changes: 31 additions & 0 deletions bin/jam/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,37 @@ JAM_LOG=trace jam dev 1
JAM_LOG=networking:debug,state:trace jam
```

### `JAM_FUZZ*` (Standard Target Packaging)

When `JAM_FUZZ` is set, the node starts in fuzz-target mode regardless of any
command-line arguments (passing CLI args alongside `JAM_FUZZ` is rejected).
This is the entrypoint expected by the
[JAM conformance fuzz target packaging](https://github.com/davxy/jam-conformance/tree/main/fuzz-proto#standard-target-packaging)
contract.

| Variable | Required | Purpose |
|----------|----------|---------|
| `JAM_FUZZ` | Yes (any non-empty value) | Activates fuzz mode. |
| `JAM_FUZZ_SPEC` | Yes | Chain spec: `tiny` or `full`. |
| `JAM_FUZZ_SOCK_PATH` | Yes | Unix domain socket path the target listens on. |
| `JAM_FUZZ_DATA_PATH` | Yes | Persistent data directory (currently unused; reserved). |
| `JAM_FUZZ_LOG_LEVEL` | No | Log verbosity: `error`, `warn`, `info`, `debug`, `trace`. Overrides `JAM_LOG` in fuzz mode. |

The target stays up across multiple fuzzer sessions; on each `Initialize`
message it resets the in-memory state to the genesis sent by the fuzzer.

**Docker example:**

```bash
docker run --rm \
-e JAM_FUZZ=1 \
-e JAM_FUZZ_SPEC=tiny \
-e JAM_FUZZ_SOCK_PATH=/tmp/jam.sock \
-e JAM_FUZZ_DATA_PATH=/tmp/jam-data \
-v /tmp:/tmp \
typeberry:latest
```

### OpenTelemetry Variables

| Variable | Description | Default |
Expand Down
205 changes: 205 additions & 0 deletions bin/jam/fuzz-env.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,205 @@
import assert from "node:assert";
import { describe, it } from "node:test";
import { KnownChainSpec, NODE_DEFAULTS } from "@typeberry/config-node";
import { Level } from "@typeberry/logger";
import { Command } from "./args.js";
import {
JAM_FUZZ,
JAM_FUZZ_DATA_PATH,
JAM_FUZZ_LOG_LEVEL,
JAM_FUZZ_SOCK_PATH,
JAM_FUZZ_SPEC,
readFuzzEnv,
synthesizeFuzzArgs,
} from "./fuzz-env.js";

describe("readFuzzEnv", () => {
it("returns null when JAM_FUZZ is unset", () => {
assert.strictEqual(readFuzzEnv({}), null);
});

it("returns null when JAM_FUZZ is empty string", () => {
assert.strictEqual(readFuzzEnv({ [JAM_FUZZ]: "" }), null);
});

it("parses tiny spec happy path", () => {
const result = readFuzzEnv({
[JAM_FUZZ]: "1",
[JAM_FUZZ_SPEC]: "tiny",
[JAM_FUZZ_SOCK_PATH]: "/tmp/jam.sock",
[JAM_FUZZ_DATA_PATH]: "/tmp/jam-data",
});

assert.deepStrictEqual(result, {
spec: KnownChainSpec.Tiny,
socketPath: "/tmp/jam.sock",
dataPath: "/tmp/jam-data",
logLevel: null,
});
});

it("parses full spec happy path", () => {
const result = readFuzzEnv({
[JAM_FUZZ]: "1",
[JAM_FUZZ_SPEC]: "full",
[JAM_FUZZ_SOCK_PATH]: "/tmp/jam.sock",
[JAM_FUZZ_DATA_PATH]: "/tmp/jam-data",
});

assert.strictEqual(result?.spec, KnownChainSpec.Full);
});

it("rejects missing JAM_FUZZ_SPEC", () => {
assert.throws(
() =>
readFuzzEnv({
[JAM_FUZZ]: "1",
[JAM_FUZZ_SOCK_PATH]: "/tmp/s",
[JAM_FUZZ_DATA_PATH]: "/tmp/d",
}),
new RegExp(`${JAM_FUZZ_SPEC} is required`),
);
});

it("rejects missing JAM_FUZZ_SOCK_PATH", () => {
assert.throws(
() =>
readFuzzEnv({
[JAM_FUZZ]: "1",
[JAM_FUZZ_SPEC]: "tiny",
[JAM_FUZZ_DATA_PATH]: "/tmp/d",
}),
new RegExp(`${JAM_FUZZ_SOCK_PATH} is required`),
);
});

it("rejects missing JAM_FUZZ_DATA_PATH", () => {
assert.throws(
() =>
readFuzzEnv({
[JAM_FUZZ]: "1",
[JAM_FUZZ_SPEC]: "tiny",
[JAM_FUZZ_SOCK_PATH]: "/tmp/s",
}),
new RegExp(`${JAM_FUZZ_DATA_PATH} is required`),
);
});

it("rejects empty JAM_FUZZ_SOCK_PATH", () => {
assert.throws(
() =>
readFuzzEnv({
[JAM_FUZZ]: "1",
[JAM_FUZZ_SPEC]: "tiny",
[JAM_FUZZ_SOCK_PATH]: "",
[JAM_FUZZ_DATA_PATH]: "/tmp/d",
}),
new RegExp(`${JAM_FUZZ_SOCK_PATH} is required`),
);
});

it("rejects bogus JAM_FUZZ_SPEC", () => {
assert.throws(
() =>
readFuzzEnv({
[JAM_FUZZ]: "1",
[JAM_FUZZ_SPEC]: "huge",
[JAM_FUZZ_SOCK_PATH]: "/tmp/s",
[JAM_FUZZ_DATA_PATH]: "/tmp/d",
}),
new RegExp(`${JAM_FUZZ_SPEC} must be one of: tiny, full`),
);
});

it("parses JAM_FUZZ_LOG_LEVEL=debug as Level.LOG", () => {
const result = readFuzzEnv({
[JAM_FUZZ]: "1",
[JAM_FUZZ_SPEC]: "tiny",
[JAM_FUZZ_SOCK_PATH]: "/tmp/s",
[JAM_FUZZ_DATA_PATH]: "/tmp/d",
[JAM_FUZZ_LOG_LEVEL]: "debug",
});
assert.strictEqual(result?.logLevel, Level.LOG);
});

it("parses JAM_FUZZ_LOG_LEVEL=TRACE case-insensitively", () => {
const result = readFuzzEnv({
[JAM_FUZZ]: "1",
[JAM_FUZZ_SPEC]: "tiny",
[JAM_FUZZ_SOCK_PATH]: "/tmp/s",
[JAM_FUZZ_DATA_PATH]: "/tmp/d",
[JAM_FUZZ_LOG_LEVEL]: "TRACE",
});
assert.strictEqual(result?.logLevel, Level.TRACE);
});

it("parses each documented log level", () => {
const cases: [string, Level][] = [
["error", Level.ERROR],
["warn", Level.WARN],
["info", Level.INFO],
["debug", Level.LOG],
["trace", Level.TRACE],
];
for (const [raw, expected] of cases) {
const result = readFuzzEnv({
[JAM_FUZZ]: "1",
[JAM_FUZZ_SPEC]: "tiny",
[JAM_FUZZ_SOCK_PATH]: "/tmp/s",
[JAM_FUZZ_DATA_PATH]: "/tmp/d",
[JAM_FUZZ_LOG_LEVEL]: raw,
});
assert.strictEqual(result?.logLevel, expected, `level for '${raw}'`);
}
});

it("rejects bogus JAM_FUZZ_LOG_LEVEL", () => {
assert.throws(
() =>
readFuzzEnv({
[JAM_FUZZ]: "1",
[JAM_FUZZ_SPEC]: "tiny",
[JAM_FUZZ_SOCK_PATH]: "/tmp/s",
[JAM_FUZZ_DATA_PATH]: "/tmp/d",
[JAM_FUZZ_LOG_LEVEL]: "BOGUS",
}),
new RegExp(`${JAM_FUZZ_LOG_LEVEL} must be one of: error, warn, info, debug, trace`),
);
});
});

describe("synthesizeFuzzArgs", () => {
it("builds a FuzzTarget Arguments value with tiny flavor override", () => {
const args = synthesizeFuzzArgs({
spec: KnownChainSpec.Tiny,
socketPath: "/tmp/jam.sock",
dataPath: "/tmp/jam-data",
logLevel: null,
});

assert.deepStrictEqual(args, {
command: Command.FuzzTarget,
args: {
nodeName: NODE_DEFAULTS.name,
config: [...NODE_DEFAULTS.config, '.flavor="tiny"'],
pvm: NODE_DEFAULTS.pvm,
socket: "/tmp/jam.sock",
version: 1,
initGenesisFromAncestry: false,
},
});
});

it("uses 'full' flavor when spec is full", () => {
const args = synthesizeFuzzArgs({
spec: KnownChainSpec.Full,
socketPath: "/tmp/jam.sock",
dataPath: "/tmp/jam-data",
logLevel: null,
});
if (args.command !== Command.FuzzTarget) {
throw new Error("expected FuzzTarget command");
}
assert.deepStrictEqual(args.args.config, [...NODE_DEFAULTS.config, '.flavor="full"']);
});
});
87 changes: 87 additions & 0 deletions bin/jam/fuzz-env.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import { KnownChainSpec, NODE_DEFAULTS } from "@typeberry/config-node";
import { Level } from "@typeberry/logger";
import { type Arguments, Command } from "./args.js";

export type FuzzEnv = {
spec: KnownChainSpec;
socketPath: string;
dataPath: string;
logLevel: Level | null;
};

export const JAM_FUZZ = "JAM_FUZZ";
export const JAM_FUZZ_SPEC = "JAM_FUZZ_SPEC";
export const JAM_FUZZ_SOCK_PATH = "JAM_FUZZ_SOCK_PATH";
export const JAM_FUZZ_DATA_PATH = "JAM_FUZZ_DATA_PATH";
export const JAM_FUZZ_LOG_LEVEL = "JAM_FUZZ_LOG_LEVEL";

const REQUIRED_VARS = [JAM_FUZZ_SPEC, JAM_FUZZ_SOCK_PATH, JAM_FUZZ_DATA_PATH] as const;

// Note the JAM-conformance vocabulary uses "debug" but the typeberry Level
// enum names the same level "LOG" (see packages/core/logger/options.ts).
const LOG_LEVELS: Record<string, Level> = {
error: Level.ERROR,
warn: Level.WARN,
info: Level.INFO,
debug: Level.LOG,
trace: Level.TRACE,
};

export function readFuzzEnv(env: NodeJS.ProcessEnv | Record<string, string | undefined>): FuzzEnv | null {
const flag = env[JAM_FUZZ] ?? "";
if (flag.trim().length === 0) {
return null;
}

for (const name of REQUIRED_VARS) {
const value = env[name] ?? "";
if (value.trim().length === 0) {
throw new Error(`${JAM_FUZZ} is set but ${name} is required.`);
}
}

const specRaw = env[JAM_FUZZ_SPEC] ?? "";
let spec: KnownChainSpec;
if (specRaw === KnownChainSpec.Tiny) {
spec = KnownChainSpec.Tiny;
} else if (specRaw === KnownChainSpec.Full) {
spec = KnownChainSpec.Full;
} else {
throw new Error(
`${JAM_FUZZ_SPEC} must be one of: ${KnownChainSpec.Tiny}, ${KnownChainSpec.Full}. Got: '${specRaw}'.`,
);
}

let logLevel: Level | null = null;
const rawLogLevel = env[JAM_FUZZ_LOG_LEVEL] ?? "";
if (rawLogLevel !== "") {
const parsed = LOG_LEVELS[rawLogLevel.toLowerCase()];
if (parsed === undefined) {
throw new Error(
`${JAM_FUZZ_LOG_LEVEL} must be one of: ${Object.keys(LOG_LEVELS).join(", ")}. Got: '${rawLogLevel}'.`,
);
}
logLevel = parsed;
}

return {
spec,
socketPath: env[JAM_FUZZ_SOCK_PATH] ?? "",
dataPath: env[JAM_FUZZ_DATA_PATH] ?? "",
logLevel,
Comment thread
tomusdrw marked this conversation as resolved.
};
}

export function synthesizeFuzzArgs(env: FuzzEnv): Arguments {
return {
command: Command.FuzzTarget,
args: {
nodeName: NODE_DEFAULTS.name,
config: [...NODE_DEFAULTS.config, `.flavor="${env.spec}"`],
pvm: NODE_DEFAULTS.pvm,
socket: env.socketPath,
version: 1,
initGenesisFromAncestry: false,
},
};
}
23 changes: 18 additions & 5 deletions bin/jam/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { exportBlocks, importBlocks, JamConfig, main, mainFuzz } from "@typeberr
import { Telemetry } from "@typeberry/telemetry";
import { asOpaqueType, workspacePathFix } from "@typeberry/utils";
import { type Arguments, Command, HELP, parseArgs } from "./args.js";
import { readFuzzEnv, synthesizeFuzzArgs } from "./fuzz-env.js";

export * from "./args.js";

Expand All @@ -20,12 +21,24 @@ let args: Arguments;
const withRelPath = workspacePathFix(`${import.meta.dirname}/../..`);

try {
const parsed = parseArgs(process.argv.slice(2), withRelPath);
if (parsed === null) {
console.info(HELP);
process.exit(0);
const fuzzEnv = readFuzzEnv(process.env);
if (fuzzEnv !== null) {
if (process.argv.length > 2) {
throw new Error("When JAM_FUZZ is set, command-line arguments are not accepted.");
}
// In fuzz mode, the logger config is determined by JAM_FUZZ_LOG_LEVEL alone;
// any JAM_LOG filters configured at module load are discarded so behavior is
// deterministic regardless of which env vars happen to be present.
Logger.configureAll("", fuzzEnv.logLevel ?? Level.LOG);
args = synthesizeFuzzArgs(fuzzEnv);
} else {
const parsed = parseArgs(process.argv.slice(2), withRelPath);
if (parsed === null) {
console.info(HELP);
process.exit(0);
}
args = parsed;
}
args = parsed;
} catch (e) {
console.error(`\n${e}\n`);
console.info(HELP);
Expand Down
Loading