BlockChain - 从搭建到发布Smart Contract

摘要:Ethereum是Block Chain的一种实现,跟比特币相比,它最大的特点之一就是可以在它的基础上发布Smart Contract的Dapp。一段时间的学习和调查,我从第一步搭建私有的Ethereum BlockChain,直到编写Smart Contract, 发布和调用,一整套流程调通。兴奋之余,把经历的种种记录下来,以便日后回顾。

本文主要介绍区块链从搭建到发布Smart Contract的基本步骤:

  1. 搭建私有区块链。
  2. 区块链的基本配置。
  3. 编写,编译和调用Smart Contract

搭建私有区块链

Ethereum 跟比特币对比,虽然都是区块链的一种,但Ethereum 的特点之一是可以编写发布Dapp[1][2]。在开发Dapp的时候,难免需要一个测试用的Chain。 在公网上,可以直接连上 Ethereum testnet (Ropsten), 或者使用testrpc 。不过在使用testrpc有可能会遇到各种不可预期的地雷。Ropsten testnet则是需要等待同步block。

用于搭建私有区块链集中可行的方案:

  1. 搭建一个Private Ethereum BlockChain. Ethereum 本身也是支持这种做法。
  2. 通过Parity提供的模板,搭建一个私有的PoA 区块链也是不错的选择。Paity的PoA模板甚至提供了配置功能WebSite Portal。做的是非常友善了。
  3. 微软的开源私有区块链 Coco Framework。(打个广告,目前还有没有GA)
  4. 通过Azure平台提供的模板搭建私有的Private Ethereum BlockChain

可虑再三,我还是比较喜欢自己手上可以控制的私有区块链。同时也可以进一步熟悉和学习区块链。这里有几个考量:

  1. 这主要也是为了偷个懒,Azure上搭建的模板,不需要我自己去一台一台的创建服务器。
  2. 托管在Azure上的服务器,可以配置公开一些站点。其他开发者,在使用这个环境进行开发和测试,不会被限制在一个局域网内。在外网能够接入。(我是一个热爱Work @ Home的工程师)
  3. 安全方面有保障。Azure Portal上有防火墙设置,可以允许特定的人员接入这个网络,也可以屏蔽掉并不需要的人员。如果自己搭建服务器,就要考虑外网访问的各种风险。我只想搭个测试环境,为什么还要在安全方面花那么多心思。
  4. 扩容方便。增加Virtual Machine只需要简单的点几下按钮就可以完成操作。
  5. 还有各种没想到的好处。例如环境发生问题,可以随意上ticket要求微软技术支持介入。(ε=ε=ε=(~ ̄▽ ̄)~) 一脸坏笑。

在学习的过程中,我选择了最后一种方式,在Azure上面搭建Private Ethereum BlockChain。喜欢的朋友可以参考这篇文章 如何在Azure上快速创建一个Ethereum Consortium Network模板 .

如何快速测试私有链的连接

私有链的模板搭设好之后,最想做的事情就是想赶快验证一下这个私有链能不能从客户端连上。最简单直接的方法,就是借助现有的一些区块链工具,连上去。现在比较流行的Dapp Browsers有以下几个[3]

  • Mist – official GUI dapp browser developed by the foundation, alpha stage. Mist as Wallet dapp is in beta.
  • Status – Mobile Ethereum browser (alpha)
  • MetaMask – Aaron Kumavis Davis’s in-browser GUI. Epicenter Bitcoin interview on github – supported by DEVgrants
  • AlethZero – C++ eth client GUI, (discontinued).
  • Supernova – (discontinued).

