Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
162 changes: 100 additions & 62 deletions packages/beacon-node/src/api/impl/validator/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -874,6 +874,105 @@ export function getValidatorApi(
throw Error("Unreachable error occurred during the builder and execution block production");
}

/**
* GLOAS block production with gossip bid selection.
* Always builds a local block. If a gossip bid is available, also builds a block with that bid
* in parallel and prefers it over the local block.
*
* TODO GLOAS: Currently always prefers gossip bid over local block regardless of builderSelection.
* Need to respect builderSelection (MaxProfit, BuilderAlways, ExecutionAlways, etc.) to let
* the user control bid source preferences and value comparison. Also add external builder api
* support when it is implemented.
*/
async function produceEngineOrBuilderBlockGloas(
slot: Slot,
randaoReveal: BLSSignature,
graffiti?: string,
{feeRecipient}: {feeRecipient?: string} = {}
): Promise<{
data: gloas.BeaconBlock;
meta: {version: ForkName; consensusBlockValue: Wei};
}> {
const fork = config.getForkName(slot);

notWhileSyncing();
await waitForSlot(slot);

const parentBlock = chain.getProposerHead(slot);
const {blockRoot: parentBlockRootHex, slot: parentSlot} = parentBlock;
const parentBlockRoot = fromHex(parentBlockRootHex);
notOnOutOfRangeData(parentBlockRoot);
metrics?.blockProductionSlotDelta.set(slot - parentSlot);

const graffitiBytes = toGraffitiBytes(
graffiti ?? getDefaultGraffiti(getLodestarClientVersion(), chain.executionEngine.clientVersion, {})
);

// Look up best gossip bid from the pool (instant)
const parentBlockHash = parentBlock.executionPayloadBlockHash;
const gossipBid =
parentBlockHash != null
? chain.executionPayloadBidPool.getBestBid(slot, parentBlockHash, parentBlockRootHex)
: null;

logger.verbose("Producing GLOAS block", {
Comment thread
ensi321 marked this conversation as resolved.
Resolved
slot,
parentSlot,
parentBlockRoot: parentBlockRootHex,
fork,
Comment thread
ensi321 marked this conversation as resolved.
Resolved
gossipBidAvailable: gossipBid !== null,
});

const commonBlockBodyPromise = chain.produceCommonBlockBody({
slot,
parentBlock,
randaoReveal,
graffiti: graffitiBytes,
});

const baseAttrs = {slot, parentBlock, randaoReveal, graffiti: graffitiBytes, feeRecipient, commonBlockBodyPromise};

// Always build local block. If gossip bid available, also build with it in parallel and prefer it.
if (gossipBid) {
Comment thread
ensi321 marked this conversation as resolved.
Resolved
const [engineResult, gossipResult] = await Promise.allSettled([
chain.produceBlock(baseAttrs),
chain.produceBlock({...baseAttrs, externalBid: gossipBid}),
]);

if (gossipResult.status === "fulfilled") {
metrics?.blockProductionSuccess.inc({source: ProducedBlockSource.builder});
logger.info("Selected gossip bid block", {
slot,
Comment thread
ensi321 marked this conversation as resolved.
Resolved
bidValue: gossipBid.message.value,
builderIndex: gossipBid.message.builderIndex,
});
return {
data: gossipResult.value.block as gloas.BeaconBlock,
meta: {version: fork, consensusBlockValue: gossipResult.value.consensusBlockValue},
};
}

if (engineResult.status === "fulfilled") {
metrics?.blockProductionSuccess.inc({source: ProducedBlockSource.engine});
logger.warn("Gossip bid block production failed, using local block", {slot});
Comment thread
ensi321 marked this conversation as resolved.
Resolved
return {
data: engineResult.value.block as gloas.BeaconBlock,
meta: {version: fork, consensusBlockValue: engineResult.value.consensusBlockValue},
};
}

throw Error("Both engine and gossip bid block production failed");
Comment thread
ensi321 marked this conversation as resolved.
Resolved
}

// No gossip bid available, build local only
const {block, consensusBlockValue} = await chain.produceBlock(baseAttrs);
metrics?.blockProductionSuccess.inc({source: ProducedBlockSource.engine});
return {
data: block as gloas.BeaconBlock,
meta: {version: fork, consensusBlockValue},
};
}

