0102 XLS-102d: WASM VM Configuration #303
Replies: 3 comments 4 replies
-
|
I'd like to hear your thoughts around how the VM is used. Is a new instance setup for every execution (Time consuming)? How could the same instance be cleaned and reused, what risk is involved if previous contract still has dangling pointers.... (there is a trade off here one is better scaling the other is safer contracts). |
Beta Was this translation helpful? Give feedback.
-
|
Just seen this. I'm Syrus, from Wasmer. I realized some of the information presented is incorrect. Wasmer actually already supports interpreter via Wasmi or WAMR backends under the hood. Wasmer has a pluggable architecture, so using the same API you can target Wasmer's own compilers (Singlepass), external compilers (Cranelift, LLVM), external runtimes (Wasmi, V8, WAMR, JavascriptCore, ...) |
Beta Was this translation helpful? Give feedback.
-
|
Two things I'm unsure about:
|
Beta Was this translation helpful? Give feedback.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
-
WASM VM Configuration
Abstract
This document describes the integration of WebAssembly (WASM) into the XRP Ledger as a secure and deterministic execution environment for smart contract logic. WASM-based execution allows developers to write custom logic in a wide range of languages, compile to a portable binary format, and run within a sandboxed virtual machine governed by the consensus process. The standard outlines the interface, constraints, and security model for deploying, invoking, and validating WASM subroutines on-ledger, while ensuring compatibility with existing ledger primitives and transaction flow.
1. Overview
This document addresses several parts of the WASM integration into rippled:
This feature does not (directly) involve any new transactions, ledger objects, or RPCs. It will be gated by the
SmartEscrowamendment. Any modification to the details in this spec will require an amendment, as it will affect transaction processing (e.g. success/failure of anEscrowFinishtransaction for a Smart Escrow).1.1. How the WASM Engine Integrates into
rippledUsing Smart Escrows as an example:
FinishFunctioncode to determine if the escrow is finishable).1.2. Background: What is a “Host Function”?
A host function is a function expressed outside WebAssembly but passed to a module as an import. They’re somewhat analogous to precompiles in the EVM world.
In other words, it’s basically an API call that fetches/interacts with data or native compute outside of the WASM VM.
1.3. Background: WASM Native Types
There are only 4 native types in the WASM spec:
i32(a signed 32-bit integer),i64(a signed 64-bit integer),f32(a 32-bit floating point number), andf64(a 64-bit floating point number). However, the floating point numbers use a different encoding from whatrippleduses.So essentially, we only have
i32andi64in terms of useful types. Every parameter and return type must be represented as these two types. This is manifested as pointers and lengths. Note that any language that has full support for extensions will have helper functions to abstract away most of the complexity (especially involving pointers and lengths).2. VM Runtime Choice
While WebAssembly has a core specification, different runtimes have flexibility in how they implement certain features that are not a part of the formal specification. For example, not all WASM runtimes can easily be embedded in a C++ project (such as
rippled).The most relevant part for the purpose of consensus is the gas cost for any operation or function. Different implementations may have different gas costs for executing a given function, due to implementation differences - e.g. some calculate gas costs by inserting additional instructions, while others have a counter in the VM logic. For instance, one basic Smart Escrow function cost 110 gas to run with WasmEdge, while it only cost 4 gas with WAMR. This would cause consensus issues if the computation limit was set at 100, for example - one runtime would succeed, while the other would fail.
There are other metrics that are important as well, such as performance considerations. See Appendix A for the full analysis comparing different runtimes.
2.1. Gas
Gas consumption is determined by the WASM runtime used for execution. Different implementations may use different metering strategies, which yields different gas costs for identical WASM code. For example, this issue discusses how WAMR measures gas consumption.
Gas will accumulate from:
grow_memory)Exceeding the provided gas budget triggers immediate execution halting, with a deterministic failure consistent across implementations.
3. Execution Limits
The XRPL cannot allow unbounded execution, as there is a time limit for ledgers to close in order for consensus to execute in a timely manner. There are three methods of ensuring that the WASM code does not take too many resources:
All of these parameters will be UNL-votable, so that they do not need a separate amendment to be modified.
4. Memory Management Strategies
WebAssembly does not include built-in memory management - there is no garbage collector or automatic heap allocation. Instead, memory is a contiguous linear buffer that can only grow (in fixed 64 KiB pages) and never shrink. That means WASM code must allocate and manage its own memory, typically via a custom allocator or language runtime, and explicitly track when memory is no longer needed. While there is progress on this front with WasmGC, it does not have full-fledged tooling support yet, and is currently only really useful for browser applications of WASM.
This is a bit of a problem for host functions, since data has to go back and forth between the caller (WASM dev) and the engine (
rippled). Some data (e.g. parameters) may be generated on the WASM side, while some data (e.g. the return data) may be generated on the rippled side.Therefore, in this design, the caller is responsible for allocating memory in advance, and must reuse or deallocate memory manually. See Appendix B for alternative designs that were considered and rejected.
5. Extension Host Functions
This section introduces WASM host functions for extensions on the XRP Ledger, enabling WASM bytecode (in an extension or smart contract) to securely interact with ledger data and the ledger’s native features. These functions provide controlled access to ledger state, transaction execution, and XRPL primitives while maintaining efficiency and security.
WASM code, whether in an extension or a smart contract, needs access to XRPL ledger data in order to be useful. A host function allows that access, in a secure way. Host functions can also be used to save gas/compute in WASM, as they can perform those same functions in C++ code instead, which will likely be more performant.
Some examples from XLS-100d:
EscrowFinishtransaction (to know who is sending the transaction).This spec only covers Smart Escrow host functions at this time.
These host functions will be accessible from extensions and smart contracts.
Note: all these functions return an
i32, unless otherwise noted (or there is no buffer parameter). If the value is positive, it's a length. If it's negative, it's an error code.5.1. General Ledger Data
This section includes ledger header data, amendments, and fees.
get_ledger_sqn()get_parent_ledger_time()get_parent_ledger_hash(out_buff_ptr: i32,out_buff_len: i32)get_ledger_account_hash(out_buff_ptr: i32,out_buff_len: i32)get_ledger_transaction_hash(out_buff_ptr: i32,out_buff_len: i32)amendment_enabled(amendment_ptr: i32,amendment_len: i32)get_base_fee()5.2. Current Ledger Object data
The current ledger object is the ledger object that the extension lives on - for Smart Escrows that's an
Escrowobject.get_current_ledger_obj_field(field: i32,out_buff_ptr: i32,out_buff_len: i32)get_current_ledger_obj_nested_field(locator_ptr: i32,locator_len: i32,out_buff_ptr: i32,out_buff_len: i32)get_current_ledger_obj_array_len(field: i32)get_current_ledger_obj_nested_array_len(locator_ptr: i32,locator_len: i32)5.2.1. Locators
A Locator allows a WASM developer located any field in any object (even nested fields) by specifying a
slot_num(1 byte); alocator_field_type(1 byte); then one of ansfield(4 bytes) or anindex(4 bytes).5.3. Current Transaction Data
The current transaction is the
EscrowFinishthat is executing the WASM logicget_tx_field(field: i32,out_buff_ptr: i32,out_buff_len: i32)get_tx_nested_field(locator_ptr: i32,locator_len: i32,out_buff_ptr: i32,out_buff_len: i32)get_tx_array_len(field: i32)get_tx_nested_array_len(locator_ptr: i32,locator_len: i32)5.4. Any Ledger Object Data
Fetch data from any other ledger object
cache_ledger_obj(keylet_ptr: i32,keylet_len: i32,cache_num: i32)get_ledger_obj_field(cache_num: i32,field: i32,out_buff_ptr: i32,out_buff_len: i32)get_ledger_obj_nested_field(cache_num: i32,locator_ptr: i32,locator_len: i32,out_buff_ptr: i32,out_buff_len: i32)get_ledger_obj_array_len(cache_num: i32,field: i32)get_ledger_obj_nested_array_len(cache_num: i32,locator_ptr: i32,locator_len: i32)5.5. Keylets
A keylet is a unique hash that represents a ledger object on the XRP Ledger. It is a 256-bit hash, constructed from unique identifiers for an object. For example, an
AccountRoot's hash is constructed from itsAccountID, and anOracle's hash is constructed from itsOwnerandDocumentID.account_keylet(account_ptr: i32,account_len: i32,out_buff_ptr: i32,out_buff_len: i32)AccountRoot's keylet from its pieces.amm_keylet(issue1_ptr: i32,issue1_len: i32,issue2_ptr: i32,issue2_len: i32,out_buff_ptr: i32,out_buff_len: i32)AMM’s keylet from its pieces.check_keylet(account_ptr: i32,account_len: i32,sequence: i32,out_buff_ptr: i32,out_buff_len: i32)Check's keylet from its pieces.credential_keylet(subject_ptr: i32,subject_len: i32,issuer_ptr: i32,issuer_len: i32,cred_type_ptr: i32,cred_type_len: i32,out_buff_ptr: i32,out_buff_len: i32)Credential's keylet from its pieces.delegate_keylet(account_ptr: i32,account_len: i32,authorize_ptr: i32,authorize_len: i32,out_buff_ptr: i32,out_buff_len: i32)Delegate's keylet from its pieces.deposit_preauth_keylet(account_ptr: i32,account_len: i32,authorize_ptr: i32,authorize_len: i32,out_buff_ptr: i32,out_buff_len: i32)DepositPreauth's keylet from its pieces.did_keylet(account_ptr: i32,account_len: i32,out_buff_ptr: i32,out_buff_len: i32)DID's keylet from its pieces.escrow_keylet(account_ptr: i32,account_len: i32,sequence: i32,out_buff_ptr: i32,out_buff_len: i32)Escrow's keylet from its pieces.line_keylet(account1_ptr: i32,account1_len: i32,account2_ptr: i32,account2_len: i32,currency_ptr: i32,currency_len: i32,out_buff_ptr: i32,out_buff_len: i32)mpt_issuance_keylet(issuer_ptr: i32,issuer_len: i32,sequence: i32,out_buff_ptr: i32,out_buff_len: i32)MPTIssuance’s keylet from its pieces.mptoken_keylet(mptid_ptr: i32,mptid_len: i32,holder_ptr: i32,holder_len: i32,out_buff_ptr: i32,out_buff_len: i32)MPToken’s keylet from its pieces.nft_offer_keylet(account_ptr: i32,account_len: i32,sequence: i32,out_buff_ptr: i32,out_buff_len: i32)NFTOffer's keylet from its pieces.offer_keylet(account_ptr: i32,account_len: i32,sequence: i32,out_buff_ptr: i32,out_buff_len: i32)Offer's keylet from its pieces.oracle_keylet(account_ptr: i32,account_len: i32,document_id: i32,out_buff_ptr: i32,out_buff_len: i32)Oracle's keylet from its pieces.paychan_keylet(account_ptr: i32,account_len: i32,destination_ptr: i32,destination_len: i32,sequence: i32,out_buff_ptr: i32,out_buff_len: i32)PayChannel’s keylet from its pieces.permissioned_domain_keylet(account_ptr: i32,account_len: i32,sequence: i32,out_buff_ptr: i32,out_buff_len: i32)PermissionedDomain’s keylet from its pieces.signers_keylet(account_ptr: i32,account_len: i32,out_buff_ptr: i32,out_buff_len: i32)SignerListSet's keylet from its pieces.ticket_keylet(account_ptr: i32,account_len: i32,sequence: i32,out_buff_ptr: i32,out_buff_len: i32)Ticket's keylet from its pieces.vault_keylet(account_ptr: i32,account_len: i32,sequence: i32,out_buff_ptr: i32,out_buff_len: i32)Vault’s keylet from its pieces.The singleton keylets (e.g.
Amendments) are a bit unnecessary to include, as a dev can simply copy the keylet directly instead. They will be included as constants incraftas well.The directory keylets and
NFTokenPagewere not included, since they are a bit more complex to parse through and it seemed unnecessary for now. These can always be added in the future.5.6. NFTs
Fetch information about NFTs.
get_nft(owner_ptr: i32,owner_len: i32,nft_id_ptr: i32,nft_id_len: i32,out_buff_ptr: i32,out_buff_len: i32)get_nft_issuer(nft_id_ptr: i32,nft_id_len: i32,out_buff_ptr: i32,out_buff_len: i32)get_nft_taxon(nft_id_ptr: i32,nft_id_len: i32,out_buff_ptr: i32,out_buff_len: i32)get_nft_flags(nft_id_ptr: i32,nft_id_len: i32)get_nft_transfer_fee(nft_id_ptr: i32,nft_id_len: i32)get_nft_serial(nft_id_ptr: i32,nft_id_len: i32,out_buff_ptr: i32,out_buff_len: i32)5.7. Utils
Miscellaneous utility functions.
check_sig(message_ptr: i32,message_len: i32,signature_ptr: i32,signature_len: i32,pubkey_ptr: i32,pubkey_len: i32,)0for invalid and1for valid. Supports bothED25519andSECP256K1.compute_sha512_half(data_ptr: i32,data_len: i32,out_buff_ptr: i32,out_buff_len: i32)sha512half hash of provided data.5.8. Floats
Helper functions for working with rippled-encoded floats (e.g. IOU amounts).
float_from_int(in_int: i64,out_buf: i32,out_len: i32,rounding_modes: i32)float_from_uint(in_uint_ptr: i32,in_uint_len: i32,out_buf: i32,out_len: i32,rounding_modes: i32)float_set(exponent: i32,mantissa: i64,out_buf: i32,out_len: i32,rounding_modes: i32)float_compare(in_buf1: i32,in_len1: i32,in_buf2: i32,in_len2: i32)float_add(in_buf1: i32,in_len1: i32,in_buf2: i32,in_len2: i32,out_buf: i32,out_len: i32,rounding_modes: i32)float_subtract(in_buf1: i32,in_len1: i32,in_buf2: i32,in_len2: i32,out_buf: i32,out_len: i32,rounding_modes: i32)float_multiply(in_buf1: i32,in_len1: i32,in_buf2: i32,in_len2: i32,out_buf: i32,out_len: i32,rounding_modes: i32)float_divide(in_buf1: i32,in_len1: i32,in_buf2: i32,in_len2: i32,out_buf: i32,out_len: i32,rounding_modes: i32)float_pow(in_buf: i32,in_len: i32,n: i32,out_buf: i32,out_len: i32,rounding_modes: i32)float_root(in_buf: i32,in_len: i32,n: i32,out_buf: i32,out_len: i32,rounding_modes: i32)float_log(in_buf: i32,in_len: i32,out_buf: i32,out_len: i32,rounding_modes: i32)5.9. Trace
Output debug info to the
rippleddebug log.trace(msg_ptr: i32,msg_len: i32,data_ptr: i32,data_len: i32,as_hex: i32)trace_num(msg_ptr: i32,msg_len: i32,number: i64)trace_opaque_float(msg_ptr: i32,msg_len: i32,opaque_float_ptr: i32,opaque_float_len: i32)trace_account(msg_ptr: i32,msg_len: i32,account_ptr: i32,account_len: i32)trace_amount(msg_ptr: i32,msg_len: i32,amount_ptr: i32,amount_len: i32)5.10. Updating Fields
Update on-chain data associated with the WASM code.
This section is the only section of functions that will likely be different for each Smart Feature. Each may have its own way of storing data.
update_data(data_ptr: i32,data_len: i32)Datafield in the ledger object that hosts the WASM code, e.g. a Smart Escrow.6. Security
6.1. Consensus
The WASM VM and spec guarantees that all WASM code will run identically on all machines (though, of course, a lot of testing will be done to ensure that this is the case).
WebAssembly is designed with deterministic execution in mind, and the specification ensures that properly constrained WASM code will produce the same output across all (compliant) runtimes. This XLS relies on those guarantees to ensure that all validators in the XRP Ledger network reach the same result when executing WASM code as part of transaction processing.
To that end:
To ensure that all of this is the case, thorough testing across platforms and architectures will be conducted. Any divergence will be considered a critical consensus-breaking bug.
6.2. Mitigations for Bugs
If there happens to be a bug in the WASM execution layer, the UNL can shut down all usage of WASM code by setting the computation limit to 0.
6.3. Data Security
User-provided WASM code is executed within a strict sandbox. It has no access to system-level resources and can only interact with the XRP Ledger via an explicitly defined host function interface. These host functions enforce strict boundaries on what ledger data is visible and what operations are permitted. For example, there is no way for user-provided WASM code to directly modify a ledger object (to e.g. transfer XRP between accounts without permission).
WASM code cannot directly traverse arbitrary ledger directories or iterate through global ledger state. All access must be via bounded, predefined inputs (e.g., keylets or account IDs passed into the subroutine). This design ensures that malicious WASM code cannot manipulate or exfiltrate ledger state beyond the narrow scope allowed by the host API.
6.4. Resource Limiting
As discussed above, there is a strict gas limit and exceeding it will result in execution being immediately terminated with an exception.
Additionally, memory and stack usage are tightly constrained - the linear memory size is bounded to a fixed number of pages, and stack depth is capped to prevent runaway recursion or stack overflows.
These constraints prevent denial-of-service attacks and ensure that WASM execution remains fast and predictable, without any WASM-related transaction taking more than its share of
rippledresources.6.5. Future-Proofing
All future changes to this spec (even just a simple change to the gas cost of a host function) will need to be gated by an amendment. Updates to the
wamrpackage may also need to be gated by an amendment - every update will need to be tested for the potential of breaking changes.For example, this is what it might look like to add a new host function:
(in
WasmVM.cpp)This ensures that smart escrows cannot use the
loan_keylethost function at all before theLendingProtocolamendment is activated, as the amendment proce`ss ensures that all nodes and validators have the code before it is run.Appendix
Appendix A: Other WASM VMs Considered
A.1. The Different WASM Compilation Modes
A.1.1. AOT (Ahead of Time)
AOT compiles WASM straight to native machine code ahead of time. If we were to use this compilation mode, users would have to store native machine code on the ledger.
This isn’t useful for our needs, as the compiled AOT code will be architecture-specific.
Can we figure out a way to support AOT? Possibly, but likely not without restricting rippled hardware to certain CPU architectures, and even then likely only one. Alternatively, to support AOT we would need to force WASM developers to supply variants that can run on any native architectures supported by rippled. Even if we imagine limiting rippled to 3 architectures, that would mean every smart contract developer would need to supply 3 different versions of their WASM, which would be wasteful from a space perspective. Last but not least, limiting rippled to 3 architectures seems counterproductive to decentralization.
A.1.2. JIT (Just-In-Time)
JIT compiles essentially as the code is run, or just before. This allows for additional caching and optimizations.
However, there are a few issues with JIT. From this Stellar blog, JIT-based VMs are also not as secure and are susceptible to “JIT Bombs.” It has longer start times and greater memory usage. Mac also does not support JIT. The use of JIT may also result in different gas costs on different machines depending on what is in the cache, which would be a consensus-breaking change. Therefore, JIT cannot be used for our needs.
A.1.3. Interpreted
Interpreted mode just runs the code, like a REPL.
We decided on this mode because, well, the other two don't work for our needs, even though they're often more performant.
A.2. The Different WASM Implementations
The 5 WASM VM implementations we investigated were:
A.2.1. Initial Investigation
Based on these findings, we narrowed down the search to WasmEdge and WAMR, which we then did further performance testing and analysis on.
A.2.2. Performance Analysis
These graphs clearly show that WAMR is much more performant.
Appendix B: Other Memory Management Strategies Considered
Options:
We decided on Option 1 for the purpose of simplicity.
Appendix C: FAQ
C.1: How does this list of host functions compare to the Xahau Hooks host functions?
The host functions on this list are heavily inspired by the Hooks host functions. Most of the changes are just naming and simplifying the functions, and reworking how they're organized.
C.2: Can we add a host function for [insert request here]?
Please share any request you have in the comments of this spec.
Some limitations:
C.3: Will gas fees be refundable if I pay for too much gas, like EVM?
Not at this time. This may be revisited later, and can be added in a future amendment.
Not all smart contract chains support refundable gas - for example, Solana does not.
C.4: Will transactions that use the WASM VM be testable via
simulate?Yes, though that needs to be tested. This should make it easier for users to estimate gas usage.
Beta Was this translation helpful? Give feedback.
All reactions