nimbus-eth1/nimbus/evm/async_operations.nim

105 lines
4.0 KiB
Nim
Raw Normal View History

Added basic async capabilities for vm2. (#1260) * Added basic async capabilities for vm2. This is a whole new Git branch, not the same one as last time (https://github.com/status-im/nimbus-eth1/pull/1250) - there wasn't much worth salvaging. Main differences: I didn't do the "each opcode has to specify an async handler" junk that I put in last time. Instead, in oph_memory.nim you can see sloadOp calling asyncChainTo and passing in an async operation. That async operation is then run by the execCallOrCreate (or asyncExecCallOrCreate) code in interpreter_dispatch.nim. In the test code, the (previously existing) macro called "assembler" now allows you to add a section called "initialStorage", specifying fake data to be used by the EVM computation run by that test. (In the long run we'll obviously want to write tests that for-real use the JSON-RPC API to asynchronously fetch data; for now, this was just an expedient way to write a basic unit test that exercises the async-EVM code pathway.) There's also a new macro called "concurrentAssemblers" that allows you to write a test that runs multiple assemblers concurrently (and then waits for them all to finish). There's one example test using this, in test_op_memory_lazy.nim, though you can't actually see it doing so unless you uncomment some echo statements in async_operations.nim (in which case you can see the two concurrently running EVM computations each printing out what they're doing, and you'll see that they interleave). A question: is it possible to make EVMC work asynchronously? (For now, this code compiles and "make test" passes even if ENABLE_EVMC is turned on, but it doesn't actually work asynchronously, it just falls back on doing the usual synchronous EVMC thing. See FIXME-asyncAndEvmc.) * Moved the AsyncOperationFactory to the BaseVMState object. * Made the AsyncOperationFactory into a table of fn pointers. Also ditched the plain-data Vm2AsyncOperation type; it wasn't really serving much purpose. Instead, the pendingAsyncOperation field directly contains the Future. * Removed the hasStorage idea. It's not the right solution to the "how do we know whether we still need to fetch the storage value or not?" problem. I haven't implemented the right solution yet, but at least we're better off not putting in a wrong one. * Added/modified/removed some comments. (Based on feedback on the PR.) * Removed the waitFor from execCallOrCreate. There was some back-and-forth in the PR regarding whether nested waitFor calls are acceptable: https://github.com/status-im/nimbus-eth1/pull/1260#discussion_r998587449 The eventual decision was to just change the waitFor to a doAssert (since we probably won't want this extra functionality when running synchronously anyway) to make sure that the Future is already finished.
2022-11-01 15:35:46 +00:00
# Nimbus
# Copyright (c) 2022 Status Research & Development GmbH
# Licensed under either of
# * Apache License, version 2.0, ([LICENSE-APACHE](LICENSE-APACHE) or
# http://www.apache.org/licenses/LICENSE-2.0)
# * MIT license ([LICENSE-MIT](LICENSE-MIT) or
# http://opensource.org/licenses/MIT)
# at your option. This file may not be copied, modified, or distributed except
# according to those terms.
import
chronos,
stint,
json_rpc/rpcclient,
web3,
./computation,
./state,
./types,
../db/accounts_cache
# Used in synchronous mode.
proc noLazyDataSource*(): LazyDataSource =
LazyDataSource(
ifNecessaryGetStorage: (proc(c: Computation, slot: UInt256): Future[void] {.async.} =
discard
)
)
# Will be used in asynchronous on-demand-data-fetching mode, once
# that is implemented.
proc realLazyDataSource*(client: RpcClient): LazyDataSource =
LazyDataSource(
ifNecessaryGetStorage: (proc(c: Computation, slot: UInt256): Future[void] {.async.} =
# TODO: find some way to check whether we already have it.
# This is WRONG, but good enough for now, considering this
# code is unused except in a few tests. I'm working on
# doing this properly.
if not c.getStorage(slot).isZero: return
# FIXME-onDemandStorageNotImplementedYet
# (I sketched in this code, but haven't actually tried running it yet.)
echo("Attempting to for-real fetch slot " & $(slot))
# ethAddressStr("0xfff33a3bd36abdbd412707b8e310d6011454a7ae")
# check hexDataStr(0.u256).string == res.string
let ethAddress = c.msg.contractAddress
let address: Address = Address(ethAddress)
let quantity: int = slot.truncate(int) # this is probably wrong; what's the right way to convert this?
let blockId: BlockIdentifier = blockId(c.vmState.parent.blockNumber.truncate(uint64)) # ditto
let res = await client.eth_getStorageAt(address, quantity, blockId)
echo("Fetched slot " & $(slot) & ", result is " & $(res))
let v = res # will res be the actual value, or do I need to convert or something?
# Before implementing this, see the note from Zahary here:
# https://github.com/status-im/nimbus-eth1/pull/1260#discussion_r999669139
#
# c.vmState.mutateStateDB:
# db.setStorage(c.msg.contractAddress, slot, UInt256.fromBytesBE(v))
)
)
# Used for unit testing. Contains some prepopulated data.
proc fakeLazyDataSource*(fakePairs: seq[tuple[key, val: array[32, byte]]]): LazyDataSource =
LazyDataSource(
ifNecessaryGetStorage: (proc(c: Computation, slot: UInt256): Future[void] {.async.} =
# See the comment above.
if not c.getStorage(slot).isZero: return
# FIXME-writeAutomatedTestsToShowThatItCanRunConcurrently
# For now, until I've implemented some more automated way to
# capture and verify the fact that this can run concurrently,
# this is useful just to see in the console that the echo
# statements from multiple Computations can run at the same
# time and be interleaved.
# echo("Attempting to fake-fetch slot " & $(slot))
# await sleepAsync(2.seconds)
let slotBytes = toBytesBE(slot)
# The linear search is obviously slow, but doesn't matter
# for tests with only a few initialStorage entries. Fix
# this if we ever want to write tests with more.
for (k, v) in fakePairs:
if slotBytes == k:
c.vmState.mutateStateDB:
db.setStorage(c.msg.contractAddress, slot, UInt256.fromBytesBE(v))
break
# echo("Finished fake-fetch of slot " & $(slot))
)
)
# Gotta find the place where we're creating a Computation without setting
# its asyncFactory in the first place, but this is fine for now.
proc asyncFactory*(c: Computation): AsyncOperationFactory =
# Does Nim have an "ifNil" macro/template?
if isNil(c.vmState.asyncFactory):
AsyncOperationFactory(lazyDataSource: noLazyDataSource()) # AARDVARK - can I make a singleton?
else:
c.vmState.asyncFactory