Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
a86ab02
chore(deps): add fast-check and test:fuzz script
nazarhussain May 28, 2026
0cc0e58
chore(tests): scaffold fuzz_numeric addon module
nazarhussain May 28, 2026
21af8ad
test(fuzz): add rtNumberF64 round-trip fuzz target
nazarhussain May 28, 2026
e578734
test(fuzz): add Number i32/u32/i64 round-trip properties
nazarhussain May 28, 2026
2c7a8be
test(fuzz): add BigInt i64/u64 round-trip properties
nazarhussain May 28, 2026
209b47b
test(fuzz): add lossless-flag introspection for toI64/toU64
nazarhussain May 28, 2026
1005609
test(fuzz): add rtBigIntI128 round-trip target
nazarhussain May 28, 2026
57bcdde
test(fuzz): add BigInt word-level round-trip
nazarhussain May 28, 2026
72f6e4b
ci: add fuzz tests job
nazarhussain May 28, 2026
4b67883
docs(fuzz): document seeds workflow and dev loop
nazarhussain May 28, 2026
0d80fa4
docs(fuzz): correct seeds README — drop unimplemented __special reviver
nazarhussain May 29, 2026
5d0c2a3
chore: restructure oracle code
nazarhussain May 29, 2026
594a4ad
test(fuzz): cover exact and LowBits BigInt conversions
nazarhussain May 29, 2026
a032b18
test: add uint8array fuzz tests
nazarhussain May 29, 2026
3689e3b
chore: restructure tests files
nazarhussain May 29, 2026
3f2ee8c
chore: rename fuzz tests script
nazarhussain May 29, 2026
17080db
test(fuzz): note getValueBigintWords overflow in rtBigIntWords
nazarhussain Oct 19, 2026
669e36c
ci(fuzz): run fast-check fuzz targets on the ChainSafe fuzz platform …
nazarhussain Nov 6, 2026
a31c600
test: move oracle checks to unit tests, drop CI fuzz job
nazarhussain Nov 6, 2026
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
175 changes: 175 additions & 0 deletions .github/workflows/fuzz-image.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
name: Fuzz image

# Builds Dockerfile.fuzz -- the fast-check round-trip properties in tests/fuzz --
# and publishes it to GHCR for the ChainSafe fuzz platform.
#
# When an image is built:
# - manual run on main (Actions tab) -> build, test, publish `main`
# - manual run on any other branch -> build, test, publish `branch-<name>`
# - pull request touching code the targets exercise -> build and test only
# Publishing is deliberately manual: the fuzz platform follows the `main` tag
# and restarts the campaign on every publish, so run this once enough good
# changes have landed, not on every commit. Pull requests still build and test
# so a broken Dockerfile.fuzz is caught before the next publish.
#
# Two jobs: `build and test` runs for every event with read-only permissions;
# `publish` runs only on a manual run, holds the only write token, and pushes
# the exact image the build job tested.
#
# Tags: `main` is what the fuzz platform runs, and only a run on main moves it.
# A run on another branch publishes `branch-<name>` instead, for trying a branch
# on the platform before it merges. `sha-<short>` names each build's source
# commit, also recorded as the org.opencontainers.image.revision label.
# Old versions beyond the newest 20 are deleted.

on:
pull_request:
paths:
- "src/**"
- "tests/**"
- "build.zig"
- "build.zig.zon"
- "package.json"
- "pnpm-lock.yaml"
- "Dockerfile.fuzz"
- "Dockerfile.fuzz.dockerignore"
- ".github/workflows/fuzz-image.yml"
workflow_dispatch:

concurrency:
group: fuzz-image-${{ github.ref }}
cancel-in-progress: true

env:
IMAGE: ghcr.io/chainsafe/zapi-fuzz

jobs:
build:
name: build and test
runs-on: ubuntu-latest
# Read-only: this job runs on pull requests and never pushes.
permissions:
contents: read
steps:
- name: Checkout Repository
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1

- name: Set up Docker Buildx
uses: docker/setup-buildx-action@f87e5991a6d7451dcb8d9637bfbc97413f497069 # v4.4.1