return {
async produceBlockV3({slot, randaoReveal, graffiti, skipRandaoVerification, builderBoostFactor, ...opts}) {
const {data, ...meta} = await produceEngineOrBuilderBlock(
Expand Down Expand Up @@ -909,68 +1008,7 @@ export function getValidatorApi(
throw new ApiError(400, `produceBlockV4 not supported for pre-gloas fork=${fork}`);
}

Comment thread
ensi321 marked this conversation as resolved.
Resolved
notWhileSyncing();
await waitForSlot(slot);

// TODO GLOAS: support producing blocks from builder bids
const source = ProducedBlockSource.engine;

// TODO GLOAS: needs to be updated after fork choice changes are merged
const parentBlock = chain.getProposerHead(slot);
const {blockRoot: parentBlockRootHex, slot: parentSlot} = parentBlock;
const parentBlockRoot = fromHex(parentBlockRootHex);
notOnOutOfRangeData(parentBlockRoot);
metrics?.blockProductionSlotDelta.set(slot - parentSlot);
metrics?.blockProductionRequests.inc({source});

const graffitiBytes = toGraffitiBytes(
graffiti ?? getDefaultGraffiti(getLodestarClientVersion(), chain.executionEngine.clientVersion, {})
);
const commonBlockBodyPromise = chain.produceCommonBlockBody({
slot,
parentBlock,
randaoReveal,
graffiti: graffitiBytes,
});

let timer: undefined | ((opts: {source: ProducedBlockSource}) => number);
try {
timer = metrics?.blockProductionTime.startTimer();
const {block, executionPayloadValue, consensusBlockValue} = await chain.produceBlock({
slot,
parentBlock,
randaoReveal,
graffiti: graffitiBytes,
feeRecipient,
commonBlockBodyPromise,
});

metrics?.blockProductionSuccess.inc({source});
metrics?.blockProductionNumAggregated.observe({source}, block.body.attestations.length);
metrics?.blockProductionConsensusBlockValue.observe({source}, Number(formatWeiToEth(consensusBlockValue)));
metrics?.blockProductionExecutionPayloadValue.observe({source}, Number(formatWeiToEth(executionPayloadValue)));

const blockRoot = toRootHex(config.getForkTypes(slot).BeaconBlock.hashTreeRoot(block));
logger.verbose("Produced block", {
slot,
executionPayloadValue,
consensusBlockValue,
root: blockRoot,
});
if (chain.opts.persistProducedBlocks) {
void chain.persistBlock(block, "produced_engine_block");
}

return {
data: block as gloas.BeaconBlock,
meta: {
version: fork,
consensusBlockValue,
},
};
} finally {
timer?.({source});
}
return produceEngineOrBuilderBlockGloas(slot, randaoReveal, graffiti, {feeRecipient});
},

async produceAttestationData({committeeIndex, slot}) {
Expand Down
2 changes: 2 additions & 0 deletions packages/beacon-node/src/chain/chain.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1025,6 +1025,7 @@ export class BeaconChain implements IBeaconChain {
feeRecipient,
commonBlockBodyPromise,
parentBlock,
externalBid,
}: BlockAttributes & {commonBlockBodyPromise: Promise<CommonBlockBody>}
): Promise<{
block: AssembledBlockType<T>;
Expand Down Expand Up @@ -1054,6 +1055,7 @@ export class BeaconChain implements IBeaconChain {
proposerIndex,
proposerPubKey,
commonBlockBodyPromise,
externalBid,
}
);

Expand Down
20 changes: 10 additions & 10 deletions packages/beacon-node/src/chain/opPools/executionPayloadBidPool.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,13 @@ type BlockRootHex = string;
type BlockHashHex = string;

/**
* Store the best execution payload bid per slot / (parent block root, parent block hash).
* Store the best signed execution payload bid per slot / (parent block root, parent block hash).
*/
export class ExecutionPayloadBidPool {
private readonly bidByParentHashByParentRootBySlot = new MapDef<
Slot,
MapDef<BlockRootHex, Map<BlockHashHex, gloas.ExecutionPayloadBid>>
>(() => new MapDef<BlockRootHex, Map<BlockHashHex, gloas.ExecutionPayloadBid>>(() => new Map()));
MapDef<BlockRootHex, Map<BlockHashHex, gloas.SignedExecutionPayloadBid>>
>(() => new MapDef<BlockRootHex, Map<BlockHashHex, gloas.SignedExecutionPayloadBid>>(() => new Map()));
private lowestPermissibleSlot = 0;

get size(): number {
Expand All @@ -31,8 +31,8 @@ export class ExecutionPayloadBidPool {
return count;
}

add(bid: gloas.ExecutionPayloadBid): InsertOutcome {
const {slot, parentBlockRoot, parentBlockHash, value} = bid;
add(signedBid: gloas.SignedExecutionPayloadBid): InsertOutcome {
const {slot, parentBlockRoot, parentBlockHash, value} = signedBid.message;
const lowestPermissibleSlot = this.lowestPermissibleSlot;

if (slot < lowestPermissibleSlot) {
Expand All @@ -45,28 +45,28 @@ export class ExecutionPayloadBidPool {
const existing = bidByParentHash.get(parentHashHex);

if (existing) {
const existingValue = existing.value;
const existingValue = existing.message.value;
const newValue = value;
if (newValue > existingValue) {
bidByParentHash.set(parentHashHex, bid);
bidByParentHash.set(parentHashHex, signedBid);
return InsertOutcome.NewData;
}
return newValue === existingValue ? InsertOutcome.AlreadyKnown : InsertOutcome.NotBetterThan;
}

bidByParentHash.set(parentHashHex, bid);
bidByParentHash.set(parentHashHex, signedBid);
return InsertOutcome.NewData;
}

/**
* Return the highest-value bid matching slot, parent block hash, and parent block root.
* Return the highest-value signed bid matching slot, parent block hash, and parent block root.
* Used for gossip validation and block production.
*/
getBestBid(
slot: Slot,
parentBlockHash: BlockHashHex,
parentBlockRoot: BlockRootHex
): gloas.ExecutionPayloadBid | null {
): gloas.SignedExecutionPayloadBid | null {
const bidByParentHash = this.bidByParentHashByParentRootBySlot.get(slot)?.get(parentBlockRoot);
return bidByParentHash?.get(parentBlockHash) ?? null;
}
Expand Down
Loading
Loading