Skip to content

Correctness, diagnostics & bench/coverage hardening (Zig-book adoption)#102

Merged
koko1123 merged 4 commits into
mainfrom
koko/zig-book-adoption
Jun 20, 2026
Merged

Correctness, diagnostics & bench/coverage hardening (Zig-book adoption)#102
koko1123 merged 4 commits into
mainfrom
koko/zig-book-adoption

Conversation

@koko1123

@koko1123 koko1123 commented Jun 19, 2026

Copy link
Copy Markdown
Contributor

What

Adopts practices surfaced by cross-reading Systems Programming with Zig (mactsouk/zigSP, GPL-3.0) against this MIT codebase. Idioms only — no code copied. Most of the book is confirmatory: eth.zig already does comptime selectors, lock-free nonce management, leak-checked tests, and a criterion-style bench with a Rust-alloy comparison better than the book. This PR lands the genuine gaps.

All changes validated locally on Zig 0.16.0: zig build, zig build fmt-check, zig fmt --check src/ tests/, and zig build test all pass.

Tier 1 — correctness & diagnostics (55e9a0f)

  1. u256 big-endian conversion is now host-endianness independent. uint256.toBigEndianBytes/fromBigEndianBytes used @byteSwap/@bitCast, correct only on little-endian hosts; on a big-endian target every ABI word, event topic, and tx field would be byte-reversed. Switched to std.mem.writeInt/readInt with explicit .big (identical codegen on LE, correct on BE). Strengthened the known-value test to pin multi-byte byte order. (Latent: no big-endian targets today.)

  2. ContractAbi (abi_json) is now backed by a single ArenaAllocator. The old per-allocation cleanup leaked already-built entries when a later ABI item failed to parse (and on a toOwnedSlice OOM). The arena makes the error path leak-free and deletes the recursive freeParams. Added a regression test — leaks trip std.testing.allocator.

  3. Provider now surfaces JSON-RPC error diagnostics. Scalar calls threw an opaque error.RpcError, discarding code/message; only the batch path kept them. Provider.lastError() now returns {code, message} so callers can tell an on-chain revert (code 3) from a transport failure. Stored inline (no extra heap, no new deinit).

  4. Provider.next_id is now std.atomic.Value(u64) (fetchAdd), so a shared Provider can't hand out duplicate request ids.

Tier 2 — bench/coverage/build infra (21cf88f)

  • Bench statistics: the harness now reports per-batch stddev alongside ns/op and emits a machine-readable BENCH_JSON|{name,ns_per_op,stddev_ns,iters} line per benchmark (verified locally). compare.sh filters those from its human view.
  • Nightly bench CI: new Bench workflow (schedule + workflow_dispatch) runs the harness off the PR-blocking path and uploads results (incl. the JSONL) as an artifact — the basis for a future committed baseline + regression diff.
  • Coverage: zig build install-test emits the unit-test binary; make coverage runs kcov over src/ (excluding vendored crypto); a non-required CI job publishes the kcov report as an artifact. (kcov is Linux-only; validated via the build step locally, the kcov run is exercised in CI.)
  • Build ergonomics: zig build fmt-check (mirrors the CI fmt job) and zig build docs (emits API docs to zig-out/docs). Makefile gains coverage/docs; .gitignore ignores kcov-out/.

Intentionally not included (2 of the 9 assessed findings)

After implementing the rest, two assessed items proved counterproductive as specified, so they were deliberately left out (discussed and agreed):

  • Named error sets on the contract/provider boundary. The pure layers (abi_encode/abi_decode/wallet) already use named error sets (EncodeError/DecodeError/WalletError). The only inferred sets left are on functions wrapping std.http; naming those would drag the entire std.http error surface into the public API, making it less stable. Keeping inferred sets there is the correct design.
  • Provider request-scoped arena. The transport owns the response buffer (allocated outside any per-call arena) and the param-builders are public + tested, so a per-call arena would add boilerplate while saving ~1 alloc — no net simplification. Worth revisiting only alongside a transport API change that lets the response be arena-allocated.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features
    • Added JSON-RPC error diagnostics surfaced via provider “last error”
    • Benchmarks now report standard deviation alongside results
  • Build & Tests
    • Added nightly benchmark automation with downloadable benchmark artifacts
    • Added coverage reporting and API documentation generation, plus formatting checks and unit-test install support
  • Bug Fixes
    • Improved benchmark comparison filtering
    • Fixed big-endian uint256 conversions to be platform-independent
    • Strengthened ABI parsing for safer memory handling and improved diagnostic behavior in error cases

