Ethereum

Table of Contents

1. Ethereum 简介

以太坊(Ethereum)是一个开源的、有智能合约功能的区块链平台。

参考:
Ethereum Documentation: http://www.ethdocs.org/en/latest/
以太坊区块链浏览器: https://etherscan.io/

1.1. Ethereum 的各个阶段

Ethereum 的各个阶段及其发布日期如表 1 所示。

Table 1: Ethereum Milestones
Version Code Name Release date
0 Olympic May, 2015
1 Frontier 30 July 2015
2 Homestead 14 March 2016
3 Metropolis 16 October 2017
4 Serenity N/A

以太坊的前三个阶段(Frontier 前沿、Homestead 家园、Metropolis 大都会)采用的是工作量证明(POW)机制。以太坊的第四个阶段 (Serenity,也称为 Ethereum 2.0)将采用 Casper,它是一种权益证明(POS)机制。

1.2. Ethereum 规范及其实现

Ethereum 规范又称为“黄皮书”,参见:https://github.com/ethereum/yellowpaper

2 列出了 Ethereum 规范的一些实现。

Table 2: Ethereum clients
Client Language Developers Latest release
go-ethereum Go Ethereum Foundation go-ethereum-v1.4.18
Parity Rust Ethcore Parity-v1.4.0
cpp-ethereum C++ Ethereum Foundation cpp-ethereum-v1.3.0
pyethapp Python Ethereum Foundation pyethapp-v1.5.0
ethereumjs-lib Javascript Ethereum Foundation ethereumjs-lib-v3.0.0
Ethereum(J) Java ether.camp ethereumJ-v1.3.1
ruby-ethereum Ruby Jan Xie ruby-ethereum-v0.9.6
ethereumH Haskell BlockApps no Homestead release yet

1.3. 以太币单位的转换

以太币的最小单位是 Wei。以太币单位的转换如表 3 所示,其沿袭了科学界的传统,用做过杰出贡献的数学、密码学专家的名字命名。

Table 3: 以太币单位的转换
Unit Wei Value Wei
wei 1 wei 1
Kwei (babbage) 1e3 wei 1,000
Mwei (lovelace) 1e6 wei 1,000,000
Gwei (shannon) 1e9 wei 1,000,000,000
microether (szabo) 1e12 wei 1,000,000,000,000
milliether (finney) 1e15 wei 1,000,000,000,000,000
ether 1e18 wei 1,000,000,000,000,000,000

1.4. 两类账户

Ethereum 中有两种类型的账户:Externally Owned Account (EOA) 和 Contract Account,如图 1 所示(图片摘自 Getting Deep Into EVM: How Ethereum Works Backstage)。其中,EOA 是普通的钱包账户,而另一种是合约账户。

eth_account_addr.png

Figure 1: Ethereum 两类账户的地址生成

每个帐户都有下面 4 个状态:

  • nonce:外部账户为交易次数,合约账户为创建的合约序号。
  • balance:此地址的以太币余额。
  • storageRoot:账户存储内容组成的默克尔树根的哈希值。
  • codeHash:账户 EVM 代码的 hash 值。合约账户即为合约代码的哈希值,外部账户为空字符串的哈希值。

其中,storageRoot,codeHash 仅适用于合约帐户。如图 2 所示(图片摘自 Getting Deep Into EVM: How Ethereum Works Backstage)。

eth_world_state.png

Figure 2: 普通帐户和合约账户

2. 以太坊客户端 geth

geth(全称为 go-ethereum)是一个使用 go 语言编写的以太坊节点。

Mac 中安装 geth 的步骤:

$ brew tap ethereum/ethereum
$ brew install ethereum

2.1. 连接测试网络

geth 支持测试网络:Goerli、Ropsten 和 Rinkeby。可分别通过命令行参数 --goerli--ropstenrinkeby 指定它们:

$ geth --help
......
  --goerli                            Görli network: pre-configured proof-of-authority test network
  --rinkeby                           Rinkeby network: pre-configured proof-of-authority test network
  --ropsten                           Ropsten network: pre-configured proof-of-work test network

2.1.1. 运行节点(The Merge 之前)

启动 geth 节点,连接测试网络:

$ geth --ropsten --http --syncmode light 2>testnet.log

