erigontech / silkworm

C++ implementation of the Ethereum protocol
Apache License 2.0
278 stars 64 forks source link

Contract code lifetime #1964

Closed chfast closed 1 month ago

chfast commented 7 months ago

First of all, an account code is almost exclusively used for execution. It may be used as data source via EXTCODECOPY but this instruction is rarely used in practice (see example stats).

When a deployed code is loaded from mdbx it is kept by reference (e.g. in IntraBlockState::existing_code_). This is because the db maps its storage to memory and accessing such code is valid until next commit (?). This works as a cache layer and saves a copy.

However, there are some problems related to the maintaining the code this way.

The existing_code_ lives only for a single block because it's lifetime is bounded by ExecutionProcessor in Blockchain::execute_block(). Maybe this should be extended until the next commit?

The evmone cannot use the code in this form. It needs additional preprocessing of the code: padding (for more efficient interpreter loop) and jumpdest analysis.

Silkworm has a LRU cache of size 5000 of such preprocessed code in EVM::analysis_cache used in EVM::execute_with_baseline_interpreter.

Potential improvement: drop LRU cache

After preprocessing the original code is useless. We can do the preprocessing when loading from db and keep the preprocessed form in the existing_code_. This removes the need for LRU cache. However, to my knowledge there is no way to inform mdbx that the value is not needed any more and can be unloaded from memory.

Potential improvement: preprocess code on deployment

More advanced approach is to preprocess the code when it is being deployed (contract creation) and store the preprocessed version in mdbx. Here we can some sub-options:

  1. Just pad the code with 33 null bytes. evmone would need to allocate another buffer for jumpdest analysis.
  2. Combine padded code and jumpdest analysis into a single buffer and store it in db as the preprocessed code. This increases the code size by ~13%.
  3. Modify the option 2 by using more compressed form of jumpdest analysis taken from the Verkle Tree proposal. Here the size overhead is ~3% but this is untested and the performance of executing jump instructions is unknown.
chfast commented 4 months ago

I implemented this PoC where code is analyzed when getting from "db" and kept in this "executable" form in IntraBlockState. Now checking if this is correct by Mainnet sync... https://github.com/erigontech/silkworm/pull/2097

With the original LRU cache of code analysis the analyze() is close to 0% in profile. This modification bumps it to ~1%.

This also doesn't solve the inefficient DB/mdbx interface where to pad the code I need a full copy of it. Having the full copy if the code the mdbx paged original code is wasted.

chfast commented 1 month ago

The conclusion from the Mainnet execution sync benchmark and manual profiling is the Code Analysis cache is effective. The cache can be also used for code, see https://github.com/erigontech/silkworm/issues/2382.