feat: provide first-class ssz support on api
Motivation
Initial discussion around this has started in https://github.com/ChainSafe/lodestar/issues/5128 to provide first-class support for SSZ in both request and response bodies but besides that, there are plenty of other shortcomings in the way we currently define routes and handle server / client responses. In a lot of cases, Lodestar was not following the spec (e.g. not setting all required headers) causing incompatibilities with other clients. The main reason for this is because our API spec tests were incomplete, headers were not checked at all or if we support SSZ on all required routes as per spec, and addressing those shortcomings requires workarounds like overriding handlers on both the client and server.
Description
This PR changes the structure of how routes are defined and makes it trivial to add SSZ support to any route. The only exception are routes with data payloads that cannot be represented as a SSZ type due to lack of JSON mapping, meaning it is not possible to translate the JSON payload into a SSZ-serialized payload.
The second major change is how we handle requests and responses on the server and client. While previously if you would request a state as SSZ from the server, the client would only be able to handle that format and if the server didn't support it, the request would fail. Now, we use content type negotiation between client and server which works based on headers and status codes to allow a more flexible and robust handling of requests and responses.
The third redesign in this PR is related to the API client, it is now possible to provide options like HTTP timeout per request and the response object is much more complete and self-contained, e.g. it's no longer needed to call ApiError.assert(...) everywhere (more examples below). This refactoring allows for further extensions in the future like a per route strategy for sending requests to fallback nodes and picking responses based on metadata, e.g. pick the most profitable block from all connected beacon nodes.
BREAKING CHANGES
While there is are no breaking changes to the validator client and beacon node, the client exported from @lodestar/api package changed significantly, and there are minor other changes to exported types but those are not as relevant for most consumers.
These are the most notable changes
Client initialization
before
const api = getClient({urls, getAbortSignal: () => opts.abortController.signal}, {config, logger});
after
const api = getClient({urls, globalInit: {signal: opts.abortController.signal}}, {config, logger});
Request with value
before
const res = await api.beacon.getGenesis();
ApiError.assert(res, "Can not fetch genesis data from beacon node");
const genesis = res.response.data;
after (.value() will assert if request was successful and throw detailed error)
const genesis = (await api.beacon.getGenesis()).value()
Request without value
before
ApiError.assert(await beaconClient.submitPoolVoluntaryExit(signedVoluntaryExit));
after (response object is self-contained, no utility like ApiError.assert is required anymore)
(await beaconClient.submitPoolVoluntaryExit({signedVoluntaryExit})).assertOk();
Passing arguments
before
api.validator.produceAttestationData(committeeIndex, slot)
after (args are passed as object, it is now possible to pass per request options, like timeout)
api.validator.produceAttestationData({committeeIndex, slot}, {timeoutMs: 4000})
TODO
- [ ] review proofs routes, do we wanna follow spec proposal?
- [ ] review lightclient routes, spec suggests to get fork version from attested_header slot, previous work from #4641? test against https://eth-light.xyz/,
Access-Control-Expose-Headers: Eth-Consensus-Versionheader required? - [ ] review network identity route, use enr package / add ssz support?
- [ ] move
@paramfrom method jsdoc to property - [ ] more documentation, update examples, update reasoning about API definitions
- [ ] more http client tests, mostly in regard to retries
- [ ] retry mechanism in http client if we receive a 415 we need to cache (per url x operationId) that server does not support it, resend different content type and on subsequent requests only sent supported content type
- [ ] default per route config to use spec compliant content types, e.g. if spec does not define SSZ request body, do not use it by default. Should be configurable to always use SSZ in a Lodestar only setups
- [ ] add option to server to disable JSON responses to avoid potential DoS vector, how to handle routes that do not support SSZ?
- [ ] ensure Lodestar is compatible with latest checkpointz release (#164, #165)
- [ ] final compatibility check with other clients and older Lodestar version using kurtosis
Closes https://github.com/ChainSafe/lodestar/issues/5128 Closes https://github.com/ChainSafe/lodestar/issues/5244 Closes https://github.com/ChainSafe/lodestar/issues/5826 Closes https://github.com/ChainSafe/lodestar/issues/6110 Closes https://github.com/ChainSafe/lodestar/issues/6564 Closes https://github.com/ChainSafe/lodestar/issues/6634
Performance Report
✔️ no performance regression detected
Full benchmark results
| Benchmark suite | Current: f64a6919e77b664bb999ed4bf3807f5b0329be06 | Previous: f6d3bce80297023aaf890f201f5e94f1e701e46c | Ratio |
|---|---|---|---|
| getPubkeys - index2pubkey - req 1000 vs - 250000 vc | 589.50 us/op | 437.23 us/op | 1.35 |
| getPubkeys - validatorsArr - req 1000 vs - 250000 vc | 41.008 us/op | 45.598 us/op | 0.90 |
| BLS verify - blst-native | 1.1170 ms/op | 1.2364 ms/op | 0.90 |
| BLS verifyMultipleSignatures 3 - blst-native | 2.3842 ms/op | 2.6262 ms/op | 0.91 |
| BLS verifyMultipleSignatures 8 - blst-native | 5.0184 ms/op | 5.7950 ms/op | 0.87 |
| BLS verifyMultipleSignatures 32 - blst-native | 18.460 ms/op | 21.259 ms/op | 0.87 |
| BLS verifyMultipleSignatures 64 - blst-native | 36.379 ms/op | 41.856 ms/op | 0.87 |
| BLS verifyMultipleSignatures 128 - blst-native | 72.226 ms/op | 83.166 ms/op | 0.87 |
| BLS deserializing 10000 signatures | 792.42 ms/op | 865.99 ms/op | 0.92 |
| BLS deserializing 100000 signatures | 7.7903 s/op | 8.6819 s/op | 0.90 |
| BLS verifyMultipleSignatures - same message - 3 - blst-native | 1.0824 ms/op | 1.2617 ms/op | 0.86 |
| BLS verifyMultipleSignatures - same message - 8 - blst-native | 1.2962 ms/op | 1.4281 ms/op | 0.91 |
| BLS verifyMultipleSignatures - same message - 32 - blst-native | 2.2675 ms/op | 2.3466 ms/op | 0.97 |
| BLS verifyMultipleSignatures - same message - 64 - blst-native | 3.4997 ms/op | 3.3842 ms/op | 1.03 |
| BLS verifyMultipleSignatures - same message - 128 - blst-native | 5.0360 ms/op | 6.3080 ms/op | 0.80 |
| BLS aggregatePubkeys 32 - blst-native | 23.419 us/op | 25.600 us/op | 0.91 |
| BLS aggregatePubkeys 128 - blst-native | 89.758 us/op | 98.296 us/op | 0.91 |
| notSeenSlots=1 numMissedVotes=1 numBadVotes=10 | 60.355 ms/op | 52.524 ms/op | 1.15 |
| notSeenSlots=1 numMissedVotes=0 numBadVotes=4 | 51.397 ms/op | 49.313 ms/op | 1.04 |
| notSeenSlots=2 numMissedVotes=1 numBadVotes=10 | 28.590 ms/op | 30.841 ms/op | 0.93 |
| getSlashingsAndExits - default max | 73.860 us/op | 86.798 us/op | 0.85 |
| getSlashingsAndExits - 2k | 238.36 us/op | 262.42 us/op | 0.91 |
| proposeBlockBody type=full, size=empty | 4.8298 ms/op | 5.3879 ms/op | 0.90 |
| isKnown best case - 1 super set check | 476.00 ns/op | 296.00 ns/op | 1.61 |
| isKnown normal case - 2 super set checks | 459.00 ns/op | 283.00 ns/op | 1.62 |
| isKnown worse case - 16 super set checks | 457.00 ns/op | 288.00 ns/op | 1.59 |
| InMemoryCheckpointStateCache - add get delete | 3.9890 us/op | 4.5270 us/op | 0.88 |
| validate api signedAggregateAndProof - struct | 2.4035 ms/op | 2.6922 ms/op | 0.89 |
| validate gossip signedAggregateAndProof - struct | 2.4231 ms/op | 2.6615 ms/op | 0.91 |
| validate gossip attestation - vc 640000 | 1.1168 ms/op | 1.2730 ms/op | 0.88 |
| batch validate gossip attestation - vc 640000 - chunk 32 | 129.95 us/op | 148.92 us/op | 0.87 |
| batch validate gossip attestation - vc 640000 - chunk 64 | 115.85 us/op | 131.35 us/op | 0.88 |
| batch validate gossip attestation - vc 640000 - chunk 128 | 109.88 us/op | 121.28 us/op | 0.91 |
| batch validate gossip attestation - vc 640000 - chunk 256 | 104.31 us/op | 117.85 us/op | 0.89 |
| pickEth1Vote - no votes | 822.80 us/op | 1.0410 ms/op | 0.79 |
| pickEth1Vote - max votes | 7.4370 ms/op | 7.5142 ms/op | 0.99 |
| pickEth1Vote - Eth1Data hashTreeRoot value x2048 | 15.983 ms/op | 15.522 ms/op | 1.03 |
| pickEth1Vote - Eth1Data hashTreeRoot tree x2048 | 20.618 ms/op | 18.609 ms/op | 1.11 |
| pickEth1Vote - Eth1Data fastSerialize value x2048 | 391.51 us/op | 507.22 us/op | 0.77 |
| pickEth1Vote - Eth1Data fastSerialize tree x2048 | 4.9297 ms/op | 5.4166 ms/op | 0.91 |
| bytes32 toHexString | 584.00 ns/op | 428.00 ns/op | 1.36 |
| bytes32 Buffer.toString(hex) | 423.00 ns/op | 248.00 ns/op | 1.71 |
| bytes32 Buffer.toString(hex) from Uint8Array | 518.00 ns/op | 347.00 ns/op | 1.49 |
| bytes32 Buffer.toString(hex) + 0x | 429.00 ns/op | 247.00 ns/op | 1.74 |
| Object access 1 prop | 0.32400 ns/op | 0.14200 ns/op | 2.28 |
| Map access 1 prop | 0.32400 ns/op | 0.13600 ns/op | 2.38 |
| Object get x1000 | 5.1270 ns/op | 5.9290 ns/op | 0.86 |
| Map get x1000 | 5.7940 ns/op | 6.7710 ns/op | 0.86 |
| Object set x1000 | 22.221 ns/op | 32.003 ns/op | 0.69 |
| Map set x1000 | 18.502 ns/op | 21.918 ns/op | 0.84 |
| Return object 10000 times | 0.29940 ns/op | 0.28940 ns/op | 1.03 |
| Throw Error 10000 times | 2.7003 us/op | 3.3436 us/op | 0.81 |
| fastMsgIdFn sha256 / 200 bytes | 1.9480 us/op | 2.1720 us/op | 0.90 |
| fastMsgIdFn h32 xxhash / 200 bytes | 400.00 ns/op | 229.00 ns/op | 1.75 |
| fastMsgIdFn h64 xxhash / 200 bytes | 463.00 ns/op | 270.00 ns/op | 1.71 |
| fastMsgIdFn sha256 / 1000 bytes | 5.9180 us/op | 7.2060 us/op | 0.82 |
| fastMsgIdFn h32 xxhash / 1000 bytes | 519.00 ns/op | 359.00 ns/op | 1.45 |
| fastMsgIdFn h64 xxhash / 1000 bytes | 525.00 ns/op | 346.00 ns/op | 1.52 |
| fastMsgIdFn sha256 / 10000 bytes | 49.188 us/op | 63.989 us/op | 0.77 |
| fastMsgIdFn h32 xxhash / 10000 bytes | 1.8240 us/op | 1.7950 us/op | 1.02 |
| fastMsgIdFn h64 xxhash / 10000 bytes | 1.2720 us/op | 1.1890 us/op | 1.07 |
| send data - 1000 256B messages | 9.6901 ms/op | 12.076 ms/op | 0.80 |
| send data - 1000 512B messages | 12.695 ms/op | 16.345 ms/op | 0.78 |
| send data - 1000 1024B messages | 24.748 ms/op | 25.663 ms/op | 0.96 |
| send data - 1000 1200B messages | 23.267 ms/op | 26.256 ms/op | 0.89 |
| send data - 1000 2048B messages | 28.857 ms/op | 31.082 ms/op | 0.93 |
| send data - 1000 4096B messages | 26.275 ms/op | 31.245 ms/op | 0.84 |
| send data - 1000 16384B messages | 66.563 ms/op | 68.924 ms/op | 0.97 |
| send data - 1000 65536B messages | 239.77 ms/op | 209.24 ms/op | 1.15 |
| enrSubnets - fastDeserialize 64 bits | 1.1500 us/op | 1.0990 us/op | 1.05 |
| enrSubnets - ssz BitVector 64 bits | 536.00 ns/op | 356.00 ns/op | 1.51 |
| enrSubnets - fastDeserialize 4 bits | 339.00 ns/op | 149.00 ns/op | 2.28 |
| enrSubnets - ssz BitVector 4 bits | 532.00 ns/op | 356.00 ns/op | 1.49 |
| prioritizePeers score -10:0 att 32-0.1 sync 2-0 | 119.88 us/op | 145.26 us/op | 0.83 |
| prioritizePeers score 0:0 att 32-0.25 sync 2-0.25 | 132.79 us/op | 161.54 us/op | 0.82 |
| prioritizePeers score 0:0 att 32-0.5 sync 2-0.5 | 290.26 us/op | 263.32 us/op | 1.10 |
| prioritizePeers score 0:0 att 64-0.75 sync 4-0.75 | 402.58 us/op | 388.34 us/op | 1.04 |
| prioritizePeers score 0:0 att 64-1 sync 4-1 | 632.65 us/op | 573.15 us/op | 1.10 |
| array of 16000 items push then shift | 1.3114 us/op | 1.6185 us/op | 0.81 |
| LinkedList of 16000 items push then shift | 6.4460 ns/op | 7.1390 ns/op | 0.90 |
| array of 16000 items push then pop | 78.471 ns/op | 105.99 ns/op | 0.74 |
| LinkedList of 16000 items push then pop | 6.3520 ns/op | 6.9530 ns/op | 0.91 |
| array of 24000 items push then shift | 1.9371 us/op | 2.3856 us/op | 0.81 |
| LinkedList of 24000 items push then shift | 6.8480 ns/op | 7.1220 ns/op | 0.96 |
| array of 24000 items push then pop | 108.70 ns/op | 130.44 ns/op | 0.83 |
| LinkedList of 24000 items push then pop | 5.9420 ns/op | 6.9330 ns/op | 0.86 |
| intersect bitArray bitLen 8 | 5.1180 ns/op | 6.4760 ns/op | 0.79 |
| intersect array and set length 8 | 37.368 ns/op | 46.424 ns/op | 0.80 |
| intersect bitArray bitLen 128 | 25.120 ns/op | 30.410 ns/op | 0.83 |
| intersect array and set length 128 | 546.91 ns/op | 674.88 ns/op | 0.81 |
| bitArray.getTrueBitIndexes() bitLen 128 | 2.3160 us/op | 1.8290 us/op | 1.27 |
| bitArray.getTrueBitIndexes() bitLen 248 | 4.2190 us/op | 3.4320 us/op | 1.23 |
| bitArray.getTrueBitIndexes() bitLen 512 | 6.6690 us/op | 6.3250 us/op | 1.05 |
| Buffer.concat 32 items | 1.0340 us/op | 1.0040 us/op | 1.03 |
| Uint8Array.set 32 items | 1.9890 us/op | 1.6220 us/op | 1.23 |
| Buffer.copy | 2.1000 us/op | 1.8650 us/op | 1.13 |
| Uint8Array.set - with subarray | 2.6590 us/op | 2.5360 us/op | 1.05 |
| Uint8Array.set - without subarray | 1.9350 us/op | 1.5370 us/op | 1.26 |
| Set add up to 64 items then delete first | 1.8317 us/op | 2.2738 us/op | 0.81 |
| OrderedSet add up to 64 items then delete first | 2.8560 us/op | 3.2732 us/op | 0.87 |
| Set add up to 64 items then delete last | 2.0576 us/op | 2.4468 us/op | 0.84 |
| OrderedSet add up to 64 items then delete last | 3.1295 us/op | 3.7159 us/op | 0.84 |
| Set add up to 64 items then delete middle | 2.0606 us/op | 2.6005 us/op | 0.79 |
| OrderedSet add up to 64 items then delete middle | 4.5305 us/op | 5.2622 us/op | 0.86 |
| Set add up to 128 items then delete first | 4.0679 us/op | 5.3942 us/op | 0.75 |
| OrderedSet add up to 128 items then delete first | 6.3876 us/op | 8.4613 us/op | 0.75 |
| Set add up to 128 items then delete last | 3.9427 us/op | 5.1035 us/op | 0.77 |
| OrderedSet add up to 128 items then delete last | 6.0037 us/op | 7.4257 us/op | 0.81 |
| Set add up to 128 items then delete middle | 3.9538 us/op | 4.8495 us/op | 0.82 |
| OrderedSet add up to 128 items then delete middle | 12.195 us/op | 13.555 us/op | 0.90 |
| Set add up to 256 items then delete first | 7.9707 us/op | 10.311 us/op | 0.77 |
| OrderedSet add up to 256 items then delete first | 12.642 us/op | 16.243 us/op | 0.78 |
| Set add up to 256 items then delete last | 7.7424 us/op | 9.9024 us/op | 0.78 |
| OrderedSet add up to 256 items then delete last | 11.960 us/op | 15.398 us/op | 0.78 |
| Set add up to 256 items then delete middle | 7.6989 us/op | 9.4761 us/op | 0.81 |
| OrderedSet add up to 256 items then delete middle | 35.261 us/op | 40.874 us/op | 0.86 |
| transfer serialized Status (84 B) | 1.4510 us/op | 1.3200 us/op | 1.10 |
| copy serialized Status (84 B) | 1.3260 us/op | 1.0670 us/op | 1.24 |
| transfer serialized SignedVoluntaryExit (112 B) | 1.7050 us/op | 1.4100 us/op | 1.21 |
| copy serialized SignedVoluntaryExit (112 B) | 1.4430 us/op | 1.1360 us/op | 1.27 |
| transfer serialized ProposerSlashing (416 B) | 2.4110 us/op | 1.8180 us/op | 1.33 |
| copy serialized ProposerSlashing (416 B) | 2.5550 us/op | 1.7270 us/op | 1.48 |
| transfer serialized Attestation (485 B) | 2.6740 us/op | 2.0400 us/op | 1.31 |
| copy serialized Attestation (485 B) | 2.5730 us/op | 1.5100 us/op | 1.70 |
| transfer serialized AttesterSlashing (33232 B) | 2.5240 us/op | 2.0600 us/op | 1.23 |
| copy serialized AttesterSlashing (33232 B) | 5.2930 us/op | 4.7460 us/op | 1.12 |
| transfer serialized Small SignedBeaconBlock (128000 B) | 3.2070 us/op | 2.3990 us/op | 1.34 |
| copy serialized Small SignedBeaconBlock (128000 B) | 10.907 us/op | 14.611 us/op | 0.75 |
| transfer serialized Avg SignedBeaconBlock (200000 B) | 3.3110 us/op | 2.5990 us/op | 1.27 |
| copy serialized Avg SignedBeaconBlock (200000 B) | 14.317 us/op | 19.612 us/op | 0.73 |
| transfer serialized BlobsSidecar (524380 B) | 3.3520 us/op | 2.7050 us/op | 1.24 |
| copy serialized BlobsSidecar (524380 B) | 74.426 us/op | 195.29 us/op | 0.38 |
| transfer serialized Big SignedBeaconBlock (1000000 B) | 3.3540 us/op | 2.5690 us/op | 1.31 |
| copy serialized Big SignedBeaconBlock (1000000 B) | 245.78 us/op | 133.33 us/op | 1.84 |
| pass gossip attestations to forkchoice per slot | 2.5806 ms/op | 3.0969 ms/op | 0.83 |
| forkChoice updateHead vc 100000 bc 64 eq 0 | 417.76 us/op | 503.51 us/op | 0.83 |
| forkChoice updateHead vc 600000 bc 64 eq 0 | 2.5545 ms/op | 3.0522 ms/op | 0.84 |
| forkChoice updateHead vc 1000000 bc 64 eq 0 | 4.1840 ms/op | 5.1607 ms/op | 0.81 |
| forkChoice updateHead vc 600000 bc 320 eq 0 | 2.8793 ms/op | 3.0994 ms/op | 0.93 |
| forkChoice updateHead vc 600000 bc 1200 eq 0 | 2.5512 ms/op | 3.1527 ms/op | 0.81 |
| forkChoice updateHead vc 600000 bc 7200 eq 0 | 2.8927 ms/op | 3.5698 ms/op | 0.81 |
| forkChoice updateHead vc 600000 bc 64 eq 1000 | 9.5913 ms/op | 11.294 ms/op | 0.85 |
| forkChoice updateHead vc 600000 bc 64 eq 10000 | 9.1835 ms/op | 11.405 ms/op | 0.81 |
| forkChoice updateHead vc 600000 bc 64 eq 300000 | 12.143 ms/op | 14.888 ms/op | 0.82 |
| computeDeltas 500000 validators 300 proto nodes | 2.8810 ms/op | 3.4913 ms/op | 0.83 |
| computeDeltas 500000 validators 1200 proto nodes | 2.8887 ms/op | 3.5217 ms/op | 0.82 |
| computeDeltas 500000 validators 7200 proto nodes | 2.9350 ms/op | 3.4650 ms/op | 0.85 |
| computeDeltas 750000 validators 300 proto nodes | 4.5187 ms/op | 5.1907 ms/op | 0.87 |
| computeDeltas 750000 validators 1200 proto nodes | 4.3749 ms/op | 5.0985 ms/op | 0.86 |
| computeDeltas 750000 validators 7200 proto nodes | 4.5147 ms/op | 5.1138 ms/op | 0.88 |
| computeDeltas 1400000 validators 300 proto nodes | 8.1111 ms/op | 9.6697 ms/op | 0.84 |
| computeDeltas 1400000 validators 1200 proto nodes | 8.2289 ms/op | 9.5734 ms/op | 0.86 |
| computeDeltas 1400000 validators 7200 proto nodes | 8.1873 ms/op | 9.5305 ms/op | 0.86 |
| computeDeltas 2100000 validators 300 proto nodes | 12.444 ms/op | 14.375 ms/op | 0.87 |
| computeDeltas 2100000 validators 1200 proto nodes | 12.438 ms/op | 14.643 ms/op | 0.85 |
| computeDeltas 2100000 validators 7200 proto nodes | 13.156 ms/op | 14.482 ms/op | 0.91 |
| altair processAttestation - 250000 vs - 7PWei normalcase | 1.5701 ms/op | 1.6363 ms/op | 0.96 |
| altair processAttestation - 250000 vs - 7PWei worstcase | 2.4092 ms/op | 2.4593 ms/op | 0.98 |
| altair processAttestation - setStatus - 1/6 committees join | 70.664 us/op | 84.204 us/op | 0.84 |
| altair processAttestation - setStatus - 1/3 committees join | 139.21 us/op | 169.63 us/op | 0.82 |
| altair processAttestation - setStatus - 1/2 committees join | 202.03 us/op | 240.32 us/op | 0.84 |
| altair processAttestation - setStatus - 2/3 committees join | 258.94 us/op | 317.10 us/op | 0.82 |
| altair processAttestation - setStatus - 4/5 committees join | 411.60 us/op | 459.19 us/op | 0.90 |
| altair processAttestation - setStatus - 100% committees join | 499.73 us/op | 551.53 us/op | 0.91 |
| altair processBlock - 250000 vs - 7PWei normalcase | 5.0384 ms/op | 3.3260 ms/op | 1.51 |
| altair processBlock - 250000 vs - 7PWei normalcase hashState | 24.417 ms/op | 25.603 ms/op | 0.95 |
| altair processBlock - 250000 vs - 7PWei worstcase | 40.972 ms/op | 38.902 ms/op | 1.05 |
| altair processBlock - 250000 vs - 7PWei worstcase hashState | 70.434 ms/op | 77.824 ms/op | 0.91 |
| phase0 processBlock - 250000 vs - 7PWei normalcase | 2.0397 ms/op | 1.7408 ms/op | 1.17 |
| phase0 processBlock - 250000 vs - 7PWei worstcase | 25.442 ms/op | 26.447 ms/op | 0.96 |
| altair processEth1Data - 250000 vs - 7PWei normalcase | 269.87 us/op | 285.63 us/op | 0.94 |
| getExpectedWithdrawals 250000 eb:1,eth1:1,we:0,wn:0,smpl:15 | 4.8610 us/op | 5.1000 us/op | 0.95 |
| getExpectedWithdrawals 250000 eb:0.95,eth1:0.1,we:0.05,wn:0,smpl:219 | 18.791 us/op | 17.372 us/op | 1.08 |
| getExpectedWithdrawals 250000 eb:0.95,eth1:0.3,we:0.05,wn:0,smpl:42 | 7.5530 us/op | 7.3090 us/op | 1.03 |
| getExpectedWithdrawals 250000 eb:0.95,eth1:0.7,we:0.05,wn:0,smpl:18 | 5.4610 us/op | 5.2740 us/op | 1.04 |
| getExpectedWithdrawals 250000 eb:0.1,eth1:0.1,we:0,wn:0,smpl:1020 | 79.469 us/op | 71.142 us/op | 1.12 |
| getExpectedWithdrawals 250000 eb:0.03,eth1:0.03,we:0,wn:0,smpl:11777 | 879.22 us/op | 659.50 us/op | 1.33 |
| getExpectedWithdrawals 250000 eb:0.01,eth1:0.01,we:0,wn:0,smpl:16384 | 658.24 us/op | 919.78 us/op | 0.72 |
| getExpectedWithdrawals 250000 eb:0,eth1:0,we:0,wn:0,smpl:16384 | 635.80 us/op | 882.51 us/op | 0.72 |
| getExpectedWithdrawals 250000 eb:0,eth1:0,we:0,wn:0,nocache,smpl:16384 | 2.0881 ms/op | 2.0678 ms/op | 1.01 |
| getExpectedWithdrawals 250000 eb:0,eth1:1,we:0,wn:0,smpl:16384 | 1.1506 ms/op | 1.5872 ms/op | 0.72 |
| getExpectedWithdrawals 250000 eb:0,eth1:1,we:0,wn:0,nocache,smpl:16384 | 2.9574 ms/op | 3.4745 ms/op | 0.85 |
| Tree 40 250000 create | 188.12 ms/op | 226.08 ms/op | 0.83 |
| Tree 40 250000 get(125000) | 108.83 ns/op | 142.21 ns/op | 0.77 |
| Tree 40 250000 set(125000) | 582.99 ns/op | 626.82 ns/op | 0.93 |
| Tree 40 250000 toArray() | 14.479 ms/op | 14.612 ms/op | 0.99 |
| Tree 40 250000 iterate all - toArray() + loop | 13.360 ms/op | 14.818 ms/op | 0.90 |
| Tree 40 250000 iterate all - get(i) | 41.355 ms/op | 50.057 ms/op | 0.83 |
| MutableVector 250000 create | 8.0741 ms/op | 8.8068 ms/op | 0.92 |
| MutableVector 250000 get(125000) | 5.6550 ns/op | 6.1020 ns/op | 0.93 |
| MutableVector 250000 set(125000) | 168.21 ns/op | 200.48 ns/op | 0.84 |
| MutableVector 250000 toArray() | 2.9482 ms/op | 3.5648 ms/op | 0.83 |
| MutableVector 250000 iterate all - toArray() + loop | 3.0301 ms/op | 3.7118 ms/op | 0.82 |
| MutableVector 250000 iterate all - get(i) | 1.3529 ms/op | 1.6437 ms/op | 0.82 |
| Array 250000 create | 2.5422 ms/op | 3.0178 ms/op | 0.84 |
| Array 250000 clone - spread | 1.2958 ms/op | 1.5576 ms/op | 0.83 |
| Array 250000 get(125000) | 0.56000 ns/op | 0.41100 ns/op | 1.36 |
| Array 250000 set(125000) | 0.57100 ns/op | 0.42900 ns/op | 1.33 |
| Array 250000 iterate all - loop | 73.831 us/op | 107.26 us/op | 0.69 |
| effectiveBalanceIncrements clone Uint8Array 300000 | 21.890 us/op | 29.851 us/op | 0.73 |
| effectiveBalanceIncrements clone MutableVector 300000 | 302.00 ns/op | 127.00 ns/op | 2.38 |
| effectiveBalanceIncrements rw all Uint8Array 300000 | 160.15 us/op | 198.73 us/op | 0.81 |
| effectiveBalanceIncrements rw all MutableVector 300000 | 56.212 ms/op | 68.041 ms/op | 0.83 |
| phase0 afterProcessEpoch - 250000 vs - 7PWei | 73.472 ms/op | 83.048 ms/op | 0.88 |
| phase0 beforeProcessEpoch - 250000 vs - 7PWei | 41.412 ms/op | 31.003 ms/op | 1.34 |
| altair processEpoch - mainnet_e81889 | 349.15 ms/op | 366.70 ms/op | 0.95 |
| mainnet_e81889 - altair beforeProcessEpoch | 43.344 ms/op | 63.927 ms/op | 0.68 |
| mainnet_e81889 - altair processJustificationAndFinalization | 5.4490 us/op | 12.420 us/op | 0.44 |
| mainnet_e81889 - altair processInactivityUpdates | 6.0817 ms/op | 5.3669 ms/op | 1.13 |
| mainnet_e81889 - altair processRewardsAndPenalties | 47.765 ms/op | 43.752 ms/op | 1.09 |
| mainnet_e81889 - altair processRegistryUpdates | 1.2610 us/op | 1.9120 us/op | 0.66 |
| mainnet_e81889 - altair processSlashings | 773.00 ns/op | 357.00 ns/op | 2.17 |
| mainnet_e81889 - altair processEth1DataReset | 698.00 ns/op | 323.00 ns/op | 2.16 |
| mainnet_e81889 - altair processEffectiveBalanceUpdates | 1.3016 ms/op | 1.0533 ms/op | 1.24 |
| mainnet_e81889 - altair processSlashingsReset | 2.1160 us/op | 3.9660 us/op | 0.53 |
| mainnet_e81889 - altair processRandaoMixesReset | 3.7030 us/op | 4.0530 us/op | 0.91 |
| mainnet_e81889 - altair processHistoricalRootsUpdate | 757.00 ns/op | 500.00 ns/op | 1.51 |
| mainnet_e81889 - altair processParticipationFlagUpdates | 8.1650 us/op | 1.7700 us/op | 4.61 |
| mainnet_e81889 - altair processSyncCommitteeUpdates | 833.00 ns/op | 385.00 ns/op | 2.16 |
| mainnet_e81889 - altair afterProcessEpoch | 81.442 ms/op | 91.269 ms/op | 0.89 |
| capella processEpoch - mainnet_e217614 | 1.3016 s/op | 1.2545 s/op | 1.04 |
| mainnet_e217614 - capella beforeProcessEpoch | 251.04 ms/op | 236.64 ms/op | 1.06 |
| mainnet_e217614 - capella processJustificationAndFinalization | 14.354 us/op | 13.504 us/op | 1.06 |
| mainnet_e217614 - capella processInactivityUpdates | 15.877 ms/op | 15.848 ms/op | 1.00 |
| mainnet_e217614 - capella processRewardsAndPenalties | 251.81 ms/op | 237.27 ms/op | 1.06 |
| mainnet_e217614 - capella processRegistryUpdates | 14.001 us/op | 12.729 us/op | 1.10 |
| mainnet_e217614 - capella processSlashings | 919.00 ns/op | 336.00 ns/op | 2.74 |
| mainnet_e217614 - capella processEth1DataReset | 752.00 ns/op | 352.00 ns/op | 2.14 |
| mainnet_e217614 - capella processEffectiveBalanceUpdates | 11.492 ms/op | 4.8556 ms/op | 2.37 |
| mainnet_e217614 - capella processSlashingsReset | 1.8080 us/op | 2.8540 us/op | 0.63 |
| mainnet_e217614 - capella processRandaoMixesReset | 3.2660 us/op | 4.3130 us/op | 0.76 |
| mainnet_e217614 - capella processHistoricalRootsUpdate | 1.8580 us/op | 374.00 ns/op | 4.97 |
| mainnet_e217614 - capella processParticipationFlagUpdates | 2.1380 us/op | 2.7090 us/op | 0.79 |
| mainnet_e217614 - capella afterProcessEpoch | 208.01 ms/op | 227.19 ms/op | 0.92 |
| phase0 processEpoch - mainnet_e58758 | 343.11 ms/op | 363.08 ms/op | 0.95 |
| mainnet_e58758 - phase0 beforeProcessEpoch | 113.92 ms/op | 110.05 ms/op | 1.04 |
| mainnet_e58758 - phase0 processJustificationAndFinalization | 11.447 us/op | 13.711 us/op | 0.83 |
| mainnet_e58758 - phase0 processRewardsAndPenalties | 41.907 ms/op | 30.215 ms/op | 1.39 |
| mainnet_e58758 - phase0 processRegistryUpdates | 7.8330 us/op | 7.5050 us/op | 1.04 |
| mainnet_e58758 - phase0 processSlashings | 709.00 ns/op | 292.00 ns/op | 2.43 |
| mainnet_e58758 - phase0 processEth1DataReset | 762.00 ns/op | 295.00 ns/op | 2.58 |
| mainnet_e58758 - phase0 processEffectiveBalanceUpdates | 1.0921 ms/op | 936.03 us/op | 1.17 |
| mainnet_e58758 - phase0 processSlashingsReset | 2.5730 us/op | 2.8560 us/op | 0.90 |
| mainnet_e58758 - phase0 processRandaoMixesReset | 3.3930 us/op | 3.3800 us/op | 1.00 |
| mainnet_e58758 - phase0 processHistoricalRootsUpdate | 715.00 ns/op | 339.00 ns/op | 2.11 |
| mainnet_e58758 - phase0 processParticipationRecordUpdates | 2.6250 us/op | 2.5390 us/op | 1.03 |
| mainnet_e58758 - phase0 afterProcessEpoch | 66.811 ms/op | 76.070 ms/op | 0.88 |
| phase0 processEffectiveBalanceUpdates - 250000 normalcase | 1.1969 ms/op | 1.5165 ms/op | 0.79 |
| phase0 processEffectiveBalanceUpdates - 250000 worstcase 0.5 | 1.4137 ms/op | 2.0190 ms/op | 0.70 |
| altair processInactivityUpdates - 250000 normalcase | 19.743 ms/op | 16.041 ms/op | 1.23 |
| altair processInactivityUpdates - 250000 worstcase | 19.963 ms/op | 16.713 ms/op | 1.19 |
| phase0 processRegistryUpdates - 250000 normalcase | 5.9800 us/op | 5.7490 us/op | 1.04 |
| phase0 processRegistryUpdates - 250000 badcase_full_deposits | 310.29 us/op | 228.62 us/op | 1.36 |
| phase0 processRegistryUpdates - 250000 worstcase 0.5 | 100.54 ms/op | 100.61 ms/op | 1.00 |
| altair processRewardsAndPenalties - 250000 normalcase | 41.277 ms/op | 39.287 ms/op | 1.05 |
| altair processRewardsAndPenalties - 250000 worstcase | 43.382 ms/op | 35.754 ms/op | 1.21 |
| phase0 getAttestationDeltas - 250000 normalcase | 5.8250 ms/op | 7.1623 ms/op | 0.81 |
| phase0 getAttestationDeltas - 250000 worstcase | 6.0663 ms/op | 6.8960 ms/op | 0.88 |
| phase0 processSlashings - 250000 worstcase | 82.388 us/op | 77.847 us/op | 1.06 |
| altair processSyncCommitteeUpdates - 250000 | 98.197 ms/op | 121.35 ms/op | 0.81 |
| BeaconState.hashTreeRoot - No change | 477.00 ns/op | 278.00 ns/op | 1.72 |
| BeaconState.hashTreeRoot - 1 full validator | 77.787 us/op | 126.72 us/op | 0.61 |
| BeaconState.hashTreeRoot - 32 full validator | 1.0224 ms/op | 1.3585 ms/op | 0.75 |
| BeaconState.hashTreeRoot - 512 full validator | 11.859 ms/op | 14.307 ms/op | 0.83 |
| BeaconState.hashTreeRoot - 1 validator.effectiveBalance | 107.61 us/op | 121.69 us/op | 0.88 |
| BeaconState.hashTreeRoot - 32 validator.effectiveBalance | 1.8280 ms/op | 1.6098 ms/op | 1.14 |
| BeaconState.hashTreeRoot - 512 validator.effectiveBalance | 17.873 ms/op | 22.730 ms/op | 0.79 |
| BeaconState.hashTreeRoot - 1 balances | 70.828 us/op | 112.80 us/op | 0.63 |
| BeaconState.hashTreeRoot - 32 balances | 637.10 us/op | 1.1711 ms/op | 0.54 |
| BeaconState.hashTreeRoot - 512 balances | 6.0512 ms/op | 10.947 ms/op | 0.55 |
| BeaconState.hashTreeRoot - 250000 balances | 126.60 ms/op | 181.47 ms/op | 0.70 |
| aggregationBits - 2048 els - zipIndexesInBitList | 20.197 us/op | 22.387 us/op | 0.90 |
| byteArrayEquals 32 | 48.601 ns/op | 51.585 ns/op | 0.94 |
| Buffer.compare 32 | 39.040 ns/op | 43.624 ns/op | 0.89 |
| byteArrayEquals 1024 | 1.2293 us/op | 1.5263 us/op | 0.81 |
| Buffer.compare 1024 | 42.737 ns/op | 50.653 ns/op | 0.84 |
| byteArrayEquals 16384 | 20.004 us/op | 24.305 us/op | 0.82 |
| Buffer.compare 16384 | 229.89 ns/op | 237.79 ns/op | 0.97 |
| byteArrayEquals 123687377 | 148.90 ms/op | 183.50 ms/op | 0.81 |
| Buffer.compare 123687377 | 5.3052 ms/op | 6.0234 ms/op | 0.88 |
| byteArrayEquals 32 - diff last byte | 45.230 ns/op | 50.493 ns/op | 0.90 |
| Buffer.compare 32 - diff last byte | 37.207 ns/op | 47.045 ns/op | 0.79 |
| byteArrayEquals 1024 - diff last byte | 1.1992 us/op | 1.5264 us/op | 0.79 |
| Buffer.compare 1024 - diff last byte | 42.991 ns/op | 52.418 ns/op | 0.82 |
| byteArrayEquals 16384 - diff last byte | 19.065 us/op | 24.291 us/op | 0.78 |
| Buffer.compare 16384 - diff last byte | 199.93 ns/op | 232.56 ns/op | 0.86 |
| byteArrayEquals 123687377 - diff last byte | 144.79 ms/op | 188.41 ms/op | 0.77 |
| Buffer.compare 123687377 - diff last byte | 5.3415 ms/op | 6.0995 ms/op | 0.88 |
| byteArrayEquals 32 - random bytes | 4.7420 ns/op | 5.1090 ns/op | 0.93 |
| Buffer.compare 32 - random bytes | 38.456 ns/op | 47.362 ns/op | 0.81 |
| byteArrayEquals 1024 - random bytes | 4.7450 ns/op | 5.0890 ns/op | 0.93 |
| Buffer.compare 1024 - random bytes | 36.523 ns/op | 45.510 ns/op | 0.80 |
| byteArrayEquals 16384 - random bytes | 4.7490 ns/op | 5.0910 ns/op | 0.93 |
| Buffer.compare 16384 - random bytes | 36.632 ns/op | 45.615 ns/op | 0.80 |
| byteArrayEquals 123687377 - random bytes | 7.6000 ns/op | 6.4800 ns/op | 1.17 |
| Buffer.compare 123687377 - random bytes | 39.750 ns/op | 47.040 ns/op | 0.85 |
| regular array get 100000 times | 29.522 us/op | 43.336 us/op | 0.68 |
| wrappedArray get 100000 times | 29.514 us/op | 32.542 us/op | 0.91 |
| arrayWithProxy get 100000 times | 9.8555 ms/op | 13.244 ms/op | 0.74 |
| ssz.Root.equals | 44.457 ns/op | 45.598 ns/op | 0.97 |
| byteArrayEquals | 43.789 ns/op | 45.036 ns/op | 0.97 |
| Buffer.compare | 9.1600 ns/op | 10.231 ns/op | 0.90 |
| shuffle list - 16384 els | 5.5299 ms/op | 6.2584 ms/op | 0.88 |
| shuffle list - 250000 els | 82.124 ms/op | 91.626 ms/op | 0.90 |
| processSlot - 1 slots | 11.634 us/op | 12.946 us/op | 0.90 |
| processSlot - 32 slots | 3.0960 ms/op | 3.1327 ms/op | 0.99 |
| getEffectiveBalanceIncrementsZeroInactive - 250000 vs - 7PWei | 44.325 ms/op | 37.518 ms/op | 1.18 |
| getCommitteeAssignments - req 1 vs - 250000 vc | 1.7234 ms/op | 2.1545 ms/op | 0.80 |
| getCommitteeAssignments - req 100 vs - 250000 vc | 3.4083 ms/op | 4.1512 ms/op | 0.82 |
| getCommitteeAssignments - req 1000 vs - 250000 vc | 3.6896 ms/op | 4.4712 ms/op | 0.83 |
| findModifiedValidators - 10000 modified validators | 232.84 ms/op | 239.38 ms/op | 0.97 |
| findModifiedValidators - 1000 modified validators | 146.07 ms/op | 175.03 ms/op | 0.83 |
| findModifiedValidators - 100 modified validators | 134.29 ms/op | 160.00 ms/op | 0.84 |
| findModifiedValidators - 10 modified validators | 130.43 ms/op | 157.60 ms/op | 0.83 |
| findModifiedValidators - 1 modified validators | 141.85 ms/op | 149.14 ms/op | 0.95 |
| findModifiedValidators - no difference | 142.66 ms/op | 154.94 ms/op | 0.92 |
| compare ViewDUs | 3.0274 s/op | 2.9386 s/op | 1.03 |
| compare each validator Uint8Array | 1.7207 s/op | 1.5263 s/op | 1.13 |
| compare ViewDU to Uint8Array | 793.16 ms/op | 1.0231 s/op | 0.78 |
| migrate state 1000000 validators, 24 modified, 0 new | 584.03 ms/op | 586.24 ms/op | 1.00 |
| migrate state 1000000 validators, 1700 modified, 1000 new | 813.54 ms/op | 825.28 ms/op | 0.99 |
| migrate state 1000000 validators, 3400 modified, 2000 new | 1.0144 s/op | 1.0262 s/op | 0.99 |
| migrate state 1500000 validators, 24 modified, 0 new | 578.41 ms/op | 590.49 ms/op | 0.98 |
| migrate state 1500000 validators, 1700 modified, 1000 new | 811.02 ms/op | 838.99 ms/op | 0.97 |
| migrate state 1500000 validators, 3400 modified, 2000 new | 997.92 ms/op | 1.0279 s/op | 0.97 |
| RootCache.getBlockRootAtSlot - 250000 vs - 7PWei | 6.2000 ns/op | 4.4100 ns/op | 1.41 |
| state getBlockRootAtSlot - 250000 vs - 7PWei | 791.07 ns/op | 700.08 ns/op | 1.13 |
| computeProposers - vc 250000 | 5.8960 ms/op | 7.5503 ms/op | 0.78 |
| computeEpochShuffling - vc 250000 | 82.224 ms/op | 90.922 ms/op | 0.90 |
| getNextSyncCommittee - vc 250000 | 95.063 ms/op | 128.01 ms/op | 0.74 |
| computeSigningRoot for AttestationData | 20.048 us/op | 22.777 us/op | 0.88 |
| hash AttestationData serialized data then Buffer.toString(base64) | 1.1073 us/op | 1.4687 us/op | 0.75 |
| toHexString serialized data | 713.72 ns/op | 860.22 ns/op | 0.83 |
| Buffer.toString(base64) | 116.97 ns/op | 169.29 ns/op | 0.69 |
by benchmarkbot/action
My major concern upto this point is the the lack of response codes from the routes definitions. Now use will never know any endpoint can response with 200, 206, 503, or 404.
We are putting alls calls in a promise box and saying it would be either fullfil or fail. But in case of failure user don't know what could be expected status codes.
This was a major information missing in earlier implementation and we added that in last refactor. I don't think loosing that context is a worthwhile.
We could add this by extending the Endpoint type but I honestly kinda happy that those are removed.
The reasons why I think those were not great are
- not correctness checked against spec, e.g. getHeader claims it only returns 200 while the spec also has 204 defined (the spec also has no 404 while our types do...)
- I have only found a single case in the code where we actually use the status code directly
- we don't enforce those status codes anywhere to ensure implementation correctness, in current unstable, it's not even possible to set the status code from the server impl (although we can do that now)
- the information communicated by the status code types is not very detailed, e.g. it says what status codes can be returned but not why those might be returned, making it hardly actionable without looking at the spec which contains more details
I personally don't think we should add status codes to each endpoint for reasons listed above but if we feel strongly about it could still be done.
Request times from validator client point of view stable (on the left) vs. feat1 (on the right)
Last 7 days, rate interval 1d
getAttesterDuties
getProposerDuties
prepareBeaconCommitteeSubnet
produceAttestationData
submitPoolAttestations
produceBlockV3
publishBlockV2
- not correctness checked against spec, e.g. getHeader claims it only returns 200 while the spec also has 204 defined (the spec also has no 404 while our types do...)
If something is not aligned withe specs, we should fix that rather remove that behavior. At least we had the flexibility specifying the status codes for endpoints and now we don't have it. So how can we check the correctness against specs now?
we don't enforce those status codes anywhere to ensure implementation correctness, in current unstable, it's not even possible to set the status code from the server impl (although we can do that now)
That was continuous improvement, when earlier refactor was done there was no multiple status codes for that point from server. So we have those only for client side.
the information communicated by the status code types is not very detailed, e.g. it says what status codes can be returned but not why those might be returned.
That's upto the documentation of the API. As long as our API client specify a status code, user should be able to see the docs for what it's meant.
My major concern on this approach is not yet addressed, by introducing a pattern of implicitly error we at the API client side deciding what response from server is an error and what is success. Without documenting those status codes our API client can't be generic and we have to write custom code to handle response for such endpoints.
If something is not aligned withe specs, we should fix that rather remove that behavior. At least we had the flexibility specifying the status codes for endpoints and now we don't have it. So how can we check the correctness against specs now?
Status codes were only defined as types, this makes it impossible to test against spec. We can define runtime status codes (specifically for errors) as js values and derive the res.status type from those but I still don't see much value in checking those against the spec as error status codes are not used anywhere, and in addition those pretty much depend on the server implementation, meaning the server decides what error status code to return.
Testing correctness of status code is pretty conditional and can't be covered by checking against the spec, e.g. for getHealth we have e2e tests to ensure the status codes are correct which relies on beacon node being in a certain state.
https://github.com/ChainSafe/lodestar/blob/8b6ecc44c1892115d0e7396b2a5f0ae8fc0d9e6f/packages/beacon-node/test/e2e/api/impl/beacon/node/endpoints.test.ts#L152-L154
Fun fact regarding this, the status codes for getHealth were actually broken after https://github.com/ChainSafe/lodestar/pull/4994 (it always returned 200) and later fixed in https://github.com/ChainSafe/lodestar/pull/5790, which adds to my point that we didn't ensure any correctness by typing status codes.
That's upto the documentation of the API. As long as our API client specify a status code, user should be able to see the docs for what it's meant.
We could add this as part of js doc, then we could at least some context to it, e.g. getBlock returns 404 if block is not found. But I am still not convinced by this either in terms of effort maintaining this vs. value we get from it + correctness against spec is still not ensured.
Without documenting those status codes our API client can't be generic and we have to write custom code to handle response for such endpoints.
Can you point out where in the code we now require custom code? I honestly can't follow your argument, at least in the Lodestar code base typing the status code or not is irrelevant (and I reviewed all api calls multiple times)
Status codes were only defined as types,
Because we didn't intended to work on the specs testing for API that time. As we are focused on it now converting status codes to values and deriving types from there is not a big issue.
Fun fact regarding this, the status codes for getHealth were actually broken after https://github.com/ChainSafe/lodestar/pull/4994 (it always returned 200) and later fixed in https://github.com/ChainSafe/lodestar/pull/5790, which adds to my point that we didn't ensure any correctness by typing status codes.
I suggest be more diligent while stating any PR broke any behavior. #4994 didn't broke any behavior, the scope of that PR was to refactor implementation of that time to status oriented response object. The feature for the custom sync status was added later.
The whole focus of previous API refactoring cycle was to convert API responses to status code based distinguishable object. Here is the objectives defined at that time https://github.com/ChainSafe/lodestar/issues/4993
- The API specs tend to specify two key information 1) Status Code 2) Response body
- The API client implementation only resolves with the response body and skip the the other piece of information
- For endpoints which are not returning body API client resolves to undefined
- For the endpoints which are returning multiple status codes e.g. GET /eth/v1/node/health, POST /eth/v1/beacon/blocks there are no way to identify what response code was returned the API call.
And here was the proposed solution:
- Resolve API calls with the either response body or status code. When response is available we resolve with it otherwise status code. This will introduce minimum change, but could introduce some confusion the API client users.
- Create a new response structure given below and always resolves with it.
{ok: boolean, status: number, [body,result]?: T}
Using ApiError.assert was also thoughtful decision to treat errors as first class objects, otherwise avoiding those would be a one line change.
@wemeetagain was fully involved into it and agreed to that approach. I feel we are reverting back to previous state without considering why we did the earlier refactor.
I align and fully upto with following refactor:
- Consolidating route definitions into one object
- Refactor in how we initialize API clients
- SSZ response support for the API endpoints
For these I think we are disregarding the previous effort without any proper reason and not considering a flexible structure for any future use cases.
- Removing status code information from the routes definitions.
- Removing ability from API client to handle success cases
As @wemeetagain was involved in earlier refactoring and in this one, he can better decide on above points, so I would inclined towards his judgement.
I suggest be more diligent while stating any PR broke any behavior. https://github.com/ChainSafe/lodestar/pull/4994 didn't broke any behavior, the scope of that PR was to refactor implementation of that time to status oriented response object. The feature for the custom sync status was added later.
This can easily be verified by checking out the commit and querying the api while the node is syncing, it returns a 200 status code even though it should return 206. This is not hard to confirm as it literally hard coded to 200 in the handler. https://github.com/ChainSafe/lodestar/blob/460ba59b9f55afdf3d262978b96e63d7ef1f8d50/packages/api/src/beacon/server/node.ts#L23
And the server side implementation does not work, as it was based on passing down the req and res but the caller never did this.
https://github.com/ChainSafe/lodestar/blob/460ba59b9f55afdf3d262978b96e63d7ef1f8d50/packages/beacon-node/src/api/impl/node/index.ts#L105
While before that PR, the status code was set in the handler correctly, see PR https://github.com/ChainSafe/lodestar/pull/3349. Although it was quite hacky as the method return the status code as a value which is not the interface of getHealth method.
With this PR, we finally have the status code (status) as a first-class citizen and can properly set it in the server implementation.
https://github.com/ChainSafe/lodestar/blob/fd1936818f055547fcc17df26e81f996a5b9b22f/packages/beacon-node/src/api/impl/node/index.ts#L76
So from a server point of view, this is a strict improvement compared to previous (hacky) approaches by overriding handlers.
The whole focus of previous API refactoring cycle was to convert API responses to status code based distinguishable object. Here is the objectives defined at that time https://github.com/ChainSafe/lodestar/issues/4993
We are still able to handle all cases outlined in the PR, the res.status still works as before and can be accessed for error or success cases if needed.
I wanna address your remaining concerns but I am not clear about the expectation here
- Removing status code information from the routes definitions.
Can you provide an example where this is useful, generally looking at the spec, there is one success response (status 200) and a bunch of error responses (4xx, 5xx).
For success responses, there is no requirement to look at the status code, it's either res.ok=true|false, this was previously asserted by ApiError.assert as well, we never looked at the status code specifically because it's not needed for all apis with the exception of submitBlindedBlock which this PR acutally fixes (https://github.com/ChainSafe/lodestar/issues/5826) and getHealth which doesn't really change in this PR either as you can still do res.status to get the status code.
For error responses, nothing really changes either in this PR, we just have a more self-contained response object now, the error assertion is still the same !res.ok --> throw ApiError, the only difference is built-in .asserOk() vs. ApiError.assert but considering in most place of the code we now have just a single line vs. 3 lines to resolve a response, I personally like it more.
We can go down that road to specify status code like this (or similar) and check it against spec
successCodes: [200], // <-- this would be 200 for all apis besides getHealth / submitBlindedBlock
errorCodes: [404, 500]
but I don't see much value right now as we don't use status codes in any meaningful way right now, we should identify a good use case first before increasing implementation complexity and maintenance.
- Removing ability from API client to handle success cases
I don't see how this ability is removed? If anything we have less type hacks now when handling getHealth response, see my previous comment https://github.com/ChainSafe/lodestar/pull/6749#discussion_r1621221385. Can you please provide a concrete example for this, it might help me understand what you mean here
Codecov Report
Attention: Patch coverage is 92.37816% with 416 lines in your changes missing coverage. Please review.
Project coverage is 62.77%. Comparing base (
f6d3bce) to head (907f221).
Additional details and impacted files
@@ Coverage Diff @@
## unstable #6749 +/- ##
============================================
+ Coverage 62.19% 62.77% +0.58%
============================================
Files 571 580 +9
Lines 60099 61306 +1207
Branches 1978 2116 +138
============================================
+ Hits 37376 38486 +1110
- Misses 22680 22781 +101
+ Partials 43 39 -4
Metrics from feat3 in addition to what's already in https://github.com/ChainSafe/lodestar/pull/6749#issuecomment-2128847283, most notable difference if of course in the REST API latency, especially if we look at it from VC point of view as it includes latency over the wire, and not just serialization / deserialization.
Other metrics look good to me as well (no regressions), or even slightly better across the board (lower gc, eventloop lag, etc.)
:tada: This PR is included in v1.20.0 :tada: