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.
There are two account types in Ethereum:
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.
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:
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.
Figure 2 provides an example of an Ethereum Transaction. Let’s walk through each field one by one:
Based on the above fields, there are three types of transactions:
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:
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:
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.
A node will perform the following actions when executing a transaction in a block:
User.balance > tx.gasLimit * effectiveGasPrice.
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!
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:
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.