我挑选了一个比较轻量级的工具MetaMask[4].

  1. MetaMask是一个可以安装在Chrome的插件。(当然它也可以安装在Brave Browser上)。
  2. 点击 “Add to Chrome”将会进行这个插件的安装。
    img
  3. 安装好之后,重启Chrome,在导航栏的右侧会出现一个狐狸头的图标。点击这个图标,可以弹出MetaMask。注册账号之后,默认的,MetaMask会连入以太网的公网。
  4. 点击右上角的图标。点Settings,进入设置。
    img
  5. 在里面输入私有链创建好之后,所监听的Ethereum-RPC-EndPoint.
    img
  6. 设置好之后,点击左上角的img图标。切换到刚刚设定好的私有网络中。如果能够正常显示数据,那么就是连接成功了。
    img
  7. 点击img图标。找到自己的账号的Address, 也就是一把公钥。
    img
  8. 在浏览器中访问自己公布出来的Admin Site. 填入自己的Address, 点击submit, 就可以给自己的account发布1000个以太币作为测试使用。给自己的账号转以太币之后,一般间隔10多秒,就能看到以太币到账。
    img

用web3.js连接到私有链上

使用工具验证了私有链部署成功。下一步,需要通过program的方式连接私有链了。web3.js是一个库的集合,它允许我们使用HTTP或IPC连接与本地或远程Ethereum的节点 进行交互。它为我们提供了JavaScript API来与geth进行通信。 它在内部使用JSON-RPC与geth进行通信,geth是一个轻量级的远程过程调用。(RPC)协议。

前期准备

在web server上面,选择使用node.js[5]来host webpage. 首先需要在Windows上面安装NPM和Node.JS.

在开发的IDE方面,我选择的Visual Studio Code[6]. 在开发工具IDE这个方面微软的Visual Studio Code做的还是非常不错的。关键是Visual Studio Code还免费,真心的一个大福利。

动手建项目

