1. Project Overview & Quickstart (FuelLabs/b256)
Technical Documentation: FuelLabs/b256
ℹ️ Provenance: Multi-Tier Ingestion · CodeWiki Reference
b256
Agent Skills & Rules
- Discovered 21 agent rule(s) and prompt configurations.
--- METRICS ---
- Files Extracted: 1
- Estimated Token Budget: ~23 tokens
- Canonical Reference: https://codewiki.google/github.com/FuelLabs/b256
2. Official Technical Reference & Guides (FuelLabs/docs)
File: README.md
Fuel Docs
A complete set of documentation for the Fuel-powered tools and services.
Building From Source
Install Dependencies
npm installBuild
npm run buildStart
npm run startFile: package.json
{
"name": "fuel-docs",
"version": "1.0.0",
"description": "The Fuel documentation.",
"main": "config/build.js",
"scripts": {
"start": "npm run build && http-server dist --cors",
"build": "parcel build public/index.html --out-dir build && node config/build",
"publish": "npm run build && now dist && now alias https://dist.fuellabs.now.sh docs.fuel.sh",
"publish-staging": "npm run build && now dist && now alias https://dist.fuellabs.now.sh docs.staging.fuel.sh",
"test": "echo "Error: no test specified" && exit 1"
},
"repository": {
"type": "git",
"url": "git+https://github.com/FuelLabs/docs.git"
},
"keywords": [
"fuel",
"docs",
"documentation"
],
"author": "Nick Dodson [email protected]",
"license": "Apache-2.0",
"bugs": {
"url": "https://github.com/FuelLabs/docs/issues"
},
"homepage": "https://github.com/FuelLabs/docs#readme",
"dependencies": {
"copy-text-to-clipboard": "^2.2.0",
"fs-extra": "^9.0.1",
"markdown-to-txt": "^1.0.1",
"hyperapp": "^2.0.4",
"esm-import-regex": "1.0.0",
"hyperapp-styled-components": "^0.1.2",
"showdown": "^1.9.1",
"regenerator-runtime": "^0.13.5"
},
"devDependencies": {
"cssnano": "^4.1.10",
"http-server": "^0.12.3",
"parcel": "^1.12.4"
}
}
File: src/v1.0.0/1. Introduction/1. Welcome.md
Welcome
Before we begin:
Try out our live interactive 🕹️ Fuel Plays Pokémon demo!
Check out the ⚡ Block Explorer.
Use the one-click 💻 Wallet SDK Example.
Introduction
Fuel is a permissionless and trustless layer-2 protocol for Ethereum, targeted at low-cost high-throughput value-transfer transactions. Fuel is powered by a highly-optimized optimistic rollup design.
Fuel's Features
- Completely decentralized: non-custodial, censorship resistant, and permissionless
- The most scalable optimistic rollup in the world, with unique UTXO-based design
- 500 TPS maximum on-chain throughput (1,500-2,000 with BLS aggregate signatures)
- Up to 10,000 TPS surge intake capacity
- Send and receive using any Ethereum address, no registration required
- ETH and all ERC-20 tokens are supported
- Non-custodial token swaps between any tokens
- Fees can be paid in any token, not just ETH
- HTLCs for cross-chain atomic swaps and additional usecases
- Withdrawals in under 10 minutes using atomic swaps
What Makes Fuel Different
- UTXO-based design optimized for maximum scalability
- Parallelism: transactions can be validated in parallel on consumer-grade hardware
- No state bottleneck: unique fraud proof scheme doesn't require state serialization
- No special key formats or on-chain account registration required
- No upfront collateral requirements (unlike channels)
- Permissionless and censorship-resistant (unlike Plasma/Validium)
- No burdensome proof generation with high latency (unlike ZK rollups and Validium)
- No exotic cryptography (unlike ZK rollups and Validium)
- No trusted setup (unlike SNARK-based ZK rollups and Validium)
- No worse-than-useless protocol-specific token (unlike...too many to count)
- Care and attention to user-level issues
- Can deposit using simple transfers from any account (including exchange accounts) using Funnel contracts
- Can use a smart contract wallet to authorize transactions on Fuel
What To Read Next?
System benchmarks can be found on the next page.
If you want to know more about the fundamentals of Fuel's architecture, our Technical Fundamentals is your next stop. A detailed analysis on the security and scalability of Fuel is available.
If you're a developer and want to build on top of, or integrate with, Fuel, check out our Getting Started guide and wallet SDK documentation.
For examples of tools and applications that can built on top of Fuel, start here.
Finally, our technical roadmap can be found here.
File: src/v1.0.0/1. Introduction/2. Benchmarks.md
Benchmarks
Simulations
Below are simulation results for Fuel v1, for a number of different representative usecases. Usecase-specific and context-sensitive optimizations are not done, so these numbers are representative of normal usage. Such optimizations include batching multiple transactions from the same sender in the same rollup block to save space, or reducing the number of bits to represent token types to only handle a tiny number of tokens.
Running Locally
The simulation can be ran locally:
git clone [email protected]:FuelLabs/fuel.git
npm install
npm run benchmark100k Transfers
Standard one-input two-output transfers.
- Transactions Submitted: 100000
- Roots committed: 360
- Blocks committed: 3
- Cumulative gas used: 185387356
- Ethereum blocks used: 23
- @100 Gwei: 18.539 ETH
- @$300 USD / ETH: $5562
100k Token Minting
One-input eight-output mass token minting transactions.
- Claims Processed: 100000
- Transactions Submitted: 12500
- Roots committed: 134
- Blocks committed: 2
- Cumulative gas used: 72457791
- Ethereum blocks used: 9
- @100 Gwei: 7.246 ETH
- @$300 USD / ETH: $2174
75k Token Burns
One-input three-output (burn, memo, change).
- Transactions Submitted: 75000
- Roots committed: 275
- Blocks committed: 3
- Cumulative gas used: 140909433
- Ethereum blocks used: 17.613679125
- @100 Gwei: 14.091 ETH
- @$300 USD / ETH: $4227
25k Subscriptions
One-input two-output.
- Transactions Submitted: 25000
- Roots committed: 87
- Blocks committed: 1
- Cumulative gas used: 45442233
- Ethereum blocks used: 5
- @100 Gwei: 4.544 ETH
- @$300 USD / ETH: $1363
Benchmarks
Benchmarks use the same transaction types as the simulations above (i.e. they are representative of normal usage).
Fuel v1 on Ropsten
100k Transfers
Contract deployed at 0x57f51C778Dd66153A44881Cc5E6e9da421c4dC85.
- Cumulative gas used: 225360000
- @100 Gwei: 22.536 ETH
- @$300 USD / ETH: $6761
100k Token Minting
Contract deployed at 0x47b13755d14BCEFC49C0606Aca5E62Dc2C1Da9eC.
- Cumulative gas used: 89184000
- @100 Gwei: 8.918 ETH
- @$300 USD / ETH: $2675
75k Token Burns
Contract deployed at 0xf73788cD87267c68dA5ed3e3a37c4A610a07513d.
- Cumulative gas used: 176400000
- @100 Gwei: 17.640 ETH
- @$300 USD / ETH: $5292
25k Subscriptions
Contract deployed at 0x232113aaC91Bab5b6351016497f966dA8C1BfB9C.
- Cumulative gas used: 56112000
- @100 Gwei: 5.611 ETH
- @$300 USD / ETH: $1683
Audits
- An audit with Hashcloak is currently underway.
- An audit with Trail of Bits is scheduled.
File: src/v1.0.0/1. Introduction/3. Use Cases.md
Use Cases
In this section we explore various use cases for the Fuel v1 optimistic rollup. All the use cases covered below are examined in more detail in the Tools and Applications section.
Transfers
Fuel can be used for high-performance, low-latency global transfers with any ERC-20 token or Ether.
This is ideal for:
- Global payment settlement
- Application-specific tokens
- Virtual gaming tokens and rewards
- Any situations where users are transferring ERC-20 tokens
Please see our interactive 🕹️ Fuel Plays Pokémon demo for a live example of transfers in action.
Non-Custodial Exchange
Fuel's unique multi-user transactions and HTLC support are a perfect foundation for non-custodial exchanges.
We support two kinds of exchange:
- Multi-user on-Fuel swaps
- Cross-chain atomic swap with HTLCs
For further reading, please see Non-Custodial Exchanges.
Mass Token Dispersal
Fuel supports multi-input, multi-output transactions, making it perfect for pre-signed mass token dispersal scenarios where a single account must disperse tokens to a large number of unique recipients cost-efficiently.
This is possible on Fuel with a Merkleized dispersal pattern and our deterministic transaction identifiers, allowing dispersal transactions to be pre-signed and distributed off-chain. Tokens are only actually dispersed when claimed, greatly reducing overhead costs for unclaimed tokens.
For further reading on how we accomplish this with low overheads, please see Token Minting
Fast Withdrawals
Fuel allows for fast (~10 minute) withdrawals using our unique HTLC output. Liquidity providers on the intended exit chain (e.g. Ethereum or Bitcoin) can allow users on Fuel to withdraw with minimal effort and at low cost.
For further reading, please see Fast Withdrawals.
Subscriptions
Fuel's pre-signed transaction format is ideal for creating regular financial subscriptions, where a user can sign up to a subscription, pre-signing a set of transactions, and then allowing any service or system to collect payment on a regular basis non-interactively.
This is allows for cheap, care-free subscriptions for magazines, blogs, forums, and more.
For further reading, please see Subscriptions.
Burning
Fuel has a "return data" transaction output, which can be used to store arbitrary data in Fuel such as: burn metadata, social media messages, timestamped hashes, or specific application data.
The burning of a token by sending it to the zero address can be associated with a specific event using the return data output, all within in the same transaction.
File: src/v1.0.0/2. Getting Started/0. Using the Wallet SDK.md
Using the Wallet SDK
The wallet is how users and developers can interact with Fuel.
Install
The Fuel module is currently available as a UMD through CDN or NPM.
node.js
npm install --save @fuel-js/walletbrowser (UMD)
<script src="https://unpkg.com/@fuel-js/wallet@latest/umd/fuel.umd.min.js" type="text/javascript">
</script>browser (ESM)
<script type="module">
import fuel from "https://unpkg.com/@fuel-js/wallet@latest/umd/fuel.umd.min.js";
</script>Creating a Wallet
The Fuel wallet can be used with or without a standard web3 or ethers provider. The provider will be used for account signing or any Ethereum related actions, such as deposits and withdrawals.
If an override privateKey is not specified, the first account given by the provider will be used.
If an Ethereum network is not specified (e.g. rinkeby), it will be requested from the provider.
import fuel from '@fueljs/wallet';
// no provider, privateKey is generated
const wallet = fuel.wallet(null);
// web3 provider, first account used, network gathered from provider
const wallet = fuel.wallet(window.web3.currentProvider);
// provider with override privateKey and network specified
const wallet = fuel.wallet(window.web3.currentProvider, {
privateKey: fuel.utils.hexlify(fuel.utils.randomBytes(32)),
network: 'rinkeby',
});Fauceting
All Fuel test networks have a token faucet that will dispense fake tokens to requesting accounts (within several seconds to minutes).
The faucet will dispense 1000 faucetTokens.
The faucet test token identifier will always be available in fuel.contants.faucetToken.
import fuel from '@fueljs/wallet';
// new wallet
const wallet = fuel.wallet(null, {
network: 'rinkeby',
});
// faucet fake tokens
await wallet.faucet();
// check your balance
console.log('my balance: ', await wallet.balance(fuel.constants.faucetToken));Depositing
You can deposit ERC-20 tokens or Ether into Fuel by using the deposit method. Simply specify the token and amount to deposit. Since Fuel uses Ethereum key pairs, this process can be done directly with your provided MetaMask account, for example.
The deposit method will make two Ethereum transactions, depositing your funds into Fuel and making them accessible for Fuel activities (e.g. transfers and swaps).
import fuel from '@fuel-js/wallet';
const wallet = new fuel.Wallet(window.web3.currentProvider);
(async () => {
// rinkeby dai address
const daiAddress = '0xc3dbf84Abb494ce5199D5d4D815b10EC29529ff8';
// deposit funds
await wallet.deposit(daiAddress, fuel.utils.parseEther('1.0'));
})();Transferring
Now that you have funds in Fuel, either by deposit or faucet you can begin to transfer them around.
Let's try transferring 1 Fuel token to our own wallet address and check the submission by its transaction ID.
import fuel from '@fuel-js/wallet';
const wallet = new fuel.Wallet(window.web3.currentProvider);
(async () => {
await wallet.faucet();
// transfer one faucet token to our own wallet
const { transactionId } = await wallet.transfer(
fuel.constants.faucetToken,
wallet.address,
fuel.utils.parseEther('1.0'));
console.log('view my transaction: https://rinkeby.fuel.sh/tx/' + transactionId);
})();Withdrawing
Currently, the Fuel wallet only supports standard withdrawals, which means you will send an initial withdraw transaction on Fuel first, than release it after the finality delay of 1 week has expired. In the future, the wallet will support fast withdrawals via atomic swaps, where total withdraw time can be reduced safely to as little as 5-10 minutes.
In order to release the tokens on Ethereum, you must have some Ether for the transaction.
import fuel from '@fuel-js/wallet';
const wallet = new fuel.Wallet(window.web3.currentProvider);
(async () => {
await wallet.faucet();
// withdraw one faucet token
await wallet.withdraw(fuel.constants.faucetToken, utils.parseEther('1.0'));
// wait one week...
// release any withdrawals onto Ethereum
await wallet.release();
})();File: src/v1.0.0/2. Getting Started/1. One-Click Example.md
One-Click Example
Below is our one-click Wallet SDK example using the Fuel test faucet on Rinkeby.
Try it out!
await import('https://unpkg.com/@fuel-js/wallet@latest/umd/fuel.umd.min.js');
// Create a new wallet with a new key
// Tip: try setting null to web3.currentProvider to run with MetaMask
const wallet = new fuel.Wallet(null, {
network: 'rinkeby',
});
// Get yourself some fake Moons
await wallet.faucet();
// Establish faucet token id
const faucetToken = fuel.constants.faucetToken;
// Get your fake moons balance
console.log('my balance: ',
await wallet.balance(faucetToken, { format: 'ether' }));
// Send yourself some faucet token
const tx = await wallet.transfer(faucetToken, wallet.address, '4.5', { units: 'ether' });
// Checkout your transaction on the Block Explorer
console.log('check out your tx: ' + window.rinkebyTx + tx.transactionId);
run
File: src/v1.0.0/2. Getting Started/2. Running a Node.md
Running a Node
The Fuel client can be used to sync with Fuel and fully validate the chain.
Binary Releases
Binary releases can be found at https://github.com/FuelLabs/fuel-js/releases
Running from Command Line
Download a release (above) and use an available network specifier and RPC endpoint to get started.
fuel --network="rinkeby" --rpc="https://localhost:8545"Additional Parameters
⚡ fuel [options]
Options:
-n, --network the ethereum network "rinkeby"; default "rinkeby"
-r, --rpc a standard Ethereum RPC provider (i.e. local go-ethereum)
-i, --infura an Infura service API key (--network must also be specified)
-es, --etherscan an Etherscan service API key (--network must also be specified)
-e, --environment use the environment variables to specify node parameters
-o, --operators a comma-separated list or seed phrase of Ethereum private keys used for block, root, and fraud commitments
Examples:
$ fuel --network="ropsten" --rpc="http://localhost:8545"Running from GUI
Coming Soon™...
File: src/v1.0.0/2. SDK/1. Wallet.md
Wallet SDK
new
new fuel . Wallet ( provider [, options ] )
Installation
node.js
npm install --save @fuel-js/walletbrowser (UMD)
<script src="https://unpkg.com/@fuel-js/wallet@latest/umd/fuel.umd.min.js" type="text/javascript">
</script>browser (ESM)
<script type="module">
import fuel from "https://unpkg.com/@fuel-js/wallet@latest/umd/fuel.umd.min.js";
</script>Usage
import fuel from '@fuel-js/wallet';
// use the window provider key / account / eth_signTypedData_v4 for transaction signing
const wallet = new fuel.Wallet(window.web3.currentProvider);
(async () => {
// make a deposit
await wallet.deposit(daiAddress, fuel.utils.parseEther('1.0'));
// make a transfer
await wallet.transfer(daiAddress, recipientAddress, fuel.utils.parseEther('1.0'));
// listen for new inputs and balance changes
wallet.on('input', input => {
console.log('listening for new inputs: ', input);
});
// get your address
console.log('my address', wallet.address);
})();Description
The Wallet object allows you to interact with a Fuel instance.
This object is heavily powered by the ethers library. web3.js providers are also supported.
The Wallet wrapper will simply use the provided provider object's eth_signTypedData_v4 method for signing transactions.
If neither are available, you can use the Wallet object as a read-only wallet. You may also provide an override privateKey which will be used in place of the provider. The storage and management of this privateKey is up to the developer.
Ethereum gasPrice and gasLimit options are only needed when using the force feature of SDK methods, which will allow users the option of creating their own rollup blocks.
Params
provider : Web3Provider | EthersProvider; a web3 or ethers
Providerobjectoptions : Object
- privateKey :
Hex|Uint8Array|SignerKey; the override 32 byte Ethereum standard private key used for signing transactions. Used instead of the provider signing usingeth_signTypedData_v4 - gasPrice :
Hex|BigNumber; the Ethereum gas price for forced transactions - gasLimit :
Hex|BigNumber; the Ethereum gas limit for forced transactions - address :
Hex; the read-only 20-byte hex string address for watching balances - db :
LevelDown; a leveldown-compliant database instance for storing inputs and outputs temporarily or persistently - sync :
Boolean; sync before each wallet action, this can be turned off for better performance but you will have to managesyncmanually by usingawait wallet.sync(), see Performance and Syncing for more details.
- privateKey :
Throws
- If the
dbis invalid or will not open - If the
privateKeyis invalid or incorrect, if provided
const wallet = new fuel.Wallet(window.web3.currentProvider, {
db: require('level-js')(), // a leveldown-compliant database
});deposit
wallet . deposit ( token, amount [, options] ) => Promise<TransactionResponse>
Description
The deposit method will deposit ERC-20 tokens or ETH into the Fuel.
Params
- token :
Hex;Number;BigNumber; the 20-byte ERC-20 token address or ID number - amount :
Number|Hex|BigNumber; the amount to deposit into Fuel - options :
OptionsObject
Returns
This method returns a TransactionResponse object.
- response :
TransactionResponse; the transaction response object
Throws
- If there is insufficient balance of the ERC-20 token or ETH to cover amount and gas fees
- If the timeout has passed and no Fuel block producer has picked up the deposit
await wallet.deposit(daiAddress, 500000, {
// gasPrice: ...,
// gasLimit: ...,
// from: '0x...',
// timeout: 10000, // seconds
});faucet
wallet . faucet ( [, options] ) => Promise
Description
This method will call the Fuel network faucet, based upon your network, and send the wallet.address test tokens to play with.
Note, this method might take up to a few minutes to work and is rate-limited.
Params
- options :
OptionsObject
Throws
- If the
options.timeout, default100seconds (or100000micro-seconds) times out.
await wallet.faucet();balance
wallet . balance ( token, [, options] ) => Promise<BigNumber>
Description
This method will give you your current balance for a specific token.
Params
- token :
Hex;Number;BigNumber; the 20-byte ERC-20 token address or ID number - options :
OptionsObject
Returns
This method returns a BigNumber object containing the balance.
- balance :
BigNumber; the balance for the token
Throws
- If the token is invalid or is not registered with Fuel
await wallet.balance(daiAddress);transfer
wallet . transfer ( token, to, amount [, options] ) => Promise<TransactionResponse>
Params
- token :
Hex;Number;BigNumber; the 20-byte ERC-20 token address or ID number - to :
Hex; the 20-byte recipient address - amount :
Number|Hex|BigNumber; the amount to transfer - options :
OptionsObject
Returns
This method returns a TransactionResponse object.
- response :
TransactionResponse; the transaction response object
Throws
- If there is insufficient balance of the ERC-20 token or ETH to cover amount and fees
- If there is a connection or provider issue
await wallet.transfer(daiAddress, someAddress, 250000, {
// inputs: [], // specify explicit array of UTXO IDs to spend
// feeLimit: 2000,
// from: '0x....',
// gasPrice: ..., // only used if forcing a tx
// gasLimit: ..., // only used if forcing a tx
// force: true, // will attempt for force tx on-chain
});withdraw
wallet . withdraw ( token, amount [, options] ) => Promise<TransactionResponse>
Description
This function allows you to start a long withdraw from Fuel. You first send the withdraw transaction on Fuel, then you complete the withdrawal on Ethereum 2 weeks later.
Params
- token :
Hex;Number;BigNumber; the 20-byte ERC-20 token address or ID number - amount :
Number|Hex|BigNumber; the amount to withdraw - options :
OptionsObject
Returns
This method returns a TransactionResponse object.
- response :
TransactionResponse; the transaction response object
Throws
- If there is insufficient balance of the ERC-20 token or ETH to cover amount and fees
- If there is a connection or provider issue
await wallet.withdraw(daiAddress, 250000, {
// inputs: [], // specify explicit array of UTXO IDs to spend
// feeLimit: 2000,
// from: '0x....',
// gasPrice: ..., // only used if forcing a tx
// gasLimit: ..., // only used if forcing a tx
// force: true, // will attempt for force tx on-chain
});retrieve
wallet . retrieve ( [options] ) => Promise<TransactionResponse>
Description
This will complete finalized withdrawals. Withdrawals must first be started using withdraw.
Params
- options :
OptionsObject
Returns
This method returns a TransactionResponse object.
- response :
TransactionResponse; the transaction response object
Throws
- If there is insufficient balance of the ERC-20 token or ETH to cover amount and gas fees
- If there is a connection or provider issue
// after 2 weeks...
await wallet.retrieve({
// inputs: [], // specify explicit array of utxo hashes to retrieve
// from: '0x....',
// gasPrice: ..., // only used if forcing a tx
// gasLimit: ..., // only used if forcing a tx
});sync
wallet . sync ( [options] ) => Promise<Void>
Description
The sync method will simply download the current UTXOs for this wallet from the Fuel provider.
Throws
- If there is a connection or provider issue
- If there was an issue getting the private account via
eth_getAccounts - If there was an issue getting the current Ethereum network from the provider
// sync your wallet with the Fuel api
await wallet.sync();on
wallet . on ( type, listener )
Description
The on method is used to register listeners for incoming transactions from the Fuel API. Once an on listener is registered, the wallet becomes live, and balances will automatically update.
If there is a problem with the listener at any time or it fails to bind, the error type will be used.
Params
- type :
String; eitherinputorerror - listener :
Function; a callback function which will include a single argument: aUTXO,RootorDepositproof object
wallet.on('input', utxoOrRootProof => {
console.log(utxoOrRootProof);
});
wallet.on('error', error => {
console.log(error);
});fee
wallet . fee ( token ) => Promise<BigNumber>
Description
Get the current feerate for an ERC-20 token.
Params
- token :
Hex;Number;BigNumber; the 20-byte ERC-20 token address or ID number
Returns
- fee :
BigNumber; the feerate for this ERC-20 token
Throws
- If this token has no feerate available, i.e. the Fuel operator has not allowed this token yet
// get the current fee for processing a specific token
await wallet.fee(daiAddress);Performance and Syncing
Note, that the Wallet by default will sync before each action (i.e. transfer, balance etc), you may turn this off by setting { ..., sync: false } in either the global wallet options or per each method.
If you do this, you must manually manage sync operations using the wallet.sync() method when needed (e.g. upon account load or when the wallet balance is zero).
Objects
OptionsObject
- inputs :
Array [ Hex, ... ]; an array of 32-byte hex values specifying either theRoot,DepositorUTXOIDs you would like to spend - feeLimit:
BigNumber; the maximum feerate to accept for submitting the transaction - gasPrice :
Hex|BigNumber; the Ethereum gas price for forced transactions - gasLimit :
Hex|BigNumber; the Ethereum gas limit for forced transactions - from :
Hex; the 20-byte sender address to specify either the provider signer or the override private key - timeout :
Number; the timeout to wait in seconds for thePromiseto either fail or be accepted - force :
Boolean; force this method into a singleRooton Ethereum - sync :
Boolean; do an accountsyncbefore the given method or action.
UTXO
- transactionId :
Hex; - outputIndex :
BigNumber; - outputType :
BigNumber; - owner :
Hex; - amount :
BigNumber; - token :
BigNumber; - digest :
Hex; - expiry :
BigNumber; - returnOwner :
Hex;
Root
- producer :
Hex; - merkleTreeRoot :
Hex; - commitmentHash :
Hex; - length :
BigNumber; - feeToken :
BigNumber; - fee :
BigNumber;
Deposit
- owner :
Hex; - token :
BigNumber; - blockNumber :
BigNumber; - value :
BigNumber;
BigNumber
- This is an
etherswrapped BigNumber object, using BN.js under the hood.
Response Objects
TransactionResponse
- transactionId :
Hex; the Ethereum transaction hash - receipts :
Array; an array of Ethereum transactions receipts, if any. Namely for deposits, withdrawals or forced transfers.
File: src/v1.0.0/3. Concepts/0. Fundamentals/1. Fuel Overview.md
Fuel Overview
This section of the docs goes over the fundamental technical details of the Fuel rollup chain: what it is, how it works, and why it is scalable and secure.
Background
Fuel is an optimistic rollup, a special sidechain architecture that is both trust-minimized and permissionless. Unlike a traditional sidechain, where the safety of user funds is dependent on the honest majority of some set of validators that is disjoint from the main chain's miners, optimistic rollups borrow security directly from the main chain (Ethereum).
The simplified version of optimistic rollups is as follows. Anyone may construct a rollup block off-chain and submit it to Ethereum as calldata (i.e. simple binary data attached to a transaction). Each block submitted in this manner must build upon the previous rollup block (a contract keeps track of block header hashes for the rollup chain) and include a parametrizable bond. If a block is invalid, a compact fraud proof can be submitted and processed on-chain by anyone, which will roll back the rollup chain to the previous block, burn a portion of the bond, and reward the fraud prover with the rest. After a parametrizable timeout, rollup blocks are finalized, and are considered valid (i.e. can no longer be proven fraudulent) and their bond unlocked. Withdrawals are initiated by burning coins on the rollup, then completing the withdrawal after that rollup block has finalized.
Interacting with Fuel
| Fuel node interacting with Ethereum. |
Users send Fuel transactions to a Fuel node, which enters the node's mempool as an unconfirmed transaction. The client combines transactions into a Fuel block, which is then sent to Ethereum as calldata, at which point the Fuel block is confirmed. The node also reads deposit events from Ethereum, adding deposits as spendable state elements on Fuel.
| Fuel normal (above) and forced (below) transactions. |
There are two ways of getting Fuel transactions included in blocks. In the first, under normal operation, the users send transactions to a Fuel node, which then combines them into batches (roots), and finally into a block (which contains several roots). In the second, a user wants to use their own Fuel node to submit a new block, i.e. a forced transaction. In this case, they can create a root and block independently of any other node and submit it to Ethereum directly.
| Entering Fuel. |
Depositing funds into Fuel is done by simply sending them to the Fuel contract (or optionally through a token funnel), at which point the deposits will be added to the state of the Fuel chain.
| Exiting Fuel normally (above) with quickly (below). |
Withdrawing funds from Fuel can be done in two ways: standard and fast. For standard withdrawals, a user simply burns their coins on Fuel, then after a delay, can complete the withdrawal on Ethereum. For fast withdrawals, a user contacts a liquidity provider and atomic swaps funds from Fuel to Ethereum in a matter of minutes.
Architecture: A High-Level View
Fuel is unique among Ethereum rollups in that it uses a UTXO-based transaction system. This allows for parallel transaction validation, and in the future stateless predicate validation for spending conditions, greatly increasing scalability compared to contemporary accounts-based blockchains and rollups.
Each transaction can spend up to eight inputs and produce up to eight outputs. This allows atomic multi-user transactions, e.g. for an on-chain atomic exchange, which also means that non-custodial exchanges can be built on top of Fuel. The deterministic and stateless nature of computing state element (UTXO) IDs produced and consumed by transactions enables long chains and even trees of pre-signed transactions to be constructed.
State elements (UTXOs and deposits) can be colored, i.e. represent different assets. This allows transactions to natively handle both ETH and any ERC-20 token.
Fuel supports a number of spending conditions that allow for powerful implicit expressivity:
- HTLC outputs, which have both a hashlock and a timelock. These enable cross-chain atomic swaps (including between Fuel and Ethereum for instant withdrawals). They can also be used as a signaling mechanism (e.g. to vote in a DAO), and when combined with a chain of pre-signed transactions, for cancellable subscriptions.
- Return outputs, which are unspendable and can include a long binary blob. These can be used to build applications that are executed client-side and use Fuel for timestamping messages cheaply.
The fraud proof scheme used by Fuel is unique in that it does not require state serialization (computing the Merkle root of the state) after every transaction, or even after every block. State serialization is one of the biggest bottlenecks for Ethereum and accounts-based rollups today, and remains a Herculean challenge to optimize implementations around. This, along with parallelizable transaction validation, make Fuel the most scalable rollup in the world.
Fuel's block production model is entirely permissionless, and requires no token (and will never require a token). Anyone may start by submitting a root, a bundle of Fuel transactions, to Ethereum. The root can then be included in a Fuel block by anyone, with a small caveat. The Fuel operator has a short period of time (on the order of minutes, a parameter) where they are the only one that can use this root in a block. This allows the operator to reliably provide a soft confirmation to a recipient that a transaction will be included in the next Fuel block. While this is the default and preferred configuration, using an operator is a system parameter that can be disabled trivially on contract creation if soft confirmations aren't needed.
What To Read Next
For a detailed analysis of Fuel's security guarantees, and how it compares to other scaling technologies, see the next page. (Warning: this document assumes a high level of technical proficiency.)
A more in-depth survey of Fuel's components is given here, with block and transaction architecture descriptions also available.
The canonical serialization scheme is defined here.
Finally, the parameters used for deployments across different networks is found here.
File: src/v1.0.0/3. Concepts/0. Fundamentals/2. Security Analysis.md
Security Analysis
- Common Definitions
- Channels
- Sidechains
- Plasma
- Rollups
- Optimistic vs ZK Rollups
- Rollup Mythbusting
- Conclusion
- See Also
This page will provide a comprehensive overview of the security and scalability properties of Fuel as it compares to other so-called "scaling solutions." It argues that Fuel is uniquely positioned in the trade-off space to be the best scaling architecture today.
Common Definitions
We must first begin by establishing common definitions for desirable properties in a scaling solution. The following definitions are informal, but sufficiently precise to enable proper discussion on the matter. Note that they apply to both "layer-2" constructions as well as base "layer-1" blockchains.
Security Security can be roughly defined as the cost to manipulate history. Example: the security of Proof-of-Work blockchains can be measured as the capex and opex of producing new blocks. The security of Proof-of-Stake blockchains can be measured as the penalty that can be applied if a history re-organization is done.
Trustless Trustlessness has two components: state liveness and state safety. State is live if it can be consumed by its owner in finite time (in other words, it cannot be burned/locked forever), and safe if it cannot be consumed by its non-owners ever (in other words, it cannot be stolen). This is a more precise concept than non-custodial. It is critical to evaluate the trustlessness of a system across both these dimensions, not only state safety. It is also critical to consider worst-case scenarios (i.e. failure modes), not only the optimistic case.
Decentralized blockchains cannot be completely trustless; to illustrate, consider that 51% of miners in a Proof-of-Work blockchain could censor any future transaction (effectively burning censored funds, which allows for indirect theft) or even re-organize the history from genesis to steal all funds. Therefore we measure trustlessness as the assumptions under which a system is trustless. As an example, if we assume 51% of miners are honest, then a blockchain is trustless. We can describe a system that is almost as trustless as another as trust-minimized with respect to the other (e.g. a trust-minimized sidechain is trustless under almost the same assumptions as its main chain).
Permissionless A system is permissionless if it requires no in-system action for a new user to begin participating. It is permissioned otherwise.
Decentralization While commonly (and incorrectly) described as "the number of nodes," decentralization is measured as the cost to run a fully validating node. Actually running a full node at all times is not required, but we must consider the worst-case scenario of having to run a full node. While permissionlessness is a necessary property for decentralization, it does not otherwise provide a metric for measuring decentralization. Note: this definition is actually missing a very important detail, but will suffice for now.
Scalability and Throughput Often a point of contention and confusion, scalability is front-and-center as the most obvious problem for contemporary blockchains. First, throughput is the total transaction throughput of the system, measured in transactions per second. This is often the metric incorrectly advertized as scalability. Scalability is the ratio of total transaction throughput to cost of running a node. Therefore, comparing transactions per second between two systems without simultaneously comparing their costs is comparing apples to oranges. It is trivial to increase throughput by just increasing the cost to run a node, but that is not an increase in scalability.
Note: It is critical to consider worst-case scalability, not average-case or optimistic-case. Blockchains are adversarial environments; if something can be griefed and DoSed it will be griefed and DoSed.
Note: While the naming of the above properties may certainly be debated, that they are desirable properties in any scaling solution, and good metrics for evaluating such solutions, cannot. Trustlessness is especially critical; consider needing to choose between using an unscalable decentralized blockchain and a high-throughput custodial exchange. Neither option is even a choice.
Desiderata
In order to be a "good" layer-2 scaling solution, we need the following properties simultaneously:
- The system must have equal or almost-equal security to the main chain. Non-trivial trade-offs in security are unacceptable from a scaling solution.
- The system must be trust-minimized with respect to Ethereum. Adding meaningful trust assumptions, similar to reducing security, is unacceptable.
- The system should be permissionless. This is not a hard requirement, so long as the system is trustless, but is highly desirable.
- The system must have higher throughput than Ethereum.
- The system should be more scalable than Ethereum. In the context of decentralization (cost of running a full node): the marginal cost of each layer-2 transaction must be lower than the marginal cost the same transaction at layer-1.
- Additional desiderata include:
- No need for hot keys
- Backups support, only private keys are sufficient to recover funds
- No novel cryptography
- Capital efficient
- Low latency
- Generalizability/expressivity
Lastly, similar decentralization to Ethereum: running a node for the layer-2 must not be more expensive than running a full node for Ethereum. Note that this is a deployment parameter, and is not intrinsic to any of the schemes discussed here. To have a common basis for comparison: we want to be able to handle more transactions in our layer-2 than layer-1 Ethereum (with the same or similar trust assumptions), with the same hardware.
Put another way, everyone who could ever be interested in a particular interaction must fully validate that interaction (trust-minimization). If a layer-2 interaction is cheaper to validate than it would have been to validate on layer-1, then the layer-2 system provides scalability. If there are trustless guarantees that a particular interaction does not need to be validated by all users, then this can increase total transaction throughput without proportionally increasing full node costs (i.e. unconditional scalability). If there are no guarantees of this, then there may be conditional scalability, usually by deploying multiple instances of a scaling scheme with different participants.
Channels
Channels are one of the earliest blockchain scaling proposals. Originally taking the form of payment channels, they can be extended to arbitrary computation with state channels.
A fixed set of participants open a channel with an on-chain transaction that defines the rules of the channel. Updates to the channel state are made by unanimous consent: all participants signing a state update, communicated off-chain. Any participant may attempt to close the channel on a particular state, which starts a timeout during which anyone may submit a later state to penalize a fraudulent channel close attempt. If no later state has been shown by the timeout, the channel closes, settling balances based on the closing state.
Metrics:
- Same security as the main chain. Channel updates are not timestamped to Ethereum, so technically it is possible to manipulate the channel history, but only under the same failure mode as a trust-minimization failure (below).
- Trust-minimized, with an additional trust assumption: if the majority of miners censor transactions (which requires attacking the main chain), a proof of fraudulent channel close attempt can be censored, allowing theft of funds. Miners can already indirectly steal funds through censorship of the main chain, so this is not a meaningfully stronger trust assumption.
- It is permissionless to open a channel, but participation in any specific channel is permissioned and fixed. This means e.g. Uniswap cannot be implemented using only channels.
- The total aggregate throughput can be substantially increased by using channels.
- For a single channel, scalability depends on the execution model used, but it will usually be much cheaper to execute a transaction in a channel than it would be to validate it on Ethereum. For multiple channels, there is conditional scalability if the sets of participants is disjoint, which is usually the case since open-participation systems cannot be built with channels in the first place.
- Additional concerns:
- Hot keys are required to take advantage of the low latency of channels.
- Backups are not possible, only copies (backups can recover everything but a delta, while copies are all-or-nothing). Only the latest channel state is useful, and in fact old channel states are worse-than-useless as attempting to close the channel with an old state will result in a penalty. Private key backups are insufficient to ensure funds cannot be lost.
- Capital inefficient: funds must be locked up inside a channel before being used, and cannot be unlocked quickly without cooperation from the specific channel participants.
- Instant finality: as soon as a state update is signed unanimously, it is instantly finalized.
- Generalizable to contracts between a fixed participant set.
In summary, while trust-minimized, channels make enormous sacrifices in terms of capital efficiency, user experience, and open participation to achieve instant finality, which is a feature not possible with blockchains. As such, unless instant finality is a critical requirement, channels are unsuitable as a scaling solution.
Channel Networks
Channel networks link together multiple channels with atomic multi-channel interactions. This allows for interactions between distinct channels, but still does not allow open participation on a shared state (i.e. Uniswap cannot be implemented using a channel network). They are unreliable unless an overwhelming majority of channel network participants are online simultaneously, which facilitates and aggravates a number of attacks.
In summary, channel networks do not diverge meaningfully from channels in the trade-off space.
Sidechains
Sidechains are blockchains that fully validate another blockchain as part of their consensus rules (e.g. a sidechain to Ethereum is a blockchain that validates the Ethereum chain). There are a number of ways sidechain blocks can be produced, including Proof-of-Authority (PoA, a single block producer), a federation, Nakomoto Consensus with Proof-of-Work (PoW), or even a consensus protocol using Proof-of-Stake (PoS) for Sybil resistance.
The two-way peg design of the sidechain (i.e. the mechanism for locking tokens for use on the sidechain, then unlocking them back to the main chain) is critical. A trust-minimized two-way peg is ideal, but is not feasible with plain sidechains.
Metrics:
- Sidechains can have the same security as the main chain by timestamping sidechain block header hashes on the main chain.
- A trust-minimized two-way peg is not possible with plain sidechains, since data withholding is not a uniquely attributable fault. A sidechain that adds features to make its two-way peg trust-minimized ends up becoming a Plasma or a rollup.
- Due to data withholding, sidechains degenerate to being permissioned: a cabal of sidechain block producers can create a block that other sidechain block producers can never build upon.
- Any number of sidechains may be deployed in parallel, so throughput can be greatly increased.
- Sidechains may have any execution model. As such, they can have a different—and more scalable—execution model than the main chain.
- Additional concerns:
- Sidechains are flexible: they can be faster, cheaper, and more expressive than the main chain.
In summary, plain sidechains have a number of nice features, namely their flexibility, but are not trust-minimized. As such, they are unacceptable as a scaling solution.
Plasma
Plasma is a scheme for trust-minimized but permissioned sidechains with exponentially lower on-chain costs. This requires transaction data to be kept entirely off-chain. Different flavors of Plasma (described below) have some common elements. Each Plasma block header is submitted by an operator to a contract on Ethereum (which means Plasma has at least as high a latency as Ethereum). Withdrawals from the Plasma chain (i.e. exits) are involved, and are what allows for (in theory) a trust-minimized two-way peg.
All Plasma variants are permissioned, or degenerate to become permissioned. Even if more than a single operator is used (e.g. using some PoS protocol for Plasma block production) a cabal of operators can produce a block and withhold data, preventing other operators from ever building upon it. In fact, using PoS for Plasma block production is actively detrimental and worse-than-useless, as it adds friction and cost to end users without actually providing any of the nice properties of PoS-based consensus protocols. This is because we cannot recover from PoS consensus failures without hard forks, and it is impossible to trustlessly upgrade a smart contract.
This scaling scheme has mostly been abandoned in favor of rollups for additional reasons that will become apparent soon.
Plasma MVP
Plasma MVP variants were the first concrete flavor of Plasma. A Plasma operator aggregates transactions off-chain, then commits to them in a Plasma block header that is processed on-chain. The UTXO data model is used, but the accounts data model can be used just as well. Exits are initiated on Ethereum against any block. If an exit is invalid (e.g. trying to exit a spent coin), a fraud proof can cancel it; otherwise, the exit is placed in a priority queue based on Plasma block number. After a timeout, the exit can be completed and funds withdrawn from the contract.
However, since Plasma block data can be withheld, the operator can create an invalid Plasma block that gives them all funds, then exit them. Block data is unavailable, so no one can prove the exit is fraudulent. Therefore all users must exit before the timeout expires by exiting their funds from an earlier (available) Plasma block than the malicious operator's exit. This is called a mass exit, an involves putting the Plasma chain's entire state onto Ethereum in a short amount of time.
Metrics:
- Security is borrowed from the main chain, as Plasma blocks are timestamped by Ethereum.
- Plasma MVP variants are only trust-minimized in theory. In practice, mass exits allow an adversary to start an attack with constant cost (posting a single invalid Plasma block header on-chain) that requires unbounded cost to defend against (users must exit the entire state in a short time, and state is essentially unbounded in size). Therefore Plasma MVP variants degenerate in practice to sidechains.
- Plasma MVP is a Plasma and thus permissioned.
- Plasma MVP may have higher throughput depending on the execution model. However, this is purely dependent on the execution model (and has no relation to having a single operator), as all users must fully validate the Plasma MVP chain to ensure their funds are safe.
- Increased scalability is possible with a more efficient execution model than the EVM.
- Additional concerns:
- No need for hot keys.
- Since data is not available on-chain, users must save a copy of their off-chain state. Private key backups are insufficient to ensure funds cannot be lost.
- Capital efficient for both users and the operator.
- Latency cannot be lower than Ethereum, as Plasma blocks are only valid once they are committed to Ethereum.
- Ethereum-style smart contracts are not possible on Plasma. Predicate spending conditions on UTXOs are possible.
In summary, Plasma MVP variants degenerate to sidechains, with funds being trivial to steal by the operator. As such, they are unacceptable as a scaling solution.
Plasma Cash
Plasma Cash variants are the second concrete flavor of Plasma. As with Plasma MVP, a Plasma operator aggregates transactions off-chain, then commits to them in a Plasma block header that is processed on-chain. However, unlike Plasma MVP, a coins data model is used, where each state element is a non-fungible coin (similar to a channel, each coin is uniquely identifiable and cannot be broken up or combined). In order to accept a coin, its entire transaction history must also be downloaded and validated. Exits are initiated on Ethereum against any block. An invalid exit can be disputed or challenged (explained below); otherwise, after a timeout, the exit can be completed and funds withdrawn from the contract.
Since each coins has a distinct owner, each user only needs to check exits that try to withdraw coins they own. There are two distinct exit challenges:
- Non-interactive: the exit is spending an spent coin, and you can prove you owned this coin at a later time, within a timeout.
- Interactive: the exit is spending a coin that you currently own. This is challenged by asking for a valid spend for your currently-owned coin (which should not exist). This can be responded to or not, within a timeout.
In order to always be able to respond to the challenges above, the entire history of a coin (in the form of inclusion and exclusion proofs) must be kept, for each coin. This can quickly become enormous. Attempts to compress this history have led to research dead ends, or involve simply posting transaction data on-chain (i.e. degenerate to a rollup).
Users must verify their coins haven't been exited at least once per timeout to ensure safety. However, due to the interactive challenge, watchtowers cannot be used instead, so users must do this themselves.
Metrics:
- Security is borrowed from the main chain, as Plasma blocks are timestamped by Ethereum.
- Plasma Cash variants are trust-minimized in the same way as channels, with a synchrony assumption on main chain censorship.
- Plasma Cash is a Plasma and thus permissioned.
- Throughput can be substantially higher than Ethereum, as each user only needs to validate the history of coins they own, not the entire Plasma chain.
- Scalability is conditional on the number of coins a user owns. If the user owns sufficient coins, it might be more expensive to download and validate inclusion and exclusion proofs than fully validating Ethereum.
- Additional concerns:
- No need for hot keys.
- Since data is not available on-chain, users must save a copy of their off-chain state. Private key backups are insufficient to ensure funds cannot be lost.
- Capital efficient for both users and the operator.
- Latency cannot be lower than Ethereum, as Plasma blocks are only valid once they are committed to Ethereum.
- Ethereum-style smart contracts are not possible on Plasma. Predicate spending conditions on UTXOs are possible.
In summary, Plasma Cash is trust-minimized, unlike Plasma MVP. However, it is permissioned, requires enormous off-chain cost to transfer coin history, and requires users to store state lest they lose their funds. As such, it is unsuitable as a scaling solution.
Validium
Validium is a Plasma-like protocol that uses validity proofs instead of fraud proofs and a synchrony assumption. In plain English:
- Validity proof: succinct (exponentially cheaper to verify) proof that some computation has a certain result.
- Fraud proof: proof that the result of some computation was incorrect. Requires a synchrony assumption (i.e. a timeout), after which the result is deemed correct if no fraud proof was submitted.
Validium has the benefits over Plasma of not having to worry about the operator producing an invalid block, and being resistant to miner censorship attacks for state safety. This means Validium blocks can be considered immediately finalized, while Plasma blocks are only finalized after the fraud proof timeout has expired. However, the production of validity proofs is expensive and has higher latency, involves novel and hard-to-audit cryptography, and may require a trusted setup.
There is one issue: how do we handle exits? Sure, the operator cannot steal user funds, but they can lock/burn user funds. We must always analyze both state liveness and state safety. In a Validium, as in a Plasma, block data is kept off-chain. As such, users may not have the state at the latest block, which would be required to exit seamlessly. We have a few choices:
- Users initiate an exit on Ethereum, and the operator must process it with a validity proof within some timeout (or provide a proof that the exit is invalid), otherwise the Plasma chain halts. This does not work, as it does not guarantee users can exit since the operator can simply stop processing anything.
- Users initiate an exit on Ethereum, and if it is not processed by the operator within some timeout, it is force-processed, unless proven invalid with a fraud proof. This means a user can get an invalid exit processed, which defeats the point of using validity proofs.
- Users initiate an exit on Ethereum, and if it is not processed by the operator within some timeout, the Validium chain rolls back to a previous block. This means we no longer have immediate finality. If we allow unbounded roll back, then there are no finality guarantees ever (which is unacceptable). If we allow bounded roll back, then that introduces a synchrony assumption which can be used to steal user funds.
Surprisingly, there is no way to ensure state liveness without re-introducing a synchrony assumption, which makes using validity proofs worse than useless!
Note: Synchrony assumptions can often be obfuscated. From the last link above: "The assumption is that we can recover to a state based on a history that took place not more than k batches ago." This
kis a synchrony assumption, and user funds can be stolen if it is violated.
In summary, Validium does not actually exist as a separate cryptoeconomic system than Plasma. The use of validity proofs is superfluous, and increases latency and cost for no concrete benefit.
Rollups
Different flavors of rollups have many common features:
- Anyone may construct a rollup block (permissionless), and post the entire block to an Ethereum smart contract as calldata. The rollup block is not validated in the contract; instead, block validity is ensured by either a validity proof (ZK rollups) or a fraud proof + synchrony assumption (optimistic rollups). Since rollup block data is available, we can guarantee that block production can always be permissionless, unlike Plasma.
- Withdrawals are initiated on the rollup chain, not Ethereum. Because of this, it is critical that anyone can have a transaction included into the rollup chain in bounded time. This means there is no requirement to monitor and challenge individual exits as in Plasma.
- Rollups borrow security from Ethereum, as all rollup blocks are timestamped on Ethereum. As such, the latency of a rollup chain cannot be lower than Ethereum's blocktime.
- Rollups may provide conditional scalability if multiple instances of a rollup (or multiple different rollups) are used, essentially as a form of heterogenous sharding. How much scalability this provides depends entirely on how often cross-rollup (similar to cross-shard) interactions happen.
Note: An alternative to permissionless block production is permissionless withdrawals only (but permissioned block production). This only works by halting the rollup chain if a withdrawal request isn't processed within some timeout. While technically trust-minimized, this is sub-optimal: exiting from the rollup may be very expensive, especially if the rollup has a lot of state.
ZK Rollups
ZK rollups were first concretely proposed by Barry Whitehat, and later refined and simplified.
In ZK rollups, each rollup block posted to the contract must be accompanied by a validity proof (a succinct proof that the block is valid), which is also verified by the contract. Blocks are thus finalized immediately, and withdrawals can be processed in the same Ethereum block. The block posted as calldata only includes sufficient information to apply each state transition (transaction). Witness data (signatures) can be excluded, as the validity proof proves that such signatures exist and are valid.
Users must do the following work to ensure their funds are safe:
- Audit the circuit, which involves novel cryptography.
- If a trusted setup is used, participate in the trusted setup, or assume the trusted setup was not compromised.
- Fully validate Ethereum.
- This includes downloading all state transitions.
- Also includes verifying a validity proof for all rollup blocks. Individual state transitions do not need to be validated.
However, we must analyze the work users must do in the worst case, not the best case. In the worst case, the rollup block producers all go offline and the user must produce their own block. In other words, we must consider both state liveness and state safety, not only state safety.
To produce a new block, a user must re-apply every state transition in the rollup to get the latest state. This is actually the whole point of posting transactions to Ethereum! Notice that while the validity proof proves that a state transition is valid, it does not help at all in applying the state transition (which involves database reads and writes). In other words, the "succinct verification" property of validity proofs only applies to computation with no side effects, not state transitions.
Note: ZK rollups can only handle transfers and simple variants (such as multi-user transfer and swaps). This is because validity proofs operate on circuits rather than programs, and every possible execution path must be proved at all times. The cost of proving generic computation is intractable in practice, and has never been demonstrated to be viable.
As ZK rollups are limited to transfers, the only computation the validity proof can reduce is signature verification. However, this is not even remotely the bottleneck; modern processors can easily verify tens of thousands of ECDSA signatures per second with libsecp256k1. The bottleneck is actually state accesses. In other words, since they do not target the bottleneck, validity proofs in this context provide essentially zero scalability.
Metrics:
- Security is borrowed from the main chain, as rollup blocks are timestamped by Ethereum.
- ZK rollups are trust-minimized under the assumptions: 1) that the novel cryptography is sound, and has been implemented correctly despite immature tooling and few qualified auditors, 2) if a trusted setup is used, that the trusted setup was not compromised.
- Rollups are permissionless.
- ZK rollups will have higher throughput than Ethereum.
- Scalability is increased, as the data and execution model is much simpler than Ethereum.
- Additional concerns:
- No need for hot keys.
- Private key backup is sufficient to recover funds.
- Novel cryptography must be audited by user, or trusted.
- Capital efficient.
- Latency cannot be lower than Ethereum, as rollup blocks are only valid once they are committed to Ethereum. ZK rollup blocks are immediately finalized once committed, however due to proof generation times take several minutes or more to be committed.
- Anything but simple transfer are not possible on ZK rollups.
Optimistic Rollups
In optimistic rollups, each rollup block posted to the contract must be accompanied by the bond. Blocks are finalized after a timeout, at which point the bond can be recovered. The block posted as calldata must include both state transition information and witness data, unlike ZK rollups.
Anyone may post a fraud proof against a non-finalized block, causing the rollup chain to roll back to the previous block (and burning a fraction of the bond while rewarding the rest to the fraud prover). Therefore, on-chain finality requires waiting until the timeout expires. Valid blocks are guaranteed to finalize, however, so anyone may validate the rollup chain to get immediate finality for off-chain applications (e.g. accepting a payment, or performing an atomic swap).
Fungible tokens can be withdrawn immediately with atomic swaps between the rollup and Ethereum using a liquidity provider. Note that non-fungible tokens (NFTs) by definition cannot be liquid, therefore withdrawing NFTs from an optimistic rollup must wait the finalization timeout.
There is no inherent restriction on the execution model of an optimistic rollup, so long as a compact fraud proof can be produced, and verifying a fraud proof can be done within a single Ethereum block (modulo splitting up a fraud proof into several non-interactive steps). However, accounts-based execution models will have much lower scalability than Ethereum today.
This is because fraud proofs for accounts-based execution models require serializing the state after every transaction (or greatly increasing the size of transactions, rendering it strictly inferior to simply using the UTXO data model to begin with). Ethereum used to do this, but no longer does since serializing the state is, as the name implies, a sequential (serial) operation, and in general is a huge bottleneck for Ethereum today. The UTXO data model, in addition to parallel transaction validation, allows for fraud proofs without serializing state, resulting in greatly increased scalability.
Metrics:
- Security is borrowed from the main chain, as rollup blocks are timestamped by Ethereum.
- Optimistic rollup are trust-minimized under the assumptions: 1) 51% of miners are not censoring Ethereum for an extended period of time, and 2) there is at least one interested party that will submit a fraud proof if a fraudulent rollup block is committed (which could be the user themselves).
- Rollups are permissionless.
- Optimistic rollups may have higher throughput than Ethereum, depending on execution model.
- Scalability may be increased over Ethereum, depending on execution model. An EVM-based optimistic rollup will result in zero scalability improvements (and in fact scalability will be much worse). A UTXO-based optimistic rollup will be much more scalable than Ethereum.
- Additional concerns:
- No need for hot keys.
- Private key backup is sufficient to recover funds.
- No novel cryptography.
- Capital efficient.
- Latency cannot be lower than Ethereum, as rollup blocks are only valid once they are committed to Ethereum. Valid optimistic rollup blocks are immediately finalized once committed, if clients validate them immediately (allowing immediate off-chain interactions, such as atomic swaps). Otherwise, the timeout to finality can be quite long (for on-chain interactions).
- Virtually anything can be done in an optimistic rollup, so long as a compact fraud proof can be produced.
Interactive Verification Games
An alternative to fraud proofs is an interactive verification game, in which an interactive game bisects an execution trace until a difference is found. This bounds the maximum cost of a single step of this game to the cost of executing a single step of the trace, unlike fraud proofs where an entire transaction must be executed.
Beyond this distinction, there is little cryptoeconomic difference between optimistic rollups with fraud proofs and interactive verification games. Interactive verification games allow for more flexible execution models (e.g. a single transaction in the rollup that consumes 20 million gas), but result in lower scalability than using a UTXO-based execution model directly (or possibly even worse scalability than Ethereum) since a special virtual machine must be used to execute transactions off-chain. This virtual machine must be designed to have cheap individual operations, but is difficult to optimize for performance.
Note: In theory, an interactive verification game will be more robust against chain congestion attacks that fraud proofs. For example, if a fraud proof costs 9M gas to verify and the block gas limit is 10M, an attacker only needs to fill up blocks up to 1.1M gas to prevent fraud proof transactions from being included. Each step of the interactive verification game is very cheap, so an attacker would need to spam much more to block them from being included. In practice however, the cost of any individual fraud proofs should be intentionally low (less than 20% of the block gas limit or so), so there is no difference.
Note: When using fraud proofs, valid blocks are guaranteed to finalize, as they cannot be proven fraudulent. When using an interactive verification game, a valid block could be rolled back if a challenge on a valid block is intentionally lost. In practice this is not an issue as it requires unanimous participation to throw a challenge.
In summary, if more flexibility is absolutely required (e.g. single huge transactions that cannot be verified with a fraud proof), then an interactive verification game is a good idea. If not, a UTXO-based optimistic rollup with fraud proofs will be more scalable with no downsides.
Optimistic vs ZK Rollups
In terms of trust-minimization, users will have to make orthogonal trust assumptions for ZK rollups and optimistic rollups:
- ZK rollups
- that the novel cryptography is sound, and has been implemented correctly despite immature tooling and few qualified auditors
- if a trusted setup is used, that the trusted setup was not compromised
- optimistic rollups
- 51% of miners are not censoring Ethereum for an extended period of time
- there is at least one interested party that will submit a fraud proof if a fraudulent rollup block is committed (which could be the user themselves)
These assumptions aren't directly comparable, so we cannot really say that one makes stronger assumptions than the other.
It may be tempting to say that miners can steal funds from an optimistic rollup but not a ZK rollup, but that ignores that layer-2 schemes do not exist in vacuo. If a majority of miners can censor Ethereum for an extended period of time, then they can also freeze user funds, which allows them to steal funds indirectly through blackmail.
Along the same vein, a common talking point in favor is ZK rollups is that a user can walk away for a decade and come back to their coins where they left them...but this is actually not true, even with ZK rollups! It is entirely possible that the layer-1 blockchain itself gets re-organized or otherwise mutated such that a user's funds in a ZK rollup–or even on the layer-1 chain–disappear. Decentralized blockchain are nothing more than mechanisms to facilitate social coordination; they are governed by people, not math or code. If a user does not actively watch the layer-1 chain (and the layer-2 with it) at regular intervals, then they cannot defend it–and their funds–from potential adversaries. In other words, regardless of what layer-2 scheme we use, we can assume that the base blockchain provides censorship resistance, because if not we must actively defend it in a timely manner.
In addition, we must consider the following facts:
- In May of 2019, an attacker stole approximately 7000 BTC from the Binance exchange (worth approximately $40M at the time). A bounty could have been put up to incentivize miners to re-organize the Bitcoin blockchain, however was met with community uproar. Miners did not even get the chance to decide on whether to take the bounty or not, because it was never put up.
- In June of 2020, a bounty of at least $5M (at least two separate $2.5M bounties) was put up to re-organize the Ethereum blockchain even a single block. No miner took the bounty.
- In August 2020, approximately $4.5B (4M ETH) was locked up in Decentralized Finance (DeFi) on Ethereum. A vast majority of DeFi protocols rely on primitives with timeouts, and assume censorship-resistance of layer-1 to function correctly. Primitives include: oracles, auctions, liquidations, and more. If miners censored Ethereum for any meaningful length of time, they could destroy the equivalent of the economy of a small nation today.
In terms of scalability, as mentioned in the analysis of ZK rollups, the specific use of validity proofs provides no scalability. We must consider the worst-case failure mode of all the ZK rollup block producers disappearing or otherwise refusing to produce new blocks, at which point users must apply all state transitions themselves, for which the validity proofs do not help at all. Signature verification is not a bottleneck, so it's all a wash between validating signatures and verifying the validity proofs.
Validity proofs are useful for safe low-latency light clients, which fraud proofs cannot provide as their timeout must be long. This allows for immediate withdrawals (especially of NFTs) and composable interaction between a ZK rollup and other systems (similar to an atomic cross-shard transactions). However, since ZK rollups are limited to transfers and simple variations thereof, the practical usability of composable interactions is dubious.
In terms of on-chain efficiency (i.e. the marginal size of each transaction that must be posted as calldata), witness size for optimistic rollups is not a problem long-term. Rather than using per-transaction signatures, BLS aggregate signatures can be used instead, reducing the on-chain footprint of witness data to constant size per block. Interestingly, optimistic rollups are even more gas-efficient than ZK rollups, as the latter require the validity proof to be posted and verified as well.
To wrap up, the assumptions under which both optimistic rollups and ZK rollups are trust-minimized aren't comparable, and thus saying that one is inherently more trust-minimized than the other would be intellectually dishonest. ZK rollups provide zero scalability gains in practice over optimistic rollups, are more limited in their expressivity, are more expensive to use, and have higher latency-to-first-confirmation. On the other hand, optimistic rollups can only support immediate withdrawals of fungible assets, so they are cumbersome to use with NFTs.
In summary, if using NFTs is critical, then ZK rollups are the better choice. For everything else, UTXO-based optimistic rollups are superior: cheaper, lower latency-to-first-confirmation, more expressive, more scalable.
Rollup Mythbusting
Using PoS for block production is a good idea! False. Staking a token is worse-than-useless, as 1) we don't need a native token for security, since we're borrowing security from Ethereum, and 2) we can't implement a proper PoS protocol without hard forks, which cannot be done with a smart contract. A token simply adds costs and friction and provides nothing in return that cannot be done with ETH. Fuel does not have, and will never have, a "utility" token.
Rollup transactions are gas-free! False. Transactions in a rollup still require metering in order to prevent DoS attacks, and to ensure that invalid transactions are fraud-provable. If the execution model of the rollup is the EVM or some other generic VM, then rollup transactions will have to be metered similarly. Fuel currently meters transactions based on size (in bytes), as generic smart contracts are not supported at this time.
Note: The above exclamation is actually half-true. Rollup transactions do not have to pay ETH fees, as the rollup block producer is the only party that must interact with Ethereum to progress the rollup. Therefore rollups can be thought of as meta-transaction systems, allowing users to pay fees in any token the rollup block producers accepts.
Rollup transactions are free (as in "free beer")! False. Each rollup transaction has a marginal cost that must be paid by the block producer as calldata. If a rollup advertizes free transactions, then they are being subsidized from elsewhere–meaning someone else is overpaying fees. Fuel does not advertize transactions as being free.
Rollup transactions are instant! False. Rollup blocks must be posted to Ethereum, therefore a rollup cannot have a lower blocktime than Ethereum. Without a fully-collateralized bond, an adversary can double-spend transactions in an unconfirmed rollup block and guarantee a profit. A system with a fully-collateralized bond would end up as a channel. Fuel makes the explicit choice of not having a bond, since that would increase costs without providing any cryptoeconomic guarantees. Instead, our priority aggregator model allows the Fuel operator to promise an unconfirmed transaction will be included with high reliability, completely non-custodially.
EVM rollups can do {1000, 10000, 100000, A(4,3)} TPS with no downsides! False. EVM execution is the bottleneck for Ethereum. Throughput cannot be increased beyond the current ~15 TPS without a fundamental sacrifice elsewhere. This could be reduced decentralization with increased full node costs, or an additional trust assumption if users simply don't fully validate the rollup. Both are unacceptable in a scaling solution. Fuel uses a unique UTXO-based execution model that is much more scalable than the EVM, allowing for unparalleled throughput without changing decentralization. This does come at a cost for now: no generic smart contracts (which will be implemented in Fuel v2).
ZK rollups scale because you only have to verify a succinct proof! False. Validity proofs are useless for applying state transitions, which is by far the bottleneck in contemporary blockchain architectures.
Conclusion
Fuel's UTXO-based optimistic rollup is uniquely suited as the best scaling solution for Ethereum available today. It is trust-minimized and permissionless, and provides high scalability along with a number of other desireable properties, the particular combination of which is unmatched.
See Also
- Scaling Ethereum with Rollups at EthDenver 2020
- Fuel - A Trustless Scalable Sidechain at EthCC 2020
- Fuel: Scaling Ethereum with Optimistic Rollups at NonCon 2020
File: src/v1.0.0/3. Concepts/0. Fundamentals/3. Block Architecture.md
Block Architecture
Being a UTXO-based blockchain, Fuel is designed with a number of similarities to existing UTXO-based chains, such as Bitcoin. Consult the Bitcoin developer reference for an overview of the minimal block layout required for such a chain.
| Overview of block contents. |
This page describes intuitions behind the top-level data structures, from blocks to leaves in the transaction tree. Discussion on the transaction architecture is relegated to the next page.
Block Header
In any blockchain, block headers commit to a list of transactions, and contain other important metadata. Blocks are block headers plus the explicit list of transactions, but do not exist as an independent data structure in Fuel.
Each block header in Fuel contains the usual suspects: an identifier for the block producer, the previous block header's hash, and the block height (the index of this rollup block in the rollup chain). A few other fields, that diverge from traditional blockchains, are also present.
First, the block number, i.e. the Ethereum block number when this rollup block was submitted to Ethereum, is included. Note that the expressions [block] height and [block] number are used to refer to the rollup block height and the Ethereum block number, respectively.
Note: Including the block number in the header (or, more specifically, requiring an Ethereum block number and Ethereum block hash to match when committing a new rollup block) prevents miners from re-organizing Ethereum and moving a submission transaction to an earlier block number, which could invalidate the timelock conditions of Fuel transactions.
Additionally, the maximum token ID and address ID used through the rollup block (i.e. in this and all previous rollup blocks) is included in the header. The transaction architecture page will discuss these in more details, but for now it is sufficient to know that addresses (token are identified by an address as well) can be registered, which allows them be to referenced by a short numerical ID rather than a full 20-byte address, resulting in smaller (and thus cheaper) transactions.
Note: An attacker can craft a rollup block that includes a transaction with a not-yet-registered ID (i.e. an invalid block). When they see a transaction that proves that block is invalid, they can front-run it with a transaction that registers the ID. Checking for maximum IDs as a block is submitted prevents this griefing vector.
Finally, rather than a single Merkle root of a list of transactions, a list of root header hashes is included. Each root header commits to a list of transactions (in other words, the block header still commits to a list of transactions, though indirectly), and is discussed in the next section.
Fields that are usually seen in layer-1 Proof-of-Work chains, such as a timestamp and a nonce, are omitted. The former is implicit in the form of the Ethereum block number, and the latter is unnecessary since work is not used in the rollup.
Root Header
Each root header commits to a list of transactions directly in two forms, and contains additional metadata that will be discussed shortly. Roots are root headers plus the explicit list of transactions, but do not exist as an independent data structure.
A list of transactions is committed to as both a Merkle root and a simple hash. The simple hash is checked when a new root is submitted on-chain, but not the Merkle root. This avoids the need to Merkleize transactions all the time, and saves on gas costs. If the Merkle root is incorrect, this can be proven with a fraud proof (which uses the simple hash to authenticate the data that will be Merkleized).
Note: Separating block submission into two steps—first submitting one or more roots, then submitting the block header that includes those roots—reduces the costs of a race condition where multiple block producers submit a block at the same time and only one is valid (and the other transactions would fail and cost gas). By making block submission extremely gas-cheap, this race condition is no a longer costly mistake. There is no race condition on submitting roots.
Note: The size bound on the list of transactions is chosen such that Merkleizing any individual list of transactions can be done within the gas limit of a single Ethereum block (with a large margin of safety).
Two transaction fields are hoisted into the root header: the fee token ID and the feerate. By enforcing that all transactions under a single root pay fees in the same token at the same rate, we can avoid needing to repeat these values for every transaction, and instead only specify them once in the root header.
Note: Transactions still sign over the fee token ID and feerate values, to avoid malicious root submissions. The values are implicitly part of the transaction data that is signed. See the next section for implicit vs explicit transaction data.
Transaction Leaves
The transaction architecture can be found in the next page. It is recommended to read it first, before returning to complete this section.
Each transaction leaf (i.e. leaf in the Merkle tree of transactions that are committed to in a root header) contains all the required data to reconstruct the state transition defined by the transaction unambiguously.
- A list of witnesses, which authorize spending a state element in this transaction.
- A list of inputs and metadata (one metadata per input). Each input provides enough information to "unlock" the state element being spent, but does not explicitly identify the state element by ID.
- A list of outputs, which each describe the spending conditions for a new state element.
Metadata is inserted by the root submitter as a means of uniquely identifying the state element being spent at each input. For regular UTXOs this would be the block height, root index, transaction index, then output index. This replaces the traditional UTXO ID (usually: a hash of the transaction and output index) with a much more compact representation.
Note: Metadata is malleable by the root submitter, and is thus not signed over by the transaction sender.
File: src/v1.0.0/3. Concepts/0. Fundamentals/4. Transaction Architecture.md
Transaction Architecture
| A transaction with up to eight inputs and eight outputs. |
A transaction in Fuel specifies a state transition: inputs tell which state elements are consumed and outputs tell which state elements are produced. As with any UTXO-based system, each state element can only be produced and consumed at most once. Modeling the rollup chain's state as a key-value store, this means that keys can never be re-used, and keys commit to values in some way.
Note: Up to eight inputs and outputs are allowed per transaction, to keep the worst-case cost of fraud proofs low while simultaneously enabling a number of applications that require multiple inputs or outputs.
Each input specifies the state element to spend, and unlocks it. For plain UTXOs and deposits, this is the UTXO/deposit ID and a valid digital signature. For HTLC UTXOs, this is the UTXO ID and a preimage (if using the hashlock spending condition) and a valid digital signature.
Each output specifies the new state element to produce (including amounts, token type, etc.), and its spending conditions (recipient address, timelock and hashlock).
Transactions can be validated statelessly, and the state database only needs to be checked for the existence of each consumed state element. In addition, there is no inherent limitation on which accounts sign a transaction, so a transaction can represent an atomic interaction between more than one user (e.g. an on-chain atomic exchange).
The transaction ID (a unique identifier for each transaction) is computed as the EIP-712 hash of the hash of the transaction data without witnesses. We will see in the next section why excluding witness data has some nice properties.
Inputs: Witnesses and Metadata
| Input witness and metadata. |
Each input is linked to a witness (generalization of a digital signature) and metadata.
Witnesses are either a digital signature, or an authorization from a smart contract on Ethereum (e.g. a smart contract wallet can be used to authorize Fuel transactions). In either case, authorization is performed on a transaction ID (as the in previous section, the non-witness transaction data), i.e. signatures are over the transaction ID.
Note: Signing over the transaction ID means that the transaction only has to be hashed once when verified, regardless of the number of inputs or outputs. This avoids the quadratic hashing issue seen in Bitcoin.
Each input specifies a state element to consume (i.e. a UTXO ID, or a deposit ID), which is a 32-byte hash. This value is part of the non-witness transaction data, and is signed over. However, when posting transactions on Ethereum, explicit state element identifiers are stripped and replaced by more compact metadata, which "point" to a unique entry in the ledger (or a deposit). In other words, metadata will point to an exact output in the totally ordered outputs, transactions, roots, and blocks.
Note: Given the one-to-one mapping between ledger entries and state element identifiers, it is easy to show the state element produced by an exact entry with a simple inclusion proof, leveraging the property that computing state element identifiers is a stateless operation.
Note: Extending the above, transaction IDs are not immediately obvious with only the data posted to Ethereum, since state element identifiers are missing. However, they can be proven, and the transaction IDs computed, with inclusion proofs for the entires pointed to by metadata.
Segregated Witnesses
| Segregated witnesses. |
Fuel is designed with a form of segregated witness, where witness data is not bundled with inputs. While the previous sections provided intuition, they did not precisely describe where witnesses are placed in transactions.
Each input actually specifies a witness index that authorizes spending the referenced state element. This means that multiple inputs could use the same witness, which greatly reduces the cost of spending multiple state elements owned by the same account.
Simple Send Example
| A simple send. |
As an example, the above is a simple one-input two-output simple send. One output is returned to the sender as change (there is no special change output type, so it looks like any other output). The other is sent to the recipient.
File: src/v1.0.0/3. Concepts/0. Fundamentals/5. Serialization.md
Fuel EIP-712 Specification
This document describes the Fuel EIP-712 specification, including encodeTypes, and encodeData functions.
EIP-712 improves usability of off-chain message signatures for use on-chain by specifying a standard for hashing and signing of typed (semi-)structured data (not just bytes).
encodeTypes
Address
encodeData:
- require
Output.owner.length > 0 - require
Output.owner.length <= 20 Output.owner =if Output.owner.length == 20 then Output.ownerelse Output.owner[:Output.owner.length]
Input
Input(
uint8 type,
uint8 witnessReference
)InputDeposit
InputDeposit(
uint8 type,
uint8 witnessReference,
address owner
)InputHTLC
InputHTLC(
uint8 type,
uint8 witnessReference,
bytes32 preImage
)Metadata
Metadata(
uint32 blockHeight,
uint8 rootIndex,
uint16 transactionIndex,
uint8 outputIndex
)MetadataDeposit
MetadataDeposit(
uint32 token,
uint32 blockNumber
)Output
Output(
uint8 type,
uint8 tokenLength,
bytes1[] token,
uint8 amountShift,
uint8 amountLength,
uint8[] amount,
uint8 ownerLength,
bytes1[] owner
)encodeData for Output:
- require
amountLength <= 32 - require
amountShift <= 256 - require
((amountLength * 8) + amountShift) <= 256 Output.amount = Output.amount.data[:Output.amountLength] << Output.amountShift
OutputHTLC
OutputHTLC(
uint8 type,
uint8 tokenLength,
bytes1[] token,
uint8 amountShift,
uint8 amountLength,
uint8[] amount,
uint8 ownerLength,
bytes1[] owner,
bytes32 digest,
uint32 expiry,
uint8 returnOwnerLength,
bytes1[] returnOwner
)OutputReturn
OutputReturn(
uint8 type,
uint16 dataLength,
bytes1[] data
)Storage
Base type for stored types. Computes index into storage of 1-, 2-, or 3-element items:
- mapping key for 1-element item is:
keccak256(0, 64) - mapping key for 2-element item is:
keccak256(0, 96) - mapping key for 3-element item is:
keccak256(0, 128)
TransactionProof
TransactionProof(
address blockProducer,
bytes32 previousBlockHash,
uint256 blockHeight,
uint256 blockNumber,
uint256 numTokens,
uint256 numAddresses,
uint16 rootsLength,
bytes32[] roots,
address rootProducer,
bytes32 merkleTreeRoot,
bytes32 commitmentHash,
uint256 rootLength,
uint256 feeToken,
uint256 fee,
uint16 rootIndex,
uint16 merkleProofLength,
bytes32[] merkleProof,
uint8 inputOutputIndex,
uint16 transactionIndex,
uint16 transactionLength,
bytes1[] transaction,
uint8 dataLength,
bytes32[] data,
uint256 signatureFeeToken,
uint256 signatureFee,
address tokenAddress,
address selector
)TransactionLeaf
TransactionLeaf(
uint16 length,
uint8 metadataLength,
bytes8[] metadata,
uint16 witnessesLength,
bytes1[] witnesses,
uint16 inputsLength,
bytes1[] inputs,
uint16 outputsLength,
bytes1[] outputs
)UTXO
UTXO(
bytes32 transactionId,
uint256 outputIndex,
uint256 outputType,
bytes32 owner,
uint256 amount,
uint256 token,
bytes32 digest,
uint256 expiry,
bytes32 returnOwner
)Witness
There is no explicit Witness type, only its sub-types of Signature, Caller, and Producer.
Signature(
uint8 type,
bytes32 r,
bytes32 s,
uint8 v
)Caller(
uint8 type,
address owner,
uint32 blockNumber
)Producer(
uint8 type,
bytes32 hash
)Special notes
`chainID`
Fuel does not mandate the use of a chainID field.
Applications can rely on existing secure chainID approaches, including EIP-155 Simple replay attack protection and EIP-1344 ChainID opcode. EIP-155 includes a chainID in the computation of a transaction's signature, and EIP-1344 defines a chainID smart contract opcode.
An important sysops note is that the use of EIP-1344 in Fuel is fully backwards compatible with all chains which implement EIP-155 chain ID domain separator for transaction signing.
References
Vitalik Buterin, "EIP-155: Simple replay attack protection," Ethereum Improvement Proposals, no. 155, October 2016. [Online serial]. Available: https://eips.ethereum.org/EIPS/eip-155.
Richard Meissner, Bryant Eisenbach, "EIP-1344: ChainID opcode," Ethereum Improvement Proposals, no. 1344, August 2018. [Online serial]. Available: https://eips.ethereum.org/EIPS/eip-1344.
File: src/v1.0.0/3. Concepts/0. Fundamentals/6. System Description Primer.md
System Description Primer
System Components
This page lists the different system components that are documented in the remainder of the Concepts section.
Data Structures
The core data structures are:
- Addresses: a registry for user account addresses.
- Blocks and block headers: top-level data structure.
- Deposits: deposits to Fuel from Ethereum.
- Inputs: transaction inputs.
- Metadata: point to state elements to consume.
- Outputs: transaction outputs.
- Roots and root headers: commit to a batch of transactions.
- Tokens: a registry for token addresses.
- Transactions: transactions.
- Withdrawals: withdrawals from Fuel to Ethereum.
- Witnesses: authorize transactions.
Verifiers
If a fraud proof is submitted to the contract, it first needs to be validated for correct formatting. This is required to ensure that a maliciously-constructed fraud proof can never be processed, otherwise a valid block could be reverted. Parsing fraud proofs is handled by verifiers.
Provers
Once a fraud proof has successfully been parsed by the appropriate verifier modules, it must be processed to prove that fraud occurred (or that the fraud proof is properly formatted but otherwise incorrect). This is handled by provers.
Additional Modules
Outside the core components above, additional optional modules are provided to e.g. improve UX and add more functionality:
- Token funnels: ephemeral contracts that accept token deposits without an
approvestep.
File: src/v1.0.0/3. Concepts/0. Fundamentals/7. Deployment Parameters.md
Deployment Parameters
Constructor Parameters
Here we will describe the Fuel contract's constructor parameters.
| field name | description |
|---|---|
| operator | address; The primary operator of the Fuel rollup. This account receives priority access on including roots in blocks production, however, anyone else may use roots with a delay specified by the submissionDelay parameter. |
| finalizationDelay | uint256; The time to finalization in Ethereum blocks. |
| submissionDelay | uint256; The number of Ethereum blocks a non-operator must wait before using a root in a block. |
| penaltyDelay | uint256; The delay which the operator will face if they commit Fraud, until the time they may come back online as the primary operator. |
| bondSize | uint256; The bond size required for block production, in wei. |
| name | string; The name string used for contract identification and EIP-712 hashing. |
| version | string; The version of the contract used for identification and EIP-712 hashing. |
| chainId | uint256; The Ethereum network chain identifier used for identification, replay attack protection, and EIP-712 hashing. |
| genesis | bytes32; The registered genesis block hash. This property is simply for vanity and can be set to any value as the minimum referenced block is always > 0. |
Deploying in Operated Mode
If you would like to deploy a Fuel chain in operated mode, use non-zero values for operator and submissionDelay.
Deploying in Un-operated Mode
If you would like to deploy Fuel in un-operated mode, where anyone may submit blocks at any time, use zero values for operator and submissionDelay.
Finalization Delays
In any optimistic rollup, finalization delay is a critical security parameter. A longer value provides more robustness against miner censorship attacks, but will result in longer withdrawals.
We recommend 1 week at the minimum, or 2 weeks on the higher end, to protect against both chain congestion attacks and censorship attacks.
Rinkeby Testnet
Contract address: 0x669c63dDD6050838faf496F46EEb7378bff05d77
Name: rinkeby
| field name | value |
|---|---|
| operator | 0x0ea6b5edc8905c85514b3676703f1bfe6ec260ad |
| finalizationDelay | 93046 (one week) |
| submissionDelay | 6646 (one day) |
| penaltyDelay | 0 (zero) |
| bondSize | 100000000000000000 (0.1 ETH) |
| name | Fuel |
| version | 1.0.0 |
| chainId | 4 |
| genesis | 0x0000000000000000000000000000000000000000000000000000000000000000 (empty hash) |
File: src/v1.0.0/3. Concepts/1. Data Structures/Addresses.md
Addresses
The address registry maintains a mapping of address (20 bytes) to address ID (unsigned integer up to and including 19 bytes). Each time a new address is registered, it is assigned the next address ID in sequence.
Note that registering an address is not necessary to send or receive funds on Fuel. It is a purely optional way of reducing transaction sizes (and thus costs).
File: src/v1.0.0/3. Concepts/1. Data Structures/Blocks.md
Blocks
BlockHeader
| name | type | size | description |
|---|---|---|---|
producer |
address |
20 | Address of block proposer committing this block. |
previousBlockHash |
bytes32 |
32 | Previous block's header hash. |
height |
uint256 |
32 | Block height. |
blockNumber |
uint256 |
32 | Ethereum block number when this block is committed. |
numTokens |
uint256 |
32 | Maximum token ID used through this block. |
numAddresses |
uint256 |
32 | Maximum address ID used through this block |
roots.length |
uint16 |
2 | Number of transaction roots. |
roots |
bytes32[] |
32* | List of root header hashes. |
Number of Tokens
Tokens IDs are registered contract-side to allow for more compact transactions. The maximum token ID used in this block and all previous blocks is included in the block header.
This is needed to prevent a griefing attack where a fraudulent rollup block is committed that spends a token ID that is not yet registered. The fraud proof can then be front-run with a transaction that registers the token ID, thereby invalidating the fraud proof.
See: token registry.
Number of Addresses
Address IDs are registered contract-side to allow for more compact transactions. The maximum address ID used in this block and all previous blocks is included in the block header.
This is needed to prevent a griefing attack where a fraudulent rollup block is committed that sends to an address ID that is not yet registered. The fraud proof can then be front-run with a transaction that registers the address ID, thereby invalidating the fraud proof.
See: address registry.
Roots
The block header includes a list of root header hashes, roots. Each individual root header hash is the hash of a root header, which commits to a list of transactions and other important metadata. For more information of why multiple roots are used instead of the more traditional single-transactions-root, see Block Architecture.
The number of root header hashes is upper-bounded by the TRANSACTION_ROOTS_MAX parameter (128).
Committing a New Block Header
When a new block header is produced and committed to Ethereum, the following checks must pass, and are enforced by the smart contract.
- The height
blockHeightmust be greater than0and at most the tip height. - The previous block hash
previousBlockHashmust match the previous block hash atblockHeight. - The number of transaction roots
roots.lengthmust be greater than0and at mostTRANSACTION_ROOTS_MAX. - Every root in
rootsmust exist in the root registry. - If the operator is not in penalty mode (discussed below):
- If the block producer
produceris not the operator:- At least the submission delay parameter Ethereum blocks must have elapsed since the root was submitted.
- If the block producer
The penalty mode becomes active if a successful fraud proof against an operator-produced block is processed. It remains active for the penalty delay parameter Ethereum blocks.
File: src/v1.0.0/3. Concepts/1. Data Structures/Deposits.md
Deposits
The deposit registry uniquely indexes deposits so they can be spent on Fuel.
Deposit
| name | type | size | description |
|---|---|---|---|
owner |
address |
20 | Owner of deposit. |
token |
uint256 |
32 | 4-byte Token ID. |
blockNumber |
uint256 |
32 | Ethereum block number deposit was made. |
value |
uint256 |
32 | Amount of tokens. |
Deposits are uniquely keyed by { owner, token, blockNumber }, with value value.
Since the granularity of deposits is in Ethereum blocks, multiple deposits from the same owner of the same token in the same Ethereum block simply update the deposited amount.
Deposits can be spent no earlier than in a Fuel block committed in the next Ethereum block.
File: src/v1.0.0/3. Concepts/1. Data Structures/Inputs.md
Inputs
Inputs indicate which state elements (usually: UTXOs) are consumed by a transaction. Each state element can be consumed and produced at most once.
InputType
enum InputType : uint8 {
Transfer = 0,
Deposit = 1,
HTLC = 2,
Root = 3,
}Input
| name | type | size | description |
|---|---|---|---|
type |
InputType |
1 | InputType.Transfer or InputType.Root. |
witnessIndex |
uint8 |
1 | Index of witness that authorizes spending the state element. |
There are no spending conditions on plain transfer UTXOs and root UTXOs (collected fees) other than a valid signature from the owner of the UTXO.
InputDeposit
| name | type | size | description |
|---|---|---|---|
type |
InputType |
1 | InputType.Deposit. |
witnessIndex |
uint8 |
1 | Index of witness that authorizes spending the deposit. |
owner |
address |
20 | Deposit owner address. |
There are no spending conditions on deposits other than a valid signature from the owner of the UTXO. The owner of the deposit must be provided as it is needed to compute the unique deposit ID.
InputHTLC
| name | type | size | description |
|---|---|---|---|
type |
InputType |
1 | InputType.HTLC. |
witnessIndex |
uint8 |
1 | Index of witness that authorizes spending the state element. |
preImage |
bytes32 |
32 | Preimage of digest if spending through hashlock. |
HTLC UTXOs require a valid signature from the owner of the UTXO. In addition, if using the hashlock condition, the preimage must be provided. If using the timelock condition, HTLC UTXOs are only spendable after the output's timelock has expired.
File: src/v1.0.0/3. Concepts/1. Data Structures/Metadata.md
Metadata
Each transaction can have metadata attached to it by the block producer. One metadata is attached per input. This allows for fraud proofs without state commitments. (See also: Compact Fraud Proofs for UTXO Chains Without Intermediate State Serialization.)
Note that all metadata objects are exactly 8 bytes.
Metadata
| name | type | size | description |
|---|---|---|---|
blockHeight |
uint32 |
4 | Rollup block height. |
rootIndex |
uint8 |
1 | Root index in block. |
transactionIndex |
uint16 |
2 | Transaction index in root. |
outputIndex |
uint8 |
1 | Output index in transaction. |
Metadata uniquely identifies an exact output in the ledger, first by the block height, then the root, transaction index, and finally output index. Essentially, metadata "references" an exact output. This allows for both compact fraud proofs without serializing the state, leveraging the property that UTXOs are produced and consumed at most once, and smaller transaction payloads than using UTXO IDs directly (a 32-byte hash).
MetadataDeposit
| name | type | size | description |
|---|---|---|---|
token |
uint32 |
4 | Token ID. |
blockNumber |
uint32 |
4 | Ethereum block number. |
Deposit metadata partially uniquely identifies a deposit. The last part of the deposit registry key is the owner address, which can be recovered from the witness (of valid transactions).
File: src/v1.0.0/3. Concepts/1. Data Structures/Outputs.md
Outputs
Outputs define which state elements (usually: UTXOs) are produced by a transaction. Each state element can be produced and consumed at most once.
OutputType
enum OutputType : uint8 {
Transfer = 0,
Withdraw = 1,
HTLC = 2,
Return = 3,
}Output
| name | type | size | description |
|---|---|---|---|
type |
OutputType |
1 | OutputType.Transfer or OutputType.Withdraw. |
token.length |
uint8 |
1 | Next field number of bytes. |
token |
bytes1[] |
1* | The token ID (<= 4 bytes). See: token registry. |
amount.shift |
uint8 |
1 | Amount shifted bytes. See: serialization. |
amount.length |
uint8 |
1 | Next field number of bytes. |
amount |
uint8[] |
1* | Amount of tokens. See: serialization. |
owner.length |
uint8 |
1 | Next field number of bytes. |
owner |
bytes1[] |
1* | Either address ID (<= 19 bytes) or address (= 20 bytes) of recipient. See: address registry. |
A simple transfer between two users on Fuel or a withdrawal from Fuel back to Ethereum.
OutputHTLC
| name | type | size | description |
|---|---|---|---|
type |
OutputType |
1 | OutputType.HTLC. |
token.length |
uint8 |
1 | Next field number of bytes. |
token |
bytes1[] |
1* | The token ID (<= 4 bytes). See: token registry. |
amount.shift |
uint8 |
1 | Amount shifted bytes. See: serialization. |
amount.length |
uint8 |
1 | Next field number of bytes. |
amount |
uint8[] |
1* | Amount of tokens. See: serialization. |
owner.length |
uint8 |
1 | Next field number of bytes. |
owner |
bytes1[] |
1* | Either address ID (<= 19 bytes) or address (= 20 bytes) of recipient. See: address registry. |
digest |
bytes32 |
32 | Hashlock digest. The preimage to this digest must be revealed to spend this output. |
expiry |
uint32 |
4 | Timelock as an Ethereum block number. |
returnOwner.length |
uint8 |
1 | Next field number of bytes. |
returnOwner |
bytes1[] |
1* | Either address ID (<= 19 bytes) or address (= 20 bytes) of owner if HTLC expires. See: address registry. |
The timelock expiry is an absolute Ethereum block number. Any rollup block committed at or after this Ethereum block number can spend the UTXO with the timelock condition.
See also: HTLC explainer on Bitcoin Wiki.
OutputReturn
| name | type | size | description |
|---|---|---|---|
type |
OutputType |
1 | OutputType.Return. |
data.length |
uint16 |
2 | Next field number of bytes. |
data |
bytes1[] |
1* | Data blob. Between 1 and 512 bytes, inclusive. |
Return outputs are provably prunable, i.e. they are not inserted into the state (the UTXO set). They can be used to record and timestamp arbitrary information to the Fuel chain, useful for building applications with client-side execution.
See also: Bitcoin's OP_RETURN.
UTXO
| name | type | size | description |
|---|---|---|---|
transactionId |
bytes32 |
32 | Transaction ID, the hash of the serialized unsigned transaction data. |
outputIndex |
uint256 |
32 | Index of output in transaction. |
outputType |
uint256 |
32 | Output's type. |
owner |
bytes32 |
32 | Owner of UTXO, either a raw address or an address ID. |
amount |
uint256 |
32 | Amount of tokens. |
token |
uint256 |
32 | Token ID. |
digest |
bytes32 |
32 | HTLC only: hashlock. |
expiry |
uint256 |
32 | HTLC only: timelock. |
returnOwner |
bytes32 |
32 | HTLC only: return owner. |
The UTXO ID is simply the hash of the UTXO.
The HTLC-only fields are set to zero (0) if the output is not an HTLC output.
File: src/v1.0.0/3. Concepts/1. Data Structures/Roots.md
Roots
RootHeader
| name | type | size | description |
|---|---|---|---|
rootProducer |
address |
20 | Address of root producer. |
merkleTreeRoot |
bytes32 |
32 | Claimed Merkle root of list of transactions. |
commitmentHash |
bytes32 |
32 | Simple hash of list of transactions. |
rootLength |
uint256 |
32 | Number of roots. |
feeToken |
uint256 |
32 | Token ID of all fees paid in this root. |
fee |
uint256 |
32 | Feerate of all fees paid in this root. |
Each root header commits to a list of transactions. Each root is a root header and a list of transactions (and does not exist as a canonical data structure). Blocks contain one or more root header hashes (i.e. hashes of root headers), and so implicitly commit to a list of transactions. For more information of why multiple roots are used instead of the more traditional single-transactions-root, see Block Architecture.
When committing a new root to the contract, the root header minus the commitmentHash is provided, along with a list of transactions. The commitmentHash is then computed by simply hashing the list of transactions. This means that root headers claim a Merkle root of their transactions, which is assumed to be correct optimistically to save gas. If the claimed Merkle root is incorrect, a fraud proof can be constructed to show this.
The maximum size in bytes of the list of transactions in a single root is the MAX_ROOT_SIZE parameter (57600). This limit is placed to ensure that the transactions can always be Merkleized within the gas limit of a single Ethereum block in a fraud proof.
The maximum number of transactions in a single root is the MAX_TRANSACTIONS_IN_ROOT parameter (2048).
File: src/v1.0.0/3. Concepts/1. Data Structures/Tokens.md
Tokens
The token registry maintains a mapping of token contract address (20 bytes) to token ID (unsigned integer up to and including 4 bytes). Each time a new token is registered, it is assigned the next token ID in sequence.
New tokens deposited to the contract are automatically assigned a new token ID without any additional interaction.
Native ether (ETH) has token ID 0.
File: src/v1.0.0/3. Concepts/1. Data Structures/Transactions.md
Transactions
Transactions consume inputs and produce outputs.
UnsignedTransaction
| name | type | size | description |
|---|---|---|---|
inputs.length |
uint16 |
2 | Next field number of bytes. |
inputs |
bytes1[] |
1* | List of inputs. |
outputs.length |
uint16 |
2 | Next field number of bytes. |
outputs |
bytes1[] |
1* | List of outputs. |
data.length |
uint8 |
1 | Next field number of elements. |
data |
bytes32[] |
32* | Implicit unique identifier of what to spend. |
signatureFeeToken |
uint256 |
32 | Implicit fee token ID. |
signatureFee |
uint256 |
32 | Implicit fee rate. |
Transaction data that is signed off-chain. The unsigned transaction data is serialized then hashed to get a unique transaction ID, which is then signed over to get a witness. Essentially, each witness authorizes an entire transaction. This scheme avoids the quadratic hashing issue present in Bitcoin.
Implicit data (data) is one 32-byte hash per input, and uniquely identifies what is being spent by that input (a UTXO ID or a deposit ID).
The transaction ID is the EIP-712 hash of the hash of the serialized unsigned transaction data.
TransactionLeaf
| name | type | size | description |
|---|---|---|---|
length |
uint16 |
2 | Total length of leaf in bytes. |
metadata.length |
uint8 |
1 | Next field number of elements. |
metadata |
bytes8[] |
8* | List of metadata, one per input. |
witnesses.length |
uint16 |
2 | Next field number of bytes. |
witnesses |
bytes1[] |
1* | List of witnesses. |
inputs.length |
uint16 |
2 | Next field number of bytes. |
inputs |
bytes1[] |
1* | List of inputs. |
outputs.length |
uint16 |
2 | Next field number of bytes. |
outputs |
bytes1[] |
1* | List of outputs. |
A leaf in a transaction Merkle tree, which is committed to in a root.
Note that the implicit data is not included in the leaf, as it can be reconstructed from metadata. Rather, the root producer replaces the larger data (state element) with the smaller metadata (a pointer to an entry in the ledger or a deposit).
Note that the implicit fee token ID and feerate are not included in the leaf. Since the value of those fields must be identical for all transaction under a root, they can be hoisted and declared once at the root header level.
The minimum transaction leaf size in bytes is set by the parameter TRANSACTION_SIZE_MIN (44).
The maximum transaction leaf size in bytes is set by the parameter TRANSACTION_SIZE_MAX (896).
The maximum number of inputs in a single transaction is INPUTS_MAX (8).
The maximum number of outputs in a single transaction is OUTPUTS_MAX (8).
File: src/v1.0.0/3. Concepts/1. Data Structures/Withdrawals.md
Withdrawals
Withdrawal Metadata
| name | padded type | compact type | size | description |
|---|---|---|---|---|
rootIndex |
uint256 |
uint32 |
4 | Index of root in list of roots. |
transactionLeafHash |
bytes32 |
bytes32 |
32 | Hash of transaction leaf in tree rooted at rootIndex. |
outputIndex |
bytes32 |
uint8 |
1 | Index of output in list of outputs of transaction in the transaction leaf. |
Withdrawal metadata is represented in padded form on Ethereum to simplify processing, and compact form on Fuel. The ID of a withdrawal is the hash of the padded-form withdrawal metadata.
A withdrawal registry keep track of whether a withdrawal has been processed or not (a Boolean value), indexed by the rollup block height and unique withdrawal ID. "Processed" withdrawals are completed withdrawals (i.e. the tokens have been withdrawn from the contract).
File: src/v1.0.0/3. Concepts/1. Data Structures/Witness.md
Witness
WitnessType
enum WitnessType : uint8 {
Signature = 0,
Caller = 1,
Producer = 2,
}Witnesses come in different types, depending on the origin of the authorization.
Signature
| name | type | size | description |
|---|---|---|---|
type |
WitnessType |
1 | Witness type. |
r |
bytes32 |
32 | Signature r value. |
s |
bytes32 |
32 | Signature s value. |
v |
uint8 |
1 | Signature v value. |
This is a normal signature that signs over the transaction ID to authorize.
See: EIP-155.
Caller
| name | type | size | description |
|---|---|---|---|
type |
WitnessType |
1 | Witness type. |
owner |
address |
20 | Caller address. |
blockNumber |
uint32 |
4 | Ethereum block number witness was registered at. |
This witness type is for when a contract on Ethereum authorizes a Fuel transaction. One use is smart contract wallets, rather than externally-owned accounts, authorizing transactions.
Caller witnesses in the witness registry are indexed by owner and block number.
Producer
| name | type | size | description |
|---|---|---|---|
type |
WitnessType |
1 | Witness type. |
hash |
bytes32 |
32 | Transaction ID being witnessed. |
Block producers can spend fee outputs (i.e. Root outputs) in blocks they produce without an explicit signature.
File: src/v1.0.0/3. Concepts/2. Verifiers/Block Header.md
Verifier: Block Headers
Validates the block header component of a transaction proof is properly formatted.
- The hash of the block header must be the block hash at
proof.blockHeader.blockHeight. - If checking for finalizable block, then:
- The current block number must be at least the block number
proof.blockHeader.blockNumber.
- The current block number must be at least the block number
- If checking for not-finalizable block, then:
- The current block number must be less than the block number
proof.blockHeader.blockNumber.
- The current block number must be less than the block number
Additional checks must pass if the root header component of a transaction proof is also being validated at the same time.
- The root index
proof.rootIndexmust be less than the number of rootsproof.blockHeader.roots.length. - The hash of the root header
proof.rootHeadermust match the root header at indexproof.rootIndex.
File: src/v1.0.0/3. Concepts/2. Verifiers/Data.md
Verifier: Data
Validates the data component of a transaction proof is properly formatted.
The data field in an unsigned transaction is a list of the unique state elements that are being consumed by the inputs. Since the key for these state elements (a hash) commits to sufficient information to verify their values, we simply need to provide the preimage to each hash.
For each provided state element stateElement and each data data in the list of proof data proof.data:
- The hash of
stateElementmust matchdata.
Note that stateElement can come in different types, depending on the type of the input (i.e. what is being spent).
File: src/v1.0.0/3. Concepts/2. Verifiers/Inputs.md
Verifier: Inputs
Validates the inputs component of a transaction proof is properly formatted.
One additional proof per input in the primary transaction proof proof must be provided, the list inputProofs.
For each input in proof.transaction.inputs and inputProof in inputProofs, indexed by index:
- If input's type is
TransferorHTLC:- The transaction proof
inputProofmust be validated. - The output metadata of
inputProofmust match the metadata of input (i.e. they must be pointing to the same entry in the ledger). - The UTXO ID being produced by the output of
inputProofmust matchproof.data[index].
- The transaction proof
- If input's type is
Deposit:- The deposit object
inputProofmust be validated:- The deposit ID of
inputProofmust matchproof.data[index].
- The deposit ID of
- The deposit object
- If input's type is
Root:- The transaction proof
inputProofmust be validated. - The UTXO ID being produced by the output of
inputProofmust matchproof.data[index].
- The transaction proof
File: src/v1.0.0/3. Concepts/2. Verifiers/Merkle Proof.md
Verifier: Merkle Proofs
Verifies the Merkle proof component of a transaction proof.
- The tree height
proof.merkleProof.lengthmust be less thanMERKLE_TREE_HEIGHT_MAX(256). - Hash transaction leaf
proof.transactionto get current hash as the leaf hash. - For height
ibetween0and ``proof.merkleProof.length - 1`:- Hash the current hash with its sibling from the Merkle proof. If the
ith bit ofproof.transactionIndexis0, the sibling is on the right. If it's1, the sibling is on the left.
- Hash the current hash with its sibling from the Merkle proof. If the
- The resultant Merkle root must match
proof.rootHeader.merkleTreeRoot.
Note: when constructing the tree, the number of nodes at a given level may not be even. In such case, the default value EMPTY_LEAF_HASH (0x0000000000000000000000000000000000000000000000000000000000000000) is used as the sibling hash value.
File: src/v1.0.0/3. Concepts/2. Verifiers/Transaction Proof.md
Verifier: Transaction Proofs
Validates a transaction proof is properly formatted.
- The header of the transaction proof must be valid.
- The Merkle proof of the transaction proof must be valid.
- The length of the transaction leaf
transaction.lengthmust be greater than0.
TransactionProof
| name | type | size | description |
|---|---|---|---|
blockHeader |
BlockHeader | variable | Block header substructure. |
rootHeader |
RootHeader | variable | Root header substructure. |
rootIndex |
uint16 |
2 | Root index. |
merkleProof.length |
uint16 |
2 | Number of nodes in Merkle proof. |
merkleProof |
bytes32[] |
32* | Merkle inclusion proof. |
inputIndex |
uint8 |
1 | Input index. |
outputIndex |
uint8 |
1 | Output index. |
transactionIndex |
uint16 |
2 | Transaction index. |
transaction |
TransactionLeaf | variable | Transaction leaf substructure. |
data.length |
uint8 |
1 | Next field number of elements. |
data |
bytes32[] |
32* | List of state elements being spent. |
signatureFeeToken |
uint256 |
32 | Implicit fee token ID. |
signatureFee |
uint256 |
32 | Implicit fee rate. |
token |
address |
20 | Token address, used for invalid sum. |
returnOwner |
address |
20 | For HTLC outputs only, the return owner. |
File: src/v1.0.0/3. Concepts/2. Verifiers/Witness.md
Verifier: Witnesses
Validates the witness component of a transaction proof is properly formatted.
This is needed to ensure that the preimage to the transaction ID provided in the transaction proof proof (i.e. the unsigned transaction) is actually valid. Data is elided when being posting transactions to Ethereum, so the integrity of the data provided in the proof must be checked.
For the provided state element stateElement, witness = proof.transaction.witnesses[0], and input = proof.transaction.inputs[0]:
- If
witness's type isSignature:- Recover the address with signature
witnessand the transaction ID of computed fromproof.transactionandproof.data[0]as message. - If
input's type isTransferorHTLC:- The recovered address must match
stateElement.owner.
- The recovered address must match
- If
input's type isDeposit:- The recovered address must match
stateElement.owner.
- The recovered address must match
- If
input's type isRoot:- The recovered address must match
proof.blockProducer.
- The recovered address must match
- Recover the address with signature
- If
witness's type isCaller:- The witness registered indexed by
stateElement.ownerandstateElement.blockNumbermust match the transaction ID of computed fromproof.transactionandproof.data[0]
- The witness registered indexed by
- If
witness's type isProducer:- The
stateElement.hashmust match the transaction ID of computed fromproof.transactionandproof.data[0].
- The
Note that stateElement can come in different types, depending on the type of the input (i.e. what is being spent).
File: src/v1.0.0/3. Concepts/3. Provers/Double Spend.md
Prover: Double Spends
Proves that a double spend occurred.
Two valid transaction proofs must be provided.
- The input metadata for each transaction proof must be different.
- The referenced input metadata (i.e. the spent state element) for each transaction proof must match.
File: src/v1.0.0/3. Concepts/3. Provers/Invalid Input.md
Prover: Invalid Inputs
Proves that an input was invalid, i.e. a non-existent state element was spent.
A valid transaction proof, proof, must be provided. In addition, a valid transaction proof block header for the output being spent, inputProof must be provided. Since this output might not exist, the transaction proof does not need to be valid, only the block header.
- Get input
inputasproof.transaction.inputs(proof.inputIndex). - If the input's type is
Deposit:- The deposit with that deposit ID must have amount of
0(i.e. a non-deposit is being spent).
- The deposit with that deposit ID must have amount of
- Get metadata
metadataasproof.transaction.metadata[proof.inputIndex]. - The metadata's root index
metadata.rootIndexmust be at leastinputProof.rootHeader.rootLength(i.e. metadata root index out of bounds). - If the input's type is
TransferorHTLC:- If the input proof is for the rightmost leaf in the root's transaction tree, then the metadata's transaction index
metadata.transactionIndexmust be greater thaninputProof.transactionIndex. It's impossible to provide an inclusion proof for an out-of-bounds transaction, so this check serves as a check for this condition. - The transaction leaf must be non-empty, i.e.
inputProof.transaction.lengthmust be greater than0. - The metadata's output index
metadata.outputIndexmust be at leastinputProof.transaction.outputs.length()(i.e. metadata output index out of bounds). - The output's type must be
WithdraworReturn(i.e. spending a non-spendable output). - If the input's type is
Transfer:- The output's type must not be
Transfer(i.e. type mismatch).
- The output's type must not be
- If the input's type is
HTLC:- The output's type must not be
HTLC(i.e. type mismatch). - If
proof.blockHeader.blockNumberis less thaninputProof.transaction.outputs(inputProof.outputIndex).expiry(i.e. the timelock has not expired):- The hashlock digest
inputProof.transaction.outputs(inputProof.outputIndex).digestmust not match the hash of the preimageinput.preImage(i.e. invalid preimage).
- The hashlock digest
- The output's type must not be
- If the input proof is for the rightmost leaf in the root's transaction tree, then the metadata's transaction index
File: src/v1.0.0/3. Concepts/3. Provers/Invalid Sum.md
Prover: Invalid Sums
Proves that a transaction spent more than allowed (i.e. sum of outputs > sum of inputs + fees).
A valid transaction proof, must be provided. The proof's witness must also be valid. The proof's data must also be valid.
- For the provided token ID, the sum of the inputs must be greater than the sum of the outputs plus fees.
- Fees for a transaction are calculated as the feerate
proof.feetimes the transaction leaf lengthproof.transaction.length.
- Fees for a transaction are calculated as the feerate
File: src/v1.0.0/3. Concepts/3. Provers/Invalid Transaction.md
Prover: Invalid Transactions
Proves that a transaction was malformed. This involves attempting to parse the transaction leaf.
A valid transaction proof, proof, must be provided.
- Get the transaction leaf component of the proof,
leaf = proof.transaction. - Metadata count
leaf.metadata.lengthmust be less than0or more thanINPUTS_MAX. - As a sanity check, witnesses length in bytes
leaf.witnesses.lengthmust be less than0or more thanTRANSACTION_SIZE_MAX. Same for inputs lengthleaf.inputs.lengthand outputs lengthleaf.outputs.length. - The leaf length
leaf.lengthmust be at less thanTRANSACTION_SIZE_MINor more thanTRANSACTION_SIZE_MAX. - The leaf total length
leaf.length+2(since theleaf.lengthfield itself is2bytes) must not match that size of the leaf component of the proof.
The witnesses are parsed:
- For each witness, shift by the length of the witness:
- If the witness type is
Caller:- The transaction ID from the witness registry at the witness ID must be
0(i.e. the witness has not bee registered). - The block number of the witness must be at least the block number in
proof(i.e. the witness was not registered strictly before the block was committed).
- The transaction ID from the witness registry at the witness ID must be
- If the witness type is
- The number of parsed witnesses must be greater than
INPUTS_MAX. - The parsed length of witnesses must not match
leaf.witnesses.length.
The inputs are parsed:
- For each input, shift by the length of the input:
- The input type must be out of bounds.
- The witness reference index
input.witnessIndexmust be out of bounds (i.e. at least the number of witnesses).
- The number of parsed inputs must be greater than
INPUTS_MAX. - The parsed length of inputs must not match
leaf.inputs.length.
The outputs are parsed:
- For each output, shift by the length of the output:
- The output type must be out of bounds.
- If the output type is
Transfer,Withdraw, orHTLC:- The output token and amount fields must be out of bounds.
- The output owner field must be out of bounds.
- If the output type is
HTLC:- the output return owner field must be out of bounds.
- If the output type is
Return:- The data length must be out of bounds.
- The number of parsed outputs must be greater than
OUTPUTS_MAX. - The parsed length of outputs must not match
leaf.outputs.length.
The sizes are parsed:
- The number of metadata elements must not match the number of inputs.
The metadata are parsed:
- For each metadata, shift by the length of the metadata:
- The metadata fields must be out of bounds.
- The parsed length of metadata must not match
leaf.metadata.length.
File: src/v1.0.0/3. Concepts/3. Provers/Invalid Witness.md
Prover: Invalid Witnesses
Proves that a witness was invalid, and that a spend wasn't authorized.
A transaction proof with valid inputs must be provided. In addition, a list of state elements (one for each input of the transaction) must be provided.
- For each input:
- The owner of the input must not match the owner of the corresponding state element.
File: src/v1.0.0/3. Concepts/3. Provers/Malformed Block.md
Prover: Malformed Blocks
Proves that a block was malformed. Given initial checks when committing a new block, the only case is when the Merkle root claimed in a root header is incorrectly computed.
- A valid block header and root header must be provided, along with the original list of transaction leaves committed to in the root header.
- Merkleizing the transaction leaves must not match the
merkleTreeRootfield in the root header.
File: src/v1.0.0/3. Concepts/4. Additional Modules/Funnels.md
Token Funnels
The usual flow for using an ERC-20 token with a contract is to first approve an amount of tokens to transfer, then use transferFrom to actually transfer the tokens. In addition to poor UX, this has a number of issues:
- Approving with a non-zero approved balance allows an adversary to front-run the approval and essential double-transfer tokens.
- Users transferring tokens directly to a contract with
transferlose them, as the contract has no way of processing the transfer. This has resulted in millions of dollars being burned through user error.
We provide a token funnel, a contract that can accept direct transfers magically. Intuitively, the funnel contract's deterministic CREATE2 address itself is a commitment to a deposit to Fuel (generally, the hash of: the sender address, the token type, and the token amount). After transferring tokens to this address, the funnel contract can be deployed at any time to transfer to deposit properly to Fuel, then immediately self-destruct.
Readers familiar with Bitcoin might notice this bears many similarities to Pay-to-Script-Hash.
Specific implementation details can be found here.
File: src/v1.0.0/4. Tools and Applications/0. Fast Withdrawals.md
Fast Withdrawals
Withdrawals must normally wait finalizationDelay Ethereum blocks until the rollup block they are included in is finalized, at which point the funds can be withdrawn. As the finalization delay is supposed to be set quite long to be robust against censorship attacks and short-term chain congestion, this leads to poor UX. We can do better!
Fungible assets (ETH, ERC-20 tokens) can be withdrawn immediately with an atomic swap powered by HTLCs, which is one of the transaction output types Fuel supports.
Liquidity providers can offer up liquidity to users wanting to withdraw quickly, in exchange for a fee. Unlike lending protocols that are popular in DeFi, liquidity providers for fast withdrawals have zero systemic risk if they fully validate Fuel blocks before accepting a swap, as valid blocks are guaranteed to finalize and they can always catch invalid blocks.
File: src/v1.0.0/4. Tools and Applications/1. Token Minting.md
Mass Token Minting
Minting and distributing tokens to a large number of users is a fairly common pattern, especially for community-driven applications and rewards or reputation points. Generally, tokens of this form are distributed in a permissioned manner by a central party, as un-gameable global reputation is non-trivial. However, once the tokens have been distributed they cannot be reclaimed by the issuer.
While it is certainly possible to do this relatively inexpensively by using one-to-many transactions (as Fuel supports up to eight inputs and eight outputs per transactions), we can do even better!
First, let us consider how one would make distributing such tokens cheaper on Ethereum today. There are two easy, concrete methods:
- The issuer pre-signs an authorization to mint a certain number of tokens for a particular user. She then sends the user the pre-signed authorization, which the user can redeem to the token contract for tokens. In this case, the issuer does not pay anything to distribute the token, as gas costs are covered by the user.
- The issuer posts a commitment (e.g. a Merkle root) of new coin balances to the token contract, which is an
O(1)-cost operation. Users can then withdraw their tokens with a Merkle inclusion proof. In this case, the issuer pays a constant cost to distribute tokens, regardless of the number of tokens or users.
These schemes do not work on Fuel v1, as generic smart contracts are not supported. But what if we combined some of the aspects of the two approaches?
| Pre-signed transaction tree. |
The scheme is actually quite simple:
- The issuer mints all the tokens at once on Ethereum, as a batch.
- The tokens are deposited to Fuel.
- The issuer now has a single UTXO (or rather, a deposit) with exactly the number of tokens they want to distribute.
- The issuer recursively pre-signs transactions that split this UTXO into eight outputs that are returned to the same address. This forms a tree of pre-signed transactions, as shown in the diagram above.
- Once the leaves are reached, each output is sent to the appropriate user's address. Users are then given the Merkle branch of pre-signed transactions to the transaction that would send them tokens.
- A user that wishes to redeem their tokens broadcasts the branch of pre-signed transactions to Fuel (or, more specifically, the transactions that haven't yet been included on Fuel from potential previous users that do the same), then spends the appropriate output with a new (potentially fee-paying) transaction.
Note: The issuer does not send the pre-signed transactions to Fuel. Indeed, the pre-signed transactions do not pay any fees and so would not be included barring out-of-bounds negotiation. Rather, the user pays fees for an entire branch of pre-signed transactions using Child-Pays-For-Parent (CPFP).
Readers familiar with Bitcoin's CTV (BIP-119) proposal should find this scheme very similar. However, rather than being enforced with on-chain rules, the commitment to a tree of transactions is simply an off-chain promise.
Note: The threat model for distributing tokens in this manner is not necessarily weaker than that of using a contract on Ethereum to enforce some rules. In this mass token minting scheme, the issuer could double-spend their pre-signed transactions. Notice however that the issuer in any case could simply just not issue the tokens in the first place, as distribution is permissioned, to the same effect.
Note: Equivalent functionality to CTV (BIP-119) is on the roadmap as a future addition with covenants.
File: src/v1.0.0/4. Tools and Applications/2. Subscriptions.md
Subscriptions
Recurring cancellable subscriptions can be accomplished through pre-signed transactions with HTLCs.
For a two-time recurring subscription, Alice can construct transactions as follows:
| Subscription pre-signed transactions. |
Each pre-signed transaction spends the HTLC UTXO of its parent after the timelock expires (which is set to the subscription period, e.g. 1 month for a monthly subscription). The transaction burns a portion of this UTXO (or otherwise pays for the subscription), and outputs the remainder to a new HTLC whose timelock is offset by another period. At any time, Alice can cancel the recurring payments by spending the HTLC UTXO with the hashlock condition, which is set to return the funds to an address she controls. Her wallet stores the preimage, or it can even be deterministically generated from her private key, e.g. by signing over the subscription identifier.
An additional output can be added to pay fees to the block producer, but that is an implementation detail.
File: src/v1.0.0/4. Tools and Applications/3. Non-Custodial Exchanges.md
Non-Custodial Exchanges
| Exchange settlement on Fuel. |
Non-custodial exchanges can be built by anyone on top of Fuel today. The order book is kept, and matching is done, off-chain, with settlement of each order happening on Fuel as a multi-user transaction.
In this example, Alice and Bob each consume one of their UTXOs in a transaction, which swaps the owner of each asset to produce two new UTXOs. (Fees and change are an implementation detail.) Note that since users sign over an entire transaction, there is no possibility of theft. Alice and Bob can each sign the transaction individually, and any time before the transaction is confirmed, can spend their UTXO in a different transaction to cancel the order.
File: src/v1.0.0/5. Future Roadmap/1. Planned Features.md
Roadmap
Fuel v0
Released as testnet at the beginning of 2020 (announcement).
Features:
- UTXO-based optimistic rollup
- Support simple transfers, deposits, and withdrawals
- Burner wallet integration
- Reddit community points transfer and swap demo
Fuel v1
Current release, as testnet.
Features in addition to v0:
- Optimize transaction format, reducing transaction sizes by ~50%
- Contract refactoring in Yul+ for additional gas cost savings
- Support HTLC and Return outputs
- Extra module: Token funnel
- Additional client-side applications:
- mass token minting
- liquidity provider for fast withdrawals
- non-custodial exchange
Fuel v1.5
Future release, expected EOY 2020.
Features in addition to v1:
- BLS signature aggregation (bringing TPS to 1,500-2,000)
- Native NFT support
- Optimistic mass migration
Fuel v2
Future released, expected 2021.
Features in addition to v1.5:
- Predicate scripting
- Covenants
- Configurable state introspection (for Ethereum-style smart contracts)
File: .github/workflows/markdown_link_checker.yml
name: Check Markdown links
on: push
jobs:
markdown-link-check:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v2
- name: Create config file
run: |
echo "{ "replacementPatterns": [ { "pattern": "^/", "replacement": "/github/workspace/" } ] }" > config.json
- name: Check links
uses: gaurav-nelson/[email protected]
with:
config-file: config.json