koko1123 added 2 commits June 19, 2026 18:03
Adopts idioms surfaced by cross-reading "Systems Programming with Zig"
against the SDK. Four contained fixes:

1. uint256 big-endian conversion is now host-endianness independent.
   toBigEndianBytes/fromBigEndianBytes used @byteswap, correct only on
   little-endian hosts; on a big-endian target every ABI word, event
   topic and tx field would be byte-reversed. Switch to std.mem.writeInt
   /readInt with explicit .big (same codegen on LE, correct on BE).

2. ContractAbi (abi_json) is now backed by a single ArenaAllocator.
   The old per-allocation cleanup leaked already-built entries when a
   later ABI item failed to parse (and on a toOwnedSlice OOM). The arena
   makes the error path leak-free and deletes the recursive freeParams.
   Regression test added (leaks would trip std.testing.allocator).

3. Provider now surfaces JSON-RPC error diagnostics. Scalar calls threw
   an opaque error.RpcError, discarding code/message; only the batch
   path kept them. Provider.lastError() now exposes {code, message} so
   callers can tell an on-chain revert (code 3) from a transport failure.

4. Provider.next_id is now std.atomic.Value(u64) (fetchAdd), so a shared
   Provider cannot hand out duplicate request ids.
…infra)

- bench harness now reports per-batch stddev alongside ns/op, and emits a
  machine-readable `BENCH_JSON|{...}` line per benchmark (name/ns_per_op/
  stddev_ns/iters). compare.sh filters those lines from its human view.
- Add a nightly + on-demand Bench workflow that runs the harness off the
  PR-blocking path and uploads results (incl. the JSONL) as an artifact.
- Coverage: `zig build install-test` emits the unit-test binary; `make
  coverage` runs kcov over src/ (excluding vendored crypto); a non-required
  CI job publishes the kcov report as an artifact.
- New build steps: `zig build fmt-check` (mirrors the CI fmt job) and
  `zig build docs` (emits API docs to zig-out/docs). Makefile gains
  `coverage`/`docs` targets; .gitignore ignores kcov-out/.
@vercel

vercel Bot commented Jun 19, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
eth-zig Ready Ready Preview, Comment Jun 20, 2026 1:57am

Request Review

@coderabbitai

coderabbitai Bot commented Jun 19, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: f31905ab-01dd-459b-af66-63a4b0de545a

📥 Commits

Reviewing files that changed from the base of the PR and between cd883bc and 2d4c659.

📒 Files selected for processing (5)
  • .github/workflows/bench.yml
  • .github/workflows/ci.yml
  • bench/compare.sh
  • src/abi_json.zig
  • tests/integration_tests.zig
🚧 Files skipped from review as they are similar to previous changes (4)
  • bench/compare.sh
  • .github/workflows/bench.yml
  • .github/workflows/ci.yml
  • src/abi_json.zig

📝 Walkthrough

Walkthrough

This PR adds per-call RPC error diagnostics and atomic request IDs to Provider, refactors ContractAbi to use an arena allocator for lifetime management, fixes uint256 big-endian byte conversion to be host-endian-independent, adds standard deviation measurement to the benchmark harness, and introduces new build steps (install-test, fmt-check, docs) along with CI jobs for nightly benchmarks and code coverage.

Changes

Source Library Changes

