We focus on how accounts (and smart contract state) is stored in Ethereum’s database. and the structure of transactions to update this database.
Disclaimer: We avoid the nitty gritty of the exact implementation as it will confuse a new reader.
Ethereum accounts
There are two account types in Ethereum:
Externally owned account (EOA). A public key that is owned by the user and a corresponding digital signature can be produced.
Smart contract account. The address is associated with a computer program (smart contract) and storage.
Let’s consider a simple example.
Can you label the following addresses as ‘EOA’ or ‘Smart contract account’?
0x7be8076f4ea4a4ad08075c2508e481d6c946d12b
0x611a4253f5d5742040f3ff72b308f7d2406ecad2
0xdac17f958d2ee523a2206206994597c13d831ec7
0x6e9723433ea94d0f6fb0e3a31a5e7f3f733d4b7d
The answer should be, no.
Without looking up the database, it should be impossible to distinguish the account types.
Ethereum database
The Ethereum database is implemented as an account-based model. Put another way, the database entry for an account can be updated over time. For example, sending a payment from Alice to Bob will update both database entries (i.e., decrement Alice’s balance and increment Bob’s balance).
Figure 1 provides an example of the Ethereum database:
Address. An externally own account or a smart contract address.
Storage. Stores the program’s state. [Smart contracts only]
Code. Stores the bytecode. [Smart contracts only]
Balance. The ETH balance of an account.
Nonce. A form of replay protection, but it differs depending on account type:
EOA: Prevents double-spending by ensuring “Transaction X” is only confirmed once in the blockchain.
Smart contract: A smart contract can deploy another smart contract and this nonce is used when computing its new address.
Database entries may not be deleted. If an externally owned account has spent all its ether, the nonce value must persist to prevent future replay attacks (just in case it later receives more ether). This is bad as a user can pay a single fee and a node has to keep the data around forever (and the problem is exacerbated with smart contracts).
Optimistically clean up the database. The Ethereum Virtual Machine has a self-destruct function to allow a smart contract to delete its own state and code from the database. To reward good behaviour, it offered developers a gas refund in the same transaction. Unfortunately, it will always be remembered as an example of misaligned financial incentives where the benefit appears obviously good at first, but it is later leveraged to attack the system. In this case, the gas refund was used to mint gas tokens when fees were low and then spent when fees were high to bypass the block’s gas limit.
Proposals for eventually removing data? There is work on stateless clients, state rent and state expiry, but they are still far away from production.
Ethereum Transaction
Figure 2 provides an example of an Ethereum Transaction. Let’s walk through each field one by one:
From. The sender’s Ethereum account (we have omitted the digital signature).
To. The receiver’s address which can be a public key or a smart contract (program).
Value. The quantity of ether, the native currency that is sent from the sender to receiver.
Tip. A bounty to entice the block producer to include this transaction in their block.
Fee cap. The maximum transaction fee the user will pay when this transaction is included in a block. A portion of this fee is “burnt”.
Gas limit. The total quantity of computation the user is willing to pay for in this transaction.
Nonce. Replay protection for the transaction (every new transaction must increment it by one).
Access list. Storage slots in the database that this transaction will access (optional).
Data payload. A payload that can be ignored, instructions for a contract interaction or the bytecode to deploy a smart contract.
Based on the above fields, there are three types of transactions:
Value transfer. Send ether from one account to another.
Invocation transaction. Invoke a function in a smart contract and optionally send ether to it.
Deployment transaction. Deploy and instantiate the bytecode for a new smart contract.
Computation on Ethereum
The transaction format is designed to interact with a stateful database. A user can specify the contract address, function signature, function arguments and the total execution they are willing to pay for on the network. This computational model brings up some novel concepts:
Metering computation (gas). Ethereum has a concept called gas which provides a standard way for nodes on the network to meter computation. It associates a gas cost with every operation code (opcode) in the virtual machine. For example, adding two numbers will cost 3 gas and storing a value in the database is 20k gas. Standardising the gas cost of computation is a non-trivial problem as witnessed by spam attacks in 2016-17 due to mispriced gas for some opcodes.
Limiting a transaction’s computation. There are two reasons to limit the computation within a single transaction:
Mindful of global resources. We need a way to prevent a single user spending $1 and hogging all resources on the network.
Decentralization to protect database integrity. Allow a target percentage of the world’s population to verify the validity of blocks and transaction’s in real-time.
In both cases, the solution adopted by the community is to place an artificial limit on the computational capacity and transaction throughput of the network. The vision is to create a marketplace for blockspace and it is achieved in two ways:
Knapsack problem. Given a list of pending transactions, the block producer must pick which transactions will be included in this block and which transactions will be left over for the next block.
Fee market. An auction mechanism which allows transaction signers to compete, via a network fee, to entice the block producer to include their transactions before other pending transactions.
Together, a user should only propose a transaction if there is a financial or meaningful reason to do so. It also creates a floor price for an attacker to hog all resources on the network. In the early days, an attacker could hog all resources on Ethereum for a few thousand dollars. Today, especially thanks to EIP-1559, the financial cost is potentially millions of dollars for an impactful short-term congestion attack.
Updating the database
A node will perform the following actions when executing a transaction in a block:
Check balance. Does the transaction signer have a sufficient balance to cover the cost of the entire transaction:
User.balance > tx.gasLimit * effectiveGasPrice.
Execute against current database state. The node processes the transaction and fetches data from the database as necessary. There are three outcomes for a transaction (as highlighted in Figure 3):
Success. The execution completes. The database is updated to reflect the execution’s impact and the user is charged based on the gas used.
Failed, out of gas. The execution did not complete within the proposed gas limit. All execution is reverted.
Failed, condition not satisfied. The smart contract can implement pre-conditions and post-conditions that should be satisfied for the execution to be considered successful. The execution is reverted if one condition fails.
Update the database. The database is updated to reflect the execution’s impact which includes:
Charge the user. The user’s account is updated and their ether balance is reduced to cover the cost of this transaction.
Account updates. A smart contract can change the state of other accounts (and smart contracts). This update is only applied if the transaction’s execution is a success.
Failed transactions are a necessary evil on Ethereum. The goal is to prevent a denial of service attack on nodes of the network by requiring all execution to be paid for. After all, nodes on the peer-to-peer network have to replicate the execution in full before deciding whether it is successful or a failure.
Horrendous user experience. No one wants to pay hundreds of dollars on a failed execution. It feels as awful as it sounds!
Transaction receipts
A transaction receipt is produced after the execution is executed and a list of its fields are provided in Figure 4. While a transaction receipt is not explicitly stored in the database, there is a commitment to all receipts in the block header. This allows a light client to verify a transaction receipt was indeed produced in a block before accepting its content.
Typical usage of a receipt includes:
Effective gas price. It identifies the final gas price paid by the user for the transaction alongside the gas charged for.
New smart contract address. It identifies the new address of a smart contract if it was deployed by this transaction.
Transaction tracking. It identifies value transfer across accounts in the transaction.
Logs. A smart contract can emit information to the external world. This makes it easier to track the activity of a transaction’s execution.
How does a smart contract execute?
We will cover this in more detail in a future article! If you want a teaser, you can check out the bitcoin article for how a script executes. Replicating the animation here … is impossible :)
But let’s finish with one final key insight:
Deterministic, but unpredictable execution: The transaction commits to what should be executed and the execution is deterministic, but what is eventually executed is not always predictable at the time of signing. This may sound very strange at first, but we find out the consequences in the building and breaking smart contract material.