启动 geth 节点,连接测试网络,并启动 console:

$ geth --ropsten --http --syncmode light console 2>testnet.log

如果需要在 web3.js 代码中执行解锁帐号等操作,则还要加上参数 --http.api="eth,net,web3,personal" ,即:

$ geth --ropsten --http --http.api="eth,net,web3,personal" --allow-insecure-unlock --syncmode light console 2>testnet.log

参数说明:
1、参数 --ropsten 表示连接测试网络(不指定参数时默认连接主网络);
2、参数 --http 表示启用 http 服务,默认监听在 localhost:8545;
3、参数 --allow-insecure-unlock 可以解决解锁帐户时“account unlock with HTTP access is forbidden”的问题;
4、参数 --syncmode light 表示采用 light 同步模式(不指定参数时默认使用 full 同步模式);
5、参数 console 表示启动交互式的 JavaScript environment,也可以在启动节点时不启动交互式环境,启动节点后使用 geth attach 连接到交互式环境;
6、上面例子中为了不使日志影响到交互式环境,已经把标准错误输出重定向到文件 testnet.log 中。

进入交互环境后,输入 eth 可以查看所支持的交互命令,如:

$ geth --ropsten --http --syncmode light console 2>testnet.log
Welcome to the Geth JavaScript console!

instance: Geth/v1.10.5-unstable-a9fd67ca-20210714/linux-amd64/go1.16.6
 datadir: /home/ubuntu/.ethereum
 modules: admin:1.0 debug:1.0 eth:1.0 ethash:1.0 miner:1.0 net:1.0 personal:1.0 rpc:1.0 txpool:1.0 web3:1.0

To exit, press ctrl-d

> eth
{
  accounts: [],
  blockNumber: 4002167,
  coinbase: undefined,
  compile: {
    lll: function(),
    serpent: function(),
    solidity: function()
  },
  defaultAccount: undefined,
  defaultBlock: "latest",
  gasPrice: 1000000000,
  hashrate: 0,
  mining: false,
  pendingTransactions: [],
  protocolVersion: "0x2712",
  syncing: {
    currentBlock: 4002181,
    highestBlock: 4141720,
    knownStates: 0,
    pulledStates: 0,
    startingBlock: 3866623
  },
  call: function(),
  contract: function(abi),
  estimateGas: function(),
  filter: function(options, callback, filterCreationErrorCallback),
  getAccounts: function(callback),
  getBalance: function(),
  getBlock: function(),
  getBlockNumber: function(callback),
  getBlockTransactionCount: function(),
  getBlockUncleCount: function(),
  getCode: function(),
  getCoinbase: function(callback),
  getCompilers: function(),
  getGasPrice: function(callback),
  getHashrate: function(callback),
  getMining: function(callback),
  getPendingTransactions: function(callback),
  getProtocolVersion: function(callback),
  getRawTransaction: function(),
  getRawTransactionFromBlock: function(),
  getStorageAt: function(),
  getSyncing: function(callback),
  getTransaction: function(),
  getTransactionCount: function(),
  getTransactionFromBlock: function(),
  getTransactionReceipt: function(),
  getUncle: function(),
  getWork: function(),
  iban: function(iban),
  icapNamereg: function(),
  isSyncing: function(callback),
  namereg: function(),
  resend: function(),
  sendIBANTransaction: function(),
  sendRawTransaction: function(),
  sendTransaction: function(),
  sign: function(),
  signTransaction: function(),
  submitTransaction: function(),
  submitWork: function()
}

2.1.2. 运行节点(The Merge 之后)

The Merge(PoW 切换到 PoS)于 2022 年 9 月 15 日生效。

我们需要同时启动两类客户端:

  1. Consensus Client,常见的 Consensus Client 有:Prysm, Teku, Nimbus, Lighthouse, Lodestar。
  2. Execution Client,常见的 Execution Client 有:Besu, Erigon, Go-Ethereum (Geth), Nethermind。

下面以 Prysm 搭配 Geth 为例介绍节点的运行:

  1. 创建 JWT Secret 文件,如 openssl rand -hex 32 | tr -d "\n" > "jwt.hex"
  2. 启动 Geth,如 geth --http --http.api eth,net,engine,admin --authrpc.port 8551 --authrpc.jwtsecret path/to/jwt.hex
  3. 启动 Prysm,如 beacon-chain --execution-endpoint http://localhost:8551 --jwt-secret path/to/jwt.hex

