我们常常听到这样的说法:以太坊(Ethereum)和比特币(Bitcoin)在链上数据模型上存在显著差异。比特币基于UTXO模型,而以太坊则采用了账户与状态模型。那么,这种账户与状态模型究竟有何独特之处?本文将深入探讨以太坊中的基本数据单元之一——账户(Account)。
简单来说,以太坊的运行机制是一种基于交易的状态机模型。整个系统由若干账户组成,类似于银行账户体系。状态(State)反映了某一账户在特定时刻的数值。在以太坊中,状态对应的基本数据结构称为StateObject。当StateObject的值发生变化时,我们称之为状态转移。在交易执行过程中,StateObject所包含的数据会因更新、删除或创建而引发状态转移,从而实现从当前状态到另一状态的转变。
在以太坊中,承载StateObject的具体实例就是账户。通常,我们所说的状态即指账户在某一时刻所包含的数据值。
- 账户(Account)对应StateObject
- 状态(State)对应账户的数据值
总体而言,账户是参与链上交易的基本角色,它作为状态机模型中的基本单位,承担了交易发起者和接收者的功能。目前,以太坊中存在两种类型的账户:外部账户(EOA)和合约账户(Contract)。
外部账户与合约账户
外部账户(EOA)
外部账户是由用户直接控制的账户,负责签名并发起交易。用户通过私钥来保证对账户数据的控制权。
合约账户(Contract)
合约账户由外部账户通过交易创建。它保存了不可篡改的图灵完备代码段以及持久化数据变量。这些代码通常使用Solidity等专用编程语言编写,并提供对外部访问的API接口函数。这些接口可通过构造交易或调用本地/第三方节点的RPC服务来触发,构成了当前DApp生态的基础。
合约中的函数用于计算、查询或修改持久化数据。我们常听到“区块链上的数据不可修改”或“不可篡改的智能合约”等说法,实际上这种描述并不完全准确。对于链上智能合约,不可修改的部分是合约中的代码逻辑,而持久化数据变量则可通过调用合约函数进行操作(增删改查),具体方式取决于函数中的代码逻辑。
根据是否修改持久化数据,合约函数可分为只读函数和写函数。如果用户仅需查询数据,可直接通过节点RPC接口调用只读函数,无需构造交易。若需更新数据,则必须构造交易来调用写函数。注意,每个交易每次只能调用一个写函数,复杂逻辑需通过接口化设计实现。
关于合约编写及交易解析的详细内容,我们将在后续文章中进行探讨。
StateObject、账户与合约的实现
在实际代码中,两种账户类型均由stateObject数据结构定义。该结构位于core/state/state_object.go文件中,属于package state。由于结构体名称以小写字母开头,表明它主要用于包内部数据操作,并不对外暴露。
地址(Address)
在stateObject结构体中,前两个成员变量为address和addrHash,分别对应20字节长的地址和32字节长的地址哈希值。在以太坊中,每个账户都有独一无二的地址,作为其身份标识,类似于现实生活中的身份证,与用户信息绑定且不可修改。
数据与状态账户(StateAccount)
成员变量data为types.StateAccount类型,它是包外部API提供的账户相关数据类型。其结构定义在core/types/state_account.go文件中,包含四个变量:
- Nonce:账户发送的交易序号,随交易数量增加而单调递增
- Balance:账户余额,指链上原生代币Ether的数量
- Root:存储层Merkle Patricia Trie的根哈希值(合约账户使用,EOA为空)
- CodeHash:合约代码的哈希值(合约账户使用,EOA为空)
数据库与缓存
db变量保存了StateDB类型的指针,用于调用相关API操作账户对应的stateObject。StateDB本质上是管理stateObject信息的内存数据库,所有账户数据的更新和检索都通过其提供的API完成。
其余成员变量主要用于内存缓存:
trie用于管理合约账户的持久化存储数据code用于缓存合约代码段- 四个Storage类型变量(
originStorage、pendingStorage、dirtyStorage、fakeStorage)用于缓存交易执行期间修改的持久化数据
对于外部账户,由于没有代码字段,这些存储相关变量均为空值。
账户安全机制
我们常听到“用户在区块链中保存的加密资产除了自己外,无人能未经许可转走”的说法。这基本正确,但需注意两点:
- 安全基于当前密码学工具的强度,在量子计算机出现前是足够安全的
- 许多代币(如ERC-20、ERC-721)是合约中的持久化数据,其安全性依赖于合约代码本身
账户生成过程
EOA账户的创建分为本地创建和链上注册两部分。使用钱包工具创建账户时,区块链上并未同步注册信息,链上账户管理通过StateDB模块完成。
本地创建账户的基本步骤:
- 通过随机数生成64位十六进制私钥(256位)
- 使用ECDSA算法和secp256k1曲线计算公钥
- 对公钥进行Keccak-256哈希运算后取后20字节作为地址
签名与验证
以太坊签名校验的核心思想是:基于ECDSA私钥对数据签名,然后通过签名和数据反推公钥,最终验证发送者地址的合法性。这套体系的安全性在于,即使知道公钥也难以推测出私钥。
合约存储机制
合约账户与外部账户的不同之处在于,它额外维护了一个存储层(Storage)用于保存持久化变量数据。在stateObject中声明的四个Storage类型变量就是作为该存储层的内存缓存。
在以太坊中,每个合约都有独立的存储空间,基本组成单元称为槽位(Slot)。每个槽位大小为256位(32字节),寻址空间从0x0到0xFF...FF(32字节)。因此,每个合约最多可保存$2^{256}-1$个槽位,理论存储空间极大。
存储层数据不会直接打包进区块,唯一与链上数据相关的是存储树根哈希值,保存在StateAccount结构的Root变量中。存储层数据的读取和修改通过EVM专用指令OpSload和OpSstore触发。
存储分配策略
根据变量长度性质,持久化变量分为定长和不定长两种。定长变量如uint256,不定长变量包括数组和映射类型。
通过多个示例分析,我们发现:
- 变量对应的存储槽位按其声明顺序分配
- 即使变量未赋值,所需槽位也在合约初始化时分配完毕
- 小于32字节的变量可能共享槽位,但会带来读写放大问题
- 映射类型变量的存储位置使用键值与槽位位置拼接后哈希确定
这种设计既节省了物理空间,又保证了所有参与者能一致性读写合约数据。
常见问题
什么是以太坊账户?
以太坊账户是参与链上交易的基本角色,分为外部账户(EOA)和合约账户两种类型。外部账户由用户私钥控制,负责发起交易;合约账户包含可执行代码和持久化数据,通过交易创建。
合约代码真的不可修改吗?
合约代码一旦部署便不可修改,但合约中的持久化数据可以通过调用函数进行修改。不可修改的是代码逻辑,数据可变性取决于代码中的函数设计。
如何保证合约存储的一致性?
合约存储使用Merkle Patricia Trie结构管理,存储根哈希保存在账户状态中。任何数据修改都会导致根哈希变化,从而确保所有节点对存储状态的一致性验证。
为什么建议使用32字节变量?
因为以太坊存储操作以32字节为单位,使用更小变量可能导致多个变量共享槽位,反而增加读写开销。32字节变量在Gas消耗上往往更经济。
映射类型如何分配存储?
映射类型变量的元素存储位置使用键值与映射变量槽位位置拼接后,经Keccak-256哈希计算确定。这种设计避免了存储冲突,确保元素均匀分布。
外部账户与合约账户有何根本区别?
根本区别在于:外部账户由私钥控制,没有代码字段;合约账户由代码控制,包含可执行代码和存储空间。合约账户不能主动发起交易,只能响应外部账户的调用。
通过深入理解以太坊账户与合约的数据结构,我们可以更好地把握智能合约开发中的存储优化和安全考量,为构建更高效的DApp奠定基础。