Skip to content
Merged
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
8 changes: 5 additions & 3 deletions docs/pages/run/validator-management/vc-configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -80,16 +80,18 @@ If you would like to set unique proposer metadata (e.g. fee recipient address) f

### Configure your builder selection and/or builder boost factor

If you are running a beacon node with connected builder relays, you may use these validator configurations to signal which block (builder vs. local execution) the beacon node should produce.
These validator configurations signal whether the beacon node should prefer a builder bid or a local execution payload. Before Gloas, builder bids require configured builder relays. Starting with Gloas, builder bids may be received in-protocol over p2p or out-of-protocol through a builder API.

With produceBlockV3 introduced in Deneb hard fork, the [`--builder.boostFactor`](./validator-cli.md#--builderboostfactor) is a percentage multiplier the block producing beacon node must apply to boost (>100) or dampen (<100) builder block value for selection against execution block. The multiplier is ignored if [`--builder.selection`](./validator-cli.md#--builderselection) is set to anything other than `maxprofit`. Even though this is set on the validator client, the calculation is requested and applied on the beacon node itself. For more information, see the [produceBlockV3 Beacon API](https://ethereum.github.io/beacon-APIs/#/ValidatorRequiredApi/produceBlockV3).
With `produceBlockV3` introduced in Deneb hard fork, the [`--builder.boostFactor`](./validator-cli.md#--builderboostfactor) is a percentage multiplier the block producing beacon node must apply to boost (>100) or dampen (<100) builder block value for selection against execution block. The multiplier is ignored if [`--builder.selection`](./validator-cli.md#--builderselection) is set to anything other than `maxprofit`. Even though this is set on the validator client, the calculation is requested and applied on the beacon node itself. For more information, see the [produceBlockV3 Beacon API](https://ethereum.github.io/beacon-APIs/#/ValidatorRequiredApi/produceBlockV3).

With `produceBlockV4` introduced in Gloas, the validator client converts [`--builder.selection`](./validator-cli.md#--builderselection) aliases to a global `builder_boost_factor`, which applies to viable builder bids regardless of whether they were received over p2p or through a builder API. A value of `0` prefers the local payload but uses a viable builder bid if local production fails or is delayed. A value of `100` selects by profit, and `18446744073709551615` (2\*\*64 - 1) prefers the builder bid with local production as fallback. For more information, see the [produceBlockV4 Beacon API](https://ethereum.github.io/beacon-APIs/#/ValidatorRequiredApi/produceBlockV4).

With Lodestar's [`--builder.selection`](./validator-cli.md#--builderselection) validator options, you can select:

- `default`: Default setting for Lodestar set at `--builder.boostFactor=90`. This default setting will have a local block boost of ~10%. Note that this value might change in the future depending on what we think is the most appropriate value to help improve censorship resistance of Ethereum.
- `maxprofit`: An alias of `--builder.boostFactor=100`, which will always choose the more profitable block. Using this option, you may customize your `--builder.boostFactor` to your preference. Examples of its usage are below.
- `executionalways`: An alias of `--builder.boostFactor=0`, which will select the local execution block, unless it fails to produce due to an error or a delay in the response from the execution client.
- `executiononly`: Beacon node will be requested to produce local execution block even if builder relays are configured. This option will always select the local execution block and will error if it couldn't produce one.
- `executiononly`: Pre-Gloas only. The beacon node will produce a local execution block even if builder relays are configured and will error if it cannot produce one. Starting with Gloas, this is treated as `executionalways` so a viable builder bid can prevent a missed proposal when local production fails or is delayed.
- `builderalways`: An alias of `--builder.boostFactor=18446744073709551615` (2\*\*64 - 1), which will select the builder block, unless the builder block fails to produce. The builder block may fail to produce if it's not available, not timely or there is an indication of censorship via `shouldOverrideBuilder` from the execution payload response.

#### Calculating builder boost factor with examples
Expand Down
13 changes: 7 additions & 6 deletions packages/api/src/beacon/routes/validator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,12 @@ export type ExtraProduceBlockOpts = {
blindedLocal?: boolean;
};

/** Lodestar-specific (non-standardized) options */
export type ExtraProduceBlockV4Opts = {
feeRecipient?: string;
strictFeeRecipientCheck?: boolean;
};

export const ProduceBlockV3MetaType = new ContainerType(
{
...VersionType.fields,
Expand Down Expand Up @@ -446,15 +452,14 @@ export type Endpoints = {
builderBoostFactor?: UintBn64;
/** Include execution payload envelope and blobs in the response when self-building */
includePayload: boolean;
} & Omit<ExtraProduceBlockOpts, "blindedLocal">,
} & ExtraProduceBlockV4Opts,
Comment thread
matthewkeil marked this conversation as resolved.
{
params: {slot: number};
query: {
randao_reveal: string;
graffiti?: string;
skip_randao_verification?: string;
fee_recipient?: string;
builder_selection?: string;
builder_boost_factor?: string;
strict_fee_recipient_check?: boolean;
include_payload: boolean;
Expand Down Expand Up @@ -918,7 +923,6 @@ export function getDefinitions(config: ChainForkConfig): RouteDefinitions<Endpoi
graffiti,
skipRandaoVerification,
feeRecipient,
builderSelection,
builderBoostFactor,
strictFeeRecipientCheck,
includePayload,
Expand All @@ -929,7 +933,6 @@ export function getDefinitions(config: ChainForkConfig): RouteDefinitions<Endpoi
graffiti: toGraffitiHex(graffiti),
skip_randao_verification: writeSkipRandaoVerification(skipRandaoVerification),
fee_recipient: feeRecipient,
builder_selection: builderSelection,
builder_boost_factor: builderBoostFactor?.toString(),
strict_fee_recipient_check: strictFeeRecipientCheck,
include_payload: includePayload,
Expand All @@ -941,7 +944,6 @@ export function getDefinitions(config: ChainForkConfig): RouteDefinitions<Endpoi
graffiti: fromGraffitiHex(query.graffiti),
skipRandaoVerification: parseSkipRandaoVerification(query.skip_randao_verification),
feeRecipient: query.fee_recipient,
builderSelection: query.builder_selection as BuilderSelection,
builderBoostFactor: parseBuilderBoostFactor(query.builder_boost_factor),
strictFeeRecipientCheck: query.strict_fee_recipient_check,
includePayload: query.include_payload,
Expand All @@ -953,7 +955,6 @@ export function getDefinitions(config: ChainForkConfig): RouteDefinitions<Endpoi
graffiti: Schema.String,
skip_randao_verification: Schema.String,
fee_recipient: Schema.String,
builder_selection: Schema.String,
builder_boost_factor: Schema.String,
strict_fee_recipient_check: Schema.Boolean,
include_payload: Schema.BooleanRequired,
Expand Down
1 change: 0 additions & 1 deletion packages/api/test/unit/beacon/testData/validator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,6 @@ export const testData: GenericServerTestCases<Endpoints> = {
skipRandaoVerification: true,
builderBoostFactor: 0n,
feeRecipient,
builderSelection: BuilderSelection.ExecutionAlways,
strictFeeRecipientCheck: true,
includePayload: true,
},
Expand Down
47 changes: 21 additions & 26 deletions packages/beacon-node/src/api/impl/validator/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,12 @@ import {getStateResponseWithRegen} from "../beacon/state/utils.js";
import {ApiError, FailureList, IndexedError, NodeIsSyncing, OnlySupportedByDVT} from "../errors.js";
import {ApiModules} from "../types.js";
import {notWhileSyncing} from "../utils.js";
import {computeSubnetForCommitteesAtSlot, getPubkeysForIndices, selectBlockProductionSource} from "./utils.js";
import {
computeSubnetForCommitteesAtSlot,
getPubkeysForIndices,
selectBlockProductionSource,
selectBlockProductionSourceByBoostFactor,
} from "./utils.js";

/**
* Cutoff time to wait from start of the slot for execution and builder block production apis to resolve.
Expand Down Expand Up @@ -577,8 +582,7 @@ export function getValidatorApi(
builderSelection,
isBuilderEnabled,
strictFeeRecipientCheck,
// winston logger doesn't like bigint
builderBoostFactor: `${builderBoostFactor}`,
builderBoostFactor,
Comment thread
matthewkeil marked this conversation as resolved.
};

logger.verbose("Assembling block with produceEngineOrBuilderBlock", loggerContext);
Expand Down Expand Up @@ -851,8 +855,8 @@ export function getValidatorApi(
randaoReveal,
graffiti,
feeRecipient,
strictFeeRecipientCheck,
includePayload,
builderSelection,
builderBoostFactor,
}) {
const fork = config.getForkName(slot);
Expand All @@ -861,11 +865,6 @@ export function getValidatorApi(
throw new ApiError(400, `produceBlockV4 not supported for pre-gloas fork=${fork}`);
}

builderSelection = builderSelection ?? routes.validator.BuilderSelection.MaxProfit;
if (builderSelection === routes.validator.BuilderSelection.BuilderOnly) {
logger.warn("Builder selection builderonly is no longer supported, treating as builderalways");
builderSelection = routes.validator.BuilderSelection.BuilderAlways;
}
builderBoostFactor = builderBoostFactor ?? BigInt(100);
if (builderBoostFactor > MAX_BUILDER_BOOST_FACTOR) {
throw new ApiError(400, `Invalid builderBoostFactor=${builderBoostFactor} > MAX_BUILDER_BOOST_FACTOR`);
Expand Down Expand Up @@ -896,23 +895,20 @@ export function getValidatorApi(
// TODO GLOAS: add external builder api support when it is implemented
const isBuildingOnFull = chain.forkChoice.shouldBuildOnFull(parentBlock, slot);
const bidParentBlockHash = isBuildingOnFull ? parentBlock.executionPayloadBlockHash : parentBlock.parentBlockHash;
// Bids are only skipped entirely with executiononly or while the circuit breaker is active,
// other engine-preferring selections still build a block with the best bid as fallback in
// case local production fails
const circuitBreakerActive = chain.builderCircuitBreaker.isActive(slot);
const builderBid =
builderSelection === routes.validator.BuilderSelection.ExecutionOnly || circuitBreakerActive
? null
: chain.executionPayloadBidPool.getBestBid(slot, bidParentBlockHash, parentBlockRootHex);
// Keep a builder bid as fallback unless the circuit breaker is active
const builderBid = circuitBreakerActive

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

could easily keep the executiononly and skip the proposal here instead of mucking with the min_bid, right?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

yes, if we wanna keep executiononly, but I wanna get rid of that

? null
Comment on lines +899 to +901

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

unsure if we should still pick a builder bid even if the circuit breaker is active in case local fails, it's not very likely that local payload production fails but having bid as a fallback could be good still, on the other hand, it could also be that builders are attacking the network with large payloads that are slow to process or equivocations, or other attacks, so not selecting builder bids at all might be a safety feature

: chain.executionPayloadBidPool.getBestBid(slot, bidParentBlockHash, parentBlockRootHex);

const logCtx = {
slot,
parentSlot,
parentBlockRoot: parentBlockRootHex,
parentBlockHash: parentBlock.executionPayloadBlockHash,
fork,
builderSelection,
builderBoostFactor,
strictFeeRecipientCheck,
circuitBreakerActive,
...(builderBid !== null
? {
Expand All @@ -936,6 +932,7 @@ export function getValidatorApi(
randaoReveal,
graffiti: graffitiBytes,
feeRecipient,
strictFeeRecipientCheck,
commonBlockBodyPromise,
};

Expand All @@ -960,12 +957,8 @@ export function getValidatorApi(
chain.produceBlock(baseAttrs)
).then((engineBlock) => {
// No need to wait for the bid block if the engine block will always be selected due to
// suspected builder censorship, a builder boost factor of 0 or executionalways selection
if (
engineBlock.shouldOverrideBuilder ||
builderBoostFactor === BigInt(0) ||
builderSelection === routes.validator.BuilderSelection.ExecutionAlways
) {
// suspected builder censorship or a builder boost factor of 0
if (engineBlock.shouldOverrideBuilder || builderBoostFactor === BigInt(0)) {
controller.abort();
}
return engineBlock;
Expand Down Expand Up @@ -994,8 +987,7 @@ export function getValidatorApi(
});
logger.warn("Selected local block: censorship suspected in builder bid", logCtx);
} else if (engineResult.status === "fulfilled" && bidResult.status === "fulfilled") {
const result = selectBlockProductionSource({
builderSelection,
const result = selectBlockProductionSourceByBoostFactor({
builderBoostFactor,
engineExecutionPayloadValue: engineResult.value.executionPayloadValue,
// The bid value is the payment to the proposer, in Gwei
Expand Down Expand Up @@ -1061,7 +1053,10 @@ export function getValidatorApi(
root: blockRoot,
});
if (chain.opts.persistProducedBlocks) {
void chain.persistBlock(block, "produced_engine_block");
void chain.persistBlock(
block,
source === ProducedBlockSource.builder ? "produced_builder_block" : "produced_engine_block"
);
}

// Include the payload for self-builds unless disabled (stateless flow)
Expand Down
45 changes: 30 additions & 15 deletions packages/beacon-node/src/api/impl/validator/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,24 +59,39 @@ export function selectBlockProductionSource({
return {source: ProducedBlockSource.engine, reason: EngineBlockSelectionReason.EnginePreferred};

case routes.validator.BuilderSelection.Default:
case routes.validator.BuilderSelection.MaxProfit: {
if (builderBoostFactor === BigInt(0)) {
return {source: ProducedBlockSource.engine, reason: EngineBlockSelectionReason.EnginePreferred};
}

if (builderBoostFactor === MAX_BUILDER_BOOST_FACTOR) {
return {source: ProducedBlockSource.builder, reason: BuilderBlockSelectionReason.BuilderPreferred};
}

if (engineExecutionPayloadValue >= (builderExecutionPayloadValue * builderBoostFactor) / BigInt(100)) {
return {source: ProducedBlockSource.engine, reason: EngineBlockSelectionReason.BlockValue};
}

return {source: ProducedBlockSource.builder, reason: BuilderBlockSelectionReason.BlockValue};
}
case routes.validator.BuilderSelection.MaxProfit:
return selectBlockProductionSourceByBoostFactor({
engineExecutionPayloadValue,
builderExecutionPayloadValue,
builderBoostFactor,
});

case routes.validator.BuilderSelection.BuilderAlways:
case routes.validator.BuilderSelection.BuilderOnly:
return {source: ProducedBlockSource.builder, reason: BuilderBlockSelectionReason.BuilderPreferred};
}
}

export function selectBlockProductionSourceByBoostFactor({
engineExecutionPayloadValue,
builderExecutionPayloadValue,
builderBoostFactor,
}: {
engineExecutionPayloadValue: bigint;
builderExecutionPayloadValue: bigint;
builderBoostFactor: bigint;
}): BlockSelectionResult {
if (builderBoostFactor === BigInt(0)) {
return {source: ProducedBlockSource.engine, reason: EngineBlockSelectionReason.EnginePreferred};
}

if (builderBoostFactor === MAX_BUILDER_BOOST_FACTOR) {
return {source: ProducedBlockSource.builder, reason: BuilderBlockSelectionReason.BuilderPreferred};
}

if (engineExecutionPayloadValue >= (builderExecutionPayloadValue * builderBoostFactor) / BigInt(100)) {
return {source: ProducedBlockSource.engine, reason: EngineBlockSelectionReason.BlockValue};
}

return {source: ProducedBlockSource.builder, reason: BuilderBlockSelectionReason.BlockValue};
}
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 @@ -1072,6 +1072,7 @@ export class BeaconChain implements IBeaconChain {
graffiti,
slot,
feeRecipient,
strictFeeRecipientCheck,
commonBlockBodyPromise,
parentBlock,
builderBid,
Expand Down Expand Up @@ -1100,6 +1101,7 @@ export class BeaconChain implements IBeaconChain {
graffiti,
slot,
feeRecipient,
strictFeeRecipientCheck,
parentBlock,
proposerIndex,
proposerPubKey,
Expand Down
13 changes: 13 additions & 0 deletions packages/beacon-node/src/chain/produceBlock/produceBlockBody.ts
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,8 @@ export type BlockAttributes = {
slot: Slot;
parentBlock: ProtoBlock;
feeRecipient?: string;
/** Verify that a locally produced execution payload uses `feeRecipient`. */
strictFeeRecipientCheck?: boolean;
Comment thread
matthewkeil marked this conversation as resolved.
/** When provided, build block with this builder bid instead of a self-build bid */
builderBid?: gloas.SignedExecutionPayloadBid;
};
Expand Down Expand Up @@ -201,6 +203,7 @@ export async function produceBlockBody<T extends BlockType>(
const {
slot: blockSlot,
feeRecipient: requestedFeeRecipient,
strictFeeRecipientCheck,
parentBlock,
proposerIndex,
proposerPubKey,
Expand Down Expand Up @@ -334,6 +337,16 @@ export async function produceBlockBody<T extends BlockType>(
executionPayloadValue = payloadRes.executionPayloadValue;
shouldOverrideBuilder = payloadRes.shouldOverrideBuilder;

if (
strictFeeRecipientCheck &&
requestedFeeRecipient &&
!byteArrayEquals(executionPayload.feeRecipient, fromHex(requestedFeeRecipient))
) {
throw Error(
`Invalid feeRecipient set in engine payload expected=${requestedFeeRecipient} actual=${toHex(executionPayload.feeRecipient)}`
);
}

if (blobsBundle === undefined) {
throw Error(`Missing blobsBundle response from getPayload at fork=${fork}`);
}
Expand Down
Loading
Loading