Layer / File(s) Summary
uint256 big-endian conversion fix
src/uint256.zig
toBigEndianBytes/fromBigEndianBytes now use std.mem.writeInt/readInt with .big endianness instead of @byteSwap/@bitCast. The known-value test verifies full byte-slice equality and round-trip.
ContractAbi arena-backed ownership
src/abi_json.zig
ContractAbi.fromJson allocates an ArenaAllocator, parses the JSON DOM into it via parseFromSliceLeaky, and stores only the arena pointer. deinit destroys the arena instead of manually freeing entries. Parse helpers drop per-allocation errdefer. parseInputs returns &.{} for missing/non-array fields. A new test covers the partial-parse leak case.
Provider atomic ID and ErrorInfo struct
src/provider.zig
next_id becomes std.atomic.Value(u64), initialized to 1. Provider.ErrorInfo and inline last_error/last_error_storage fields are added. lastError() returns stored diagnostics or null.
Provider RPC error capture in request methods
src/provider.zig
A new extractResult() helper calls captureRpcError() on error.RpcError. getTransactionReceipt, getBlock, and getLogs intercept error.RpcError from their parse calls and invoke captureRpcError. rpcCall() resets last_error per call. captureRpcError parses the JSON-RPC error object into provider-owned storage (code + 256-byte truncated message).
BatchCaller atomic IDs and diagnostics tests
src/provider.zig, tests/integration_tests.zig
BatchCaller.execute() uses atomic fetchAdd(n, .monotonic) for contiguous ID reservation. New tests validate lastError() initial null, captureRpcError code/message capture, and atomic next_id starting at 1. Integration tests updated to load next_id atomically.

Tooling, CI, and Benchmark Improvements

Layer / File(s) Summary
Benchmark stddev measurement and output
bench/bench.zig, bench/compare.sh
BenchResult gains stddev_ns: u64. runBench collects per-batch samples and computes Bessel-corrected stddev. runAndPrint includes stddev_ns in human-readable and BENCH_JSON output. Table header gains a stddev column. compare.sh filters BENCH_JSON lines before passing output to the Python parser.
New build.zig steps
build.zig
Adds install-test (exposes unit-test binary for external coverage), fmt-check (zig fmt --check over src/tests/bench/build.zig), and docs (emits and installs API docs from eth module into zig-out/docs).
CI workflows, Makefile, and .gitignore
.github/workflows/bench.yml, .github/workflows/ci.yml, Makefile, .gitignore
Adds a nightly bench.yml workflow capturing BENCH_JSON output as text and JSONL artifacts. Adds a ci.yml coverage job that builds the test binary, runs kcov, and uploads the kcov-out artifact. Pins actions/checkout and mlugg/setup-zig to commit SHAs in existing jobs. Makefile gains KCOV variable and coverage/docs targets. kcov-out/ is added to .gitignore.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Possibly related PRs

  • StrobeLabs/eth.zig#24: Modifies the benchmark harness structure and output pipeline in bench/bench.zig, directly overlapping with this PR's stddev_ns addition to the same harness.
  • StrobeLabs/eth.zig#60: Modifies the benchmark harness in bench/bench.zig (changing the timing/IO infrastructure and updating runBench/output), so the changes directly overlap at the benchmark code level.
  • StrobeLabs/eth.zig#84: Modifies src/abi_json.zig around ContractAbi.fromJson/ABI parsing internals, with changes overlapping at the same parse helper areas like parseInputs/parseParam.

Poem

🐇 Hopping through the code at night,
Arenas sweep the allocations right,
Stddev measured, errors caught,
Atomic IDs precisely wrought,
Big-endian bytes now truly big—
This rabbit does a joyful jig! ✨

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the primary changes: correctness fixes (endianness, memory leaks, error diagnostics, atomic IDs) and infrastructure hardening (benchmarks, coverage, build tools).
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch koko/zig-book-adoption

Comment @coderabbitai help to get the list of available commands and usage tips.

Comment thread .github/workflows/bench.yml Fixed
Comment thread .github/workflows/bench.yml Fixed
Comment thread .github/workflows/ci.yml Fixed
Comment thread .github/workflows/ci.yml Fixed

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 5

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
.github/workflows/bench.yml (1)

