Standalone engine #328
Replies: 2 comments 3 replies
-
No, I had mentioned that FFIs are necessarily designed in terms of the C ABI. That is, all function signatures, data types, and data structures used in the FFI layer must be C-compatible so that they can be consumed on the other side of the FFI. |
Beta Was this translation helpful? Give feedback.
-
This is an expensive operation (it is ok doing it occasionally) but beware the current size of the state used by With that in mind, doing a snapshot every 50 blocks (roughly 50 seconds) means we will be storing >100Mb every 50 seconds. We should not be doing snapshots too often, or rather doing them only once at startup, and after that relay only on updates from the sync tool (indexer). This however, open new questions about how to efficiently access the state. In columns 4..8 we should only store values after they changed. To access a particular value of the state we can run a lower bound. However, this means we need to start indexing by height rather than by hash, and extra care must be taken with reorgs. To avoid reorgs all-together we can establish that only finalised state will be added to these columns. Note: To replay a tx, a persistent version of all state must be accessible, even the state that is not modified by the tx, given that it can be queried anyway.
If the field is indexed by height, lower bound can be used instead. Motivation for Logs in a transaction can be computed efficiently without cloning, i.e. one log can be derived from the previous log by performing small number of operations. Cloning a log, means the stack and memory will be duplicated per log which can be inefficient, regarding the overhead created for storing, buffering and communicating this data. The main goal of having a |
Beta Was this translation helpful? Give feedback.
-
Requirements
NOTE: Current state of this document is that the changes to Sputnik EVM are clear. The why of the Engine standalone is in. The storage aspect is clear. The FFI interfaces are still WIP.
The purpose of this change is to add geth-like [
debug_traceTransaction
] logic to Sputnik EVM (rust-blockchain/evm). On top of that the ability to run a standalone engine that is able to keep in sync with the network EVM.Why this is an important change is that it is absolutely necessary for this logic for both Block Scout and Etherscan's upcoming block explorer for Aurora.
On Sputnik EVM, this is a relatively straight forward change.
rust-engine-standalone
The heart and bulk of what we need to do is here. We need to create a new library.
It needs to consist of a few parts in order to accomplish our described goals:
tracing
feature enabled.Design
The purpose of the rust-engine-standalone is to enable the ability to run a local instance of a 1:1 compatible EVM to our Engine.
What this solves is the ability to be able to re-run transactions through a local instance of the EVM which carries its own state. This enables key features that partners of ours such as Etherscan to use methods such as
debug_traceTransaction
. For now, enabling everything to unblock our partner is the sole focus of this project.FFI Interface
In regards as to why FFI was chosen over other methods is for a few reasons. Firstly, we didn't want to run it as a service with a websocket. Also, we didn't want to run it as a CLI as that would have us create quite a bit of infrastructure to run. In the end, given the requirements, WASM and FFI were the two most liekly best choices. WASM would do great in a Javascript context for instance. However, to ensure maximum compatibility, FFI was ultimately chosen.
We need to be mindful that errors could possibly crash the Relayer which would be absolutely devasting. We can not afford any downtime. For that reason, we need to route all possible
panic!
s to be handled in another way which prevents a crash.FFI Methods
WIP, the general idea is very straight forward. However, there is a bit more that needs to be expanded upon here. For example, of course we just need a simple trace function that takes in a transaction ID and spits out the return of the geth-like tracer as Javascript. Also, the methods that will pass the current blocks with just Aurora information to keep the standalone in sync with the network.
Arto had mentioned that we should do the FFI in C, however Rust is perfectly capable of doing C compatible FFI in with the libc crate. This does still need to be explored if we should just do it in Rust, or in C, or both.
The examples below will be simply in Rust.
Below, the
trace_transaction
method must take in a transaction hash and return aTransactionTrace
. The method must be able to replay a transaction with the current state in the exact same manner it was initially executed. It also must return aTransactionTrace
which is defined below.geth-like Tracer
From the following design, it should be used with the FFI returns as Javascript, based on the
JavaScript-based tracing
section of the debug_traceTransaction documentation.Storage implementation
The standalone engine will have its own storage implementation. The goal of this storage layer is to efficiently represent the EVM state from block to block. Here we describe a proposal for how the storage should be implemented.
I propose we use RocksDB as the underlying database. RocksDB is a key-value store with sorted keys to allow efficient range queries. Therefore the key layout is important. I propose we have the following key layout:
I propose the database contain the following columns indicators (given in hex):
(BlockHash, u16)
, which represents the block and position in that block where the transaction was included. If the transaction was included in multiple blocks, then the one which is in the canonical chain is given as the value. This means this column must be modified in the case of a re-org.[u8; 32]
. Only non-zero values are stored. Just like the previous three columns, this is used for snapshotting.High level storage interface
The interface to interact with this storage layer will be as follows:
Example Recipies of the storage
Replay given transaction
Tracking addresses and balances
Getting a list of addresses with non-zero nonce (or really any nonce) bound is trivial by simply iterating over some block in column 06. Similarly for balances with column 05. Moreover, the diffs could be used to see only the new accounts and balances between two blocks. This could be useful for analytics or more advanced relayer rate-limiting.
Sync
It is important to be able to keep the storage up to date with the NEAR network. When starting the system for the first time we can bootstrap a storage snapshot (eg by consuming the JSON dump from contract state library). Once we have an initial snapshot, we can update the state by consuming a stream of new information from a NEAR RPC node directly. The NEAR indexer framework is a library which is designed for exactly this purpose. To populate the diffs the standalone engine would need to execute all transactions targeting
aurora
locally, therefore the transaction outcomes from NEAR are not important. We only need to know what blocks included transactions that targetaurora
.Relayer
On the relayer side it needs to be able to run the wasm compiled code of the new code. The compiled uses the same backend and EVM with precompiles AS the engine. We simply would need to pass the transaction hash and return the trace in full as described above in the FFI methods above with the method
trace_transaction
.On the relayer, it must be able to provide the full capability fullfilling the JavaScript-based tracing requires as mentions in the debug_traceTransaction documention in geth.
Of course as geth is written in Go, and our relayer implementation is in Typescript, keeping it all in Typescript should be fine and sufficient especially given our means which are more Javascript orientated.
Additionally, the relayer must pass the ongoing state changes to the standalone also described with the methods above in FFI methods. This will keep the standalone in sync.
Sputnik EVM
In the Sputnik EVM library there already exists a tracer however it does not have all the complete data required for geth-style tracing.
With the two events
Step
andStepResult
, both can be combined into a single trace. However, there are parts that are missing that should be added in order fulfil the requirements.Note that there are multiple
Step
s and a singleStepResult
per trace.In geth, the object is similar to the below as follows:
For reference, you can look at debug_traceTransaction example section.
Sputnik EVM Tracing Status
For a similar data structure we can pull most of what we need from the Sputnik EVM tracing itself. However, it is missing some key details which need to be added. The current status of what we can pull from the existing tracing in EVM is as follows.
The main trace object is as follows:
Log objects:
From the above, we can create a similar structure in Rust on our new
rust-engine-standalone
library.Tasks
Additions to Step
Aurora Engine
Right now there isn't anything we need to change on the engine itself.
Beta Was this translation helpful? Give feedback.
All reactions