-
-
Notifications
You must be signed in to change notification settings - Fork 460
feat: add stateless path to publishExecutionPayloadEnvelope #9401
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
base: unstable
Are you sure you want to change the base?
Changes from all commits
268607c
6d78f2f
7b5460e
edb8c8d
1f555b9
5017b92
fd38e7c
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 |
|---|---|---|
| @@ -0,0 +1,123 @@ | ||
| import {describe, expect, it} from "vitest"; | ||
| import {createChainForkConfig, defaultChainConfig} from "@lodestar/config"; | ||
| import {ssz} from "@lodestar/types"; | ||
| import {BroadcastValidation, getDefinitions} from "../../../src/beacon/routes/beacon/block.js"; | ||
| import {WireFormat} from "../../../src/utils/wireFormat.js"; | ||
|
|
||
| function lowercaseKeys<T extends Record<string, string>>(headers: T): T { | ||
| return Object.fromEntries(Object.entries(headers).map(([k, v]) => [k.toLowerCase(), v])) as T; | ||
| } | ||
|
|
||
| describe("publishExecutionPayloadEnvelope route", () => { | ||
| const config = createChainForkConfig({...defaultChainConfig, GLOAS_FORK_EPOCH: 0}); | ||
| const definitions = getDefinitions(config); | ||
| const route = definitions.publishExecutionPayloadEnvelope; | ||
|
|
||
| const signedExecutionPayloadEnvelope = ssz.gloas.SignedExecutionPayloadEnvelope.defaultValue(); | ||
|
|
||
| describe("JSON wire format", () => { | ||
| it("round-trips a bare SignedExecutionPayloadEnvelope", () => { | ||
| const written = route.req.writeReqJson({signedExecutionPayloadEnvelope}); | ||
| expect(written.body).toHaveProperty("message"); | ||
| expect(written.body).toHaveProperty("signature"); | ||
| expect(written.body).not.toHaveProperty("signed_execution_payload_envelope"); | ||
|
|
||
| const parsed = route.req.parseReqJson({ | ||
| body: written.body, | ||
| headers: lowercaseKeys(written.headers), | ||
| query: written.query ?? {}, | ||
| }); | ||
| expect(parsed.signedExecutionPayloadEnvelope).toEqual(signedExecutionPayloadEnvelope); | ||
| expect(parsed.blobs).toBeUndefined(); | ||
| expect(parsed.kzgProofs).toBeUndefined(); | ||
| }); | ||
|
|
||
| it("round-trips a SignedExecutionPayloadEnvelopeContents wrapper when blobs are supplied", () => { | ||
| const blobs = [ssz.deneb.Blob.defaultValue()]; | ||
| const kzgProofs = Array.from({length: 128}, () => ssz.deneb.KZGProof.defaultValue()); | ||
|
|
||
| const written = route.req.writeReqJson({ | ||
| signedExecutionPayloadEnvelope, | ||
| blobs, | ||
| kzgProofs, | ||
| broadcastValidation: BroadcastValidation.consensus, | ||
| }); | ||
| expect(written.body).toHaveProperty("signed_execution_payload_envelope"); | ||
| expect(written.body).toHaveProperty("kzg_proofs"); | ||
| expect(written.body).toHaveProperty("blobs"); | ||
| expect(written.query?.broadcast_validation).toBe(BroadcastValidation.consensus); | ||
|
|
||
| const parsed = route.req.parseReqJson({ | ||
| body: written.body, | ||
| headers: lowercaseKeys(written.headers), | ||
| query: written.query ?? {}, | ||
| }); | ||
| expect(parsed.signedExecutionPayloadEnvelope).toEqual(signedExecutionPayloadEnvelope); | ||
| expect(parsed.blobs).toEqual(blobs); | ||
| expect(parsed.kzgProofs).toEqual(kzgProofs); | ||
| expect(parsed.broadcastValidation).toBe(BroadcastValidation.consensus); | ||
| }); | ||
| }); | ||
|
|
||
| describe("SSZ wire format", () => { | ||
| it("round-trips a bare SignedExecutionPayloadEnvelope", () => { | ||
| const written = route.req.writeReqSsz({signedExecutionPayloadEnvelope}); | ||
| const parsed = route.req.parseReqSsz({ | ||
| body: written.body, | ||
| headers: lowercaseKeys(written.headers), | ||
| query: written.query ?? {}, | ||
| }); | ||
| expect(parsed.signedExecutionPayloadEnvelope).toEqual(signedExecutionPayloadEnvelope); | ||
| expect(parsed.blobs).toBeUndefined(); | ||
| expect(parsed.kzgProofs).toBeUndefined(); | ||
| }); | ||
|
|
||
| it("round-trips a SignedExecutionPayloadEnvelopeContents wrapper", () => { | ||
| const blobs = [ssz.deneb.Blob.defaultValue()]; | ||
| const kzgProofs = Array.from({length: 128}, () => ssz.deneb.KZGProof.defaultValue()); | ||
|
|
||
| const written = route.req.writeReqSsz({signedExecutionPayloadEnvelope, blobs, kzgProofs}); | ||
| const parsed = route.req.parseReqSsz({ | ||
| body: written.body, | ||
| headers: lowercaseKeys(written.headers), | ||
| query: written.query ?? {}, | ||
| }); | ||
| expect(parsed.signedExecutionPayloadEnvelope).toEqual(signedExecutionPayloadEnvelope); | ||
| expect(parsed.blobs).toEqual(blobs); | ||
| expect(parsed.kzgProofs).toEqual(kzgProofs); | ||
| }); | ||
| }); | ||
|
|
||
| it("uses SSZ as the default request wire format", () => { | ||
| expect(route.init?.requestWireFormat).toBe(WireFormat.ssz); | ||
| }); | ||
|
|
||
| describe("partial blobs/kzgProofs rejection", () => { | ||
| const blobs = [ssz.deneb.Blob.defaultValue()]; | ||
| const kzgProofs = Array.from({length: 128}, () => ssz.deneb.KZGProof.defaultValue()); | ||
|
|
||
| it("writeReqJson throws when only blobs are supplied", () => { | ||
| expect(() => route.req.writeReqJson({signedExecutionPayloadEnvelope, blobs})).toThrow( | ||
| /blobs and kzgProofs must both be supplied/ | ||
| ); | ||
| }); | ||
|
|
||
| it("writeReqJson throws when only kzgProofs are supplied", () => { | ||
| expect(() => route.req.writeReqJson({signedExecutionPayloadEnvelope, kzgProofs})).toThrow( | ||
| /blobs and kzgProofs must both be supplied/ | ||
| ); | ||
| }); | ||
|
|
||
| it("writeReqSsz throws when only blobs are supplied", () => { | ||
| expect(() => route.req.writeReqSsz({signedExecutionPayloadEnvelope, blobs})).toThrow( | ||
| /blobs and kzgProofs must both be supplied/ | ||
| ); | ||
| }); | ||
|
|
||
| it("writeReqSsz throws when only kzgProofs are supplied", () => { | ||
| expect(() => route.req.writeReqSsz({signedExecutionPayloadEnvelope, kzgProofs})).toThrow( | ||
| /blobs and kzgProofs must both be supplied/ | ||
| ); | ||
| }); | ||
| }); | ||
| }); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -49,6 +49,7 @@ import { | |
| ProduceFullFulu, | ||
| ProduceFullGloas, | ||
| } from "../../../../chain/produceBlock/index.js"; | ||
| import {validateCellsAndKzgCommitments} from "../../../../chain/produceBlock/validateBlobsAndKzgCommitments.js"; | ||
| import {validateGossipBlock} from "../../../../chain/validation/block.js"; | ||
| import {validateApiExecutionPayloadBid} from "../../../../chain/validation/executionPayloadBid.js"; | ||
| import {validateApiExecutionPayloadEnvelope} from "../../../../chain/validation/executionPayloadEnvelope.js"; | ||
|
|
@@ -665,7 +666,9 @@ export function getBeaconBlockApi({ | |
| await publishBlock(args, context, opts); | ||
| }, | ||
|
|
||
| async publishExecutionPayloadEnvelope({signedExecutionPayloadEnvelope}) { | ||
| async publishExecutionPayloadEnvelope({signedExecutionPayloadEnvelope, blobs, kzgProofs, broadcastValidation}) { | ||
| // TODO GLOAS: honor broadcastValidation (gossip|consensus|consensus_and_equivocation) per beacon-APIs PR #580 | ||
| void broadcastValidation; | ||
| const seenTimestampSec = Date.now() / 1000; | ||
| const envelope = signedExecutionPayloadEnvelope.message; | ||
| const slot = envelope.payload.slotNumber; | ||
|
|
@@ -688,11 +691,51 @@ export function getBeaconBlockApi({ | |
|
|
||
| await validateApiExecutionPayloadEnvelope(chain, signedExecutionPayloadEnvelope); | ||
|
|
||
| // TODO GLOAS: if block and payload are submitted in parallel, payloadInput may not yet exist. | ||
| // A queuing mechanism is needed to handle this case. See https://github.com/ChainSafe/lodestar/issues/8915 | ||
| const payloadInput = chain.seenPayloadEnvelopeInputCache.get(blockRootHex); | ||
| if (!payloadInput) { | ||
| throw new ApiError(404, `PayloadEnvelopeInput not found for block root ${blockRootHex}`); | ||
|
Comment on lines
+696
to
+698
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.
Move the Useful? React with 👍 / 👎. |
||
| } | ||
|
|
||
| if ((blobs === undefined) !== (kzgProofs === undefined)) { | ||
| throw new ApiError(400, "blobs and kzgProofs must both be supplied or both omitted"); | ||
| } | ||
| const hasSuppliedBlobs = blobs !== undefined && kzgProofs !== undefined; | ||
|
ensi321 marked this conversation as resolved.
|
||
| const isSelfBuild = envelope.builderIndex === BUILDER_INDEX_SELF_BUILD; | ||
| let dataColumnSidecars: gloas.DataColumnSidecar[] = []; | ||
|
|
||
| if (isSelfBuild) { | ||
| // For self-builds, construct and publish data column sidecars from cached block production data | ||
| if (hasSuppliedBlobs) { | ||
| const expectedBlobs = payloadInput.getBlobKzgCommitments().length; | ||
| if (blobs.length !== expectedBlobs) { | ||
| throw new ApiError(400, `Expected ${expectedBlobs} blobs to match bid kzg_commitments, got ${blobs.length}`); | ||
| } | ||
| if (kzgProofs.length !== blobs.length * NUMBER_OF_COLUMNS) { | ||
| throw new ApiError( | ||
| 400, | ||
| `Expected ${blobs.length * NUMBER_OF_COLUMNS} kzg_proofs for ${blobs.length} blobs, got ${kzgProofs.length}` | ||
| ); | ||
| } | ||
| if (blobs.length > 0) { | ||
| const timer = metrics?.peerDas.dataColumnSidecarComputationTime.startTimer(); | ||
| const cells = blobs.map((blob) => kzg.computeCells(blob)); | ||
|
ensi321 marked this conversation as resolved.
|
||
| try { | ||
| await validateCellsAndKzgCommitments(payloadInput.getBlobKzgCommitments(), kzgProofs, cells); | ||
| } catch (e) { | ||
| throw new ApiError( | ||
| 400, | ||
| `Invalid supplied blobs/kzg_proofs against bid kzg_commitments: ${(e as Error).message}` | ||
| ); | ||
| } | ||
| const cellsAndProofs = cells.map((rowCells, rowIndex) => ({ | ||
| cells: rowCells, | ||
| proofs: kzgProofs.slice(rowIndex * NUMBER_OF_COLUMNS, (rowIndex + 1) * NUMBER_OF_COLUMNS), | ||
| })); | ||
| dataColumnSidecars = getGloasDataColumnSidecars(slot, envelope.beaconBlockRoot, cellsAndProofs); | ||
| timer?.(); | ||
| } | ||
| } else if (isSelfBuild) { | ||
| // Stateful self-build path: reconstruct data column sidecars from cached cells + proofs. | ||
| const cachedResult = chain.blockProductionCache.get(blockRootHex) as ProduceFullGloas | undefined; | ||
| if (cachedResult === undefined) { | ||
| throw new ApiError(404, `No cached block production result found for block root ${blockRootHex}`); | ||
|
|
@@ -717,8 +760,6 @@ export function getBeaconBlockApi({ | |
| dataColumnSidecars = getGloasDataColumnSidecars(slot, envelope.beaconBlockRoot, cellsAndProofs); | ||
| timer?.(); | ||
| } | ||
| } else { | ||
| // TODO GLOAS: will this api be used by builders or only for self-building? | ||
| } | ||
|
|
||
| // If called near a slot boundary (e.g. late in slot N-1), hold briefly so gossip aligns with slot N. | ||
|
|
@@ -727,13 +768,6 @@ export function getBeaconBlockApi({ | |
| await sleep(msToBlockSlot); | ||
| } | ||
|
|
||
| // TODO GLOAS: if block and payload are submitted in parallel, payloadInput may not yet exist. | ||
| // A queuing mechanism is needed to handle this case. See https://github.com/ChainSafe/lodestar/issues/8915 | ||
| const payloadInput = chain.seenPayloadEnvelopeInputCache.get(blockRootHex); | ||
| if (!payloadInput) { | ||
| throw new ApiError(404, `PayloadEnvelopeInput not found for block root ${blockRootHex}`); | ||
| } | ||
|
|
||
| payloadInput.addPayloadEnvelope({ | ||
| envelope: signedExecutionPayloadEnvelope, | ||
| source: PayloadEnvelopeInputSource.api, | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.
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.
this doesn't make any sense, we need a metadata header to differentiate this, the spec is not complete, left a comment here ethereum/beacon-APIs#580 (comment), I think we should push for introducing a
BlindedExecutionPayloadEnvelopefor the stateful flowUh oh!
There was an error while loading. Please reload this page.
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.
I agree. I believe other clients implemented this way. The original plan was we just need something transient so we can test external bids with buildoor in devnet 4 which is now dead.