12-33: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Harden workflow token scope and pin action revisions.

Line [12] lacks explicit permissions, and Lines [18], [19], [27] use mutable tags instead of immutable SHAs. Pinning and least-privilege permissions are needed to reduce supply-chain and token-risk exposure.

Suggested hardening
+permissions:
+  contents: read
+
 jobs:
   bench:
@@
-      - uses: actions/checkout@v4
-      - uses: mlugg/setup-zig@v2
+      - uses: actions/checkout@<pinned-commit-sha>
+        with:
+          persist-credentials: false
+      - uses: mlugg/setup-zig@<pinned-commit-sha>
@@
-      - name: Upload benchmark results
-        uses: actions/upload-artifact@v4
+      - name: Upload benchmark results
+        uses: actions/upload-artifact@<pinned-commit-sha>
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/bench.yml around lines 12 - 33, The workflow lacks
explicit permissions configuration and uses mutable version tags for actions
instead of immutable commit SHAs. Add a permissions block at the job level
(after the runs-on line in the bench job) with minimal required permissions, and
pin all action revisions by replacing the mutable tags: update
actions/checkout@v4 to its specific commit SHA, mlugg/setup-zig@v2 to its
specific commit SHA, and actions/upload-artifact@v4 to its specific commit SHA.
This reduces supply-chain risk and token exposure by using immutable references
and following least-privilege principles.

Source: Linters/SAST tools

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In @.github/workflows/ci.yml:
- Around line 51-71: The coverage job needs GitHub Actions security hardening
applied. Add a permissions block at the job level in the coverage job (after the
runs-on line) to specify explicit least-privilege token permissions.
Additionally, pin the three action references to immutable commit SHAs instead
of mutable version tags: replace actions/checkout@v4, mlugg/setup-zig@v2, and
actions/upload-artifact@v4 with their full commit SHA values to prevent
supply-chain drift from tag mutations.

In `@bench/compare.sh`:
- Line 22: The grep -v command on line 22 can return exit code 1 when all lines
matching the pattern are filtered out, causing the script to abort under
pipefail mode even when zig build bench succeeds. Fix this by adding a fallback
to true after the grep -v pipeline, such that the overall pipeline always
returns 0 regardless of whether grep finds matching lines or not. This allows
the filtered output to be echoed without aborting the script due to a non-zero
exit code from grep.

In `@src/abi_json.zig`:
- Around line 130-142: The parseInputs function is too lenient when handling
malformed input, treating invalid data as empty parameter lists instead of
rejecting it. On line 134, when a field exists but is not an array, change the
return statement from returning an empty slice to returning an error from the
ComponentError union (such as UnknownType). Similarly, on line 138, change the
non-object item handling from silently continuing the loop to returning an
error, so that malformed entries in the array are properly rejected rather than
ignored. This ensures that invalid ABIs cannot be parsed into different
signatures or shapes.
- Around line 31-33: The catch block in the parseFromSliceLeaky call currently
masks all errors as error.InvalidJson, which prevents callers from
distinguishing between allocation failures and malformed input. Modify the catch
block to explicitly check if the caught error is error.OutOfMemory and propagate
it directly, while converting all other parsing errors to error.InvalidJson.
This allows the caller to handle resource exhaustion separately from invalid
JSON input.

In `@src/provider.zig`:
- Line 23: Update the integration tests in tests/integration_tests.zig to
properly access the atomic next_id field from the provider object. Since next_id
is now defined as std.atomic.Value(u64) in src/provider.zig, all direct accesses
to provider.next_id at lines 385, 388, 390, and 392 must be wrapped with the
.load() method call to retrieve the actual u64 value. Use an appropriate memory
ordering parameter like .acquire or .seq_cst in the .load() calls depending on
your synchronization requirements, such as provider.next_id.load(.acquire).

---

