Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 58 additions & 0 deletions packages/api/src/beacon/routes/beacon/block.ts
Original file line number Diff line number Diff line change
Expand Up @@ -226,6 +226,20 @@ export type Endpoints = {
EmptyMeta
>;

/**
* Publish signed execution payload bid.
* Instructs the beacon node to broadcast a signed execution payload bid to the network,
* to be gossiped for potential inclusion in block building. A success response (20x) indicates
* that the bid passed gossip validation and was successfully broadcast onto the network.
*/
publishExecutionPayloadBid: Endpoint<
"POST",
{signedExecutionPayloadBid: gloas.SignedExecutionPayloadBid},
{body: unknown; headers: {[MetaHeader.Version]: string}},
EmptyResponseData,
EmptyMeta
>;

/**
* Get signed execution payload envelope.
* Retrieves signed execution payload envelope for a given block id.
Expand Down Expand Up @@ -646,6 +660,50 @@ export function getDefinitions(config: ChainForkConfig): RouteDefinitions<Endpoi
requestWireFormat: WireFormat.ssz,
},
},
publishExecutionPayloadBid: {
url: "/eth/v1/beacon/execution_payload_bid",
method: "POST",
req: {
writeReqJson: ({signedExecutionPayloadBid}) => {
const fork = config.getForkName(signedExecutionPayloadBid.message.slot);
return {
body: getPostGloasForkTypes(fork).SignedExecutionPayloadBid.toJson(signedExecutionPayloadBid),
headers: {
[MetaHeader.Version]: fork,
},
};
},
parseReqJson: ({body, headers}) => {
const fork = toForkName(fromHeaders(headers, MetaHeader.Version));
return {
signedExecutionPayloadBid: getPostGloasForkTypes(fork).SignedExecutionPayloadBid.fromJson(body),
};
},
writeReqSsz: ({signedExecutionPayloadBid}) => {
const fork = config.getForkName(signedExecutionPayloadBid.message.slot);
return {
body: getPostGloasForkTypes(fork).SignedExecutionPayloadBid.serialize(signedExecutionPayloadBid),
headers: {
[MetaHeader.Version]: fork,
},
};
},
parseReqSsz: ({body, headers}) => {
const fork = toForkName(fromHeaders(headers, MetaHeader.Version));
return {
signedExecutionPayloadBid: getPostGloasForkTypes(fork).SignedExecutionPayloadBid.deserialize(body),
};
},
schema: {
body: Schema.Object,
headers: {[MetaHeader.Version]: Schema.String},
},
},
resp: EmptyResponseCodec,
init: {
requestWireFormat: WireFormat.ssz,
},
},
getSignedExecutionPayloadEnvelope: {
url: "/eth/v1/beacon/execution_payload_envelope/{block_id}",
method: "GET",
Expand Down
1 change: 0 additions & 1 deletion packages/api/test/unit/beacon/oapiSpec.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,6 @@ const ignoredOperations = [
"getDepositSnapshot", // Won't fix for now, see https://github.com/ChainSafe/lodestar/issues/5697
"getNextWithdrawals", // https://github.com/ChainSafe/lodestar/issues/5696
// TODO GLOAS: required by v5.0.0-alpha.1
"publishExecutionPayloadBid",
"getExecutionPayloadBid",
"getSignedExecutionPayloadEnvelope",
];
Expand Down
4 changes: 4 additions & 0 deletions packages/api/test/unit/beacon/testData/beacon.ts
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,10 @@ export const testData: GenericServerTestCases<Endpoints> = {
args: {signedExecutionPayloadEnvelope: ssz.gloas.SignedExecutionPayloadEnvelope.defaultValue()},
res: undefined,
},
publishExecutionPayloadBid: {
args: {signedExecutionPayloadBid: ssz.gloas.SignedExecutionPayloadBid.defaultValue()},
res: undefined,
},
getSignedExecutionPayloadEnvelope: {
args: {blockId: "head"},
res: {
Expand Down
52 changes: 51 additions & 1 deletion packages/beacon-node/src/api/impl/beacon/blocks/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ import {
fulu,
gloas,
isDenebBlockContents,
ssz,
sszTypesFor,
} from "@lodestar/types";
import {fromHex, sleep, toHex, toRootHex} from "@lodestar/utils";
Expand All @@ -41,7 +42,14 @@ import {ImportBlockOpts} from "../../../../chain/blocks/types.js";
import {verifyBlocksInEpoch} from "../../../../chain/blocks/verifyBlock.js";
import {BeaconChain} from "../../../../chain/chain.js";
import {ChainEvent} from "../../../../chain/emitter.js";
import {BlockError, BlockErrorCode, BlockGossipError} from "../../../../chain/errors/index.js";
import {
BlockError,
BlockErrorCode,
BlockGossipError,
ExecutionPayloadBidError,
ExecutionPayloadBidErrorCode,
GossipAction,
} from "../../../../chain/errors/index.js";
import {
BlockType,
ProduceFullBellatrix,
Expand All @@ -50,6 +58,7 @@ import {
ProduceFullGloas,
} from "../../../../chain/produceBlock/index.js";
import {validateGossipBlock} from "../../../../chain/validation/block.js";
import {validateApiExecutionPayloadBid} from "../../../../chain/validation/executionPayloadBid.js";
import {validateApiExecutionPayloadEnvelope} from "../../../../chain/validation/executionPayloadEnvelope.js";
import {OpSource} from "../../../../chain/validatorMonitor.js";
import {
Expand Down Expand Up @@ -825,6 +834,47 @@ export function getBeaconBlockApi({
});
},

async publishExecutionPayloadBid({signedExecutionPayloadBid}) {
const fork = config.getForkName(signedExecutionPayloadBid.message.slot);
if (!isForkPostGloas(fork)) {
throw new ApiError(400, `publishExecutionPayloadBid not supported for pre-gloas fork=${fork}`);
}

const logCtx = {
slot: signedExecutionPayloadBid.message.slot,
builderIndex: signedExecutionPayloadBid.message.builderIndex,
};

try {
const {proposerIndex} = await validateApiExecutionPayloadBid(chain, signedExecutionPayloadBid);

const insertOutcome = chain.executionPayloadBidPool.add(signedExecutionPayloadBid.message);
metrics?.opPool.executionPayloadBidPool.apiInsertOutcome.inc({insertOutcome});

chain.validatorMonitor?.registerExecutionPayloadBid(
OpSource.api,
proposerIndex,
signedExecutionPayloadBid.message
);
await network.publishExecutionPayloadBid(signedExecutionPayloadBid);

chain.emitter.emit(routes.events.EventType.executionPayloadBid, {
version: fork,
data: signedExecutionPayloadBid,
});
Comment on lines +859 to +864

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

It is recommended to log the number of peers the bid was successfully published to, consistent with the implementation of publishExecutionPayloadEnvelope. This improves observability and helps in debugging network propagation issues.

        const sentPeers = await network.publishExecutionPayloadBid(signedExecutionPayloadBid);

        chain.emitter.emit(routes.events.EventType.executionPayloadBid, {
          version: fork,
          data: signedExecutionPayloadBid,
        });

        chain.logger.info("Published execution payload bid", {...logCtx, sentPeers});

} catch (e) {
if (e instanceof ExecutionPayloadBidError && e.type.code === ExecutionPayloadBidErrorCode.BID_ALREADY_KNOWN) {
chain.logger.debug("Ignoring known execution payload bid", logCtx);
return;
}
chain.logger.verbose("Error on publishExecutionPayloadBid", logCtx, e as Error);
if (e instanceof ExecutionPayloadBidError && e.action === GossipAction.REJECT) {
chain.persistInvalidSszValue(ssz.gloas.SignedExecutionPayloadBid, signedExecutionPayloadBid, "api_reject");
}
Comment on lines +871 to +873

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Using sszTypesFor(fork).SignedExecutionPayloadBid is more idiomatic in Lodestar and ensures that the correct fork-specific type is used for persistence, especially as the protocol evolves in future forks.

Suggested change
if (e instanceof ExecutionPayloadBidError && e.action === GossipAction.REJECT) {
chain.persistInvalidSszValue(ssz.gloas.SignedExecutionPayloadBid, signedExecutionPayloadBid, "api_reject");
}
if (e instanceof ExecutionPayloadBidError && e.action === GossipAction.REJECT) {
chain.persistInvalidSszValue(sszTypesFor(fork).SignedExecutionPayloadBid, signedExecutionPayloadBid, "api_reject");
}

throw e;
}
},

async getSignedExecutionPayloadEnvelope({blockId}, context) {
const {block, executionOptimistic, finalized} = await getBlockResponse(chain, blockId);
const slot = block.message.slot;
Expand Down
1 change: 1 addition & 0 deletions packages/beacon-node/src/network/interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,7 @@ export interface INetwork extends INetworkCorePublic {
publishSignedExecutionPayloadEnvelope(signedEnvelope: gloas.SignedExecutionPayloadEnvelope): Promise<number>;
publishPayloadAttestationMessage(payloadAttestationMessage: gloas.PayloadAttestationMessage): Promise<number>;
publishProposerPreferences(signedProposerPreferences: gloas.SignedProposerPreferences): Promise<number>;
publishExecutionPayloadBid(signedExecutionPayloadBid: gloas.SignedExecutionPayloadBid): Promise<number>;

// Debug
dumpGossipQueue(gossipType: GossipType): Promise<PendingGossipsubMessage[]>;
Expand Down
11 changes: 11 additions & 0 deletions packages/beacon-node/src/network/network.ts
Original file line number Diff line number Diff line change
Expand Up @@ -537,6 +537,17 @@ export class Network implements INetwork {
);
}

async publishExecutionPayloadBid(signedExecutionPayloadBid: gloas.SignedExecutionPayloadBid): Promise<number> {
const epoch = computeEpochAtSlot(signedExecutionPayloadBid.message.slot);
const boundary = this.config.getForkBoundaryAtEpoch(epoch);

return this.publishGossip<GossipType.execution_payload_bid>(
{type: GossipType.execution_payload_bid, boundary},
signedExecutionPayloadBid,
{ignoreDuplicatePublishError: true}
);
}

private async publishGossip<K extends GossipType>(
topic: GossipTopicMap[K],
object: GossipTypeMap[K],
Expand Down
Loading