Skip to content

test: property-based fuzz testing for js->zig->js roundtrip - #41

Open
nazarhussain wants to merge 19 commits into
nh/bigint-exact-conversionsfrom
nh/fuzz-testing
Open

nazarhussain wants to merge 19 commits into
nh/bigint-exact-conversionsfrom
nh/fuzz-testing

Conversation

@nazarhussain

@nazarhussain nazarhussain commented May 29, 2026 •

Copy link
Copy Markdown
Contributor

Adds a property-based fuzz harness for zapi Number, BigInt and Uint8Array converters (vitest + fast-check, driven through a dedicated test_fuzz_numeric.node addon). The harness surfaced several latent bugs and an API sharp edge.

Stacked on #83. The library fixes and API changes the harness found (exact-by-default toI64/toU64/toI128, the *LowBits variants, BigInt.fromWords, the getValueBigintWords sign fix and the Number doc comments) now live in #83. This PR contains only the fuzz tests, the build wiring and CI. Merge #83 first.

Changes

  • tests/fuzz/: round-trip fuzz targets and fast-check properties for Number, BigInt and Uint8Array, with a seeds workflow (see tests/fuzz/README.md).
  • tests/unit/conversions.test.ts: fixed edge-value checks of the same targets against their oracles, run by test:js.
  • tests/build.zig.zon / tests/build.zig: a separate Zig package for the fuzz addon, mirroring examples/ and consuming zapi via --fork=... It installs to the root zig-out/.
  • package.json: build:zig now runs build:zig:examples and build:zig:tests; test:js also runs tests/unit/; adds test:fuzz:round_trip and the fast-check dev dependency.
  • Dockerfile.fuzz, tests/fuzz/platform/run-fuzz.ts and .github/workflows/fuzz-image.yml: the fuzz image for the ChainSafe fuzz platform. Pull requests build and smoke-test it, which covers the fuzz properties in CI; publishing to GHCR is a manual run.

🤖 Generated with Claude Code

Comment thread .github/workflows/ci.yml Outdated
- name: Test
run: pnpm test:js

test-fuzz:

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

seems a little strange to fuzz in CI

@wemeetagain wemeetagain left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the API changes and additional comments are good

@matthewkeil matthewkeil moved this from Awaiting Author to Review Requested in Lodestar Team Coordination Sep 10, 2026
@nazarhussain
nazarhussain force-pushed the nh/fuzz-testing branch 2 times, most recently from 4b09223 to e3384ed Compare October 19, 2026 10:11
@nazarhussain
nazarhussain changed the base branch from main to nh/bigint-exact-conversions October 19, 2026 10:11
nazarhussain and others added 16 commits October 19, 2026 12:31
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Round-trips JS BigInt → i128 → JS BigInt via toI128 and fromWords.
Property test covers the i128 boundary plus out-of-range truncation via
BigInt.asIntN(128, ·). Oracle test adds deterministic boundary cases for
zero, I128_MIN/MAX (exact), just-out-of-range ±1, and oversized values
whose low 128 bits are all zero (expected result: 0n).
The seeds-workflow doc claimed NaN/Infinity could be persisted via a
{"__special": ...} encoding, but no corresponding reviver exists in
fuzz.test.ts — following the README literally would silently corrupt
test input. NaN/Infinity are already in edgeNumbers and exercised every
run, so persisting them adds nothing. Trim the README to match reality.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
nazarhussain and others added 2 commits November 6, 2026 10:50
…90)

Packages the fast-check fuzz targets from #41 as a Docker image for the
[ChainSafe fuzz
platform](https://github.com/ChainSafe/lodestar-ansible-development/tree/main/playbooks/fuzz),
so they run continuously as one campaign (`zapi:round-trip`).

> **Stacked on #41** (which is stacked on #83). Merge those first.

### Changes
- `Dockerfile.fuzz` (+ `.dockerignore`): a build stage that downloads
Zig 0.16.0 (checksum-verified) and builds `test_fuzz_numeric.node` with
`ReleaseSafe` and `-Dcpu=baseline`. The runtime stage is `node:24-slim`
with the addon, the tests and `node_modules`.
- `tests/fuzz/platform/run-fuzz.ts`: the entrypoint, which implements
the platform's container contract. It re-runs every `*.fuzz.test.ts` in
a loop (100 000 cases per property by default, a fresh random seed each
time) until SIGTERM.
- A failing property is saved once to
`$FUZZ_FAILURES/failure-<hash>.json`, de-duplicated by test name and
shrunk counterexample. The file includes the seed and path for replay.
Fuzzing then continues.
- `$FUZZ_METRICS/status` is rewritten atomically after each iteration.
It reports `execs_total`, `execs_per_second`, `crashes_total` and
`last_find_timestamp_seconds`. There is no coverage or corpus field,
because fast-check is not coverage-guided.
  - Counters persist in `$FUZZ_STATE/stats.json`.
- A test file that fails to load, or a run with no tests, exits non-zero
(an infrastructure failure).
- `.github/workflows/fuzz-image.yml`: mirrors lodestar-z's
`fuzz-image.yml`. A `build and test` job runs with read-only permissions
and smoke-tests the image under the platform's restrictions (non-root
uid, read-only rootfs, no capabilities, no network, `--init`).
Publishing is **manual** (`workflow_dispatch`): a separate `publish`
job, the only one with `packages: write`, pushes the exact tested image
as `main` (run on `main`) or `branch-<name>` (any other branch), plus
`sha-<short>`, and keeps the newest 20 versions. lodestar-z's AFL++-only
steps (seed replay, `core_pattern`, minimize) are omitted; the platform
reads none of them.
- `tests/fuzz/README.md`: a section on continuous fuzzing.

### Verified locally (arm64 Docker, platform restrictions)
- Runs continuously and stops on SIGTERM with exit 0.
- The status file is written and updated.
- Counters resume across restarts.
- A planted failing property is saved once over about 20 failing
iterations.
- A hidden addon gives `FATAL a test suite failed to load` and exit 1.

The amd64 build and the workflow itself have not run yet; this PR's CI
is their first run.

### Follow-ups
- After merging, publish by running the workflow on `main` from the
Actions tab. Then set the GHCR package `zapi-fuzz` to **public**. The
platform pulls without credentials.
- Add the target to `group_vars/fuzzer/vars.yml` in
lodestar-ansible-development:
  ```yaml
  - project: zapi
    target: round-trip
    image: ghcr.io/chainsafe/zapi-fuzz:main
    cpu_mhz: 1000
    memory_mb: 1024
    duration: 0
    uid: 60011
    gid: 60011
  ```

🤖 Generated with [Claude Code](https://claude.com/claude-code)
The fuzz-image workflow already runs the fuzz properties on pull requests,
so the separate test-fuzz CI job is redundant. The fixed edge-value checks
move to tests/unit and run with test:js.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Review Requested

Development

Successfully merging this pull request may close these issues.

2 participants