区块链底层-账户模型

本文最后更新于:2023年6月19日 晚上

我们采用和以太坊相似的账户模型。

账户数据结构

以太坊数据以账户为单位组织,账户数据的变更引起账户状态变化, 从而引起以太坊状态变化。

1
2
3
4
5
6
7
// core/state/state_object.go:100
type Account struct {
Nonce uint64
Balance *big.Int
Root common.Hash
CodeHash []byte
}
  • 在密码学领域,Nonce 代表一个只使用一次的数字。它往往是一个随机或伪随机数,以避免重复。 以太坊账户中加入 Nonce,可避免重放攻击(不过不是随机产生)。账户 Nonce 起始值是 0,后续每触发一次账户执行则 Nonce 值计加一次。 其中一处的计数逻辑如下:
1
2
// core/state_transition.go:212
st.state.SetNonce(msg.From(), st.state.GetNonce(sender.Address())+1)
  • Balance 则记录该账户所拥有的以太(ETH)数量,称为账户余额.

当然必须保证转账方余额充足,在转移前需要 CanTransfer 检查, 如果余额充足,则执行 Transfer 转移 Value 数量的以太。

  • 账户状态哈希值 StateRoot,是一颗默克尔压缩前缀树(Merkle Patricia Tree)的根值。可以直接利用 StateRoot 从 Leveldb 中快速读取具体的某个状态数据


上图是以太坊账户数据存储结构,账户内部实际只存储关键数据,而合约代码以及合约自身数据则通过对应的哈希值关联。 因为每个账户对象,将作为一个以太坊账户树的一个叶子数据存储, 不能太大。
从以太坊作为一个世界态(World State)状态机视角看数据关系如下:

我们通过一段示例代码,感受下以太坊账户数据存储。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
import(...)
var toAddr =common.HexToAddress
var toHash =common.BytesToHash

func main() {
statadb, _ := state.New(common.Hash{},
state.NewDatabase(rawdb.NewMemoryDatabase()))// ❶

acct1:=toAddr("0x0bB141C2F7d4d12B1D27E62F86254e6ccEd5FF9a")// ❷
acct2:=toAddr("0x77de172A492C40217e48Ebb7EEFf9b2d7dF8151B")

statadb.AddBalance(acct1,big.NewInt(100))
statadb.AddBalance(acct2,big.NewInt(888))

contract:=crypto.CreateAddress(acct1,statadb.GetNonce(acct1))// ❸
statadb.CreateAccount(contract)
statadb.SetCode(contract,[]byte("contract code bytes"))// ❹

statadb.SetNonce(contract,1)
statadb.SetState(contract,toHash([]byte("owner")),toHash(acct1.Bytes()))//❺
statadb.SetState(contract,toHash([]byte("name")),toHash([]byte("ysqi")))

statadb.SetState(contract,toHash([]byte("online")),toHash([]byte{1})
statadb.SetState(contract,toHash([]byte("online")),toHash([]byte{}))//❻

statadb.Commit(true)// ❼
fmt.Println(string(statadb.Dump()))// ❽
}

上面代码中,我们创建了三个账户,并且提交到数据库中。最终打印出当前数据中所有账户的数据信息:

  • ❶ 一行代码涉及多个操作。首先是创建一个内存 KV 数据库,再包装为 stata 数据库实例, 最后利用一个空的 DB 级的 StateRoot,初始化一个以太坊 statadb
  • ❷ 定义两个账户 acct1 和 acct2,并分别添加 100 和 888 到账户余额。
  • ❸ 模拟合约账户的创建过程,由外部账户 acct1 创建合约账户地址,并将此地址载入 statadb。
  • ❹ 在将合约代码加入刚刚创建的合约账户中,在写入合约代码的同时, 会利用 crypto.Keccak256Hash(code)计算合约代码哈希,保留在账户数据中。
  • ❺ 模拟合约执行过程,涉及修改合约状态,新增三项状态数据 owner,name 和 online,分别对应不同值。
  • ❻ 这里和前面不同的是,是给状态 online 赋值为空[]byte{},因为所有状态的默认值均是[]byte{}, 在提交到数据库时,如 Leveldb 认为这些状态无有效值,会从数据库文件中删除此记录。 因此,此操作实际是一个删除状态 online 操作。
  • ❼ 上面所有操作,还都只是发生在 statdb 内存中,并未真正的写入数据库文件。 执行 Commit,才会将关于 statadb 的所有变更更新到数据库文件中。
  • ❽ 一旦提交数据,则可以使用 Dump 命令从数据库中查找此 stata 相关的所有数据,包括所有账户。 并以 JSON 格式返还。这里,我们将返还结果直接打印输出。

代码执行输出结果如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
{
"root": "3a25b0816cf007c0b878ca7a62ba35ee0337fa53703f281c41a791a137519f00",
"accounts": {
"0bb141c2f7d4d12b1d27e62f86254e6cced5ff9a": {
"balance": "100",
"nonce": 0,
"root": "56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421",
"codeHash": "c5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470",
"code": "",
"storage": {}
},
"77de172a492c40217e48ebb7eeff9b2d7df8151b": {
"balance": "888",
"nonce": 0,
"root": "56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421",
"codeHash": "c5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470",
"code": "",
"storage": {}
},
"80580f576731dc1e1dcc53d80b261e228c447cdd": {
"balance": "0",
"nonce": 1,
"root": "1f6d937817f2ac217d8b123c4983c45141e50bd0c358c07f3c19c7b526dd4267",
"codeHash": "c668dac8131a99c411450ba912234439ace20d1cc1084f8e198fee0a334bc592",
"code": "636f6e747261637420636f6465206279746573",
"storage": {
"000000000000000000000000000000000000000000000000000000006e616d65": "8479737169",
"0000000000000000000000000000000000000000000000000000006f776e6572": "940bb141c2f7d4d12b1d27e62f86254e6cced5ff9a"
}
}
}
}

本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!