- name: Build
uses: docker/build-push-action@c3c9e263c25d99ce0380d002d59b67737d91b0dc # v7.4.0
with:
context: .
file: Dockerfile.fuzz
platforms: linux/amd64
build-args: REVISION=${{ github.sha }}
tags: zapi-fuzz:ci
load: true
# Off so the image is a single manifest: one package version per
# publish, which the version cleanup below relies on.
provenance: false
sbom: false
cache-from: type=gha,scope=fuzz-image
cache-to: type=gha,scope=fuzz-image,mode=max

# The same restrictions the fuzz platform applies: a non-root uid, read-only
# root filesystem, no capabilities, no network, an init process.
- name: A campaign starts and reports status under the platform's restrictions
run: |
mkdir -p /tmp/fz/state /tmp/fz/artifacts /tmp/fz/failures /tmp/fz/metrics /tmp/fz/local
sudo chown -R 60001:60001 /tmp/fz
docker run -d --name smoke --user 60001:60001 --read-only --cap-drop all \
--security-opt no-new-privileges --network none --init --pids-limit 4096 \
-e FUZZ_PROJECT=zapi -e FUZZ_TARGET=round-trip \
-e FUZZ_STATE=/fuzz/state -e FUZZ_ARTIFACTS=/fuzz/artifacts \
-e FUZZ_FAILURES=/fuzz/failures -e FUZZ_METRICS=/fuzz/metrics \
-e TMPDIR=/local \
-v /tmp/fz/state:/fuzz/state -v /tmp/fz/artifacts:/fuzz/artifacts \
-v /tmp/fz/failures:/fuzz/failures -v /tmp/fz/metrics:/fuzz/metrics \
-v /tmp/fz/local:/local \
zapi-fuzz:ci
sleep 30
docker logs smoke
test "$(docker inspect -f '{{.State.Running}}' smoke)" = true
! docker logs smoke 2>&1 | grep -q '^FATAL'
cat /tmp/fz/metrics/status
awk '$1 == "execs_total" && $2 > 0 { ok = 1 } END { exit !ok }' /tmp/fz/metrics/status
# Every property passes on a clean build; a failure here is a real bug.
awk '$1 == "crashes_total" && $2 == 0 { ok = 1 } END { exit !ok }' /tmp/fz/metrics/status
docker stop -t 60 smoke
test "$(docker inspect -f '{{.State.ExitCode}}' smoke)" = 0

# Hand the tested image itself to the publish job, so what is published is
# exactly what passed above -- no second build.
- name: Save the tested image
if: github.event_name == 'workflow_dispatch' && startsWith(github.ref, 'refs/heads/')
run: docker save zapi-fuzz:ci -o /tmp/zapi-fuzz.tar

- name: Upload the tested image
if: github.event_name == 'workflow_dispatch' && startsWith(github.ref, 'refs/heads/')
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: zapi-fuzz-image
path: /tmp/zapi-fuzz.tar
retention-days: 1

publish:
name: publish
needs: build
# Manual runs on a branch only; see the header comment.
if: github.event_name == 'workflow_dispatch' && startsWith(github.ref, 'refs/heads/')
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps:
- name: Download the tested image
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
with:
name: zapi-fuzz-image
path: /tmp

- name: Log in to GHCR
uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}

- name: Push
id: push
run: |
# main moves `main`; any other branch gets its own tag, sanitized to
# Docker's tag charset and length.
if [ "$GITHUB_REF_NAME" = main ]; then
tag=main
else
tag="branch-$(printf '%s' "$GITHUB_REF_NAME" | tr 'A-Z' 'a-z' | tr -c 'a-z0-9._-' '-' | cut -c1-100)"
fi
docker load -i /tmp/zapi-fuzz.tar
docker tag zapi-fuzz:ci "${IMAGE}:sha-${GITHUB_SHA::7}"
docker tag zapi-fuzz:ci "${IMAGE}:${tag}"
docker push "${IMAGE}:sha-${GITHUB_SHA::7}"
docker push "${IMAGE}:${tag}"
echo "tag=${tag}" >> "$GITHUB_OUTPUT"
echo "digest=$(docker image inspect -f '{{index .RepoDigests 0}}' "${IMAGE}:${tag}" | sed 's/.*@//')" >> "$GITHUB_OUTPUT"