2.1.3. 检测 Execution Client 同步状态

启动 geth 节点后,过一段时间会自动启动数据同步功能。使用 eth.blockNumber 可以检测目前已经同步到哪个块:

> eth.blockNumber
3076701

使用 eth.syncing 可以检测现在的同步状态,下面是它的输出实例:

> eth.syncing
{
  currentBlock: 3928302,
  highestBlock: 4141720,
  knownStates: 0,
  pulledStates: 0,
  startingBlock: 3866623
}

如果目前没处于同步状态(这可能是同步完成了,也可能是没开始同步),则 eth.syncing 会输出:

> eth.syncing
false

使用 net.peerCount 可以检查连接了多少个节点,下面是它的输出实例:

> net.peerCount
1

如果不能自动发现其它节点,可以手动配置其它节点,参考:https://geth.ethereum.org/docs/interface/peer-to-peer

2.1.4. 检测 Consensus Client 同步状态

执行下面命令可以检测 Consensus Client 同步状态:

$ curl http://localhost:3500/eth/v1/node/syncing
{"data":{"head_slot":"3877737","sync_distance":"857832","is_syncing":true,"is_optimistic":false}}

参考:https://ethereum.github.io/beacon-APIs/

2.1.5. 获取测试币

在网站 https://faucet.metamask.io (需要安装 MetaMask 浏览器插件)上可以获取测试网络的以太币。
在网站 https://faucet.rinkeby.io 上可以获取 Rinkeby 测试网的以太币。
在网站 https://faucet.kovan.network 上可以获取 Kovan 测试网的以太币。

2.1.6. Tips:减少硬盘占用空间

当 geth 节点运行一段时间后,占用的硬盘空间会比较大。有一种办法可以减少占用的硬盘空间,停止节点后运行下面命令:

$ geth snapshot prune-state

在运行上面命令结束后,再启动节点即可。

参考:https://gist.github.com/yorickdowne/3323759b4cbf2022e191ab058a4276b2

2.2. 账号相关

2.2.1. 创建账号

使用 personal.newAccount() 可以创建账号,会提示输入密码。如:

> personal.newAccount('12345')   # 密码为12345
"0x6bf91905d0434c6029a79b08127722e0c2311512"
2.2.1.1. 导出账号的私钥

通过 personal.newAccount() 创建的账号,无法方便地导出私钥。只能通过 key 文件中描述的算法来恢复私钥。Mac 系统中,key 位于:

$ ls ~/Library/Ethereum/testnet/keystore/
UTC--2019-11-18T09-31-07.341356000Z--47985821a97f2d199e712a882a6cb51e4cb3372e
$ cat ~/Library/Ethereum/testnet/keystore/UTC--2019-11-18T09-31-07.341356000Z--47985821a97f2d199e712a882a6cb51e4cb3372e
{"address":"47985821a97f2d199e712a882a6cb51e4cb3372e","crypto":{"cipher":"aes-128-ctr","ciphertext":"182252392cfaa5371ce7808445ab57f293cabc44d37efc25f61b02c056923a41","cipherparams":{"iv":"ad3a92debfb76ac4dbf5398844a73a8e"},"kdf":"scrypt","kdfparams":{"dklen":32,"n":262144,"p":1,"r":8,"salt":"78392626dac750e1a6f692502466f7d7500766eb6f328d16e050368acefa8200"},"mac":"3f3c2df22759881a39571d47d3aa73b6b7a9a99f983eee70bd3aa8557d0349ab"},"id":"23a1e014-90be-48ab-a363-fdbea8bbe168","version":3}

key 文件的最后一部分是钱包地址,如上面例子是 0x47985821a97f2d199e712a882a6cb51e4cb3372e 的 key 文件。

2.2.2. 导入账号

使用 personal.importRawKey("priv key", "your password") 可以导入账号。如:

> personal.importRawKey('af180cf61900d1c741bab0a99dedb7cc0da78988ffafc364ba06feab4c88be97', '12345678')
"0xe4f394093e0dc30d28a216daece79a6d0d537042"

