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

Add block production support for merge blocks #3203

Closed
wants to merge 4 commits into from
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
12 changes: 11 additions & 1 deletion packages/beacon-state-transition/src/merge/utils.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import {merge, ssz} from "@chainsafe/lodestar-types";
import {allForks, merge, ssz} from "@chainsafe/lodestar-types";

/**
* Execution enabled = merge is done.
Expand Down Expand Up @@ -32,3 +32,13 @@ export function isMergeComplete(state: merge.BeaconState): boolean {
ssz.merge.ExecutionPayloadHeader.defaultTreeBacked()
);
}

/** Type guard for merge.BeaconState */
export function isMergeStateType(state: allForks.BeaconState): state is merge.BeaconState {
return (state as merge.BeaconState).latestExecutionPayloadHeader !== undefined;
}

/** Type guard for merge.BeaconBlockBody */
export function isMergeBlockBodyType(blockBody: allForks.BeaconBlockBody): blockBody is merge.BeaconBlockBody {
return (blockBody as merge.BeaconBlockBody).executionPayload !== undefined;
}
16 changes: 2 additions & 14 deletions packages/cli/src/cmds/beacon/initBeaconState.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,7 @@ import {TreeBacked} from "@chainsafe/ssz";
import {createIBeaconConfig, IBeaconConfig, IChainForkConfig} from "@chainsafe/lodestar-config";
import {fromHex, ILogger} from "@chainsafe/lodestar-utils";
import {computeEpochAtSlot, allForks} from "@chainsafe/lodestar-beacon-state-transition";
import {
IBeaconDb,
Eth1Provider,
IBeaconNodeOptions,
initStateFromAnchorState,
initStateFromEth1,
} from "@chainsafe/lodestar";
import {IBeaconDb, IBeaconNodeOptions, initStateFromAnchorState, initStateFromEth1} from "@chainsafe/lodestar";
// eslint-disable-next-line no-restricted-imports
import {getStateTypeFromBytes} from "@chainsafe/lodestar/lib/util/multifork";
import {downloadOrLoadFile} from "../../util";
Expand Down Expand Up @@ -127,13 +121,7 @@ export async function initBeaconState(
const config = createIBeaconConfig(chainForkConfig, anchorState.genesisValidatorsRoot);
return await initStateFromAnchorState(config, db, logger, anchorState);
} else {
return await initStateFromEth1(
chainForkConfig,
db,
logger,
new Eth1Provider(chainForkConfig, options.eth1, signal),
signal
);
return await initStateFromEth1({config: chainForkConfig, db, logger, opts: options.eth1, signal});
}
}
}
15 changes: 7 additions & 8 deletions packages/fork-choice/src/forkChoice/forkChoice.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ export class ForkChoice implements IForkChoice {
private readonly config: IChainForkConfig,
private readonly fcStore: IForkChoiceStore,
/** Nullable until merge time comes */
private transitionStore: ITransitionStore | null,
private readonly transitionStore: ITransitionStore,
/** The underlying representation of the block DAG. */
private readonly protoArray: ProtoArray,
/**
Expand All @@ -91,12 +91,6 @@ export class ForkChoice implements IForkChoice {
this.head = this.updateHead();
}

/** For merge transition. Initialize transition store when merge fork condition is met */
initializeTransitionStore(transitionStore: ITransitionStore): void {
if (this.transitionStore !== null) throw Error("transitionStore already initialized");
this.transitionStore = transitionStore;
}

/**
* Returns the block root of an ancestor of `blockRoot` at the given `slot`.
* (Note: `slot` refers to the block that is *returned*, not the one that is supplied.)
Expand Down Expand Up @@ -304,7 +298,12 @@ export class ForkChoice implements IForkChoice {
});
}

if (this.transitionStore && merge.isMergeBlock(state as merge.BeaconState, (block as merge.BeaconBlock).body)) {
if (
this.transitionStore.initialized &&
merge.isMergeStateType(state) &&
merge.isMergeBlockBodyType(block.body) &&
merge.isMergeBlock(state, block.body)
) {
const {powBlock, powBlockParent} = preCachedData || {};
if (!powBlock) throw Error("onBlock preCachedData must include powBlock");
if (!powBlockParent) throw Error("onBlock preCachedData must include powBlock");
Expand Down
3 changes: 0 additions & 3 deletions packages/fork-choice/src/forkChoice/interface.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,13 @@
import {Epoch, Slot, ValidatorIndex, phase0, allForks, Root, RootHex} from "@chainsafe/lodestar-types";
import {IProtoBlock} from "../protoArray/interface";
import {CheckpointWithHex} from "./store";
import {ITransitionStore} from "./transitionStore";

export type CheckpointHex = {
epoch: Epoch;
root: RootHex;
};

export interface IForkChoice {
/** For merge transition. Initialize transition store when merge fork condition is met */
initializeTransitionStore(transitionStore: ITransitionStore): void;
/**
* Returns the block root of an ancestor of `block_root` at the given `slot`. (Note: `slot` refers
* to the block that is *returned*, not the one that is supplied.)
Expand Down
7 changes: 7 additions & 0 deletions packages/fork-choice/src/forkChoice/transitionStore.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,13 @@
export interface ITransitionStore {
/**
* Equivalent to spec check `TransitionStore not null`.
* Since the TransitionStore is used in fork-choice + block production it's simpler for it to be always not null,
* and handle the initialized state internally.
*/
initialized: boolean;
/**
* Cumulative total difficulty over the entire Ethereum POW network.
* Value may not be always available
*/
terminalTotalDifficulty: bigint;
}
4 changes: 2 additions & 2 deletions packages/fork-choice/test/unit/forkChoice/forkChoice.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import {ForkChoice, IForkChoiceStore, ProtoArray} from "../../../src";
import {ForkChoice, IForkChoiceStore, ITransitionStore, ProtoArray} from "../../../src";
import {config} from "@chainsafe/lodestar-config/default";
import {expect} from "chai";
import {fromHexString} from "@chainsafe/ssz";
Expand Down Expand Up @@ -44,7 +44,7 @@ describe("Forkchoice", function () {
bestJustifiedCheckpoint: {epoch: genesisEpoch, root: fromHexString(finalizedRoot), rootHex: finalizedRoot},
};

const transitionStore = null;
const transitionStore: ITransitionStore = {initialized: false, terminalTotalDifficulty: BigInt(0)};

it("getAllAncestorBlocks", function () {
protoArr.onBlock(block);
Expand Down
1 change: 1 addition & 0 deletions packages/lodestar/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@
"@ethersproject/abi": "^5.0.0",
"@types/datastore-level": "^3.0.0",
"bl": "^5.0.0",
"buffer-xor": "^2.0.2",
"cross-fetch": "^3.1.4",
"datastore-level": "^6.0.2",
"deepmerge": "^3.2.0",
Expand Down
2 changes: 0 additions & 2 deletions packages/lodestar/src/api/impl/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,12 @@ import {IBeaconChain} from "../../chain";
import {IBeaconDb} from "../../db";
import {IBeaconSync} from "../../sync";
import {INetwork} from "../../network";
import {IEth1ForBlockProduction} from "../../eth1";
import {IMetrics} from "../../metrics";

export type ApiModules = {
config: IChainForkConfig;
chain: IBeaconChain;
db: IBeaconDb;
eth1: IEth1ForBlockProduction;
logger: ILogger;
metrics: IMetrics | null;
network: INetwork;
Expand Down
17 changes: 2 additions & 15 deletions packages/lodestar/src/api/impl/validator/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,15 +55,7 @@ const SYNC_TOLERANCE_EPOCHS = 1;
* Server implementation for handling validator duties.
* See `@chainsafe/lodestar-validator/src/api` for the client implementation).
*/
export function getValidatorApi({
chain,
config,
eth1,
logger,
metrics,
network,
sync,
}: ApiModules): routes.validator.Api {
export function getValidatorApi({chain, config, logger, metrics, network, sync}: ApiModules): routes.validator.Api {
let genesisBlockRoot: Root | null = null;

/** Compute and cache the genesis block root */
Expand Down Expand Up @@ -149,12 +141,7 @@ export function getValidatorApi({
await waitForSlot(slot); // Must never request for a future slot > currentSlot

timer = metrics?.blockProductionTime.startTimer();
const block = await assembleBlock(
{config, chain, eth1, metrics},
slot,
randaoReveal,
toGraffitiBuffer(graffiti || "")
);
const block = await assembleBlock({chain, metrics}, slot, randaoReveal, toGraffitiBuffer(graffiti || ""));
metrics?.blockProductionSuccess.inc();
return {data: block, version: config.getForkName(block.slot)};
} finally {
Expand Down
2 changes: 1 addition & 1 deletion packages/lodestar/src/chain/blocks/stateTransition.ts
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ export async function runStateTransition(

// current justified checkpoint should be prev epoch or current epoch if it's just updated
// it should always have epochBalances there bc it's a checkpoint state, ie got through processEpoch
let justifiedBalances: number[] = [];
let justifiedBalances: number[] | undefined = undefined;
if (postState.currentJustifiedCheckpoint.epoch > forkChoice.getJustifiedCheckpoint().epoch) {
const justifiedState = checkpointStateCache.get(toCheckpointHex(postState.currentJustifiedCheckpoint));
if (!justifiedState) {
Expand Down
15 changes: 13 additions & 2 deletions packages/lodestar/src/chain/chain.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,10 +43,16 @@ import {
import {ForkDigestContext, IForkDigestContext} from "../util/forkDigestContext";
import {LightClientIniter} from "./lightClient";
import {Archiver} from "./archiver";
import {IEth1ForBlockProduction} from "../eth1";
import {IExecutionEngine} from "../executionEngine";

export class BeaconChain implements IBeaconChain {
readonly genesisTime: Number64;
readonly genesisValidatorsRoot: Root;
readonly eth1: IEth1ForBlockProduction;
readonly executionEngine: IExecutionEngine;
// Expose config for convenience in modularized functions
readonly config: IBeaconConfig;

bls: IBlsVerifier;
forkChoice: IForkChoice;
Expand Down Expand Up @@ -75,7 +81,6 @@ export class BeaconChain implements IBeaconChain {
readonly seenContributionAndProof = new SeenContributionAndProof();

protected readonly blockProcessor: BlockProcessor;
protected readonly config: IBeaconConfig;
protected readonly db: IBeaconDb;
protected readonly logger: ILogger;
protected readonly metrics: IMetrics | null;
Expand All @@ -97,13 +102,17 @@ export class BeaconChain implements IBeaconChain {
metrics,
anchorState,
transitionStore,
eth1,
executionEngine,
}: {
config: IBeaconConfig;
db: IBeaconDb;
logger: ILogger;
metrics: IMetrics | null;
anchorState: TreeBacked<allForks.BeaconState>;
transitionStore: ITransitionStore | null;
transitionStore: ITransitionStore;
eth1: IEth1ForBlockProduction;
executionEngine: IExecutionEngine;
}
) {
this.opts = opts;
Expand All @@ -113,6 +122,8 @@ export class BeaconChain implements IBeaconChain {
this.metrics = metrics;
this.genesisTime = anchorState.genesisTime;
this.genesisValidatorsRoot = anchorState.genesisValidatorsRoot.valueOf() as Uint8Array;
this.eth1 = eth1;
this.executionEngine = executionEngine;

this.forkDigestContext = new ForkDigestContext(config, this.genesisValidatorsRoot);

Expand Down
102 changes: 76 additions & 26 deletions packages/lodestar/src/chain/factory/block/body.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,21 @@
* @module chain/blockAssembly
*/

import {List} from "@chainsafe/ssz";
import {ForkName} from "@chainsafe/lodestar-params";
import {Bytes96, Bytes32, phase0, allForks, altair, Root, Slot} from "@chainsafe/lodestar-types";
import {IChainForkConfig} from "@chainsafe/lodestar-config";
import {CachedBeaconState} from "@chainsafe/lodestar-beacon-state-transition";

import {IEth1ForBlockProduction} from "../../../eth1";
import xor from "buffer-xor";
import {List, hash} from "@chainsafe/ssz";
import {Bytes96, Bytes32, phase0, allForks, altair, Root, Slot, BLSSignature, ssz} from "@chainsafe/lodestar-types";
import {
CachedBeaconState,
computeEpochAtSlot,
computeTimeAtSlot,
getCurrentEpoch,
getRandaoMix,
merge,
} from "@chainsafe/lodestar-beacon-state-transition";
import {IBeaconChain} from "../../interface";

export async function assembleBody(
{chain, config, eth1}: {chain: IBeaconChain; config: IChainForkConfig; eth1: IEth1ForBlockProduction},
chain: IBeaconChain,
currentState: CachedBeaconState<allForks.BeaconState>,
randaoReveal: Bytes96,
graffiti: Bytes32,
Expand All @@ -34,11 +38,11 @@ export async function assembleBody(
const [attesterSlashings, proposerSlashings] = chain.opPool.getSlashings(currentState);
const voluntaryExits = chain.opPool.getVoluntaryExits(currentState);
const attestations = chain.aggregatedAttestationPool.getAttestationsForBlock(currentState);
const {eth1Data, deposits} = await eth1.getEth1DataAndDeposits(
const {eth1Data, deposits} = await chain.eth1.getEth1DataAndDeposits(
currentState as CachedBeaconState<allForks.BeaconState>
);

const blockBodyPhase0: phase0.BeaconBlockBody = {
const blockBody: phase0.BeaconBlockBody = {
randaoReveal,
graffiti,
eth1Data,
Expand All @@ -49,25 +53,71 @@ export async function assembleBody(
voluntaryExits: voluntaryExits as List<phase0.SignedVoluntaryExit>,
};

const blockFork = config.getForkName(blockSlot);
switch (blockFork) {
case ForkName.phase0:
return blockBodyPhase0;
const blockEpoch = computeEpochAtSlot(blockSlot);

case ForkName.altair: {
const block: altair.BeaconBlockBody = {
...blockBodyPhase0,
syncAggregate: chain.syncContributionAndProofPool.getAggregate(
syncAggregateData.parentSlot,
syncAggregateData.parentBlockRoot
),
};
return block;
}
if (blockEpoch >= chain.config.ALTAIR_FORK_EPOCH) {
(blockBody as altair.BeaconBlockBody).syncAggregate = chain.syncContributionAndProofPool.getAggregate(
syncAggregateData.parentSlot,
syncAggregateData.parentBlockRoot
);
}

if (blockEpoch >= chain.config.MERGE_FORK_EPOCH) {
(blockBody as merge.BeaconBlockBody).executionPayload = await getExecutionPayload(
chain,
currentState as merge.BeaconState,
randaoReveal
);
}

return blockBody;
}

default:
throw new Error(`Block processing not implemented for fork ${blockFork}`);
/**
* Produce ExecutionPayload for pre-merge, merge, and post-merge.
*
* Expects `eth1MergeBlockFinder` to be actively searching for blocks well in advance to being called.
*/
async function getExecutionPayload(
chain: IBeaconChain,
state: merge.BeaconState,
randaoReveal: BLSSignature
): Promise<merge.ExecutionPayload> {
if (!merge.isMergeComplete(state)) {
const terminalPowBlockHash = chain.eth1.getMergeBlockHash();
if (terminalPowBlockHash === null) {
// Pre-merge, empty payload
ssz.merge.ExecutionPayload.defaultValue();
} else {
// Signify merge via producing on top of the last PoW block
const parentHash = terminalPowBlockHash;
return produceExecutionPayload(chain, state, parentHash, randaoReveal);
}
}

// Post-merge, normal payload
const parentHash = state.latestExecutionPayloadHeader.blockHash;
return produceExecutionPayload(chain, state, parentHash, randaoReveal);
}

async function produceExecutionPayload(
chain: IBeaconChain,
state: merge.BeaconState,
parentHash: Root,
randaoReveal: BLSSignature
): Promise<merge.ExecutionPayload> {
const timestamp = computeTimeAtSlot(chain.config, state.slot, state.genesisTime);
const randaoMix = computeRandaoMix(state, randaoReveal);

// NOTE: This is a naive implementation that does not give sufficient time to the eth1 block to produce an optimal
// block. Probably in the future there will exist mechanisms to optimize block production, such as giving a heads
// up to the execution client, then calling assembleBlock. Stay up to spec updates and update accordingly.
return chain.executionEngine.assembleBlock(parentHash, timestamp, randaoMix);
}

function computeRandaoMix(state: merge.BeaconState, randaoReveal: BLSSignature): Bytes32 {
const epoch = getCurrentEpoch(state);
return xor(Buffer.from(getRandaoMix(state, epoch) as Uint8Array), Buffer.from(hash(randaoReveal as Uint8Array)));
}

/** process_sync_committee_contributions is implemented in syncCommitteeContribution.getSyncAggregate */
Loading