Skip to content

Commit 1b3fe18

Browse files
authored
Fluffy: Portal EVM call (#3119)
1 parent cb72975 commit 1b3fe18

File tree

5 files changed

+312
-8
lines changed

5 files changed

+312
-8
lines changed

execution_chain/common/common.nim

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -172,16 +172,17 @@ proc init(com : CommonRef,
172172
genesis : Genesis,
173173
initializeDb: bool) =
174174

175+
175176
config.daoCheck()
176177

177-
com.db = db
178-
com.config = config
178+
com.db = db
179+
com.config = config
179180
com.forkTransitionTable = config.toForkTransitionTable()
180-
com.networkId = networkId
181-
com.syncProgress= SyncProgress()
182-
com.extraData = ShortClientId
183-
com.taskpool = taskpool
184-
com.gasLimit = DEFAULT_GAS_LIMIT
181+
com.networkId = networkId
182+
com.syncProgress = SyncProgress()
183+
com.extraData = ShortClientId
184+
com.taskpool = taskpool
185+
com.gasLimit = DEFAULT_GAS_LIMIT
185186

186187
# com.forkIdCalculator and com.genesisHash are set
187188
# by setForkId

execution_chain/stateless/multi_keys.nim

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
# Nimbus
2-
# Copyright (c) 2020-2024 Status Research & Development GmbH
2+
# Copyright (c) 2020-2025 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)

fluffy/evm/portal_evm.nim

Lines changed: 256 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,256 @@
1+
# Fluffy
2+
# Copyright (c) 2025 Status Research & Development GmbH
3+
# Licensed and distributed under either of
4+
# * MIT license (license terms in the root directory or at https://opensource.org/licenses/MIT).
5+
# * Apache v2 license (license terms in the root directory or at https://www.apache.org/licenses/LICENSE-2.0).
6+
# at your option. This file may not be copied, modified, or distributed except according to those terms.
7+
8+
{.push raises: [].}
9+
10+
import
11+
std/sets,
12+
stew/byteutils,
13+
chronos,
14+
chronicles,
15+
stint,
16+
results,
17+
eth/common/[hashes, addresses, accounts, headers],
18+
../../execution_chain/db/ledger,
19+
../../execution_chain/common/common,
20+
../../execution_chain/transaction/call_evm,
21+
../../execution_chain/evm/[types, state, evm_errors],
22+
../network/history/history_network,
23+
../network/state/[state_endpoints, state_network]
24+
25+
from web3/eth_api_types import TransactionArgs
26+
27+
export
28+
results, chronos, hashes, history_network, state_network, TransactionArgs, CallResult
29+
30+
logScope:
31+
topics = "portal_evm"
32+
33+
# The Portal EVM uses the Nimbus in-memory EVM to execute transactions using the
34+
# portal state network state data. Currently only call is supported.
35+
#
36+
# Rather than wire in the portal state lookups into the EVM directly, the approach
37+
# taken here is to optimistically execute the transaction multiple times with the
38+
# goal of building the correct access list so that we can then lookup the accessed
39+
# state from the portal network, store the state in the in-memory EVM and then
40+
# finally execute the transaction using the correct state. The Portal EVM makes
41+
# use of data in memory during the call and therefore each piece of state is never
42+
# fetched more than once. We know we have found the correct access list if it
43+
# doesn't change after another execution of the transaction.
44+
#
45+
# The assumption here is that network lookups for state data are generally much
46+
# slower than the time it takes to execute a transaction in the EVM and therefore
47+
# executing the transaction multiple times should not significally slow down the
48+
# call given that we gain the ability to fetch the state concurrently.
49+
#
50+
# There are multiple reasons for choosing this approach:
51+
# - Firstly updating the existing Nimbus EVM to support using a different state
52+
# backend (portal state in this case) is difficult and would require making
53+
# non-trivial changes to the EVM.
54+
# - This new approach allows us to look up the state concurrently in the event that
55+
# multiple new state keys are discovered after executing the transaction. This
56+
# should in theory result in improved performance for certain scenarios. The
57+
# default approach where the state lookups are wired directly into the EVM gives
58+
# the worst case performance because all state accesses inside the EVM are
59+
# completely sequential.
60+
61+
const EVM_CALL_LIMIT = 10000
62+
63+
type
64+
AccountQuery = object
65+
address: Address
66+
accFut: Future[Opt[Account]]
67+
68+
StorageQuery = object
69+
address: Address
70+
slotKey: UInt256
71+
storageFut: Future[Opt[UInt256]]
72+
73+
CodeQuery = object
74+
address: Address
75+
codeFut: Future[Opt[Bytecode]]
76+
77+
PortalEvm* = ref object
78+
historyNetwork: HistoryNetwork
79+
stateNetwork: StateNetwork
80+
com: CommonRef
81+
82+
func init(T: type AccountQuery, adr: Address, fut: Future[Opt[Account]]): T =
83+
T(address: adr, accFut: fut)
84+
85+
func init(
86+
T: type StorageQuery, adr: Address, slotKey: UInt256, fut: Future[Opt[UInt256]]
87+
): T =
88+
T(address: adr, slotKey: slotKey, storageFut: fut)
89+
90+
func init(T: type CodeQuery, adr: Address, fut: Future[Opt[Bytecode]]): T =
91+
T(address: adr, codeFut: fut)
92+
93+
proc init*(T: type PortalEvm, hn: HistoryNetwork, sn: StateNetwork): T =
94+
let config =
95+
try:
96+
networkParams(MainNet).config
97+
except ValueError as e:
98+
raiseAssert(e.msg) # Should not fail
99+
except RlpError as e:
100+
raiseAssert(e.msg) # Should not fail
101+
102+
let com = CommonRef.new(
103+
DefaultDbMemory.newCoreDbRef(),
104+
taskpool = nil,
105+
config = config,
106+
initializeDb = false,
107+
)
108+
109+
PortalEvm(historyNetwork: hn, stateNetwork: sn, com: com)
110+
111+
proc call*(
112+
evm: PortalEvm,
113+
tx: TransactionArgs,
114+
blockNumOrHash: uint64 | Hash32,
115+
optimisticStateFetch = true,
116+
): Future[Result[CallResult, string]] {.async: (raises: [CancelledError]).} =
117+
let
118+
to = tx.to.valueOr:
119+
return err("to address is required")
120+
header = (await evm.historyNetwork.getVerifiedBlockHeader(blockNumOrHash)).valueOr:
121+
return err("Unable to get block header")
122+
# Start fetching code in the background while setting up the EVM
123+
codeFut = evm.stateNetwork.getCodeByStateRoot(header.stateRoot, to)
124+
125+
debug "Executing call", to, blockNumOrHash
126+
127+
let txFrame = evm.com.db.baseTxFrame().txFrameBegin()
128+
defer:
129+
txFrame.dispose() # always dispose state changes
130+
131+
# TODO: review what child header to use here (second parameter)
132+
let vmState = BaseVMState.new(header, header, evm.com, txFrame)
133+
134+
var
135+
# Record the keys of fetched accounts, storage and code so that we don't
136+
# bother to fetch them multiple times
137+
fetchedAccounts = initHashSet[Address]()
138+
fetchedStorage = initHashSet[(Address, UInt256)]()
139+
fetchedCode = initHashSet[Address]()
140+
141+
# Set code of the 'to' address in the EVM so that we can execute the transaction
142+
let code = (await codeFut).valueOr:
143+
return err("Unable to get code")
144+
vmState.ledger.setCode(to, code.asSeq())
145+
fetchedCode.incl(to)
146+
debug "Code to be executed", code = code.asSeq().to0xHex()
147+
148+
var
149+
lastWitnessKeys: WitnessTable
150+
witnessKeys = vmState.ledger.getWitnessKeys()
151+
callResult: EvmResult[CallResult]
152+
evmCallCount = 0
153+
154+
# Limit the max number of calls to prevent infinite loops and/or DOS in the
155+
# event of a bug in the implementation.
156+
while evmCallCount < EVM_CALL_LIMIT:
157+
debug "Starting PortalEvm execution", evmCallCount
158+
159+
let sp = vmState.ledger.beginSavepoint()
160+
callResult = rpcCallEvm(tx, header, vmState)
161+
inc evmCallCount
162+
vmState.ledger.rollback(sp) # all state changes from the call are reverted
163+
164+
# Collect the keys after executing the transaction
165+
lastWitnessKeys = ensureMove(witnessKeys)
166+
witnessKeys = vmState.ledger.getWitnessKeys()
167+
vmState.ledger.clearWitnessKeys()
168+
169+
try:
170+
var
171+
accountQueries = newSeq[AccountQuery]()
172+
storageQueries = newSeq[StorageQuery]()
173+
codeQueries = newSeq[CodeQuery]()
174+
175+
# Loop through the collected keys and fetch the state concurrently.
176+
# If optimisticStateFetch is enabled then we fetch state for all the witness
177+
# keys and await all queries before continuing to the next call.
178+
# If optimisticStateFetch is disabled then we only fetch and then await on
179+
# one piece of state (the next in the ordered witness keys) while the remaining
180+
# state queries are still issued in the background just incase the state is
181+
# needed in the next iteration.
182+
var stateFetchDone = false
183+
for k, v in witnessKeys:
184+
let (adr, _) = k
185+
186+
if v.storageMode:
187+
let slotIdx = (adr, v.storageSlot)
188+
if slotIdx notin fetchedStorage:
189+
debug "Fetching storage slot", address = adr, slotKey = v.storageSlot
190+
let storageFut = evm.stateNetwork.getStorageAtByStateRoot(
191+
header.stateRoot, adr, v.storageSlot
192+
)
193+
if not stateFetchDone:
194+
storageQueries.add(StorageQuery.init(adr, v.storageSlot, storageFut))
195+
if not optimisticStateFetch:
196+
stateFetchDone = true
197+
elif adr != default(Address):
198+
doAssert(adr == v.address)
199+
200+
if adr notin fetchedAccounts:
201+
debug "Fetching account", address = adr
202+
let accFut = evm.stateNetwork.getAccount(header.stateRoot, adr)
203+
if not stateFetchDone:
204+
accountQueries.add(AccountQuery.init(adr, accFut))
205+
if not optimisticStateFetch:
206+
stateFetchDone = true
207+
208+
if v.codeTouched and adr notin fetchedCode:
209+
debug "Fetching code", address = adr
210+
let codeFut = evm.stateNetwork.getCodeByStateRoot(header.stateRoot, adr)
211+
if not stateFetchDone:
212+
codeQueries.add(CodeQuery.init(adr, codeFut))
213+
if not optimisticStateFetch:
214+
stateFetchDone = true
215+
216+
if optimisticStateFetch:
217+
# If the witness keys did not change after the last execution then we can
218+
# stop the execution loop because we have already executed the transaction
219+
# with the correct state.
220+
if lastWitnessKeys == witnessKeys:
221+
break
222+
else:
223+
# When optimisticStateFetch is disabled and stateFetchDone is not set then
224+
# we know that all the state has already been fetched in the last iteration
225+
# of the loop and therefore we have already executed the transaction with
226+
# the correct state.
227+
if not stateFetchDone:
228+
break
229+
230+
# Store fetched state in the in-memory EVM
231+
for q in accountQueries:
232+
let acc = (await q.accFut).valueOr:
233+
return err("Unable to get account")
234+
vmState.ledger.setBalance(q.address, acc.balance)
235+
vmState.ledger.setNonce(q.address, acc.nonce)
236+
fetchedAccounts.incl(q.address)
237+
238+
for q in storageQueries:
239+
let slotValue = (await q.storageFut).valueOr:
240+
return err("Unable to get slot")
241+
vmState.ledger.setStorage(q.address, q.slotKey, slotValue)
242+
fetchedStorage.incl((q.address, q.slotKey))
243+
244+
for q in codeQueries:
245+
let code = (await q.codeFut).valueOr:
246+
return err("Unable to get code")
247+
vmState.ledger.setCode(q.address, code.asSeq())
248+
fetchedCode.incl(q.address)
249+
except CatchableError as e:
250+
# TODO: why do the above futures throw a CatchableError and not CancelledError?
251+
raiseAssert(e.msg)
252+
253+
callResult.mapErr(
254+
proc(e: EvmErrorObj): string =
255+
"EVM execution failed: " & $e.code
256+
)

fluffy/nim.cfg

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,3 +15,5 @@
1515
--styleCheck:usages
1616
--styleCheck:error
1717
--hint[Processing]:off
18+
19+
-d:"stateless"

fluffy/rpc/rpc_eth_api.nim

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import
1717
../network/history/[history_network, history_content],
1818
../network/state/[state_network, state_content, state_endpoints],
1919
../network/beacon/beacon_light_client,
20+
../evm/portal_evm,
2021
../version
2122

2223
from ../../execution_chain/errors import ValidationError
@@ -125,12 +126,23 @@ template getOrRaise(stateNetwork: Opt[StateNetwork]): StateNetwork =
125126
raise newException(ValueError, "state sub-network not enabled")
126127
sn
127128

129+
template getOrRaise(portalEvm: Opt[PortalEvm]): PortalEvm =
130+
let evm = portalEvm.valueOr:
131+
raise newException(ValueError, "portal evm not enabled")
132+
evm
133+
128134
proc installEthApiHandlers*(
129135
rpcServer: RpcServer,
130136
historyNetwork: Opt[HistoryNetwork],
131137
beaconLightClient: Opt[LightClient],
132138
stateNetwork: Opt[StateNetwork],
133139
) =
140+
let portalEvm =
141+
if historyNetwork.isSome() and stateNetwork.isSome():
142+
Opt.some(PortalEvm.init(historyNetwork.get(), stateNetwork.get()))
143+
else:
144+
Opt.none(PortalEvm)
145+
134146
rpcServer.rpc("web3_clientVersion") do() -> string:
135147
return clientVersion
136148

@@ -418,3 +430,36 @@ proc installEthApiHandlers*(
418430
storageHash: proofs.account.storageRoot,
419431
storageProof: storageProof,
420432
)
433+
434+
rpcServer.rpc("eth_call") do(
435+
tx: TransactionArgs, quantityTag: RtBlockIdentifier, optimisticStateFetch: Opt[bool]
436+
) -> seq[byte]:
437+
# TODO: add documentation
438+
439+
if tx.to.isNone():
440+
raise newException(ValueError, "to address is required")
441+
442+
if quantityTag.kind == bidAlias:
443+
raise newException(ValueError, "tag not yet implemented")
444+
445+
let
446+
hn = historyNetwork.getOrRaise()
447+
sn = stateNetwork.getOrRaise()
448+
evm = portalEvm.getOrRaise()
449+
450+
let callResult = (
451+
await evm.call(
452+
tx,
453+
quantityTag.number.uint64,
454+
if optimisticStateFetch.isNone():
455+
true
456+
else:
457+
optimisticStateFetch.get(),
458+
)
459+
).valueOr:
460+
raise newException(ValueError, error)
461+
462+
if callResult.error.len() > 0:
463+
raise newException(ValueError, callResult.error)
464+
465+
callResult.output

0 commit comments

Comments
 (0)