Outside diff comments:
In @.github/workflows/bench.yml:
- Around line 12-33: The workflow lacks explicit permissions configuration and
uses mutable version tags for actions instead of immutable commit SHAs. Add a
permissions block at the job level (after the runs-on line in the bench job)
with minimal required permissions, and pin all action revisions by replacing the
mutable tags: update actions/checkout@v4 to its specific commit SHA,
mlugg/setup-zig@v2 to its specific commit SHA, and actions/upload-artifact@v4 to
its specific commit SHA. This reduces supply-chain risk and token exposure by
using immutable references and following least-privilege principles.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 461356a4-e93f-4dc6-b37f-bf30c6b1c398

📥 Commits

Reviewing files that changed from the base of the PR and between 554e174 and 21cf88f.

📒 Files selected for processing (10)
  • .github/workflows/bench.yml
  • .github/workflows/ci.yml
  • .gitignore
  • Makefile
  • bench/bench.zig
  • bench/compare.sh
  • build.zig
  • src/abi_json.zig
  • src/provider.zig
  • src/uint256.zig

Comment thread .github/workflows/ci.yml
Comment thread bench/compare.sh Outdated
Comment thread src/abi_json.zig Outdated
Comment thread src/abi_json.zig
Comment thread src/provider.zig
- Add `permissions: contents: read` to the CI and Bench workflows
  (resolves CodeQL actions/missing-workflow-permissions). Artifact upload
  uses the Actions runtime token, so no extra scope is needed.
- Pin actions/checkout, mlugg/setup-zig and actions/upload-artifact to
  commit SHAs (resolves CodeQL actions/unpinned-tag).
- Coverage job: kcov was dropped from Ubuntu's repos (gone in 24.04) and
  ships no prebuilt binaries, so the `apt-get install kcov` step failed.
  Build a pinned kcov release (v43) from source instead.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In @.github/workflows/bench.yml:
- Line 22: The checkout action at the beginning of the bench workflow does not
include the persist-credentials parameter, which means the GitHub token will
remain accessible to subsequent build steps. Add persist-credentials: false as a
parameter to the actions/checkout step to prevent the token from being
accessible to build scripts or potentially included in uploaded artifacts,
following the security best practice of limiting credential exposure.

In @.github/workflows/ci.yml:
- Line 24: Add the `persist-credentials: false` parameter to all four
`actions/checkout` steps in the workflow file at lines 24, 35, 49, and 62 to
prevent the GITHUB_TOKEN from being exposed to subsequent steps. For each
`actions/checkout` action, add a new line with the parameter
`persist-credentials: false` under the `with:` section (or create a `with:`
section if it doesn't exist) to explicitly disable credential persistence as a
defense-in-depth security measure alongside the existing workflow-level
`contents: read` permission.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: ad86cdae-e30d-47c9-813a-c899b43b6d54

📥 Commits

Reviewing files that changed from the base of the PR and between 21cf88f and cd883bc.

📒 Files selected for processing (2)
  • .github/workflows/bench.yml
  • .github/workflows/ci.yml

Comment thread .github/workflows/bench.yml
Comment thread .github/workflows/ci.yml
- tests/integration_tests.zig: access the now-atomic provider.next_id via
  .load(.monotonic) (lines 385/388/390/392). These didn't compile after the
  atomic change; the unit-test CI job never builds the integration suite, so
  it slipped through. Verified `zig build integration-test` now compiles.
- abi_json fromJson: propagate error.OutOfMemory from parseFromSliceLeaky
  instead of masking allocator exhaustion as error.InvalidJson.
- bench/compare.sh: `grep -v` can exit 1 if it filters every line, aborting
  the script under `set -o pipefail`; append `|| true`.
- ci.yml/bench.yml: add `persist-credentials: false` to all checkout steps
  (defense in depth; keeps the token out of later steps/artifacts).

Skipped (with reason): the lenient parseInputs handling (non-array field ->
empty list, non-object entries skipped) is pre-existing behavior preserved
verbatim by the arena refactor; tightening ABI-validation strictness is a
separate behavioral change out of scope for this PR.
@koko1123
koko1123 merged commit 93af6dd into main Jun 20, 2026
13 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants