Deep Understanding of Ethereum

深入理解以太坊

专有名词

EOA, Contract Accounts, Account state, Account nonce, World state, Transaction, Receipt, Block, Uncle block, Nonce, Gas, Gas price, Gas Price Oracle

Zero Knowledge Proof, EVM, Message, RLP, MPT (Merkle Patricia Tree), Patricia Trie, Merkle Tree, Whisper, Light Ethereum Subprotocol, Swarm, LLL, Sperpent, Mutan, Solidity, EIPs(ERC20, ERC721)

Uncle block

Uncle blocks (or Ommer) are created when two or more miners create blocks at nearly the same time. Only one block can be mined and accepted as canonical on the blockchain. The others are uncle blocks, which are not included but still provide a reward to their miners for the work done.

在比特币网络中,uncle block 是没有奖励的;但在以太坊网络中,uncle block 有奖励,并且一个区块最多能引用 2 个 uncle block;

  1. uncle block reward 是奖励给 uncle block 的矿工的,前提是被确认的区块引用了这个 uncle block
  2. 包含了 uncle block 的区块,会获得一个额外的奖励,这个奖励是 uncle block reward 的 1/32 主要作用是:提高网络安全性,提高矿工的积极性
  1. 以太坊为什么要设置区块的叔块奖励?
  2. There are two uncle rewards

在以太坊升级 PoS 后,uncle block 还有吗?

  1. Block header structure change under the Merge to Proof of Stake?
  2. How The Merge Impacts Ethereum’s Application Layer

总结下来,以太坊从 PoW 升级到 PoS 后,部分和 PoW 共识相关的字段就无用了,但是出于兼容和一致性的考虑,这些字段会被设置默认值,其中 uncle (Ommer) 是其中一项。

In order to minimize disruption to tooling and infrastructure, these fields are set to 0, or their data structure’s equivalent, rather than being entirely removed from the data structure. The full changes to block fields can be found in EIP-3675.

账户模型

以太坊中,账户分为:外部账户(EOAs)和合约账户(contract account)

参考

  1. 账户

EOAs

EOAs-外部账户(external owned accouts)是由人们通过私钥创建的账户。 是真实世界的金融账户的映射,拥有该账户私钥的任何人都可以控制该账户。 如同银行卡,到ATM机取款时只需要密码输入正确即可交易。 这也是人类与以太坊账本沟通的唯一媒介,因为以太坊中的交易需要签名, 而只能使用拥有私有外部账户签名。

外部账户特点总结:

  1. 拥有以太余额。
  2. 能发送交易,包括转账和执行合约代码。
  3. 被私钥控制。
  4. 没有相关的可执行代码。

合约账户

含有合约代码的账户。 被外部账户或者合约创建,合约在创建时被自动分配到一个账户地址, 用于存储合约代码以及合约部署或执行过程中产生的存储数据。 合约账户地址是通过SHA3哈希算法产生,而非私钥。 因无私钥,因此无人可以拿合约账户当做外部账户使用。 只能通过外部账户来驱动合约执行合约代码。

// 合约地址生成算法
// sender: 指交易的发起者的地址
// nonce: 指该交易的随机数
Keccak256(rlp([sender,nonce])[12:])

合约账户特点总结:

  1. 拥有以太余额。
  2. 有相关的可执行代码(合约代码)。
  3. 合约代码能够被交易或者其他合约消息调用。
  4. 合约代码被执行时可再调用其他合约代码。
  5. 合约代码被执行时可执行复杂运算,可永久地改变合约内部的数据存储。

账户抽象

在最近的 EIP 中提出了账户抽象的概念(关于账户抽象看 Account abstraction)

账户数据结构

type Account struct {
    Nonce    uint64
    Balance  *big.Int
    Root     common.Hash
    CodeHash []byte
}

Account Model Account Model

以太坊中的交易

指由一个外部账户转移一定资产给某个账户, 或者发出一个消息指令到某个智能合约

参考资料

  1. 以太坊技术与实现 - 交易
  2. 区块链架构之美 - EVM / Transaction
  3. 深入理解以太坊

Ethereum Node Architecture

在 The Merge 之后,以太坊节点有两部分组成:执行客户端 (EL, Execution Client) 和共识客户端 (CL, Consensus Client):

  • EL 负责交易处理,交易广播,状态管理以及对 EVM 的支持
    • Geth 是一种 EL 的实现,还有其他的实现,比如 Parity
  • CL 负责区块创建,区块广播和共识逻辑
  • EL 和 CL 之间的通信使用的是 Engine API, 基于本地的 JSON-RPC
    • CL 是在 The Merge 之后开始生效的,正式从 PoW 切换到 PoS

他们之间的关系如下图

几个规范需要关注

  1. Execution API
  2. Execution Specs
  3. Engine API
  4. Consensus Specs

EVM

Event logs

TODO

  1. https://ethereum.org/en/developers/docs/evm/

Storage Layouts in EVM

深入理解 EVM 的存储布局非常重要,因为它直接影响到合约的 gas 消耗,以及合约的安全性。

Solidity 中有三种内存类型:storage, memory, calldata

  1. memory 类型的变量和参数,用在函数的内部,只在函数执行期间存在,函数执行完毕后,内存被清空
  2. calldata 类型是在外部调用函数时,传递的参数,只读,不能修改
  3. storage 类型的变量,用来存储合约的状态,永久存在,直到合约被销毁

