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
30 changes: 19 additions & 11 deletions packages/api/src/beacon/routes/validator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import {
ValidatorIndex,
RootHex,
StringType,
Wei,
} from "@lodestar/types";
import {
RoutesData,
Expand All @@ -24,6 +25,7 @@ import {
ContainerData,
Schema,
WithVersion,
WithBlockValue,
reqOnlyBody,
ReqSerializers,
jsonType,
Expand Down Expand Up @@ -147,7 +149,11 @@ export type Api = {
* @returns any Success response
* @throws ApiError
*/
produceBlock(slot: Slot, randaoReveal: BLSSignature, graffiti: string): Promise<{data: allForks.BeaconBlock}>;
produceBlock(
slot: Slot,
randaoReveal: BLSSignature,
graffiti: string
): Promise<{data: allForks.BeaconBlock; blockValue: Wei}>;

/**
* Requests a beacon node to produce a valid block, which can then be signed by a validator.
Expand All @@ -163,13 +169,13 @@ export type Api = {
slot: Slot,
randaoReveal: BLSSignature,
graffiti: string
): Promise<{data: allForks.BeaconBlock; version: ForkName}>;
): Promise<{data: allForks.BeaconBlock; version: ForkName; blockValue: Wei}>;

produceBlindedBlock(
slot: Slot,
randaoReveal: BLSSignature,
graffiti: string
): Promise<{data: allForks.BlindedBeaconBlock; version: ForkName}>;
): Promise<{data: allForks.BlindedBeaconBlock; version: ForkName; blockValue: Wei}>;

/**
* Produce an attestation data
Expand Down Expand Up @@ -441,14 +447,16 @@ export function getReturnTypes(): ReturnTypes<Api> {
getAttesterDuties: WithDependentRootExecutionOptimistic(ArrayOf(AttesterDuty)),
getProposerDuties: WithDependentRootExecutionOptimistic(ArrayOf(ProposerDuty)),
getSyncCommitteeDuties: ContainerDataExecutionOptimistic(ArrayOf(SyncDuty)),
produceBlock: ContainerData(ssz.phase0.BeaconBlock),
produceBlockV2: WithVersion((fork: ForkName) => ssz[fork].BeaconBlock),
produceBlindedBlock: WithVersion((fork: ForkName) => {
if (fork === ForkName.phase0 || fork === ForkName.altair) {
throw Error(`No BlindedBlock for fork ${fork} previous to bellatrix`);
}
return ssz[fork].BlindedBeaconBlock;
}),
produceBlock: WithBlockValue(ContainerData(ssz.phase0.BeaconBlock)),
produceBlockV2: WithBlockValue(WithVersion((fork: ForkName) => ssz[fork].BeaconBlock)),
produceBlindedBlock: WithBlockValue(
WithVersion((fork: ForkName) => {
if (fork === ForkName.phase0 || fork === ForkName.altair) {
throw Error(`No BlindedBlock for fork ${fork} previous to bellatrix`);
}
return ssz[fork].BlindedBeaconBlock;
})
),
produceAttestationData: ContainerData(ssz.phase0.AttestationData),
produceSyncCommitteeContribution: ContainerData(ssz.altair.SyncCommitteeContribution),
getAggregatedAttestation: ContainerData(ssz.phase0.Attestation),
Expand Down
16 changes: 16 additions & 0 deletions packages/api/src/utils/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,22 @@ export function WithExecutionOptimistic<T extends {data: unknown}>(
};
}

/**
* SSZ factory helper to wrap an existing type with `{blockValue: Wei}`
*/
export function WithBlockValue<T extends {data: unknown}>(type: TypeJson<T>): TypeJson<T & {blockValue: bigint}> {
return {
toJson: ({blockValue, ...data}) => ({
...(type.toJson((data as unknown) as T) as Record<string, unknown>),
block_value: blockValue.toString(),
}),
fromJson: ({block_value, ...data}: T & {block_value: string}) => ({
...type.fromJson(data),
blockValue: BigInt(block_value),
}),
};
}

type JsonCase = "snake" | "constant" | "camel" | "param" | "header" | "pascal" | "dot" | "notransform";

/** Helper to only translate casing */
Expand Down
10 changes: 7 additions & 3 deletions packages/api/test/unit/beacon/testData/validator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,15 +44,19 @@ export const testData: GenericServerTestCases<Api> = {
},
produceBlock: {
args: [32000, randaoReveal, graffiti],
res: {data: ssz.phase0.BeaconBlock.defaultValue()},
res: {data: ssz.phase0.BeaconBlock.defaultValue(), blockValue: ssz.Wei.defaultValue()},
},
produceBlockV2: {
args: [32000, randaoReveal, graffiti],
res: {data: ssz.altair.BeaconBlock.defaultValue(), version: ForkName.altair},
res: {data: ssz.altair.BeaconBlock.defaultValue(), version: ForkName.altair, blockValue: ssz.Wei.defaultValue()},
},
produceBlindedBlock: {
args: [32000, randaoReveal, graffiti],
res: {data: ssz.bellatrix.BlindedBeaconBlock.defaultValue(), version: ForkName.bellatrix},
res: {
data: ssz.bellatrix.BlindedBeaconBlock.defaultValue(),
version: ForkName.bellatrix,
blockValue: ssz.Wei.defaultValue(),
},
},
produceAttestationData: {
args: [2, 32000],
Expand Down
8 changes: 4 additions & 4 deletions packages/beacon-node/src/api/impl/validator/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -203,13 +203,13 @@ export function getValidatorApi({chain, config, logger, metrics, network, sync}:
chain.forkChoice.updateHead();

timer = metrics?.blockProductionTime.startTimer();
const block = await chain.produceBlindedBlock({
const {block, blockValue} = await chain.produceBlindedBlock({
slot,
randaoReveal,
graffiti: toGraffitiBuffer(graffiti || ""),
});
metrics?.blockProductionSuccess.inc();
return {data: block, version: config.getForkName(block.slot)};
return {data: block, version: config.getForkName(block.slot), blockValue};
} finally {
if (timer) timer();
}
Expand All @@ -233,14 +233,14 @@ export function getValidatorApi({chain, config, logger, metrics, network, sync}:
chain.recomputeForkChoiceHead();

timer = metrics?.blockProductionTime.startTimer();
const block = await chain.produceBlock({
const {block, blockValue} = await chain.produceBlock({
slot,
randaoReveal,
graffiti: toGraffitiBuffer(graffiti || ""),
});
metrics?.blockProductionSuccess.inc();
metrics?.blockProductionNumAggregated.observe(block.body.attestations.length);
return {data: block, version: config.getForkName(block.slot)};
return {data: block, version: config.getForkName(block.slot), blockValue};
} finally {
if (timer) timer();
}
Expand Down
14 changes: 8 additions & 6 deletions packages/beacon-node/src/chain/chain.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import {
PubkeyIndexMap,
} from "@lodestar/state-transition";
import {IBeaconConfig} from "@lodestar/config";
import {allForks, UintNum64, Root, phase0, Slot, RootHex, Epoch, ValidatorIndex, eip4844} from "@lodestar/types";
import {allForks, UintNum64, Root, phase0, Slot, RootHex, Epoch, ValidatorIndex, eip4844, Wei} from "@lodestar/types";
import {CheckpointWithHex, ExecutionStatus, IForkChoice, ProtoBlock} from "@lodestar/fork-choice";
import {ProcessShutdownCallback} from "@lodestar/validator";
import {ILogger, pruneSetToMax, toHex} from "@lodestar/utils";
Expand Down Expand Up @@ -353,25 +353,27 @@ export class BeaconChain implements IBeaconChain {
return await this.db.block.get(fromHexString(block.blockRoot));
}

produceBlock(blockAttributes: BlockAttributes): Promise<allForks.BeaconBlock> {
produceBlock(blockAttributes: BlockAttributes): Promise<{block: allForks.BeaconBlock; blockValue: Wei}> {
return this.produceBlockWrapper<BlockType.Full>(BlockType.Full, blockAttributes);
}

produceBlindedBlock(blockAttributes: BlockAttributes): Promise<allForks.BlindedBeaconBlock> {
produceBlindedBlock(
blockAttributes: BlockAttributes
): Promise<{block: allForks.BlindedBeaconBlock; blockValue: Wei}> {
return this.produceBlockWrapper<BlockType.Blinded>(BlockType.Blinded, blockAttributes);
}

async produceBlockWrapper<T extends BlockType>(
blockType: T,
{randaoReveal, graffiti, slot}: BlockAttributes
): Promise<AssembledBlockType<T>> {
): Promise<{block: AssembledBlockType<T>; blockValue: Wei}> {
const head = this.forkChoice.getHead();
const state = await this.regen.getBlockSlotState(head.blockRoot, slot, RegenCaller.produceBlock);
const parentBlockRoot = fromHexString(head.blockRoot);
const proposerIndex = state.epochCtx.getBeaconProposer(slot);
const proposerPubKey = state.epochCtx.index2pubkey[proposerIndex].toBytes();

const {body, blobs} = await produceBlockBody.call(this, blockType, state, {
const {body, blobs, blockValue} = await produceBlockBody.call(this, blockType, state, {
randaoReveal,
graffiti,
slot,
Expand Down Expand Up @@ -410,7 +412,7 @@ export class BeaconChain implements IBeaconChain {
);
}

return block;
return {block, blockValue};
}

/**
Expand Down
6 changes: 3 additions & 3 deletions packages/beacon-node/src/chain/interface.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import {allForks, UintNum64, Root, phase0, Slot, RootHex, Epoch, ValidatorIndex, eip4844} from "@lodestar/types";
import {allForks, UintNum64, Root, phase0, Slot, RootHex, Epoch, ValidatorIndex, eip4844, Wei} from "@lodestar/types";
import {CachedBeaconStateAllForks} from "@lodestar/state-transition";
import {IBeaconConfig} from "@lodestar/config";
import {CompositeTypeAny, TreeView, Type} from "@chainsafe/ssz";
Expand Down Expand Up @@ -113,8 +113,8 @@ export interface IBeaconChain {

getBlobsSidecar(beaconBlock: eip4844.BeaconBlock): eip4844.BlobsSidecar;

produceBlock(blockAttributes: BlockAttributes): Promise<allForks.BeaconBlock>;
produceBlindedBlock(blockAttributes: BlockAttributes): Promise<allForks.BlindedBeaconBlock>;
produceBlock(blockAttributes: BlockAttributes): Promise<{block: allForks.BeaconBlock; blockValue: Wei}>;
produceBlindedBlock(blockAttributes: BlockAttributes): Promise<{block: allForks.BlindedBeaconBlock; blockValue: Wei}>;

/** Process a block until complete */
processBlock(block: BlockInput, opts?: ImportBlockOpts): Promise<void>;
Expand Down
30 changes: 21 additions & 9 deletions packages/beacon-node/src/chain/produceBlock/produceBlockBody.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
BLSSignature,
capella,
eip4844,
Wei,
} from "@lodestar/types";
import {
CachedBeaconStateAllForks,
Expand Down Expand Up @@ -89,11 +90,12 @@ export async function produceBlockBody<T extends BlockType>(
proposerIndex: ValidatorIndex;
proposerPubKey: BLSPubkey;
}
): Promise<{body: AssembledBodyType<T>; blobs: BlobsResult}> {
): Promise<{body: AssembledBodyType<T>; blobs: BlobsResult; blockValue: Wei}> {
// Type-safe for blobs variable. Translate 'null' value into 'preEIP4844' enum
// TODO: Not ideal, but better than just using null.
// TODO: Does not guarantee that preEIP4844 enum goes with a preEIP4844 block
let blobsResult: BlobsResult;
let blockValue: Wei;

// TODO:
// Iterate through the naive aggregation pool and ensure all the attestations from there
Expand Down Expand Up @@ -170,8 +172,9 @@ export async function produceBlockBody<T extends BlockType>(
proposerPubKey
);
(blockBody as allForks.BlindedBeaconBlockBody).executionPayloadHeader = builderRes.header;
blockValue = builderRes.blockValue;
if (ForkSeq[fork] >= ForkSeq.eip4844) {
const {blobKzgCommitments} = builderRes as {blobKzgCommitments: eip4844.BlobKzgCommitments};
const {blobKzgCommitments} = builderRes;
if (blobKzgCommitments === undefined) {
throw Error(`Invalid builder getHeader response for fork=${fork}, missing blobKzgCommitments`);
}
Expand Down Expand Up @@ -203,6 +206,7 @@ export async function produceBlockBody<T extends BlockType>(
fork
].ExecutionPayload.defaultValue();
blobsResult = {type: BlobsResultType.preEIP4844};
blockValue = BigInt(0);
} else {
const {prepType, payloadId} = prepareRes;
if (prepType !== PayloadPreparationType.Cached) {
Expand All @@ -214,12 +218,14 @@ export async function produceBlockBody<T extends BlockType>(
await sleep(PAYLOAD_GENERATION_TIME_MS);
}

const payload = await this.executionEngine.getPayload(fork, payloadId);
(blockBody as allForks.ExecutionBlockBody).executionPayload = payload;
const engineRes = await this.executionEngine.getPayload(fork, payloadId);
const {executionPayload} = engineRes;
(blockBody as allForks.ExecutionBlockBody).executionPayload = executionPayload;
blockValue = engineRes.blockValue;

const fetchedTime = Date.now() / 1000 - computeTimeAtSlot(this.config, blockSlot, this.genesisTime);
this.metrics?.blockPayload.payloadFetchedTime.observe({prepType}, fetchedTime);
if (payload.transactions.length === 0) {
if (executionPayload.transactions.length === 0) {
this.metrics?.blockPayload.emptyPayloads.inc({prepType});
}

Expand All @@ -231,14 +237,14 @@ export async function produceBlockBody<T extends BlockType>(
const blobsBundle = await this.executionEngine.getBlobsBundle(payloadId);

// Sanity check consistency between getPayload() and getBlobsBundle()
const blockHash = toHex(payload.blockHash);
const blockHash = toHex(executionPayload.blockHash);
if (blobsBundle.blockHash !== blockHash) {
throw Error(`blobsBundle incorrect blockHash ${blobsBundle.blockHash} != ${blockHash}`);
}

// Optionally sanity-check that the KZG commitments match the versioned hashes in the transactions
if (this.opts.sanityCheckExecutionEngineBlobs) {
validateBlobsAndKzgCommitments(payload, blobsBundle);
validateBlobsAndKzgCommitments(executionPayload, blobsBundle);
}

(blockBody as eip4844.BeaconBlockBody).blobKzgCommitments = blobsBundle.kzgs;
Expand All @@ -262,6 +268,7 @@ export async function produceBlockBody<T extends BlockType>(
fork
].ExecutionPayload.defaultValue();
blobsResult = {type: BlobsResultType.preEIP4844};
blockValue = BigInt(0);
} else {
// since merge transition is complete, we need a valid payload even if with an
// empty (transactions) one. defaultValue isn't gonna cut it!
Expand All @@ -271,14 +278,15 @@ export async function produceBlockBody<T extends BlockType>(
}
} else {
blobsResult = {type: BlobsResultType.preEIP4844};
blockValue = BigInt(0);
}

if (ForkSeq[fork] >= ForkSeq.capella) {
// TODO: blsToExecutionChanges should be passed in the produceBlock call
(blockBody as capella.BeaconBlockBody).blsToExecutionChanges = blsToExecutionChanges;
}

return {body: blockBody as AssembledBodyType<T>, blobs: blobsResult};
return {body: blockBody as AssembledBodyType<T>, blobs: blobsResult, blockValue};
}

/**
Expand Down Expand Up @@ -376,7 +384,11 @@ async function prepareExecutionPayloadHeader(
fork: ForkExecution,
state: CachedBeaconStateBellatrix,
proposerPubKey: BLSPubkey
): Promise<{header: allForks.ExecutionPayloadHeader; blobKzgCommitments?: eip4844.BlobKzgCommitments}> {
): Promise<{
header: allForks.ExecutionPayloadHeader;
blockValue: Wei;
blobKzgCommitments?: eip4844.BlobKzgCommitments;
}> {
if (!chain.executionBuilder) {
throw Error("executionBuilder required");
}
Expand Down
12 changes: 9 additions & 3 deletions packages/beacon-node/src/execution/builder/http.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import {allForks, bellatrix, Slot, Root, BLSPubkey, ssz, eip4844} from "@lodestar/types";
import {allForks, bellatrix, Slot, Root, BLSPubkey, ssz, eip4844, Wei} from "@lodestar/types";
import {IChainForkConfig} from "@lodestar/config";
import {getClient, Api as BuilderApi} from "@lodestar/api/builder";
import {byteArrayEquals, toHexString} from "@chainsafe/ssz";
Expand Down Expand Up @@ -57,9 +57,15 @@ export class ExecutionBuilderHttp implements IExecutionBuilder {
slot: Slot,
parentHash: Root,
proposerPubKey: BLSPubkey
): Promise<{header: allForks.ExecutionPayloadHeader; blobKzgCommitments?: eip4844.BlobKzgCommitments}> {
): Promise<{
header: allForks.ExecutionPayloadHeader;
blockValue: Wei;
blobKzgCommitments?: eip4844.BlobKzgCommitments;
}> {
const {data: signedBid} = await this.api.getHeader(slot, parentHash, proposerPubKey);
return signedBid.message;
const {header, value: blockValue} = signedBid.message;
const {blobKzgCommitments} = signedBid.message as {blobKzgCommitments?: eip4844.BlobKzgCommitments};
return {header, blockValue, blobKzgCommitments};
}

async submitBlindedBlock(signedBlock: allForks.SignedBlindedBeaconBlock): Promise<allForks.SignedBeaconBlock> {
Expand Down
8 changes: 6 additions & 2 deletions packages/beacon-node/src/execution/builder/interface.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import {allForks, bellatrix, Root, Slot, BLSPubkey, eip4844} from "@lodestar/types";
import {allForks, bellatrix, Root, Slot, BLSPubkey, eip4844, Wei} from "@lodestar/types";

export interface IExecutionBuilder {
/**
Expand All @@ -15,7 +15,11 @@ export interface IExecutionBuilder {
slot: Slot,
parentHash: Root,
proposerPubKey: BLSPubkey
): Promise<{header: allForks.ExecutionPayloadHeader; blobKzgCommitments?: eip4844.BlobKzgCommitments}>;
): Promise<{
header: allForks.ExecutionPayloadHeader;
blockValue: Wei;
blobKzgCommitments?: eip4844.BlobKzgCommitments;
}>;
submitBlindedBlock(signedBlock: allForks.SignedBlindedBeaconBlock): Promise<allForks.SignedBeaconBlock>;
submitBlindedBlockV2(
signedBlock: allForks.SignedBlindedBeaconBlock
Expand Down
7 changes: 5 additions & 2 deletions packages/beacon-node/src/execution/engine/http.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import {RootHex, allForks} from "@lodestar/types";
import {RootHex, allForks, Wei} from "@lodestar/types";
import {SLOTS_PER_EPOCH, ForkName, ForkSeq} from "@lodestar/params";

import {ErrorJsonRpcResponse, HttpRpcError} from "../../eth1/provider/jsonRpcHttpClient.js";
Expand Down Expand Up @@ -284,7 +284,10 @@ export class ExecutionEngineHttp implements IExecutionEngine {
* 2. The call MUST be responded with 5: Unavailable payload error if the building process identified by the payloadId doesn't exist.
* 3. Client software MAY stop the corresponding building process after serving this call.
*/
async getPayload(fork: ForkName, payloadId: PayloadId): Promise<allForks.ExecutionPayload> {
async getPayload(
fork: ForkName,
payloadId: PayloadId
): Promise<{executionPayload: allForks.ExecutionPayload; blockValue: Wei}> {
const method =
ForkSeq[fork] >= ForkSeq.eip4844
? "engine_getPayloadV3"
Expand Down
Loading