Skip to content

Commit 204c69a

Browse files
authored
EIP-7928: Implement engine_getPayloadBodiesByHashV2 and engine_getPayloadBodieByRangeV2 (#3967)
1 parent 831663a commit 204c69a

File tree

6 files changed

+220
-30
lines changed

6 files changed

+220
-30
lines changed

execution_chain/beacon/api_handler.nim

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,10 @@ export
2626
getPayloadV4,
2727
getPayloadV5,
2828
getPayloadV6,
29-
getPayloadBodiesByHash,
30-
getPayloadBodiesByRange,
29+
getPayloadBodiesByHashV1,
30+
getPayloadBodiesByHashV2,
31+
getPayloadBodiesByRangeV1,
32+
getPayloadBodiesByRangeV2,
3133
newPayload,
3234
forkchoiceUpdated,
3335
getBlobsV1,

execution_chain/beacon/api_handler/api_getbodies.nim

Lines changed: 68 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
# Nimbus
2-
# Copyright (c) 2023-2025 Status Research & Development GmbH
2+
# Copyright (c) 2023-2026 Status Research & Development GmbH
33
# Licensed under either of
44
# * Apache License, version 2.0, ([LICENSE-APACHE](LICENSE-APACHE))
55
# * MIT license ([LICENSE-MIT](LICENSE-MIT))
@@ -14,14 +14,13 @@ import
1414
web3/execution_types,
1515
./api_utils
1616

17-
{.push gcsafe, raises:[CatchableError].}
17+
{.push gcsafe, raises: [CatchableError].}
1818

19-
const
20-
maxBodyRequest = 32
19+
const maxBodyRequest = 32
2120

22-
proc getPayloadBodiesByHash*(ben: BeaconEngineRef,
23-
hashes: seq[Hash32]):
24-
seq[Opt[ExecutionPayloadBodyV1]] =
21+
proc getPayloadBodiesByHashV1*(
22+
ben: BeaconEngineRef, hashes: seq[Hash32]
23+
): seq[Opt[ExecutionPayloadBodyV1]] =
2524
if hashes.len > maxBodyRequest:
2625
raise tooLargeRequest("request exceeds max allowed " & $maxBodyRequest)
2726

@@ -35,9 +34,25 @@ proc getPayloadBodiesByHash*(ben: BeaconEngineRef,
3534

3635
move(list)
3736

38-
proc getPayloadBodiesByRange*(ben: BeaconEngineRef,
39-
start: uint64, count: uint64):
40-
seq[Opt[ExecutionPayloadBodyV1]] =
37+
proc getPayloadBodiesByHashV2*(
38+
ben: BeaconEngineRef, hashes: seq[Hash32]
39+
): seq[Opt[ExecutionPayloadBodyV2]] =
40+
if hashes.len > maxBodyRequest:
41+
raise tooLargeRequest("request exceeds max allowed " & $maxBodyRequest)
42+
43+
var list = newSeqOfCap[Opt[ExecutionPayloadBodyV2]](hashes.len)
44+
45+
for h in hashes:
46+
var body = ben.chain.payloadBodyV2ByHash(h).valueOr:
47+
list.add Opt.none(ExecutionPayloadBodyV2)
48+
continue
49+
list.add Opt.some(move(body))
50+
51+
move(list)
52+
53+
proc getPayloadBodiesByRangeV1*(
54+
ben: BeaconEngineRef, start: uint64, count: uint64
55+
): seq[Opt[ExecutionPayloadBodyV1]] =
4156
if start == 0:
4257
raise invalidParams("start block should greater than zero")
4358

@@ -47,8 +62,7 @@ proc getPayloadBodiesByRange*(ben: BeaconEngineRef,
4762
if count > maxBodyRequest:
4863
raise tooLargeRequest("request exceeds max allowed " & $maxBodyRequest)
4964

50-
var
51-
last = start+count-1
65+
var last = start + count - 1
5266

5367
if start > ben.chain.latestNumber:
5468
# requested range beyond the latest known block.
@@ -58,11 +72,11 @@ proc getPayloadBodiesByRange*(ben: BeaconEngineRef,
5872
last = ben.chain.latestNumber
5973

6074
let base = ben.chain.baseNumber
61-
var list = newSeqOfCap[Opt[ExecutionPayloadBodyV1]](last-start+1)
75+
var list = newSeqOfCap[Opt[ExecutionPayloadBodyV1]](last - start + 1)
6276

6377
if start < base:
6478
# get bodies from database.
65-
for bn in start..min(last, base):
79+
for bn in start .. min(last, base):
6680
var body = ben.chain.payloadBodyV1ByNumber(bn).valueOr:
6781
list.add Opt.none(ExecutionPayloadBodyV1)
6882
continue
@@ -75,3 +89,43 @@ proc getPayloadBodiesByRange*(ben: BeaconEngineRef,
7589
ben.chain.payloadBodyV1InMemory(start, last, list)
7690

7791
move(list)
92+
93+
proc getPayloadBodiesByRangeV2*(
94+
ben: BeaconEngineRef, start: uint64, count: uint64
95+
): seq[Opt[ExecutionPayloadBodyV2]] =
96+
if start == 0:
97+
raise invalidParams("start block should greater than zero")
98+
99+
if count == 0:
100+
raise invalidParams("blocks count should greater than zero")
101+
102+
if count > maxBodyRequest:
103+
raise tooLargeRequest("request exceeds max allowed " & $maxBodyRequest)
104+
105+
var last = start + count - 1
106+
107+
if start > ben.chain.latestNumber:
108+
# requested range beyond the latest known block.
109+
return
110+
111+
if last > ben.chain.latestNumber:
112+
last = ben.chain.latestNumber
113+
114+
let base = ben.chain.baseNumber
115+
var list = newSeqOfCap[Opt[ExecutionPayloadBodyV2]](last - start + 1)
116+
117+
if start < base:
118+
# get bodies from database.
119+
for bn in start .. min(last, base):
120+
var body = ben.chain.payloadBodyV2ByNumber(bn).valueOr:
121+
list.add Opt.none(ExecutionPayloadBodyV2)
122+
continue
123+
list.add Opt.some(move(body))
124+
125+
# get bodies from cache in FC module.
126+
if last > base:
127+
ben.chain.payloadBodyV2InMemory(base, last, list)
128+
else:
129+
ben.chain.payloadBodyV2InMemory(start, last, list)
130+
131+
move(list)

execution_chain/core/chain/forked_chain.nim

Lines changed: 65 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ import
2929

3030
from std/sequtils import mapIt
3131
from std/heapqueue import len
32-
from web3/engine_api_types import ExecutionPayloadBodyV1
32+
from web3/engine_api_types import ExecutionPayloadBodyV1, ExecutionPayloadBodyV2
3333

3434
logScope:
3535
topics = "forked chain"
@@ -991,7 +991,7 @@ proc blockByHash*(c: ForkedChainRef, blockHash: Hash32): Result[Block, string] =
991991

992992
proc payloadBodyV1ByHash*(c: ForkedChainRef, blockHash: Hash32): Result[ExecutionPayloadBodyV1, string] =
993993
c.hashToBlock.withValue(blockHash, loc):
994-
return ok(toPayloadBody(loc[].blk))
994+
return ok(toPayloadBodyV1(loc[].blk))
995995

996996
var header = ?c.baseTxFrame.getBlockHeader(blockHash)
997997
var blk = c.baseTxFrame.getExecutionPayloadBodyV1(header)
@@ -1001,7 +1001,25 @@ proc payloadBodyV1ByHash*(c: ForkedChainRef, blockHash: Hash32): Result[Executio
10011001
if c.isPortalActive:
10021002
var blockBodyPortal = ?c.portal.getBlockBodyByHeader(header)
10031003
# Same as above
1004-
return ok(toPayloadBody(EthBlock.init(move(header), move(blockBodyPortal))))
1004+
return ok(toPayloadBodyV1(EthBlock.init(move(header), move(blockBodyPortal))))
1005+
1006+
move(blk)
1007+
1008+
proc payloadBodyV2ByHash*(c: ForkedChainRef, blockHash: Hash32): Result[ExecutionPayloadBodyV2, string] =
1009+
c.hashToBlock.withValue(blockHash, loc):
1010+
return ok(toPayloadBodyV2(loc[].blk, ?loc[].txFrame.getBlockAccessList(loc[].hash)))
1011+
1012+
var header = ?c.baseTxFrame.getBlockHeader(blockHash)
1013+
var blk = c.baseTxFrame.getExecutionPayloadBodyV2(header)
1014+
1015+
if blk.isErr:
1016+
# Serve portal data if block not found in db
1017+
if c.isPortalActive:
1018+
var blockBodyPortal = ?c.portal.getBlockBodyByHeader(header)
1019+
# Same as above
1020+
return ok(toPayloadBodyV2(
1021+
EthBlock.init(move(header), move(blockBodyPortal)),
1022+
?c.baseTxFrame.getBlockAccessList(header.computeRlpHash())))
10051023

10061024
move(blk)
10071025

@@ -1018,13 +1036,38 @@ proc payloadBodyV1ByNumber*(c: ForkedChainRef, number: BlockNumber): Result[Exec
10181036
if c.isPortalActive:
10191037
var blockBodyPortal = ?c.portal.getBlockBodyByHeader(header)
10201038
# same as above
1021-
return ok(toPayloadBody(EthBlock.init(move(header), move(blockBodyPortal))))
1039+
return ok(toPayloadBodyV1(EthBlock.init(move(header), move(blockBodyPortal))))
10221040

10231041
return blk
10241042

10251043
for it in ancestors(c.latest):
10261044
if number >= it.number:
1027-
return ok(toPayloadBody(it.blk))
1045+
return ok(toPayloadBodyV1(it.blk))
1046+
1047+
err("Block not found, number = " & $number)
1048+
1049+
proc payloadBodyV2ByNumber*(c: ForkedChainRef, number: BlockNumber): Result[ExecutionPayloadBodyV2, string] =
1050+
if number > c.latest.number:
1051+
return err("Requested block number not exists: " & $number)
1052+
1053+
if number <= c.base.number:
1054+
var header = ?c.baseTxFrame.getBlockHeader(number)
1055+
let blk = c.baseTxFrame.getExecutionPayloadBodyV2(header)
1056+
1057+
if blk.isErr:
1058+
# Serve portal data if block not found in db
1059+
if c.isPortalActive:
1060+
var blockBodyPortal = ?c.portal.getBlockBodyByHeader(header)
1061+
# same as above
1062+
return ok(toPayloadBodyV2(
1063+
EthBlock.init(move(header), move(blockBodyPortal)),
1064+
?c.baseTxFrame.getBlockAccessList(header.computeRlpHash())))
1065+
1066+
return blk
1067+
1068+
for it in ancestors(c.latest):
1069+
if number >= it.number:
1070+
return ok(toPayloadBodyV2(it.blk, ?it.txFrame.getBlockAccessList(it.hash)))
10281071

10291072
err("Block not found, number = " & $number)
10301073

@@ -1065,7 +1108,7 @@ proc receiptsByBlockHash*(c: ForkedChainRef, blockHash: Hash32): Result[seq[Stor
10651108

10661109
c.baseTxFrame.getReceipts(header.receiptsRoot)
10671110

1068-
func payloadBodyV1InMemory*(c: ForkedChainRef,
1111+
proc payloadBodyV1InMemory*(c: ForkedChainRef,
10691112
first: BlockNumber,
10701113
last: BlockNumber,
10711114
list: var seq[Opt[ExecutionPayloadBodyV1]]) =
@@ -1078,7 +1121,22 @@ func payloadBodyV1InMemory*(c: ForkedChainRef,
10781121

10791122
for i in countdown(blocks.len-1, 0):
10801123
let y = blocks[i]
1081-
list.add Opt.some(toPayloadBody(y.blk))
1124+
list.add Opt.some(toPayloadBodyV1(y.blk))
1125+
1126+
proc payloadBodyV2InMemory*(c: ForkedChainRef,
1127+
first: BlockNumber,
1128+
last: BlockNumber,
1129+
list: var seq[Opt[ExecutionPayloadBodyV2]]) =
1130+
var
1131+
blocks = newSeqOfCap[BlockRef](last-first+1)
1132+
1133+
for it in ancestors(c.latest):
1134+
if it.number >= first and it.number <= last:
1135+
blocks.add(it)
1136+
1137+
for i in countdown(blocks.len-1, 0):
1138+
let y = blocks[i]
1139+
list.add Opt.some(toPayloadBodyV2(y.blk, y.txFrame.getBlockAccessList(y.hash).expect("ok")))
10821140

10831141
func equalOrAncestorOf*(c: ForkedChainRef, blockHash: Hash32, headHash: Hash32): bool =
10841142
if blockHash == headHash:

execution_chain/db/payload_body_db.nim

Lines changed: 69 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
# nimbus-execution-client
2-
# Copyright (c) 2025 Status Research & Development GmbH
2+
# Copyright (c) 2025-2026 Status Research & Development GmbH
33
# Licensed under either of
44
# * Apache License, version 2.0, ([LICENSE-APACHE](LICENSE-APACHE) or
55
# http://www.apache.org/licenses/LICENSE-2.0)
@@ -13,7 +13,7 @@
1313
import
1414
chronicles,
1515
web3/engine_api_types,
16-
eth/common/blocks_rlp,
16+
eth/common/[blocks_rlp, block_access_lists_rlp],
1717
eth/common/hashes,
1818
./core_db/base,
1919
./core_db/core_apps,
@@ -76,7 +76,53 @@ proc getExecutionPayloadBodyV1*(
7676

7777
ok(move(body))
7878

79-
func toPayloadBody*(blk: Block): ExecutionPayloadBodyV1 =
79+
proc getExecutionPayloadBodyV2*(
80+
db: CoreDbTxRef;
81+
header: Header;
82+
): Result[ExecutionPayloadBodyV2, string] =
83+
const info = "getExecutionPayloadBodyV2()"
84+
var body: ExecutionPayloadBodyV2
85+
86+
for encodedTx in db.getBlockTransactionData(header.txRoot):
87+
body.transactions.add TypedTransaction(encodedTx)
88+
89+
# Txs not there in db - Happens during era1/era import, when we don't store txs and receipts
90+
if (body.transactions.len == 0 and header.txRoot != emptyRoot):
91+
return err("No transactions found in db for txRoot " & $header.txRoot)
92+
93+
if header.withdrawalsRoot.isSome:
94+
let withdrawalsRoot = header.withdrawalsRoot.value
95+
if withdrawalsRoot == emptyRoot:
96+
var wds: seq[WithdrawalV1]
97+
body.withdrawals = Opt.some(wds)
98+
return ok(move(body))
99+
100+
wrapRlpException info:
101+
let bytes = db.get(withdrawalsKey(withdrawalsRoot).toOpenArray).valueOr:
102+
if error.error != KvtNotFound:
103+
warn info, withdrawalsRoot, error=($$error)
104+
else:
105+
# Fallback to old withdrawals format
106+
var wds: seq[WithdrawalV1]
107+
for wd in db.getWithdrawals(WithdrawalV1, withdrawalsRoot):
108+
wds.add(wd)
109+
body.withdrawals = Opt.some(wds)
110+
return ok(move(body))
111+
112+
var list = rlp.decode(bytes, seq[WithdrawalV1])
113+
body.withdrawals = Opt.some(move(list))
114+
115+
if header.blockAccessListHash.isSome():
116+
let bal = ?db.getBlockAccessList(header.computeRlpHash())
117+
body.blockAccessList =
118+
if bal.isSome():
119+
Opt.some(bal.get()[].encode())
120+
else:
121+
Opt.none(seq[byte])
122+
123+
ok(move(body))
124+
125+
func toPayloadBodyV1*(blk: Block): ExecutionPayloadBodyV1 =
80126
var wds: seq[WithdrawalV1]
81127
if blk.withdrawals.isSome:
82128
for w in blk.withdrawals.get:
@@ -91,3 +137,23 @@ func toPayloadBody*(blk: Block): ExecutionPayloadBodyV1 =
91137
else:
92138
Opt.none(seq[WithdrawalV1])
93139
)
140+
141+
func toPayloadBodyV2*(blk: Block, blockAccessList: Opt[BlockAccessListRef]): ExecutionPayloadBodyV2 =
142+
var wds: seq[WithdrawalV1]
143+
if blk.withdrawals.isSome:
144+
for w in blk.withdrawals.get:
145+
wds.add w3Withdrawal(w)
146+
147+
ExecutionPayloadBodyV2(
148+
transactions: w3Txs(blk.transactions),
149+
# pre Shanghai block return null withdrawals
150+
# post Shanghai block return at least empty slice
151+
withdrawals: if blk.withdrawals.isSome:
152+
Opt.some(wds)
153+
else:
154+
Opt.none(seq[WithdrawalV1]),
155+
blockAccessList: if blockAccessList.isSome():
156+
Opt.some(blockAccessList.get()[].encode())
157+
else:
158+
Opt.none(seq[byte])
159+
)

execution_chain/rpc/engine_api.nim

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,9 @@ const supportedMethods: HashSet[string] =
3737
"engine_forkchoiceUpdatedV3",
3838
"engine_forkchoiceUpdatedV4",
3939
"engine_getPayloadBodiesByHashV1",
40-
"engine_getPayloadBodiesByRangeV1",
40+
"engine_getPayloadBodiesByHashV2",
41+
"engine_getPayloadBodiesByRangeV1",
42+
"engine_getPayloadBodiesByRangeV2",
4143
"engine_getClientVersionV1",
4244
"engine_getBlobsV1",
4345
"engine_getBlobsV2",
@@ -113,11 +115,19 @@ proc setupEngineAPI*(engine: BeaconEngineRef, server: RpcServer) =
113115

114116
server.rpc("engine_getPayloadBodiesByHashV1") do(hashes: seq[Hash32]) ->
115117
seq[Opt[ExecutionPayloadBodyV1]]:
116-
return engine.getPayloadBodiesByHash(hashes)
118+
return engine.getPayloadBodiesByHashV1(hashes)
119+
120+
server.rpc("engine_getPayloadBodiesByHashV2") do(hashes: seq[Hash32]) ->
121+
seq[Opt[ExecutionPayloadBodyV2]]:
122+
return engine.getPayloadBodiesByHashV2(hashes)
117123

118124
server.rpc("engine_getPayloadBodiesByRangeV1") do(
119125
start: Quantity, count: Quantity) -> seq[Opt[ExecutionPayloadBodyV1]]:
120-
return engine.getPayloadBodiesByRange(start.uint64, count.uint64)
126+
return engine.getPayloadBodiesByRangeV1(start.uint64, count.uint64)
127+
128+
server.rpc("engine_getPayloadBodiesByRangeV2") do(
129+
start: Quantity, count: Quantity) -> seq[Opt[ExecutionPayloadBodyV2]]:
130+
return engine.getPayloadBodiesByRangeV2(start.uint64, count.uint64)
121131

122132
server.rpc("engine_getClientVersionV1") do(version: ClientVersionV1) ->
123133
seq[ClientVersionV1]:

0 commit comments

Comments
 (0)