storage memory layout

每个合约都有自己的存储区域,它是一个可以持久化、读写的内存区域,合约只能访问自己的内存空间,不能访问其他合约的;合约的存储空间被划分为一个个的存储槽,每个槽的大小为 256 位,每个槽都有一个唯一的索引,从 0 开始,每个槽都可以存储一个 256 位的值,也就是说,每个槽都可以存储一个 uint256 类型的值,或者 32 个字节的值;并且 EVM 一次需要直接访问 32 字节的数据,槽的总数是 2^256 大小。

这个抽象非常类似虚拟内存,EVM 会记录每个合约中槽的使用情况,最开始槽是没有初始化的

字节序:大端和小端;在 EVM 中 bytes 和 string 使用的是大端;其他类型用的是小端

  1. 如果一个类型不足 32 bytes,会被填充到 32 bytes;但是填充会浪费内存空间,但是带来了 gas 的节约,因为读写成本更低
  2. 但是在某些情况下,会把紧邻的不足 32 bytes 的变量包装在一起,可以节省内存空间,但是会增加 gas 消耗,因为读写成本更高,需要做更多的位运算

References

Subsections of Deep Understanding of Ethereum

构建以太坊网络

测试网络

搭建私有以太坊私有网络

搭建私有网络

# 1. Clone go-ethereum
git clone https://github.com/ethereum/go-ethereum.git

# 2. Build go-ethereum, make sure you have installed go, version >= 1.19
cd go-ethereum && make all

# 3. Copy geth to $GOPATH/bin
cp build/bin/geth $GOPATH/bin

# 3. Create private network with two nodes
cd $HOME/priveth && mkdir node1 node2

# 4. Create accounts
# On node1
geth --datadir $HOME/priveth/node1 account new
# Password: foobar
# Account1 -> Public address of the key:   0x46eE6c1779eB3421Fc355132D867804894eFB1F9
# Account2 -> Public address of the key:   0x4B3Beee7E6E067080f71336fF118E76E53C63cA3

# Signer address -> 0x3D43ed16b178611C0aBA87a00Dd6b2655aa89287

# On node2
geth --datadir $HOME/priveth/node2 account new
# Public address of the key:   0x69b24ce645c0Bbdfcc0FE4297804b82F888DE8Fe

# 5. Create genesis config
geth --datadir $HOME/priveth/node1 init genesis.json
geth --datadir $HOME/priveth/node2 init genesis.json

# 6. Start bootnode, 在其中一个 node 启动即可
bootnode -genkey boot.key
bootnode -nodekey boot.key -addr :30305

# 6. Start 
# Start node1, locate to $HOME/priveth
geth --datadir node1 --port 30306 --bootnodes 'enode://3988b0cf1d24581e50d1a888e0c9588b1f7403477cf621f46c7add34b2bf786865601c4b6a07924e5f1eed250214192355b5ea5571e5887055e464ccb91dce3e@127.0.0.1:0?discport=30305' --networkid 12345 --unlock 0x3D43ed16b178611C0aBA87a00Dd6b2655aa89287 --password node1/walletpwd.txt --authrpc.port 8551 --http.vhosts=*

# Start node2
geth --datadir node2 --port 30307 --bootnodes 'enode://3988b0cf1d24581e50d1a888e0c9588b1f7403477cf621f46c7add34b2bf786865601c4b6a07924e5f1eed250214192355b5ea5571e5887055e464ccb91dce3e@127.0.0.1:0?discport=30305' --networkid 12345 --unlock 0x69b24ce645c0Bbdfcc0FE4297804b82F888DE8Fe --password node2/walletpwd.txt --authrpc.port 8552

# Attach
geth attach node1/geth.ipc

curl --data '{"jsonrpc":"2.0","method":"eth_getBalance", "params": ["0x3D43ed16b178611C0aBA87a00Dd6b2655aa89287", "latest"], "id":2}' -H "Content-Type: application/json" localhost:8551

genesis.json content

{
  "config": {
    "chainId": 12345,
    "homesteadBlock": 0,
    "eip150Block": 0,
    "eip155Block": 0,
    "eip158Block": 0,
    "byzantiumBlock": 0,
    "constantinopleBlock": 0,
    "petersburgBlock": 0,
    "istanbulBlock": 0,
    "muirGlacierBlock": 0,
    "berlinBlock": 0,
    "londonBlock": 0,
    "arrowGlacierBlock": 0,
    "grayGlacierBlock": 0,
    "clique": {
      "period": 5,
      "epoch": 30000
    }
  },
  "difficulty": "1",
  "gasLimit": "800000000",
  "extradata": "0x00000000000000000000000000000000000000000000000000000000000000003D43ed16b178611C0aBA87a00Dd6b2655aa892870000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
  "alloc": {
    "3D43ed16b178611C0aBA87a00Dd6b2655aa89287": { "balance": "500000" },
    "69b24ce645c0Bbdfcc0FE4297804b82F888DE8Fe": { "balance": "500000" },
    "46eE6c1779eB3421Fc355132D867804894eFB1F9": { "balance": "300000" },
    "4B3Beee7E6E067080f71336fF118E76E53C63cA3": { "balance": "200000" }
  }
}

可以看这个官方教程来理解更多关于私有网络的搭建: Private network