-
Notifications
You must be signed in to change notification settings - Fork 725
vm_ledger_interaction
This page descibes how the VM interacts with the SCE ledger.
VM execution happens in steps: there is one execution step at every slot. During the execution of a slot S, background async tasks at S are executed first, then the block at slot S (if any) is executed.
The SCE ledger is a hashmap mapping an Address to a balance, optional bytecode, and a datastore hashmap mapping hashes to arbitrary bytes.
The VM maintains a single full copy of the SCE ledger at the output of the latest SCE-final slot, as well as an active execution step history containing the changes caused to the ledger by every active slot. The step history can be sequentially applied on top of the SCE-final ledger to get an SCE ledger entry at the output of any active slot.
During execution, the executed code can access the SCE ledger with read and limited write rights.
The SCE Ledger can become large (up to 1TB). For now, it is in RAM but this needs to be fixed.
The SCE Ledger is represented by the SCELedger structure which acts as a hashmap associating an Address to a SCELedgerEntry.
The SCELedgerEntry structure represents an entry in the ledger and has the following properties:
-
balance: the SCE balance of the address -
opt_module: an optional executable module -
data: HHashMap<Hash, Vec<u8>>: a generic datastore associating a hash to bytes
The SCELedgerChanges struct is a hashmap associating an Address to a SCELedgerChange and represents the entries of the SCE ledger that have changed. The exact change to each entry is described by the SCELedgerChange enum that can be:
-
Delete: the entry was deleted -
Set(SCELedgerEntry): a new entry was inserted or an existing entry was reset to a completely new value -
Update(SCELedgerEntryUpdate): an existing entry was modified. The modifications are described bySCELedgerEntryUpdate
The SCELedgerEntryUpdate struct describes modifications to an SCE ledger entry and has the following fields:
-
update_balance: Option<Amount>: optionally updates the balance of the entry to a new value -
update_opt_module: Option<Option<Bytecode>>: optionally updates the module of the entry to a new one (or to None) -
update_data: HHashMap<Hash, Option<Vec<u8>>>: a list of datastore entries that have been updated to a new value (or deleted if the hashmap value is None)
The VM is represented by the VM structure with the following properties:
-
step_history: a history of active execution steps -
execution_interface: an interface for the interpreter -
execution_context: an execution context
Those fields are described in the next subsections.
The VM contains a step_history property which is a list of StepHistoryItem active steps that have been executed on top of the current final SCE ledger. Each StepHistoryItem represents the summarized consequences of a given active step and has the following properties:
-
slot: the slot to which the step is associated -
opt_block_id: an optional block ID if a block is present at that slot, or None if there is a miss -
ledger_changes: aSCELedgerChangesobject listing the SCE ledger changes caused by that step
The state of an entry of the SCE ledger at the output of a given active execution step can be retrieved by taking the corresponding entry in the final SCE ledger (available in the execution context, see below) and applying ledger changes from the step_history one after the other until the desired active step (included).
TODO
The execution_context field of the VM sturct represents the context in which the current execution runs.
ExecutionContext has the following fields:
-
ledger_stepis aSCELedgerStepthat represents the state of the SCE ledger up to the latest point of the execution of the latest active step -
max_gasis the max amount of gas for the execution -
coinsis the amount of coins that have been transferred to the called SC in the context of the call -
gas_priceis the price (in coins) per unit of gas for the execution -
slotis the slot of the execution step -
opt_block_idblock id being executed (None if absent) -
opt_block_creator_addraddress of the block producer (None if block absent) -
call_stack: call stack listing calling addresses. The latest one is rightmost and should be the address of the called SC when applicable
The SCELedgerStep struct allows accumulating changes caused by the step execution and reading the latest SCE ledger state during execution. Fields:
-
final_ledger_slotaFinalLedgerstructure containing the current finalSCELedgeras well as the slot at the output of which the final ledger is attached -
cumulative_history_changesis aSCELedgerChangesobtained by accumulating theledger_changesof all the previous elements of the VM'sstep_history -
caused_changesis aSCELedgerChangesrepresenting all the changes that happened so far in the current execution
In order to transparently obtain a ledger entry at the current point of the execution, SCELedgerStep provides convenience methods that gather entries from the final ledger, apply the cumulative_history_changes and then the caused_changes. It also provides convenience methods for applying changes to caused_changes.
Whenever the SCE tells the VM to execute an active block or active miss at slot S (see the VM block feed specification), the corresponding execution step is executed by the VM and the state changes caused by the execution are compiled into a StepHistoryItem and added to the step_history.
The detailed algorithm is the following:
- get the execution context ready by resetting
ledger_step.caused_changesand computingledger_step.cumulative_history_changesbased on thestep_history - TODO run async background tasks
- if there is a block B at slot S:
- Note that the block would have been rejected before if the sum of the
max_gasof its operations exceededconfig.max_block_gas - for every
ExecuteSCoperation Op of the block B :- Note that Consensus has already debited
Op.max_gas*Op.gas_price+Op.coinsfrom Op's sender's CSS balance or rejected the block B if there wasn't enough balance to do so - prepare the context for execution:
- make
context.ledger_stepcredit Op's sender withOp.coinsin the SCE ledger - make
context.ledger_stepcredit the producer of the block B withOp.max_gas * Op.gas_pricein the SCE ledger
- make
- save a snapshot (named
ledger_changes_backup) of thecontext.ledger_step.caused_changesthat will be used to rollback the step's effects on the SCE ledger backt to this point in case bytecode execution fails. This is done because on bytecode execution failure (whether it fails completely or midway) we want to credit the block producer with fees (it's not their fault !) and Op's sender withOp.coins(otherwise those coins will be lost !) but revert all the effects of a bytecode execution that failed midway - parse and run (call
main()) the bytecode of operation Op- in case of failure (e.g. invalid bytecode), revert
context.ledger_step.caused_changes = ledger_changes_backup
- in case of failure (e.g. invalid bytecode), revert
- Note that Consensus has already debited
- Note that the block would have been rejected before if the sum of the
- push back the SCE ledger changes caused by the slot
StepHistoryItem { step, block_id (optional), ledger_changes: context.ledger_step.caused_changes }intostep_history
Whenever the SCE tells the VM to execute a final block or final miss at slot S (see the VM block feed specification), the VM first checks if that step was already executed (it should match the first/oldest step in step_history).
If it matches (it should almost always), the step result is popped out of step_history and its ledger_changes are applied to the SCE final ledger.
In the case where the step is not found at the front of step_history, it might mean that there was a deep blockclique change, or that there was nothing in step_history due to a recent bootstrap for example. In that case, step_history is cleared, the Active execution requests process described above is executed again, and its resulting history item is then applied to the final SCE ledger.
After this process, the SCE final ledger now represents the SCE ledger state at the output of slot S.
TODO detail how each one works
// gets the current execution context
get_context() -> {
// call stack
call_stack: Vec<Address>
// last item (stack top) is the current SC context, first item (stack bottom) is the initial caller
// number of coins transferred to the called address during the call
transferred_coins: int
// max gas
max_gas: int
// gas price
gas_price: int
// block ID in which the execution happens (only for ExecuteSC)
block_id: Option<int>
// are we in a read-only execution context ?
is_readonly: bool
// remaining gas
remaining_gas
}
// transfer coins from the current address (if any) towards another
transfer_coins(recipient_address, amount) -> Result<()>
// get the balance of an address
get_balance(address) -> Result<int>
// read the bytecode of an address
get_bytecode(address) -> Result<Vec<u8>>
// runs arbitrary bytecode in the current context by calling a function in it
run_bytecode(bytecode, function_name, parameters, max_gas, gas_price, coins)
// create a new ledger entry and initialize it with a balance and bytecode
// returns the address of the entry
create_sc(balance: int, bytecode) -> Result<Address>
// delete the current address from the ledger, sending freed coins to a recipient
self_destruct(recipient_addr)
// calls a public method of a target SC in the context of the target SC
call(target_addr, function_name, params, max_gas, gas_price, coins) -> Result< ... Return type ? ... >
// gets data from an addresses' storage
data_get(addr: Address, key: Hash) -> Result<Vec<u8>>
// sets data in the current addresses' storage (insert if absent)
data_set(key: Hash, value: Vec<u8>) -> Result<()>
// delete data in the current addresses's storage
data_remove(key: Hash) -> Result<bool>