Skip to content

Commit 11f6d0b

Browse files
authored
Merge 88a406a into 19b723c
2 parents 19b723c + 88a406a commit 11f6d0b

File tree

8 files changed

+244
-72
lines changed

8 files changed

+244
-72
lines changed

packages/api/src/beacon/routes/beacon/block.ts

+66
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,13 @@ export type BlockHeaderResponse = {
4444
header: phase0.SignedBeaconBlockHeader;
4545
};
4646

47+
export enum BroadcastValidation {
48+
none = "none",
49+
gossip = "gossip",
50+
consensus = "consensus",
51+
consensusAndEquivocation = "consensus_and_equivocation",
52+
}
53+
4754
export type Api = {
4855
/**
4956
* Get block
@@ -167,6 +174,20 @@ export type Api = {
167174
HttpStatusCode.BAD_REQUEST | HttpStatusCode.SERVICE_UNAVAILABLE
168175
>
169176
>;
177+
178+
publishBlockV2(
179+
blockOrContents: allForks.SignedBeaconBlock | SignedBlockContents,
180+
opts: {broadcastValidation?: BroadcastValidation}
181+
): Promise<
182+
ApiClientResponse<
183+
{
184+
[HttpStatusCode.OK]: void;
185+
[HttpStatusCode.ACCEPTED]: void;
186+
},
187+
HttpStatusCode.BAD_REQUEST | HttpStatusCode.SERVICE_UNAVAILABLE
188+
>
189+
>;
190+
170191
/**
171192
* Publish a signed blinded block by submitting it to the mev relay and patching in the block
172193
* transactions beacon node gets in response.
@@ -180,6 +201,19 @@ export type Api = {
180201
HttpStatusCode.BAD_REQUEST | HttpStatusCode.SERVICE_UNAVAILABLE
181202
>
182203
>;
204+
205+
publishBlindedBlockV2(
206+
blindedBlockOrContents: allForks.SignedBlindedBeaconBlock | SignedBlindedBlockContents,
207+
opts: {broadcastValidation?: BroadcastValidation}
208+
): Promise<
209+
ApiClientResponse<
210+
{
211+
[HttpStatusCode.OK]: void;
212+
[HttpStatusCode.ACCEPTED]: void;
213+
},
214+
HttpStatusCode.BAD_REQUEST | HttpStatusCode.SERVICE_UNAVAILABLE
215+
>
216+
>;
183217
/**
184218
* Get block BlobSidecar
185219
* Retrieves BlobSidecar included in requested block.
@@ -204,7 +238,9 @@ export const routesData: RoutesData<Api> = {
204238
getBlockHeaders: {url: "/eth/v1/beacon/headers", method: "GET"},
205239
getBlockRoot: {url: "/eth/v1/beacon/blocks/{block_id}/root", method: "GET"},
206240
publishBlock: {url: "/eth/v1/beacon/blocks", method: "POST"},
241+
publishBlockV2: {url: "/eth/v2/beacon/blocks", method: "POST"},
207242
publishBlindedBlock: {url: "/eth/v1/beacon/blinded_blocks", method: "POST"},
243+
publishBlindedBlockV2: {url: "/eth/v2/beacon/blinded_blocks", method: "POST"},
208244
getBlobSidecars: {url: "/eth/v1/beacon/blob_sidecars/{block_id}", method: "GET"},
209245
};
210246

@@ -220,7 +256,9 @@ export type ReqTypes = {
220256
getBlockHeaders: {query: {slot?: number; parent_root?: string}};
221257
getBlockRoot: BlockIdOnlyReq;
222258
publishBlock: {body: unknown};
259+
publishBlockV2: {body: unknown; query: {broadcast_validation?: string}};
223260
publishBlindedBlock: {body: unknown};
261+
publishBlindedBlockV2: {body: unknown; query: {broadcast_validation?: string}};
224262
getBlobSidecars: BlockIdOnlyReq;
225263
};
226264

@@ -277,7 +315,35 @@ export function getReqSerializers(config: ChainForkConfig): ReqSerializers<Api,
277315
},
278316
getBlockRoot: blockIdOnlyReq,
279317
publishBlock: reqOnlyBody(AllForksSignedBlockOrContents, Schema.Object),
318+
publishBlockV2: {
319+
writeReq: (item, {broadcastValidation}) => ({
320+
body: AllForksSignedBlockOrContents.toJson(item),
321+
query: {broadcast_validation: broadcastValidation},
322+
}),
323+
parseReq: ({body, query}) => [
324+
AllForksSignedBlockOrContents.fromJson(body),
325+
{broadcastValidation: query.broadcast_validation as BroadcastValidation},
326+
],
327+
schema: {
328+
body: Schema.Object,
329+
query: {broadcast_validation: Schema.String},
330+
},
331+
},
280332
publishBlindedBlock: reqOnlyBody(AllForksSignedBlindedBlockOrContents, Schema.Object),
333+
publishBlindedBlockV2: {
334+
writeReq: (item, {broadcastValidation}) => ({
335+
body: AllForksSignedBlindedBlockOrContents.toJson(item),
336+
query: {broadcast_validation: broadcastValidation},
337+
}),
338+
parseReq: ({body, query}) => [
339+
AllForksSignedBlindedBlockOrContents.fromJson(body),
340+
{broadcastValidation: query.broadcast_validation as BroadcastValidation},
341+
],
342+
schema: {
343+
body: Schema.Object,
344+
query: {broadcast_validation: Schema.String},
345+
},
346+
},
281347
getBlobSidecars: blockIdOnlyReq,
282348
};
283349
}

packages/api/src/beacon/routes/beacon/index.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ import * as state from "./state.js";
1515
export * as block from "./block.js";
1616
export * as pool from "./pool.js";
1717
export * as state from "./state.js";
18-
export {BlockId, BlockHeaderResponse} from "./block.js";
18+
export {BlockId, BlockHeaderResponse, BroadcastValidation} from "./block.js";
1919
export {AttestationFilters} from "./pool.js";
2020
// TODO: Review if re-exporting all these types is necessary
2121
export {

packages/api/test/unit/beacon/testData/beacon.ts

+14-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,12 @@
11
import {toHexString} from "@chainsafe/ssz";
22
import {ForkName} from "@lodestar/params";
33
import {ssz, Slot, allForks} from "@lodestar/types";
4-
import {Api, BlockHeaderResponse, ValidatorResponse} from "../../../../src/beacon/routes/beacon/index.js";
4+
import {
5+
Api,
6+
BlockHeaderResponse,
7+
BroadcastValidation,
8+
ValidatorResponse,
9+
} from "../../../../src/beacon/routes/beacon/index.js";
510
import {GenericServerTestCases} from "../../../utils/genericServerTest.js";
611

712
const root = Buffer.alloc(32, 1);
@@ -52,10 +57,18 @@ export const testData: GenericServerTestCases<Api> = {
5257
args: [ssz.phase0.SignedBeaconBlock.defaultValue()],
5358
res: undefined,
5459
},
60+
publishBlockV2: {
61+
args: [ssz.phase0.SignedBeaconBlock.defaultValue(), {broadcastValidation: BroadcastValidation.none}],
62+
res: undefined,
63+
},
5564
publishBlindedBlock: {
5665
args: [getDefaultBlindedBlock(64)],
5766
res: undefined,
5867
},
68+
publishBlindedBlockV2: {
69+
args: [getDefaultBlindedBlock(64), {broadcastValidation: BroadcastValidation.none}],
70+
res: undefined,
71+
},
5972
getBlobSidecars: {
6073
args: ["head"],
6174
res: {executionOptimistic: true, data: ssz.deneb.BlobSidecars.defaultValue()},

packages/beacon-node/src/api/impl/beacon/blocks/index.ts

+131-67
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import {fromHexString, toHexString} from "@chainsafe/ssz";
22
import {routes, ServerApi, isSignedBlockContents, isSignedBlindedBlockContents} from "@lodestar/api";
33
import {computeTimeAtSlot} from "@lodestar/state-transition";
44
import {SLOTS_PER_HISTORICAL_ROOT} from "@lodestar/params";
5-
import {sleep} from "@lodestar/utils";
5+
import {sleep, toHex} from "@lodestar/utils";
66
import {allForks, deneb} from "@lodestar/types";
77
import {
88
BlockSource,
@@ -19,6 +19,8 @@ import {NetworkEvent} from "../../../../network/index.js";
1919
import {ApiModules} from "../../types.js";
2020
import {resolveBlockId, toBeaconHeaderResponse} from "./utils.js";
2121

22+
type PublishBlockOpts = ImportBlockOpts & {broadcastValidation?: routes.beacon.BroadcastValidation};
23+
2224
/**
2325
* Validator clock may be advanced from beacon's clock. If the validator requests a resource in a
2426
* future slot, wait some time instead of rejecting the request because it's in the future
@@ -37,6 +39,127 @@ export function getBeaconBlockApi({
3739
network,
3840
db,
3941
}: Pick<ApiModules, "chain" | "config" | "metrics" | "network" | "db">): ServerApi<routes.beacon.block.Api> {
42+
const publishBlock: ServerApi<routes.beacon.block.Api>["publishBlock"] = async (
43+
signedBlockOrContents,
44+
opts: PublishBlockOpts = {}
45+
) => {
46+
const seenTimestampSec = Date.now() / 1000;
47+
let blockForImport: BlockInput, signedBlock: allForks.SignedBeaconBlock, signedBlobs: deneb.SignedBlobSidecars;
48+
49+
if (isSignedBlockContents(signedBlockOrContents)) {
50+
({signedBlock, signedBlobSidecars: signedBlobs} = signedBlockOrContents);
51+
blockForImport = getBlockInput.postDeneb(
52+
config,
53+
signedBlock,
54+
BlockSource.api,
55+
// The blobsSidecar will be replaced in the followup PRs with just blobs
56+
blobSidecarsToBlobsSidecar(
57+
config,
58+
signedBlock,
59+
signedBlobs.map((sblob) => sblob.message)
60+
),
61+
null
62+
);
63+
} else {
64+
signedBlock = signedBlockOrContents;
65+
signedBlobs = [];
66+
// TODO: Once API supports submitting data as SSZ, replace null with blockBytes
67+
blockForImport = getBlockInput.preDeneb(config, signedBlock, BlockSource.api, null);
68+
}
69+
70+
// check what validations have been requested before broadcasting and publishing the block
71+
// TODO: add validation time to metrics
72+
const broadcastValidation = opts.broadcastValidation ?? routes.beacon.BroadcastValidation.none;
73+
// if block is locally produced, full or blinded, it already is 'consensus' validated as it went through
74+
// state transition to produce the stateRoot
75+
const slot = signedBlock.message.slot;
76+
const blockRoot = toHex(chain.config.getForkTypes(slot).BeaconBlock.hashTreeRoot(signedBlock.message));
77+
const blockLocallyProduced =
78+
chain.producedBlockRoot.has(blockRoot) || chain.producedBlindedBlockRoot.has(blockRoot);
79+
const valLogMeta = {broadcastValidation, blockRoot, blockLocallyProduced, slot};
80+
81+
switch (broadcastValidation) {
82+
case routes.beacon.BroadcastValidation.none: {
83+
if (blockLocallyProduced) {
84+
chain.logger.debug("No broadcast validation requested for the block", valLogMeta);
85+
} else {
86+
chain.logger.warn("No broadcast validation requested for the block", valLogMeta);
87+
}
88+
break;
89+
}
90+
case routes.beacon.BroadcastValidation.consensus: {
91+
// check if this beacon node produced the block else run validations
92+
if (!blockLocallyProduced) {
93+
// error or log warning that we support consensus val on blocks produced via this beacon node
94+
const message = "Consensus validation not implemented yet for block not produced by this beacon node";
95+
if (chain.opts.broadcastValidationStrictness === "error") {
96+
throw Error(message);
97+
} else {
98+
chain.logger.warn(message, valLogMeta);
99+
}
100+
}
101+
break;
102+
}
103+
104+
default: {
105+
// error or log warning we do not support this validation
106+
const message = `Broadcast validation of ${broadcastValidation} type not implemented yet`;
107+
if (chain.opts.broadcastValidationStrictness === "error") {
108+
throw Error(message);
109+
} else {
110+
chain.logger.warn(message);
111+
}
112+
}
113+
}
114+
115+
// Simple implementation of a pending block queue. Keeping the block here recycles the API logic, and keeps the
116+
// REST request promise without any extra infrastructure.
117+
const msToBlockSlot =
118+
computeTimeAtSlot(config, blockForImport.block.message.slot, chain.genesisTime) * 1000 - Date.now();
119+
if (msToBlockSlot <= MAX_API_CLOCK_DISPARITY_MS && msToBlockSlot > 0) {
120+
// If block is a bit early, hold it in a promise. Equivalent to a pending queue.
121+
await sleep(msToBlockSlot);
122+
}
123+
124+
// TODO: Validate block
125+
metrics?.registerBeaconBlock(OpSource.api, seenTimestampSec, blockForImport.block.message);
126+
const publishPromises = [
127+
// Send the block, regardless of whether or not it is valid. The API
128+
// specification is very clear that this is the desired behaviour.
129+
() => network.publishBeaconBlock(signedBlock) as Promise<unknown>,
130+
() =>
131+
// there is no rush to persist block since we published it to gossip anyway
132+
chain.processBlock(blockForImport, {...opts, eagerPersistBlock: false}).catch((e) => {
133+
if (e instanceof BlockError && e.type.code === BlockErrorCode.PARENT_UNKNOWN) {
134+
network.events.emit(NetworkEvent.unknownBlockParent, {
135+
blockInput: blockForImport,
136+
peer: IDENTITY_PEER_ID,
137+
});
138+
}
139+
throw e;
140+
}),
141+
...signedBlobs.map((signedBlob) => () => network.publishBlobSidecar(signedBlob)),
142+
];
143+
await promiseAllMaybeAsync(publishPromises);
144+
};
145+
146+
const publishBlindedBlock: ServerApi<routes.beacon.block.Api>["publishBlindedBlock"] = async (
147+
signedBlindedBlockOrContents,
148+
opts: PublishBlockOpts = {}
149+
) => {
150+
const executionBuilder = chain.executionBuilder;
151+
if (!executionBuilder) throw Error("exeutionBuilder required to publish SignedBlindedBeaconBlock");
152+
// Mechanism for blobs & blocks on builder is not yet finalized
153+
if (isSignedBlindedBlockContents(signedBlindedBlockOrContents)) {
154+
throw Error("exeutionBuilder not yet implemented for deneb+ forks");
155+
} else {
156+
const signedBlockOrContents = await executionBuilder.submitBlindedBlock(signedBlindedBlockOrContents);
157+
// the full block is published by relay and it's possible that the block is already known to us by gossip
158+
// see https://github.com/ChainSafe/lodestar/issues/5404
159+
return publishBlock(signedBlockOrContents, {...opts, ignoreIfKnown: true});
160+
}
161+
};
162+
40163
return {
41164
async getBlockHeaders(filters) {
42165
// TODO - SLOW CODE: This code seems like it could be improved
@@ -189,74 +312,15 @@ export function getBeaconBlockApi({
189312
};
190313
},
191314

192-
async publishBlindedBlock(signedBlindedBlockOrContents) {
193-
const executionBuilder = chain.executionBuilder;
194-
if (!executionBuilder) throw Error("exeutionBuilder required to publish SignedBlindedBeaconBlock");
195-
// Mechanism for blobs & blocks on builder is not yet finalized
196-
if (isSignedBlindedBlockContents(signedBlindedBlockOrContents)) {
197-
throw Error("exeutionBuilder not yet implemented for deneb+ forks");
198-
} else {
199-
const signedBlockOrContents = await executionBuilder.submitBlindedBlock(signedBlindedBlockOrContents);
200-
// the full block is published by relay and it's possible that the block is already known to us by gossip
201-
// see https://github.com/ChainSafe/lodestar/issues/5404
202-
return this.publishBlock(signedBlockOrContents, {ignoreIfKnown: true});
203-
}
204-
},
205-
206-
async publishBlock(signedBlockOrContents, opts: ImportBlockOpts = {}) {
207-
const seenTimestampSec = Date.now() / 1000;
208-
let blockForImport: BlockInput, signedBlock: allForks.SignedBeaconBlock, signedBlobs: deneb.SignedBlobSidecars;
315+
publishBlock,
316+
publishBlindedBlock,
209317

210-
if (isSignedBlockContents(signedBlockOrContents)) {
211-
({signedBlock, signedBlobSidecars: signedBlobs} = signedBlockOrContents);
212-
blockForImport = getBlockInput.postDeneb(
213-
config,
214-
signedBlock,
215-
BlockSource.api,
216-
// The blobsSidecar will be replaced in the followup PRs with just blobs
217-
blobSidecarsToBlobsSidecar(
218-
config,
219-
signedBlock,
220-
signedBlobs.map((sblob) => sblob.message)
221-
),
222-
null
223-
);
224-
} else {
225-
signedBlock = signedBlockOrContents;
226-
signedBlobs = [];
227-
// TODO: Once API supports submitting data as SSZ, replace null with blockBytes
228-
blockForImport = getBlockInput.preDeneb(config, signedBlock, BlockSource.api, null);
229-
}
230-
231-
// Simple implementation of a pending block queue. Keeping the block here recycles the API logic, and keeps the
232-
// REST request promise without any extra infrastructure.
233-
const msToBlockSlot =
234-
computeTimeAtSlot(config, blockForImport.block.message.slot, chain.genesisTime) * 1000 - Date.now();
235-
if (msToBlockSlot <= MAX_API_CLOCK_DISPARITY_MS && msToBlockSlot > 0) {
236-
// If block is a bit early, hold it in a promise. Equivalent to a pending queue.
237-
await sleep(msToBlockSlot);
238-
}
318+
async publishBlindedBlockV2(signedBlindedBlockOrContents, opts) {
319+
await publishBlindedBlock(signedBlindedBlockOrContents, opts);
320+
},
239321

240-
// TODO: Validate block
241-
metrics?.registerBeaconBlock(OpSource.api, seenTimestampSec, blockForImport.block.message);
242-
const publishPromises = [
243-
// Send the block, regardless of whether or not it is valid. The API
244-
// specification is very clear that this is the desired behaviour.
245-
() => network.publishBeaconBlock(signedBlock) as Promise<unknown>,
246-
() =>
247-
// there is no rush to persist block since we published it to gossip anyway
248-
chain.processBlock(blockForImport, {...opts, eagerPersistBlock: false}).catch((e) => {
249-
if (e instanceof BlockError && e.type.code === BlockErrorCode.PARENT_UNKNOWN) {
250-
network.events.emit(NetworkEvent.unknownBlockParent, {
251-
blockInput: blockForImport,
252-
peer: IDENTITY_PEER_ID,
253-
});
254-
}
255-
throw e;
256-
}),
257-
...signedBlobs.map((signedBlob) => () => network.publishBlobSidecar(signedBlob)),
258-
];
259-
await promiseAllMaybeAsync(publishPromises);
322+
async publishBlockV2(signedBlockOrContents, opts) {
323+
await publishBlock(signedBlockOrContents, opts);
260324
},
261325

262326
async getBlobSidecars(blockId) {

0 commit comments

Comments
 (0)