2.2.3. 列出已有账号

使用 eth.accounts 或者 personal.listAccounts 可以列出 geth 创建或者导入的账号,如:

> eth.accounts
["0x6bf91905d0434c6029a79b08127722e0c2311512", "0x33bcd711ce5917bdaf3079488f402bc75118b026", "0xe4f394093e0dc30d28a216daece79a6d0d537042"]

2.2.4. 查询余额

使用 eth.getBalance 可以查看账号余额,其单位为 wei,如:

> eth.getBalance('0xe4f394093e0dc30d28a216daece79a6d0d537042')
1000000000000000000

如果要显示以 ether 为单位的余额,可以执行下面命令:

> web3.fromWei(eth.getBalance('0xe4f394093e0dc30d28a216daece79a6d0d537042'), "ether")
1.0

注:如果执行命令时提示“Error: no suitable peers available”,即出现:

> eth.getBalance('0xe4f394093e0dc30d28a216daece79a6d0d537042')
Error: no suitable peers available
    at web3.js:3143:20
    at web3.js:6347:15
    at web3.js:5081:36
    at <anonymous>:1:1

则稍微等一下,再尝试即可。

2.2.5. 转账

使用 eth.sendTransaction 可以进行转账操作。如:

> personal.unlockAccount("0xe4f394093e0dc30d28a216daece79a6d0d537042", "12345678")    // 解锁账号
> eth.sendTransaction({from:"0xe4f394093e0dc30d28a216daece79a6d0d537042", to:"0x6bf91905d0434c6029a79b08127722e0c2311512", value:web3.toWei(0.5, 'ether')})
"0x026956208c00f047f05eadf900e8297e3fc445e2cbe7e85f51b95742f5e86a00"

3. web3.js 库

web3.js 是一个以太坊的 Javascript 库(可以通过 npm install web3 安装),它实现了 Generic JSON RPC spec 。在使用 web3.js 前你需要本地搭建一个以太坊节点(或者使用别人提供的节点)。

下面是一个实例程序 test.js:

var Web3 = require("web3");
var web3 = new Web3();

// 连接到以太坊节点(请在本地自己搭建好以太坊节点)
web3.setProvider(new Web3.providers.HttpProvider("http://localhost:8545"));

console.log('web3 version=' + web3.version);

// 输出连接节点中生成或导入的帐号
web3.eth.getAccounts().then(console.log);

// 输出指定账号的余额
web3.eth.getBalance("0x6bf91905d0434c6029a79b08127722e0c2311512").then(console.log);

测试上面程序,得到下面输出:

$ node test.js
web3 version=1.0.0-beta.36
[ '0x6bf91905d0434c6029a79B08127722e0c2311512',
  '0x33bCd711cE5917BdaF3079488F402bc75118b026',
  '0xe4F394093E0Dc30d28A216dAEce79A6D0D537042' ]
500000000000000000

注:web3.js 1.0 版本和之前的版本有较大的区别,本文例子使用是的 1.0 版本。

参考:
https://web3js.readthedocs.io/en/1.0/web3.html

4. 理解智能合约

A contract is a collection of code (its functions) and data (its state) that resides at a specific address on the Ethereum blockchain.

4.1. 数据存储管理(Storage, Memory and the Stack)

EVM 中有三个地方可以存储数据:Storage、Memory、Stack,如图 3 所示。

eth_data.png

Figure 3: EVM 的数据存储

下面对它们一一进行介绍:

Each account has a data area called storage, which is persistent between function calls and transactions. Storage is a key-value store that maps 256-bit words to 256-bit words. It is not possible to enumerate storage from within a contract, it is comparatively costly to read, and even more to initialise and modify storage. Because of this cost, you should minimize what you store in persistent storage to what the contract needs to run. Store data like derived calculations, caching, and aggregates outside of the contract. A contract can neither read nor write to any storage apart from its own.

The second data area is called memory, of which a contract obtains a freshly cleared instance for each message call. Memory is linear and can be addressed at byte level, but reads are limited to a width of 256 bits, while writes can be either 8 bits or 256 bits wide. Memory is expanded by a word (256-bit), when accessing (either reading or writing) a previously untouched memory word (i.e. any offset within a word). At the time of expansion, the cost in gas must be paid. Memory is more costly the larger it grows (it scales quadratically).

