From 314d60ec75d475d2048a85f85317873f488c7c36 Mon Sep 17 00:00:00 2001 From: Julien Eluard Date: Fri, 16 Feb 2024 17:41:10 +0100 Subject: [PATCH 1/2] fix: support eth_getBlockByNumber --- .../src/proof_provider/payload_store.ts | 70 +++++++++---------- .../src/proof_provider/proof_provider.ts | 19 ++--- packages/prover/src/utils/consensus.ts | 35 ++++++---- packages/prover/src/utils/evm.ts | 5 +- packages/prover/src/utils/validation.ts | 17 +++-- packages/prover/src/utils/verification.ts | 5 +- 6 files changed, 80 insertions(+), 71 deletions(-) diff --git a/packages/prover/src/proof_provider/payload_store.ts b/packages/prover/src/proof_provider/payload_store.ts index 7cc5e3753f4e..183a56274e5c 100644 --- a/packages/prover/src/proof_provider/payload_store.ts +++ b/packages/prover/src/proof_provider/payload_store.ts @@ -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; /** @@ -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(); + private finalizedRoots = new OrderedMap(); // Unfinalized blocks may change over time and may have conflicting roots // We can receive multiple light-client headers for the same block of execution @@ -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; @@ -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}); } } @@ -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 @@ -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; @@ -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(); } @@ -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); } } diff --git a/packages/prover/src/proof_provider/proof_provider.ts b/packages/prover/src/proof_provider/proof_provider.ts index d888f4269840..b94bdb453a4a 100644 --- a/packages/prover/src/proof_provider/proof_provider.ts +++ b/packages/prover/src/proof_provider/proof_provider.ts @@ -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, @@ -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"); } diff --git a/packages/prover/src/utils/consensus.ts b/packages/prover/src/utils/consensus.ts index d008a8e42459..a07409458fb6 100644 --- a/packages/prover/src/utils/consensus.ts +++ b/packages/prover/src/utils/consensus.ts @@ -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 { + 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, @@ -13,7 +20,7 @@ export async function fetchNearestBlock( ): Promise { 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); @@ -43,7 +50,7 @@ export async function getExecutionPayloads({ startSlot: number; endSlot: number; logger: Logger; -}): Promise> { +}): Promise> { [startSlot, endSlot] = [Math.min(startSlot, endSlot), Math.max(startSlot, endSlot)]; if (startSlot === endSlot) { logger.debug("Fetching EL payload", {slot: startSlot}); @@ -51,18 +58,18 @@ export async function getExecutionPayloads({ logger.debug("Fetching EL payloads", {startSlot, endSlot}); } - const payloads: Record = {}; + const payloads = new Map(); 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; @@ -76,16 +83,16 @@ export async function getExecutionPayloadForBlockNumber( api: Api, startSlot: number, blockNumber: number -): Promise> { - const payloads: Record = {}; +): Promise> { + const payloads = new Map(); - 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; diff --git a/packages/prover/src/utils/evm.ts b/packages/prover/src/utils/evm.ts index 63cfd3f14026..0b45866ad559 100644 --- a/packages/prover/src/utils/evm.ts +++ b/packages/prover/src/utils/evm.ts @@ -167,12 +167,13 @@ export async function executeVMCall({ network: NetworkName; }): Promise { 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({ diff --git a/packages/prover/src/utils/validation.ts b/packages/prover/src/utils/validation.ts index 3754bc0add87..1615df1db9b4 100644 --- a/packages/prover/src/utils/validation.ts +++ b/packages/prover/src/utils/validation.ts @@ -104,29 +104,28 @@ export async function isValidBlock({ logger: Logger; config: ChainForkConfig; }): Promise { - 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()), diff --git a/packages/prover/src/utils/verification.ts b/packages/prover/src/utils/verification.ts index 968c2c3c8756..b168ea327e03 100644 --- a/packages/prover/src/utils/verification.ts +++ b/packages/prover/src/utils/verification.ts @@ -86,8 +86,9 @@ export async function verifyBlock({ logger: Logger; }): Promise> { 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}; From 7cb4598fa00f6a044df0707a8b5cdf1374bce56c Mon Sep 17 00:00:00 2001 From: Julien Eluard Date: Fri, 16 Feb 2024 17:46:01 +0100 Subject: [PATCH 2/2] chore: fix tests --- .../unit/proof_provider/payload_store.test.ts | 47 ++++++++++--------- 1 file changed, 24 insertions(+), 23 deletions(-) diff --git a/packages/prover/test/unit/proof_provider/payload_store.test.ts b/packages/prover/test/unit/proof_provider/payload_store.test.ts index 738b6ef55f2a..66b866c85c08 100644 --- a/packages/prover/test/unit/proof_provider/payload_store.test.ts +++ b/packages/prover/test/unit/proof_provider/payload_store.test.ts @@ -9,6 +9,7 @@ import {ForkName} from "@lodestar/params"; import {PayloadStore} from "../../../src/proof_provider/payload_store.js"; import {MAX_PAYLOAD_HISTORY} from "../../../src/constants.js"; +const slotNumber = 10; const createHash = (input: string): Uint8Array => hash(Buffer.from(input, "utf8")); const buildPayload = ({blockNumber}: {blockNumber: number}): allForks.ExecutionPayload => @@ -71,14 +72,14 @@ describe("proof_provider/payload_store", function () { }); it("should return undefined if no finalized block", () => { - store.set(buildPayload({blockNumber: 10}), false); + store.set(buildPayload({blockNumber: 10}), slotNumber, false); expect(store.finalized).toBeUndefined(); }); it("should return finalized payload", () => { const payload = buildPayload({blockNumber: 10}); - store.set(payload, true); + store.set(payload, slotNumber, true); expect(store.finalized).toEqual(payload); }); @@ -86,8 +87,8 @@ describe("proof_provider/payload_store", function () { it("should return highest finalized payload", () => { const payload1 = buildPayload({blockNumber: 10}); const payload2 = buildPayload({blockNumber: 11}); - store.set(payload1, true); - store.set(payload2, true); + store.set(payload1, slotNumber, true); + store.set(payload2, slotNumber, true); expect(store.finalized).toEqual(payload2); }); @@ -101,8 +102,8 @@ describe("proof_provider/payload_store", function () { it("should return latest payload if finalized", () => { const payload1 = buildPayload({blockNumber: 10}); const payload2 = buildPayload({blockNumber: 11}); - store.set(payload1, true); - store.set(payload2, true); + store.set(payload1, slotNumber, true); + store.set(payload2, slotNumber, true); expect(store.latest).toEqual(payload2); }); @@ -110,8 +111,8 @@ describe("proof_provider/payload_store", function () { it("should return latest payload if not finalized", () => { const payload1 = buildPayload({blockNumber: 10}); const payload2 = buildPayload({blockNumber: 11}); - store.set(payload1, false); - store.set(payload2, false); + store.set(payload1, slotNumber, false); + store.set(payload2, slotNumber, false); expect(store.latest).toEqual(payload2); }); @@ -124,14 +125,14 @@ describe("proof_provider/payload_store", function () { it("should return undefined for non existing block id", async () => { const payload1 = buildPayload({blockNumber: 10}); - store.set(payload1, false); + store.set(payload1, slotNumber, false); await expect(store.get(11)).resolves.toBeUndefined(); }); it("should return undefined for non existing block hash", async () => { const payload1 = buildPayload({blockNumber: 10}); - store.set(payload1, false); + store.set(payload1, slotNumber, false); const nonExistingBlockHash = createHash("non-existing-block-hash"); await expect(store.get(toHexString(nonExistingBlockHash))).resolves.toBeUndefined(); @@ -140,7 +141,7 @@ describe("proof_provider/payload_store", function () { describe("block hash as blockId", () => { it("should return payload for a block hash", async () => { const payload1 = buildPayload({blockNumber: 10}); - store.set(payload1, false); + store.set(payload1, slotNumber, false); await expect(store.get(toHexString(payload1.blockHash))).resolves.toEqual(payload1); }); @@ -149,7 +150,7 @@ describe("proof_provider/payload_store", function () { describe("block number as blockId", () => { it("should throw error to use block hash for un-finalized blocks", async () => { const finalizedPayload = buildPayload({blockNumber: 10}); - store.set(finalizedPayload, true); + store.set(finalizedPayload, slotNumber, true); await expect(store.get(11)).rejects.toThrow( "Block number 11 is higher than the latest finalized block number. We recommend to use block hash for unfinalized blocks." @@ -158,28 +159,28 @@ describe("proof_provider/payload_store", function () { it("should return undefined if payload exists but not-finalized", async () => { const payload1 = buildPayload({blockNumber: 10}); - store.set(payload1, false); + store.set(payload1, slotNumber, false); await expect(store.get(10)).resolves.toBeUndefined(); }); it("should return payload for a block number in hex", async () => { const payload1 = buildPayload({blockNumber: 10}); - store.set(payload1, true); + store.set(payload1, slotNumber, true); await expect(store.get(`0x${payload1.blockNumber.toString(16)}`)).resolves.toEqual(payload1); }); it("should return payload for a block number as string", async () => { const payload1 = buildPayload({blockNumber: 10}); - store.set(payload1, true); + store.set(payload1, slotNumber, true); await expect(store.get(payload1.blockNumber.toString())).resolves.toEqual(payload1); }); it("should return payload for a block number as integer", async () => { const payload1 = buildPayload({blockNumber: 10}); - store.set(payload1, true); + store.set(payload1, slotNumber, true); await expect(store.get(10)).resolves.toEqual(payload1); }); @@ -199,7 +200,7 @@ describe("proof_provider/payload_store", function () { .calledWith(unavailableBlockNumber) .thenResolve(buildBlockResponse({blockNumber: unavailableBlockNumber, slot: unavailableBlockNumber})); - store.set(availablePayload, true); + store.set(availablePayload, slotNumber, true); const result = await store.get(unavailablePayload.blockNumber); @@ -214,7 +215,7 @@ describe("proof_provider/payload_store", function () { describe("set", () => { it("should set the payload for non-finalized blocks", async () => { const payload1 = buildPayload({blockNumber: 10}); - store.set(payload1, false); + store.set(payload1, slotNumber, false); // Unfinalized blocks are not indexed by block hash await expect(store.get(toHexString(payload1.blockHash))).resolves.toEqual(payload1); @@ -223,7 +224,7 @@ describe("proof_provider/payload_store", function () { it("should set the payload for finalized blocks", async () => { const payload1 = buildPayload({blockNumber: 10}); - store.set(payload1, true); + store.set(payload1, slotNumber, true); await expect(store.get(payload1.blockNumber.toString())).resolves.toEqual(payload1); expect(store.finalized).toEqual(payload1); @@ -324,7 +325,7 @@ describe("proof_provider/payload_store", function () { const numberOfPayloads = MAX_PAYLOAD_HISTORY + 2; for (let i = 1; i <= numberOfPayloads; i++) { - store.set(buildPayload({blockNumber: i}), true); + store.set(buildPayload({blockNumber: i}), slotNumber, true); } expect(store["payloads"].size).toEqual(numberOfPayloads); @@ -338,7 +339,7 @@ describe("proof_provider/payload_store", function () { const numberOfPayloads = MAX_PAYLOAD_HISTORY; for (let i = 1; i <= numberOfPayloads; i++) { - store.set(buildPayload({blockNumber: i}), true); + store.set(buildPayload({blockNumber: i}), slotNumber, true); } expect(store["payloads"].size).toEqual(MAX_PAYLOAD_HISTORY); @@ -352,7 +353,7 @@ describe("proof_provider/payload_store", function () { const numberOfPayloads = MAX_PAYLOAD_HISTORY - 1; for (let i = 1; i <= numberOfPayloads; i++) { - store.set(buildPayload({blockNumber: i}), true); + store.set(buildPayload({blockNumber: i}), slotNumber, true); } expect(store["payloads"].size).toEqual(numberOfPayloads); @@ -366,7 +367,7 @@ describe("proof_provider/payload_store", function () { const numberOfPayloads = MAX_PAYLOAD_HISTORY + 2; for (let i = 1; i <= numberOfPayloads; i++) { - store.set(buildPayload({blockNumber: i}), true); + store.set(buildPayload({blockNumber: i}), slotNumber, true); } expect(store["finalizedRoots"].size).toEqual(numberOfPayloads);