Introduction
Note: This document is a work in progress. Its structure is not yet finalized, and significant changes may still be made to the content.
Fluent is the first blended execution network - an Ethereum L2 and framework that blends Wasm, EVM and (soon) SVM-based smart contracts into a unified execution environment.
Smart contracts from different VM targets can directly call each other on Fluent. Fluent is in public devnet and currently supports apps composed of Solidity, Vyper, and Rust contracts.
Glossary
IR (Intermediary Representation) — An intermediary representation of some bytecode, that is usually used to represent and optimized and execution-ready application state.
ZK (Zero Knowledge) — A cryptographic method where one party (the prover) can prove to another party (the verifier) that they know a value without conveying any information apart from the fact that they know the value.
STF (State Transition Function) — A function used to ensure the correctness of a system's state transition.
ISA (Instruction Set Architecture) — The part of the computer architecture related to programming, which includes the instruction set, word size, memory address modes, processor registers, and address and data formats.
AOT (Ahead Of Time) — A type of compilation that converts high-level code to a lower-level code or machine code before execution.
JIT (Just In Time) — A type of compilation that converts high-level code to intermediate or machine code at runtime, typically to enhance performance during execution.
EVM (Ethereum Virtual Machine) — The runtime environment for smart contracts in Ethereum, allowing code to be executed exactly as intended.
DA (Data Availability) — Refers to the accessibility and correctness of the data required to reconstruct the state of a blockchain.
Our Vision
While developing a zkVM for an EVM-compatible, Wasm based rollup on Ethereum, Fluent explored methods to optimize the proving process of smart contracts. A major challenge lies in the fact that using a zkVM to support multiple VMs only allows for the proof of the root STF, while the remaining nested execution of other VMs requires emulation.
To address this issue, Fluent proposes a VM capable of performing nested execution without incurring additional emulation overhead. This is achieved through a hardware acceleration process similar to the translation of smart contracts, applications, and precompiled contracts into a specialized low-level intermediary representation (IR) binary structure known as rWasm. This structure can be proven much more efficient compared to running emulator software.
With its unique interruption system, rWasm emerged as an innovative solution for handling nested calls and compiling diverse smart contracts, revolutionizing the landscape of efficient zk-Wasm based applications on Ethereum.
Meet Blended Execution
With blended execution on Fluent, developers can create applications using languages and tools from various VMs yet coexisting in the same execution space. This is possible because all smart contracts share the same account space. As a result, native composability is achieved for anything within Fluent that can be represented as rWasm (which relies on Wasm and LLVM, making 17+ languages available for development).
Different VMs can be supported using an AOT translation process (directly into Rwasm) or an emulation process. Fluent can enable execution environments like the EVM, bringing Solidity and Vyper support. Similarly, the new language Sway from Fuel can be integrated by running an FVM execution precompile.
Both users and developers obtain major benefits from blended execution because it enhances the experience of developers who are ready to expand the Fluent ecosystem by bringing more development languages and execution environments on board. Additionally, it bridges the gap between Web2 and Web3 users by providing a shared set of tools and new ways to interact with applications through account abstraction backed by EIP-7702 and modules such as OAuth 2.0 and WebAuthn.
Product Vision
The Fluent Blended Execution Layer will revolutionize the way developers create, deploy, and prove smart contracts by unifying multiple VMs into a single, highly efficient execution environment. At the heart of this vision is rWasm, a unique IR language designed to bridge the gap between various VMs and EEs such as the EVM, SVM, and Wasm. By eliminating the emulation overhead typically associated with nested execution, Fluent enables faster, more secure, and more scalable smart contract execution.
Key features:
- Unified Execution Environment: Support for multiple VMs and EEs through a single execution environment, enabling native composability between different smart contracts, tools, and languages.
- rWasm Integration: Full compatibility with standard Wasm and support for over 17+ traditional programming languages, allowing developers to use familiar tools such as Rust, C, Solidity, TinyGo, and AssemblyScript without sacrificing performance.
- Optimized Proving Infrastructure: Efficient proving through rWasm that drastically reduces overhead and complexity, providing optimal performance for ZK circuits.
- Seamless Expansion: Advanced JIT/AOT compilers to integrate new VMs and EEs, ensuring extensibility for future-proofing and scalability.
- Security and Extensibility: BlendedVM unifies the execution space, providing enhanced security and preventing vulnerabilities that arise from managing multiple VMs while enabling new VMs to be added.
The Fluent Blended Execution Layer empowers developers to build diverse applications without managing disparate systems, optimizing their experience and fostering rapid growth in the Ethereum ecosystem.
Technical Vision
Extracting traces is crucial for proving. It involves obtaining snapshots of stack and memory operations to feed into zk circuits. Aligning the IR language with the trace structure and ensuring its compatibility with zk is essential to minimize circuit size, ultimately improving proving speed and reducing code complexity. rWasm, as an IR binary language, combines execution concepts and maps its execution trace to the zkVM circuit structure. This approach aims to achieve optimal performance, minimizing both proving and execution overhead.
Blended execution natively supports multiple VMs and EEs within a single execution environment, and within Fluent, is enabled by employing a single IR known as rWasm, which serves as its primary VM. This IR enables the verification of all state transitions occurring within the system, encompassing various VMs and EEs. In the case of the Fluent L2, the EVM, SVM, and Wasm are supported. As a result, developers familiar with any of these primitives can leverage circuits designed for rWasm and effortlessly obtain the necessary optimal proving infrastructure for their applications. In essence, blended execution acts as a state verification function responsible for representing every operation within the Fluent execution layer.
Given that rWasm serves as the IR language for Fluent, it has the potential to represent not only Wasm or EVM but also other VMs and EEs. This is facilitated by providing dedicated compilation or emulation software for these platforms or in some cases utilizing emulation software.
rWasm is a derivative of the Wasm assembly language that keeps 100% backward compatibility with original Wasm standards. Leveraging the extensive adoption and support of Wasm, rWasm enables blockchain developers to effortlessly create new applications in traditional languages like Rust and C. Fluent's advanced AOT compilers handle all IR compilation tasks, simplifying the development and deployment process of Wasm-based blockchain applications in the Ethereum ecosystem.
Blended Execution
Blended Execution - is an approach that aims to increase the efficiency of executing smart contracts from diverse VMs by utilizing a unified IR known as rWasm. rWasm facilitates the expansion of system functionality by leveraging native composability across different execution environments. This innovative approach enables seamless execution of smart contracts, optimizing performance and enhancing the overall capabilities of the system.
This system allows for the verification of all state transitions within a unified execution environment, thereby minimizing emulation overhead typically associated with nested execution in various VMs. By employing rWasm, which retains full compatibility with standard Wasm, developers can seamlessly deploy and integrate smart contracts across multiple programming languages and execution environments. Fluent features native, real-time composability, as the different applications the network supports share the same execution space. The use of compilation software allows for efficient integration of various VMs, such as the EVM and SVM, while maintaining optimal proving infrastructure.
Ultimately, blended execution serves as a state verification function, streamlining the development process and broadening the capabilities of builders on Ethereum the Fluent ecosystem without affecting developer experience.
BlendedVM vs MultiVM
MultiVM is an execution layer that provides multiple VMs for running user applications. It offers developers the opportunity to create new types of applications and combine different programming languages in a single platform. However, MultiVM requires developers to manage these VMs within the execution layer. Most of these VMs use varying ISAs, which impact execution costs and supported functionality. Developers must carefully handle these parameters to avoid miscalculations or vulnerabilities that could lead to malicious state trie modifications.
BlendedVM takes a similar approach to MultiVM, but instead of having multiple VMs, it uses a single VM to represent all VM operations. This is achieved by employing compilation and emulation software to translate user applications into native instructions for BlendedVM. This approach enhances security by preventing malicious state access outside the single VM, even in the event of a vulnerability in the AOT compiler. Furthermore, BlendedVM offers system extensibility, allowing developers to incorporate support for additional VMs or EEs in a trusted or trustless manner.
Architecture
Fluent is an Ethereum Layer L2 rollup designed to natively execute EVM, SVM and Wasm-based programs. Fluent exists as a unified state machine, where all contracts can call each other, regardless of which VM they were originally built for.
As a rollup, Fluent supports scalable and efficient execution by committing state changes to Ethereum L1. This process involves compressing the state changes using ZK proofs, specifically SNARKs.
The base architecture of Fluent
The Fluent operates on a modified version of Reth, using its own execution engine that replaces Revm. It maintains backward compatibility with most existing Ethereum standards, such as transaction and block structures. However, Fluent is not confined to Reth exclusively, as it features an independent execution runtime.
Furthermore, Fluent enables a fork-less runtime upgrade model by incorporating the most critical and upgradable runtime execution codebase within the genesis state. The only persistent element within the runtime is the transaction format.
Additionally, Fluent is always post-Prague compatible and does not support any EIPs implemented before the Prague fork. Maintaining backward compatibility with all previous forks is unnecessary. The EVM runtime can be upgraded to retain compatibility with EVM.
Execution Environment
The Fluent EE is designed to be universal, supporting various VMs and EEs. This universality is achieved through the rWasm VM, which executes and simulates different EEs. By using rWasm, Fluent translates all applications into a single execution language, enabling different EEs to share the same state trie. This shared state trie facilitates seamless interoperability among applications.
Given that each EE/VM can introduce unique execution standards, various integration challenges may arise. Let's explore the most significant ones.
-
Incompatible Cryptography: EEs can consume an entire transaction as an input, requiring the use of system bindings to verify signature correctness. Fluent supports pre-compiled contracts for a range of commonly used cryptographic functions (e.g., secp256k1, ed25519, bn254, bls384). Any missing cryptographic functions can be implemented as custom precompiled contracts.
-
Different Address Format: Fluent uses an Ethereum-compatible 20-byte address format. If an address uses a different derivation format or has more bytes than a contract can store, there needs to be a way to handle this. Such addresses can be stored as so-called projected addresses, which are calculated using any hashing function.
-
Varying Gas Calculation Policies: EEs/VMs may use different gas calculation policies compared to Fluent. To address this, Fluent has two distinct gas calculation policies:
- Standard Gas Calculation: Applies to typical operations, assigning a specific gas cost to each rWasm opcode.
- Manual Gas Calculation: Available exclusively for genesis precompiled smart contracts on Fluent.
This approach ensures flexibility and maintains the integrity of operations across different environments.
-
Custom Bytecode Storage: There is a need to store immutable data like custom bytecode or some EE-specific context information. Fluent provides special metadata accounts that can be used to store immutable data, including custom bytecode or other data.
-
Variable Storage Size: Some EEs implement variable storage key/value lengths. For instance, Solana uses 10kB chunks, while Cosmos utilizes variable key/value lengths with certain constraints. Storing these datasets within Ethereum's standard 32-byte storage format may lead to significant performance issues and increased gas consumption. To address this, Fluent offers a metadata API that allows developers to use custom storage solutions for varying data lengths efficiently.
Account Ownership
Fluent extends the original EVM account structure to support account ownership. Account ownership is a special modification of an EIP-7702 account. Instead of delegating an account to a contract, you assign it an owner.
At first glance the idea looks similar, but it enables managing metadata storage directly within the account. This metadata can store arbitrary data as linear storage and is used by both the EVM and SVM runtimes to keep track of information such as bytecode, code hashes, and other account metadata.
Ownable accounts begin with a special 0xEF44
prefix and follow the structure below:
#![allow(unused)] fn main() { /// Ownable account bytecode representation /// /// Format: /// `0xEF44` (MAGIC) + `0x00` (VERSION) + 20 bytes of owner address + metadata. pub struct OwnableAccountBytecode { /// The owner of this account. pub owner_address: Address, /// Account version. pub version: u8, /// Extra bytes stored by the runtime. pub metadata: Bytes, } }
The concept is simple but unlocks powerful new features for EVM extensibility.
Account Delegation
This mechanism is similar to EIP-7702, but with one key difference: ownership cannot be revoked by the runtime, since the metadata structure is strictly bound to runtime logic.
Once a smart contract is deployed, its ownership is permanently assigned—every contract has an owner.
The runtime resolution logic is straightforward:
#![allow(unused)] fn main() { pub fn resolve_precompiled_runtime_from_input(input: &[u8]) -> Address { if input.len() > WASM_MAGIC_BYTES.len() && input[..WASM_MAGIC_BYTES.len()] == WASM_MAGIC_BYTES { PRECOMPILE_WASM_RUNTIME } else if input.len() > SVM_ELF_MAGIC_BYTES.len() && input[..SVM_ELF_MAGIC_BYTES.len()] == SVM_ELF_MAGIC_BYTES { PRECOMPILE_SVM_RUNTIME } else if input.len() > ERC20_MAGIC_BYTES.len() && input[..ERC20_MAGIC_BYTES.len()] == ERC20_MAGIC_BYTES { PRECOMPILE_ERC20_RUNTIME } else { PRECOMPILE_EVM_RUNTIME } } }
Currently, Fluent supports the following runtime formats:
- WASM — for compiling Wasm into rWasm.
- SVM — rPBF (ELF) binaries for running Solana applications.
- ERC20 — a specialized runtime for fast ERC20 token transfers.
- EVM — for running EVM applications.
Account Derivation
Ownable accounts can also derive new accounts within the same runtime.
This process is similar to CREATE2
, but it differs in two key ways:
- It does not use bytecode as input (the runtime is fixed).
- It allows runtimes themselves to spawn new accounts.
This mechanism resembles Program Derived Addresses (PDA) in Solana, where subaccounts can be deterministically created.
To avoid collisions with the CREATE2
scheme, Fluent uses a custom hashing function for deriving ownable accounts:
0x44 || account_owner || salt
Composability
Blended EEs within Fluent operating as execution proxies. Since Fluent only supports rWasm bytecode, then every operation inside Fluent must be represented using rWasm ISA.
Architecture design of Blended EE
There are two main options for this approach:
-
Translation: A translation from one binary format into rWasm. This concept is used for Wasm application deployment where runtime is a Wasm → rWasm compiler.
-
Runtime: An precompiled execution runtime developed using Wasm and compiled into machine code. This concept is used by EVM, SVM, ERC20 runtimes.
For example, Fluent presently incorporates the EVM/SVM using the account ownership method (aka runtime proxy). A proxy with a delegate call forwards execution to a unique EVM/SVM loader smart contract. This setup eliminates the need for address mapping or transaction verification. ABI encoding/decoding format can be used, and contracts can be managed using default EVM/SVM-compatible data structures, such as storage, block/transaction structures.
To achieve native composability on Fluent, an EE must coexist within the same trie space as the default EVM+Wasm EEs. It should adhere to established EVM standards, including address formatting and derivation strategies (for example, CREATE/CREATE2 for smart contract deployment). The EE must not execute arbitrary account or state modifications and can only manage basic Ethereum-compatible accounts.
As a result, apps built with the proxy model can natively interoperate with other native-compatible EEs. Since they share the same address space, isolation isn't required. Consequently, Wasm apps can directly interact with EVM apps and vice versa.
State Access
Fluent operates with state tries through a pure functional approach, where every smart contract and root-STF can be represented as a function. In this model, the input provides a list of dependencies, and the output yields an execution result along with a number of logs.
However, this isn't entirely feasible due to cold storage reads and external storage dependencies, such as CODEHASH-like EVM opcodes. To address this, Fluent employs an interruption system to "request" missing information from the root-STF. This is particularly useful for operations involving cold storage or invalidated warm storage.
Interruption System
Fluent interoperability relies heavily on its interruption system. Smart contracts on Fluent are limited to pure functions, preventing system calls from accessing external resources such as bytecodes, cold or invalidated storage slots, or performing nested calls. Including system calls imposes additional proving overhead, as proving gadgets must be developed for each call, complicating system development. This also impacts the flexibility in managing rights or extending contracts, making the system less sustainable for the fork-less concept. In such a scenario, system contracts cannot be upgraded without updating the circuits.
Fluent solves this problem by enabling an interruption system that helps manage context switching between nested apps and the so-called STF that stands for context management.
For simplicity, let's assume that STF, smart contracts and EEs are all functions (since they are essentially state transition functions or a part of STF). Functions can be categorized as either root or non-root. A root function is defined as a function where the depth level is equal to 0. The root function is pivotal because it handles all context switching and cross-contract accesses, serving as a security layer.
The root function has the ultimate authority over all state transitions within the blockchain. Additionally, the root function is in charge of managing and executing system calls. The root function cannot be interrupted, but it is capable of handling interruptions.
System Bindings
The system bindings manage context switching in a Fluent interruption system. Functions signatures are provided below. These functions maintain the entire flow of an interruption system.
#![allow(unused)] fn main() { /// Low-level function that terminates the execution of the program and exits with the specified /// exit code. /// /// This function is typically used to perform an immediate and final exit of a program, /// bypassing Rust's standard teardown mechanisms. /// It effectively stops execution and prevents further operations, including cleanup or /// unwinding. /// /// # Parameters /// - `code` (i32): The non-positive exit code indicating the reason for termination. /// /// # Notes /// - This function is generally invoked in specialized environments, such as WebAssembly /// runtimes, or through higher-level abstractions. /// - Consider alternatives in standard applications, such as returning control to the caller or /// using Rust's standard exit mechanisms, for safer options. pub fn _exit(code: i32) -> !; /// Executes a nested call with specified bytecode poseidon hash. /// /// # Parameters /// - `hash32_ptr`: A pointer to a 254-bit poseidon hash of a contract to be called. /// - `input_ptr`: A pointer to the input data (const u8). /// - `input_len`: The length of the input data (u32). /// - `fuel16_ptr`: A 16 byte array of elements where [fuel_limit/fuel_used, fuel_refunded] /// - `state`: A state value (u32), used internally to maintain function state. /// /// Fuel ptr can be set to zero if you want to delegate all remaining gas. /// In this case sender won't get the consumed gas result. /// /// # Returns /// - An `i32` value indicating the result of the execution, negative or zero result stands for /// terminated execution, but positive code stands for interrupted execution (works only for /// root execution level) pub fn _exec( hash32_ptr: *const u8, input_ptr: *const u8, input_len: u32, fuel16_ptr: *mut [i64; 2], state: u32, ) -> i32; /// Resumes the execution of a previously suspended function call. /// /// This function is designed to handle the resumption of a function call /// that was previously paused. /// It takes several parameters that provide /// the necessary context and data for resuming the call. /// /// # Parameters /// /// * `call_id` - A unique identifier for the call that needs to be resumed. /// * `return_data_ptr` - A pointer to the return data that needs to be passed back to the resuming function. This should point to a byte array. /// * `return_data_len` - The length of the return data in bytes. /// * `exit_code` - An integer code that represents the exit status of the resuming function. Typically, this might be 0 for success or an error code for failure. /// * `fuel_limit` - A fuel used representing the fuel need to be charged, also it puts a consumed fuel result into the same pointer pub fn _resume( call_id: u32, return_data_ptr: *const u8, return_data_len: u32, exit_code: i32, fuel16_ptr: *mut [i64; 2], ) -> i32; }
During execution, once shared resources are requested, it pauses the execution and forwards params into the STF. Once the interruption is over, it resumes the previous frame.
Here is the basic flow of an interruption:
Application Exit
The application exit binding terminates function execution with the specified exit code. The function is designed to exit from any smart contract or application. It immediately halts the contract execution and forwards all execution results, including the exit code and return data, to the caller contract.
Constraints:
The exit code must always be a negative 32-bit integer.
Supplying a positive exit code will result in a NonNegativeExitCode
execution error.
Positive exit codes indicate interrupted execution and are exclusive to the _exec
or _resume
functions.
Exec & Resume
During execution, once shared resources are requested, it pauses the execution and forwards params into the STF.
Interrupted params contain the following items:
hash32_ptr
: A pointer to a 32-byte code hash of a contract to be called (bytecode hash for STF or syscall code hash).input
: An input parameter for smart contract or input for interruption.fuel_ptr
: A mutable pointer to a fuel value. The consumed and refunded fuel is stored in the same pointer after execution.state
: A state value (u32), used internally to maintain function state (main or deploy).
This binding executes a nested call or sends an interruption to the parent execution call though context switching. If the depth level is greater than 0 (non STF), then an interruption occurs; otherwise, bytecode is executed.
Once the interruption is resolved, it resumes the execution of a previously suspended function call by specifying return data, resulting exit code and fuel consumed.
The resume function operates similarly to exec, but it requires an interrupted call ID and the interruption result (including return data and exit code). Interruption events may also occur during the resume process, requiring an execution loop capable of handling and correctly processing these interruptions.
System Calls
System calls use the same approach as an interruption system. Since the root-STF function is responsible for all state transitions, including cold/warm storage reads, then a syscall can be represented as an interruption to the ephemeral smart contract.
For accessing state data, Fluent uses special ephemeral smart contracts to access information located outside a smart contract. For example, in case of storage cache invalidation, the contract must request the newest info from the root call instead of reading invalidated cache. Also, nested calls to other contracts require ACL checks that must be checked and verified by the root-STF.
Here is an example of what the system call looks like for Rust contracts.
#![allow(unused)] fn main() { fn syscall_storage_read<SDK: NativeAPI>(native_sdk: &mut SDK, slot: &U256) -> U256 { // do a call to the root-STF to request some storage slot let (_, exit_code) = native_sdk.exec( &SYSCALL_ID_STORAGE_READ, // an unique storage read code hash slot.as_le_slice(), // a requesting slice with data (aka call-input) GAS_LIMIT_SYSCALL_STORAGE_READ, // a gas limit for this call (max threshold) STATE_MAIN, // state of the call (must always be 0, except some special tricky cases) ); // make sure returning result is zero (Ok) assert_eq!(exit_code, 0); // read output from the return data (storage slot value is always 32 bytes) let mut output: [u8; 32] = [0u8; 32]; native_sdk.read_output(&mut output, 0); // convert return data to the U256 value U256::from_le_bytes(output) } }
For example, if smart contract A needs to send a message to smart contract B, it can trigger a special system call interruption. Upon interruption, the STF processes the interruption, performs ACL checks, executes the target application, and then resumes the previous context with the appropriate exit code and return data.
Blended VM
Blended VM implements Blended Execution concepts inside Fluent. It works on the top of rWasm VM, the runtime system bindings and interruption system. rWasm VM represents all state transitions within the VM, including Wasm instructions. The interruption system efficiently manages interruptions during system calls or cross-contract calls.
rWASM
rWasm (reduced WebAssembly) is an EIP-3540 compatible binary IR of Wasm. It is designed to simplify the execution process of Wasm binaries while maintaining 99% compatibility with original Wasm features.
rWasm is a specially modified binary IR of Wasm execution. It retains 99% compatibility with the original Wasm bytecode and instruction set but features a modified binary structure that avoids the pitfalls of non-zk friendly elements, without altering opcode behavior.
The main issue with Wasm is its use of relative offsets for type mappings, function mappings, and block/loop statements, which complicates the proving process. rWasm addresses this by adopting a more flattened binary structure without relative offsets and eliminating the need for a type mapping validator, allowing for straightforward execution.
The flattened structure of rWasm simplifies the process of proving the correctness of each opcode execution and places several verification steps in the hands of the developer. This modification makes rWasm a more efficient and zk-friendly option for integrating Web2 developers into the Web3 ecosystem.
Technology
Key Differences between rWasm and Wasm:
- Deterministic Function Order: Functions are ordered based on their position in the codebase.
- Block/Loop Replacement: Blocks and loops are replaced with Br-family instructions.
- Redesigned Break Instructions: Break instructions now support program counter (PC) offsets instead of depth-level.
- Simplified Binary Verification: Most sections are removed to streamline binary verification.
- Unified Memory Segment Section: Implements all Wasm memory standards in one place.
- Removed Global Variables Section: A global variables section is eliminated.
- Eliminated Type Mapping: Type mapping is no longer necessary as the code is fully validated.
- Special Entrypoint Function: A unique entry point function encompasses all segments.
The new binary representation ensures a fully equivalently compatible Wasm runtime module from the binary. Some features are no longer supported by the rWasm runtime: module imports; global variables; memory imports; global variables export. These features are unnecessary as Fluent does not utilize them.
Structure
The rWasm binary format supports the following sections:
- Bytecode Section: Replaces the function/code/entrypoint sections.
- Memory Section: Replaces memory/data sections for all active/passive/declare section types.
The following sections, currently implemented, are scheduled for removal:
- Function Section: This section determines the size of each function and is used to properly allocate functions in
memory. Once the
CallInternal
opcode is eliminated, this section can be dropped as well. - Element Section: This section defines allocations for tables used in indirect calls. The
CallIndirect
instruction is being phased out, and once this process is complete, the section will also be removed.
Bytecode Section
This section consolidates Wasm's original function, code, and start sections. It contains all instructions for the entire binary without any additional separators for functions. Functions are recovered from the bytecode by reading the function section, which contains function lengths. The entrypoint function is injected at the end, which is used to initialize all segments according to Wasm constraints.
Note: The function section is planned to be removed and entrypoint stored at offset 0. To achieve this, eliminating stack calls must be achieved while implementing indirect breaks. Although Fluent has an implementation for this, it is not yet satisfactory, and a migration is planned to a register-based IR before finalizing it.
Memory & Data Section
In Wasm, memory and data sections are handled separately. In rWasm, the Memory section defines memory bounds (lower and upper limits), and data sections, which can be either active or passive, and specify data to be mapped inside memory. Unlike Wasm, rWasm eliminates the separate memory section, modifies the corresponding instruction logic, and merges all data sections.
Here's an example of a WAT file that initializes memory with minimum and maximum memory bounds (default allocated memory is one page, and the maximum possible allocated pages are two):
(module
(memory 1 2)
)
To support this, the memory.grow
instruction is injected into the entrypoint to initialize the default memory.
A special preamble is also added to all memory.grow
instructions to perform upper bound checks.
Here is an example of the resulting entrypoint injection:
(module
(func $__entrypoint
i32.const $_init_pages
memory.init
drop)
)
According to Wasm standards, a memory overflow causes u32::MAX
to be placed on the stack.
For upper-bound checks, the memory.size
opcode can be used. Here is an example of such an injection:
(module
(func $_func_uses_memory_grow
(block
local.get 1
memory.size
i32.add
i32.const $_max_pages
i32.gts
drop
i32.const 4294967295
br 0
memory.grow)
)
)
These injections fully comply with Wasm standards, allowing Fluent to support official Wasm memory constraint checks for the memory section.
For the data section, the process is more complex because Fluent needs to support three different data section types:
- Active: Has a pre-defined compile-time offset.
- Passive: Can be initialized dynamically at runtime.
To address this, all sections are merged. If the memory is active, it is initialized inside the entrypoint with
re-mapped offsets. Otherwise, the offset is remembered in a special mapping to adjust passive segments when the user
calls memory.init
manually.
Here is an example of an entrypoint injection for an active data segment:
(module
(func $__entrypoint
i32.const $_relative_offset
i64.const $_data_offset
i64.const $_data_length // or u64::MAX in case of overflow
memory.init 0
data.drop $segment_index+1
)
)
The data segment must be dropped finally. According to Wasm standards, once the segment is initialized, it must be entirely removed from memory. To simulate this behavior, Fluent uses zero segments as a default and stores special data segment flags to know which segments are still active.
For passive data segments, the logic is similar, but data segment offsets must be recalculated on the fly.
(module
(func $_func_uses_memory_init
// adjust length
(block
local.get 1
local.get 3
i32.add
i32.const $_data_len
i32.gts
br_if_eqz 0
i32.const 4294967295 // an error
local.set 1
)
// adjust offset
i32.const $_data_offset
local.get 3
i32.add
local.set 2
// do init
memory.init $_segment_index+1
)
)
The provided injections are examples and may vary based on specific requirements.
EVM
Fluent integrates with the EVM by leveraging special EVM precompiled contracts. These contracts facilitate the execution of EVM bytecode, enabling the deployment and operation of smart contracts designed for the EVM ecosystem. This allows developers to seamlessly deploy their applications built for EVM platforms using languages like Solidity or Viper.
The EVM executor, a Rust-based smart contract, provides two key functions:
deploy
: Deploys the EVM application and stores the bytecode state.main
: Executes the already deployed EVM application.
During deployment, a specialized rWasm proxy is deployed under the smart contract address. This proxy redirects all deployment and execution calls to the EVM executor.
The deployment process is identical to that of Ethereum and other Ethereum-compatible platforms. Additionally, there are no differences in calling conventions or contract interactions. This consistency ensures a smooth app migration process for developers.
WebAssembly
Fluent offers near-native support for Wasm, with the primary distinction being that, during deployment, it's compiled into rWasm. A Wasm application can use the same system calls as EVM applications without any restrictions.
During the deployment process, Fluent enhances the rWasm codebase with additional checks for gas measurement and modifies certain instructions or segment structures when necessary. For more details, refer to the rWasm section.
Solana Integration
Solana natively supports composability with both EVM and Wasm applications. This is made possible because Fluent addresses Solana applications by mapping them into Fluent's account space. To achieve SVM support, a special rPBF executor is employed, which defines the execution of Solana binaries and specifies a list of mapped system bindings and calls. Native support for rPBF bytecode is achieved by mapping each operation into the Fluent EE space.
Address Format
Solana uses a 32-byte address format, while Fluent operates with a 20-byte format. Instead of storing a mapping from the 32-byte to the 20-byte format, Fluent employs a special address convention to convert addresses between formats. A unique magic prefix is attached to Solana addresses to make them convertible into the same binary representation within the 20-byte Fluent account trie.
The first 12 bytes of the address are used to route transfers and calls between SVM and EVM+Wasm accounts. This routing is necessary to achieve full EE compatibility and perform additional containment checks when required. For example, while a simple transfer without invoking callee bytecode is not allowed in EVM, it is possible in SVM. Therefore, differentiating between SVM and EVM accounts is essential.
Transaction Type
Fluent does not support Solana transactions directly; instead, EIP-1559 transactions must be used. During deployment, Fluent automatically detects Solana applications (EFL+rPBF) and specifies a special proxy that refers to the SVM executor system's precompiled contract.
Currently, Fluent does not support an additional transaction type for Solana transactions. However, this feature could be added in the future if there is enough demand.
PDA (Program-Derivable Address)
Program Derived Addresses (PDA) are a core feature of Solana, allowing for the management of nested contracts with
dedicated storage. Fluent leverages various derivation schemes for Solana programs, ensuring that the migration process
remains seamless for developers. The CREATE2
derivation scheme is used to replicate the PDA functionality.
To maintain an identical storage layout, Fluent offers supplementary storage interfaces. These interfaces enable Solana applications to efficiently manage data chunks without incurring extra overhead.
Accounts
Fluent employs account projection, also known as EE simulation, to integrate Solana accounts into the unified Fluent account space. This approach ensures that all EVM, Wasm, and SVM accounts are treated uniformly, leveraging the same rWasm VM. Consequently, this enables seamless interoperability and balance transfers between various account types.
System Builtins
A collection of system functions provides access to low-level operations, serving both our VM runtime and our circuit definitions. Each system function is replaced with a specialized ZK-gadget to speed up the proving process. These functions can include hashing algorithms, I/O operations, and nested call functions.
WARNING: The system functions API/ABI are still under development and may change in the future.
#![allow(unused)] fn main() { #[link(wasm_import_module = "fluentbase_v1preview")] extern "C" { /// Functions that provide access to crypto elements, right now we support following: /// - Keccak256 /// - Poseidon (two modes, message hash and two elements hash) /// - Ecrecover pub fn _keccak256(data_offset: *const u8, data_len: u32, output32_offset: *mut u8); pub fn _poseidon(data_offset: *const u8, data_len: u32, output32_offset: *mut u8); pub fn _poseidon_hash( fa32_offset: *const u8, fb32_offset: *const u8, fd32_offset: *const u8, output32_offset: *mut u8, ); pub fn _ecrecover( digest32_offset: *const u8, sig64_offset: *const u8, output65_offset: *mut u8, rec_id: u32, ); /// Basic system methods that are available for every app (shared and sovereign) pub fn _exit(code: i32) -> !; pub fn _write(offset: *const u8, length: u32); pub fn _input_size() -> u32; pub fn _read(target: *mut u8, offset: u32, length: u32); pub fn _output_size() -> u32; pub fn _read_output(target: *mut u8, offset: u32, length: u32); pub fn _forward_output(offset: u32, len: u32); pub fn _state() -> u32; pub fn _read_context(target_ptr: *mut u8, offset: u32, length: u32); /// Executes a nested call with specified bytecode poseidon hash. /// /// # Parameters /// - `hash32_ptr`: A pointer to a 254-bit poseidon hash of a contract to be called. /// - `input_ptr`: A pointer to the input data (const u8). /// - `input_len`: The length of the input data (u32). /// - `fuel_ptr`: A mutable pointer to a fuel value (u64), consumed fuel is stored in the same /// pointer after execution. /// - `state`: A state value (u32), used internally to maintain function state. /// /// Fuel ptr can be set to zero if you want to delegate all remaining gas. /// In this case sender won't get consumed gas result. /// /// # Returns /// - An `i32` value indicating the result of the execution, /// negative or zero result stands for terminated execution, /// but positive code stands for interrupted execution (works only for root execution level) pub fn _exec( hash32_ptr: *const u8, input_ptr: *const u8, input_len: u32, fuel_ptr: *mut u64, state: u32, ) -> i32; /// Resumes the execution of a previously suspended function call. /// /// This function is designed to handle the resumption of a function call /// that was previously paused. /// It takes several parameters that provide /// the necessary context and data for resuming the call. /// /// # Parameters /// /// * `call_id` - A unique identifier for the call that needs to be resumed. /// * `return_data_ptr` - A pointer to the return data that needs to be passed back to the /// resuming function. /// This should point to a byte array. /// * `return_data_len` - The length of the return data in bytes. /// * `exit_code` - An integer code that represents the exit status of the resuming function. /// Typically, this might be 0 for success or an error code for failure. /// * `fuel_ptr` - A mutable pointer to a 64-bit unsigned integer representing the fuel need to /// be charged, also it puts a consumed fuel result into the same pointer pub fn _resume( call_id: u32, return_data_ptr: *const u8, return_data_len: u32, exit_code: i32, fuel_ptr: *mut u64, ) -> i32; pub fn _charge_fuel(delta: u64) -> u64; pub fn _fuel() -> u64; /// Journaled ZK Trie methods to work with blockchain state pub fn _preimage_size(hash32_ptr: *const u8) -> u32; pub fn _preimage_copy(hash32_ptr: *const u8, preimage_ptr: *mut u8); pub fn _debug_log(msg_ptr: *const u8, msg_len: u32); } }
For each system function, a unique identifier is assigned.
During the rWASM translation, every function call is replaced with a Call(SysCallIdx)
instruction.
This approach significantly enhances the efficiency and simplicity of the proving process.
#![allow(unused)] fn main() { #[repr(u32)] #[allow(non_camel_case_types)] pub enum SysFuncIdx { #[default] UNKNOWN = 0x0000, // crypto KECCAK256 = 0x0101, POSEIDON = 0x0102, POSEIDON_HASH = 0x0103, ECRECOVER = 0x0104, // SYS host EXIT = 0x0001, STATE = 0x0002, READ = 0x0003, INPUT_SIZE = 0x0004, WRITE = 0x0005, OUTPUT_SIZE = 0x0006, READ_OUTPUT = 0x0007, EXEC = 0x0009, RESUME = 0x000a, FORWARD_OUTPUT = 0x000b, CHARGE_FUEL = 0x000c, FUEL = 0x000d, READ_CONTEXT = 0x000e, // preimage PREIMAGE_SIZE = 0x070D, PREIMAGE_COPY = 0x070E, DEBUG_LOG = 0x0901, } }