Skip to content

Commit 498caac

Browse files
committed
fix(eip8025): compute newPayloadRequestRoot from full ExecutionPayload, not header
Fixes the newPayloadRequestRoot computation to match Lighthouse's approach: tree_hash_root(NewPayloadRequest) uses the full ExecutionPayload (with transactions, withdrawals) not the compact ExecutionPayloadHeader. The spec's NewPayloadRequestHeader is a separate concept used for proof engine verification, not for computing the root.
1 parent afa46fb commit 498caac

File tree

4 files changed

+70
-12
lines changed

4 files changed

+70
-12
lines changed

packages/beacon-node/src/chain/blocks/verifyBlocksExecutionPayloads.ts

Lines changed: 18 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,12 @@ import {
99
} from "@lodestar/fork-choice";
1010
import {ForkSeq} from "@lodestar/params";
1111
import {IBeaconStateView, isExecutionBlockBodyType} from "@lodestar/state-transition";
12-
import {SignedExecutionProof, bellatrix, electra} from "@lodestar/types";
12+
import {SignedExecutionProof, bellatrix, deneb, electra} from "@lodestar/types";
1313
import {ErrorAborted, Logger, toRootHex} from "@lodestar/utils";
1414
import {ExecutionPayloadStatus, IExecutionEngine} from "../../execution/engine/interface.js";
1515
import {Metrics} from "../../metrics/metrics.js";
1616
import {IClock} from "../../util/clock.js";
17+
import {computeNewPayloadRequestRoot} from "../eip8025/newPayloadRequestHeader.js";
1718
import {BlockError, BlockErrorCode} from "../errors/index.js";
1819
import {ExecutionProofPool} from "../opPools/executionProofPool.js";
1920
import {BlockProcessOpts} from "../options.js";
@@ -288,17 +289,24 @@ function verifyBlockExecutionPayloadByProof(
288289
executionBlock: executionPayload.blockNumber,
289290
};
290291

291-
// EIP-8025: Look up proofs by newPayloadRequestRoot.
292-
// Find the requestRoot that maps to this block's blockRoot via the reverse mapping.
293-
// TODO EIP-8025: Compute NewPayloadRequestHeader.hashTreeRoot() directly
294-
// instead of relying on the reverse mapping.
292+
// EIP-8025: Compute the newPayloadRequestRoot from the block data and look up proofs by it.
295293
let proofs: SignedExecutionProof[] = [];
296294
if (chain.executionProofPool) {
297-
for (const [requestRootHex, mappedBlockRootHex] of chain.requestRootToBlockRoot ?? []) {
298-
if (mappedBlockRootHex === blockRootHex) {
299-
proofs = chain.executionProofPool.getByRequestRootHex(requestRootHex);
300-
break;
301-
}
295+
const block = blockInput.getBlock();
296+
const body = block.message.body as deneb.BeaconBlockBody;
297+
const newPayloadRequestRoot = computeNewPayloadRequestRoot(
298+
chain.config,
299+
blockInput.slot,
300+
executionPayload,
301+
body.blobKzgCommitments ?? [],
302+
block.message.parentRoot,
303+
(body as electra.BeaconBlockBody).executionRequests
304+
);
305+
proofs = chain.executionProofPool.getByRequestRoot(newPayloadRequestRoot);
306+
307+
// Populate the reverse mapping so maybeTransitionToValidOnProofArrival can find the block
308+
if (chain.requestRootToBlockRoot) {
309+
chain.requestRootToBlockRoot.set(toRootHex(newPayloadRequestRoot), blockRootHex);
302310
}
303311
}
304312

packages/beacon-node/src/chain/chain.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ import {PrivateKey} from "@libp2p/interface";
33
import {Type} from "@chainsafe/ssz";
44
import {BeaconConfig} from "@lodestar/config";
55
import {
6-
CheckpointWithHex,
76
CheckpointWithPayloadStatus,
87
ExecutionStatus,
98
IForkChoice,
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import {ContainerType, ListCompositeType} from "@chainsafe/ssz";
2+
import {ChainForkConfig} from "@lodestar/config";
3+
import {ForkSeq, MAX_BLOB_COMMITMENTS_PER_BLOCK} from "@lodestar/params";
4+
import {ExecutionPayload, Root, Slot, deneb, electra, ssz} from "@lodestar/types";
5+
import {kzgCommitmentToVersionedHash} from "../../util/blobs.js";
6+
7+
/**
8+
* EIP-8025: Compute the new_payload_request_root for a given block.
9+
*
10+
* The `newPayloadRequestRoot` is the tree hash root of `NewPayloadRequest`,
11+
* which contains the full execution payload (not the header), versioned hashes,
12+
* parent beacon block root, and execution requests.
13+
*
14+
* This matches Lighthouse's computation: `NewPayloadRequest.tree_hash_root()`.
15+
*/
16+
export function computeNewPayloadRequestRoot(
17+
config: ChainForkConfig,
18+
slot: Slot,
19+
executionPayload: ExecutionPayload,
20+
blobKzgCommitments: deneb.BlobKzgCommitments,
21+
parentBeaconBlockRoot: Root,
22+
executionRequests: electra.ExecutionRequests
23+
): Uint8Array {
24+
const forkSeq = config.getForkSeq(slot);
25+
const versionedHashes = blobKzgCommitments.map(kzgCommitmentToVersionedHash);
26+
27+
// Build the NewPayloadRequest container dynamically.
28+
// Uses the full ExecutionPayload (not the header) — matches Lighthouse's tree_hash_root.
29+
const payloadSszType = getExecutionPayloadSszType(forkSeq);
30+
const NewPayloadRequest = new ContainerType({
31+
executionPayload: payloadSszType,
32+
versionedHashes: new ListCompositeType(ssz.deneb.VersionedHash, MAX_BLOB_COMMITMENTS_PER_BLOCK),
33+
parentBeaconBlockRoot: ssz.Root,
34+
executionRequests: ssz.electra.ExecutionRequests,
35+
});
36+
37+
return NewPayloadRequest.hashTreeRoot({
38+
executionPayload: executionPayload,
39+
versionedHashes,
40+
parentBeaconBlockRoot,
41+
executionRequests,
42+
});
43+
}
44+
45+
function getExecutionPayloadSszType(forkSeq: ForkSeq): ContainerType<any> {
46+
// ExecutionPayload is the same from deneb through electra/fulu
47+
if (forkSeq >= ForkSeq.deneb) {
48+
return ssz.deneb.ExecutionPayload;
49+
}
50+
throw new Error(`EIP-8025 requires at least deneb fork, got fork sequence ${forkSeq}`);
51+
}

packages/types/src/eip8025/sszTypes.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import {ByteListType, ContainerType, ListBasicType, UintNumberType} from "@chainsafe/ssz";
22
import {EXECUTION_PROOF_TYPE_COUNT, MAX_PROOF_DATA_BYTES} from "@lodestar/params";
3-
import {BLSSignature, Root, Slot, Uint8, UintNum64, ValidatorIndex} from "../primitive/sszTypes.js";
3+
import {BLSSignature, Root, Uint8, UintNum64, ValidatorIndex} from "../primitive/sszTypes.js";
44

55
/**
66
* ExecutionProofId identifies which zkVM/proof system + EL combination a proof belongs to.

0 commit comments

Comments
 (0)