The EVM is not a register machine but a stack machine, so all computations are performed on a data area called the stack. It has a maximum size of 1024 elements and contains words of 256 bits. Access to the stack is limited to the top end in the following way: It is possible to copy one of the topmost 16 elements to the top of the stack or swap the topmost element with one of the 16 elements below it. All other operations take the topmost two (or one, or more, depending on the operation) elements from the stack and push the result onto the stack. Of course it is possible to move stack elements to storage or memory in order to get deeper access to the stack, but it is not possible to just access arbitrary elements deeper in the stack without first removing the top of the stack.

摘自:https://solidity.readthedocs.io/en/latest/introduction-to-smart-contracts.html#storage-memory-and-the-stack

4.1.1. Calldata(msg.data)

除了前面介绍的三个位置外,合约还可以访问名为 Calldata 的数据。

Calldata 中的数据是只读的,有三个 EVM 操作符可以操作 Calldata:

  1. CALLDATASIZE tells the size of the transaction data.
  2. CALLDATALOAD loads 32 bytes of the transaction data onto the stack.
  3. CALLDATACOPY copies a number of bytes of the transaction data to memory.

Solidity 中可以使用 msg.data 来访问 Calldata。

参考:https://blog.openzeppelin.com/ethereum-in-depth-part-2-6339cf6bddb9/

4.2. 监控合约代码的执行(Event Log)

EVM 中有个 Log 的概念,可以用于记录合约执行时的一些日志,它属于 Transaction Receipt 中的内容。EVM 提供了五个 Log 操作指令(即 LOG0, LOG1, LOG2, LOG3, LOG4 )用于往 Log 中写入数据。

Event 是 Solidity 中的概念, 触发(emit)一条 Event 就是往 Log 中写入一条数据。

pragma solidity ^0.4.24;

contract Store {
  event ItemSet(bytes32 key, bytes32 value);

  string public version;
  mapping (bytes32 => bytes32) public items;

  constructor(string _version) public {
    version = _version;
  }

  function setItem(bytes32 key, bytes32 value) external {
    items[key] = value;
    emit ItemSet(key, value);   // 编译后,会转换为 EVM 的 Log 操作指令
  }
}

Log 有什么用处呢?它可以监控合约代码的执行。比如,上面例子中,我们读取合约的 Log 就能知道 setItem 是否被调用(发现一条 Log 就表示 setItem 被调用过一次),以及调用时的参数。

参考:How Solidity Events are Implemented

4.2.1. 合约和外界的交互

Log 是合约和外界交互的重要方式,如图 4 所示(图片摘自 Getting Deep Into EVM: How Ethereum Works Backstage)。

eth_evm_log.png

Figure 4: 合约和外界的交互

在 DApp 中,往往需要监听合约的 Log 才知道合约是否被执行,从而知道用户是否使用了 DApp 的某项功能。

4.2.2. Event Log Topic

Event Log 保存在两个部分中:topic 部分(优点是可以快速查找)和 data 部分(优点是 gas 低)。 如果定义 Event 时,参数用 indexed 修饰,则该参数对应数据会存到 topic 部分中,便于快速查找。而未加 indexed 修饰的参数值会存到 data 部分中,成为“原始日志”。需要注意的是,如果增加 indexed 属性的是数组类型(包括 string 和 bytes),那么 topic 存储对应的数据的哈希值,而不是存原始数据,因为 topic 是用于快速查找的,不能存任意长度的数据。

下面的 Transfer Event 中参数 from 和 to 使用 indexed 修饰,它们会保存在日志的 topic 部分中,而 value 没有使用 indexed 修饰,则直接保存在 data 部分中。

event Transfer(address indexed from, address indexed to, uint256 value);

5 是 Transfer Event 产生的 Log 实例。

eth_event_log_example.gif

Figure 5: Example of Transfer Event

从图 5 中我们可以发现 topic 数据中有 3 个元素,但我们仅仅指定了 2 个参数为 indexed ,为什么会多一个呢?这是因为, 对于非匿名 Event,其 topic 的首元素固定为“Event 的签名的哈希”。 以 Transfer Event 为例,其签名计算为:

