Skip to content

Commit

Permalink
test: update beacon api spec to v2.5.0 (#6354)
Browse files Browse the repository at this point in the history
* fix: update events test data

* fix: reflect typo fix

* fix: remove workarounds

* chore: remove now irrelevan dropOneOf option

* Fix parent block number in SSE payload attributes

* Tests pass against latest spec version

* Update comment

* Lint

* Update beacon api spec to v2.5.0

---------

Co-authored-by: Nico Flaig <nflaig@protonmail.com>
  • Loading branch information
jeluard and nflaig authored Feb 27, 2024
1 parent f62bc13 commit 645d491
Show file tree
Hide file tree
Showing 4 changed files with 45 additions and 75 deletions.
34 changes: 8 additions & 26 deletions packages/api/test/unit/beacon/oapiSpec.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ import {testData as validatorTestData} from "./testData/validator.js";
// eslint-disable-next-line @typescript-eslint/naming-convention
const __dirname = path.dirname(fileURLToPath(import.meta.url));

const version = "v2.4.2";
const version = "v2.5.0";
const openApiFile: OpenApiFile = {
url: `https://github.com/ethereum/beacon-APIs/releases/download/${version}/beacon-node-oapi.json`,
filepath: path.join(__dirname, "../../../oapi-schemas/beacon-node-oapi.json"),
Expand Down Expand Up @@ -89,11 +89,14 @@ const ignoredOperations = [
/* https://github.com/ChainSafe/lodestar/issues/5694 */
"getSyncCommitteeRewards",
"getAttestationsRewards",
/* https://github.com/ChainSafe/lodestar/issues/6058 */
"postStateValidators",
"postStateValidatorBalances",
"getDepositSnapshot", // Won't fix for now, see https://github.com/ChainSafe/lodestar/issues/5697
"getBlindedBlock", // https://github.com/ChainSafe/lodestar/issues/5699
"getNextWithdrawals", // https://github.com/ChainSafe/lodestar/issues/5696
"getDebugForkChoice", // https://github.com/ChainSafe/lodestar/issues/5700
/* https://github.com/ChainSafe/lodestar/issues/6080 */
/* Ensure operationId matches spec value, blocked by https://github.com/ChainSafe/lodestar/pull/6080 */
"getLightClientBootstrap",
"getLightClientUpdatesByRange",
"getLightClientFinalityUpdate",
Expand Down Expand Up @@ -145,37 +148,16 @@ runTestCheckAgainstSpec(
reqSerializers,
returnTypes,
testDatas,
{
// TODO: Investigate why schema validation fails otherwise (see https://github.com/ChainSafe/lodestar/issues/6187)
routesDropOneOf: [
"produceBlockV2",
"produceBlockV3",
"produceBlindedBlock",
"publishBlindedBlock",
"publishBlindedBlockV2",
],
},
ignoredOperations,
ignoredProperties
);

const ignoredTopics = [
/*
https://github.com/ChainSafe/lodestar/issues/6167
eventTestData[bls_to_execution_change] does not match spec's example
https://github.com/ChainSafe/lodestar/issues/6470
topic block_gossip not implemented
*/
"bls_to_execution_change",
/*
https://github.com/ChainSafe/lodestar/issues/6170
Error: Invalid slot=0 fork=phase0 for lightclient fork types
*/
"light_client_finality_update",
"light_client_optimistic_update",
/*
https://github.com/ethereum/beacon-APIs/pull/379
SyntaxError: Unexpected non-whitespace character after JSON at position 629 (line 1 column 630)
*/
"payload_attributes",
"block_gossip",
];

// eventstream types are defined as comments in the description of "examples".
Expand Down
36 changes: 31 additions & 5 deletions packages/api/test/unit/beacon/testData/events.ts
Original file line number Diff line number Diff line change
Expand Up @@ -114,8 +114,8 @@ export const eventTestData: EventData = {
message: {
validator_index: "1",
from_bls_pubkey:
"0x9048a71944feba4695ef870dfb5745c934d81c5efd934c0250a12942fcc2a2dfd6b20d53314379dec7aae5ca5fe9e9c4",
to_execution_address: "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
"0x933ad9491b62059dd065b560d256d8957a8c402cc6e8d8ee7290ae11e8f7329267a8811c397529dac52ae1342ba58c95",
to_execution_address: "0x9Be8d619c56699667c1feDCD15f6b14D8B067F72",
},
signature:
"0x1b66ac1fb663c9bc59509846d6ec05345bd908eda73e670af888da41af171505cc411d61252fb6cb3fa0017b679f8bb2305b26a285fa2737f175668d0dff91cc1b66ac1fb663c9bc59509846d6ec05345bd908eda73e670af888da41af171505",
Expand Down Expand Up @@ -213,8 +213,34 @@ export const eventTestData: EventData = {
}),
},
[EventType.payloadAttributes]: {
version: ForkName.bellatrix,
data: ssz.bellatrix.SSEPayloadAttributes.defaultValue(),
version: ForkName.capella,
data: ssz.capella.SSEPayloadAttributes.fromJson({
proposer_index: "123",
proposal_slot: "10",
parent_block_number: "9",
parent_block_root: "0xcf8e0d4e9587369b2301d0790347320302cc0943d5a1884560367e8208d920f2",
parent_block_hash: "0x9a2fefd2fdb57f74993c7780ea5b9030d2897b615b89f808011ca5aebed54eaf",
payload_attributes: {
timestamp: "123456",
prev_randao: "0xcf8e0d4e9587369b2301d0790347320302cc0943d5a1884560367e8208d920f2",
suggested_fee_recipient: "0x0000000000000000000000000000000000000000",
withdrawals: [
{
index: "5",
validator_index: "10",
address: "0x0000000000000000000000000000000000000000",
amount: "15640",
},
],
},
}),
},
[EventType.blobSidecar]: blobSidecarSSE.defaultValue(),
[EventType.blobSidecar]: blobSidecarSSE.fromJson({
block_root: "0xcf8e0d4e9587369b2301d0790347320302cc0943d5a1884560367e8208d920f2",
index: "1",
kzg_commitment:
"0x1b66ac1fb663c9bc59509846d6ec05345bd908eda73e670af888da41af171505cc411d61252fb6cb3fa0017b679f8bb2305b26a285fa2737f175668d0dff91cc1b66ac1fb663c9bc59509846d6ec05345bd908eda73e670af888da41af171505",
slot: "1",
versioned_hash: "0xcf8e0d4e9587369b2301d0790347320302cc0943d5a1884560367e8208d920f2",
}),
};
26 changes: 2 additions & 24 deletions packages/api/test/utils/checkAgainstSpec.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,11 @@
import Ajv, {ErrorObject} from "ajv";
import {expect, describe, beforeAll, it} from "vitest";
import {ReqGeneric, ReqSerializer, ReturnTypes, RouteDef} from "../../src/utils/types.js";
import {applyRecursively, JsonSchema, OpenApiJson, parseOpenApiSpec, ParseOpenApiSpecOpts} from "./parseOpenApiSpec.js";
import {applyRecursively, JsonSchema, OpenApiJson, parseOpenApiSpec} from "./parseOpenApiSpec.js";
import {GenericServerTestCases} from "./genericServerTest.js";

const ajv = new Ajv({
strict: true,
strictTypes: false, // TODO Enable once beacon-APIs is fixed. See https://github.com/ChainSafe/lodestar/issues/6206
allErrors: true,
});

// Ensure embedded schema 'example' do not fail validation
Expand Down Expand Up @@ -68,11 +66,10 @@ export function runTestCheckAgainstSpec(
reqSerializers: Record<string, ReqSerializer<any, any>>,
returnTypes: Record<string, ReturnTypes<any>[string]>,
testDatas: Record<string, GenericServerTestCases<any>[string]>,
opts?: ParseOpenApiSpecOpts,
ignoredOperations: string[] = [],
ignoredProperties: Record<string, IgnoredProperty> = {}
): void {
const openApiSpec = parseOpenApiSpec(openApiJson, opts);
const openApiSpec = parseOpenApiSpec(openApiJson);

for (const [operationId, routeSpec] of openApiSpec.entries()) {
const isIgnored = ignoredOperations.some((id) => id === operationId);
Expand Down Expand Up @@ -106,15 +103,6 @@ export function runTestCheckAgainstSpec(
it(`${operationId}_request`, function () {
const reqJson = reqSerializers[routeId].writeReq(...(testData.args as [never])) as unknown;

if (operationId === "publishBlock" || operationId === "publishBlindedBlock") {
// For some reason AJV invalidates valid blocks if multiple forks are defined with oneOf
// `.data - should match exactly one schema in oneOf`
// Dropping all definitions except (phase0) pases the validation
if (routeSpec.requestSchema?.oneOf) {
routeSpec.requestSchema = routeSpec.requestSchema?.oneOf[0];
}
}

// Stringify param and query to simulate rendering in HTTP query
// TODO: Review conversions in fastify and other servers
stringifyProperties((reqJson as ReqGeneric).params ?? {});
Expand All @@ -137,16 +125,6 @@ export function runTestCheckAgainstSpec(
it(`${operationId}_response`, function () {
const resJson = returnTypes[operationId].toJson(testData.res as any);

// Patch for getBlockV2
if (operationId === "getBlockV2" || operationId === "getStateV2") {
// For some reason AJV invalidates valid blocks if multiple forks are defined with oneOf
// `.data - should match exactly one schema in oneOf`
// Dropping all definitions except (phase0) pases the validation
if (responseOkSchema.properties?.data.oneOf) {
responseOkSchema.properties.data = responseOkSchema.properties.data.oneOf[1];
}
}

const ignoredProperties = ignoredProperty?.response;
if (ignoredProperties) {
// Remove ignored properties from schema validation
Expand Down
24 changes: 4 additions & 20 deletions packages/api/test/utils/parseOpenApiSpec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,23 +82,17 @@ enum ContentType {
json = "application/json",
}

export type ParseOpenApiSpecOpts = {
routesDropOneOf?: string[];
};

export function parseOpenApiSpec(openApiJson: OpenApiJson, opts?: ParseOpenApiSpecOpts): Map<OperationId, RouteSpec> {
export function parseOpenApiSpec(openApiJson: OpenApiJson): Map<OperationId, RouteSpec> {
const routes = new Map<OperationId, RouteSpec>();

for (const [routeUrl, routesByMethod] of Object.entries(openApiJson.paths)) {
for (const [httpMethod, routeDefinition] of Object.entries(routesByMethod)) {
const responseOkSchema = routeDefinition.responses[StatusCode.ok]?.content?.[ContentType.json]?.schema;

const dropOneOf = opts?.routesDropOneOf?.includes(routeDefinition.operationId);

// Force all properties to have required, else ajv won't validate missing properties
if (responseOkSchema) {
try {
preprocessSchema(responseOkSchema, {dropOneOf});
preprocessSchema(responseOkSchema);
} catch (e) {
// eslint-disable-next-line no-console
console.log(responseOkSchema);
Expand All @@ -107,7 +101,7 @@ export function parseOpenApiSpec(openApiJson: OpenApiJson, opts?: ParseOpenApiSp
}

const requestSchema = buildReqSchema(routeDefinition);
preprocessSchema(requestSchema, {dropOneOf});
preprocessSchema(requestSchema);

routes.set(routeDefinition.operationId, {
url: routeUrl,
Expand All @@ -121,7 +115,7 @@ export function parseOpenApiSpec(openApiJson: OpenApiJson, opts?: ParseOpenApiSp
return routes;
}

function preprocessSchema(schema: JsonSchema, opts?: {dropOneOf?: boolean}): void {
function preprocessSchema(schema: JsonSchema): void {
// Require all properties
applyRecursively(schema, (obj) => {
if (obj.type === "object" && obj.properties && !obj.required) {
Expand All @@ -141,16 +135,6 @@ function preprocessSchema(schema: JsonSchema, opts?: {dropOneOf?: boolean}): voi
}
});

if (opts?.dropOneOf) {
// Pick single oneOf, AJV has trouble validating against blocks and states
applyRecursively(schema, (obj) => {
if (obj.oneOf) {
// splice(1) = mutate array in place to drop all items after index 1 (included)
obj.oneOf.splice(1);
}
});
}

// Remove non-intersecting allOf enum
applyRecursively(schema, (obj) => {
if (obj.allOf && obj.allOf.every((s) => s.enum)) {
Expand Down

0 comments on commit 645d491

Please sign in to comment.