- name: Summary
run: |
{
echo "### Fuzz image published"
echo
echo "\`${IMAGE}:${{ steps.push.outputs.tag }}\` now points at \`${{ steps.push.outputs.digest }}\`,"
echo "built from \`${GITHUB_SHA}\`. Fuzz platform targets following this tag"
echo "restart on the new image; their counters carry over."
} >> "$GITHUB_STEP_SUMMARY"

# Without provenance or SBOM attestations, each publish is exactly one
# package version, so this keeps the newest 20 builds.
- name: Delete old image versions
uses: actions/delete-package-versions@e5bc658cc4c965c472efe991f8beea3981499c55 # v5.0.0
with:
package-name: zapi-fuzz
package-type: container
min-versions-to-keep: 20
63 changes: 63 additions & 0 deletions Dockerfile.fuzz
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
# Fuzz image for the ChainSafe fuzz platform: the fast-check round-trip properties
# in tests/fuzz/, driven by tests/fuzz/platform/run-fuzz.ts. The platform provides
# the FUZZ_* environment and the /fuzz volume. See tests/fuzz/README.md.
#
# docker build -f Dockerfile.fuzz -t zapi-fuzz .

# --- build: Zig compiles the test_fuzz_numeric.node addon
FROM node:24-bookworm-slim AS build

ARG ZIG_VERSION=0.16.0
# From https://ziglang.org/download/index.json -- update together with ZIG_VERSION.
ARG ZIG_SHA256_X86_64=70e49664a74374b48b51e6f3fdfbf437f6395d42509050588bd49abe52ba3d00
ARG ZIG_SHA256_AARCH64=ea4b09bfb22ec6f6c6ceac57ab63efb6b46e17ab08d21f69f3a48b38e1534f17
ARG TARGETARCH