keccak('Transfer(address,address,uint256)')
ddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef

这个哈希值和图 5 是一致的。

Event Log 存储的 EVM 指令是 LOG0/LOG1/LOG2/LOG3/LOG4,它们的含义是:

Value  Mnemonic     Description
0xa0    LOG0        Append log record with no topics.
0xa1    LOG1        Append log record with one topic.
...     ...         ...
0xa4    LOG4        Append log record with four topics.

可知,最多可以有 4 个 Topics,除去第 1 个 Topic(前面介绍了它固定为“Event 的签名的哈希”)外,我们最多指定 3 个 Topics,这就是为什么 Event 中 indexed 修饰符最多只能对 3 个参数进行标记的原因。

参考:
https://medium.com/mycrypto/understanding-event-logs-on-the-ethereum-blockchain-f4ae7ba50378
https://codeburst.io/deep-dive-into-ethereum-logs-a8d2047c7371

4.2.3. 读取 Event Log

Golang 读取 Event Log 可参考:https://goethereumbook.org/event-read/

4.3. 合约 ABI

合约的 ABI (Application Binary Interface) 定义函数如何编码,某类型数据如何编码等等细节。有了 ABI,我们知道该如何调用合约中函数,以及如何解码函数的返回值。

参考:Contract ABI Specification

4.4. 合约函数定位

考虑问题:EVM 是如何定位合约中的函数?

其实, EVM 是直接执行整个合约代码编译后的虚拟机指令。定位函数的过程并不由 EVM 负责,Solidity 编译器(或其它编译器)在编译合约时会生成类似下面的代码(仅仅是原理性的演示):

method_id = first 4 bytes of msg.data
if method_id == 0x25d8dcf2 jump to 0x11
if method_id == 0xaabbccdd jump to 0x22
if method_id == 0xffaaccee jump to 0x33
other code <- Solidity fallback function code could be here
0x11:
code for function with method id 0x25d8dcf2
0x22:
code for function with method id 0xaabbccdd
0x33:
code for function with method id 0xffaaccee

也就是说,EVM 仅执行合约的虚拟机指令,合约的虚拟机指令会根据传进来的 Calldata (msg.data) 的前面 4 个字节(称为“function selector”)来执行不同的函数。

考虑下面合约:

pragma solidity >=0.4.16 <0.7.0;

contract Foo {
    function bar(bytes3[2] memory) public pure {}
    function baz(uint32 x, bool y) public pure returns (bool r) { r = x > 32 || y; }
    function sam(bytes memory, bool, uint[] memory) public pure {}
}

调用 baz(69, true) 时,其 Calldata 为:

0xcdcd77c000000000000000000000000000000000000000000000000000000000000000450000000000000000000000000000000000000000000000000000000000000001

其解释如下:

0xcdcd77c0  # 函数签名 Hash 的前 4 个字节,即 Keccak256("baz(uint32,bool)") 的前 4 个字节
0000000000000000000000000000000000000000000000000000000000000045  # 69 = b(0100 0101),采用“大端法”
0000000000000000000000000000000000000000000000000000000000000001  # true 编码

调用 bar(["abc", "def"]) 时,其 Calldata 为:

0xfce353f661626300000000000000000000000000000000000000000000000000000000006465660000000000000000000000000000000000000000000000000000000000

其解释如下:

0xfce353f6 # 函数签名 Hash 的前 4 个字节,即 Keccak256("bar(bytes3[2])") 的前 4 个字节
6162630000000000000000000000000000000000000000000000000000000000 # "abc" 编码
6465660000000000000000000000000000000000000000000000000000000000 # "def" 编码

参考:https://ethereum.stackexchange.com/questions/7602/how-does-the-evm-find-the-entry-of-a-called-function

4.5. 合约部署(Creation Bytecode VS. Runtime Bytecode)

以太坊节点的 RPC 方法 eth_sendTransaction 不仅可以提交 tx,还可以用于部署合约: 如果调用 eth_sendTransaction 时,不指定目标地址(即 to 为 null),则表示“创建新合约”,合约的 Creation Bytecode 由 data 参数所指定。

下面是创建新合约时 eth_sendTransaction 的参数的例子:

