Skip to content

Commit

Permalink
fix: support eth_getBlockByNumber (#6442)
Browse files Browse the repository at this point in the history
* fix: support eth_getBlockByNumber

* chore: fix tests
  • Loading branch information
jeluard committed Mar 14, 2024
1 parent c94ecd2 commit 30d347d
Show file tree
Hide file tree
Showing 7 changed files with 103 additions and 93 deletions.
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 @@ -112,20 +113,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
33 changes: 20 additions & 13 deletions packages/prover/src/utils/consensus.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,13 @@ 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);

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

export async function fetchNearestBlock(
api: Api,
slot: number,
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

0 comments on commit 30d347d

Please sign in to comment.