Skip to content

refactor(blst): use lodestar-z pubkey cache - #9728

Open
spiral-ladder wants to merge 21 commits into
bing/blst-zfrom
bing/blst-pk-cache
Open

refactor(blst): use lodestar-z pubkey cache#9728
spiral-ladder wants to merge 21 commits into
bing/blst-zfrom
bing/blst-pk-cache

Conversation

@spiral-ladder

@spiral-ladder spiral-ladder commented Jul 30, 2026

Copy link
Copy Markdown
Member

Motivation

Replaces the pk cache implementation with calls to the native centralized cache.

Description

Reverts 'refactor: restore TS pubkey cache, use lodestar-z for BLS crypto only' (d614a9e) and adapts call sites to the refactored native pubkey cache API from ChainSafe/lodestar-z#522

Sample output

Generating deterministic valid BLS public keys...
{
  node: 'v24.16.0',
  platform: 'linux-x64',
  cacheSize: 16384,
  benchmarkTime: 1000,
  warmupTime: 250
}
┌─────────┬──────────────┬──────────┬─────────────┐
│ (index) │ cache        │ totalMs  │ nsPerLookup │
├─────────┼──────────────┼──────────┼─────────────┤
│ 0       │ 'TypeScript' │ '2.042'  │ '124.6'     │
│ 1       │ 'Zig native' │ '22.418' │ '1368.3'    │
└─────────┴──────────────┴──────────┴─────────────┘
    ✔ BLS indexed getOrThrow - TypeScript                                  5263158 ops/s    190.0000 ns/op        -     815160 runs   1.00 s
    ✔ BLS indexed getOrThrow - Zig native                                  4255319 ops/s    235.0000 ns/op        -     791887 runs   1.00 s
    ✔ BLS indexed getOrThrow + toBytes - TypeScript                       474608.4 ops/s    2.107000 us/op        -     273294 runs   1.00 s
    ✔ BLS indexed getOrThrow + toBytes - Zig native                       458715.6 ops/s    2.180000 us/op        -     269953 runs   1.00 s
    ✔ BLS indexed getPubkeyBytes - Zig native                             554631.2 ops/s    1.803000 us/op        -     314441 runs   1.00 s
    ✔ BLS aggregatePublicKeys 1 - TypeScript cache                        469483.6 ops/s    2.130000 us/op        -     258249 runs   1.00 s
    ✔ BLS nativeCache.aggregate 1 - Zig native cache                       4761905 ops/s    210.0000 ns/op        -     845962 runs   1.00 s
    ✔ BLS aggregatePublicKeys 32 - TypeScript cache                       37808.61 ops/s    26.44900 us/op        -      27792 runs   1.00 s
    ✔ BLS nativeCache.aggregate 32 - Zig native cache                     46242.77 ops/s    21.62500 us/op        -      33270 runs   1.00 s
    ✔ BLS aggregatePublicKeys 128 - TypeScript cache                      10596.14 ops/s    94.37400 us/op        -       7898 runs   1.00 s
    ✔ BLS nativeCache.aggregate 128 - Zig native cache                    13808.53 ops/s    72.41900 us/op        -      10279 runs   1.00 s
    ✔ BLS aggregatePublicKeys 512 - TypeScript cache                      2708.214 ops/s    369.2470 us/op        -       2028 runs   1.00 s
    ✔ BLS nativeCache.aggregate 512 - Zig native cache                    3621.063 ops/s    276.1620 us/op        -       2709 runs   1.00 s
    ✔ BLS mixed 32 signature sets - TypeScript                            2088.485 ops/s    478.8160 us/op        -       1563 runs   1.00 s
    ✔ BLS mixed 32 signature sets - Zig native                            2664.492 ops/s    375.3060 us/op        -       1994 runs   1.00 s


  15 passing
  0 failed
  0 pending

AI Assistance Disclosure

codex-gpt-sol was used to generate this code

