Skip to content
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

refactor: update the block production race #6241

Merged
merged 41 commits into from
Feb 5, 2024
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
31e489f
Update the promise race implementation
nazarhussain Dec 29, 2023
fdbc32c
Update block production race
nazarhussain Jan 19, 2024
e5dcc96
Fix spelling
nazarhussain Jan 19, 2024
95f088c
Work on feedback
nazarhussain Jan 22, 2024
c0334e7
Update the test file name
nazarhussain Jan 22, 2024
74e6d1e
Update the promise tests
nazarhussain Jan 22, 2024
8149719
Update util to switch
nazarhussain Jan 23, 2024
2c60bf9
chore: add early return on censoring builder or 0 builder boost
wemeetagain Jan 24, 2024
aab884f
Update the promise to extended promise
nazarhussain Jan 26, 2024
70a35b7
Update the builder flow
nazarhussain Jan 26, 2024
535fb23
Merge branch 'nh/6159-block-prod-race' of github.com:ChainSafe/lodest…
nazarhussain Jan 26, 2024
a1e8a0c
Fix the types
nazarhussain Jan 26, 2024
8698e46
Fix lint errors
nazarhussain Jan 26, 2024
c6150bb
Simplify logging
nazarhussain Jan 26, 2024
efd684c
Improve log messages for block values
nazarhussain Jan 26, 2024
6865972
Merge branch 'unstable' into nh/6159-block-prod-race
nazarhussain Jan 26, 2024
9c7942d
Update the promise to be typesafe
nazarhussain Jan 29, 2024
4597d06
Update the validator implementation
nazarhussain Jan 29, 2024
17686b9
Restructure test file for better review
nazarhussain Jan 29, 2024
f4c93e7
Fix lint errors
nazarhussain Jan 29, 2024
3391626
Fix lint error
nazarhussain Jan 29, 2024
73943ee
Make the tyep more flexible
nazarhussain Jan 29, 2024
374aeff
Fix flaky tests
nazarhussain Jan 29, 2024
6196428
Improve log message
nazarhussain Jan 29, 2024
7e6e91c
Simplify implementation
nazarhussain Jan 29, 2024
ddad503
Update log message function
nazarhussain Jan 29, 2024
7a2027a
chore: add review feedback
wemeetagain Jan 29, 2024
806c88f
Merge branch 'unstable' into nh/6159-block-prod-race
wemeetagain Jan 29, 2024
a0dc19f
chore: fix linter error
wemeetagain Jan 30, 2024
ba444cc
chore: address PR comments
wemeetagain Feb 1, 2024
f6e6619
Update packages/beacon-node/src/api/impl/validator/index.ts
wemeetagain Feb 1, 2024
e14889b
chore: address PR comments
wemeetagain Feb 1, 2024
15c7210
Merge branch 'unstable' into nh/6159-block-prod-race
wemeetagain Feb 1, 2024
689e087
chore: clean up selectBlockProductionSource
wemeetagain Feb 1, 2024
72e4a2b
Fix unit tests
nazarhussain Feb 5, 2024
76768b6
Add support for routes.validator.BuilderSelection.ExecutionOnly
nazarhussain Feb 5, 2024
f44fd8a
Fix unit tests
nazarhussain Feb 5, 2024
74fdad3
Increase the timeout for e2e env
nazarhussain Feb 5, 2024
64e6605
Apply suggestions from code review
nazarhussain Feb 5, 2024
9a291fc
Increase the timeout for e2e env
nazarhussain Feb 5, 2024
cb022e4
Revert e2e timeout
nazarhussain Feb 5, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
329 changes: 163 additions & 166 deletions packages/beacon-node/src/api/impl/validator/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ import {
phase0,
} from "@lodestar/types";
import {ExecutionStatus} from "@lodestar/fork-choice";
import {toHex, racePromisesWithCutoff, RaceEvent} from "@lodestar/utils";
import {toHex, resolveOrRacePromises} from "@lodestar/utils";
import {
AttestationError,
AttestationErrorCode,
Expand All @@ -57,7 +57,7 @@ import {getValidatorStatus} from "../beacon/state/utils.js";
import {validateGossipFnRetryUnknownRoot} from "../../../network/processor/gossipHandlers.js";
import {SCHEDULER_LOOKAHEAD_FACTOR} from "../../../chain/prepareNextSlot.js";
import {ChainEvent, CheckpointHex, CommonBlockBody} from "../../../chain/index.js";
import {computeSubnetForCommitteesAtSlot, getPubkeysForIndices} from "./utils.js";
import {computeSubnetForCommitteesAtSlot, getPubkeysForIndices, selectBlockProductionSource} from "./utils.js";

/**
* If the node is within this many epochs from the head, we declare it to be synced regardless of
Expand Down Expand Up @@ -472,6 +472,15 @@ export function getValidatorApi({
chain.executionBuilder !== undefined &&
builderSelection !== routes.validator.BuilderSelection.ExecutionOnly;

// At any point either the builder or execution or both flows should be active.
//
// Ideally such a scenario should be prevented on startup, but proposerSettingsFile or keymanager
// configurations could cause a validator pubkey to have builder disabled with builder selection builder only
// (TODO: independently make sure such an options update is not successful for a validator pubkey)
nazarhussain marked this conversation as resolved.
Show resolved Hide resolved
//
// So if builder is disabled ignore builder selection of builderonly if caused by user mistake
const isEngineEnabled = !isBuilderEnabled || builderSelection !== routes.validator.BuilderSelection.BuilderOnly;

const loggerContext = {
fork,
builderSelection,
nazarhussain marked this conversation as resolved.
Show resolved Hide resolved
Expand All @@ -490,196 +499,184 @@ export function getValidatorApi({
});
logger.debug("Produced common block body", loggerContext);

logger.info("Block production race (builder vs execution) starting", {
wemeetagain marked this conversation as resolved.
Show resolved Hide resolved
...loggerContext,
cutoffMs: BLOCK_PRODUCTION_RACE_CUTOFF_MS,
timeoutMs: BLOCK_PRODUCTION_RACE_TIMEOUT_MS,
});

// Start calls for building execution and builder blocks
const blindedBlockPromise = isBuilderEnabled
? // can't do fee recipient checks as builder bid doesn't return feeRecipient as of now
produceBuilderBlindedBlock(slot, randaoReveal, graffiti, {
feeRecipient,
// skip checking and recomputing head in these individual produce calls
skipHeadChecksAndUpdate: true,
commonBlockBody,
}).catch((e) => {
logger.error("produceBuilderBlindedBlock failed to produce block", {slot}, e);
return null;
})
: null;

const fullBlockPromise =
// At any point either the builder or execution or both flows should be active.
//
// Ideally such a scenario should be prevented on startup, but proposerSettingsFile or keymanager
// configurations could cause a validator pubkey to have builder disabled with builder selection builder only
// (TODO: independently make sure such an options update is not successful for a validator pubkey)
//
// So if builder is disabled ignore builder selection of builderonly if caused by user mistake
!isBuilderEnabled || builderSelection !== routes.validator.BuilderSelection.BuilderOnly
? // TODO deneb: builderSelection needs to be figured out if to be done beacon side
// || builderSelection !== BuilderSelection.BuilderOnly
produceEngineFullBlockOrContents(slot, randaoReveal, graffiti, {
feeRecipient,
strictFeeRecipientCheck,
// skip checking and recomputing head in these individual produce calls
skipHeadChecksAndUpdate: true,
commonBlockBody,
}).catch((e) => {
logger.error("produceEngineFullBlockOrContents failed to produce block", {slot}, e);
return null;
})
: null;

let blindedBlock, fullBlock;
if (blindedBlockPromise !== null && fullBlockPromise !== null) {
// reference index of promises in the race
const promisesOrder = [ProducedBlockSource.builder, ProducedBlockSource.engine];
[blindedBlock, fullBlock] = await racePromisesWithCutoff<
| ((routes.validator.ProduceBlockOrContentsRes | routes.validator.ProduceBlindedBlockRes) & {
shouldOverrideBuilder?: boolean;
})
| null
>(
[blindedBlockPromise, fullBlockPromise],
BLOCK_PRODUCTION_RACE_CUTOFF_MS,
BLOCK_PRODUCTION_RACE_TIMEOUT_MS,
// Callback to log the race events for better debugging capability
(event: RaceEvent, delayMs: number, index?: number) => {
const eventRef = index !== undefined ? {source: promisesOrder[index]} : {};
logger.verbose("Block production race (builder vs execution)", {
event,
...eventRef,
delayMs,
cutoffMs: BLOCK_PRODUCTION_RACE_CUTOFF_MS,
timeoutMs: BLOCK_PRODUCTION_RACE_TIMEOUT_MS,
slot,
});
}
);
if (blindedBlock instanceof Error) {
// error here means race cutoff exceeded
logger.error("Failed to produce builder block", {slot}, blindedBlock);
blindedBlock = null;
const builderDisabledError = new Error("Builder disabled");
const engineDisabledError = new Error("Engine disabled");
const [builder, engine] = await resolveOrRacePromises(
[
isBuilderEnabled
? // can't do fee recipient checks as builder bid doesn't return feeRecipient as of now
produceBuilderBlindedBlock(slot, randaoReveal, graffiti, {
feeRecipient,
// skip checking and recomputing head in these individual produce calls
skipHeadChecksAndUpdate: true,
commonBlockBody,
})
: Promise.reject<ReturnType<typeof produceBuilderBlindedBlock>>(builderDisabledError),

isEngineEnabled // TODO deneb: builderSelection needs to be figured out if to be done beacon side
nazarhussain marked this conversation as resolved.
Show resolved Hide resolved
? // || builderSelection !== BuilderSelection.BuilderOnly
produceEngineFullBlockOrContents(slot, randaoReveal, graffiti, {
feeRecipient,
strictFeeRecipientCheck,
// skip checking and recomputing head in these individual produce calls
skipHeadChecksAndUpdate: true,
commonBlockBody,
})
: Promise.reject<ReturnType<typeof produceEngineFullBlockOrContents>>(engineDisabledError),
],
{
resolveTimeoutMs: BLOCK_PRODUCTION_RACE_CUTOFF_MS,
raceTimeoutMs: BLOCK_PRODUCTION_RACE_TIMEOUT_MS,
}
if (fullBlock instanceof Error) {
logger.error("Failed to produce execution block", {slot}, fullBlock);
fullBlock = null;
}
} else if (blindedBlockPromise !== null && fullBlockPromise === null) {
blindedBlock = await blindedBlockPromise;
fullBlock = null;
} else if (blindedBlockPromise === null && fullBlockPromise !== null) {
blindedBlock = null;
fullBlock = await fullBlockPromise;
} else {
);

if (
builder.status === "rejected" &&
builder.reason === builderDisabledError &&
nazarhussain marked this conversation as resolved.
Show resolved Hide resolved
engine.status === "rejected" &&
engine.reason === engineDisabledError
) {
throw Error(
`Internal Error: Neither builder nor execution proposal flow activated isBuilderEnabled=${isBuilderEnabled} builderSelection=${builderSelection}`
);
}

const builderPayloadValue = blindedBlock?.executionPayloadValue ?? BigInt(0);
const enginePayloadValue = fullBlock?.executionPayloadValue ?? BigInt(0);
const consensusBlockValueBuilder = blindedBlock?.consensusBlockValue ?? BigInt(0);
const consensusBlockValueEngine = fullBlock?.consensusBlockValue ?? BigInt(0);
if (builder.status === "rejected" && builder.reason !== builderDisabledError) {
logger.warn(
"Builder failed to produce the block",
{
durationMs: builder.durationMs,
slot,
},
builder.reason as Error
);
}

const blockValueBuilder = builderPayloadValue + consensusBlockValueBuilder; // Total block value is in wei
const blockValueEngine = enginePayloadValue + consensusBlockValueEngine; // Total block value is in wei
if (engine.status === "rejected" && engine.reason !== engineDisabledError) {
logger.warn(
"Engine failed to produce the block",
{
durationMs: engine.durationMs,
slot,
},
engine.reason as Error
);
}

let executionPayloadSource: ProducedBlockSource | null = null;
const shouldOverrideBuilder = fullBlock?.shouldOverrideBuilder ?? false;
if (builder.status === "pending" && engine.status === "pending") {
throw Error(
`Builder and execution both timeout to proposed the block in ${BLOCK_PRODUCTION_RACE_TIMEOUT_MS}ms`
);
}

// handle the builder override case separately
if (shouldOverrideBuilder === true) {
executionPayloadSource = ProducedBlockSource.engine;
logger.info("Selected engine block as censorship suspected in builder blocks", {
// winston logger doesn't like bigint
enginePayloadValue: `${enginePayloadValue}`,
consensusBlockValueEngine: `${consensusBlockValueEngine}`,
blockValueEngine: `${blockValueEngine}`,
shouldOverrideBuilder,
if (builder.status === "fulfilled" && engine.status !== "fulfilled") {
nflaig marked this conversation as resolved.
Show resolved Hide resolved
const builderBlock = builder.value;
const builderPayloadValue = builderBlock.executionPayloadValue ?? BigInt(0);
const builderConsensusValue = builderBlock.consensusBlockValue ?? BigInt(0);
const builderBlockValue = builderPayloadValue + builderConsensusValue;

logger.info("Selected builder block: no engine block produced", {
durationMs: builder.durationMs,
slot,
builderPayloadValue: `${builderPayloadValue}`,
builderConsensusValue: `${builderConsensusValue}`,
builderBlockValue: `${builderBlockValue}`,
});
} else if (fullBlock && blindedBlock) {
switch (builderSelection) {
case routes.validator.BuilderSelection.MaxProfit: {
if (
// explicitly handle the two special values mentioned in spec for builder preferred / engine preferred
wemeetagain marked this conversation as resolved.
Show resolved Hide resolved
builderBoostFactor !== MAX_BUILDER_BOOST_FACTOR &&
(builderBoostFactor === BigInt(0) ||
blockValueEngine >= (blockValueBuilder * builderBoostFactor) / BigInt(100))
) {
executionPayloadSource = ProducedBlockSource.engine;
} else {
executionPayloadSource = ProducedBlockSource.builder;
}
break;
}

case routes.validator.BuilderSelection.ExecutionOnly: {
executionPayloadSource = ProducedBlockSource.engine;
break;
}
return {...builder.value, executionPayloadBlinded: true, executionPayloadSource: ProducedBlockSource.builder};
}

// For everything else just select the builder
default: {
executionPayloadSource = ProducedBlockSource.builder;
}
if (engine.status === "fulfilled" && builder.status !== "fulfilled") {
const engineBlock = engine.value;
const enginePayloadValue = engineBlock.executionPayloadValue ?? BigInt(0);
const engineConsensusValue = engineBlock.consensusBlockValue ?? BigInt(0);
const engineBlockValue = enginePayloadValue + engineConsensusValue;

logger.info("Selected engine block: no builder block produced", {
durationMs: engine.durationMs,
slot,
enginePayloadValue: `${enginePayloadValue}`,
engineConsensusValue: `${engineConsensusValue}`,
engineBlockValue: `${engineBlockValue}`,
});

return {...engine.value, executionPayloadBlinded: false, executionPayloadSource: ProducedBlockSource.engine};
}

if (engine.status === "fulfilled" && builder.status === "fulfilled") {
const engineBlock = engine.value;
const enginePayloadValue = engineBlock.executionPayloadValue ?? BigInt(0);
const engineConsensusValue = engineBlock.consensusBlockValue ?? BigInt(0);
const engineBlockValue = enginePayloadValue + engineConsensusValue;

const builderBlock = builder.value;
const builderPayloadValue = builderBlock.executionPayloadValue ?? BigInt(0);
const builderConsensusValue = builderBlock.consensusBlockValue ?? BigInt(0);
const builderBlockValue = builderPayloadValue + builderConsensusValue;

if (engineBlock.shouldOverrideBuilder) {
logger.info("Selected engine block as censorship suspected in builder blocks", {
nazarhussain marked this conversation as resolved.
Show resolved Hide resolved
// winston logger doesn't like bigint
enginePayloadValue: `${enginePayloadValue}`,
engineConsensusValue: `${engineConsensusValue}`,
engineBlockValue: `${engineBlockValue}`,
shouldOverrideBuilder: engineBlock.shouldOverrideBuilder,
slot,
});

return {...engine.value, executionPayloadBlinded: false, executionPayloadSource: ProducedBlockSource.engine};
}

const executionPayloadSource = selectBlockProductionSource({
nflaig marked this conversation as resolved.
Show resolved Hide resolved
builderBlockValue,
engineBlockValue,
builderBoostFactor,
builderSelection,
});

logger.info(`Selected executionPayloadSource=${executionPayloadSource} block`, {
nazarhussain marked this conversation as resolved.
Show resolved Hide resolved
builderSelection,
// winston logger doesn't like bigint
builderBoostFactor: `${builderBoostFactor}`,
enginePayloadValue: `${enginePayloadValue}`,
builderPayloadValue: `${builderPayloadValue}`,
consensusBlockValueEngine: `${consensusBlockValueEngine}`,
consensusBlockValueBuilder: `${consensusBlockValueBuilder}`,
blockValueEngine: `${blockValueEngine}`,
blockValueBuilder: `${blockValueBuilder}`,
shouldOverrideBuilder,
slot,
});
} else if (fullBlock && !blindedBlock) {
executionPayloadSource = ProducedBlockSource.engine;
logger.info("Selected engine block: no builder block produced", {
// winston logger doesn't like bigint
enginePayloadValue: `${enginePayloadValue}`,
consensusBlockValueEngine: `${consensusBlockValueEngine}`,
blockValueEngine: `${blockValueEngine}`,
shouldOverrideBuilder,
slot,
});
} else if (blindedBlock && !fullBlock) {
executionPayloadSource = ProducedBlockSource.builder;
logger.info("Selected builder block: no engine block produced", {
// winston logger doesn't like bigint
builderPayloadValue: `${builderPayloadValue}`,
consensusBlockValueBuilder: `${consensusBlockValueBuilder}`,
blockValueBuilder: `${blockValueBuilder}`,
shouldOverrideBuilder,
engineConsensusValue: `${engineConsensusValue}`,
builderConsensusValue: `${builderConsensusValue}`,
engineBlockValue: `${engineBlockValue}`,
builderBlockValue: `${builderBlockValue}`,
shouldOverrideBuilder: engineBlock.shouldOverrideBuilder,
slot,
});
}

if (executionPayloadSource === null) {
throw Error(`Failed to produce engine or builder block for slot=${slot}`);
if (executionPayloadSource === ProducedBlockSource.engine) {
return {
...engineBlock,
executionPayloadBlinded: false,
executionPayloadSource,
} as routes.validator.ProduceBlockOrContentsRes & {
executionPayloadBlinded: false;
executionPayloadSource: ProducedBlockSource;
};
} else {
return {
...builderBlock,
executionPayloadBlinded: true,
executionPayloadSource,
} as routes.validator.ProduceBlindedBlockRes & {
executionPayloadBlinded: true;
executionPayloadSource: ProducedBlockSource;
};
}
}

if (executionPayloadSource === ProducedBlockSource.engine) {
return {
...fullBlock,
executionPayloadBlinded: false,
executionPayloadSource,
} as routes.validator.ProduceBlockOrContentsRes & {
executionPayloadBlinded: false;
executionPayloadSource: ProducedBlockSource;
};
} else {
return {
...blindedBlock,
executionPayloadBlinded: true,
executionPayloadSource,
} as routes.validator.ProduceBlindedBlockRes & {
executionPayloadBlinded: true;
executionPayloadSource: ProducedBlockSource;
};
}
throw Error("Unreachable error occurred during the builder and execution block production");
};

const produceBlock: ServerApi<routes.validator.Api>["produceBlock"] = async function produceBlock(
Expand Down