深入解析以太坊账户与合约的数据结构

·

我们常常听到这样的说法:以太坊(Ethereum)和比特币(Bitcoin)在链上数据模型上存在显著差异。比特币基于UTXO模型,而以太坊则采用了账户与状态模型。那么,这种账户与状态模型究竟有何独特之处?本文将深入探讨以太坊中的基本数据单元之一——账户(Account)。

简单来说,以太坊的运行机制是一种基于交易的状态机模型。整个系统由若干账户组成,类似于银行账户体系。状态(State)反映了某一账户在特定时刻的数值。在以太坊中,状态对应的基本数据结构称为StateObject。当StateObject的值发生变化时,我们称之为状态转移。在交易执行过程中,StateObject所包含的数据会因更新、删除或创建而引发状态转移,从而实现从当前状态到另一状态的转变。

在以太坊中,承载StateObject的具体实例就是账户。通常,我们所说的状态即指账户在某一时刻所包含的数据值。

总体而言,账户是参与链上交易的基本角色,它作为状态机模型中的基本单位,承担了交易发起者和接收者的功能。目前,以太坊中存在两种类型的账户:外部账户(EOA)和合约账户(Contract)。

外部账户与合约账户

外部账户(EOA)

外部账户是由用户直接控制的账户,负责签名并发起交易。用户通过私钥来保证对账户数据的控制权。

合约账户(Contract)

合约账户由外部账户通过交易创建。它保存了不可篡改的图灵完备代码段以及持久化数据变量。这些代码通常使用Solidity等专用编程语言编写,并提供对外部访问的API接口函数。这些接口可通过构造交易或调用本地/第三方节点的RPC服务来触发,构成了当前DApp生态的基础。

合约中的函数用于计算、查询或修改持久化数据。我们常听到“区块链上的数据不可修改”或“不可篡改的智能合约”等说法,实际上这种描述并不完全准确。对于链上智能合约,不可修改的部分是合约中的代码逻辑,而持久化数据变量则可通过调用合约函数进行操作(增删改查),具体方式取决于函数中的代码逻辑。

根据是否修改持久化数据,合约函数可分为只读函数写函数。如果用户仅需查询数据,可直接通过节点RPC接口调用只读函数,无需构造交易。若需更新数据,则必须构造交易来调用写函数。注意,每个交易每次只能调用一个写函数,复杂逻辑需通过接口化设计实现。

关于合约编写及交易解析的详细内容,我们将在后续文章中进行探讨。

StateObject、账户与合约的实现

在实际代码中,两种账户类型均由stateObject数据结构定义。该结构位于core/state/state_object.go文件中,属于package state。由于结构体名称以小写字母开头,表明它主要用于包内部数据操作,并不对外暴露。

地址(Address)

stateObject结构体中,前两个成员变量为addressaddrHash,分别对应20字节长的地址和32字节长的地址哈希值。在以太坊中,每个账户都有独一无二的地址,作为其身份标识,类似于现实生活中的身份证,与用户信息绑定且不可修改。

数据与状态账户(StateAccount)

成员变量datatypes.StateAccount类型,它是包外部API提供的账户相关数据类型。其结构定义在core/types/state_account.go文件中,包含四个变量:

数据库与缓存

db变量保存了StateDB类型的指针,用于调用相关API操作账户对应的stateObject。StateDB本质上是管理stateObject信息的内存数据库,所有账户数据的更新和检索都通过其提供的API完成。

其余成员变量主要用于内存缓存:

对于外部账户,由于没有代码字段,这些存储相关变量均为空值。

账户安全机制

我们常听到“用户在区块链中保存的加密资产除了自己外,无人能未经许可转走”的说法。这基本正确,但需注意两点:

  1. 安全基于当前密码学工具的强度,在量子计算机出现前是足够安全的
  2. 许多代币(如ERC-20、ERC-721)是合约中的持久化数据,其安全性依赖于合约代码本身

👉 获取更多安全存储策略

账户生成过程

EOA账户的创建分为本地创建和链上注册两部分。使用钱包工具创建账户时,区块链上并未同步注册信息,链上账户管理通过StateDB模块完成。

本地创建账户的基本步骤:

  1. 通过随机数生成64位十六进制私钥(256位)
  2. 使用ECDSA算法和secp256k1曲线计算公钥
  3. 对公钥进行Keccak-256哈希运算后取后20字节作为地址

签名与验证

以太坊签名校验的核心思想是:基于ECDSA私钥对数据签名,然后通过签名和数据反推公钥,最终验证发送者地址的合法性。这套体系的安全性在于,即使知道公钥也难以推测出私钥。

合约存储机制

合约账户与外部账户的不同之处在于,它额外维护了一个存储层(Storage)用于保存持久化变量数据。在stateObject中声明的四个Storage类型变量就是作为该存储层的内存缓存。

在以太坊中,每个合约都有独立的存储空间,基本组成单元称为槽位(Slot)。每个槽位大小为256位(32字节),寻址空间从0x0到0xFF...FF(32字节)。因此,每个合约最多可保存$2^{256}-1$个槽位,理论存储空间极大。

存储层数据不会直接打包进区块,唯一与链上数据相关的是存储树根哈希值,保存在StateAccount结构的Root变量中。存储层数据的读取和修改通过EVM专用指令OpSload和OpSstore触发。

存储分配策略

根据变量长度性质,持久化变量分为定长和不定长两种。定长变量如uint256,不定长变量包括数组和映射类型。

通过多个示例分析,我们发现:

  1. 变量对应的存储槽位按其声明顺序分配
  2. 即使变量未赋值,所需槽位也在合约初始化时分配完毕
  3. 小于32字节的变量可能共享槽位,但会带来读写放大问题
  4. 映射类型变量的存储位置使用键值与槽位位置拼接后哈希确定

这种设计既节省了物理空间,又保证了所有参与者能一致性读写合约数据。

常见问题

什么是以太坊账户?

以太坊账户是参与链上交易的基本角色,分为外部账户(EOA)和合约账户两种类型。外部账户由用户私钥控制,负责发起交易;合约账户包含可执行代码和持久化数据,通过交易创建。

合约代码真的不可修改吗?

合约代码一旦部署便不可修改,但合约中的持久化数据可以通过调用函数进行修改。不可修改的是代码逻辑,数据可变性取决于代码中的函数设计。

如何保证合约存储的一致性?

合约存储使用Merkle Patricia Trie结构管理,存储根哈希保存在账户状态中。任何数据修改都会导致根哈希变化,从而确保所有节点对存储状态的一致性验证。

为什么建议使用32字节变量?

因为以太坊存储操作以32字节为单位,使用更小变量可能导致多个变量共享槽位,反而增加读写开销。32字节变量在Gas消耗上往往更经济。

映射类型如何分配存储?

映射类型变量的元素存储位置使用键值与映射变量槽位位置拼接后,经Keccak-256哈希计算确定。这种设计避免了存储冲突,确保元素均匀分布。

外部账户与合约账户有何根本区别?

根本区别在于:外部账户由私钥控制,没有代码字段;合约账户由代码控制,包含可执行代码和存储空间。合约账户不能主动发起交易,只能响应外部账户的调用。

通过深入理解以太坊账户与合约的数据结构,我们可以更好地把握智能合约开发中的存储优化和安全考量,为构建更高效的DApp奠定基础。