ACP-194: Streaming Asynchronous Execution
Details for Avalanche Community Proposal 194: Streaming Asynchronous Execution
ACP | 194 |
---|---|
Title | Streaming Asynchronous Execution |
Author(s) | Arran Schlosberg (@ARR4N), Stephen Buttolph (@StephenButtolph) |
Status | Proposed (Discussion) |
Track | Standards |
Abstract
Streaming Asynchronous Execution (SAE) decouples consensus and execution by introducing a queue upon which consensus is performed. A concurrent execution stream is responsible for clearing the queue and reporting a delayed state root for recording by later rounds of consensus. Validation of transactions to be pushed to the queue is lightweight but guarantees eventual execution.
Motivation
Performance improvements
- Concurrent consensus and execution streams eliminate node context switching, reducing latency caused by each waiting on the other. In particular, "VM time" (akin to CPU time) more closely aligns with wall time since it is no longer eroded by consensus. This increases gas per wall-second even without an increase in gas per VM-second.
- Lean, execution-only clients can rapidly execute the queue agreed upon by consensus, providing accelerated receipt issuance and state computation. Without the need to compute state roots, such clients can eschew expensive Merkle data structures. End users see expedited but identical transaction results.
- Irregular stop-the-world events like database compaction are amortised over multiple blocks.
- Introduces additional bursty throughput by eagerly accepting transactions, without a reduction in security guarantees.
- Third-party accounting of non-data-dependent transactions, such as EOA-to-EOA transfers of value, can be performed prior to execution.
Future features
Performing transaction execution after consensus sequencing allows the usage of consensus artifacts in execution. This unblocks some additional future improvements:
- Exposing a real-time VRF during transaction execution.
- Using an encrypted mempool to reduce front-running.
This ACP does not introduce these, but some form of asynchronous execution is required to correctly implement them.
User stories
- A sophisticated DeFi trader runs a highly optimised execution client, locally clearing the transaction queue well in advance of the network—setting the stage for HFT DeFi.
- A custodial platform filters the queue for only those transactions sent to one of their EOAs, immediately crediting user balances.
Description
In all execution models, a block is proposed and then verified by validators before being accepted. To assess a block's validity in synchronous execution, its transactions are first executed and only then accepted by consensus. This immediately and implicitly settles all of the block's transactions by including their execution results at the time of acceptance.
Under SAE, a block is considered valid if all of its transactions can be paid for when eventually executed, after which the block is accepted by consensus. The act of acceptance enqueues the block to be executed asynchronously. In the future, some as-yet-unknown later block will reference the execution results and settle all transactions from the executed block.
Block lifecycle
Proposing blocks
The validator selection mechanism for block production is unchanged. However, block builders are no longer expected to execute transactions during block building.
The block builder is expected to include transactions by building upon the most recently settled state and to apply worst-case bounds on the execution of the ancestor blocks prior to the most recently settled block.
The worst-case bounds enforce minimum balances of sender accounts and the maximum required base fee. The worst-case bounds are described below.
Prior to adding a proposed block to consensus, all validators MUST verify that the block builder correctly enforced the worst-case bounds while building the block. This guarantees that the block can be executed successfully if it is accepted.
[!NOTE] The worst-case bounds guarantee does not provide assurance about whether or not a transaction will revert nor whether its computation will run out of gas by reaching the specified limit. The verification only ensures the transaction is capable of paying for the accrued fees.
Accepting blocks
Once a block is marked as accepted by consensus, the block is put in a FIFO execution queue.
Executing blocks
Each client runs a block executor in parallel, which constantly executes the blocks from the FIFO queue.
In addition to executing the blocks, the executor provides deterministic timestamps for the beginning and end of each block's execution.
Time is measured two ways by the block executor:
- The timestamp included in the block header.
- The amount of gas charged during the execution of blocks.
[!NOTE] Execution timestamps are more granular than block header timestamps to allow sub-second block execution times.
As soon as there is a block available in the execution queue, the block executor starts processing the block.
If the executor's current timestamp is prior to the current block's timestamp, the executor's timestamp is advanced to match the block's. Advancing the timestamp in this scenario results in unused gas capacity, reducing the gas excess from which the price is determined.
The block is then executed on top of the last executed (not settled) state.
After executing the block, the executor advances its timestamp based on the gas usage of the block, also increasing the gas excess for the pricing algorithm.
The block's execution time is now timestamped and the block is available to be settled.
Settling blocks
Already-executed blocks are settled once a following block that includes the results of the executed block is accepted. The results are included by setting the state root to that of the last executed block and the receipt root to that of a MPT of all receipts since last settlement, possibly from more than one block. The following block's timestamp is used to determine which blocks to settle—blocks are settled if said timestamp is greater than or equal to the execution time of the executed block plus a constant delay.
The additional delay amortises any sporadic slowdowns the block executor may have encountered.
Specification
Background
ACP-103 introduced the following variables for calculating the gas price:
the target gas consumed per second | |
minimum gas price | |
gas price update constant | |
gas capacity added per second |
ACP-176 provided a mechanism to make dynamic and set:
The excess actual consumption beyond the target is tracked via numerical integration and used to calculate the gas price as:
Gas charged
We introduce , , and as the gas limit, used, and charged per transaction, respectively. We define
where enforces a lower bound on the gas charged based on the gas limit.
[!NOTE] is rounded up by actually calculating
In all previous instances where execution referenced gas used, from now on, we will reference gas charged. For example, the gas excess will be modified by rather than .
Block size
The constant time delay between block execution and settlement is defined as seconds.
The maximum allowed size of a block is defined as:
Any block whose total sum of gas limits for transactions exceed MUST be considered invalid.
Queue size
The maximum allowed size of the execution queue prior to adding a new block is defined as:
Any block that attempts to be enqueued while the current size of the queue is larger than MUST be considered invalid.
[!NOTE] By restricting the size of the queue prior to enqueueing the new block, is guaranteed to be the only limitation on block size.
Block executor
During the activation of SAE, the block executor's timestamp is initialised to the timestamp of the last accepted block.
Prior to executing a block with timestamp , the executor's timestamp and excess is updated:
The block is then executed with the gas price calculated from the current value of .
After executing a block that charged gas in total, the executor's timestamp and excess is updated:
[!NOTE] The update rule here assumes that is a timestamp that tracks the passage of time both by gas and by wall-clock time. MUST NOT be simply rounded. Rather, the gas accumulation MUST be left as a fraction.
is now this block's execution timestamp.
Handling gas target changes
When a block is produced that modifies , both the consensus thread and the execution thread will update to the modified after their own handling of the block.
For example, restrictions of the queue size MUST be calculated based on the parent block's .
Similarly, the time spent executing a block MUST be calculated based on the parent block's .
Block settlement
For a proposed block that includes timestamp , all ancestors whose execution timestamp is are considered settled. Note that is not an integer as it tracks fractional seconds with gas consumption, which is not the case for .
The proposed block MUST include the stateRoot
produced by the execution of the most recently settled block.
For any newly settled blocks, the proposed block MUST include all execution artifacts:
receiptsRoot
logsBloom
gasUsed
The receipts root MUST be computed as defined in EIP-2718 except that the tree MUST be built from the concatenation of receipts from all blocks being settled.
[!NOTE] If the block executor has fallen behind, the node may not be able to determine precisely which ancestors should be considered settled. If this occurs, validators MUST allow the block executor to catch up prior to deciding the block's validity.
Block validity and building
After determining which blocks to settle, all remaining ancestors of the new block must be inspected to determine the worst-case bounds on and account balances. Account nonces are able to be known immediately.
The worst-case bound on can be calculated by following the block executor update rules using rather than .
The worst-case bound on account balances can be calculated by charging the worst-case gas cost to the sender of a transaction along with deducting the value of the transaction from the sender's account balance.
The baseFeePerGas
field MUST be populated with the gas price based on the worst-case bound on at the start of block execution.
Configuration Parameters
As noted above, SAE depends on the values of and to be set as parameters and the values of and are derived from .
Parameters to specify for the C-Chain are:
Parameter | Description | C-Chain Configuration |
---|---|---|
duration between execution and settlement | ||
minimum conversion from gas limit to gas charged |
Backwards Compatibility
This ACP modifies the meaning of multiple fields in the block. A comprehensive list of changes will be produced once a reference implementation is available.
Likely fields to change include:
stateRoot
receiptsRoot
logsBloom
gasUsed
extraData
Reference Implementation
A reference implementation is still a work-in-progress. This ACP will be updated to include a reference implementation once one is available.
Security Considerations
Worst-case transaction validity
To avoid a DoS vulnerability on execution, we require an upper bound on transaction gas cost (i.e. amount price) beyond the regular requirements for transaction validity (e.g. nonce, signature, etc.). We therefore introduced "worst-case cost" validity.
We can prove that if every transaction were to use its full gas limit this would result in the greatest possible:
- Consumption of gas units (by definition of the gas limit); and
- Gas excess (and therefore gas price) at the time of execution.
For a queue of blocks the gas excess immediately prior to execution of block is a monotonic, non-decreasing function of the gas usage of all preceding blocks in the queue; i.e. .
To see this, consider block consuming gas . A decrease in reduces the immediate increase of . Furthermore, this lowered consumption might further reduce at the start of executing the next block if and therefore . Hence any decrease of is predicted. The excess, and hence gas price, for every later block is therefore reduced:
Given maximal gas consumption under (1), the monotonicity of implies (2).
Since we are working with non-negative integers, it follows that multiplying a transaction's gas limit by the hypothetical gas price of (2) results in its worst-case gas cost. Any sender able to pay for this upper bound (in addition to value transfers) is guaranteed to be able to pay for the actual execution cost. Transaction acceptance under worst-case cost validity is therefore a guarantee of settlement.
Queue DoS protection
Worst-case cost validity only protects against DoS at the point of execution but leaves the queue vulnerable to high-limit, low-usage transactions.
For example, a malicious user could send a transfer-only transaction (21k gas) with a limit set to consume the block's full gas limit.
Although they would have to have sufficient funds to theoretically pay for all the reserved gas, they would never actually be charged this amount. Pushing a sufficient number of such transactions to the queue would artificially inflate the worst-case cost of other users.
Therefore, the gas charged was modified from being equal to the gas usage to the above
The gas limit is typically set higher than the predicted gas consumption to allow for a buffer should the prediction be imprecise. This precludes setting . Conversely, setting would allow users to attack the queue with high-limit, low-consumption transactions.
Setting allows for a 100% buffer on gas-usage estimates without penalising the sender, while still disincentivising falsely high limits.
Upper bound on queue DoS
Recall (gas capacity per second) for rate and (gas charged) as already defined.
The actual gas excess has an upper bound of the worst-case excess , both of which can be used to calculate respective base fees and (the variable element of gas prices) from the existing exponential function:
Mallory is attempting to maximize the DoS ratio
by maximizing to maximize .
[!TIP] Although shadows a variable in ACP-176, that one is very different to anything here so there won't be confusion.
Recall that the increasing excess occurs such that
Since the largest allowed size of the queue when enqueuing a new block is , we can derive an upper bound on the difference in the changes to worst-case and actual gas excess caused by the transactions in the queue before the new block is added:
Note that we can express Mallory's DoS quotient as:
When the queue is empty (i.e. the execution stream has caught up with accepted transactions), the worst-case fee estimate is known to be the actual base fee ; i.e. . The previous bound on also bounds Mallory's ability such that:
Therefore, for the values suggested by this ACP:
In summary, Mallory can require users to increase their gas price by at most ~12%. In practice, the gas price often fluctuates more than 12% on a regular basis. Therefore, this does not appear to be a significant attack vector.
However, any deviation that dislodges the gas price bidding mechanism from a true bidding mechanism is of note.
Appendix
JSON RPC methods
Although asynchronous execution decouples the transactions and receipts recorded by a specific block, APIs MUST NOT alter their behavior to mirror this.
In particular, the API method eth_getBlockReceipts
MUST return the receipts corresponding to the block's transactions, not the receipts settled in the block.
Named blocks
The Ethereum Mainnet APIs allow for retrieving blocks by named parameters that the API server resolves based on their consensus mechanism. Other than the earliest (genesis) named block, which MUST be interpreted in the same manner, all other named blocks are mapped to SAE in terms of the execution status of blocks and MUST be interpreted as follows:
- pending: the most recently accepted block;
- latest: the block that was most recently executed;
- safe and finalized: the block that was most recently settled.
[!NOTE] The finality guarantees of Snowman consensus remove any distinction between safe and finalized. Furthermore, the latest block is not at risk of re-org, only of a negligible risk of data corruption local to the API node.
Observations around transaction prioritisation
As EOA-to-EOA transfers of value are entirely guaranteed upon acceptance, block builders MAY choose to prioritise other transactions for earlier execution.
A reliable marker of such transactions is a gas limit of 21,000 as this is an indication from the sender that they do not intend to execute bytecode.
However, this could delay the ability to issue transactions that depend on these EOA-to-EOA transfers.
Block builders are free to make their own decisions around which transactions to include.
Acknowledgments
Thank you to the following non-exhaustive list of individuals for input, discussion, and feedback on this ACP.
Copyright
Copyright and related rights waived via CC0.
Is this guide helpful?