RUN apt-get update \
&& apt-get install -y --no-install-recommends ca-certificates curl xz-utils \
&& rm -rf /var/lib/apt/lists/*

RUN case "${TARGETARCH}" in \
amd64) arch=x86_64; sum="${ZIG_SHA256_X86_64}" ;; \
arm64) arch=aarch64; sum="${ZIG_SHA256_AARCH64}" ;; \
*) echo "unsupported TARGETARCH '${TARGETARCH}'" >&2; exit 1 ;; \
esac \
&& curl -fsSL -o /tmp/zig.tar.xz \
"https://ziglang.org/download/${ZIG_VERSION}/zig-${arch}-linux-${ZIG_VERSION}.tar.xz" \
&& echo "${sum} /tmp/zig.tar.xz" | sha256sum -c - \
&& mkdir /opt/zig && tar -xJf /tmp/zig.tar.xz -C /opt/zig --strip-components=1 \
&& rm /tmp/zig.tar.xz
ENV PATH=/opt/zig:$PATH

WORKDIR /src
RUN corepack enable
COPY package.json pnpm-lock.yaml ./
RUN pnpm install --frozen-lockfile
COPY . .

# ReleaseSafe keeps Zig's safety checks (bounds, overflow), so those bugs panic
# instead of passing silently. -Dcpu=baseline: otherwise the addon targets the
# build runner's CPU and dies with SIGILL on a host without its extensions.
RUN --mount=type=cache,target=/root/.cache/zig \
cd tests && zig build --fork=.. --prefix ../zig-out -Doptimize=ReleaseSafe -Dcpu=baseline

# --- runtime: node, the addon, the tests and their node_modules only
FROM node:24-bookworm-slim

WORKDIR /opt/zapi
COPY --from=build /src/node_modules ./node_modules
COPY --from=build /src/zig-out/lib/test_fuzz_numeric.node ./zig-out/lib/
COPY --from=build /src/package.json ./
COPY --from=build /src/tests/fuzz ./tests/fuzz
COPY --from=build /src/tests/utils ./tests/utils
# The platform runs the container as an arbitrary non-root uid.
RUN chmod -R a+rX /opt/zapi

ARG REVISION=unknown
ENV FUZZ_REVISION=${REVISION}
LABEL org.opencontainers.image.source="https://github.com/ChainSafe/zapi" \
org.opencontainers.image.title="zapi-fuzz" \
org.opencontainers.image.description="fast-check round-trip fuzz targets for zapi" \
org.opencontainers.image.revision="${REVISION}"

ENTRYPOINT ["node", "/opt/zapi/tests/fuzz/platform/run-fuzz.ts"]
7 changes: 7 additions & 0 deletions Dockerfile.fuzz.dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# Build context for Dockerfile.fuzz (BuildKit reads <Dockerfile>.dockerignore).
.git
**/.zig-cache
**/zig-out
**/zig-pkg
**/node_modules
lib
8 changes: 6 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,12 @@
"scripts": {
"build": "pnpm build:js && pnpm build:zig",
"build:js": "tsc --outDir lib",
"build:zig": "cd examples && zig build --fork=..",
"build:zig": "pnpm build:zig:examples && pnpm build:zig:tests",
"build:zig:examples": "cd examples && zig build --fork=..",
"build:zig:tests": "cd tests && zig build --fork=.. --prefix ../zig-out",
"test:zig": "zig build test:zapi",
"test:js": "vitest run examples/**/*.test.ts",
"test:js": "vitest run examples/**/*.test.ts tests/unit/",
"test:fuzz:round_trip": "pnpm build:zig && vitest run tests/fuzz/",
"test": "pnpm test:zig && pnpm test:js",
"lint:zig": "zig fmt src",
"lint:js": "pnpm biome check",
Expand All @@ -34,6 +37,7 @@
"@biomejs/biome": "^2.5.8",
"@chainsafe/biomejs-config": "^1.0.0",
"@types/node": "^24.1.0",
"fast-check": "^4.8.0",
"typescript": "^5.8.3",
"vitest": "^4.1.10"
},
Expand Down
16 changes: 16 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 8 additions & 0 deletions tests/build.zig
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
const std = @import("std");
const zbuild = @import("zbuild");

pub fn build(b: *std.Build) !void {
@setEvalBranchQuota(200_000);
const manifest = @import("build.zig.zon");
_ = try zbuild.configureBuild(b, manifest, .{});
}
38 changes: 38 additions & 0 deletions tests/build.zig.zon
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
.{
.name = .zapi_tests,
.version = "0.0.0",
.fingerprint = 0xd21dc10774fb3dc7,
.minimum_zig_version = "0.16.0",
.paths = .{
"build.zig",
"build.zig.zon",
"fuzz",
},
.dependencies = .{
// Repository builds use --fork so the tests consume the local
// checkout while preserving the URL/hash dependency boundary.
.zapi = .{
.url = "https://github.com/ChainSafe/zapi/archive/b934988f8595e5a5fdc497b7212631004f800116.tar.gz",
.hash = "zapi-3.1.0-rIqzUfHFBADg6qo5AXJzbfHgs6LqpwgVKYal4qCG4XSW",
},
.zbuild = .{
.url = "git+https://github.com/chainsafe/zbuild.git#f7d5f19de09f2808e54acf599605f9e18c9bd804",
.hash = "zbuild-0.4.0-XJFav-1nAgDmfVY1nVSIIzbkcbuHZj7yMb7Fv8yvznPO",
},
},
.modules = .{
.test_fuzz_numeric = .{
.root_source_file = "fuzz/mod.zig",
.imports = .{.zapi},
.link_libc = true,
},
},
.libraries = .{
.test_fuzz_numeric = .{
.root_module = .test_fuzz_numeric,
.linkage = .dynamic,
.linker_allow_shlib_undefined = true,
.dest_sub_path = "test_fuzz_numeric.node",
},
},
}
Loading
Loading