Overview
Ethereum is often described as a transaction-based state machine, fundamentally different from Bitcoin's UTXO model. This system comprises multiple accounts, similar to bank accounts, where the state reflects an account's value at any given moment. In Ethereum, the state is represented by a fundamental data structure called a StateObject. When a StateObject's value changes, we refer to it as a state transition. Account data updates, deletions, or creations triggered by transaction executions cause these state transitions, moving the StateObject from one state to another.
In practical terms, the StateObject instance in Ethereum is an Account. The state specifically refers to the data contained within an account at a particular time.
- Account → StateObject
- State → The value/data of the Account
Accounts are the basic entities participating in on-chain transactions, serving as both initiators and receivers. Ethereum currently supports two primary account types: Externally Owned Accounts (EOAs) and Contract Accounts.
Externally Owned Accounts (EOAs)
Externally Owned Accounts are controlled directly by users via private keys. These accounts are responsible for signing and initiating transactions, ensuring users maintain control over their account data through cryptographic security.
Contract Accounts
Contract Accounts, often called smart contracts, are created by EOAs through transactions. These accounts store immutable, Turing-complete code segments and persistent data variables. Typically written in Solidity, this code provides API interface functions accessible by constructing transactions or via node RPC services. This framework forms the foundation of the current DApp ecosystem.
Contract functions enable computations, queries, and modifications of persistent data. Contrary to popular belief, the phrase "data recorded on the blockchain is immutable" is not entirely accurate. While a smart contract's code logic is indeed unchangeable once deployed, its persistent data variables can be modified (Create, Update, Read, Delete) by calling the contract's functions, following the defined logic.
Contract functions are categorized into two types: read-only and write functions. Querying data without modification requires only a call to a read-only function, achievable via direct RPC calls to a node. However, modifying data necessitates constructing a transaction to invoke a write function. Each transaction can call one write function in one contract. Complex on-chain logic requires modularizing write functions to incorporate more operations.
StateObject, Account, and Contract Structures
Core Components
In the Go-Ethereum source code, both account types are defined by the stateObject数据结构, located in core/state/state_object.go. This structure is internal to the state package, indicated by its lowercase naming convention in Go.
The key components of a stateObject include:
- address: A
common.Addresstype representing the account's unique 20-byte Ethereum address. - addrHash: A
common.Hashtype storing the Keccak256 hash of the address. - data: A
types.StateAccountobject holding core account information. - db: A pointer to a
StateDB, an in-memory database abstraction for managingstateObjectdata and interacting with lower-level physical storage. - Cache Fields: Several variables (
trie,code,originStorage,pendingStorage,dirtyStorage,fakeStorage) act as in-memory caches for contract code and storage modifications during transaction execution. These are empty for EOAs.
The StateAccount Structure
The data field in a stateObject is a types.StateAccount struct, defined in core/types/state_account.go. It contains the essential consensus-related account data:
- Nonce: A transaction counter, incrementing with each transaction sent from the account.
- Balance: The account's balance of Ether, the native cryptocurrency.
- Root: The Merkle Patricia Trie (MPT) root hash for the account's storage layer (empty for EOAs).
- CodeHash: The hash of the contract's bytecode (empty for EOAs).
Account Security and Generation
Who Controls Your Account?
The common assertion that "only you control your cryptocurrency assets on the blockchain" is largely accurate. Native tokens like Ether cannot be transferred without a transaction signed by the account's private key. This security relies on the strength of Ethereum's cryptographic algorithms, making it computationally infeasible to derive the private key within a reasonable time frame.
However, this guarantee has nuances. First, it depends on the continued resistance of cryptographic tools (like ECDSA) to attacks, a concern addressed by research into quantum-resistant cryptography. Second, many assets like ERC-20 tokens or NFTs (ERC-721) are not native tokens but data entries within smart contracts. The security of these assets depends entirely on the contract's code security. Vulnerable contracts can be exploited to drain tokens, even if the user's private key remains secure. 👉 Explore secure development strategies
Generating an EOA
Creating an EOA involves a local generation process and an on-chain registration. Wallet tools like MetaMask handle local creation, while on-chain registration is managed by the StateDB module.
Locally, the NewAccount function in accounts/keystore/keystore.go initiates creation. A passphrase encrypts the local keystore file but isn't involved in cryptographic key generation. The core process involves:
- Private Key Generation: Using
ecdsa.GenerateKey, a cryptographically secure random 32-byte (256-bit) private key is generated. - Public Key Derivation: The private key is used to compute its corresponding public key on the secp256k1 elliptic curve.
- Address Calculation: The Ethereum address is derived by taking the last 20 bytes of the Keccak256 hash of the public key.
Signatures and Verification
Digital signatures prove ownership and authorize transactions. The process uses ECDSA with the secp256k1 curve:
- Signing: A message (transaction data) is signed with the private key to produce a signature.
- Verification: The signature and original message can recover the public key. The recovered address is compared to the transaction sender's address for verification. This system's security ensures that public keys cannot feasibly be reverse-engineered to reveal private keys.
Deep Dive into Contract Storage
Storage Layer Structure
A key difference between EOAs and Contracts is the storage layer. Contracts maintain an independent storage space for persistent variables, built from sequential slots. Each slot holds 256 bits (32 bytes) of data. The storage layer is indexed like a massive array, with addresses from 0x00...00 to 0xFF...FF, allowing for a theoretical maximum of 2²⁵⁶ slots.
An MPT (Merkle Patricia Trie) indexes these slots. The root hash of this storage trie is stored in the Root field of the account's StateAccount. Changes to storage affect this root, eventually propagating to the world state root. Storage is accessed and modified by the EVM using dedicated opcodes SLOAD and SSTORE.
Storage Examples and Layout
Solidity variables are stored based on type and declaration order.
- Example 1: Sequential Fixed-length Variables: Three consecutive
uint256variables occupy three sequential slots (0, 1, 2). The slot's position in the storage trie is hashed for indexing (keccak256(slot_position)). - Example 2: Declaration Order Matters: Changing the declaration order changes the slot assignment. Slots are allocated based on the order of declaration, not assignment.
- Example 3: Unassigned Variables: Slot allocation happens at contract initialization, not upon first assignment. An unassigned
uint256still reserves its slot. - Example 4: Packing Smaller Variables: Smaller types (e.g.,
address: 20 bytes,bool: 1 byte) can be packed into a single slot if declared consecutively. However, reading or writing any variable in a packed slot requires reading/writing the entire slot, which has gas cost implications. Using 32-byte types can sometimes be more gas-efficient. - Example 5: Mappings and Dynamic Arrays: Storage layout is more complex. A mapping's value is not stored at the key's hash. Instead, the slot position is calculated as
keccak256(abi.encode(key, slot)), whereslotis the storage slot assigned to the mapping variable itself. This pseudo-randomly distributes data across storage.
Frequently Asked Questions
What is the fundamental difference between Bitcoin's UTXO model and Ethereum's Account/State model?
Bitcoin tracks unspent transaction outputs (UTXOs), which are chunks of currency. Ethereum uses an account-based model where balances are stored directly in account states, similar to bank accounts. This simplifies transaction design and enables smart contract state persistence.
How is my Ethereum account's security guaranteed?
Security is based on cryptographic principles. Your private key, which you control, is required to sign transactions. Without it, spending funds is computationally impossible due to the strength of the Elliptic Curve Digital Signature Algorithm (ECDSA) used. However, assets stored in smart contracts depend on the contract's code security, not just your private key.
What is the difference between an EOA and a contract account?
An EOA is controlled by a private key and has no associated code. It can send transactions. A contract account has associated code (smart contract) and is controlled by its logic. It cannot initiate transactions itself but can execute code in response to transactions sent to it.
How are variables stored inside a smart contract?
Persistent variables are stored in the contract's storage—a key-value store where each key and value is 32 bytes. Fixed-size variables are stored sequentially based on declaration order. Smaller types may be packed into a single slot. Complex types like mappings and arrays use more complex, hashed storage layouts to avoid collisions.
Why might using a uint256 be more gas-efficient than a uint8?
The EVM operates on 32-byte words. If a uint8 is stored alone, it still occupies a full 32-byte slot. If multiple small variables are packed, reading or writing one requires reading and writing the entire 32-byte slot, which can cost more gas than operating on a naturally aligned 32-byte value.
Can a deployed smart contract's code be changed?
No. The bytecode of a deployed contract is immutable and its hash (CodeHash) is fixed. This ensures verifiability and trustlessness. However, the data stored by the contract can be changed by calling its functions, following the immutable rules defined in the code. Patterns like proxy contracts can create upgradeability illusions by delegating calls to changeable logic contracts.