-
-
Notifications
You must be signed in to change notification settings - Fork 289
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
fix: make multiple api errors spec compliant #6479
Changes from all commits
55f4cd4
16611c8
a86e85a
0ad7c21
e32fb50
0acfc71
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
|
@@ -15,6 +15,7 @@ import { | |||||
SyncCommitteeError, | ||||||
} from "../../../../chain/errors/index.js"; | ||||||
import {validateGossipFnRetryUnknownRoot} from "../../../../network/processor/gossipHandlers.js"; | ||||||
import {IndexedError} from "../../errors.js"; | ||||||
|
||||||
export function getBeaconPoolApi({ | ||||||
chain, | ||||||
|
@@ -52,7 +53,7 @@ export function getBeaconPoolApi({ | |||||
|
||||||
async submitPoolAttestations(attestations) { | ||||||
const seenTimestampSec = Date.now() / 1000; | ||||||
const errors: Error[] = []; | ||||||
const errors: {index: number; error: Error}[] = []; | ||||||
|
||||||
await Promise.all( | ||||||
attestations.map(async (attestation, i) => { | ||||||
|
@@ -91,7 +92,7 @@ export function getBeaconPoolApi({ | |||||
return; | ||||||
} | ||||||
|
||||||
errors.push(e as Error); | ||||||
errors.push({index: i, error: e as Error}); | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Since this is not a plain array of errors anymore, we could consider just tracking failures here directly as we log the errors (incl. stack trace) separately anyways
Suggested change
|
||||||
logger.error(`Error on submitPoolAttestations [${i}]`, logCtx, e as Error); | ||||||
if (e instanceof AttestationError && e.action === GossipAction.REJECT) { | ||||||
chain.persistInvalidSszValue(ssz.phase0.Attestation, attestation, "api_reject"); | ||||||
|
@@ -100,10 +101,8 @@ export function getBeaconPoolApi({ | |||||
}) | ||||||
); | ||||||
|
||||||
if (errors.length > 1) { | ||||||
throw Error("Multiple errors on submitPoolAttestations\n" + errors.map((e) => e.message).join("\n")); | ||||||
} else if (errors.length === 1) { | ||||||
throw errors[0]; | ||||||
if (errors.length > 0) { | ||||||
throw new IndexedError("Some errors submitting attestations", errors); | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The error message is not really correct, there was an error(s) processing / validating the attestations, not submitting them. Regarding the "some" I would probably just leave it out as per spec if one attestation failed validation it is already considered an error
We could use the term "validating" or "processing" but since we do additional things like publishing the attestation to the network which can fail as well I think "processing" is better
Suggested change
|
||||||
} | ||||||
}, | ||||||
|
||||||
|
@@ -127,7 +126,7 @@ export function getBeaconPoolApi({ | |||||
}, | ||||||
|
||||||
async submitPoolBlsToExecutionChange(blsToExecutionChanges) { | ||||||
const errors: Error[] = []; | ||||||
const errors: {index: number; error: Error}[] = []; | ||||||
|
||||||
await Promise.all( | ||||||
blsToExecutionChanges.map(async (blsToExecutionChange, i) => { | ||||||
|
@@ -143,7 +142,7 @@ export function getBeaconPoolApi({ | |||||
await network.publishBlsToExecutionChange(blsToExecutionChange); | ||||||
} | ||||||
} catch (e) { | ||||||
errors.push(e as Error); | ||||||
errors.push({index: i, error: e as Error}); | ||||||
logger.error( | ||||||
`Error on submitPoolBlsToExecutionChange [${i}]`, | ||||||
{validatorIndex: blsToExecutionChange.message.validatorIndex}, | ||||||
|
@@ -153,10 +152,8 @@ export function getBeaconPoolApi({ | |||||
}) | ||||||
); | ||||||
|
||||||
if (errors.length > 1) { | ||||||
throw Error("Multiple errors on submitPoolBlsToExecutionChange\n" + errors.map((e) => e.message).join("\n")); | ||||||
} else if (errors.length === 1) { | ||||||
throw errors[0]; | ||||||
if (errors.length > 0) { | ||||||
throw new IndexedError("Some errors submitting BLS to execution change", errors); | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should be plural to match other error messages, looks like in ethereum/beacon-APIs#279 it was missed to change the operationId to plural as well when it was changed to a batch API
Suggested change
|
||||||
} | ||||||
}, | ||||||
|
||||||
|
@@ -180,7 +177,7 @@ export function getBeaconPoolApi({ | |||||
// TODO: Fetch states at signature slots | ||||||
const state = chain.getHeadState(); | ||||||
|
||||||
const errors: Error[] = []; | ||||||
const errors: {index: number; error: Error}[] = []; | ||||||
|
||||||
await Promise.all( | ||||||
signatures.map(async (signature, i) => { | ||||||
|
@@ -220,7 +217,7 @@ export function getBeaconPoolApi({ | |||||
return; | ||||||
} | ||||||
|
||||||
errors.push(e as Error); | ||||||
errors.push({index: i, error: e as Error}); | ||||||
logger.debug( | ||||||
`Error on submitPoolSyncCommitteeSignatures [${i}]`, | ||||||
{slot: signature.slot, validatorIndex: signature.validatorIndex}, | ||||||
|
@@ -233,10 +230,8 @@ export function getBeaconPoolApi({ | |||||
}) | ||||||
); | ||||||
|
||||||
if (errors.length > 1) { | ||||||
throw Error("Multiple errors on submitPoolSyncCommitteeSignatures\n" + errors.map((e) => e.message).join("\n")); | ||||||
} else if (errors.length === 1) { | ||||||
throw errors[0]; | ||||||
if (errors.length > 0) { | ||||||
throw new IndexedError("Some errors submitting sync committee signatures", errors); | ||||||
} | ||||||
}, | ||||||
}; | ||||||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -35,3 +35,17 @@ export class OnlySupportedByDVT extends ApiError { | |
super(501, "Only supported by distributed validator middleware clients"); | ||
} | ||
} | ||
|
||
export class IndexedError extends ApiError { | ||
failures: {index: number; message: string}[]; | ||
|
||
constructor(message: string, errors: {index: number; error: Error}[]) { | ||
super(400, message); | ||
|
||
const failures = []; | ||
for (const {index, error} of errors) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. As noted in other comment, we could just pass failures in as is to avoid this conversion from array of errors to correct failures format. Slight UX improvement we should consider is sorting the failures by index as order is not guaranteed due to the async processing. |
||
failures.push({index: index, message: error.message}); | ||
} | ||
this.failures = failures; | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -5,7 +5,7 @@ import bearerAuthPlugin from "@fastify/bearer-auth"; | |
import {RouteConfig} from "@lodestar/api/beacon/server"; | ||
import {ErrorAborted, Gauge, Histogram, Logger} from "@lodestar/utils"; | ||
import {isLocalhostIP} from "../../util/ip.js"; | ||
import {ApiError, NodeIsSyncing} from "../impl/errors.js"; | ||
import {ApiError, IndexedError, NodeIsSyncing} from "../impl/errors.js"; | ||
import {HttpActiveSocketsTracker, SocketMetrics} from "./activeSockets.js"; | ||
|
||
export type RestApiServerOpts = { | ||
|
@@ -66,6 +66,14 @@ export class RestApiServer { | |
server.setErrorHandler((err, req, res) => { | ||
if (err.validation) { | ||
void res.status(400).send(err.validation); | ||
} else if (err instanceof IndexedError) { | ||
// api's returning IndexedError need to formatted in a certain way | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This comment is applicable to any error just that we don't do it for other error responses (yet) |
||
const body = { | ||
code: err.statusCode, | ||
message: err.message, | ||
failures: err.failures, | ||
}; | ||
void res.status(err.statusCode).send(body); | ||
} else { | ||
// Convert our custom ApiError into status code | ||
const statusCode = err instanceof ApiError ? err.statusCode : 500; | ||
|
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
@@ -0,0 +1,95 @@ | ||||||
import {describe, beforeAll, afterAll, it, expect} from "vitest"; | ||||||
import {createBeaconConfig} from "@lodestar/config"; | ||||||
import {chainConfig as chainConfigDef} from "@lodestar/config/default"; | ||||||
import {Api, getClient} from "@lodestar/api"; | ||||||
import {ssz} from "@lodestar/types"; | ||||||
import {LogLevel, testLogger} from "../../../../../utils/logger.js"; | ||||||
import {getDevBeaconNode} from "../../../../../utils/node/beacon.js"; | ||||||
import {BeaconNode} from "../../../../../../src/node/nodejs.js"; | ||||||
|
||||||
describe("beacon pool api", function () { | ||||||
const restPort = 9596; | ||||||
const config = createBeaconConfig(chainConfigDef, Buffer.alloc(32, 0xaa)); | ||||||
const validatorCount = 512; | ||||||
|
||||||
let bn: BeaconNode; | ||||||
let client: Api["beacon"]; | ||||||
|
||||||
beforeAll(async () => { | ||||||
bn = await getDevBeaconNode({ | ||||||
params: chainConfigDef, | ||||||
options: { | ||||||
sync: {isSingleNode: true}, | ||||||
network: {allowPublishToZeroPeers: true}, | ||||||
api: { | ||||||
rest: { | ||||||
enabled: true, | ||||||
port: restPort, | ||||||
}, | ||||||
}, | ||||||
chain: {blsVerifyAllMainThread: true}, | ||||||
}, | ||||||
validatorCount, | ||||||
logger: testLogger("Node-A", {level: LogLevel.info}), | ||||||
}); | ||||||
client = getClient({baseUrl: `http://127.0.0.1:${restPort}`}, {config}).beacon; | ||||||
}); | ||||||
|
||||||
afterAll(async () => { | ||||||
await bn.close(); | ||||||
}); | ||||||
|
||||||
describe("submitPoolAttestations", () => { | ||||||
it("should return correctly formatted errors responses", async () => { | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. could be more descriptive, maybe something like?
Suggested change
|
||||||
const attestations = [ssz.phase0.Attestation.defaultValue()]; | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. might be better to test with at least > 2 items, or have two tests cases, single item and multiple items |
||||||
const res = await client.submitPoolAttestations(attestations); | ||||||
|
||||||
expect(res.ok).toBe(false); | ||||||
expect(res.status).toBe(400); | ||||||
|
||||||
const expectedErrorBody = { | ||||||
code: 400, | ||||||
message: "Some errors submitting attestations", | ||||||
failures: [{index: 0, message: "ATTESTATION_ERROR_NOT_EXACTLY_ONE_AGGREGATION_BIT_SET"}], | ||||||
}; | ||||||
const expectedErrorMessage = `Bad Request: ${JSON.stringify(expectedErrorBody)}`; | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Need to come up with a human readable format for the error message, would have to give this a try myself with > 10 and see how it reads in the logs but I'd imagine having the stringified json body as message is not that great. Could just update |
||||||
expect(res.error?.message).toEqual(expectedErrorMessage); | ||||||
}); | ||||||
}); | ||||||
|
||||||
describe("submitPoolBlsToExecutionChange", () => { | ||||||
it("should return correctly formatted errors responses", async () => { | ||||||
const blsToExecutionChanges = [ssz.capella.SignedBLSToExecutionChange.defaultValue()]; | ||||||
const res = await client.submitPoolBlsToExecutionChange(blsToExecutionChanges); | ||||||
|
||||||
expect(res.ok).toBe(false); | ||||||
expect(res.status).toBe(400); | ||||||
|
||||||
const expectedErrorBody = { | ||||||
code: 400, | ||||||
message: "Some errors submitting BLS to execution change", | ||||||
failures: [{index: 0, message: "BLS_TO_EXECUTION_CHANGE_ERROR_INVALID"}], | ||||||
}; | ||||||
const expectedErrorMessage = `Bad Request: ${JSON.stringify(expectedErrorBody)}`; | ||||||
expect(res.error?.message).toEqual(expectedErrorMessage); | ||||||
}); | ||||||
}); | ||||||
|
||||||
describe("submitPoolSyncCommitteeSignatures", () => { | ||||||
it("should return correctly formatted errors responses", async () => { | ||||||
const signatures = [ssz.altair.SyncCommitteeMessage.defaultValue()]; | ||||||
const res = await client.submitPoolSyncCommitteeSignatures(signatures); | ||||||
|
||||||
expect(res.ok).toBe(false); | ||||||
expect(res.status).toBe(400); | ||||||
|
||||||
const expectedErrorBody = { | ||||||
code: 400, | ||||||
message: "Some errors submitting sync committee signatures", | ||||||
failures: [{index: 0, message: "Empty SyncCommitteeCache"}], | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. These tests will likely break in the future, I am not sure it is the best approach to check for the exact messages here. What we really wanna make sure is that the format is correct and that we return a error for each failure. |
||||||
}; | ||||||
const expectedErrorMessage = `Bad Request: ${JSON.stringify(expectedErrorBody)}`; | ||||||
expect(res.error?.message).toEqual(expectedErrorMessage); | ||||||
}); | ||||||
}); | ||||||
}); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We should probably add a type to the API package to make sure we notice when the error format ever changes between server / client