The FuelVM is a 64-bit register machine, featuring locally writeable, globally-readable memory, multiple execution context types, and runtime configurability.
In this article we will dive into the asset, call, and memory models, storage layout, execution contexts, and instruction set used by the FuelVM. We will also compare and contrast with the Ethereum Virtual Machine from time to time for any EVM developers reading.
Asset Model
The FuelVM supports multiple fungible assets that contracts may interact with just as they do with the native asset. Using the following syntax, assets can be attached to a contract call by their asset ID.
In the EVM, Ether is the only native asset that is given “first-class” support, meaning only transfers of Ether can be directly attached to external contract calls. In the FuelVM, all native assets have this support.
Call Model
Contracts may be called by other contracts or scripts. Each contract call will push a new call frame to the call stack. The new call frame has access to the following information.
contract ID
asset ID of forwarded asset (zero, if none)
reserved registers from the previous context
contract code size, in bytes
first call parameter
second call parameter
contract code
The executing contract has read access to the global memory for the transaction, but the memory area to which it may write is allocated based on the execution context. More on this later.
In the EVM, memory is readable only within the current executing context. Any contract call will create a new linear memory instance that will be freed at the end of that execution context. This requires contracts to encode and decode data to send and return data to one another via calldata and returndata, respectively.
By using globally readable memory, contracts in the FuelVM can write data to memory, then send and return pointers to the relevant data.
The contract call syntax in assembly is as follows where a
is a pointer in memory to the contract ID and arguments, b
is the amount of a given asset to send, c
is the ID of the asset to send, and d
is the amount of gas to forward to the execution sub-context.
call a, b, c, d
Memory Model
Memory exists at the transaction level and is statically sized with an upper bound set in the VM configuration as MEM_MAX_ACCESS_SIZE.
In the EVM, memory expansion gas costs increase quadratically with the size of memory while the FuelVM removes the dynamic gas in favor of a deterministic amount of memory.
Memory can be manipulated via the stack or the heap; it can be written to, read from, copied, and cleared using the memory instructions below.
Storage Layout
Persistent storage consists of key-value pairs, both 32 bytes in size, on the contract level.
Storage can only be written to and read from within the currently executing contract.
This behavior is functionally identical to the EVM.
Execution Context
In the FuelVM, there are three execution context types; predicates, scripts, and contracts. Scripts and predicates are external execution contexts while contracts are internal. This means scripts and predicates may only be called by external accounts, while contracts may only be called within another execution context; either scripts, predicates, or other contracts.
VM Initialization
Before an external execution context begins, the VM must be initialized. The following steps are taken to initialize the FuelVM.
Memory of size
VM_MAX_RAM
is allocated for the stack and heap.The stack begins at 0 and grows upward.
The heap begins at
VM_MAX_RAM - 1
and grows downward.
The 32 byte transaction hash is computed then pushed to the stack.
Up to eight pairs of asset IDs and balances are pushed to the stack.
In a predicate, these are all zeroes.
In a script, if there are fewer than eight asset IDs, the remaining values are zeroes.
Transaction length, in bytes, as a
uint64
, is pushed to the stack.The transaction data is serialized and sequentially pushed to the stack.
The stack-start pointer (
ssp
) register is set to be the slot immediately following the transaction data.The stack pointer (
sp
) register is set to equal thessp
register.The heap pointer (
hp
) register is set toVM_MAX_RAM - 1
.
Script
A script is FuelVM bytecode that holds no state of its own, but is aware of the global state. On execution, the VM is initialized as documented above, then the script is executed as follows.
The program counter (
pc
) and instruction start (is
) registers are set to the start of the transaction script’s bytecode.The globally available gas (
ggas
) and execution context gas (cgas
) registers are set to the current transaction’s gas limit.For each instruction:
Compute the instruction’s gas cost.
Subtract the gas cost from the context’s available gas, reverting the context on underflow.
Subtract the gas cost from the globally available gas.
Execute the instruction.
Increase the program counter as appropriate.
Predicate
For any predicate inputs of type Coin
(input types listed here), if the predicate has code (non-zero predicate length), the UTXO is treated as a Pay-To-Script Hash (P2SH) as opposed to a Pay-To-Public-Key Hash (P2PKH). Essentially this means the asset being sent includes an executable that must evaluate as true
to be considered a valid transaction. If at any time execution halts and returns false
, the transaction is considered invalid. It is also worth noting that if a predicate evaluates as true, it is monotonic with respect to time; that is to say the predicate will always evaluate as true in the future.
On execution, provided the predicate length is non-zero, for each input of type Coin
, the VM is initialized as documented above, then the predicate code is executed as follows.
The program counter (
pc
) and instruction start (is
) registers are set to the start of the input’spredicate
field.For each instruction:
Execute the instruction.
Halt and return false on contract-related instructions.
Halt and return false on jump instructions where the program counter is:
decreased
increased to a value greater than the predicate length
Increment the program counter.
CPU Architecture
The FuelVM CPU has 64 registers, the first 16 of which are reserved and may not be written to directly. The following is the list of reserved registers and their respective indexes.
Instructions can be divided into arithmetic, bitwise, logical, control flow, memory, contract, cryptographic, and miscellaneous instructions.
Arithmetic, Bitwise, and Logical Instructions
These instructions are relatively simple. Read values from some registers, perform an operation on them, and write the result to another register.
Notice instructions that end with i
indicates one of the two inputs is an “immediate” or compile-time constant value.
Arithmetic
The following is a list of arithmetic instructions and their behavior.
Bitwise and Logical
The following is a list of bitwise and logical instructions and their behavior. Notice instructions that end with i indicates one of the two inputs is an “immediate” or compile-time constant value.
Control Flow Instructions
Control flow instructions involve manipulating the program counter to jump to arbitrary places in the executable code. The following is a list of control flow instructions and their behavior.
Memory Instructions
Memory instructions involve manipulating memory, both the stack and the heap. The following is a list of memory instructions and their behavior.
Contract Instructions
Contract instructions relate to contract calls, contract state reads and writes, event logging, asset minting and burning, code copying, balance reading, and environment variables more broadly.
Cryptography Instructions
Cryptography instructions include cryptographic operations built into the FuelVM directly. The following is a list of cryptography instructions and their behavior.
Miscellaneous Instructions
The following instructions include operations that do not fit into any other category and their respective behavior.
On the flag instruction and register:
if the first bit in the flag register is set to one, math operations will not panic
if the second bit in the flag register is set to one, bit wrapping will not panic
Conclusion
That’s all for this one. The FuelVM makes some distinct improvements against the EVM, but many design decisions are quite similar and the machine is still relatively simple, despite its distinct register machine architecture and memory model.
Sources for this article include the FuelVM Specification, the Fuel Book, the Sway Book, and the FuelVM Instruction Set. All of the instruction set tables in this article can be found in this gist.
If you like this content or have feedback, feel free to reach out on twitter and subscribe below!
Until next time, good hacking. 🤘
man I love jtriley