Reverts 'refactor: restore TS pubkey cache, use lodestar-z for BLS
crypto only' (d614a9e) and adapts call sites to the refactored
native pubkey cache API from lodestar-z cayman/refactor-pubkey-cache
(PR #522): set() -> append(), load(filepath, maxCapacity).
@matthewkeil matthewkeil moved this to In Progress in Lodestar Team Coordination Jul 30, 2026
@matthewkeil matthewkeil added this to the mainnet-blst milestone Jul 30, 2026
@spiral-ladder

Copy link
Copy Markdown
Member Author

https://github.com/spiral-ladder/pubkey-cache-benchmarks

This is a standalone personal repo benchmarking the original pk cache vs the new one, instructions to reproduce, results and more details in the README. TLDR: pubkey aggregation is about ~15% faster across different set sizes, lookups are slower (since lookups have to cross napi boundary)

cc @twoeths you might be interested in this

@wemeetagain wemeetagain changed the title refactor(blst-z): use native lodestar-z pubkey cache everywhere refactor(blst-z): use lodestar-z pubkey cache Aug 6, 2026
@wemeetagain wemeetagain changed the title refactor(blst-z): use lodestar-z pubkey cache refactor(blst): use lodestar-z pubkey cache Aug 6, 2026
@twoeths

twoeths commented Aug 10, 2026

Copy link
Copy Markdown
Member

https://github.com/spiral-ladder/pubkey-cache-benchmarks

This is a standalone personal repo benchmarking the original pk cache vs the new one, instructions to reproduce, results and more details in the README. TLDR: pubkey aggregation is about ~15% faster across different set sizes, lookups are slower (since lookups have to cross napi boundary)

cc @twoeths you might be interested in this

thanks for the statistic @spiral-ladder

  • there is a performance regression on getOrThrown() api (70ns with native pubkey cache vs 32ns on nodeJS pubkey cache) which is mainly used for SingleAttestation gossip validation. As validators are supposed to always submit SingleAttestation per epoch, this flow is quite heavy
  • I think we can write a new getPubkeyBytes() and use it instead to avoid that. The bls worker needs the Uint8Array pubkey, not the BLSPubkey.
    • if we go with this direction, need a new benchmark to compare today's getOrThrow() + serialize() vs the new getPubkeyBytes()
  • on the benchmark:
    • please run on one of our common infrastructure node, we may have a different result there
    • given 1M active validators, we have 1M/32/64 ~ 500 members per committee, so need to also benchmark pubkey aggregation with this number (or 512)

spiral-ladder added a commit to spiral-ladder/pubkey-cache-benchmarks that referenced this pull request Aug 10, 2026
Address review feedback from
ChainSafe/lodestar#9728 (comment):

- Aggregate sizes are now 1, 32, 128, and 512. Size 512 covers a full
  committee at 1M active validators (1M / 32 slots / 64 committees).
- New indexed cases compare getOrThrow() + toBytes() with the native
  getPubkeyBytes() from lodestar-z d479f339, matching the BLS worker job
  path that consumes Uint8Array pubkeys. getPubkeyBytes had 26% lower
  latency than getOrThrow() + toBytes().
- Refresh README results from a new run on the same machine.
@spiral-ladder

Copy link
Copy Markdown
Member Author

https://github.com/spiral-ladder/pubkey-cache-benchmarks
This is a standalone personal repo benchmarking the original pk cache vs the new one, instructions to reproduce, results and more details in the README. TLDR: pubkey aggregation is about ~15% faster across different set sizes, lookups are slower (since lookups have to cross napi boundary)
cc @twoeths you might be interested in this

thanks for the statistic @spiral-ladder

  • there is a performance regression on getOrThrown() api (70ns with native pubkey cache vs 32ns on nodeJS pubkey cache) which is mainly used for SingleAttestation gossip validation. As validators are supposed to always submit SingleAttestation per epoch, this flow is quite heavy

  • I think we can write a new getPubkeyBytes() and use it instead to avoid that. The bls worker needs the Uint8Array pubkey, not the BLSPubkey.

    • if we go with this direction, need a new benchmark to compare today's getOrThrow() + serialize() vs the new getPubkeyBytes()
  • on the benchmark:

    • please run on one of our common infrastructure node, we may have a different result there
    • given 1M active validators, we have 1M/32/64 ~ 500 members per committee, so need to also benchmark pubkey aggregation with this number (or 512)

updated the benchmarks with new sample output.

This uses:

  • getPubkeyBytes, a new API (PR here)
  • @ChainSafe/benchmark for the benchmarking
  • benchmarks 512 key aggregation

spiral-ladder added a commit to ChainSafe/lodestar-z that referenced this pull request Aug 13, 2026
Expose the cached 48-byte compressed pubkey bytes by validator index
without materializing a PublicKey wrapper or serializing in JS. The BLS
worker job path in Lodestar needs Uint8Array pubkeys, so this avoids the
getOrThrow() + toBytes() round trip on the SingleAttestation gossip
validation hot path.

Refs
ChainSafe/lodestar#9728 (comment)

@twoeths twoeths 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 was supposed to post this asyncAggregateWithRandomness() improvement but @wemeetagain already implemented it here
bing/blst-pk-cache...cayman/blst-pk-cache

@spiral-ladder

Copy link
Copy Markdown
Member Author

right now with same message verification, we do:

  • get pubkey object from native given validator index (when validating gossip attestation)
  • asyncAggregateWithRandomness(){pubkey, Uint8Array}[N]

we can improve asyncAggregateWithRandomness by:

  • not to get pubkey from native
  • asyncAggregateWithRandomness(){validatorIndex, Uint8Array}[N]

(maybe as a follow up, not in this PR)

i kinda think this might be small enough that we can make this part of this PR? I would be comfortable letting this soak for a few more days before next release if we change this

@spiral-ladder

spiral-ladder commented Aug 13, 2026

Copy link
Copy Markdown
Member Author

I guess one caveat is this would be mostly replaced by the new branch that @wemeetagain is working on

edit: lol u edited too

@spiral-ladder
spiral-ladder marked this pull request as ready for review August 13, 2026 08:10
@spiral-ladder
spiral-ladder requested a review from a team as a code owner August 13, 2026 08:10
@codecov

codecov Bot commented Aug 13, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 52.58%. Comparing base (4f2f64b) to head (dab776d).

Additional details and impacted files
@@               Coverage Diff               @@
##           bing/blst-z    #9728      +/-   ##
===============================================
- Coverage        52.58%   52.58%   -0.01%     
===============================================
  Files              848      848              
  Lines            59977    59975       -2     
  Branches          4418     4418              
===============================================
- Hits             31540    31538       -2     
  Misses           28378    28378              
  Partials            59       59              
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@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: dab776d058

ℹ️ 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".


addPubkey(index: ValidatorIndex, pubkey: Uint8Array): void {
this.pubkeyCache.set(index, pubkey);
this.pubkeyCache.append(index, pubkey);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Preserve pubkeys for competing states

When two valid competing blocks extend the same pre-state with different deposits, each can assign a different pubkey to the same new validator index. The process-wide native cache is append-only and cannot remap an existing index, so after the first branch calls append, processing the second branch either fails or continues using the first branch's pubkey, causing valid signatures on the second branch to be rejected. The cache must support branch-specific mappings or avoid publishing unfinalized registry additions globally.

Useful? React with 👍 / 👎.

Comment on lines +85 to +87
const headroomEpochs = (90 * 24 * 60 * 60) / (config.SECONDS_PER_SLOT * SLOTS_PER_EPOCH);
const pubkeyCacheHeadroom = MAX_PENDING_DEPOSITS_PER_EPOCH * Math.ceil(headroomEpochs);
pubkeyCache.ensureCapacity(anchorState.validators.length + pubkeyCacheHeadroom);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Account for pre-Electra registry growth

For a node started before Electra, new validators can be added by up to MAX_DEPOSITS per slot, but this reservation assumes the post-Electra limit of MAX_PENDING_DEPOSITS_PER_EPOCH per epoch. At the maximum pre-Electra rate, the advertised 90-day headroom is exhausted in roughly 90 / SLOTS_PER_EPOCH days, after which append reallocates while BLS worker threads may be reading, despite the preceding comment identifying that operation as unsafe. Size the reservation according to the active fork or make growth synchronized.

AGENTS.md reference: AGENTS.md:L187-L197

Useful? React with 👍 / 👎.

asyncAggregateWithRandomness now resolves public keys from the native
pubkey cache by validator index, so same-message signature sets carry
{index, signature} and the per-attestation getOrThrow + PublicKey object
crossing on gossip validation disappears.
@spiral-ladder

Copy link
Copy Markdown
Member Author

my claude force pushed while i was working on something oops

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: In Progress

Development

Successfully merging this pull request may close these issues.

3 participants