环境搭建好之后,可以使用npm创建一个项目的模板。

  1. 打开CMD, 转到要创建项目文件的路径
  2. 输入下面的命令行,一路ENTER到底,项目创建完成。
    mkdir deploy_contract
    cd deploy_contract
    npm init #一路enter到底
    img
  3. 安装web.js
    Npm install web3

  4. 到目前为止, 项目模板已经创建成功了。可以使用Visual Studio Code 进行编程。

  5. 打开Visual Studio Code, File -> Open Workspace. 指定到刚才模板创建的路径上。
  6. 点导航栏左上角的+创建一个文件, index.js
    img
  7. 贴入下面的代码,就可以通过web3.js连入到私有链上。按下F5,可以进入运行模式查看结果。代码如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    const Web3 = require('web3');
    /*
    * connect to ethereum node
    */
    const ethereumUri = 'http://mynodes.centralus.cloudapp.azure.com:8540/';
    let web3 = new Web3();
    web3.setProvider(new web3.providers.HttpProvider(ethereumUri));

    if(!web3.isConnected()){
    throw new Error('unable to connect to ethereum node at ' + ethereumUri);
    }else{
    console.log('connected to ehterum node at ' + ethereumUri);
    let coinbase = web3.eth.coinbase;
    console.log('coinbase:' + coinbase);
    let balance = web3.eth.getBalance(coinbase);
    console.log('balance:' + web3.fromWei(balance, 'ether') + " ETH");
    let accounts = web3.eth.accounts;
    console.log(accounts);
  8. ethereumUri指定的是私有链提供的地址. 网上能查询到的大多数的案例都是使用本机的地址http://localhost:8450. 但是我在这个测试的例子中,是将私有链部署在Azure上,可以在Azure Portal上面查看到对应的JSON RPC URL信息。把ethereumUri指定到对应的URL上面。

  9. 调用isConnected() 可以检查连接到私有链上是否成功。如果调用这个方法失败,最常见的原因就是在之前的步骤中安装的web3 的版本不对,太新或者太旧。可以在上面3#安装web3.js的时候指定版本号
  10. coinbase 就是 私有链上node的Authority account位址。这个account在创世块的文件中能够找到。

编译智能合约

  1. 要部署智能合约,首先要在项目上安装solc.js.
    npm install solc
  2. 在Visual Studio Code中创建一个项目文件夹 Contracts. 创建一个智能合约文件Test.sol, 代码如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    pragma solidity ^0.4.11;
    contract sample {
    string public name = "ZeonLab";
    function set(string _name) {
    name = _name;
    }

    function get() constant returns (string) {
    return name;
    }
    }
  3. 在index.js文件中,加载 fs 和 solc模块。加入smart contract的编译部分。代码如下:

    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
    42
    const Web3 = require('web3');
    const fs = require('fs');
    const solc = require('solc');

    /*
    * connect to ethereum node
    */
    const ethereumUri = 'http://mynodes.centralus.cloudapp.azure.com:8540/';
    const address = '0x35849250780dc951adadf138492bec762784f208'; // user

    let web3 = new Web3();
    web3.setProvider(new web3.providers.HttpProvider(ethereumUri));

    if(!web3.isConnected()){
    throw new Error('unable to connect to ethereum node at ' + ethereumUri);
    }else{
    if (web3.personal.unlockAccount(address, 'M!crosoft20120731')) {
    console.log(`${address} is unlocaked`);
    }else{
    console.log(`unlock failed, ${address}`);
    }
    let balance = web3.fromWei(web3.eth.getBalance(address));
    console.log(balance);
    }

    /*
    * Compile Contract and Fetch ABI
    */
    let source = fs.readFileSync("./contracts/Test.sol", 'utf8');

    console.log('compiling contract...');
    let compiledContract = solc.compile(source);
    console.log('done');

    for (let contractName in compiledContract.contracts) {
    // code and ABI that are needed by web3

    var bytecode = compiledContract.contracts[contractName].bytecode;
    var abi = JSON.parse(compiledContract.contracts[contractName].interface);
    }

    console.log(JSON.stringify(abi, undefined, 2));
    • fs.readFileSync是用来读取test.sol文件里面的内容
    • solc.compile(source) 是对smart contract进行编译,最后生成ABI。
    • ABI里面有test.sol的各种定义
    • 相同的代码,执行完编译之后应该是获得相同的bytecode以及ABI。我们可以用这两个object来验证其他人deploy的smart contract的code的真伪。

关于ABI (Application Binary Interface)的描述,看这里 https://solidity.readthedocs.io/en/develop/abi-spec.html

部署智能合约

编译智能合约是在开发环境中对代码进行编译并且转换成为bytecode和ABI。这个步骤并不需要涉及到node上的操作。部署智能合约,就是将一段代码发布到node上面,就跟在node上面做一个transaction 是一样的性质。为了发布smart contract就需要付交易的手续费(gas). 所以要求一个有足够ETH的账号,在发布的时候需要用这个账号来发布。在上面的步骤中,通过portal已经给自己的账号发了一些ETH, 所以ETH应该不是问题。

因为在默认的情况下,账户是锁定状态,这就意味着不能从这个账号发送交易,智能进行查询。接下来需要调用web3.personal.unlockAccount方面解锁这个账号才能做到提供交易所需要的手续费。为了解锁账户,需要提供密码,这个密码就是与账户关联的私钥,从而运行用账户来签署交易。

代码如下:

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
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
const Web3 = require('web3');
const fs = require('fs');
const solc = require('solc');

/*
* connect to ethereum node
*/
const ethereumUri = 'http://mynodes.centralus.cloudapp.azure.com:8540/';
const address = '0x35849250780dc951adadf138492bec762784f208'; // user

let web3 = new Web3();
web3.setProvider(new web3.providers.HttpProvider(ethereumUri));

if(!web3.isConnected())
{
throw new Error('unable to connect to ethereum node at ' + ethereumUri);
}
else
{
if (web3.personal.unlockAccount(address, 'M!crosoft20120731'))
{
console.log(`${address} is unlocaked`);
}else
{
console.log(`unlock failed, ${address}`);
}

let balance = web3.fromWei(web3.eth.getBalance(address));
console.log(balance);
}

/*
* Compile Contract and Fetch ABI
*/
let source = fs.readFileSync("./contracts/Test.sol", 'utf8');

console.log('compiling contract...');
let compiledContract = solc.compile(source);
console.log('done');

for (let contractName in compiledContract.contracts) {
// code and ABI that are needed by web3
// console.log(contractName + ': ' + compiledContract.contracts[contractName].bytecode);
// console.log(contractName + '; ' + JSON.parse(compiledContract.contracts[contractName].interface));
var bytecode = compiledContract.contracts[contractName].bytecode;
var abi = JSON.parse(compiledContract.contracts[contractName].interface);
}

console.log(JSON.stringify(abi, undefined, 2));

/*
* deploy contract
*/
let gasEstimate = web3.eth.estimateGas({data: '0x' + bytecode});
console.log('gasEstimate = ' + gasEstimate);
let MyContract = web3.eth.contract(abi);
console.log('deploying contract...');
let myContractReturned = MyContract.new( {
from: address,
data: '0x'+ bytecode,
gas: gasEstimate * 2
}, function (err, myContract) {
if (!err)
{
// NOTE: The callback will fire twice!
// Once the contract has the transactionHash property set and once its deployed on an address.

// e.g. check tx hash on the first call (transaction send)
if (!myContract.address) {
console.log(`myContract.transactionHash = ${myContract.transactionHash}`); // The hash of the transaction, which deploys the contract
// myContract.transactionHash : 0xdbcd587c157e3b74ca0e8fff07a82160bbc6a4eebb7520e14d5783cc4eb24443
// check address on the second call (contract deployed)
} else {
console.log(`myContract.address = ${myContract.address}`); // the contract address
// myContract.address : 0xb6b3582d40f06832b4658b89df0d8b5bd14b1450
global.contractAddress = myContract.address;

var CoursetroContract = web3.eth.contract(abi);
var Coursetro = CoursetroContract.at(myContract.address);
console.log(Coursetro.toString());
console.log(Coursetro.safeAdd(15,25))
}

// Note that the returned "myContractReturned" === "myContract",
// so the returned "myContractReturned" object will also get the address set.
}
else
{
console.log(err);
}
});

(function wait () {
setTimeout(wait, 1000);
})();

在部署智能合约的时候, 如果只传入bytecode,执行的时候会发送下面的invalid params 错误。这是因为按照JSON RPC 2.0的规范,数值要以0x开头。Solc.js产生的bytecode并没有包含0x的开头,所以需要手工加入。

img

在unlock account的时候有可能会碰到 The method personal_unlockAccount does not exist/is not available 的错误。可以参考这篇文章FIX – The method personal_unlockAccount does not exist/is not available

调用智能合约

在部署完智能合约以后,会获得一个contract address. 在调用smart contract的时候这个地址作为参数送过去。代码如下:

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
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
const Web3 = require('web3');

const ethereumUri = 'http://mynodes.centralus.cloudapp.azure.com:8540/';
const address = '0x35849250780dc951adadf138492bec762784f208'; // user

let web3 = new Web3();
web3.setProvider(new web3.providers.HttpProvider(ethereumUri));

if(!web3.isConnected())
{
throw new Error('unable to connect to ethereum node at ' + ethereumUri);
}
else
{
if (web3.personal.unlockAccount(address, 'M!crosoft20120731'))
{
console.log(`${address} is unlocaked`);
}
else
{
console.log(`unlock failed, ${address}`);
}
}

var abi = [
{
"constant": true,
"inputs": [],
"name": "name",
"outputs": [
{
"name": "",
"type": "string"
}
],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": false,
"inputs": [
{
"name": "_name",
"type": "string"
}
],
"name": "set",
"outputs": [],
"payable": false,
"stateMutability": "nonpayable",
"type": "function"
},
{
"constant": true,
"inputs": [],
"name": "get",
"outputs": [
{
"name": "",
"type": "string"
}
],
"payable": false,
"stateMutability": "view",
"type": "function"
}
];


var contractAddress = "0x4a6761871b363f57b0e73103cb8bc067dd9e5661";

var sampleContract = web3.eth.contract(abi);
var sampleContractInstance = sampleContract.at(contractAddress);
console.log(sampleContractInstance.get.call());

参考

[1]. The General Theory of Decentralized Applications, Dapps
[2]. What is a DApp
[3]. Ethereum Homestead – Dapp browsers
[4]. How to install Metamask
[5]. How to install Node.js and NPM on Windows
[6]. Solidity Integration with Visual Studio

Sonic Guo