Overview
We often hear the statement that "one of the biggest differences between Ethereum and Bitcoin is their distinct on-chain data models. Bitcoin operates on a UTXO-based blockchain/ledger system, while Ethereum uses an Account/State model." But what exactly makes this Account/State model different? In this article, we'll explore Ethereum's fundamental data unit - the Account.
Simply put, Ethereum operates as a transaction-based state machine model. The system consists of multiple accounts (Account), similar to bank accounts. The state (State) reflects the value of an account at a specific moment. In Ethereum, the basic data structure corresponding to State is called StateObject. When a StateObject's value changes, we call it a state transition. In Ethereum's operational model, StateObject data updates are triggered by Transaction execution, causing state transitions where the StateObject moves from its current State to another.
In Ethereum, the concrete instance carrying StateObject is the Account. Typically, when we mention State, we're referring to the data value contained in an Account at a particular time.
- Account --> StateObject
- State --> The value/data of the Account
In summary, Accounts are the fundamental actors participating in on-chain transactions (Transaction), serving as basic units in Ethereum's state machine model that initiate and receive transactions. Currently, Ethereum has two types of Accounts: Externally Owned Accounts (EOA) and Contract Accounts (Contract).
EOA
Externally Owned Accounts (EOA) are controlled directly by users, responsible for signing and initiating transactions. Users maintain control over account data through private keys.
Contract Accounts (Contract), or simply contracts, are created by external accounts through Transactions. Contract accounts store immutable, Turing-complete code segments along with persistent data variables. These codes are written in specialized Turing-complete programming languages (like Solidity) and typically provide API interface functions for external access. These API functions can be called by constructing Transactions or through local/third-party node RPC services, forming the foundation of today's DApp ecosystem.
Typically, contract functions are used for computation, querying, or modifying persistent data. We often see descriptions like "once recorded on the blockchain, data cannot be modified" or "immutable smart contracts." Now we understand these broad descriptions are inaccurate. For an on-chain smart contract, the unmodifiable part is the code segment - the function logic cannot be altered. However, persistent data variables in contracts can be modified through function calls (CRUD), depending on the code logic.
Contract functions can be divided into two types: read-only functions and write functions. If users only want to query persistent data without modification, they can call relevant read-only functions directly via RPC interfaces without constructing a Transaction. To update contract data, users must construct a Transaction to call the corresponding write function. Note that each Transaction can only call one write function per contract at a time. For complex logic, write functions need to be modularized to incorporate more operations.
We'll explore contract writing and how Ethereum's execution layer parses Transactions to call corresponding contract functions in later articles.
StateObject, Account, Contract
Overview
In actual code, both account types are defined by the stateObject data structure. The stateObject code is located in core/state/state_object.go, belonging to package state. Below is the structure code of stateObject. Notice it begins with a lowercase letter, indicating it's primarily for internal package operations and not exposed externally.
type stateObject struct {
address common.Address
addrHash common.Hash // hash of ethereum address of the account
data types.StateAccount
db *StateDB
dbErr error
// Write caches.
trie Trie // storage trie, which becomes non-nil on first access
code Code // contract bytecode, which gets set when code is loaded
originStorage Storage // Storage cache of original entries
pendingStorage Storage // Storage entries needing flush to disk
dirtyStorage Storage // Modified entries in current transaction
fakeStorage Storage // Fake storage for debugging
// Cache flags.
dirtyCode bool // true if the code was updated
suicided bool
deleted bool
}Address
The first two variables in stateObject are address and its hash addrHash. address is of type common.Address, and addrHash is common.Hash, representing 20-byte and 32-byte arrays respectively:
const (
HashLength = 32
AddressLength = 20
)
type Address [AddressLength]byte
type Hash [HashLength]byteIn Ethereum, each Account has a unique address. The Address serves as identity information, similar to real-life ID cards, permanently bound to user information and unmodifiable.
Data and StateAccount
The data member variable is of type types.StateAccount. This corresponds to the "State Account" data type provided by Package State for external APIs. The StateAccount structure is defined in "core/types/state_account.go":
type StateAccount struct {
Nonce uint64
Balance *big.Int
Root common.Hash // merkle root of storage trie
CodeHash []byte
}Variables include:
- Nonce: Transaction sequence number
- Balance: Account balance (in Ether)
- Root: Storage layer Merkle Patricia Trie root (for contracts)
- CodeHash: Contract code hash (empty for EOAs)
DB
The db variable holds a pointer to StateDB, facilitating operations on the Account's stateObject. StateDB is essentially an abstracted in-memory database managing stateObject information. All Account data updates and retrievals use StateDB APIs.
Cache
Remaining variables are mainly for memory caching. trie manages persistent variable storage for contracts, code caches contract code segments, and the four Storage fields cache modified persistent data during Transaction execution.
Deep Dive into Account
Who Controls Your Account
The common statement that "users' Cryptocurrency/Token in blockchain systems can't be transferred without approval" is mostly correct. Currently, native tokens like Ether can't be transferred without signed Transactions. However, this security relies on cryptographic tool strength and assumes contract code safety for non-native tokens (like ERC-20).
Account Generation
EOA creation involves local creation and on-chain registration. Local creation generates private/public key pairs using ECDSA algorithms with spec256k1 curve. The process:
- Randomly generate a 32-byte private key
- Derive public key through ECDSA
- Generate address from public key hash (last 20 bytes of Keccak-256)
Signature & Verification
- Uses ECDSA with spec256k1 curve
- Signatures allow verification without exposing private keys
- Security depends on elliptic curve cryptography strength
Deep Dive into Contract
Contract Storage
Contracts maintain independent storage for persistent variables, organized in 256-bit Slots addressed via 32-byte indexes. Storage uses MPT (Merkle Patricia Trie) for indexing.
Storage Example 1
In a simple storage contract with three uint256 variables, Slots are allocated sequentially:
{
"0x290de...3e563": {
"key": "0x00...00",
"value": "0x00...01"
},
"0xb10e2...a0cf6": {
"key": "0x00...01",
"value": "0x00...02"
},
"0x40578...bb5ace": {
"key": "0x00...02",
"value": "0x00...03"
}
}Storage Example 2
Variable declaration order affects Slot allocation - earlier declarations get lower-positioned Slots.
Storage Example 3
Unassigned variables still reserve Slots during initialization.
Storage Example 4
Smaller types (address, bool) may share Slots to save space, but this causes read/write amplification.
Storage Example 5
Map variables use keccak256(key, slotPosition) for storage location to ensure uniform distribution.
FAQ
What's the difference between EOA and Contract Accounts?
EOAs are controlled by private keys and initiate transactions, while Contracts contain executable code and persistent storage.
How are contract storage slots allocated?
Slots are allocated sequentially based on declaration order. Fixed-size variables reserve slots during initialization.
Why do some variables share slots?
Smaller types (<32 bytes) may share slots to save space, though this can impact gas efficiency.
How are map elements stored?
Map elements use keccak256(key, position) for slot addressing to ensure uniform distribution.
Is contract code modifiable?
No, contract code is immutable once deployed, but persistent data variables can be modified through function calls.