feat: integrate zig state transition#9516
Conversation
There was a problem hiding this comment.
Code Review
This pull request integrates a native (Zig) state transition implementation into Lodestar, allowing state transitions and slot processing to be delegated to native bindings when the --chain.nativeStateView flag is enabled. Feedback on these changes highlights several key issues: blindly casting signedBlock to SignedBeaconBlock during serialization will fail for blinded blocks, and re-creating epochCtx from scratch when reloading cached states introduces a major performance bottleneck. Additionally, using a relative link: path for the native dependency in package.json makes the package non-portable, and there is a duplicated comment in the parity test file that should be cleaned up.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
| const blockBytes = config | ||
| .getForkTypes(signedBlock.message.slot) | ||
| .SignedBeaconBlock.serialize(signedBlock as SignedBeaconBlock); |
There was a problem hiding this comment.
The signedBlock parameter can be either a SignedBeaconBlock or a SignedBlindedBeaconBlock. Casting it blindly to SignedBeaconBlock and serializing it with the SignedBeaconBlock schema will fail or produce corrupt serialized bytes when processing a blinded block. You should check if the block is blinded and serialize it using the appropriate schema.
| const blockBytes = config | |
| .getForkTypes(signedBlock.message.slot) | |
| .SignedBeaconBlock.serialize(signedBlock as SignedBeaconBlock); | |
| const forkTypes = config.getForkTypes(signedBlock.message.slot); | |
| const isBlinded = "executionPayloadHeader" in signedBlock.message.body; | |
| const blockBytes = isBlinded | |
| ? forkTypes.SignedBlindedBeaconBlock.serialize(signedBlock as SignedBlindedBeaconBlock) | |
| : forkTypes.SignedBeaconBlock.serialize(signedBlock as SignedBeaconBlock); |
| function reloadCachedState(prev: CachedBeaconStateAllForks, postStateBytes: Uint8Array): CachedBeaconStateAllForks { | ||
| const {config, epochCtx} = prev; | ||
| const view = getStateTypeFromBytes(config, postStateBytes).deserializeToViewDU(postStateBytes); | ||
| return createCachedBeaconState( | ||
| view, | ||
| {config, pubkeyCache: epochCtx.pubkeyCache}, | ||
| {skipSyncPubkeys: true} | ||
| ) as CachedBeaconStateAllForks; | ||
| } |
There was a problem hiding this comment.
Re-creating the epochCtx (EpochContext) from scratch via createCachedBeaconState on every block or slot transition is a major performance bottleneck. This discards all cached shufflings, proposer indices, and other precomputed epoch-specific values, defeating the performance benefits of CachedBeaconState. Consider refactoring this to incrementally update or reuse the existing epochCtx instead of discarding it.
| // generateValidator returns the same hardcoded pubkey for every validator, which makes the | ||
| // native binding's global pubkey_to_index map collapse to a single entry while index_to_pubkey | ||
| // keeps N. That leaves syncPubkeys's consistency check tripping on the second createFromBytes | ||
| // call. Production states always have unique pubkeys, so just patch the test fixture. | ||
| // generateValidator returns the same hardcoded pubkey for every validator, which makes the | ||
| // native binding's global pubkey_to_index map collapse to a single entry while index_to_pubkey | ||
| // keeps N — tripping syncPubkeys's consistency check on the second createFromBytes call. | ||
| // Production states always have unique pubkeys; patch the fixture with unique valid BLS pubkeys. |
There was a problem hiding this comment.
The comment explaining why the test fixture is patched with unique BLS pubkeys is duplicated word-for-word. Please remove the redundant copy.
| // generateValidator returns the same hardcoded pubkey for every validator, which makes the | |
| // native binding's global pubkey_to_index map collapse to a single entry while index_to_pubkey | |
| // keeps N. That leaves syncPubkeys's consistency check tripping on the second createFromBytes | |
| // call. Production states always have unique pubkeys, so just patch the test fixture. | |
| // generateValidator returns the same hardcoded pubkey for every validator, which makes the | |
| // native binding's global pubkey_to_index map collapse to a single entry while index_to_pubkey | |
| // keeps N — tripping syncPubkeys's consistency check on the second createFromBytes call. | |
| // Production states always have unique pubkeys; patch the fixture with unique valid BLS pubkeys. | |
| // generateValidator returns the same hardcoded pubkey for every validator, which makes the | |
| // native binding's global pubkey_to_index map collapse to a single entry while index_to_pubkey | |
| // keeps N — tripping syncPubkeys's consistency check on the second createFromBytes call. | |
| // Production states always have unique pubkeys; patch the fixture with unique valid BLS pubkeys. |
| "@lodestar/params": "workspace:^", | ||
| "@lodestar/types": "workspace:^", | ||
| "@lodestar/utils": "workspace:^", | ||
| "@chainsafe/lodestar-z": "link:../../../lodestar-z-native-stf", |
There was a problem hiding this comment.
Using a relative link: path pointing outside the repository root (../../../lodestar-z-native-stf) makes the package non-portable. This will break in CI/CD environments, Docker builds, or for other developers unless they manually clone the native STF repository at that exact relative path. Consider defining this as a workspace dependency if it is part of the same monorepo, or fetch it via a git URL / npm registry if it is external.
78d8369 to
3641d99
Compare
9a695b0 to
d4be300
Compare
Performance Report✔️ no performance regression detected Full benchmark results
|
No description provided.