feat: update stateTransition interface to take raw block bytes instead#9632
feat: update stateTransition interface to take raw block bytes instead#9632spiral-ladder wants to merge 3 commits into
Conversation
This is a change to align with Zig STF (which takes raw block bytes instead of a block object), see #9516, specifically [this comment](#9516 (comment)) Two key points, there a few paths affected, with two key paths (namely block import and block production) being latency sensitive. **block import**: we can get the serialized block from cache easily since it is [cached upon receiving from gossip](https://github.com/ChainSafe/lodestar/blob/47eedd48a24ab5833c3a0a58676be812d07ee08d/packages/beacon-node/src/network/processor/gossipHandlers.ts#L704). **block production**: it seems like we have to serialize/deserialize in an ugly manner unfortunately since we're assembling the block ourselves, so we have to pay the serialization cost. **regen**: we pass in the `serializedCache` in `RegenModules` and use it that way, though it might be a cache miss for older blocks. **Historical state replay**: seems like we have to pay the ser/deser cost here unless we store block bytes. Spec tests are also changed to take bytes.
There was a problem hiding this comment.
Code Review
This pull request optimizes the state transition process by changing the stateTransition method signature to accept serialized block bytes (Uint8Array) and an isBlinded flag instead of structured block objects. This change allows the node to leverage cached serialized blocks from gossip, reducing deserialization overhead in critical paths. Feedback on the changes highlights a security concern in readSignedBlockSlot where the length check is too permissive and could lead to out-of-bounds reads on malformed inputs. Additionally, improvements are suggested in the test suites to avoid accessing the internal cachedState property of BeaconStateView by reusing local configuration variables.
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.
| function readSignedBlockSlot(signedBlockBytes: Uint8Array): Slot { | ||
| if (signedBlockBytes.byteLength < 12) { | ||
| throw Error("Invalid signed block bytes: too short to read message offset and slot"); | ||
| } | ||
|
|
||
| const view = new DataView(signedBlockBytes.buffer, signedBlockBytes.byteOffset, signedBlockBytes.byteLength); | ||
| const messageOffset = view.getUint32(0, true); | ||
| if (messageOffset + 8 > signedBlockBytes.byteLength) { | ||
| throw Error("Invalid signed block bytes: message offset out of range"); | ||
| } | ||
|
|
||
| return Number(view.getBigUint64(messageOffset, true)); | ||
| } |
There was a problem hiding this comment.
The minimum valid length of a serialized SignedBeaconBlock or SignedBlindedBeaconBlock is 108 bytes (100 bytes for the fixed part containing the 4-byte offset of message and the 96-byte signature, plus at least 8 bytes for the slot field of the message container).
Checking signedBlockBytes.byteLength < 12 is too permissive and can lead to out-of-bounds reads or reading garbage data if the input is malformed.
Additionally, the messageOffset (which is the offset of the first variable-sized field message) must be exactly 100. If it is not 100, the bytes are invalid and we should throw an error immediately to prevent reading arbitrary offsets.
| function readSignedBlockSlot(signedBlockBytes: Uint8Array): Slot { | |
| if (signedBlockBytes.byteLength < 12) { | |
| throw Error("Invalid signed block bytes: too short to read message offset and slot"); | |
| } | |
| const view = new DataView(signedBlockBytes.buffer, signedBlockBytes.byteOffset, signedBlockBytes.byteLength); | |
| const messageOffset = view.getUint32(0, true); | |
| if (messageOffset + 8 > signedBlockBytes.byteLength) { | |
| throw Error("Invalid signed block bytes: message offset out of range"); | |
| } | |
| return Number(view.getBigUint64(messageOffset, true)); | |
| } | |
| function readSignedBlockSlot(signedBlockBytes: Uint8Array): Slot { | |
| if (signedBlockBytes.byteLength < 108) { | |
| throw Error("Invalid signed block bytes: too short to contain a valid signed block"); | |
| } | |
| const view = new DataView(signedBlockBytes.buffer, signedBlockBytes.byteOffset, signedBlockBytes.byteLength); | |
| const messageOffset = view.getUint32(0, true); | |
| if (messageOffset !== 100) { | |
| throw Error(`Invalid signed block bytes: message offset must be 100, got ${messageOffset}`); | |
| } | |
| return Number(view.getBigUint64(messageOffset, true)); | |
| } |
| let state = new BeaconStateView(createCachedBeaconStateTest(testcase.pre, getConfig(fork))); | ||
| const verify = shouldVerify(testcase); | ||
| for (let i = 0; i < testcase.meta.blocks_count; i++) { | ||
| const signedBlock = testcase[`blocks_${i}`] as bellatrix.SignedBeaconBlock; | ||
|
|
||
| state = stateTransition(state, signedBlock, { | ||
| // Should assume payload valid and blob data available for this test | ||
| executionPayloadStatus: ExecutionPayloadStatus.valid, | ||
| dataAvailabilityStatus: DataAvailabilityStatus.Available, | ||
| verifyStateRoot: false, | ||
| verifyProposer: verify, | ||
| verifySignatures: verify, | ||
| assertCorrectProgressiveBalances, | ||
| }); | ||
| state = state.stateTransition( | ||
| state.cachedState.config.getForkTypes(signedBlock.message.slot).SignedBeaconBlock.serialize(signedBlock), |
There was a problem hiding this comment.
Avoid accessing the internal cachedState property of BeaconStateView to retrieve the config. Since getConfig(fork) is already called, we can store the config in a local variable and use it directly.
| let state = new BeaconStateView(createCachedBeaconStateTest(testcase.pre, getConfig(fork))); | |
| const verify = shouldVerify(testcase); | |
| for (let i = 0; i < testcase.meta.blocks_count; i++) { | |
| const signedBlock = testcase[`blocks_${i}`] as bellatrix.SignedBeaconBlock; | |
| state = stateTransition(state, signedBlock, { | |
| // Should assume payload valid and blob data available for this test | |
| executionPayloadStatus: ExecutionPayloadStatus.valid, | |
| dataAvailabilityStatus: DataAvailabilityStatus.Available, | |
| verifyStateRoot: false, | |
| verifyProposer: verify, | |
| verifySignatures: verify, | |
| assertCorrectProgressiveBalances, | |
| }); | |
| state = state.stateTransition( | |
| state.cachedState.config.getForkTypes(signedBlock.message.slot).SignedBeaconBlock.serialize(signedBlock), | |
| const config = getConfig(fork); | |
| let state = new BeaconStateView(createCachedBeaconStateTest(testcase.pre, config)); | |
| const verify = shouldVerify(testcase); | |
| for (let i = 0; i < testcase.meta.blocks_count; i++) { | |
| const signedBlock = testcase[`blocks_${i}`] as bellatrix.SignedBeaconBlock; | |
| state = state.stateTransition( | |
| config.getForkTypes(signedBlock.message.slot).SignedBeaconBlock.serialize(signedBlock), |
| let wrappedState = new BeaconStateView(createCachedBeaconStateTest(stateTB, getConfig(fork))); | ||
| const verify = shouldVerify(testcase); | ||
| for (let i = 0; i < testcase.meta.blocks_count; i++) { | ||
| const signedBlock = testcase[`blocks_${i}`] as deneb.SignedBeaconBlock; | ||
| wrappedState = stateTransition(wrappedState, signedBlock, { | ||
| // Assume valid and available for this test | ||
| executionPayloadStatus: ExecutionPayloadStatus.valid, | ||
| dataAvailabilityStatus: DataAvailabilityStatus.Available, | ||
| verifyStateRoot: verify, | ||
| verifyProposer: verify, | ||
| verifySignatures: verify, | ||
| assertCorrectProgressiveBalances, | ||
| }); | ||
|
|
||
| wrappedState = wrappedState.stateTransition( | ||
| wrappedState.cachedState.config | ||
| .getForkTypes(signedBlock.message.slot) | ||
| .SignedBeaconBlock.serialize(signedBlock), |
There was a problem hiding this comment.
Avoid accessing the internal cachedState property of BeaconStateView to retrieve the config. Since getConfig(fork) is already called, we can store the config in a local variable and use it directly.
| let wrappedState = new BeaconStateView(createCachedBeaconStateTest(stateTB, getConfig(fork))); | |
| const verify = shouldVerify(testcase); | |
| for (let i = 0; i < testcase.meta.blocks_count; i++) { | |
| const signedBlock = testcase[`blocks_${i}`] as deneb.SignedBeaconBlock; | |
| wrappedState = stateTransition(wrappedState, signedBlock, { | |
| // Assume valid and available for this test | |
| executionPayloadStatus: ExecutionPayloadStatus.valid, | |
| dataAvailabilityStatus: DataAvailabilityStatus.Available, | |
| verifyStateRoot: verify, | |
| verifyProposer: verify, | |
| verifySignatures: verify, | |
| assertCorrectProgressiveBalances, | |
| }); | |
| wrappedState = wrappedState.stateTransition( | |
| wrappedState.cachedState.config | |
| .getForkTypes(signedBlock.message.slot) | |
| .SignedBeaconBlock.serialize(signedBlock), | |
| const config = getConfig(fork); | |
| let wrappedState = new BeaconStateView(createCachedBeaconStateTest(stateTB, config)); | |
| const verify = shouldVerify(testcase); | |
| for (let i = 0; i < testcase.meta.blocks_count; i++) { | |
| const signedBlock = testcase[`blocks_${i}`] as deneb.SignedBeaconBlock; | |
| wrappedState = wrappedState.stateTransition( | |
| config | |
| .getForkTypes(signedBlock.message.slot) | |
| .SignedBeaconBlock.serialize(signedBlock), |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 4917710527
ℹ️ About Codex in GitHub
Codex has been enabled to automatically 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 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
| import {describe, expect, it, vi} from "vitest"; | ||
| import {BitArray} from "@chainsafe/ssz"; | ||
| import {IBeaconStateViewNative} from "../../../src/stateView/interface.js"; | ||
| import {DataAvailabilityStatus, ExecutionPayloadStatus} from "../../../src/index.ts"; |
There was a problem hiding this comment.
Use a
.js extension for the relative test import
The root AGENTS.md and Biome configuration require .js extensions for relative TypeScript imports, but this new runtime import uses index.ts. In this test file, pnpm lint will reject the import; change it to ../../../src/index.js like the neighboring relative imports.
Useful? React with 👍 / 👎.
|
| Benchmark suite | Current: 1b4be64 | Previous: 47eedd4 | Ratio |
|---|---|---|---|
| phase0 processBlock - 250000 vs - 7PWei normalcase | 8.3458 ms/op | 1.3651 ms/op | 6.11 |
| phase0 processBlock - 250000 vs - 7PWei worstcase | 51.173 ms/op | 16.732 ms/op | 3.06 |
Full benchmark results
| Benchmark suite | Current: 1b4be64 | Previous: 47eedd4 | Ratio |
|---|---|---|---|
| getPubkeys - index2pubkey - req 1000 vs - 250000 vc | 1.4245 ms/op | 919.05 us/op | 1.55 |
| getPubkeys - validatorsArr - req 1000 vs - 250000 vc | 41.694 us/op | 39.970 us/op | 1.04 |
| BLS verify - blst | 708.76 us/op | 730.37 us/op | 0.97 |
| BLS verifyMultipleSignatures 3 - blst | 1.3856 ms/op | 1.3096 ms/op | 1.06 |
| BLS verifyMultipleSignatures 8 - blst | 2.1861 ms/op | 2.0417 ms/op | 1.07 |
| BLS verifyMultipleSignatures 32 - blst | 6.8669 ms/op | 6.5327 ms/op | 1.05 |
| BLS verifyMultipleSignatures 64 - blst | 13.636 ms/op | 12.639 ms/op | 1.08 |
| BLS verifyMultipleSignatures 128 - blst | 26.044 ms/op | 24.743 ms/op | 1.05 |
| BLS deserializing 10000 signatures | 637.77 ms/op | 629.56 ms/op | 1.01 |
| BLS deserializing 100000 signatures | 6.3971 s/op | 6.2435 s/op | 1.02 |
| BLS verifyMultipleSignatures - same message - 3 - blst | 707.50 us/op | 792.03 us/op | 0.89 |
| BLS verifyMultipleSignatures - same message - 8 - blst | 901.04 us/op | 923.43 us/op | 0.98 |
| BLS verifyMultipleSignatures - same message - 32 - blst | 1.4721 ms/op | 1.5157 ms/op | 0.97 |
| BLS verifyMultipleSignatures - same message - 64 - blst | 2.4035 ms/op | 2.3227 ms/op | 1.03 |
| BLS verifyMultipleSignatures - same message - 128 - blst | 4.0685 ms/op | 4.0326 ms/op | 1.01 |
| BLS aggregatePubkeys 32 - blst | 17.861 us/op | 17.536 us/op | 1.02 |
| BLS aggregatePubkeys 128 - blst | 63.717 us/op | 63.115 us/op | 1.01 |
| getSlashingsAndExits - default max | 48.828 us/op | 56.645 us/op | 0.86 |
| getSlashingsAndExits - 2k | 383.35 us/op | 468.99 us/op | 0.82 |
| proposeBlockBody type=full, size=empty | 694.46 us/op | 708.80 us/op | 0.98 |
| isKnown best case - 1 super set check | 178.00 ns/op | 162.00 ns/op | 1.10 |
| isKnown normal case - 2 super set checks | 164.00 ns/op | 160.00 ns/op | 1.02 |
| isKnown worse case - 16 super set checks | 157.00 ns/op | 160.00 ns/op | 0.98 |
| validate api signedAggregateAndProof - struct | 1.5314 ms/op | 1.4584 ms/op | 1.05 |
| validate gossip signedAggregateAndProof - struct | 1.5048 ms/op | 1.4621 ms/op | 1.03 |
| batch validate gossip attestation - vc 640000 - chunk 32 | 106.04 us/op | 108.46 us/op | 0.98 |
| batch validate gossip attestation - vc 640000 - chunk 64 | 96.308 us/op | 94.890 us/op | 1.01 |
| batch validate gossip attestation - vc 640000 - chunk 128 | 89.660 us/op | 91.053 us/op | 0.98 |
| batch validate gossip attestation - vc 640000 - chunk 256 | 87.344 us/op | 88.838 us/op | 0.98 |
| bytes32 toHexString | 294.00 ns/op | 276.00 ns/op | 1.07 |
| bytes32 Buffer.toString(hex) | 169.00 ns/op | 165.00 ns/op | 1.02 |
| bytes32 Buffer.toString(hex) from Uint8Array | 244.00 ns/op | 241.00 ns/op | 1.01 |
| bytes32 Buffer.toString(hex) + 0x | 169.00 ns/op | 170.00 ns/op | 0.99 |
| Return object 10000 times | 0.21560 ns/op | 0.21090 ns/op | 1.02 |
| Throw Error 10000 times | 3.4041 us/op | 3.2988 us/op | 1.03 |
| toHex | 105.85 ns/op | 93.345 ns/op | 1.13 |
| Buffer.from | 87.763 ns/op | 85.589 ns/op | 1.03 |
| shared Buffer | 60.233 ns/op | 62.352 ns/op | 0.97 |
| fastMsgIdFn sha256 / 200 bytes | 1.4860 us/op | 1.4730 us/op | 1.01 |
| fastMsgIdFn h32 xxhash / 200 bytes | 156.00 ns/op | 152.00 ns/op | 1.03 |
| fastMsgIdFn h64 xxhash / 200 bytes | 205.00 ns/op | 205.00 ns/op | 1.00 |
| fastMsgIdFn sha256 / 1000 bytes | 4.7770 us/op | 4.7610 us/op | 1.00 |
| fastMsgIdFn h32 xxhash / 1000 bytes | 245.00 ns/op | 239.00 ns/op | 1.03 |
| fastMsgIdFn h64 xxhash / 1000 bytes | 251.00 ns/op | 256.00 ns/op | 0.98 |
| fastMsgIdFn sha256 / 10000 bytes | 42.245 us/op | 42.500 us/op | 0.99 |
| fastMsgIdFn h32 xxhash / 10000 bytes | 1.2660 us/op | 1.2600 us/op | 1.00 |
| fastMsgIdFn h64 xxhash / 10000 bytes | 815.00 ns/op | 808.00 ns/op | 1.01 |
| send data - 1000 256B messages | 4.3304 ms/op | 5.6576 ms/op | 0.77 |
| send data - 1000 512B messages | 5.1218 ms/op | 5.3618 ms/op | 0.96 |
| send data - 1000 1024B messages | 5.8480 ms/op | 5.6477 ms/op | 1.04 |
| send data - 1000 1200B messages | 6.3398 ms/op | 6.2470 ms/op | 1.01 |
| send data - 1000 2048B messages | 11.765 ms/op | 18.886 ms/op | 0.62 |
| send data - 1000 4096B messages | 62.846 ms/op | 43.588 ms/op | 1.44 |
| send data - 1000 16384B messages | 222.19 ms/op | 79.094 ms/op | 2.81 |
| send data - 1000 65536B messages | 1.4826 s/op | 761.22 ms/op | 1.95 |
| enrSubnets - fastDeserialize 64 bits | 740.00 ns/op | 764.00 ns/op | 0.97 |
| enrSubnets - ssz BitVector 64 bits | 267.00 ns/op | 258.00 ns/op | 1.03 |
| enrSubnets - fastDeserialize 4 bits | 100.00 ns/op | 101.00 ns/op | 0.99 |
| enrSubnets - ssz BitVector 4 bits | 264.00 ns/op | 260.00 ns/op | 1.02 |
| prioritizePeers score -10:0 att 32-0.1 sync 2-0 | 209.70 us/op | 198.66 us/op | 1.06 |
| prioritizePeers score 0:0 att 32-0.25 sync 2-0.25 | 230.89 us/op | 226.44 us/op | 1.02 |
| prioritizePeers score 0:0 att 32-0.5 sync 2-0.5 | 340.90 us/op | 329.50 us/op | 1.03 |
| prioritizePeers score 0:0 att 64-0.75 sync 4-0.75 | 596.80 us/op | 583.00 us/op | 1.02 |
| prioritizePeers score 0:0 att 64-1 sync 4-1 | 710.49 us/op | 686.61 us/op | 1.03 |
| array of 16000 items push then shift | 1.2979 us/op | 1.2329 us/op | 1.05 |
| LinkedList of 16000 items push then shift | 7.3880 ns/op | 7.3760 ns/op | 1.00 |
| array of 16000 items push then pop | 70.683 ns/op | 70.761 ns/op | 1.00 |
| LinkedList of 16000 items push then pop | 6.0320 ns/op | 5.9140 ns/op | 1.02 |
| array of 24000 items push then shift | 1.9200 us/op | 1.8439 us/op | 1.04 |
| LinkedList of 24000 items push then shift | 7.0850 ns/op | 7.3590 ns/op | 0.96 |
| array of 24000 items push then pop | 99.446 ns/op | 101.50 ns/op | 0.98 |
| LinkedList of 24000 items push then pop | 6.1280 ns/op | 6.0950 ns/op | 1.01 |
| intersect bitArray bitLen 8 | 4.8100 ns/op | 4.7240 ns/op | 1.02 |
| intersect array and set length 8 | 29.621 ns/op | 29.516 ns/op | 1.00 |
| intersect bitArray bitLen 128 | 24.460 ns/op | 24.189 ns/op | 1.01 |
| intersect array and set length 128 | 502.06 ns/op | 496.10 ns/op | 1.01 |
| bitArray.getTrueBitIndexes() bitLen 128 | 1.0250 us/op | 976.00 ns/op | 1.05 |
| bitArray.getTrueBitIndexes() bitLen 248 | 1.7630 us/op | 1.7460 us/op | 1.01 |
| bitArray.getTrueBitIndexes() bitLen 512 | 3.5670 us/op | 3.6530 us/op | 0.98 |
| Full columns - reconstruct all 6 blobs | 119.25 us/op | 244.41 us/op | 0.49 |
| Full columns - reconstruct half of the blobs out of 6 | 67.226 us/op | 70.121 us/op | 0.96 |
| Full columns - reconstruct single blob out of 6 | 36.225 us/op | 30.198 us/op | 1.20 |
| Half columns - reconstruct all 6 blobs | 395.11 ms/op | 389.50 ms/op | 1.01 |
| Half columns - reconstruct half of the blobs out of 6 | 196.47 ms/op | 194.97 ms/op | 1.01 |
| Half columns - reconstruct single blob out of 6 | 71.232 ms/op | 69.986 ms/op | 1.02 |
| Set add up to 64 items then delete first | 1.6588 us/op | 1.6032 us/op | 1.03 |
| OrderedSet add up to 64 items then delete first | 2.4061 us/op | 2.4863 us/op | 0.97 |
| Set add up to 64 items then delete last | 1.8004 us/op | 2.2154 us/op | 0.81 |
| OrderedSet add up to 64 items then delete last | 2.8112 us/op | 2.7920 us/op | 1.01 |
| Set add up to 64 items then delete middle | 1.8798 us/op | 1.8502 us/op | 1.02 |
| OrderedSet add up to 64 items then delete middle | 4.3031 us/op | 4.2523 us/op | 1.01 |
| Set add up to 128 items then delete first | 3.7645 us/op | 3.6825 us/op | 1.02 |
| OrderedSet add up to 128 items then delete first | 5.7706 us/op | 5.7284 us/op | 1.01 |
| Set add up to 128 items then delete last | 3.6294 us/op | 3.5258 us/op | 1.03 |
| OrderedSet add up to 128 items then delete last | 5.4002 us/op | 5.6598 us/op | 0.95 |
| Set add up to 128 items then delete middle | 3.8028 us/op | 3.5514 us/op | 1.07 |
| OrderedSet add up to 128 items then delete middle | 11.319 us/op | 11.223 us/op | 1.01 |
| Set add up to 256 items then delete first | 7.4435 us/op | 7.2776 us/op | 1.02 |
| OrderedSet add up to 256 items then delete first | 11.888 us/op | 11.989 us/op | 0.99 |
| Set add up to 256 items then delete last | 7.5034 us/op | 7.3488 us/op | 1.02 |
| OrderedSet add up to 256 items then delete last | 11.316 us/op | 11.253 us/op | 1.01 |
| Set add up to 256 items then delete middle | 7.2020 us/op | 7.0186 us/op | 1.03 |
| OrderedSet add up to 256 items then delete middle | 35.245 us/op | 35.178 us/op | 1.00 |
| runFastConfirmationRules vc:100000 bc:96 eq:0 | 5.5685 ms/op | 4.4229 ms/op | 1.26 |
| runFastConfirmationRules vc:600000 bc:96 eq:0 | 36.220 ms/op | 34.901 ms/op | 1.04 |
| runFastConfirmationRules vc:1000000 bc:96 eq:0 | 60.305 ms/op | 58.498 ms/op | 1.03 |
| runFastConfirmationRules vc:600000 bc:320 eq:0 | 36.290 ms/op | 35.072 ms/op | 1.03 |
| runFastConfirmationRules vc:100000 bc:96 eq:1000 | 1.1487 s/op | 1.1384 s/op | 1.01 |
| pass gossip attestations to forkchoice per slot | 2.5715 ms/op | 2.5725 ms/op | 1.00 |
| forkChoice updateHead vc 100000 bc 64 eq 0 | 414.23 us/op | 400.48 us/op | 1.03 |
| forkChoice updateHead vc 600000 bc 64 eq 0 | 2.4540 ms/op | 2.3988 ms/op | 1.02 |
| forkChoice updateHead vc 1000000 bc 64 eq 0 | 4.1131 ms/op | 4.0181 ms/op | 1.02 |
| forkChoice updateHead vc 600000 bc 320 eq 0 | 2.4861 ms/op | 2.4363 ms/op | 1.02 |
| forkChoice updateHead vc 600000 bc 1200 eq 0 | 2.5439 ms/op | 2.5094 ms/op | 1.01 |
| forkChoice updateHead vc 600000 bc 7200 eq 0 | 2.8503 ms/op | 3.3134 ms/op | 0.86 |
| forkChoice updateHead vc 600000 bc 64 eq 1000 | 2.4612 ms/op | 2.4212 ms/op | 1.02 |
| forkChoice updateHead vc 600000 bc 64 eq 10000 | 2.5912 ms/op | 2.4888 ms/op | 1.04 |
| forkChoice updateHead vc 600000 bc 64 eq 300000 | 6.6211 ms/op | 6.4802 ms/op | 1.02 |
| computeDeltas 1400000 validators 0% inactive | 12.245 ms/op | 11.910 ms/op | 1.03 |
| computeDeltas 1400000 validators 10% inactive | 11.609 ms/op | 11.299 ms/op | 1.03 |
| computeDeltas 1400000 validators 20% inactive | 10.900 ms/op | 10.850 ms/op | 1.00 |
| computeDeltas 1400000 validators 50% inactive | 8.9540 ms/op | 8.7998 ms/op | 1.02 |
| computeDeltas 2100000 validators 0% inactive | 18.293 ms/op | 18.031 ms/op | 1.01 |
| computeDeltas 2100000 validators 10% inactive | 17.403 ms/op | 16.996 ms/op | 1.02 |
| computeDeltas 2100000 validators 20% inactive | 16.399 ms/op | 15.982 ms/op | 1.03 |
| computeDeltas 2100000 validators 50% inactive | 10.921 ms/op | 10.535 ms/op | 1.04 |
| altair processAttestation - 250000 vs - 7PWei normalcase | 1.9154 ms/op | 2.4636 ms/op | 0.78 |
| altair processAttestation - 250000 vs - 7PWei worstcase | 2.6419 ms/op | 2.9279 ms/op | 0.90 |
| altair processAttestation - setStatus - 1/6 committees join | 103.92 us/op | 101.91 us/op | 1.02 |
| altair processAttestation - setStatus - 1/3 committees join | 205.01 us/op | 201.88 us/op | 1.02 |
| altair processAttestation - setStatus - 1/2 committees join | 293.39 us/op | 264.73 us/op | 1.11 |
| altair processAttestation - setStatus - 2/3 committees join | 370.89 us/op | 354.54 us/op | 1.05 |
| altair processAttestation - setStatus - 4/5 committees join | 525.04 us/op | 496.59 us/op | 1.06 |
| altair processAttestation - setStatus - 100% committees join | 614.51 us/op | 592.92 us/op | 1.04 |
| altair processBlock - 250000 vs - 7PWei normalcase | 6.1964 ms/op | 4.2436 ms/op | 1.46 |
| altair processBlock - 250000 vs - 7PWei normalcase hashState | 21.437 ms/op | 13.411 ms/op | 1.60 |
| altair processBlock - 250000 vs - 7PWei worstcase | 27.825 ms/op | 21.724 ms/op | 1.28 |
| altair processBlock - 250000 vs - 7PWei worstcase hashState | 57.718 ms/op | 41.246 ms/op | 1.40 |
| phase0 processBlock - 250000 vs - 7PWei normalcase | 8.3458 ms/op | 1.3651 ms/op | 6.11 |
| phase0 processBlock - 250000 vs - 7PWei worstcase | 51.173 ms/op | 16.732 ms/op | 3.06 |
| altair processEth1Data - 250000 vs - 7PWei normalcase | 295.00 us/op | 275.65 us/op | 1.07 |
| getExpectedWithdrawals 250000 eb:1,eth1:1,we:0,wn:0,smpl:16 | 3.1060 us/op | 3.3200 us/op | 0.94 |
| getExpectedWithdrawals 250000 eb:0.95,eth1:0.1,we:0.05,wn:0,smpl:220 | 20.177 us/op | 21.143 us/op | 0.95 |
| getExpectedWithdrawals 250000 eb:0.95,eth1:0.3,we:0.05,wn:0,smpl:43 | 6.8560 us/op | 6.8380 us/op | 1.00 |
| getExpectedWithdrawals 250000 eb:0.95,eth1:0.7,we:0.05,wn:0,smpl:19 | 3.5040 us/op | 3.5780 us/op | 0.98 |
| getExpectedWithdrawals 250000 eb:0.1,eth1:0.1,we:0,wn:0,smpl:1021 | 91.945 us/op | 92.236 us/op | 1.00 |
| getExpectedWithdrawals 250000 eb:0.03,eth1:0.03,we:0,wn:0,smpl:11778 | 1.3952 ms/op | 1.3592 ms/op | 1.03 |
| getExpectedWithdrawals 250000 eb:0.01,eth1:0.01,we:0,wn:0,smpl:16384 | 1.8288 ms/op | 1.8328 ms/op | 1.00 |
| getExpectedWithdrawals 250000 eb:0,eth1:0,we:0,wn:0,smpl:16384 | 1.7417 ms/op | 1.9202 ms/op | 0.91 |
| getExpectedWithdrawals 250000 eb:0,eth1:0,we:0,wn:0,nocache,smpl:16384 | 3.5256 ms/op | 4.3836 ms/op | 0.80 |
| getExpectedWithdrawals 250000 eb:0,eth1:1,we:0,wn:0,smpl:16384 | 2.0716 ms/op | 2.2531 ms/op | 0.92 |
| getExpectedWithdrawals 250000 eb:0,eth1:1,we:0,wn:0,nocache,smpl:16384 | 3.7813 ms/op | 3.9941 ms/op | 0.95 |
| Tree 40 250000 create | 346.80 ms/op | 356.18 ms/op | 0.97 |
| Tree 40 250000 get(125000) | 91.987 ns/op | 92.959 ns/op | 0.99 |
| Tree 40 250000 set(125000) | 963.80 ns/op | 1.0213 us/op | 0.94 |
| Tree 40 250000 toArray() | 14.721 ms/op | 16.922 ms/op | 0.87 |
| Tree 40 250000 iterate all - toArray() + loop | 14.751 ms/op | 15.822 ms/op | 0.93 |
| Tree 40 250000 iterate all - get(i) | 37.386 ms/op | 38.227 ms/op | 0.98 |
| Array 250000 create | 2.2555 ms/op | 2.1861 ms/op | 1.03 |
| Array 250000 clone - spread | 687.96 us/op | 671.04 us/op | 1.03 |
| Array 250000 get(125000) | 0.28500 ns/op | 0.29100 ns/op | 0.98 |
| Array 250000 set(125000) | 0.28700 ns/op | 0.29600 ns/op | 0.97 |
| Array 250000 iterate all - loop | 55.714 us/op | 57.607 us/op | 0.97 |
| phase0 afterProcessEpoch - 250000 vs - 7PWei | 53.317 ms/op | 41.168 ms/op | 1.30 |
| Array.fill - length 1000000 | 2.3011 ms/op | 2.1874 ms/op | 1.05 |
| Array push - length 1000000 | 9.5111 ms/op | 9.4167 ms/op | 1.01 |
| Array.get | 0.20698 ns/op | 0.20469 ns/op | 1.01 |
| Uint8Array.get | 0.23338 ns/op | 0.22778 ns/op | 1.02 |
| phase0 beforeProcessEpoch - 250000 vs - 7PWei | 16.087 ms/op | 14.869 ms/op | 1.08 |
| altair processEpoch - mainnet_e81889 | 334.46 ms/op | 301.74 ms/op | 1.11 |
| mainnet_e81889 - altair beforeProcessEpoch | 19.050 ms/op | 20.937 ms/op | 0.91 |
| mainnet_e81889 - altair processJustificationAndFinalization | 8.3100 us/op | 6.6740 us/op | 1.25 |
| mainnet_e81889 - altair processInactivityUpdates | 3.4869 ms/op | 4.9618 ms/op | 0.70 |
| mainnet_e81889 - altair processRewardsAndPenalties | 18.452 ms/op | 23.150 ms/op | 0.80 |
| mainnet_e81889 - altair processRegistryUpdates | 556.00 ns/op | 568.00 ns/op | 0.98 |
| mainnet_e81889 - altair processSlashings | 131.00 ns/op | 139.00 ns/op | 0.94 |
| mainnet_e81889 - altair processEth1DataReset | 127.00 ns/op | 135.00 ns/op | 0.94 |
| mainnet_e81889 - altair processEffectiveBalanceUpdates | 1.6830 ms/op | 1.2721 ms/op | 1.32 |
| mainnet_e81889 - altair processSlashingsReset | 683.00 ns/op | 681.00 ns/op | 1.00 |
| mainnet_e81889 - altair processRandaoMixesReset | 1.3030 us/op | 1.2460 us/op | 1.05 |
| mainnet_e81889 - altair processHistoricalRootsUpdate | 130.00 ns/op | 136.00 ns/op | 0.96 |
| mainnet_e81889 - altair processParticipationFlagUpdates | 457.00 ns/op | 423.00 ns/op | 1.08 |
| mainnet_e81889 - altair processSyncCommitteeUpdates | 106.00 ns/op | 105.00 ns/op | 1.01 |
| mainnet_e81889 - altair afterProcessEpoch | 41.231 ms/op | 41.990 ms/op | 0.98 |
| capella processEpoch - mainnet_e217614 | 983.00 ms/op | 805.86 ms/op | 1.22 |
| mainnet_e217614 - capella beforeProcessEpoch | 73.428 ms/op | 58.578 ms/op | 1.25 |
| mainnet_e217614 - capella processJustificationAndFinalization | 6.7420 us/op | 6.5920 us/op | 1.02 |
| mainnet_e217614 - capella processInactivityUpdates | 13.692 ms/op | 16.645 ms/op | 0.82 |
| mainnet_e217614 - capella processRewardsAndPenalties | 95.214 ms/op | 95.884 ms/op | 0.99 |
| mainnet_e217614 - capella processRegistryUpdates | 4.3890 us/op | 4.3710 us/op | 1.00 |
| mainnet_e217614 - capella processSlashings | 125.00 ns/op | 130.00 ns/op | 0.96 |
| mainnet_e217614 - capella processEth1DataReset | 124.00 ns/op | 126.00 ns/op | 0.98 |
| mainnet_e217614 - capella processEffectiveBalanceUpdates | 16.090 ms/op | 15.431 ms/op | 1.04 |
| mainnet_e217614 - capella processSlashingsReset | 676.00 ns/op | 683.00 ns/op | 0.99 |
| mainnet_e217614 - capella processRandaoMixesReset | 1.3990 us/op | 1.3690 us/op | 1.02 |
| mainnet_e217614 - capella processHistoricalRootsUpdate | 122.00 ns/op | 129.00 ns/op | 0.95 |
| mainnet_e217614 - capella processParticipationFlagUpdates | 423.00 ns/op | 433.00 ns/op | 0.98 |
| mainnet_e217614 - capella afterProcessEpoch | 109.14 ms/op | 111.59 ms/op | 0.98 |
| phase0 processEpoch - mainnet_e58758 | 348.35 ms/op | 391.04 ms/op | 0.89 |
| mainnet_e58758 - phase0 beforeProcessEpoch | 70.389 ms/op | 83.140 ms/op | 0.85 |
| mainnet_e58758 - phase0 processJustificationAndFinalization | 6.8370 us/op | 6.8960 us/op | 0.99 |
| mainnet_e58758 - phase0 processRewardsAndPenalties | 16.292 ms/op | 17.412 ms/op | 0.94 |
| mainnet_e58758 - phase0 processRegistryUpdates | 2.2580 us/op | 2.2800 us/op | 0.99 |
| mainnet_e58758 - phase0 processSlashings | 124.00 ns/op | 132.00 ns/op | 0.94 |
| mainnet_e58758 - phase0 processEth1DataReset | 125.00 ns/op | 167.00 ns/op | 0.75 |
| mainnet_e58758 - phase0 processEffectiveBalanceUpdates | 821.70 us/op | 840.71 us/op | 0.98 |
| mainnet_e58758 - phase0 processSlashingsReset | 1.0030 us/op | 965.00 ns/op | 1.04 |
| mainnet_e58758 - phase0 processRandaoMixesReset | 1.3890 us/op | 1.4230 us/op | 0.98 |
| mainnet_e58758 - phase0 processHistoricalRootsUpdate | 130.00 ns/op | 131.00 ns/op | 0.99 |
| mainnet_e58758 - phase0 processParticipationRecordUpdates | 1.2280 us/op | 1.3240 us/op | 0.93 |
| mainnet_e58758 - phase0 afterProcessEpoch | 32.602 ms/op | 33.872 ms/op | 0.96 |
| phase0 processEffectiveBalanceUpdates - 250000 normalcase | 1.0940 ms/op | 1.0164 ms/op | 1.08 |
| phase0 processEffectiveBalanceUpdates - 250000 worstcase 0.5 | 1.6299 ms/op | 1.6290 ms/op | 1.00 |
| altair processInactivityUpdates - 250000 normalcase | 10.676 ms/op | 10.965 ms/op | 0.97 |
| altair processInactivityUpdates - 250000 worstcase | 10.656 ms/op | 12.755 ms/op | 0.84 |
| phase0 processRegistryUpdates - 250000 normalcase | 2.1660 us/op | 2.3310 us/op | 0.93 |
| phase0 processRegistryUpdates - 250000 badcase_full_deposits | 144.76 us/op | 139.32 us/op | 1.04 |
| phase0 processRegistryUpdates - 250000 worstcase 0.5 | 67.481 ms/op | 65.967 ms/op | 1.02 |
| altair processRewardsAndPenalties - 250000 normalcase | 15.794 ms/op | 16.760 ms/op | 0.94 |
| altair processRewardsAndPenalties - 250000 worstcase | 15.636 ms/op | 15.884 ms/op | 0.98 |
| phase0 getAttestationDeltas - 250000 normalcase | 5.5374 ms/op | 5.4825 ms/op | 1.01 |
| phase0 getAttestationDeltas - 250000 worstcase | 8.4924 ms/op | 5.4786 ms/op | 1.55 |
| phase0 processSlashings - 250000 worstcase | 61.126 us/op | 63.435 us/op | 0.96 |
| altair processSyncCommitteeUpdates - 250000 | 10.503 ms/op | 11.984 ms/op | 0.88 |
| BeaconState.hashTreeRoot - No change | 185.00 ns/op | 163.00 ns/op | 1.13 |
| BeaconState.hashTreeRoot - 1 full validator | 98.421 us/op | 84.342 us/op | 1.17 |
| BeaconState.hashTreeRoot - 32 full validator | 1.0996 ms/op | 905.87 us/op | 1.21 |
| BeaconState.hashTreeRoot - 512 full validator | 9.3547 ms/op | 9.3505 ms/op | 1.00 |
| BeaconState.hashTreeRoot - 1 validator.effectiveBalance | 111.43 us/op | 113.45 us/op | 0.98 |
| BeaconState.hashTreeRoot - 32 validator.effectiveBalance | 2.1923 ms/op | 1.5297 ms/op | 1.43 |
| BeaconState.hashTreeRoot - 512 validator.effectiveBalance | 17.965 ms/op | 21.734 ms/op | 0.83 |
| BeaconState.hashTreeRoot - 1 balances | 93.965 us/op | 83.699 us/op | 1.12 |
| BeaconState.hashTreeRoot - 32 balances | 1.3115 ms/op | 766.65 us/op | 1.71 |
| BeaconState.hashTreeRoot - 512 balances | 6.7204 ms/op | 6.8077 ms/op | 0.99 |
| BeaconState.hashTreeRoot - 250000 balances | 183.02 ms/op | 143.46 ms/op | 1.28 |
| aggregationBits - 2048 els - zipIndexesInBitList | 20.537 us/op | 20.683 us/op | 0.99 |
| regular array get 100000 times | 23.252 us/op | 22.984 us/op | 1.01 |
| wrappedArray get 100000 times | 23.313 us/op | 23.047 us/op | 1.01 |
| arrayWithProxy get 100000 times | 13.158 ms/op | 10.034 ms/op | 1.31 |
| ssz.Root.equals | 21.810 ns/op | 21.402 ns/op | 1.02 |
| byteArrayEquals | 21.570 ns/op | 21.191 ns/op | 1.02 |
| Buffer.compare | 8.9220 ns/op | 8.8120 ns/op | 1.01 |
| processSlot - 1 slots | 11.248 us/op | 9.6300 us/op | 1.17 |
| processSlot - 32 slots | 2.7503 ms/op | 2.2314 ms/op | 1.23 |
| getEffectiveBalanceIncrementsZeroInactive - 250000 vs - 7PWei | 6.7734 ms/op | 4.8978 ms/op | 1.38 |
| getCommitteeAssignments - req 1 vs - 250000 vc | 1.6527 ms/op | 1.6728 ms/op | 0.99 |
| getCommitteeAssignments - req 100 vs - 250000 vc | 3.4870 ms/op | 3.4447 ms/op | 1.01 |
| getCommitteeAssignments - req 1000 vs - 250000 vc | 3.6621 ms/op | 3.6348 ms/op | 1.01 |
| findModifiedValidators - 10000 modified validators | 776.83 ms/op | 685.05 ms/op | 1.13 |
| findModifiedValidators - 1000 modified validators | 445.19 ms/op | 555.92 ms/op | 0.80 |
| findModifiedValidators - 100 modified validators | 281.81 ms/op | 319.46 ms/op | 0.88 |
| findModifiedValidators - 10 modified validators | 266.30 ms/op | 284.96 ms/op | 0.93 |
| findModifiedValidators - 1 modified validators | 189.65 ms/op | 189.14 ms/op | 1.00 |
| findModifiedValidators - no difference | 184.75 ms/op | 169.68 ms/op | 1.09 |
| migrate state 1500000 validators, 3400 modified, 2000 new | 4.0080 s/op | 3.3349 s/op | 1.20 |
| RootCache.getBlockRootAtSlot - 250000 vs - 7PWei | 3.9200 ns/op | 3.4400 ns/op | 1.14 |
| state getBlockRootAtSlot - 250000 vs - 7PWei | 380.98 ns/op | 404.51 ns/op | 0.94 |
| computeProposerIndex 100000 validators | 1.3719 ms/op | 1.3885 ms/op | 0.99 |
| getNextSyncCommitteeIndices 1000 validators | 2.9261 ms/op | 2.9111 ms/op | 1.01 |
| getNextSyncCommitteeIndices 10000 validators | 25.969 ms/op | 25.664 ms/op | 1.01 |
| getNextSyncCommitteeIndices 100000 validators | 88.040 ms/op | 89.791 ms/op | 0.98 |
| computeProposers - vc 250000 | 552.00 us/op | 548.97 us/op | 1.01 |
| computeEpochShuffling - vc 250000 | 38.993 ms/op | 39.967 ms/op | 0.98 |
| getNextSyncCommittee - vc 250000 | 9.5877 ms/op | 11.304 ms/op | 0.85 |
| nodejs block root to RootHex using toHex | 99.159 ns/op | 95.918 ns/op | 1.03 |
| nodejs block root to RootHex using toRootHex | 63.167 ns/op | 62.236 ns/op | 1.01 |
| nodejs fromHex(blob) | 862.55 us/op | 842.31 us/op | 1.02 |
| nodejs fromHexInto(blob) | 618.76 us/op | 641.80 us/op | 0.96 |
| nodejs block root to RootHex using the deprecated toHexString | 523.70 ns/op | 549.92 ns/op | 0.95 |
| nodejs byteArrayEquals 32 bytes (block root) | 25.929 ns/op | 26.721 ns/op | 0.97 |
| nodejs byteArrayEquals 48 bytes (pubkey) | 37.503 ns/op | 38.206 ns/op | 0.98 |
| nodejs byteArrayEquals 96 bytes (signature) | 33.574 ns/op | 36.845 ns/op | 0.91 |
| nodejs byteArrayEquals 1024 bytes | 40.985 ns/op | 43.927 ns/op | 0.93 |
| nodejs byteArrayEquals 131072 bytes (blob) | 1.7456 us/op | 1.8165 us/op | 0.96 |
| browser block root to RootHex using toHex | 141.38 ns/op | 146.06 ns/op | 0.97 |
| browser block root to RootHex using toRootHex | 128.87 ns/op | 135.26 ns/op | 0.95 |
| browser fromHex(blob) | 1.7502 ms/op | 1.6041 ms/op | 1.09 |
| browser fromHexInto(blob) | 639.59 us/op | 612.90 us/op | 1.04 |
| browser block root to RootHex using the deprecated toHexString | 366.19 ns/op | 359.59 ns/op | 1.02 |
| browser byteArrayEquals 32 bytes (block root) | 28.326 ns/op | 28.275 ns/op | 1.00 |
| browser byteArrayEquals 48 bytes (pubkey) | 39.989 ns/op | 39.936 ns/op | 1.00 |
| browser byteArrayEquals 96 bytes (signature) | 74.409 ns/op | 74.918 ns/op | 0.99 |
| browser byteArrayEquals 1024 bytes | 758.04 ns/op | 762.61 ns/op | 0.99 |
| browser byteArrayEquals 131072 bytes (blob) | 96.159 us/op | 95.487 us/op | 1.01 |
by benchmarkbot/action
twoeths
left a comment
There was a problem hiding this comment.
we should pass in both the SignedBlock* object and the ssz bytes
current state-transition will only consume object
while native BeaconStateView will only consume Uint8Array
this is the same approach I'm using for BeaconEngine
|
spoke to @twoeths in a private conversation, this PR needs some semi-big changes. The interface should take both bytes and object to be compatible for now: export interface IBeaconStateView {
.. // other defs
stateTransition(
signedBlockBytes: Uint8Array,
signedBlock: SignedBlock | SignedBlindedBlock,
isBlinded: boolean,
options: StateTransitionOpts,
modules: StateTransitionModules
): IBeaconStateView;The only path that we can't get around this cost is block production since we always build a block from object. We can consider creating a new |
This is a change to align with Zig STF (which takes raw block bytes instead of a block object), see #9516, specifically this comment
Two key points, there a few paths affected, with
two key paths (namely block import and block production) being latency sensitive.
block import: we can get the
serialized block from cache easily since it is cached upon receiving from
gossip.
block production: we have to serialize/deserialize in an ugly manner unfortunately since we're assembling the block ourselves, so we have to pay the serialization cost
regen: we pass in the
serializedCacheinRegenModulesand use it that way, though it might be a cache miss for older blocks.Historical state replay: we have to pay the ser/deser cost here unless we store block bytes.
Spec tests are also changed to take bytes. We change all usages of
stateTransitionto the instance methodstate.stateTransition, so that the tests also work for the bindings when native stf is turned onUpdate on approach in this PR: see this comment
AI disclosure: talked to Codex to track down paths which could be impacted by this change in API, especially those that can't avoid the ser/deser cost