truffle migrate

truffle migrate做了什么?

Truffle 是目前以太坊合约开发中最常用的框架,写好合约、部署脚本和配置文件,一句truffle migrate就能够直接完成合约部署,但是truffle migrate背后发生了什么?为什么部署脚本都是数字开头?今天细看了一下,解决了这些疑惑,记录一下

先简单说说truffle的使用

1.Truffle init

Truffle init会将当前文件夹初始化为一个Truffle项目文件夹,结构如下:

1
2
3
4
5
6
7
.
├── contracts
│   └── Migrations.sol
├── migrations
│   └── 1_initial_migration.js
├── test
└── truffle-config.js

其中Migrations.sol用于记录当前执行到了第几个部署脚本,1_initial_migration.js是Migrations.sol的部署脚本。Migrations.sol内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
pragma solidity >=0.4.21 <0.6.0;

contract Migrations {
address public owner;
uint public last_completed_migration;

constructor() public {
owner = msg.sender;
}

modifier restricted() {
if (msg.sender == owner) _;
}

function setCompleted(uint completed) public restricted {
last_completed_migration = completed;
}

function upgrade(address new_address) public restricted {
Migrations upgraded = Migrations(new_address);
upgraded.setCompleted(last_completed_migration);
}
}

主要字段为last_completed_migration,记录了上一个执行完成的部署脚本的编号。truffle migrate时会执行migrations文件夹下的部署脚本,每执行完一个部署脚本之后都会调用Migrations.sol合约的setCompleted方法,将last_completed_migration更新为部署脚本开头的数字。

2.编写自己的合约

现在我们写一个简单的合约Store.sol,然后加入对应得部署脚本,现在文件夹结构如下:

1
2
3
4
5
6
7
8
9
.
├── contracts
│   ├── Migrations.sol
│   └── Store.sol
├── migrations
│   ├── 1_initial_migration.js
│   └── 2_depoly_store.js
├── test
└── truffle-config.js

合约内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
pragma solidity >=0.4.21 <0.6.0;

contract Store {
uint a;
address owner;

constructor () public {
owner = msg.sender;
a = 10;
}

function getA() public returns (uint) {
return a;
}

function setA(uint x) public {
a = x;
}
}

部署脚本:

1
2
3
4
5
const Store = artifacts.require("Store");

module.exports = function(deployer) {
deployer.deploy(Store);
};

3.truffle-config.js

修改配置文件truffle-config.js,在networks中将如下部分的注释去掉:

1
2
3
4
5
45      development: {
46 host: "127.0.0.1", // Localhost (default: none)
47 port: 8545, // Standard Ethereum port (default: none)
48 network_id: "*", // Any network (default: none)
49 },

然后在本地开启ganache-cli,未安装的直接npm install -g ganache-cli即可

4.Truffle migrate

完成以上步骤,直接truffle migrate就可以将我们的合约部署到本地测试网了,下面是truffle migrate背后的一系列步骤:

  • 如果没有事先编译合约,truffle migrate会先编译合约,你也可以在truffle migrate之前用truffle compile编译好合约
  • 按照migrations文件夹中的前缀数字编号顺序执行部署脚本,第一个部署的是Migrations.sol,第二个是Store.sol
  • 每执行完一个部署脚本,调用Migrations合约的setCompleted方法,将last_completed_migration值设为部署脚本编号

所以,这里运行truffle migrate一共会产生4笔交易:

  1. Migrations.sol合约的部署
  2. 调用setCompleted将last_completed_migration设为1
  3. Store.sol合约的部署
  4. 调用setCompleted将last_completed_migration设为2

5.更新合约

如果之后合约有更新,在migrations文件夹内加入新的部署脚本即可,注意文件名前缀数字需在当前的最大值上加1。然后直接truffle migrate,这时,truffle会从前缀编号为last_completed_migration + 1的脚本开始执行,如果你的项目包含了很多合约,这样只部署改动过的合约,避免了重复部署。

如果只是想知道如何使用truffle,看到这里就ok了,如果还想了解更多,请继续往下看:

上面说了,truffle会自动从编号为last_completed_migration + 1的脚本开始执行,那么问题来了,每次执行truffle migrate的时候truffle是怎么知道last_completed_migration的值的?两种可能:

  1. 从链上获取,那这样truffle就必须知道Migrations.sol合约的地址
  2. 保存在本地

