CAP: 0056
Title: Soroban intra-transaction module caching
Working Group:
Owner: Graydon Hoare <@graydon>
Authors: Graydon Hoare <@graydon>
Consulted: Jay Geng <@jayz22>, Dmytro Kozhevin <@dmkozh>, Nicolas Barry <@MonsieurNicolas>, Tomer Weller <@tomerweller>
Status: Final
Created: 2024-03-13
Discussion: https://github.com/stellar/stellar-protocol/discussions/1460
Protocol version: 21
Lower total costs by caching parsed Wasm modules within a Soroban transaction.
As specified in the Preamble.
To lower the CPU cost charged for each contract invocation transaction thereby admitting more such transactions per ledger and increasing total throughput.
This change is aligned with the goal of lowering the cost and increasing the scale of the network.
Soroban transactions may invoke the same contract more than once in a transaction, for example by making multiple calls to different methods on the same token contract. Currently each such cross-contract call re-parses the called contract as part of instantiating it. This CAP proposes adding a module cache to the Soroban host, such that contracts are only parsed once per transaction.
Each Soroban transaction typically consists of a tree of contract invocations. Each contract in that tree of invocations must be parsed and instantiated at least once, but if the tree calls the same contract more than once within a transaction, there is an easy opportunity for a performance improvement by not parsing the contract repeatedly for each call, but reusing a cached copy.
To do this requires both caching parsed contracts, as well as separately charging for parsing (which can be done one per module per transaction) and later phases of instantiation (which must be repeated per invocation). This separation existed in the previous (unused) distinction between the VmInstantiation
and VmCachedInstantiation
cost types, and is preserved by the separation of new cost types in CAP-0054. This CAP can be accepted with or without CAP-0054, but the split in cost-types in CAP-0054 between parsing and instantiation is motivated by this CAP.
There are no XDR changes beyond those proposed in CAP-0054, which as mentioned above are actually optional too. This CAP works with or without it.
This section describes the change to observable behaviour.
- When a Soroban transaction is constructed, before it begins executing:
- For each
ContractCode
ledger entry mentioned in the read footprint of the transaction:- If CAP-0054 is accepted and the ledger entry contains the new CAP-0054 refined cost model input types:
- The transaction is charged the new refined
ParseWasm*
module-parsing cost models with the new refined cost model inputs
- The transaction is charged the new refined
- Else:
- The transaction is charged the old
VmInstantiation
cost type, which will have been recalibrated by this change to only cover the cost of parsing theContractCode
's Wasm module, not fully instantiating it.
- The transaction is charged the old
- If CAP-0054 is accepted and the ledger entry contains the new CAP-0054 refined cost model input types:
- For each
- When a Soroban transaction performs an invocation on some contract implemented by some Wasm module:
- If CAP-0054 is accepted and the ledger entry contains the new CAP-0054 refined cost model input types:
- The transaction is charged the new refined
InstantiateWasm*
module-instantiating cost models with the new refined cost model inputs
- The transaction is charged the new refined
- Else:
- The transaction is charged the old
VmCachedInstantiation
cost type, which will have been recalibrated by this change to only cover the cost of instantiating an already-parsed module.
- The transaction is charged the old
- If CAP-0054 is accepted and the ledger entry contains the new CAP-0054 refined cost model input types:
This section explains the implementation. It is intended for illustration purposes, and other possible impementations that achieve the same observable normative behaviour are possible.
- When a Soroban
Host
is constructed for a transaction, it contains a new module cache that will hold parsed Wasm modules. - The module cache is then, before any modules are executed, unconditionally populated with a parsed Wasm module for every contract mentioned in the transaction's read footprint.
- When parsing a module for caching, it will be charged to either the cost model of the old
VmInstantiation
cost type, or the new refined cost models for the CAP-0054ParseWasm*
cost types.
- When parsing a module for caching, it will be charged to either the cost model of the old
- As invocations require instantiation of parsed modules, pre-parsed modules will be extracted from the cache and instantiated.
- When instantiating a cached module, it will be charged to either the cost modelof the old
VmCachedInstantiation
cost type, or the new refined cost models for the CAP-0054InstantiateWasm*
cost types.
- When instantiating a cached module, it will be charged to either the cost modelof the old
For the most part the design is straightforward: add a cache and use it.
The only subtle rationale is the need for eager instantiation. Due to particularities of the way the Wasm VM in Soroban works -- the VM's "engine" is locked during execution -- the cache must be fully populated before any execution begins; caching cannot happen "on demand" as a contract runs.
As a result, the set of cached contracts will depend solely on the read footprint, not any further deviation from the footprint in the contract's execution: every contract present in the read footprint of a transaction will be parsed and cached eagerly when initializing the transaction.
Eager cache population is also the most likely structure to be compatible with future extensions of this work to include caching modules across transactions, or even across ledgers. However, such future work requires more complex fee and transaction-queueing logic, and is out of scope for the current proposed change.
The change is broadly backward compatible (new software can continue to process old data).
The change will charge nonzero costs to a cost type that, before the change, sees only zero costs. But users and operators should not be relying on those costs to be zero.
While it is possible to construct a transaction that is charged more with the eager parsing in this CAP than it would with lazy parsing on today's network, such a transaction is quite contrived: it would require contract A to call contract B during simulation (when the footprint is recorded and initial fee is estimated) and then change its decision when executing on-chain for real and not call contract B, typically just moments after the simulation that recorded its intent to call B.
This type of transaction seems sufficiently unlikely to occur that we think it can be discounted, especially given that the only penalty for it occurring would not be a transaction failure but merely a slightly higher-than-necessary fee being charged: the fee returned from simulation, rather than a lower one that (somehow) anticipated the transaction's changed decision.
We expect in practice that all transactions will be charged either the same as they are before the change (if there are no cache hits) or significantly less (if there are cache hits).
None apparent.
TBD
A preliminary implementation is underway in the soroban-env-host repository