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

fix: support eth_getBlockByNumber #6442

Merged
merged 2 commits into from
Mar 14, 2024
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
70 changes: 35 additions & 35 deletions packages/prover/src/proof_provider/payload_store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,15 @@ import {Api} from "@lodestar/api";
import {allForks, capella} from "@lodestar/types";
import {Logger} from "@lodestar/utils";
import {MAX_PAYLOAD_HISTORY} from "../constants.js";
import {getExecutionPayloadForBlockNumber, getExecutionPayloads} from "../utils/consensus.js";
import {fetchBlock, getExecutionPayloadForBlockNumber} from "../utils/consensus.js";
import {bufferToHex, hexToNumber} from "../utils/conversion.js";
import {OrderedMap} from "./ordered_map.js";

type BlockELRoot = string;
type BlockELRootAndSlot = {
blockELRoot: BlockELRoot;
slot: number;
};
type BlockCLRoot = string;

/**
Expand All @@ -15,7 +19,7 @@ type BlockCLRoot = string;
export class PayloadStore {
// We store the block root from execution for finalized blocks
// As these blocks are finalized, so not to be worried about conflicting roots
private finalizedRoots = new OrderedMap<BlockELRoot>();
private finalizedRoots = new OrderedMap<BlockELRootAndSlot>();

// Unfinalized blocks may change over time and may have conflicting roots
// We can receive multiple light-client headers for the same block of execution
Expand All @@ -39,7 +43,7 @@ export class PayloadStore {

const finalizedMaxRoot = this.finalizedRoots.get(maxBlockNumberForFinalized);
if (finalizedMaxRoot) {
return this.payloads.get(finalizedMaxRoot);
return this.payloads.get(finalizedMaxRoot.blockELRoot);
}

return undefined;
Expand Down Expand Up @@ -94,35 +98,39 @@ export class PayloadStore {
let blockELRoot = this.finalizedRoots.get(blockNumber);
// check if we have payload cached locally else fetch from api
if (!blockELRoot) {
const payloads = await getExecutionPayloadForBlockNumber(this.opts.api, minBlockNumberForFinalized, blockNumber);
for (const payload of Object.values(payloads)) {
this.set(payload, true);
const finalizedMaxRoot = this.finalizedRoots.get(maxBlockNumberForFinalized);
const slot = finalizedMaxRoot?.slot;
if (slot !== undefined) {
const payloads = await getExecutionPayloadForBlockNumber(this.opts.api, slot, blockNumber);
for (const [slot, payload] of payloads.entries()) {
this.set(payload, slot, true);
}
}
}

blockELRoot = this.finalizedRoots.get(blockNumber);
if (blockELRoot) {
return this.payloads.get(blockELRoot);
return this.payloads.get(blockELRoot.blockELRoot);
}

return undefined;
}

set(payload: allForks.ExecutionPayload, finalized: boolean): void {
const blockRoot = bufferToHex(payload.blockHash);
this.payloads.set(blockRoot, payload);
set(payload: allForks.ExecutionPayload, slot: number, finalized: boolean): void {
const blockELRoot = bufferToHex(payload.blockHash);
this.payloads.set(blockELRoot, payload);

if (this.latestBlockRoot) {
const latestPayload = this.payloads.get(this.latestBlockRoot);
if (latestPayload && latestPayload.blockNumber < payload.blockNumber) {
this.latestBlockRoot = blockRoot;
this.latestBlockRoot = blockELRoot;
}
} else {
this.latestBlockRoot = blockRoot;
this.latestBlockRoot = blockELRoot;
}

if (finalized) {
this.finalizedRoots.set(payload.blockNumber, blockRoot);
this.finalizedRoots.set(payload.blockNumber, {blockELRoot, slot});
}
}

Expand All @@ -136,7 +144,7 @@ export class PayloadStore {
// ==== Finalized blocks ====
// if the block is finalized, we need to update the finalizedRoots map
if (finalized) {
this.finalizedRoots.set(blockNumber, blockELRoot);
this.finalizedRoots.set(blockNumber, {blockELRoot, slot: blockSlot});

// If the block is finalized and we already have the payload
// We can remove it from the unfinalizedRoots map and do nothing else
Expand All @@ -147,17 +155,12 @@ export class PayloadStore {
// If the block is finalized and we do not have the payload
// We need to fetch and set the payload
else {
this.payloads.set(
bufferToHex(header.execution.blockHash),
(
await getExecutionPayloads({
api: this.opts.api,
startSlot: blockSlot,
endSlot: blockSlot,
logger: this.opts.logger,
})
)[blockSlot]
);
const block = await fetchBlock(this.opts.api, blockSlot);
if (block) {
this.payloads.set(blockELRoot, block.message.body.executionPayload);
} else {
this.opts.logger.error("Failed to fetch block", blockSlot);
}
}

return;
Expand All @@ -178,15 +181,12 @@ export class PayloadStore {
this.unfinalizedRoots.set(blockCLRoot, blockELRoot);

// We do not have the payload for this block, we need to fetch it
const payload = (
await getExecutionPayloads({
api: this.opts.api,
startSlot: blockSlot,
endSlot: blockSlot,
logger: this.opts.logger,
})
)[blockSlot];
this.set(payload, false);
const block = await fetchBlock(this.opts.api, blockSlot);
if (block) {
this.set(block.message.body.executionPayload, blockSlot, false);
} else {
this.opts.logger.error("Failed to fetch finalized block", blockSlot);
}
this.prune();
}

Expand All @@ -202,7 +202,7 @@ export class PayloadStore {
) {
const blockELRoot = this.finalizedRoots.get(blockNumber);
if (blockELRoot) {
this.payloads.delete(blockELRoot);
this.payloads.delete(blockELRoot.blockELRoot);
this.finalizedRoots.delete(blockNumber);
}
}
Expand Down
19 changes: 10 additions & 9 deletions packages/prover/src/proof_provider/proof_provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {Logger} from "@lodestar/utils";
import {LCTransport, RootProviderInitOptions} from "../interfaces.js";
import {assertLightClient} from "../utils/assertion.js";
import {
fetchBlock,
getExecutionPayloads,
getGenesisData,
getSyncCheckpoint,
Expand Down Expand Up @@ -120,20 +121,20 @@ export class ProofProvider {
endSlot: end,
logger: this.logger,
});
for (const payload of Object.values(payloads)) {
this.store.set(payload, false);
for (const [slot, payload] of payloads.entries()) {
this.store.set(payload, slot, false);
}

// Load the finalized payload from the CL
const finalizedSlot = this.lightClient.getFinalized().beacon.slot;
this.logger.debug("Getting finalized slot from lightclient", {finalizedSlot});
const finalizedPayload = await getExecutionPayloads({
api: this.opts.api,
startSlot: finalizedSlot,
endSlot: finalizedSlot,
logger: this.logger,
});
this.store.set(finalizedPayload[finalizedSlot], true);
const block = await fetchBlock(this.opts.api, finalizedSlot);
if (block) {
this.store.set(block.message.body.executionPayload, finalizedSlot, true);
} else {
this.logger.error("Failed to fetch finalized block", finalizedSlot);
}

this.logger.info("Proof provider ready");
}

Expand Down
35 changes: 21 additions & 14 deletions packages/prover/src/utils/consensus.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,21 @@ import {Logger} from "@lodestar/utils";
import {MAX_PAYLOAD_HISTORY} from "../constants.js";
import {hexToBuffer} from "./conversion.js";

export async function fetchBlock(api: Api, slot: number): Promise<capella.SignedBeaconBlock | undefined> {
const res = await api.beacon.getBlockV2(slot);
Copy link
Member

Choose a reason for hiding this comment

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

are we fine with throwing here if there is a network error?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

If an error is thrown here then a light-client header is not processed (that error will be logged).

I think that it's ok in the context of this PR as it doesn't change this behavior. If required a proper solution could be implemented separately.


if (res.ok) return res.response.data as capella.SignedBeaconBlock;
return;
}

export async function fetchNearestBlock(
api: Api,
slot: number,
direction: "up" | "down" = "down"
): Promise<capella.SignedBeaconBlock> {
const res = await api.beacon.getBlockV2(slot);

if (res.ok) return res.response.data;
if (res.ok) return res.response.data as capella.SignedBeaconBlock;

if (!res.ok && res.error.code === 404) {
return fetchNearestBlock(api, direction === "down" ? slot - 1 : slot + 1);
Expand Down Expand Up @@ -43,26 +50,26 @@ export async function getExecutionPayloads({
startSlot: number;
endSlot: number;
logger: Logger;
}): Promise<Record<number, allForks.ExecutionPayload>> {
}): Promise<Map<number, allForks.ExecutionPayload>> {
[startSlot, endSlot] = [Math.min(startSlot, endSlot), Math.max(startSlot, endSlot)];
if (startSlot === endSlot) {
logger.debug("Fetching EL payload", {slot: startSlot});
} else {
logger.debug("Fetching EL payloads", {startSlot, endSlot});
}

const payloads: Record<number, allForks.ExecutionPayload> = {};
const payloads = new Map<number, allForks.ExecutionPayload>();

let slot = endSlot;
let block = await fetchNearestBlock(api, slot, "down");
payloads[block.message.slot] = block.message.body.executionPayload;
let block = await fetchNearestBlock(api, slot);
payloads.set(block.message.slot, block.message.body.executionPayload);
slot = block.message.slot - 1;

while (slot >= startSlot) {
const previousBlock = await fetchNearestBlock(api, block.message.slot - 1, "down");
const previousBlock = await fetchNearestBlock(api, block.message.slot - 1);

if (block.message.body.executionPayload.parentHash === previousBlock.message.body.executionPayload.blockHash) {
payloads[block.message.slot] = block.message.body.executionPayload;
payloads.set(block.message.slot, block.message.body.executionPayload);
}

slot = block.message.slot - 1;
Expand All @@ -76,16 +83,16 @@ export async function getExecutionPayloadForBlockNumber(
api: Api,
startSlot: number,
blockNumber: number
): Promise<Record<number, allForks.ExecutionPayload>> {
const payloads: Record<number, allForks.ExecutionPayload> = {};
): Promise<Map<number, allForks.ExecutionPayload>> {
const payloads = new Map<number, allForks.ExecutionPayload>();

let block = await fetchNearestBlock(api, startSlot, "down");
payloads[block.message.slot] = block.message.body.executionPayload;
let block = await fetchNearestBlock(api, startSlot);
payloads.set(block.message.slot, block.message.body.executionPayload);

while (payloads[block.message.slot].blockNumber !== blockNumber) {
const previousBlock = await fetchNearestBlock(api, block.message.slot - 1, "down");
while (payloads.get(block.message.slot)?.blockNumber !== blockNumber) {
const previousBlock = await fetchNearestBlock(api, block.message.slot - 1);
block = previousBlock;
payloads[block.message.slot] = block.message.body.executionPayload;
payloads.set(block.message.slot, block.message.body.executionPayload);
}

return payloads;
Expand Down
5 changes: 3 additions & 2 deletions packages/prover/src/utils/evm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -167,12 +167,13 @@ export async function executeVMCall({
network: NetworkName;
}): Promise<RunTxResult["execResult"]> {
const {from, to, gas, gasPrice, maxPriorityFeePerGas, value, data, input} = tx;
const {result: block} = await rpc.request("eth_getBlockByHash", [bufferToHex(executionPayload.blockHash), true], {
const blockHash = bufferToHex(executionPayload.blockHash);
const {result: block} = await rpc.request("eth_getBlockByHash", [blockHash, true], {
raiseError: true,
});

if (!block) {
throw new Error(`Block not found: ${bufferToHex(executionPayload.blockHash)}`);
throw new Error(`Block not found: ${blockHash}`);
}

const {execResult} = await vm.evm.runCall({
Expand Down
17 changes: 8 additions & 9 deletions packages/prover/src/utils/validation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -104,29 +104,28 @@ export async function isValidBlock({
logger: Logger;
config: ChainForkConfig;
}): Promise<boolean> {
const common = getChainCommon(config.PRESET_BASE);
common.setHardforkByBlockNumber(executionPayload.blockNumber, undefined, executionPayload.timestamp);

const blockObject = Block.fromBlockData(blockDataFromELBlock(block), {common});

if (bufferToHex(executionPayload.blockHash) !== bufferToHex(blockObject.hash())) {
if (bufferToHex(executionPayload.blockHash) !== block.hash) {
logger.error("Block hash does not match", {
rpcBlockHash: bufferToHex(blockObject.hash()),
rpcBlockHash: block.hash,
beaconExecutionBlockHash: bufferToHex(executionPayload.blockHash),
});

return false;
}

if (bufferToHex(executionPayload.parentHash) !== bufferToHex(blockObject.header.parentHash)) {
if (bufferToHex(executionPayload.parentHash) !== block.parentHash) {
logger.error("Block parent hash does not match", {
rpcBlockHash: bufferToHex(blockObject.header.parentHash),
rpcBlockHash: block.parentHash,
beaconExecutionBlockHash: bufferToHex(executionPayload.parentHash),
});

return false;
}

const common = getChainCommon(config.PRESET_BASE);
common.setHardforkByBlockNumber(executionPayload.blockNumber, undefined, executionPayload.timestamp);
const blockObject = Block.fromBlockData(blockDataFromELBlock(block), {common});

if (!(await blockObject.validateTransactionsTrie())) {
logger.error("Block transactions could not be verified.", {
blockHash: bufferToHex(blockObject.hash()),
Expand Down
5 changes: 3 additions & 2 deletions packages/prover/src/utils/verification.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,8 +86,9 @@ export async function verifyBlock({
logger: Logger;
}): Promise<VerificationResult<ELBlock>> {
try {
const executionPayload = await proofProvider.getExecutionPayload(payload.params[0]);
const block = await getELBlock(rpc, payload.params);
const blockNumber = payload.params[0];
const executionPayload = await proofProvider.getExecutionPayload(blockNumber);
const block = await getELBlock(rpc, [blockNumber, true]); // Always request hydrated blocks as we need access to `transactions` details

// If response is not valid from the EL we don't need to verify it
if (!block) return {data: block, valid: false};
Expand Down
Loading
Loading