我起初以为是第一种可能,但是我在执行truffle migrate的时候发现ganache-cli的输出日志并没有显示有合约调用,而且,如果要从链上获取last_completed_migration的值,truffle必须知道合约的部署地址,那本地必定有地方保存了Migration合约的地址。

一番查找之后,并未找到任何相关文件。这样一来,可能2也直接否定了。

无奈,只能翻truffle源码,查找半天,没有找到和链交互的代码,但是看到了下面这段代码:

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
33
34
35
36
37
38
39
40
41
326421 var Networks = {
326422 deployed: function(options, callback) {
326423 fs.readdir(options.contracts_build_directory, function(err, files) {
326424 if (err) {
326425 // We can't read the directory. Act like we found nothing.
326426 files = [];
326427 }
326428
326429 var promises = [];
326430
326431 files.forEach(function(file) {
326432 promises.push(new Promise(function(accept, reject) {
326433 fs.readFile(path.join(options.contracts_build_directory, file), "utf8", functi on(err, body) {
326434 if (err) return reject(err);
326435
326436 try {
326437 body = JSON.parse(body);
326438 } catch (e) {
326439 return reject(e);
326440 }
326441
326442 accept(body);
326443 });
326444 }));
......
326467 binaries.forEach(function(json) {
326468 Object.keys(json.networks).forEach(function(network_id) {
326469 var network_name = ids_to_names[network_id] || network_id;
326470
326471 if (networks[network_name] == null) {
326472 networks[network_name] = {};
326473 }
326474
326475 var address = json.networks[network_id].address;
326476
326477 if (address == null) return;
326478
326479 networks[network_name][json.contractName] = address;
326480 });
326481 });
......

看到这里才发现truffle还有一个networks命令,truffle networks可以显示当前所有合约的链上地址。结合326423行和326479行,确定地址是从文件里读取的,326423行表明是从build文件夹里读取的,之前一直以为build文件夹里面的json文件就只是合约编译输出的ABI信息,所以一直没注意,绕了这么一大圈。

打开Store.json,发现了一个networks字段,保存了合约的部署信息:

1
2
3
4
5
6
7
8
908   "networks": {
909 "1551517567987": {
910 "events": {},
911 "links": {},
912 "address": "0xdc18E55D691869b5295027C1B4B861d54B495E81",
913 "transactionHash": "0x627ba390efd246dd7553b237395f413ada96292fa4a764c28d504c96033d3e12"
914 }
915 },

终于,破案~

那现在问题又来了,既然本地都保存了合约部署的信息,干嘛还要在链上部署一个Migration合约呢?每次执行部署脚本的时候检查一下对应合约编译结果的networks字段不就ok了?如果为空就重新部署,如果不空就不重新部署了嘛

试一试

删掉Store.json里的networks字段内容,现在networks字段为空,再执行truffle migrate,输出:

1
2
3
4
5
Compiling your contracts...
===========================
> Everything is up to date, there is nothing to compile.

Network up to date.

然后ganache-cli输出日志有合约调用记录,看来还不是上面说的那么简单,部署的时候先检查本地json文件的networks字段,然后再检查链上合约里的last_completed_migration字段。

现在,可以更全面地理一下truffle migrate背后的流程了(此时假设Migration合约已经部署):

  • 如果没有编译合约,首先编译合约,输出json文件放入build文件夹
  • 从migrations文件夹中第一个部署脚本开始,检查对应合约的编译输出json文件
    • 如果networks字段不为空,不执行此脚本,开始执行下一个脚本
    • 如果networks字段为空,到链上查询last_completed_migration
      • 如果此值大于部署脚本前缀,说明之前已经部署过对应合约,跳过
      • 如果此值小于部署脚本前缀数字,说明还未部署对应得合约,执行部署合约操作,然后调用Migration的setCompleted方法将last_completed_migration字段值设为此脚本前缀数字,并将合约部署信息写入到对应合约json文件的networks字段

最后说一句,默认的Migration合约其实并非必要,完全可以每次直接:

1
truffle migrate --reset

重新部署所有合约

但是这两种做法各有利弊:

  • 如果你的项目只有一两个合约,为了部署这几个合约还要部署一个Migration合约,每次部署完自己的合约还要调用Migration合约,要多消耗不少手续费,这种情况每次重新部署比较划算
  • 如果你的项目有很多合约,每次重新部署所有合约的代价高于了维护一个Migration合约的成本,那还是最好选择使用Migration合约