Skip to content

Commit

Permalink
refactor: move block getter from API impl to chain (#5570)
Browse files Browse the repository at this point in the history
* Move block getter from API impl to chain

* Fix tests

* Fix type error

---------

Co-authored-by: Cayman <caymannava@gmail.com>
  • Loading branch information
dapplion and wemeetagain authored May 31, 2023
1 parent 399687b commit cef8562
Show file tree
Hide file tree
Showing 13 changed files with 95 additions and 207 deletions.
18 changes: 9 additions & 9 deletions packages/beacon-node/src/api/impl/beacon/blocks/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,9 +99,9 @@ export function getBeaconBlockApi({
return {executionOptimistic: false, data: []};
}
const canonicalRoot = config
.getForkTypes(canonicalBlock.message.slot)
.BeaconBlock.hashTreeRoot(canonicalBlock.message);
result.push(toBeaconHeaderResponse(config, canonicalBlock, true));
.getForkTypes(canonicalBlock.block.message.slot)
.BeaconBlock.hashTreeRoot(canonicalBlock.block.message);
result.push(toBeaconHeaderResponse(config, canonicalBlock.block, true));

// fork blocks
// TODO: What is this logic?
Expand All @@ -128,22 +128,22 @@ export function getBeaconBlockApi({
},

async getBlockHeader(blockId) {
const {block, executionOptimistic} = await resolveBlockId(chain.forkChoice, db, blockId);
const {block, executionOptimistic} = await resolveBlockId(chain, blockId);
return {
executionOptimistic,
data: toBeaconHeaderResponse(config, block, true),
};
},

async getBlock(blockId) {
const {block} = await resolveBlockId(chain.forkChoice, db, blockId);
const {block} = await resolveBlockId(chain, blockId);
return {
data: block,
};
},

async getBlockV2(blockId) {
const {block, executionOptimistic} = await resolveBlockId(chain.forkChoice, db, blockId);
const {block, executionOptimistic} = await resolveBlockId(chain, blockId);
return {
executionOptimistic,
data: block,
Expand All @@ -152,7 +152,7 @@ export function getBeaconBlockApi({
},

async getBlockAttestations(blockId) {
const {block, executionOptimistic} = await resolveBlockId(chain.forkChoice, db, blockId);
const {block, executionOptimistic} = await resolveBlockId(chain, blockId);
return {
executionOptimistic,
data: Array.from(block.message.body.attestations),
Expand Down Expand Up @@ -188,7 +188,7 @@ export function getBeaconBlockApi({
}

// Slow path
const {block, executionOptimistic} = await resolveBlockId(chain.forkChoice, db, blockId);
const {block, executionOptimistic} = await resolveBlockId(chain, blockId);
return {
executionOptimistic,
data: {root: config.getForkTypes(block.message.slot).BeaconBlock.hashTreeRoot(block.message)},
Expand Down Expand Up @@ -268,7 +268,7 @@ export function getBeaconBlockApi({
},

async getBlobSidecars(blockId) {
const {block, executionOptimistic} = await resolveBlockId(chain.forkChoice, db, blockId);
const {block, executionOptimistic} = await resolveBlockId(chain, blockId);
const blockRoot = config.getForkTypes(block.message.slot).BeaconBlock.hashTreeRoot(block.message);

let {blobSidecars} = (await db.blobSidecars.get(blockRoot)) ?? {};
Expand Down
82 changes: 19 additions & 63 deletions packages/beacon-node/src/api/impl/beacon/blocks/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,10 @@ import {allForks} from "@lodestar/types";
import {routes} from "@lodestar/api";
import {blockToHeader} from "@lodestar/state-transition";
import {ChainForkConfig} from "@lodestar/config";
import {IForkChoice} from "@lodestar/fork-choice";
import {fromHexString} from "@chainsafe/ssz";
import {IBeaconDb} from "../../../../db/index.js";
import {GENESIS_SLOT} from "../../../../constants/index.js";
import {ApiError, ValidationError} from "../../errors.js";
import {isOptimisticBlock} from "../../../../util/forkChoice.js";
import {IBeaconChain} from "../../../../chain/interface.js";
import {rootHexRegex} from "../../../../eth1/provider/utils.js";

export function toBeaconHeaderResponse(
config: ChainForkConfig,
Expand All @@ -25,91 +23,49 @@ export function toBeaconHeaderResponse(
}

export async function resolveBlockId(
forkChoice: IForkChoice,
db: IBeaconDb,
chain: IBeaconChain,
blockId: routes.beacon.BlockId
): Promise<{block: allForks.SignedBeaconBlock; executionOptimistic: boolean}> {
const {block, executionOptimistic} = await resolveBlockIdOrNull(forkChoice, db, blockId);
if (!block) {
const res = await resolveBlockIdOrNull(chain, blockId);
if (!res) {
throw new ApiError(404, `No block found for id '${blockId}'`);
}

return {block, executionOptimistic};
return res;
}

async function resolveBlockIdOrNull(
forkChoice: IForkChoice,
db: IBeaconDb,
chain: IBeaconChain,
blockId: routes.beacon.BlockId
): Promise<{block: allForks.SignedBeaconBlock | null; executionOptimistic: boolean}> {
): Promise<{block: allForks.SignedBeaconBlock; executionOptimistic: boolean} | null> {
blockId = String(blockId).toLowerCase();
if (blockId === "head") {
const head = forkChoice.getHead();
return {
block: await db.block.get(fromHexString(head.blockRoot)),
executionOptimistic: isOptimisticBlock(head),
};
return chain.getBlockByRoot(chain.forkChoice.getHead().blockRoot);
}

if (blockId === "genesis") {
return {
block: await db.blockArchive.get(GENESIS_SLOT),
executionOptimistic: false,
};
return chain.getCanonicalBlockAtSlot(GENESIS_SLOT);
}

if (blockId === "finalized") {
return {
block: await db.blockArchive.get(forkChoice.getFinalizedBlock().slot),
executionOptimistic: false,
};
return chain.getCanonicalBlockAtSlot(chain.forkChoice.getFinalizedBlock().slot);
}

if (blockId === "justified") {
const justified = forkChoice.getJustifiedBlock();
return {
block: await db.block.get(fromHexString(justified.blockRoot)),
executionOptimistic: isOptimisticBlock(justified),
};
return chain.getBlockByRoot(chain.forkChoice.getJustifiedBlock().blockRoot);
}

let blockSummary;
let getBlockByBlockArchive;

if (blockId.startsWith("0x")) {
const blockHash = fromHexString(blockId);
blockSummary = forkChoice.getBlock(blockHash);
getBlockByBlockArchive = async () => db.blockArchive.getByRoot(blockHash);
} else {
// block id must be slot
const blockSlot = parseInt(blockId, 10);
if (isNaN(blockSlot) && isNaN(blockSlot - 0)) {
if (!rootHexRegex.test(blockId)) {
throw new ValidationError(`Invalid block id '${blockId}'`, "blockId");
}
blockSummary = forkChoice.getCanonicalBlockAtSlot(blockSlot);
getBlockByBlockArchive = async () => db.blockArchive.get(blockSlot);
return chain.getBlockByRoot(blockId);
}

if (blockSummary) {
// All unfinalized blocks **and the finalized block** are tracked by the fork choice.
// Unfinalized blocks are stored in the block repository, but the finalized block is in the block archive
const finalized = forkChoice.getFinalizedBlock();
if (blockSummary.slot === finalized.slot) {
return {
block: await db.blockArchive.get(finalized.slot),
executionOptimistic: isOptimisticBlock(blockSummary),
};
} else {
return {
block: await db.block.get(fromHexString(blockSummary.blockRoot)),
executionOptimistic: false,
};
}
} else {
// Blocks not in the fork choice are in the block archive
return {
block: await getBlockByBlockArchive(),
executionOptimistic: false,
};
// block id must be slot
const blockSlot = parseInt(blockId, 10);
if (isNaN(blockSlot) && isNaN(blockSlot - 0)) {
throw new ValidationError(`Invalid block id '${blockId}'`, "blockId");
}
return chain.getCanonicalBlockAtSlot(blockSlot);
}
2 changes: 1 addition & 1 deletion packages/beacon-node/src/api/impl/proof/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ export function getProofApi(
throw new Error("Requested proof is too large.");
}

const {block} = await resolveBlockId(chain.forkChoice, db, blockId);
const {block} = await resolveBlockId(chain, blockId);

// Commit any changes before computing the state root. In normal cases the state should have no changes here
const blockNode = config.getForkTypes(block.message.slot).BeaconBlock.toView(block.message).node;
Expand Down
8 changes: 5 additions & 3 deletions packages/beacon-node/src/api/impl/validator/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,9 +79,11 @@ export function getValidatorApi({
genesisBlockRoot = state.blockRoots.get(0);
}

const genesisBlock = await chain.getCanonicalBlockAtSlot(GENESIS_SLOT);
if (genesisBlock) {
genesisBlockRoot = config.getForkTypes(genesisBlock.message.slot).SignedBeaconBlock.hashTreeRoot(genesisBlock);
const blockRes = await chain.getCanonicalBlockAtSlot(GENESIS_SLOT);
if (blockRes) {
genesisBlockRoot = config
.getForkTypes(blockRes.block.message.slot)
.SignedBeaconBlock.hashTreeRoot(blockRes.block);
}
}

Expand Down
1 change: 1 addition & 0 deletions packages/beacon-node/src/chain/archiver/archiveBlocks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ export async function archiveBlocks(
currentEpoch: Epoch
): Promise<FinalizedData> {
// Use fork choice to determine the blocks to archive and delete
// getAllAncestorBlocks response includes the finalized block, so it's also moved to the cold db
const finalizedCanonicalBlocks = forkChoice.getAllAncestorBlocks(finalizedCheckpoint.rootHex);
const finalizedNonCanonicalBlocks = forkChoice.getAllNonAncestorBlocks(finalizedCheckpoint.rootHex);

Expand Down
42 changes: 35 additions & 7 deletions packages/beacon-node/src/chain/chain.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ import {IEth1ForBlockProduction} from "../eth1/index.js";
import {IExecutionEngine, IExecutionBuilder, TransitionConfigurationV1} from "../execution/index.js";
import {Clock, ClockEvent, IClock} from "../util/clock.js";
import {ensureDir, writeIfNotExist} from "../util/file.js";
import {isOptimisticBlock} from "../util/forkChoice.js";
import {CheckpointStateCache, StateContextCache} from "./stateCache/index.js";
import {BlockProcessor, ImportBlockOpts} from "./blocks/index.js";
import {ChainEventEmitter, ChainEvent} from "./emitter.js";
Expand Down Expand Up @@ -369,16 +370,43 @@ export class BeaconChain implements IBeaconChain {
return this.regen.getBlockSlotState(head.blockRoot, startSlot, {dontTransferCache: true}, regenCaller);
}

async getCanonicalBlockAtSlot(slot: Slot): Promise<allForks.SignedBeaconBlock | null> {
async getCanonicalBlockAtSlot(
slot: Slot
): Promise<{block: allForks.SignedBeaconBlock; executionOptimistic: boolean} | null> {
const finalizedBlock = this.forkChoice.getFinalizedBlock();
if (finalizedBlock.slot > slot) {
return this.db.blockArchive.get(slot);
if (slot > finalizedBlock.slot) {
// Unfinalized slot, attempt to find in fork-choice
const block = this.forkChoice.getCanonicalBlockAtSlot(slot);
if (block) {
const data = await this.db.block.get(fromHexString(block.blockRoot));
if (data) {
return {block: data, executionOptimistic: isOptimisticBlock(block)};
}
}
// A non-finalized slot expected to be found in the hot db, could be archived during
// this function runtime, so if not found in the hot db, fallback to the cold db
// TODO: Add a lock to the archiver to have determinstic behaviour on where are blocks
}
const block = this.forkChoice.getCanonicalBlockAtSlot(slot);
if (!block) {
return null;

const data = await this.db.blockArchive.get(slot);
return data && {block: data, executionOptimistic: false};
}

async getBlockByRoot(
root: string
): Promise<{block: allForks.SignedBeaconBlock; executionOptimistic: boolean} | null> {
const block = this.forkChoice.getBlockHex(root);
if (block) {
const data = await this.db.block.get(fromHexString(root));
if (data) {
return {block: data, executionOptimistic: isOptimisticBlock(block)};
}
// If block is not found in hot db, try cold db since there could be an archive cycle happening
// TODO: Add a lock to the archiver to have determinstic behaviour on where are blocks
}
return this.db.block.get(fromHexString(block.blockRoot));

const data = await this.db.blockArchive.getByRoot(fromHexString(root));
return data && {block: data, executionOptimistic: false};
}

produceBlock(blockAttributes: BlockAttributes): Promise<{block: allForks.BeaconBlock; blockValue: Wei}> {
Expand Down
9 changes: 7 additions & 2 deletions packages/beacon-node/src/chain/interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -120,9 +120,14 @@ export interface IBeaconChain {
* Since we can have multiple parallel chains,
* this methods returns blocks in current chain head according to
* forkchoice. Works for finalized slots as well
* @param slot
*/
getCanonicalBlockAtSlot(slot: Slot): Promise<allForks.SignedBeaconBlock | null>;
getCanonicalBlockAtSlot(
slot: Slot
): Promise<{block: allForks.SignedBeaconBlock; executionOptimistic: boolean} | null>;
/**
* Get local block by root, does not fetch from the network
*/
getBlockByRoot(root: RootHex): Promise<{block: allForks.SignedBeaconBlock; executionOptimistic: boolean} | null>;

getBlobSidecars(beaconBlock: deneb.BeaconBlock): deneb.BlobSidecars;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ export async function* onBlocksOrBlobSidecarsByRange(
const headRoot = chain.forkChoice.getHeadRoot();
// TODO DENEB: forkChoice should mantain an array of canonical blocks, and change only on reorg
const headChain = chain.forkChoice.getAllAncestorBlocks(headRoot);
// getAllAncestorBlocks response includes the head node, so it's the full chain.

// Iterate head chain with ascending block numbers
for (let i = headChain.length - 1; i >= 0; i--) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {ssz} from "@lodestar/types";
import {GENESIS_SLOT} from "@lodestar/params";

import {setupApiImplTestServer, ApiImplTestModules} from "../../../unit/api/impl/index.test.js";
import {zeroProtoBlock} from "../../../utils/mocks/chain.js";

describe("getBlobSideCar", function () {
let server: ApiImplTestModules;
Expand All @@ -12,7 +13,8 @@ describe("getBlobSideCar", function () {
server = setupApiImplTestServer();
});

it("getBlobSideCar From BlobSidecars", async () => {
// TODO: Write actual tests against the real BeaconChain class, this test is useless
it.skip("getBlobSideCar From BlobSidecars", async () => {
const block = config.getForkTypes(GENESIS_SLOT).SignedBeaconBlock.defaultValue();
const blobSidecars = ssz.deneb.BlobSidecars.defaultValue();
const wrappedBlobSidecars = {
Expand All @@ -21,6 +23,8 @@ describe("getBlobSideCar", function () {
blobSidecars,
};

server.forkChoiceStub.getFinalizedBlock.returns(zeroProtoBlock);

server.dbStub.blockArchive.get.resolves(block);
server.dbStub.blobSidecars.get.resolves(wrappedBlobSidecars);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,9 @@ describe("api - beacon - getBlockHeaders", function () {

it.skip("no filters - assume head slot", async function () {
server.forkChoiceStub.getHead.returns(generateProtoBlock({slot: 1}));
server.chainStub.getCanonicalBlockAtSlot.withArgs(1).resolves(ssz.phase0.SignedBeaconBlock.defaultValue());
server.chainStub.getCanonicalBlockAtSlot
.withArgs(1)
.resolves({block: ssz.phase0.SignedBeaconBlock.defaultValue(), executionOptimistic: false});
server.forkChoiceStub.getBlockSummariesAtSlot.withArgs(1).returns([
generateProtoBlock(),
//canonical block summary
Expand Down Expand Up @@ -48,7 +50,9 @@ describe("api - beacon - getBlockHeaders", function () {

it("finalized slot", async function () {
server.forkChoiceStub.getHead.returns(generateProtoBlock({slot: 2}));
server.chainStub.getCanonicalBlockAtSlot.withArgs(0).resolves(ssz.phase0.SignedBeaconBlock.defaultValue());
server.chainStub.getCanonicalBlockAtSlot
.withArgs(0)
.resolves({block: ssz.phase0.SignedBeaconBlock.defaultValue(), executionOptimistic: false});
server.forkChoiceStub.getBlockSummariesAtSlot.withArgs(0).returns([]);
const {data: blockHeaders} = await server.blockApi.getBlockHeaders({slot: 0});
expect(blockHeaders.length).to.be.equal(1);
Expand Down
Loading

0 comments on commit cef8562

Please sign in to comment.