Add step by step emulator #1794
Open
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Why?
Like any development, writing smart contracts is error prone. What if you read more than you need from a slice? You'll get a VM crash with code 9. But where did the crash occur? What was on the stack just before the crash? How did we get to that branch? A debugger can answer all these questions.
The debugger allows us to go through the program step by step and analyze what went wrong, get the state at any time and gives the ability to analyze this data outside the debugger to understand what went wrong.
Step-by-step emulation is the first step in creating a runtime debugger.
Challenges
The current implementation of the virtual machine and transaction emulator is designed to execute all instructions one by one without interruptions, which makes it impossible to make a runtime debugger in its current form. This PR solves this problem by dividing the execution into several separate parts, which in normal mode follow each other and implement exactly the same behavior as before, but also makes it possible to assemble an API for step-by-step execution from these parts.
The main difficulty of changes for the debugger implementation is returning control after each step.
Let's look at a normal execution. User in this case can be a debugger or another tool:
When we want to call a get method, we send a request and get the result. In the process, TVM executes the code of the given get method, executing each instruction one after another.
Now let's look at what happens during the step-by-step execution that is necessary for debugging:
As you can see, in this case we do not get the execution result right away, in the first step we only prepared the emulator and TVM for executing the get method, but no code has been executed yet. At the same time, we return control back to the user and wait until it sends the next request to execute the first instruction. And so step by step the user can go through the entire code of the get method and at each step request additional information, such as the current stack or the value in the control registers.
Thus, the main difficulty in step-by-step execution comes down to the fact that if in normal execution the entire execution state was local, since all the code was executed at once, in step-by-step execution the state must be saved between each execution step so that when the user requests the next step, we restore this state and execute the next step.
Implementation
Step-by-step execution is not needed in production, so the main user and interface from the outside is a transaction emulator that is later built to WASM and then used in the sandbox for local testing and transaction emulation.
TvmEmulator vs TransactionEmulator
In the current implementation of the emulator, the execution of get methods and the emulation of transactions are separated.
TvmEmulator
is responsible for emulating get methods, andTransactionEmulator
is responsible for transactions. This duality leads to the fact that the following functions described that are exported and can be called from WASM have two forms, one forTvmEmulator
and one forTransactionEmulator
.transaction_emulator_sbs_emulate_transaction & tvm_emulator_sbs_run_get_method
Prepare a transaction or get method to be emulated (as shown in the previous diagram in the first step).
transaction_emulator_sbs_step & tvm_emulator_sbs_step
Perform the next step of the emulation.
transaction_emulator_sbs_result && tvm_emulator_sbs_get_method_result
Complete the emulation and return the result.
Data getters
The functions for getting the current state (stack, c7 and current position) are also duplicated.
For a more complete description, see the documentation in the code.
Shared state
As described earlier, step-by-step execution adds the need to store the state not locally, but somewhere where it will be preserved between calls. Such places became the emulators themselves:
TvmEmulator
andTransactionEmulator
. Both emulators now contain the state of the virtual machine and a logger. These two fields retain their values from the beginning of emulation in step-by-step mode until the moment of callingtransaction_emulator_sbs_result
andtvm_emulator_sbs_get_method_result
, where they are reset.TransactionEmulator
also has a number of other fields:Which save the execution state that was previously also local. After calling
transaction_emulator_sbs_result
these fields are reset so that next transactions will not use the old values.Unresolved issues
TODO
Thanks