This blog explains our decision to use an interpreter, specifically WASMI, and not a JIT for Soroban's initial release.
One of the reasons we chose WASM for Soroban is the abundance of mature tooling. These include multiple runtimes, some of which feature a JIT (just-in-time) compiler.
A JIT runtime introduces an earlier “compilation” phase before execution where it converts a contract to native code all at once, so that execution (both in the current and potentially subsequent invocations) runs at “full native-code speed.” An interpreter, in comparison, runs the contract “directly” by repeatedly loading the next WASM instruction and switching on it in a loop, which means it will re-inspect every WASM instruction on each execution, and re-inspect any instruction that is in a loop in a contract every time through the loop.
Note: We use JIT colloquially but actually mean utilizing any compilation step, whether it is just-in-time or ahead-of-time (AOT)
On the surface it sounds like a no-brainer: JIT => faster => better. No? Not exactly.
Smart contract platforms run un-vetted 3rd party code: contracts uploaded by users. That is dangerous to begin with – but JITs make it a whole lot more dangerous in two ways. First, due to the fact that a JIT is emitting native code it is a much larger codebase which is doing significantly more error-prone work, and bugs in its implementation are significantly more likely to be critical (remote code execution – taking over a validator). Every JIT that has ever shipped has had multiple critical vulnerabilities of this sort, and we cannot expect the ones we've seen in the past to be the last. Second, the compilation phase of a JIT is itself vulnerable to malicious input that can cause it to take too long to run or use too much memory, a phenomenon called "JIT bombs" which many JITs are vulnerable to.
The type of JITs that are not vulnerable to “JIT bombs” are typically referred to as “single-pass compilers” or “baseline compilers” – they are designed to complete compilation in a bounded amount of space and time (usually linear in the size of their input). There is currently only one single-pass WASM compiler on the market: Wasmer. Wasmer is a popular tool used by various companies, including some blockchains. However, it is not a dependency we're comfortable introducing to the Stellar ecosystem. While it's open source, it is fairly large at 180k lines of code and developed by a for-profit company that's seeing a lot of drama. Our selected interpreter, WASMI, is an extremely simple 13k lines of code codebase that can be easily modified and replaced.
But it's not just that. Let's say that you use a JIT. Compiling is more involved than interpreting so you're adding start time latency from code upload to first run. This latency is likely too high to accept on each transaction (we run hundreds to thousands of transactions per second) and so contracts will have to be compiled ahead of execution and their native code cached, and new contracts cannot be uploaded and run in a single transaction or even a single block. Also a malicious user can exert pressure on the cache by uploading many contracts, so caching all contracts is probably not feasible, which means we need to reflect the difference between cached and uncached contracts in the fee model exposed to users. What started out looking like a simple performance optimization has turned into a significant reworking of the entire contract pricing, submission, and execution flow.
To top that, we don't know for sure that JITting will provide a significant performance boost. Soroban is built so that contracts use host functions (which are already native code) for heavy computation, not WASM code interpreted by the WASM runtime. Essentially the whole design premise for Soroban is to have WASM code drive the “outer loops” of execution (which don’t matter if they are interpreted) and have host-function native code do all the “inner loops”.
Then there's metering. Blockchains need to track the amount of resources used by contracts. Metering needs to have a strong correlation with fees and wall time or else the system becomes unfair and/or inefficient. This is a unique feature of blockchains, and not included in most WASM runtimes. There are two general ways to retrofit metering into a WASM runtime: either have the runtime capture this information systematically as part of interpretation or compilation, or inject instrumentation into the contract code before submitting it to the runtime.
Given the complex nature of JITs and unexpected optimization paths it's hard to ensure stability or portability of JIT-based metering. In most cases – and probably our case if we used a JIT – JIT-based blockchain users opt for WASM code instrumentation. This makes the latency problem even worse as now before a contract gets compiled it needs to be instrumented; it also introduces an additional potential type of vulnerability, where WASM contract code interferes with injected instrumentation to try to evade cost controls.
For Soroban, since we are using a simple interpreter we are essentially already paying enough per-WASM-instruction overhead that it was easy to modify the interpreter to count WASM instructions executed and charge for them. And again, since Soroban's design delegates most “inner loops” to host functions, we perform metering of these completely separately from WASM-based metering, using cost models calibrated in advance through offline CPU measurements.
To conclude, we are conservative engineers – that’s an important thing when building financial infrastructure trusted by some of the biggest financial services in the world. We appreciate simplicity and predictability. At this moment, this means choosing a simple and predictable WASM runtime – the WASMI Interpreter.
We're not attached to this decision and happy to re-evaluate that in the future. In fact, we're closely monitoring the work wasmtime is doing on single-pass compilation and may revisit the decision as that codebase matures. Again, one nice thing about WASM is that there are usually many possible paths to choose from.