Skip to content

fix(bindings): size BLS thread pool by cgroup-aware CPU count - #386

Merged
spiral-ladder merged 14 commits into
mainfrom
bing/cgroup-cpu-count
Jun 11, 2026
Merged

fix(bindings): size BLS thread pool by cgroup-aware CPU count#386
spiral-ladder merged 14 commits into
mainfrom
bing/cgroup-cpu-count

Conversation

@GrapeBaBa

@GrapeBaBa GrapeBaBa commented Jun 5, 2026

Copy link
Copy Markdown
Contributor

Motivation

std.Thread.getCpuCount() only reads the CPU affinity mask, so under a cgroup CPU quota (docker --cpus=N / k8s limits.cpu) it reports the host core count rather than the quota. That over-sizes the BLS verification thread pool at NAPI init, which can cause thread oversubscription and CFS throttling in CPU-limited containers.

Changes

  • Add src/cpu_count.ziggetNumCpus(gpa, io) returns min(cgroup quota, affinity), locating the cpu controller via /proc/self/{cgroup,mountinfo} and reading cpu.max / cpu.cfs_quota_us (cgroup v1 + v2). Parsing logic ported from the num_cpus crate.
  • The quota is the minimum along the cgroup ancestor chain (leaf up to the mount point), matching Rust std::thread::available_parallelism: the kernel enforces every level but each level's file reports only its own limit, so leaf-only reads miss LXC/Proxmox cpulimit, systemd CPUQuota= on a parent slice, and sub-cgroups inside a limited container.
  • Hardened beyond num_cpus: cgroup paths containing : (containerd's …slice:cri-containerd:<id> cgroupfs naming) are kept intact, and .. paths (process outside its cgroupns root, cgroup_namespaces(7)) fall back cleanly instead of erroring.
  • Use it at NAPI thread-pool init (bindings/napi/root.zig) instead of std.Thread.getCpuCount().
  • Run test:cpu_count in CI (the build-test job enumerates module test steps explicitly).

Notes:

  • /proc pseudo-files report size 0, so they are streamed to EOF (readerStreaming + allocRemaining), not read by stat size (readFileAlloc reads empty).
  • Error policy: getNumCpus itself is fail-fast — a genuinely absent quota (non-Linux, no cpu controller, no cgroup mount, unresolvable path, unlimited) is null → affinity fallback, while a broken read of an existing resource (unreadable /proc or quota file, unopenable cgroup dir, malformed content) is an error, so a readable quota is never silently masked. The NAPI call site catches detection errors, logs a warning, and sizes by the affinity count — a sizing probe must not prevent the module from loading.
  • ceilDiv is overflow-safe; the allocator is passed in by the caller (the module stays allocator-agnostic / libc-free).

Testing

  • 22 unit tests covering the parse-and-resolve pipeline and the ancestor walk; test vectors adapted from seanmonstar/num_cpus (MIT).
  • Verified end-to-end in Linux containers (OrbStack): --cpus=2 → 2, --cpus=4 → 4, --cpus=1.5 → 2 (ceil), unlimited → host count. The old getCpuCount() returned the host count in all cases.
  • Ancestor walk verified end-to-end: with --cpus=2 and the process moved into an unconstrained child cgroup (0::/child, no cpu.max of its own), the walk finds the limit at the cgroupns root and returns 2 — leaf-only reads return the host count here.
  • Colon handling verified end-to-end: a process in 0::/x.slice:cri-containerd:y resolves its cgroup dir and returns the quota.

🤖 This PR was developed with AI assistance (Claude Code).

@GrapeBaBa
GrapeBaBa requested a review from a team as a code owner June 5, 2026 03:42
std.Thread.getCpuCount() only reads the CPU affinity mask, so under a
cgroup CPU quota (docker --cpus=N / k8s limits.cpu) it returns the host
core count rather than the quota. That over-sized the BLS thread pool at
NAPI init and caused CFS throttling in CPU-limited containers.

Add src/cpu_count.zig: getNumCpus(gpa, io) returns min(cgroup quota,
affinity), locating the cpu controller via /proc/self/{cgroup,mountinfo}
and reading cpu.max / cpu.cfs_quota_us (cgroup v1 + v2). Use it at NAPI
thread-pool init.

- /proc pseudo-files report size 0, so they are streamed to EOF
  (readerStreaming + allocRemaining), not read by stat size.
- ceilDiv is overflow-safe; detection failure logs a warning and falls
  back to the affinity count.
- Non-Linux falls back to the affinity count.

CPU-count logic ported from the num_cpus crate; test vectors adapted from
seanmonstar/num_cpus (MIT).
@GrapeBaBa
GrapeBaBa force-pushed the bing/cgroup-cpu-count branch from a4435e1 to 295ace4 Compare June 5, 2026 03:48
@GrapeBaBa

Copy link
Copy Markdown
Contributor Author

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Another round soon, please!

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

GrapeBaBa added 2 commits June 7, 2026 21:10
getNumCpus previously collapsed every failure into a silent fallback: any
cgroup detection error returned null (sizing by affinity), and getCpuCount
itself was `catch 1`. An unreadable or malformed quota was indistinguishable
from a genuinely unlimited one, so a CPU-limited container could silently
over-size the BLS thread pool.

Make the contract explicit:

- null (-> affinity) only when no quota can be located: non-Linux, no cpu
  controller, no cgroup mount, unresolvable path, quota file absent, or
  unlimited (cpu.max "max" / cfs_quota_us "-1").
- error otherwise: unreadable /proc or quota file, unopenable cgroup dir,
  malformed/zero content, or an unavailable affinity count -- now propagated
  to the NAPI init caller (via `try`) instead of masked.

Also fixes a latent parse hazard: the v1 "-1" and v2 "max" unlimited
sentinels are matched before parseUnsigned, so they can no longer be taken
for malformed input now that parse errors propagate.

Drop the redundant log.warn calls; a thrown error is its own signal.
A bare `8 << 20` next to the cgroup file's `1 << 20` reads as arbitrary and
invites tightening; mountinfo scales with the mount count and can reach
several MB under thousands of container mounts. Note it as a generous
ceiling so the limit is not narrowed without thought.
@GrapeBaBa

Copy link
Copy Markdown
Contributor Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 8286a791c2

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread bindings/napi/root.zig Outdated
if (options.thread_count == 0) {
cpu_count = @max((try std.Thread.getCpuCount()) - 1, 1);
std.debug.print("Note: no -Dthread-count set, will use runtime CPU count minus 1: {}\n", .{cpu_count});
cpu_count = @max(try @import("cpu_count").getNumCpus(allocator, napi_io.get()), 2) - 1;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Fall back when cgroup probing fails

In Linux runtimes where /proc/self/cgroup or /proc/self/mountinfo is unavailable/unreadable (for example restricted chroots or sandboxed launches), getNumCpus returns an error and this try propagates out of NAPI initialization, so loading the bindings fails even though the previous std.Thread.getCpuCount() path would still size the pool. Since the cgroup lookup is only an optimization over the affinity count, catch probe failures here (or inside getNumCpus) and fall back to std.Thread.getCpuCount() instead of aborting module init.

Useful? React with 👍 / 👎.

@GrapeBaBa GrapeBaBa Jun 8, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

we already fall back to std.Thread.getCpuCount() inside getNumCpus @codex

GrapeBaBa added 10 commits June 10, 2026 21:06
splitScalar's next() truncated the /proc/self/cgroup path field at the
next ':', so colon-named cgroup directories (containerd's
...slice:cri-containerd:<id> cgroupfs naming) resolved to a nonexistent
path — silently falling back to the affinity count, or erroring NAPI
init under host cgroupns. Take the remainder with rest() instead, the
same way runc's ParseCgroupFile splits the line.
The kernel reports a relative path like 0::/../foo when the process
sits outside its cgroupns root (cgroup_namespaces(7)). Joining '..'
onto the mount point escapes the cgroupfs, so openDir failed and the
error aborted NAPI init. The quota is simply not locatable through
this mount: return null and fall back to the affinity count, per the
module's documented unresolvable-path contract.
The kernel enforces the smallest CFS limit of the whole ancestor
chain, but each level's quota file reports only its own limit — a
constrained child still reads 'max'. Reading only the leaf therefore
missed quotas set above it: LXC/Proxmox cpulimit (limit at the
cgroupns root), systemd CPUQuota= on a parent slice, or any
process-manager sub-cgroup inside a limited container, sizing the BLS
pool to the host core count despite an enforceable quota.

Walk from the leaf up to the cgroup mount point and take the minimum,
matching Rust std::thread::available_parallelism. The leaf keeps the
fail-fast contract; unopenable ancestors end the walk best-effort.
Bounded by the component count of the leaf path.
The build-test job enumerates per-module test steps explicitly, so the
new test:cpu_count step never ran in CI — the only Linux environment
exercising the real /proc + cgroup path.
- Assert n != 0 in ceilDiv instead of silently returning 0: both
  callers reject a zero quota first, so n == 0 is violated-invariant
  space and should crash next to the existing d != 0 assert.
- Drop pub from parseCpuMaxV2/parseCpuV1: tests live in-file and the
  module's public surface is getNumCpus alone.
- Factor the triple-duplicated FileNotFound catch into readQuotaFile.
- Remove the boxed test-section banner; the fixture comment orients.
- Rename q/p to quota_s/period_s (matches parseCpuMaxV2), rc/bc/mp to
  full words per the styleguide's no-abbreviation rule.
- Wrap everything to the 100-column hard limit; build the /proc
  mountinfo fixtures from per-line ++ continuations with a shared
  prefix instead of 770-column single lines.
…med lines

- A hybrid fixture (cgroup2 mount ordered before the v1 cpu mount)
  exercises MountInfo.load's version filter in both directions; before
  this, no test fed the filter a parseable mount of the other version,
  so dropping the version check would have passed the whole suite.
- translate()'s two out-buffer capacity guards were never executed
  (the helper always passed a max_path_bytes buffer); cover both
  branches plus the exact-fit boundary.
- Cover MountInfo.parseLine structural failures: missing "-"
  separator (the while-else path), truncated line, line ending at
  "-".
- Use page_allocator for the two short-lived /proc reads instead of
  adding a third per-file DebugAllocator/c_allocator pair to the
  bindings; io.zig and blst.initThreadPool already use page_allocator
  for one-shot init-time allocation.
- Wrap the init print to the 100-column limit.
- Drop the duplicate blst.deinitThreadPool() in cleanup() (idempotent
  no-op left over from the zapi DSL rewrite).
getNumCpus reads /proc via allocRemaining, which grows an ArrayList
incrementally — with page_allocator every grow is an mmap round trip
at page granularity with no reuse. page_allocator fits the one-shot
large/long-lived init allocations it backs elsewhere in the bindings
(ThreadPool.init, the Threaded executor), not incremental small
growth; c_allocator is the bindings' general-purpose allocator and
malloc reuses buckets across the doubling steps. Leak coverage for
these defer-freed buffers lives in the module's unit tests via
std.testing.allocator.
…ount probe

Match the bindings-wide pattern (blst.zig, stateTransition.zig,
metrics.zig): DebugAllocator in Debug builds for double-free and
use-after-free detection on this path, c_allocator in release.
…errors

getNumCpus keeps its fail-fast contract (a broken read of an existing
resource errors, so a readable quota is never silently masked), but
propagating that error out of init() made require() of the whole
native addon throw — a thread-pool sizing probe should not take down
SSZ/state-transition/BLS in an exotic container setup (/proc-less
chroot, seccomp-blocked reads, malformed quota content). Catch at the
call site, warn, and size by the affinity count instead, matching the
pre-detection behavior.

@spiral-ladder spiral-ladder 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.

looks good, some comments

Comment thread bindings/napi/root.zig
pubkeys.state.deinit();
pool.state.deinit();
metrics.deinit();
blst.deinitThreadPool();

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.

is this intentional?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Yes — intentional. Both calls came in together with the #320 zapi rewrite (merge artifact), and the first position is the correct one to keep: ThreadPool.deinit drains the queue and joins all workers, which must happen before the pubkeys/pool caches (which in-flight jobs read) are freed, and before napi_io.deinit() (pool teardown uses the io). The removed second call was a guaranteed idempotent no-op.

Comment thread src/cpu_count.zig
Comment on lines +1 to +8
//! cgroup-aware logical CPU count for Linux.
//!
//! `std.Thread.getCpuCount()` only reads the CPU affinity mask and is blind to
//! the cgroup CFS quota (`cpu.max` / `cpu.cfs_quota_us`), so under
//! `docker --cpus=N` or k8s `limits.cpu` it reports the host core count. This
//! returns `min(quota, affinity)` instead, where the quota is the smallest one
//! along the cgroup ancestor chain — the kernel enforces every level, but each
//! level's quota file reports only its own limit.

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.

It would be nice to also include where we are consuming this:

Suggested change
//! cgroup-aware logical CPU count for Linux.
//!
//! `std.Thread.getCpuCount()` only reads the CPU affinity mask and is blind to
//! the cgroup CFS quota (`cpu.max` / `cpu.cfs_quota_us`), so under
//! `docker --cpus=N` or k8s `limits.cpu` it reports the host core count. This
//! returns `min(quota, affinity)` instead, where the quota is the smallest one
//! along the cgroup ancestor chain — the kernel enforces every level, but each
//! level's quota file reports only its own limit.
//! cgroup-aware logical CPU count for Linux.
//!
//! `std.Thread.getCpuCount()` only reads the CPU affinity mask and is blind to
//! the cgroup CFS quota (`cpu.max` / `cpu.cfs_quota_us`), so under
//! `docker --cpus=N` or k8s `limits.cpu` it reports the host core count. This
//! returns `min(quota, affinity)` instead, where the quota is the smallest one
//! along the cgroup ancestor chain — the kernel enforces every level, but each
//! level's quota file reports only its own limit.
//!
//! This module is important when consumed by docker deployed applications using
//! multithreaded operations within `lodestar-z`, eg.
//! our threadpool implementation used for bls operations in `bls/ThreadPool.zig`.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Added in 62b1494 (condensed wording).

Comment thread src/cpu_count.zig Outdated
Comment on lines +35 to +41
/// Logical CPU count from the affinity mask. Propagates the error rather than
/// silently degrading — a failed affinity count would size the pool to 1 worker.
fn logicalCpus() !usize {
const n = try std.Thread.getCpuCount();
assert(n >= 1);
return n;
}

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.

we can do this inline and assert n >= 1 in getNumCpus and also get rid of the test, this is just doing an unnecessary abstraction on std.Thread.getCpuCount() imo

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Done in 62b1494 — inlined with the n >= 1 assert moved into getNumCpus, and dropped the "never exceeds logical" test since the result <= logical postcondition assert now checks it on every call (including from the remaining smoke test).

GrapeBaBa added a commit to blockblaz/zeam that referenced this pull request Jun 10, 2026
- Keep cgroup paths containing ':' intact (containerd's
  ...slice:cri-containerd:<id> cgroupfs naming): take the path with
  rest() instead of truncating at the next ':'.
- Treat '..' cgroup paths (process outside its cgroupns root) as
  unresolvable -> affinity fallback instead of a hard error.
- Take the minimum quota along the cgroup ancestor chain (leaf up to
  the mount point), matching Rust available_parallelism: the kernel
  enforces every level but each level's file reports only its own
  limit, so leaf-only reads missed LXC/Proxmox cpulimit and systemd
  CPUQuota= on a parent slice.
- Polish per review: assert ceilDiv preconditions, dedup quota-file
  reads, drop unused pubs, 100-col wraps, hybrid-mount and
  buffer-limit test coverage (23 tests).

Synced verbatim from ChainSafe/lodestar-z#386 (commits 7c84fab7..f504039f);
verified there end-to-end in containers including the ancestor and
colon-path scenarios.
- Document the consumer in the module doc: NAPI thread-pool init
  sizing the BLS verification pool (spiral-ladder).
- Inline the logicalCpus wrapper — it was an unnecessary abstraction
  over std.Thread.getCpuCount(); the n >= 1 assert moves into
  getNumCpus. Drop the now-redundant 'never exceeds logical' test:
  the result <= logical postcondition assert checks it on every call,
  including from the remaining smoke test (spiral-ladder).
@GrapeBaBa
GrapeBaBa requested a review from spiral-ladder June 11, 2026 09:42

@spiral-ladder spiral-ladder 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.

In general looks good but this should really be a library on its own, but we can leave that for a followup cleanup, let's merge this so we can cut a release and deploy bls updates

@spiral-ladder
spiral-ladder merged commit 3ae9522 into main Jun 11, 2026
31 checks passed
markolazic01 pushed a commit to markolazic01/lodestar-z that referenced this pull request Jun 17, 2026
…afe#386)

## Motivation

`std.Thread.getCpuCount()` only reads the CPU affinity mask, so under a
cgroup CPU quota (`docker --cpus=N` / k8s `limits.cpu`) it reports the
host core count rather than the quota. That over-sizes the BLS
verification thread pool at NAPI init, which can cause thread
oversubscription and CFS throttling in CPU-limited containers.

## Changes

- Add `src/cpu_count.zig` — `getNumCpus(gpa, io)` returns `min(cgroup
quota, affinity)`, locating the cpu controller via
`/proc/self/{cgroup,mountinfo}` and reading `cpu.max` /
`cpu.cfs_quota_us` (cgroup v1 + v2). Parsing logic ported from the
`num_cpus` crate.
- The quota is the **minimum along the cgroup ancestor chain** (leaf up
to the mount point), matching Rust `std::thread::available_parallelism`:
the kernel enforces every level but each level's file reports only its
own limit, so leaf-only reads miss LXC/Proxmox `cpulimit`, systemd
`CPUQuota=` on a parent slice, and sub-cgroups inside a limited
container.
- Hardened beyond `num_cpus`: cgroup paths containing `:` (containerd's
`…slice:cri-containerd:<id>` cgroupfs naming) are kept intact, and `..`
paths (process outside its cgroupns root, `cgroup_namespaces(7)`) fall
back cleanly instead of erroring.
- Use it at NAPI thread-pool init (`bindings/napi/root.zig`) instead of
`std.Thread.getCpuCount()`.
- Run `test:cpu_count` in CI (the build-test job enumerates module test
steps explicitly).

Notes:
- `/proc` pseudo-files report size 0, so they are streamed to EOF
(`readerStreaming` + `allocRemaining`), not read by stat size
(`readFileAlloc` reads empty).
- Error policy: `getNumCpus` itself is fail-fast — a genuinely
**absent** quota (non-Linux, no cpu controller, no cgroup mount,
unresolvable path, unlimited) is `null` → affinity fallback, while a
**broken read** of an existing resource (unreadable `/proc` or quota
file, unopenable cgroup dir, malformed content) is an error, so a
readable quota is never silently masked. The NAPI call site catches
detection errors, logs a warning, and sizes by the affinity count — a
sizing probe must not prevent the module from loading.
- `ceilDiv` is overflow-safe; the allocator is passed in by the caller
(the module stays allocator-agnostic / libc-free).

## Testing

- 22 unit tests covering the parse-and-resolve pipeline and the ancestor
walk; test vectors adapted from `seanmonstar/num_cpus` (MIT).
- Verified end-to-end in Linux containers (OrbStack): `--cpus=2` → 2,
`--cpus=4` → 4, `--cpus=1.5` → 2 (ceil), unlimited → host count. The old
`getCpuCount()` returned the host count in all cases.
- Ancestor walk verified end-to-end: with `--cpus=2` and the process
moved into an unconstrained child cgroup (`0::/child`, no `cpu.max` of
its own), the walk finds the limit at the cgroupns root and returns 2 —
leaf-only reads return the host count here.
- Colon handling verified end-to-end: a process in
`0::/x.slice:cri-containerd:y` resolves its cgroup dir and returns the
quota.

---
🤖 This PR was developed with AI assistance (Claude Code).
@github-actions github-actions Bot mentioned this pull request Jul 30, 2026
@wemeetagain
wemeetagain deleted the bing/cgroup-cpu-count branch August 17, 2026 19:39
wemeetagain pushed a commit that referenced this pull request Aug 19, 2026
🤖 I have created a release *beep* *boop*
---


##
[1.0.0](v0.1.2...v1.0.0)
(2026-08-19)


### Features

* add `state.getBuildersLength()` binding
([#472](#472))
([be2b5ab](be2b5ab))
* **beacon-node:** add block state cache and checkpoint datastore
([#452](#452))
([2145faa](2145faa))
* bindings to `getExpectedWithdrawals` and native tweaks
([#350](#350))
([f47bc66](f47bc66))
* **bindings:** add pubkey cache syncPubkeys
([#537](#537))
([542779f](542779f))
* **bindings:** aggregate cached public keys by validator index
([#397](#397))
([2f90603](2f90603))
* **bindings:** align `BeaconStateView` with `IBeaconStateView`
([#347](#347))
([b8ec273](b8ec273))
* **bindings:** configurable pubkey cache growth step
([#481](#481))
([133ef24](133ef24))
* **bindings:** expose more APIs for STF
([#444](#444))
([7fe2609](7fe2609))
* **bls:** add small MSM for npoints &lt; 32
([#393](#393))
([b430638](b430638))
* **blst:** use external buffers for blst operations
([#358](#358))
([78e4678](78e4678))
* **ci:** conditionally publish bindings with tag
([#355](#355))
([ea77919](ea77919))
* **clock:** add clock module for slot/epoch timing
([#354](#354))
([385b077](385b077))
* **fork_choice:** add Prometheus metrics module
([#309](#309))
([cbc9d8d](cbc9d8d))
* **forkchoice:** implement the forkchoice module
([#246](#246))
([7c62a9b](7c62a9b))
* getSyncCommitteesWitness
([#367](#367))
([ef77649](ef77649))
* implement `loadState` API and binding
([#165](#165))
([f903519](f903519)),
closes [#159](#159)
* **metrics:** metrics bindings
([#455](#455))
([dd41999](dd41999))
* migrate blst,pubkeys to use zapi js dsl
([#331](#331))
([fcd26ca](fcd26ca))
* **pubkeys:** add getPubkeyBytes binding
([#555](#555))
([4ca51cf](4ca51cf))
* publish ARM64 musl bindings
([#482](#482))
([ac764c9](ac764c9))
* **shuffle:** add swap-or-not shuffling module and binding
([#559](#559))
([c2db37c](c2db37c))
* split nextValue fn
([#464](#464))
([b47faeb](b47faeb))
* support getLatestWeakSubjectivityCheckpointEpoch
([#366](#366))
([dcf3883](dcf3883))
* update fulu deposit processing
([#442](#442))
([064335c](064335c))


### Bug Fixes

* avoid set ([#484](#484))
([2e25d97](2e25d97))
* better generation of rand scalar
([#388](#388))
([74dce77](74dce77))
* **bindings:** accept `dontTransferCache` in processSlots for backward
compatibility
([#460](#460))
([65df5af](65df5af))
* **bindings:** check signature infinity by default
([#509](#509))
([2f5f281](2f5f281))
* **bindings:** clean up failed async BLS work
([#527](#527))
([1111b00](1111b00))
* **bindings:** free metrics writer on scrape failure
([#529](#529))
([4c8d94a](4c8d94a))
* **bindings:** harden random aggregate scalars
([#528](#528))
([8e89a63](8e89a63))
* **bindings:** log level for missing fields
([#435](#435))
([08faf41](08faf41))
* **bindings:** misordering of print for cpu count
([#381](#381))
([752a972](752a972))
* **bindings:** populate epoch participation for test fixtures
([#436](#436))
([8dbdd2e](8dbdd2e))
* **bindings:** refcount Pool to fix teardown panic
([#352](#352))
([23b2f68](23b2f68))
* **bindings:** roll back partial N-API initialization
([#491](#491))
([31c5ebb](31c5ebb))
* **bindings:** size BLS thread pool by cgroup-aware CPU count
([#386](#386))
([3ae9522](3ae9522))
* **bindings:** validate class types before unwrap
([#514](#514))
([2fd2ad5](2fd2ad5))
* **bindings:** validate secret key hex length
([#517](#517))
([136e415](136e415))
* **bls:** align PublicKey.uncompress validation with
Signature.uncompress
([#508](#508))
([5a8dbe9](5a8dbe9))
* **bls:** bound randomized aggregation inputs
([#548](#548))
([779d0bf](779d0bf)),
closes [#542](#542)
* **bls:** clean up partial thread pool initialization
([#490](#490))
([d55e598](d55e598))
* **bls:** convert pippenger scratch bytes to element counts
([#513](#513))
([a12ca92](a12ca92))
* **bls:** enforce 32-byte signing roots
([#545](#545))
([72fd308](72fd308))
* **bls:** make batch cardinality structural
([#547](#547))
([a06d8b2](a06d8b2))
* **bls:** preserve aggregate outputs on failure
([#521](#521))
([e0b6dd1](e0b6dd1))
* **bls:** reject empty keygen salts
([#524](#524))
([d2a9c86](d2a9c86))
* **bls:** reject unknown BLST error codes
([#525](#525))
([9e4a6ad](9e4a6ad))
* **bls:** size pairing buffers for 32-bit targets
([#531](#531))
([dc64a27](dc64a27))
* **blst:** default signature infinity check to true if not provided
([#387](#387))
([021cdcb](021cdcb))
* **build:** remove `zig-out` from `files`
([#360](#360))
([c52af09](c52af09))
* **ci:** fix caching spec test version
([#439](#439))
([96885a1](96885a1))
* dangling state pointer in loadOtherState
([#450](#450))
([81cbd5f](81cbd5f))
* **epoch_cache:** compute missing `next_proposers`
([#447](#447))
([0088a29](0088a29))
* **epoch_cache:** populate decision roots in afterProcessEpoch
([#453](#453))
([4b70a5e](4b70a5e))
* export asyncAggregateWithRandomness through napi binding
([#371](#371))
([1d04c2b](1d04c2b))
* harden memory safety across PMT, SSZ tree views, and state transition
([#377](#377))
([d6f5897](d6f5897))
* improve atomic ordering in ThreadPool and NAPI init
([#310](#310))
([4b0a1cc](4b0a1cc))
* interface compatbility with NativeBeaconStateView
([#445](#445))
([89e13d1](89e13d1))
* missing deinits in loadOtherState
([#459](#459))
([094d278](094d278))
* missing state commits
([#454](#454))
([a432b55](a432b55))
* no-op when syncPubkeys run on a pk cache with shrinking validator set
([#432](#432))
([ed05a99](ed05a99))
* param order in BeaconBlockBody
([#348](#348))
([d8b9c06](d8b9c06))
* pendingConsolidations bindings
([#449](#449))
([b9c497e](b9c497e))
* **pmt,ssz:** harden chunked-leaf and zero-copy tree-view memory safety
([#400](#400))
([de50c53](de50c53))
* populate cache balances during rewards/penalties processing
([#474](#474))
([5bf23dc](5bf23dc))
* re-expose sizes
([#369](#369))
([64b81f3](64b81f3))
* remove `slashValidator` gating on active status
([#448](#448))
([d319a0d](d319a0d))
* **ssz:** drop redundant default-init pass in fixed-list decode
([#468](#468))
([0c757be](0c757be))
* **ssz:** publish child cache entries after lookup
([#565](#565))
([21e78c9](21e78c9))
* state transition binding exports
([#456](#456))
([895982c](895982c))
* **state-transition:** group-check signature sets
([#515](#515))
([42774e9](42774e9)),
closes [#502](#502)
* **state-transition:** isolate epoch step cache mutations
([#535](#535))
([a83741a](a83741a))
* **state-transition:** repair Pool.init call broken by
[#346](https://github.com/ChainSafe/lodestar-z/issues/346)×[#367](https://github.com/ChainSafe/lodestar-z/issues/367)
merge skew ([#394](#394))
([b42944f](b42944f))
* various fixes around config
([#433](#433))
([c4f082c](c4f082c))


### Performance Improvements

* **bindings:** drop TS BLS comparison benches and report benchmarks on
PRs ([#552](#552))
([c909c6f](c909c6f))
* **bls:** add cache-aware signature verifier
([#562](#562))
([063857e](063857e))
* **bls:** bypass worker queue for small batches
([#553](#553))
([3f8a6df](3f8a6df))
* **epoch:** replace AutoHashMap with array lookup in reward/penalty
caches ([#286](#286))
([e4e181b](e4e181b)),
closes [#243](#243)
* **pmt:** chunked-leaf packing for basic lists and container_struct
([#346](#346))
([ba156c4](ba156c4))


### Code Refactoring

* allocate `AsyncAggRandData` in one obj
([#384](#384))
([459750f](459750f))
* **bindings/pubkeys:** simplify allocation strategy for aggregate
([#518](#518))
([b82750f](b82750f))
* **bindings:** rename blst Lifecycle to State
([#516](#516))
([0a9c179](0a9c179))
* **bindings:** use zapi js.io() instead of local io module
([#469](#469))
([2b34cc0](2b34cc0))
* **bindings:** wake only required number of workers
([#383](#383))
([1db57f1](1db57f1))
* **bls:** allocations around VMAS
([#395](#395))
([dfda58c](dfda58c))
* **bls:** clean up bls
([#398](#398))
([e0f3b9b](e0f3b9b))
* **bls:** remove need for tracking results for
verifyMultipleAggregateSignatures
([#389](#389))
([6fe5c3f](6fe5c3f))
* **bls:** remove single-threaded fallback
([#390](#390))
([e057713](e057713))
* **clock:** single public Clock; internalize SlotClock
([#463](#463))
([fbab1fa](fbab1fa))
* make XXXDecisionRoot fns return `js.String`
([#342](#342))
([aef4420](aef4420))
* move shuffle into swap_or_not_shuffle module
([#558](#558))
([e56efb2](e56efb2))
* **pubkeys:** centralize the process-wide cache
([#522](#522))
([dc9669d](dc9669d))


### Miscellaneous Chores

* avoid slow tests in AGENTS.md
([#546](#546))
([c60f2a9](c60f2a9))
* bump zapi to include musl build
([#485](#485))
([0b488cc](0b488cc))
* **ci:** pin github actions with sha hashes
([#507](#507))
([167b8f5](167b8f5))
* deprecate unused blst APIs
([#575](#575))
([7b547fa](7b547fa))
* **deps:** bump zapi v2.1.0 -&gt; v2.2.0
([#376](#376))
([0c240d8](0c240d8))
* **deps:** bump zbuild
([#403](#403))
([e2545de](e2545de))
* **deps:** compile blst with ReleaseFast
([#391](#391))
([753a896](753a896))
* **deps:** update zapi to 3.1.0
([#483](#483))
([f3e5827](f3e5827))
* **deps:** use zapi v2.1.0
([#372](#372))
([88f403a](88f403a))
* disable gemini auto code review
([#382](#382))
([63e42a4](63e42a4)),
closes [#380](#380)
* **docs:** add comments section in AGENTS.md
([#566](#566))
([0c09750](0c09750))
* move state clones out of benchmark run functions
([#324](#324))
([e4035de](e4035de))
* prepare 1.0.0 release
([#576](#576))
([20b657b](20b657b))
* release v0.1.2-rc.3
([#370](#370))
([e4fc551](e4fc551))
* **release:** 0.1.2-rc.2
([#365](#365))
([7046128](7046128))
* **release:** v0.1.2-rc.10
([#477](#477))
([9a4fad5](9a4fad5))
* **release:** v0.1.2-rc.4
([#373](#373))
([09468f1](09468f1))
* **release:** v0.1.2-rc.5
([#374](#374))
([f344efa](f344efa))
* **release:** v0.1.2-rc.6
([#375](#375))
([bdf5b67](bdf5b67))
* **release:** v0.1.2-rc.8
([#401](#401))
([06f91c2](06f91c2))
* **release:** v0.1.2-rc.9
([#404](#404))
([6024800](6024800))
* remove merge transition code
([#359](#359))
([09b175d](09b175d))
* remove stale epoch cache TODOs
([#534](#534))
([27a547a](27a547a))
* rename era shortHistoricalRoot to shortEraRoot
([#473](#473))
([c75a4d3](c75a4d3))
* **scripts:** build bindings with preset
([#434](#434))
([a1b5ef7](a1b5ef7))
* silence debug log when used in release builds
([#486](#486))
([c5377d7](c5377d7))
* support dev workflow
([#364](#364))
([fcb9a78](fcb9a78))
* update gloas types to align with the latest specs
([#431](#431))
([1f065b5](1f065b5))
* update spec test version to v1.7.0-alpha.11
([#451](#451))
([5875660](5875660))
* update spec-test-version: v1.6.0-beta.2 -&gt; v1.7.0-alpha.10
([#441](#441))
([f932b1c](f932b1c))
* update zapi to 4.0.0
([#571](#571))
([de8e3fd](de8e3fd))


### Documentation

* document security threat model
([#557](#557))
([e678b87](e678b87))
* more comprehensive AGENTS.md
([#520](#520))
([c74b386](c74b386))
* **pkix:** document load provenance requirement
([#556](#556))
([37e0aa2](37e0aa2))

---
This PR was generated with [Release
Please](https://github.com/googleapis/release-please). See
[documentation](https://github.com/googleapis/release-please#release-please).

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
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