Parallelized Executions #1
damip
announced in
Announcements
Replies: 0 comments
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Parallelized Executions
General idea
The idea is to create a blockchain that runs transactions causing state changes in a parallelized way.
Block producers are responsible for packing the blocks with hints on how to schedule the execution of transactions across
THREAD_COUNTparallel execution threads.Block format
The block format is classical, except for execution hints. For every transaction in the block, the following header is added by the block producer:
where:
thread_indexis the execution thread to which the execution of that transaction is bound.gas_offsetis the CPU time (in terms of gas) at which the execution of that transaction starts in threadthread_indexgas_usageis the exact amount of gas required for this executionaccess_listis a list of(resource_key, access_type)triplets listing the resources to which the execution accesses.access_typecan take one of the following three values:READ_ONLY: the resource is only ever readREAD_THEN_WRITE: the resource gets written after being readWRITE_FIRST: the resource gets written without having been read beforehandBlock Execution
Executing a block happens in two passes.
Pass 1: Scheduling
First, the nodes iterate over all the transactions in the block to pre-process them. For each one they:
thread_index < THREAD_COUNTgas_offset + gas_usage <= MAX_THREAD_GASwhereMAX_THREAD_GASis a constant defining the maximal amount of gas allocated to each threadthread_indexhave both theirgas_offsetandgas_offset + gas_usageeither strictly lower than thegas_offsetof the transaction being processed, or both strictly higher than thegas_offset + gas_usageof the operation being pre-processed. This basically ensures that there are no overlaps between executions in a given thread.A) belonging to other threads for which the execution times overlap that of the transaction being processed (B), check access list collisions:Rthat is both in A's and B's access list, perform the following checks:Ris accessed as eitherREAD_THEN_WRITEorWRITE_FIRSTbyAwhileBaccessesRas eitherREAD_ONLYorREAD_THEN_WRITE, consider the scheduling check as failedRthat is both in A's and B's access list, perform the following checks:Ris accessed as eitherREAD_THEN_WRITEorWRITE_FIRSTbyBwhileAaccessesRas eitherREAD_ONLYorREAD_THEN_WRITE, consider the scheduling check as failedIf any of those checks fail, abort the execution of the block transactions: the block producer has not properly scheduled their operations and won't benefit from any of their executions.
If all checks succeeded, for each thread, output the list of transactions to execute sorted by increasing
gas_offset.Pass 2: Executing
As a second step, for each
0 <= thread_index < THREAD_COUNTin parallel:Tinthread_index, sorted by increasinggas_offset:(other.gas_offset + other.gas_usage, other.thread_index) < (T.gas_offset, T.thread_index)to finishT, track its access lists, and generate a list of candidate state changes caused by the execution ofTTor at the end of the execution ofTthe used gas or access lists do not match the declared ones, cancel the execution of T (and only that one). Discard the state changes caused by the execution ofTand consider its execution finished (but the transaction not executed). The block producer does not get any rewards from it and the transaction can be re-included in future blocks.(other.gas_offset + other.gas_usage, other.thread_index) < (T.gas_offset + T.gas_usage, T.thread_index)to finish executingTto the current state. This includes a fee reward to the block producer. Ideally, part of the fee is systematically burned.Tfinished (and transactionTas executed, so it can't be re-included in future blocks). If the execution failed for reasons that are not under the control of the block producer (eg. smart contract code panic), the execution is still considered as done ans the fees transferred.Special case: parallel Argument-Of-Knowledge proof verification
General case
In this section we assume a block containing only transactions each made of a single STARK/SNARK proof verification causing a state update.
In the case of SNARK/STARK proof verification, the fundamental difference with the approach above is that each transaction is a proof verification that accesses resources as inputs and/or outputs, but expects a transaction-specified value for each resource it accesses as input, and outputs transaction-specified values in the resources it writes. A proof verification transaction cannot be executed if the values it uses as inputs do not have the exact values expected by the transaction when its execution starts.
There is only one thing the block producer needs to provide: the absolute order in which all the proof transactions in the block need to be executed one after the other, as if there was no parallelization.
The fact that all the proof verification transactions explicitly specify all the values they use as inputs and outputs allows verifying all the proof transactions of a block in parallel right away, even if they have a sequential execution order in practice.
During proof verification, declared gas limits are checked.
While all proofs are still being verified, validators can simulate applying the changes caused to the state by each proof transaction sequentially, ensuring that each transaction receives its expected input value (the block transactions are dropped if this check fails), and computing the total accumulated state changes caused by the block. This task is very lightweight compared to proof verification and can be done in parallel to proof verification.
The process for block production is more complicated as it requires deducing sequential dependencies among a list of proof transactions in order to deduce a valid absolute order of execution .
Here is an example assuming there are only two transactions
T1andT2to include in the block. We assume that the two transactions are accessing the same resourceRfor which the initial state at the beginning of the block isR0:T1access toRT2access toRRRi1)Ri2)Ri1 != R0T1cannot be includedRi1)Ri2)Ri2 != R0T2cannot be includedRi1)Ro2)Ri1 != Ro2T1cannot be included afterT2Ri1)Ro2)Ri1 != R0T1cannot be included beforeT2Ri1)Ri2) && Write(Ro2)Ri1 != R0T1cannot be included beforeT2Ri1)Ri2) && Write(Ro2)Ro2 != Ri1T1cannot be included afterT2Ri1)Ri2) && Write(Ro2)Ri2 != R0T2cannot be includedRo1)Ri2) && Write(Ro2)Ri2 != R0T2cannot be included beforeT1Ro1)Ri2) && Write(Ro2)Ri2 != Ro1T2cannot be included afterT1Ri1) && Write (Ro1)Ri2) && Write(Ro2)Ri2 != Ro1T2cannot be included afterT1Ri1) && Write (Ro1)Ri2) && Write(Ro2)Ri1 != Ro2T1cannot be included afterT2Ri1) && Write (Ro1)Ri2) && Write(Ro2)Ri1 != R0T1cannot be included beforeT2Ri1) && Write (Ro1)Ri2) && Write(Ro2)Ri2 != R0T2cannot be included beforeT1(all other cases are deduced by reversing
T1andT2in the table)Note that the constraints caused by all value conditions of all resources
Raccumulate. For example, if access to resourceR1requiresT1to run strictly beforeT2and access toR2requiresT2to run strictly beforeT1, then it means that there is no suitable order forT1andT2and the two transactions can't co-exist in a block.This example gives an intuition on only 2 transactions but generalizing it to more transactions requires heavier computation.
An idea to reduce the amount of ordering constraints
Sometimes the proven program does not really use the absolute value of an input, but more restricted information about it. In that case, it becomes possible to make the program inputs only depend on that subset of information, and accompany the provable program with a normal (non-provable) pre-processor program that extracts that information.
For example, if the provable program only accesses the resource
Riby checkingif Ri > 1000, it could be rewritten as a normal non-proven pre-processor program that outputs the booleanRi > 1000and then the proven program simply takes this boolean as input. In that case, even if other transactions modify the absolute value ofRi, as long as it does not change the output of the pre-processor, it won't generate extra ordering constraints.This could be pushed to its maximum in the following way:
This has the following consequences:
Note that something similar could be done for outputs (but for security reasons, this should be done manually by the developer). For example, if a program seeks to increment a value from the state, instead of reading the full value, incrementing it and setting its new value as output, the program could only output the increment "delta" and a post-processor would read the previous value, increment it by "delta" and update it.
Here is an example program:
let's imagine that program was run twice in parallel by different people:
public_counter = 100000T1generated a proof assuming a public inputpublic_counter == 100000andsecret_increment == 1yielding an outputpublic_counter == 100001T2generated a proof assuming a public inputpublic_counter == 100000andsecret_increment == 2yielding an outputpublic_counter == 100002As described in the table above, we are in the case
T1: Read(Ri1) && Write (Ro1);T2: Read(Ri2) && Write(Ro2)withRi2 != Ro1(T2cannot be included afterT1) but alsoRi1 != Ro2(T1cannot be included afterT2), henceT1andT2cannot co-exist in the same block. Only one of them can be included in the block, the other one will need to be discarded.Now imagine we split the program in 3 parts:
the way this triplet is executed is :
preprocessin a VM to generatepublic_thresholdto feed the public input ofprovable_programpublic_thresholdends up being different than the value assumed in the proof of execution ofprovable_program, then it means that the incompatibility persistsprovable_programthat assumes a fixed value forpublic_thresholdand outptusdeltapostprocessin a VM to consume thedeltaoutput ofprovable_progand increment thepublic_counterNote that those 3 runs can all be done in parallel.
Getting back to our example with
T1andT2, we see that the execution of any one of them does not change the value ofpublic_thresholdfor the other. This means that the incompatibility is completely lifted and bothT1andT2can be included in the same block, and in any order !Note that this example is trivial for illustrative purposes, but in the typical case
provable_programis designed to read minimal information from its public inputs (eg. comparison booleans instead of absolute values), perform heavy tasks together with secret inputs, and output minimal information as well (eg. increments instead of new absolute values).All reactions