主页 > token.im官网 > 以太坊账户模型

以太坊账户模型

token.im官网 2023-04-05 06:53:25

与比特币的“UTXO”平衡模型相反,以太坊使用的是“账户”平衡模型。 以太坊丰富了账户的内容,除此之外,它还可以自定义和存储任意数量的数据。 并利用账户数据的可维护性来构建智能合约账户。

事实上以太坊官网查余额,以太坊是为了...

与比特币的“UTXO”平衡模型相反,以太坊使用的是“账户”平衡模型。 以太坊丰富了账户的内容,除此之外,它还可以自定义和存储任意数量的数据。 并利用账户数据的可维护性来构建智能合约账户。

事实上,以太坊是一种为实现智能合约而提炼的账户模型。 数据在逐个帐户的基础上安全隔离。 账户之间的信息相互独立,互不干扰。 结合以太坊虚拟机,可以运行智能合约沙箱。

作为智能合约运行平台,以太坊将账户分为两类:外部账户(EOAs)和合约账户(contract account)。

账户基本概念 外部账户

EOAs - 外部拥有的账户是由拥有私钥的人创建的账户。 它是一个真实世界金融账户的映射,任何人只要拥有该账户的私钥就可以控制该账户。 就像银行卡一样,去ATM取款时,只需要输入正确的密码就可以进行交易。 它也是人类与以太坊账本进行通信的唯一媒介,因为以太坊中的交易需要签名,而签名只能使用私人外部账户进行签名。

外部账户功能总结:

有以太平衡。 可以发送交易,包括转账和执行合​​约代码。 由私钥控制。 没有关联的可执行代码合约账户

具有合约代码的帐户。 由外部账户或合约创建,合约在创建时会自动分配一个账户地址,用于存放合约代码以及合约部署或执行时产生的存储数据。 合约账户地址是通过SHA3哈希算法生成的,不是私钥。 因为没有私钥,任何人都不能将合约账户作为外部账户使用。 只能通过外部账户驱动合约执行合约代码。

下面是合约地址生成算法:Keccak256(rlp([sender,nonce])[12:]

// crypto/crypto.go:74
func CreateAddress(b common.Address, nonce uint64) common.Address {
    data, _ := rlp.EncodeToBytes([]interface{}{b, nonce})
    return common.BytesToAddress(Keccak256(data)[12:])
}

因为合约是由其他账户创建的,创建者地址和交易的随机数进行哈希处理,生成截取的部分。

特别值得注意的是 EIP1014 中提出的另一种生成合约地址的算法。 其目的是为状态通道提供便利,通过确定内容输出一个稳定的合约地址。 在部署合约之前可以知道确切的合约地址。 这里是算法方法:keccak256(0xff++address++salt++keccak256(init_code))[12:]。

// crypto/crypto.go:81
func CreateAddress2(b common.Address, salt [32]byte, inithash []byte) common.Address {
    return common.BytesToAddress(Keccak256([]byte{0xff}, b.Bytes(), salt[:], inithash)[12:])
}

合约账户功能总结:

有以太平衡。 有关联的可执行代码(合约代码)。 合约代码可以被交易或其他合约消息调用。 当执行合约代码时,可以调用其他合约代码。 执行合约代码时,可以进行复杂的计算,可以永久改变合约内部的数据存储。差异比较

综上所述,下表列出了两种账户的区别,合约账户优于外部账户。 然而,外部账户是人们与以太坊沟通的唯一媒介,它们是合约账户的补充。

项目外部账户合约账户

私钥private key

✔️

✖️

余额余额

✔️

✔️

代码代码

✖️

✔️

多重签名

✖️

✔️

控制方式

私钥控制

通过外部账户执行合同

上面列出的多重签名是因为以太坊的外部账户只是一个独立的私钥创建的,不能多重签名。 但是,合约是可编程的,可以编写符合多重签名的逻辑来实现一个支持多重签名的账户。

帐户数据结构

以太坊数据是以账户为单位组织的,账户数据的变化会引起账户状态的变化。 这样一来,以太坊的状态就会发生变化(关于以太坊的状态,后面会写另一篇文章)。

在程序逻辑上,两类账户的数据结构是一致的:

以太坊账户数据结构

对应代码如下:

// core/state/state_object.go:100
type Account struct {
    Nonce    uint64
    Balance  *big.Int
    Root     common.Hash
    CodeHash []byte
}

但在数据存储上略有不同,因为外部账户没有内部存储数据和合约代码,所以外部账户数据中的StateRootHash和CodeHash默认为空值。 一旦为空默认值,则不会存储到对应的物理数据库中。 在程序逻辑中,如果有代码,就是合约账户。 即当CodeHash为空时,该账户为外部账户,否则为合约账户。

以太坊账户数据存储结构

上图为以太坊账户的数据存储结构。 账户内部实际存储的只是关键数据,而合约代码和合约本身的数据是通过相应的哈希值关联起来的。 因为每个账户对象都会存储为以太坊账户树的叶子数据,所以不能太大。

从以太坊作为世界状态(World State)状态机的角度来看,数据关系如下:

以太坊世界态

在密码学中,Nonce 表示仅使用一次的数字。 它通常是一个随机数或伪随机数,以避免重复。 给以太坊账户添加Nonce可以避免重放攻击(在讲解以太坊交易流程时会详细介绍),但不是随机生成的。 账户Nonce初始值为0,后续每次触发账户执行时都会加上Nonce值。 其中一个计数逻辑如下:

// core/state_transition.go:212
st.state.SetNonce(msg.From(), st.state.GetNonce(sender.Address())+1)

这样做的额外好处是,Nonce 一般可以作为账户的交易计数计数器,尤其是合约账户,可以准确记录合约被调用的次数。

而Balance记录的是账户拥有的以太币(ETH)的数量,称为账户余额(注意这里的余额单位是Wei)。 资产转移(Transfer)是添加到一个账户的余额中,从另一个账户中减去。

// core/evm.go:94
func Transfer(db vm.StateDB, sender, recipient common.Address, amount *big.Int) {
    db.SubBalance(sender, amount)
    db.AddBalance(recipient, amount)
}
// core/vm/evm.go:191
if !evm.Context.CanTransfer(evm.StateDB, caller.Address(), value) {
    return nil, gas, ErrInsufficientBalance
}
// core/vm/evm.go:214
evm.Transfer(evm.StateDB, caller.Address(), to.Address(), value)

当然,必须保证转出方有足够的余额。 转账前需要进行 CanTransfer 检查。 如果余额足够,将执行 Transfer 以转移以太币的价值。

账户状态哈希值 StateRoot 是由合约拥有的方法和字段信息组成的 Merkle 压缩前缀树(Merkle Patricia Tree 将在后续独立文章中解释)的根值。 简单来说就是二叉树的根节点值。 合约状态的任何细微变化最终都会导致 StateRoot 发生变化,因此合约状态的变化将反映在账户的 StateRoot 中。

同时可以直接使用StateRoot快速从Leveldb中读取特定的状态数据,比如合约的创建者。 合约中任何地方的数据都可以通过以太坊 API 读取。

下面,我们通过一段示例代码来体验一下以太坊账户数据的存储。

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()))//❽
}

在上面的代码中以太坊官网查余额,我们创建了三个账户并提交到数据库中。 最后打印出当前数据中所有账户的数据信息:

代码执行输出如下:

{
    "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"
            }
        }
    }
}

我们看到这些展示数据,直接对应了我们刚才的所有操作。 只有合约账户才有存储和代码。 外部账户的codeHash与根值相同,为默认值。

本文参与登联社区写作激励计划,好文章好收益,欢迎正在阅读的你加入。