{
  "from": "0xbd04d16f09506e80d1fd1fd8d0c79afa49bd9976",
  "to": null, // 指定 null 表示创建新合约
  "gas": "68653",
  "gasPrice": "1", // 10000000000000
  "data": "0x60606040523415600e57600080fd5b603580601b6000396000f3006060604052600080fd00a165627a7a723058204bf1accefb2526a5077bcdfeaeb8020162814272245a9741cc2fddd89191af1c0029"  // data 指定合约的 Creation Bytecode
}

需要说明的,这个 data 是一个 EVM-code fragment(称为 Creation Bytecode),执行它会返回另一个 EVM-code fragment(Runtime Bytecode,也称为 Deployed Bytecode),第 2 个 EVM-code fragment 才是链上真正保存的合约代码。 Creation Bytecode 仅在部署时执行一次;当部署完后,后续调用合约时执行的都是 Runtime Bytecode。

注:合约的创建过程可参考“黄皮书” 4.3. The Transaction

通过表 4 中的方法可以访问 Creation Bytecode 和 Runtime Bytecode。

Table 4: 访问 Creation Bytecode 和 Runtime Bytecode
Item On-chain Retrival Off-chain Retrieval
Creation Bytecode type(ContractName).creationCode eth_getTransactionByHash
Runtime Bytecode (Deployed Bytecode) extcodecopy(a) or type(ContractName).runtimeCode eth_getCode

关于 type(ContractName).creationCodetype(ContractName).runtimeCode 的使用说明可参考:https://docs.soliditylang.org/en/v0.6.0/units-and-global-variables.html#type-information

在合约中使用 CREATE2 可以部署其它的合约,它的好处是在部署合约之前可以提前计算出将要部署的合约的地址,详情可参考:https://solidity-by-example.org/app/create2/

    // 如何计算 CREATE2 所需要的参数 init code hash,即 keccak256(init_code)?

    // 情况一,待部署合约无构造函数:
    bytes32 initCodeHash = keccak256(type(ContractName).creationCode);

    // 情况二,待部署合约的构造函数有参数(假设为 arg1,arg2,arg3,arg4)时:
    bytes memory initCode = abi.encodePacked(type(ContractName).creationCode, abi.encode(arg1, arg2, arg3, arg4));
    bytes32 initCodeHash = keccak256(initCode);

使用 CREATE2 部署的新合约,其地址为:

keccak256(0xff ++ deployer_address ++ salt ++ keccak256(init_code))[12:]

如果我们要把同一个合约部署到多个的 EVM 兼容链上,而且想保持新合约在各个 EVM 链上的地址相同。怎么办呢?可以使用 SingletonFactory,参考:ERC-2470: Singleton Factory

4.6. 什么是 Gas

每个参与到网络的节点都会运行 EVM 作为区块验证协议的一部分。每个网络中的全节点都会进行相同的计算并储存相同的值。合约执行会在所有节点中被多次重复,而且任何人都可以发布执行合约,这使得合约执行的消耗非常昂贵,所以为防止以太坊网络发生蓄意攻击或滥用的现象,以太坊协议规定交易或合约调用的每个运算步骤都需要收费。这笔费用以 Gas(也就是俗称的燃料)作为单位计数。

每个 EVM 中的命令都被设置了相应的 Gas 消耗值。如果用 gasUsed 表示所有被执行命令的 Gas 消耗值总和,则有:

交易费 = gasUsed * gasPrice

注:用户可以自行设置的 gasPrice 的值。由于网络拥堵等多种情况,此价格会动态变化,可根据交易字节数和当前区块中打包的平均值进行大概的预估。在 web3 中可以使用 web3.eth.getGasPrice() 得到 gasPrice 的估计值。

参考:https://blog.csdn.net/wo541075754/article/details/79042558

4.7. 可升级合约

我们知道, 智能合约部署以后不能再修改。如果重新部署那么合约的地址会变化(相当于部署另外一个合约),更麻烦的是用户的数据都还在“旧合约”中。

有两种常见思路可以实现合约的“可升级”:
1、Data separation
2、Delegatecall-based proxies

参考:
Contract upgrade anti-patterns: https://blog.trailofbits.com/2018/09/05/contract-upgrade-anti-patterns/
OpenZeppelin Proxy Upgrade Pattern: https://docs.openzeppelin.com/upgrades/2.7/proxies

