How Stateless Account Abstraction works on Fuel
from contract account wallets to stateless execution - Fuel hits different.
Over the past few years there has been a significant amount of discussion around Account Abstraction (AA), particularly on the Ethereum side, the benefits of AA and what it means for everyday users and the overall Web3 ecosystem. To better understand what AA looks like on Fuel and its differences in implementation, we need to start at what it looks like on Ethereum. I’m not going to do a full deep dive on Ethereum AA here, there are many other posts out there on the topic, but I will go over some of the key points to get you up to speed. We will briefly discuss some of the differences in virtual machine (VM) design and what this allows us to build in terms of transaction logic and the effects it has on blockchain size.
The ultra-short Crash Course on AA:
Currently on Ethereum there are two types of accounts: EOA’s - Externally owned accounts. An EOA is the type of account a traditional wallet would have controlled by a private key. The other type is a CA - Contract Accounts, any smart contract controlled by its own bytecode. The goal of account abstraction is to reduce the number of account types on Ethereum from 2 (EOA’s & CA’s) to just 1 (CA’s), and to move functionality such as signature verification, replay protection and gas payment mechanisms out of the core Ethereum protocol and onto the EVM. i.e., make accounts more programmable. Currently, any transaction on Ethereum has to start from an EOA, which makes having a third party (like a CA) start a transaction not possible today in the protocol.
Enter Account Abstraction or EIP-4337. Hang-on, lets just take a small tangent for a second to qualify what the word “abstraction“ means here. What is it to have account abstraction? To abstract something is to extract or isolate the essential qualities or characteristics from a complex object or topic. In the sense of accounts on blockchain systems, account abstraction means to remove unnecessary elements like private key, mnemonic phrases, derivation paths etc and get down to the bare bones of what an account is and what it needs to do. An account needs to store assets in a secure manner, do transactions and pay gas to do those transactions.
When it comes to abstracting away unnecessary details from accounts, or wallets as we colloquially call them at the moment. The main difference is the idea of a Contract Account wallet (CA wallet). Instead of having assets tied to an address all secured by a private key (e.g. a standard EOA wallet, like a browser extension or mobile app), users will have their assets tied up in a smart contract that lives on the blockchain and assets are secured by code. That’s it. How users interact with their new smart contract account wallet brings with it a significant amount of benefit but also opens up questions about the disadvantages.
The advantages:
Social recovery - ever lost your wallet and all the assets within it because you changed computer and forgot to write down your mnemonic phrase or simply forgot where you wrote it down? Maybe your computer got stolen or just broke and you lost access to your assets. What social recovery does is allow you to appoint parties that can help you recover access to your wallet in the event you loose access to it.
Multi-signature transactions - get more than 1 signature on a transaction . Selling your house or boat? need an escrow to sign off on something as well as yourself?
Have arbitrary logic with a transaction - set conditions for transaction execution (send Bob this amount of asset at 4pm on Saturday).
Gassless transactions - Have someone else pay for your transaction fees.
Easy wallet migration and upgradeability - migrate to a different wallet that has more features.
The disadvantages:
To make things easier we have to make them more difficult. To get all of this to happen its a fairly complex arrangement of infrastructure. The idea of a User Operation (UserOp) is born. Users don’t send transactions anymore, they send UserOp’s to an singleton entry point contract to get validated and “run”. For the everyday user this will all be hidden behind something that looks and feels familiar, but at the specification level its more complicated than an EOA simply sending a transaction and paying gas.
(in terms of Ethereums implementation):
The biggest question is that of state bloat. How much of an impact is this going to have on the size of the blockchain itself, the size of the chaindata on the hard drive of every full node. If users are going to end up with a smart contract that represents their new wallet and every transaction is recorded. How many bytes, gigabytes, terrabytes over time is this going to add? What are the impacts of this on nodes, networking communications and its impact on decentralization and client diversity?
This is where things between Ethereum and Fuel start to differ and we start talking about Fuel’s approach to account abstraction which ties deeply into the design philosophy of the virtual machine itself. But first lets touch on the thing mentioned above “state bloat”.
Stateful & Stateless Abstraction
What does it mean to be stateful or stateless? When we do transactions on the blockchain, whether its sending your friend some base asset (ETH) directly or interacting with a smart contract, we are changing the state of the blockchain itself. What’s happening is we are adding bytes of data to an ever growing data file that is stored on every full node that is connected to the network. If we deploy a smart contract we add data. If we interact with a smart contract and change its state, we add data. All of this increases the size of the state of the blockchain and we pay for this by consuming gas. Adding new features to a blockchain ecosystem is not only a task for developers writing the code, but also careful consideration on how new features impact the overall ecosystem. If node operators start to have increasing costs in upgrading hardware to store, process and network large amounts of chain data, this becomes a problem for decentralization and the speed at which transactions can occur, validation happens and consensus gets met.
Some figures as of August 2023 comparing the chain data size of:
Bitcoin - 505.88 GB
Ethereum - 1.168 TB
Litecoin - 121.20 GB
Cardano - 133.45 GB
Solana - 2TB/year
State bloat is the result of adding more data to the blockchain database itself. You could imagine if the next 1 billion users flowed in to Web3 what impact this would have on the size of the chain database. Fuel has designed its virtual machine to take advantage of stateless execution and reduce state bloat. i.e., transactions that don’t add data to the overall chain data. To understand how stateless execution is done, yes you guessed it, we need to understand the differences between the execution contexts within the Ethereum Virtual Machine (EVM) and the Fuel Virtual Machine (Fuel-VM).
VM Execution Contexts & Memory Models:
The Fuel-VM and the EVM both use a standard idea in VM design called an execution context (sometimes referred to as an execution environment). A place where manipulation of variables happens and bytecode is executed within the “context” of a memory space that was created for the program that is being run. The EVM creates new memory spaces per execution context to run smart contracts, it uses call data and return data buffers to communicate with other contexts. The Fuel-VM makes a distinction between what “type” of program is being run. In the Fuel-VM there are four (4) different execution contexts varying in their ability to modify and /or see state. They are:
Predicate Estimation - Estimates the gas required for predicate validation. Failing if any contract instructions are hit and/or if the program counter is set to a value outside the end of the predicate bytecode during execution. This context is considered external with the frame pointer set to zero.
Predicate Verification - Returns true if execution does not run out of gas, no contract instructions are hit and the program counter is not set to a value outside the end of the predicate bytecode. i.e. if validation is successful return true, else return false. Again this is considered external like above.
Script - In this context the environment can not save its state (modify variables and use then later), but it is aware of a global state, it can read the state of other contracts or chain data. This context is considered external with the frame pointer set to zero.
Calls - A contract call context. In this context a new call frame is pushed onto the stack. This is used for inter-contract calls. This is considered an internal call with the frame pointer set to a non-zero value.
Note: In reality predicate estimation & verification are just called a predicate context, the only difference being that predicate estimation happens before verification. Verification uses the variable predicateGasUsed
which is set to tx.gasLimit - $ggas
from the Estimation context first.
Predicate Context(s):
That’s all I’m going to touch on for the VM differences as that’s whats relevant to this blog. There are in fact many differences between both VM’s and I don’t want to bloat this post with a VM discussion - see a later post of a discussion on the VM. If you want to know more about the differences you can check out this page.
Before we move on I want to touch on some terminology for the future. There are in fact quite a few blockchains that use the terms “contract“, “script” or “validator script” loosely to describe the execution of some arbitrary code that results in something being done. Notice I’m being rather vague here about what exactly gets done, because it varies between blockchain and the way inputs and outputs to transactions are controlled along with the VM design itself. With respect to that, a transaction “validator” may be stateful or stateless. So its understandable that the terminology gets confusing because its meaning varies between ecosystems. If we go all the way back; Script was the language used by Bitcoin to determine who can spend an unspent transaction output (UTXO). Maybe you have heard the term Pay-to-Script-Hash (P2SH) before? When we talk about a validator script for the rest of this blog, I’m fast forwarding the technology but still bringing some old terminology with me. I’m referring to an arbitrary piece of bytecode that when executed, takes some input and evaluates it to produce a valid or invalid transaction. I’m going to add a condition that this “validator” must be stateless, it can’t see or modify any global state - this is more-or-less what Fuel calls a predicate.
Predicates:
This is when things start to come together. We’ve talked about stateful and statelessness, the concept of abstraction of accounts, taken a trip down VM-lane, lets wrap it up with the thing that’s doing all the heavy lifting, the predicate.
What does the term predicate even mean? In formal logic, a predicate usually takes one or more arguments and forms a proposition when these arguments are applied. Now in simple terms, a predicate takes one or more arguments, evaluates them and says “true” or “false” in response to those arguments.
“A predicate is just a piece of code“ who hasn’t heard that when being told what a predicate is. It’s kind of true though. A predicate is simply compiled code that validates or invalidates a transaction by taking some input (one or more arguments) and evaluates to one of two possible outcomes; true or false. Ok so what? Well, let me give you a basic example and then i’ll make my point. Alice wants to send Bob 100 coins but only if Bob inputs the correct value of some variable ‘a’ into the validator (the predicate). The validator code looks something like this:
If the value of ‘a’ that is input to the predicate is 5 (as a unsigned 64-bit number) then the predicate will evaluate to true, otherwise false. So if Alice wants to send Bob 100 coins she can compile the bytecode above, which will produce the script hash below. This the the “address“ that Alice will use to send Bob the coins - Alice wont send Bob coins directly, she will use the predicate as an intermediary.
Predicate "address":
0x5b8a58d4e0e01278c732a257fc172c026cfea7b6364c22fa42e3b10f71ca2f6d
Alice will lock the 100 coins (as a UTXO) at the above address. i.e., she will send 100 coins to the predicate address. Now on Bobs end, Alice needs to give Bob the bytecode for the compiled predicate and tell him what value of ‘a’ he needs to supply to the predicate to unlock the 100 coins. Technically speaking Bob is consuming the UTXO’s at the predicate hash.
You may be thinking, well why cant I just unlock the funds at the address and take the 100 coins for myself, what makes Bob so special? Nothing! you can spend the UTXOs at the predicate hash. Nothing is stopping you or anyone else for that matter from doing that other than knowing the predicate bytecode and the value of ‘a‘ that will make the evaluation of the predicate return true. So how do we constrain the validity conditions of the predicate for Bob only? We replace the value of ‘a’ with some other variable and change the execution logic to something specific to Bob. In the case of our smart contract wallet that we discussed earlier when talking about Ethereums EIP-4337, it would be easy to add a storage variable to the smart contract wallet, “the_owner“ and have this variable be the public key hash of the signer of a UserOp to the CA wallet. That’s not how it works with a predicate wallet. Remember predicates are stateless, they have no mutable storage variables and no way of changing anything within themselves except for volatile memory - they have no persistent memory. So the same predicate (bytecode and hash) can be used by many transactions, it has no owner, and does not care who runs it, all the predicate cares about is if it evaluates to true or false.
Ok so here’s by point. The predicate root (or “address” as we sometimes call it inaccurately) is unique to the bytecode of the predicate itself. If I change the value of 5 to 6 in the above example, it changes the predicate root completely.
If we want Bob to be the only person who can validate this predicate to true instead of anyone with the correct value of ‘a’ we need to make the predicate bytecode specific to Bob. What is specific and unique to Bob(?) - his signature and public key hash.
The Fuel-VM has the ability to recover the public key hashes from messages signed by private keys with elliptic curves Secp256k1 and Secp256r1, and to do signature verification for edDSA over curve25519. This makes us able to send some signed data into a predicate and get the VM to recover the public key that was used to sign the data.
The way we can construct a stateless account wallet is by using a predicate to validate transactions that operate on UTXOs sitting at the predicate hash. The predicate takes as an argument a signed transaction. A signed transaction by Bob who signed it using his private key. The predicate internally decodes the transaction, recovers the public key hash that was used to sign the transaction and asks itself if the recovered key is equal to Bobs key (that is baked into the predicate as a constant). Remember we can bake values into a predicate bytecode but cant change them. If we bake Bobs public key into the predicate and then compile the predicate source code, we get a predicate hash that is unique to Bob and whatever we use as logic in determining if the argument (the signed transaction) is signed by Bob.
Just as a note, if someone else creates and compiles a predicate that has Bobs public key in it and the exact same logic, they will get the same predicate hash. If they then send coins to that predicate hash, it still means that Bob is the only one who can unlock the coins.
In the predicate below (not real code), I’ve hashed out what this would looks like. The predicate takes a transaction data (in some form), that has been signed by Bob. The predicate decodes it and extracts the signature, gets the hash of the transaction and recovers the public key that signed the transaction data. If the public key recovered matches Bobs the predicate will evaluate true, unlocking any specific UTXOs sitting at the predicate hash. If another key has signed the transaction the entire transaction will revert.
Obviously there is something rather important that im missing here and that the idea of a nonce. To avoid the signed transaction being replayed multiple times and someone draining Bobs predicate wallet, a unique number for that particular transaction needs to be included in the transaction data itself and that value changed after transaction execution. There are also some things that need to happen behind the scenes like adding input and outputs to the final transaction that gets sent for execution, but the essential concepts are there to illustrate the point of a stateless execution. This is an example of how one can build a “wallet” on Fuel using a predicate that has assets locked at it (the predicate root or “address”) and the only way to unlock (or spend) those UTXO’s is to provide a transaction that once decoded and checked to be signed by a particular public key returns true. This mechanism of using a predicate in transaction execution does not rely on the state of external contracts or other state data apart from whether there is enough coin inputs to satisfy the transaction itself. So Account Abstraction on Fuel looks quite different from Ethereum implementation but adds stateless transactions. This brings with it the benefits of account programability, while minimising the impact to state growth.
Summary:
The landscape for accounts and the functionality that they have is rapidly changing. More programability and control over how transactions get processed with added features like social recovery and payments of fees in any token helps with user adoption into Web3. However adding features while retaining core the core protocol brings with it challenges for any ecosystem. Fuel has had things like state bloat and stateless and parallel execution in mind from the start and designed the VM in a way that brings together many great parts from multiple blockchain implementations and adds more.
If you liked this post hit the subscribe button below.