4.7.1. Data separation

Data separation 模式中,数据和逻辑分开放在不同的合约中,逻辑合约调用数据合约。只有逻辑合约可以升级,而数据合约不升级。

数据合约的例子:

contract DataContract is Owner {
  uint public myVar;

  function setMyVar(uint new_value) onlyOwner public {
    myVar = new_value;
  }
}

由于逻辑合约可能需要升级,为了对用户透明(用户调用合约地址不能变化),我们需要增加一个代理合约,它的作用是转发请求给真正的逻辑合约。代理合约中保存着逻辑合约的地址,部署新逻辑合约后,在代理合约中更新一下新逻辑合约的地址即可完成升级操作。

Data separation 模式如图 6(摘自:Contract upgrade anti-patterns)所示。

eth_data_separation.png

Figure 6: Data separation

4.7.2. Delegatecall-based proxies

Delegatecall-based proxies 模式中,数据和逻辑分开也放在不同的合约中,不过它是数据合约调用逻辑合约(注:在 Data separation 模式中,是逻辑合约调用数据合约)。

介绍这个模式前,先介绍一下 EVM 中的 delegatecall 指令,它的含义是 “calls a method in another contract, using the storage of the current contract”。

delegatecall 可用于实现 library,如:

pragma solidity ^0.4.24;

library Lib {

  struct Data { uint val; }

  function set(Data storage self, uint new_val) public {
    self.val = new_val;
  }
}

contract C {
  Lib.Data public myVal;

  function set(uint new_val) public {
    Lib.set(myVal, new_val);         // 编译后,会产生 delegatecall 指令
  }
}

上面代码中,Lib.set 执行时会修改存储在合约 C 中的状态变量 myVal。

Delegatecall-based proxies 模式如图 7(摘自:Contract upgrade anti-patterns)所示。

eth_delegatecall_based_proxies.png

Figure 7: Delegatecall-based proxies

采用 Delegatecall-based proxies 模式时,需要注意,只能在后面增加合约中的状态变量,不能修改已有的状态变量。这是因为合约中状态变量是按下标一个一个存放的,旧数据已经存在了,不能再调整,只能往往后面增加。

假设有合约:

// contracts/Counter.sol
pragma solidity ^0.6.0;

contract Counter {
    uint256 public value;  // 只能在后面增加合约中的状态变量,这样才不会破坏内存

    function increase(uint256 amount) public {
        value += amount;
    }
}

如果我们想升级上面的合约,需要增加状态变量,则只能在状态变量 value 的后面增加。

OpenZeppelin 采用 Delegatecall-based proxies 模式实现合约的可升级。 利用 OpenZeppelin 提供的工具( npx oz upgrade )可以自动检测修改已有状态变量的危险情况,如前面合约中如果我们在状态变量 value 前增加另外一个状态变量(uint256 public value_other),则升级时会提示下面错误:

New variable 'uint256 value_other' was inserted in contract Counter in contracts/Counter.sol:1. You should only add new variables at the end of your contract.

关于这种升级模式,可参考下面 EIP:
EIP-1967: Standard Proxy Storage Slots

5. Bitcoin VS. Ethereum

下面简单地列举一些比特币和以太坊的不同之处。

不同点一:和比特币相比, 以太坊支持完善的智能合约 ,可以方便地在以太坊上开发 DApp。
不同点二:它们产生区块的速度不一样:比特币约 10 分钟一个区块;而以太坊约 15 秒一个区块。
不同点三:比特币采用“UTXO”(区块链网络本身并不直接保存余额等信息)而以太坊才是采用的我们经常用的“账户余额模型”。对于以太坊账户,网络中直接保存着以下几个状态:

  • nonce:外部账户为交易次数,合约账户为创建的合约序号。
  • balance:此地址的以太币余额。
  • storageRoot:账户存储内容组成的默克尔树根的哈希值。
  • codeHash:账户 EVM 代码的 hash 值。合约账户即为合约代码的哈希值,外部账户为空字符串的哈希值。

Author: cig01

Created: <2018-09-02 Sun>

Last updated: <2022-09-20 Tue>

Creator: Emacs 27.1 (Org mode 9.4)