原文链接:醒者呆的博客园,https://www.cnblogs.com/Evsward/p/contract.html
这两天被老大搞去搬砖,学习计划有变但无大碍,这篇文章将仔细分析智能合约相关内容。
关键字:智能合约,remix,Solidity,truffle,geth,leveldb,datadir,ganache,web3j
合约也称合同、协议,是甲乙双方参与的,制定一系列条目规范双方权利与义务的文件。智能合约是电子化的,自动执行的,去中心化的,具有不可抵赖性,本质上它是一段代码,依托于区块链技术,它可以做很多事情,基于以太坊的智能合约可以让你的区块链扩展出任何你想要的功能。
我相信,智能合约是区块链的未来,因为基于它能做的商业模型太多样了,远远不仅是数字货币一种。
智能合约的编程语言是Solidity,扩展名为.sol,它是基于C++、JavaScript、Python创造而来的,这里是官方文档。
Solidity是静态类型的,支持继承,有自己的函数库,它同样支持面向对象语言的自定义类型等其他功能。
Solidity编写的智能合约代码运行在EVM,即以太坊虚拟机,正如java编写的代码运行在JVM一样,在同一个区块链中每一个结点的EVM都是相同的运行环境。通过智能合约,可以开发匿名投票、匿名拍卖、众筹以及多重签名的钱包等,以太坊每一个结点可以有多个账户,所以每个结点都可以称作钱包,可以管理名下的账户,以及转账、挖矿等操作。
官方推荐IDE:Remix
其实Solidity智能合约开发的IDE有很多,官方推荐的Remix是基于浏览器的,运行环境可以切换:
我使用以后,觉得浏览器的方式还是不习惯,尤其保存的文件无故消失,让我始终心有余悸,经过调研,下面我们将采用goLand,安装Intellij-Solidity-2.0.4插件的方式开发智能合约,然后使用Remix环境进行智能合约的部署。当然我们也可以使用Remix进行运行、测试以及调试工作,下面酌情展示。
区块链中比较有意思的命名,相当于手续费但又有些不同。gas为天然气,用来代表我们程序运行所有的能耗,当发生交易等操作时会消耗相应的gas,gas的计算方式是
gas 单价 × gas 数量
其中gas单价是由用户,像我们这样的发起者愿意为此次操作付出多少以太币而定的(相当于你开车上路前愿意给你的油箱加多少油,假设你的油箱是无限大的)。gas数量是程序根据你操作的复杂度自动定义的。
智能合约也是一样的,当一个发起者部署运行一段智能合约时,以太坊会收取gas费用,就像汽车行驶需要烧油一样,直到你的智能合约运行完毕,“油箱”中剩余的gas会退还给你,如果你的代码死循环了,耗尽了你“油箱”中的gas,那么以太坊会自动报出异常停止你的智能合约。我们在学习智能合约阶段,可以使用testnet环境来避免真的花费以太币。
Dapp为Solidity提供了源码构建工具,包管理工具,单元测试以及智能合约部署,一会儿我们看看是否必须要用它。有时它也被称作去中心化的应用程序(Decentralized App)。这种应用程序除了有一段代码的智能合约以外,还需要UI,UE设计等,正如apple的app开发,我们未来的目标之一可以是开发自己的Dapp。
首先要开启一个本地的EVM,前面的文章对Geth做了详细的介绍,这里直接启动一个本地开发模式的结点。
`geth --datadir testNet --dev console 2>>Documents/someLogs/testGeth.log`
简介一下geth的参数选项:
Ephemeral proof-of-authority network with a pre-funded developer account, mining enabled
短暂的认证证明网络,同时创建一个预存款很多钱的一个开发者账户,并自动开始挖矿。
datadir,指定结点文件目录,如果没有会自动创建一个,该目录包含:
以上目录中元素精解:
① nodekey
结点之间相互寻找是通过一个发现协议:一个基于S/Kademlia的网络协议。这个协议会把包含IP地址的公钥联系起来。实际上在结点之间的peer连接使用的是一个完全不同的,加密的协议(RLPX)。RLPX加密的工作方式需要远程终端连接发起者的公钥作为身份识别。本质上来说,这个key链接了发现协议和RLPX。
你可以随时删除这个nodekey,重启的时候会自动生成一个新的。
② keystore/UTC–2018-02-06T03-46-35.626115529Z–740b9c48d67cf333c8b1c0e609b6b90b40d3cdea
这是存储结点私钥的位置,文件名为时间戳加上本地账户拼成的字符串。打开文件,内容为一个json,格式化以后为:
{
"address": "740b9c48d67cf333c8b1c0e609b6b90b40d3cdea", "comment":"本地账户地址",
"crypto": {
"cipher": "aes-128-ctr", "comment":"加密协议采用的是AES-128",
"ciphertext": "b331a3dbdde9abd14991116ac0bb1b742f22edda162b567974f8fbf1d694daef", "comment":"密文",
"cipherparams": {
"iv": "06d0df7a5b7160da852fbb01339149ae", "comment":"加密参数"
},
"kdf": "scrypt", "comment":"Key Derivation Function, 将短密码加盐hash成长密码,防彩虹表、防暴力破解",
"kdfparams": {
"dklen": 32, "comment":"KDF加密参数",
"n": 262144,
"p": 1,
"r": 8,
"salt": "6ffbd23fac4ed386aac703bc180f50be02690bef5239057a34dde4dd4de2416b", "comment":"盐值,加盐加密"
},
"mac": "06b7d92b98a3b732dc1e63e7e09b8e3d79a9e8e1d43ee7a1b40482db295ea367", "comment":"message authentication code,消息认证码"
},
"id": "ff7e243a-150e-45f6-ac64-06b0ed2e68ec", "comment":"文件主键",
"version": 3
}
这部分范畴属于密码学方面了,可以参考《应用密码学初探》
③transactions.rlp
RLP(Recursive Length Prefix),递归长度前缀。是以太坊中用于序列号对象的主要编码方法。根据文件名可以猜出,这是所有交易的序列化对象文件。
④ chaindata
数据库采用leveldb,存储了区块数据以及状态数据。该目录下打包存储以.ldb为扩展名的每个区块的数据文件。每个块文件有容量的最大值,目前我本机默认的是2.1M,我们设想一下目前以太坊的区块高度为5039768,如果一个块是2.1M的话,那么整个区块链的数据大小为10TB。
⑤ leveldb
Google出品的另一利器,使用C++编写,基于LSM(Log-Structured-Merge Tree)日志结构化合并树,是一个高效的键值对存储系统,是没有Sql语句的非关系型数据库。键值对均采用字符串类型,按照key排序。
特点包括:
局限性包括:
console命令在EVM启动的同时开启了一个交互控制台,后面的一串命令是将输出的log转存到文件testGeth.log中去,启动时的日志文件:
WARN [02-06|11:46:35] No etherbase set and no accounts found as default
INFO [02-06|11:46:37] Using developer account address=0x740b9C48D67Cf333C8b1c0E609b6b90b40D3CdeA
INFO [02-06|11:46:37] Starting peer-to-peer node instance=Geth/v1.7.3-stable-4706005b/linux-amd64/go1.9.2
INFO [02-06|11:46:37] Allocated cache and file handles database=/home/liuwenbin/testNet/geth/chaindata cache=128 handles=1024
INFO [02-06|11:46:37] Writing custom genesis block
INFO [02-06|11:46:37] Initialised chain configuration config="{ChainID: 1337 Homestead: 0 DAO: DAOSupport: false EIP150: 0 EIP155: 0 EIP158: 0 Byzantium: 0 Engine: clique}"
INFO [02-06|11:46:37] Initialising Ethereum protocol versions="[63 62]" network=1
INFO [02-06|11:46:37] Loaded most recent local header number=0 hash=593c0e…256b90 td=1
INFO [02-06|11:46:37] Loaded most recent local full block number=0 hash=593c0e…256b90 td=1
INFO [02-06|11:46:37] Loaded most recent local fast block number=0 hash=593c0e…256b90 td=1
INFO [02-06|11:46:37] Regenerated local transaction journal transactions=0 accounts=0
INFO [02-06|11:46:37] Starting P2P networking
INFO [02-06|11:46:37] started whisper v.5.0
INFO [02-06|11:46:37] RLPx listener up self="enode://ede08b763001ed3642e0b3860d57e694489bcc1f47dde8563f2577bdec48e6949748826d9b88f55f456af2ae1e75ce2ea04a59eb0ef1c2c53330be92e44e6515@[::]:46591?discport=0"
INFO [02-06|11:46:37] Transaction pool price threshold updated price=18000000000
INFO [02-06|11:46:37] IPC endpoint opened: /home/liuwenbin/testNet/geth.ipc
INFO [02-06|11:46:37] Starting mining operation
INFO [02-06|11:46:37] Commit new mining work number=1 txs=0 uncles=0 elapsed=53.048µs
我们逐行分析,
1. 启动时第一行并未找到以太坊base的设置以及默认账户。
2. 说明使用了开发者账户,后面给出了账户地址。
3. 开始p2p网络结点,实例采用的是基于go1.9.2版本的geth实例。
4. 分配缓存和文件句柄(打开文件的唯一标识,给一个文件、设备、socket或管道一个名字,隐藏关联细节),数据库位置在/home/liuwenbin/testNet/geth/chaindata,缓存大小为128M, 文件句柄数为1024。
5. 写入当前创世块。
6. 初始化链配置,展示配置信息。
7. 初始化以太坊协议。
8. 载入大部分最近的本地数据头
9. 载入大部分最近的本地完整块数据
10. 载入大部分最近的本地最高块数据
11. 重新生成本地交易账本
12. 开始p2p网络
13. 开始whisper
14. RLPx开始监控,并打印出当前enode信息
15. 交易池价格阀值更新,价格为=18000000000
16. IPC端点开启:/home/liuwenbin/testNet/geth.ipc
17. 开始挖矿操作
18. 提交新的挖矿工作
下面在console中查看一下当前账户的余额,发现开发环境默认给分配的余额太大,并不好测试,那么我们自己再创建一个用户,余额为0,然后用第一个“大款”账户转账给新创建用户1个以太币。
> eth.sendTransaction({from: '0x740b9c48d67cf333c8b1c0e609b6b90b40d3cdea',to:'0x1d863371462223910a1f05329b6dea0b0f9c49f8',value:web3.toWei(1,"ether")})
"0xb456244e4fb25b74108f05afe53670b5f1a857f5671e7d3fa2e221419d04382c"
> eth.getBalance(eth.accounts[1])
333333333333333333
我发现一个事,之前乘三那个geth还存在呢(捂脸笑出泪),让我改一下吧。改后我重新部署了geth命令,然后将新建用户的3个以太转回大款账户,由于gas的存在(实际上即使转账时你自己指定,也是基于一个最小值,往多了给,如果低于这个最小值,就会报错:“你加的油太少啦,我根本跑不过去”。所以最终费了大力,让新账户保留下了
> eth.getBalance(eth.accounts[1])
79000
这79000wei的以太币是无法转出去了,因为我的余额付不起油钱。实际上79000这个数字可读性还行,所以拿这个测试也可以。
上面说道了我们采用goLand安装Solidity插件的方式来开发智能合约。JetBrain系列IDE插件的安装我就不介绍了,网上随便查。下面我们开始编码:
pragma solidity ^0.4.0;
contract helloworld {
string content;
function helloworld(string _str) public {
content = _str;
}
function getContent() constant public returns (string){
return content;
}
}
代码编写很简单,我们逐行解读:
1. 通过关键字pragma标识Solidity的版本为0.4.0,我们下面的代码都会采用该版本来编译。
2. contract关键字定义一个合约,它可以有自己的方法,自己的属性(智能合约里面更愿意称为状态),将会存储在区块链中特定的地址。
3. 声明了一个字符串类型(注意首字母小写的类型关键字string)的content状态(叫做属性、成员变量都可以)
4. 通过关键字function定义一个构造方法,需要传入一个字符串数据,注意该方法的权限public被标识在了参数列表的后面。
5. 通过该方法赋值给状态content(注意不用使用this),方法的参数变量名采用了下划线开头的方式用来代表该变量的作用域很小,是私有变量,这是编程语言中的一种约定俗成的命名规则。
6. 通过关键字function定义一个打印方法,返回状态content的值,注意除了public权限以外,public的前侧还有一个constant关键字,后侧还通过关键字returns定义了返回值类型。
上面我们使用了goLand的Solidity插件进行了合约代码的开发,然而该插件的功能仅包括:
1. 语法高亮,代码提示
2. 代码完整性检查
3. 文件模板
4. goto声明
5. Find usages
6. 代码格式化
可以说都是针对编码辅助的操作,然而若我们要部署智能合约,还得回到Remix,我们新建一个sol文件,粘贴进去上面写好的helloworld代码,然后点击右侧Details,弹出的界面包含了名字、字节码、元数据等内容,我们只要其中的WEB3DEPLOY,复制出其中内容,将第一行传入参数“hello world”:
var string_str = "hello world" ;
var helloworldContract = web3.eth.contract([{"constant":true,"inputs":[],"name":"getContent","outputs":[{"name":"","type":"string"}],"payable":false,"stateMutability":"view","type":"function"},{"inputs":[{"name":"string_str","type":"string"}],"payable":false,"stateMutability":"nonpayable","type":"constructor"}]);
var helloworld = helloworldContract.new(
string_str,
{
from: web3.eth.accounts[0],
data: '0x6060604052341561000f57600080fd5b6040516102b83803806102b8833981016040528080518201919050508060009080519060200190610041929190610048565b50506100ed565b828054600181600116156101000203166002900490600052602060002090601f016020900481019282601f1061008957805160ff19168380011785556100b7565b828001600101855582156100b7579182015b828111156100b657825182559160200191906001019061009b565b5b5090506100c491906100c8565b5090565b6100ea91905b808211156100e65760008160009055506001016100ce565b5090565b90565b6101bc806100fc6000396000f300606060405260043610610041576000357c0100000000000000000000000000000000000000000000000000000000900463ffffffff16806359016c7914610046575b600080fd5b341561005157600080fd5b6100596100d4565b6040518080602001828103825283818151815260200191508051906020019080838360005b8381101561009957808201518184015260208101905061007e565b50505050905090810190601f1680156100c65780820380516001836020036101000a031916815260200191505b509250505060405180910390f35b6100dc61017c565b60008054600181600116156101000203166002900480601f0160208091040260200160405190810160405280929190818152602001828054600181600116156101000203166002900480156101725780601f1061014757610100808354040283529160200191610172565b820191906000526020600020905b81548152906001019060200180831161015557829003601f168201915b5050505050905090565b6020604051908101604052806000815250905600a165627a7a72305820f4bd9a6659a8625f89177c604c901764cf9cca4fa8aa2e792525da3647ca7a510029',
gas: '4700000'
}, function (e, contract){
console.log(e, contract);
if (typeof contract.address !== 'undefined') {
console.log('Contract mined! address: ' + contract.address + ' transactionHash: ' + contract.transactionHash);
}
})
仔细观察上面的代码,Remix帮我们将代码转成了EVM可识别的样子,也就是将Solidity代码编译成web3的版本,其中也帮我们估算好了gas的金额,当我们执行这段合约时会自动扣掉我们余额中相应的数值作为gas费用。
接着,我们回到console,先解锁智能合约发布者的账号,我们选择刚才新建的
> personal.unlockAccount(eth.accounts[1],"lwb")
true
然后将上面的web3版的代码复制过来,回车,输出:
Contract mined! address: 0x71db931bdb2f9516cf892aa0c620bd686d1095e5 transactionHash: 0x6e39a97dd2f260517bedeb9934cf88430526b46a379d5680cc092d8ea3f44602
合约被挖出,打印出来了合约地址,交易hash(这在以太坊中也被认定为是一笔交易,我们付费gas给以太坊)。
然后继续在console中输入
> helloworld.getContent()
"hello world"
由于我们余额是79000,上面gas给预估的是4700000,所以预想结果是您的余额不足,合约无法运行,然而合约部署运行成功了。
我们从大款那再转账一个以太币过来。然后关闭重启geth console,重复上面的操作。
TODO: 余额仍旧未减少。不知道gas扣到哪去了。
同步查看日志输出:
INFO [02-06|17:36:34] Submitted contract creation fullhash=0x6e39a97dd2f260517bedeb9934cf88430526b46a379d5680cc092d8ea3f44602 contract=0x71DB931bdb2f9516Cf892aA0c620bD686D1095E5
INFO [02-06|17:36:34] Commit new mining work number=18 txs=1 uncles=0 elapsed=313.823µs
INFO [02-06|17:36:34] Successfully sealed new block number=18 hash=37913b…f101af
INFO [02-06|17:36:34] ? mined potential block number=18 hash=37913b…f101af
每当我们提交了一个合约,
#Solidity语法
上面使用Solidity编写了一个helloworld智能合约,稍显力不从心,下面我们专门来学习一下Solidity语法,为未来我们编写复杂的智能合约工程打下基础。
学习一门新的编程语言,首先要看它的类型,Solidity是静态类型语言,跟java一样,也就是说在编译之前都要指定好每个变量的具体类型。类型可以分为值类型和引用类型,与java类似。
值类型作为参数时永远传的是值,每一次入参出参都是内存中值的副本。包括:
下面是针对以上类型的字面量类型:
字面量是一种针对某种值的表示法,简单来说,就是变量赋值时必须是等号右边的部分。
mapping类型就是键值对,现在最新语言都会给自身增加键值对数据结构的封装支持。mapping的声明方式为:
mapping(_KeyType => _ValueType)
键值对中间通过一个“=>”连接。元素内容,Solidity类型均可,与其他键值对使用差不多,遇到问题再深入研究。
关于Solidity其他语法这里暂不过多介绍,掌握以上Solidity的类型知识,我想其他语法可以在实战中解决掉。下面会以“Solidit语法补充说明”的形式对新遇到的语法问题进行补充研究。
上面我们开发部署运行智能合约helloworld时,编码是在goLand,编译是在Remix,部署运行是在geth console,感觉好混乱,也不适合大规模工程开发,是否有一种工具可以集成这一切?
Truffle!
由于truffle是依赖于nodejs,可能会有版本不兼容的问题,因此要先完全删除你机器上的nodejs和npm,然后再安装纯净版的nodejs,npm,truffle,请按照以下命令进行。
sudo apt-get remove nodejs
sudo apt-get remove npm
sudo apt-get update
which node
wget https://nodejs.org/dist/v8.8.0/node-v8.8.0-linux-x64.tar.gz
sudo tar -xf node-v8.8.0-linux-x64.tar.gz --directory /usr/local --strip-components 1
node --version
npm --version
sudo npm install -g truffle
此时应该可以直接使用命令truffle了,下面我们建立一个工作间truffle-workspace,然后在工作间执行:
mkdir MetaCoin
cd MetaCoin
truffle unbox metacoin
原来使用truffle init,但现在它存在于unbox。
Truffle 的盒子Boxs装有很多非常实用的项目样板,可以让你忽略一些环境配置问题,从而可以集中与开发你自己的DApp的业务唯一性。除此之外,Truffle Boxes能够容纳其他有用的组件、Solidity合约或者库,前后端视图等等。所有这些都是一个完整的实例Dapp程序。都可以下载下来逐一研究,寻找适合自己公司目前业务模型的组件。
Truffle的官方Boxes地址
可以看到,现在官方盒子还不多,总共7个,有三个是关于react的,两个是truffle自己的项目,可以下载体验,剩下两个是我们比较关心的,一个是metacoin,非常好的入门示例,另一个是webpack,顾名思义,它是一套比起metacoin更加完整的模板的存在。既然我们是初学,下面我们就从metacoin入手学习。
进入metacoin目录,当前目录已经被初始化成一个新的空的以太坊工程,目录结构如下:
pragma solidity ^0.4.17;
contract Migrations {
address public owner;
uint public last_completed_migration;
modifier restricted() {
if (msg.sender == owner) _;
}
function Migrations() public {
owner = msg.sender;
}
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);
}
}
上面我们学习了Solidity具体的类型语法,我们来分析一下这个文件:
modifier的使用方法,就看上面的Migrations合约的例子即可,它可以自动改变函数的行为,例如你可以给他预设一个条件,他会不断检查,一旦符合条件即可走预设分支。它可以影响当前合约以及派生合约。
pragma solidity ^0.4.11;
contract owned {
function owned() public { owner = msg.sender; }
address owner;
// 这里仅定义了一个modifier但是没有使用,它将被子类使用,方法体在这里“_;”,这意味着如果owner调用了这个函数,函数会被执行,其他人调用会抛出一个异常。
modifier onlyOwner {
require(msg.sender == owner);
_;
}
}
// 通过is关键字来继承一个合约类,mortal是owned的子类,也叫派生类。
contract mortal is owned {
// 当前合约派生了owned,此方法使用了父类的onlyOwner的modifier
// public onlyOwner, 这种写法挺让人困惑,下面给出了我的思考,暂理解为派生类要使用基类的modifier。
function close() public onlyOwner {
selfdestruct(owner);
}
}
contract priced {
// Modifiers可以接收参数
modifier costs(uint price) {
// 这里modifier方法体是通过条件判断,是否满足,满足则执行“_;”分支。
if (msg.value >= price) {
_;
}
}
}
contract Register is priced, owned {
mapping (address => bool) registeredAddresses;
uint price;
// 构造函数给全局变量price赋值。
function Register(uint initialPrice) public { price = initialPrice; }
// payable关键字重申,如果不声明的话,函数关于以太币交易的操作都会被拒回。
function register() public payable costs(price) {
registeredAddresses[msg.sender] = true;
}
// 此派生类也要使用基类的modifier。
function changePrice(uint _price) public onlyOwner {
price = _price;
}
}
contract Mutex {
bool locked;
modifier noReentrancy() {
require(!locked);
locked = true;
_;
locked = false;
}
function f() public noReentrancy returns (uint) {
require(msg.sender.call());
return 7;
}
}
又延伸出来一个盲点:require关键字,它是错误判断,提到assert就懂了,官方文档的解释为:
require(bool condition):
throws if the condition is not met - to be used for errors in inputs or external components.
总结一下modifier:
限制访问一种针对合约的常见模式。但其实你永远不可能限制得了任何人或电脑读取你的交易内容或者你的合同状态。你可以使用加密增大困难,但你的合约就是用来读取数据的,那么其他人也会看到。所以,其实上面的modifier onlyOwner是一个特别好的可读性极高的限制访问的手段。
那么restricted关键字如何使用呢?
好吧,我刚刚带着modifier的知识重新看了上面的Migrations合约的内容发现,restricted并不是关键字,而是modifier的方法名,在其下的想增加该modifier功能的函数中,都使用了public restricted的方式来声明。
说到这里,我又明白了为什么要使用public onlyOwner这种写法,因为public是函数可见性修饰符,onlyOwner是自定义的限制访问的modifier方法,他们都是关于函数使用限制方面的,所以会写在一起,可以假想一个括号将它俩括起来,他们占一个位置,就是原来属于public|private|internal|external的那个位置。
这一点很重要了,我们研究一下Solidity自身携带的特殊变量以及函数:
1. block.blockhash(uint blockNumber) returns (bytes32): 返回参数区块编号的hash值。(范围仅限于最近256块,还不包含当然块)
2. block.coinbase (address): 当前区块矿工地址
3. block.difficulty (uint): 当前区块难度
4. block.gaslimit (uint): 当前区块的gaslimit
5. block.number (uint): 当前区块编号
6. block.timestamp (uint): 当前区块的timestamp,使用UNIX时间秒
7. msg.data (bytes): 完整的calldata
8. msg.gas (uint): 剩余的gas
9. msg.sender (address): 信息的发送方 (当前调用)
10. msg.sig (bytes4): calldata的前四个字节 (i.e. 函数标识符)
11. msg.value (uint): 消息发送的wei的数量
12. now (uint): 当前区块的timestamp (block.timestamp别名)
13. tx.gasprice (uint): 交易的gas单价
14. tx.origin (address): 交易发送方地址(完全的链调用)
msg有两个属性,一个是msg.sender,另一个是msg.value,这两个值可以被任何external函数调用,包含库里面的函数。
注意谨慎使用block.timestamp, now and block.blockhash,因为他们都是有可能被篡改的。
pragma solidity ^0.4.18;
import "./ConvertLib.sol";
// 这是一个简单的仿币合约的例子。它并不是标准的可兼容其他币或token的合约,
// 如果你想创建一个标准兼容的token,请转到 https://github.com/ConsenSys/Tokens(TODO:一会儿我们再过去转)
contract MetaCoin {
mapping (address => uint) balances;// 定义了一个映射类型变量balances,key为address类型,值为无符整型,应该是用来存储每个账户的余额,可以存多个。
event Transfer(address indexed _from, address indexed _to, uint256 _value);// Solidity语法event,TODO:见下方详解。
function MetaCoin() public {// 构造函数,tx.origin查查上面,找到它会返回交易发送方的地址,也就是说合约实例创建时会默认为当前交易发送方的余额塞10000,单位应该是你的仿币。
balances[tx.origin] = 10000;
}
function sendCoin(address receiver, uint amount) public returns(bool sufficient) {// 函数声明部分没有盲点,方法名,参数列表,函数可见性,返回值类型定义。
if (balances[msg.sender] < amount) return false;// 如果余额不足,则返回发送币失败
balances[msg.sender] -= amount;// 否则从发送方余额中减去发送值,注意Solidity也有 “-=”,“+=” 的运算符哦
balances[receiver] += amount;// 然后在接收方的余额中加入发送值数量。
Transfer(msg.sender, receiver, amount);// 使用以上event关键字声明的方法
return true;
}
function getBalanceInEth(address addr) public view returns(uint){// 获取以太币余额
return ConvertLib.convert(getBalance(addr),2);// 调用了其他合约的方法,TODO:稍后介绍ConvertLib合约时说明。
}
function getBalance(address addr) public view returns(uint) {// 获取当前账户的仿币余额
return balances[addr];
}
}
Events allow the convenient usage of the EVM logging facilities, which in turn can be used to “call” JavaScript callbacks in the user interface of a dapp, which listen for these events.
Events提供了日志支持,进而可用于在用户界面上“调用”dapp JavaScript回调,监听了这些事件。简单来说,我们的DApp是基于web服务器上的web3.js与EVM以太坊结点进行交互的,而智能合约是部署在EVM以太坊结点上的。举一个例子:
contract ExampleContract {
// some state variables ...
function foo(int256 _value) returns (int256) {
// manipulate state ...
return _value;
}
}
合约ExampleContract有个方法foo被部署在EVM的一个结点上运行了,此时用户如果想在DApp上调用合约内部的这个foo方法,如何操作呢,有两种办法:
1. var returnValue = exampleContract.foo.call(2);// 通过web3 的message的call来调用。
2. 合约内部再声明一个event ReturnValue(address indexed _from, int256 _value);并在foo方法内使用该event用来返回方法执行结果。
第一种办法在方法本身比较耗时的情况下会阻塞,或者不会获取到准确的返回值。所以采用第二种办法:就是通过Solidity的关键字event。event在这里就是一个回调函数的概念,当函数运行结束以后(交易进块),会通过event返回给web3,也就是DApp用户界面相应的结果。这是以太坊一种客户端异步调用方法。关于这个回调,要在DApp使用web3时显示编写:
exampleEvent.watch(function(err, result) {
if (err) {
console.log(err)
return;
}
console.log(result.args._value)
// 检查合约方法是否反返回结果,若有则将结果显示在用户界面并且调用exampleEvent.stopWatching()方法停止异步回调监听。
})
写Solidity最大的不同在于,我们要随时计算好我们的gas消耗,方法的复杂度,变量类型的存储位置(memory,storage等等)都会决定gas的消耗量。
使用event可以获得比storage更便宜的gas消耗。
总结一下event,就是如果你的Dapp客户端web3.js想调用智能合约内部的函数,则使用event作为桥梁,它能方便执行异步调用同时又节约gas消耗。
pragma solidity ^0.4.4;
library ConvertLib{
function convert(uint amount,uint conversionRate) public pure returns (uint convertedAmount)
{
return amount * conversionRate;
}
}
与MetaCoin智能合约不同的是,ConvertLib是由library声明的一个库,它只有一个方法,就是返回给定的两个无符整数值相乘的结果。返回到上面的MetaCoin中该库的使用位置去分析,即可知道,MetaCoin的仿币的价格是以太币的一倍,所以MetaCoin是以以太币为标杆,通过智能合约发布的一个token,仿币。
这似乎就可以很好地解决我在《以太坊RPC机制与API实例》文章中需要发布三倍以太币的token的需求了,而我们完全不必更改以太坊源码,但那篇文章通过这个需求的路线研究了以太坊的Go源码也算功不可没。
var Migrations = artifacts.require("./Migrations.sol");
module.exports = function(deployer) {
deployer.deploy(Migrations);
};
这个js文件是nodejs的写法,看上去它的作用就是部署了上面的Migrations智能合约文件。
var ConvertLib = artifacts.require("./ConvertLib.sol");
var MetaCoin = artifacts.require("./MetaCoin.sol");
module.exports = function(deployer) {
deployer.deploy(ConvertLib);
deployer.link(ConvertLib, MetaCoin);
deployer.deploy(MetaCoin);
};
这个文件是meatcoin智能合约的部署文件,里面约定了部署顺序,依赖关系。这里我们看到了MetaCoin智能合约是要依赖于库ConvertLib的,所以要先部署ConvertLib,然后link他们,再部署MetaCoin,这部分js的写法可以参照官方文档DEPLOYER API,主要就是介绍了一下deploy、link以及then三个方法的详细用法,不难这里不再赘述。
module.exports = {
// See
// to customize your Truffle configuration!
};
module.exports = {
// See
// to customize your Truffle configuration!
};
这两个文件也都是nodejs,他们都是配置文件,可能作用域不同,目前它俩是完全相同的(因为啥也没有)。我们去它推荐的网站看一看。给出了一个例子:
module.exports = {
networks: {
development: {
host: "127.0.0.1",
port: 8545,
network_id: "*" // Match any network id
}
}
};
这个例子展示了该配置文件可以配置网络环境,暂先到这,以后遇上了针对该配置文件进行研究。
This is a placeholder file to ensure the parent directory in the git repository. Feel free to remove.
翻译过来就是:placeholder文件是用来保证在git库中父级目录的,可以删除。
和下面的文件一样,他们的功能都是用来做单元测试的,truffle在编译期间会自动执行这些测试脚本。当前文件为js版本,模拟用户在DApp客户端用户界面操作的情形。
var MetaCoin = artifacts.require("./MetaCoin.sol"); // 这与1_initial_migration.js文件的头是一样的,引入了一个智能合约文件。
contract('MetaCoin', function(accounts) {
it("should put 10000 MetaCoin in the first account", function() {
return MetaCoin.deployed().then(function(instance) {
return instance.getBalance.call(accounts[0]);
}).then(function(balance) {
assert.equal(balance.valueOf(), 10000, "10000 wasn't in the first account");
});
});
it("should call a function that depends on a linked library", function() {
var meta;
var metaCoinBalance;
var metaCoinEthBalance;
return MetaCoin.deployed().then(function(instance) {
meta = instance;
return meta.getBalance.call(accounts[0]);
}).then(function(outCoinBalance) {
metaCoinBalance = outCoinBalance.toNumber();
return meta.getBalanceInEth.call(accounts[0]);
}).then(function(outCoinBalanceEth) {
metaCoinEthBalance = outCoinBalanceEth.toNumber();
}).then(function() {
assert.equal(metaCoinEthBalance, 2 * metaCoinBalance, "Library function returned unexpected function, linkage may be broken");
});
});
it("should send coin correctly", function() {
var meta;
// Get initial balances of first and second account.
var account_one = accounts[0];
var account_two = accounts[1];
var account_one_starting_balance;
var account_two_starting_balance;
var account_one_ending_balance;
var account_two_ending_balance;
var amount = 10;
return MetaCoin.deployed().then(function(instance) {
meta = instance;
return meta.getBalance.call(account_one);
}).then(function(balance) {
account_one_starting_balance = balance.toNumber();
return meta.getBalance.call(account_two);
}).then(function(balance) {
account_two_starting_balance = balance.toNumber();
return meta.sendCoin(account_two, amount, {from: account_one});
}).then(function() {
return meta.getBalance.call(account_one);
}).then(function(balance) {
account_one_ending_balance = balance.toNumber();
return meta.getBalance.call(account_two);
}).then(function(balance) {
account_two_ending_balance = balance.toNumber();
assert.equal(account_one_ending_balance, account_one_starting_balance - amount, "Amount wasn't correctly taken from the sender");
assert.equal(account_two_ending_balance, account_two_starting_balance + amount, "Amount wasn't correctly sent to the receiver");
});
});
});
我们来分析一波这个truffle metacoin js版本的单元测试:
这是官方文档,详细说明如何使用JS来编写智能合约的单元测试。
好下面来看看Solidity智能合约版本的单元测试。一般来讲,这种文件的命名规则是Test加待测智能合约的名字拼串组成。
pragma solidity ^0.4.2;
import "truffle/Assert.sol";
import "truffle/DeployedAddresses.sol";
import "../contracts/MetaCoin.sol";
contract TestMetacoin {
function testInitialBalanceUsingDeployedContract() public {
MetaCoin meta = MetaCoin(DeployedAddresses.MetaCoin());
uint expected = 10000;
Assert.equal(meta.getBalance(tx.origin), expected, "Owner should have 10000 MetaCoin initially");
}
function testInitialBalanceWithNewMetaCoin() public {
MetaCoin meta = new MetaCoin();
uint expected = 10000;
Assert.equal(meta.getBalance(tx.origin), expected, "Owner should have 10000 MetaCoin initially");
}
}
继续分析:
这是官方文档,详细说明如何使用Solidity来编写智能合约的单元测试。
键入
truffle compile
输出情况:
liuwenbin@liuwenbin-H81M-DS2:~/work/truffle-workspace/MetaCoin$ truffle compile
Compiling ./contracts/ConvertLib.sol...
Compiling ./contracts/MetaCoin.sol...
Compiling ./contracts/Migrations.sol...
Writing artifacts to ./build/contracts
根据编译输出的路径地址./build/contracts,我们去查看一下
liuwenbin@liuwenbin-H81M-DS2:~/work/truffle-workspace/build/contracts$ ls
ConvertLib.json MetaCoin.json Migrations.json
可以看到原来所在在contracts目录下的智能合约文件(有合约contract,有库library)均被编译成了json文件。
这些json文件就是truffle用来部署合约的编译文件,这与上面通过Remix编译的WEB3DEPLOY的js代码段不同。
移植,对这里叫移植,但下面我们仍使用“部署”这个词,truffle中部署的命令为:
truffle migrate
这里遇到的问题较多,我来一一解决:
以太坊客户端有很多,truffle自己就有一个ganache,但我没安装成功,下面列举一下:
当然了,我们还是继续使用geth,仍旧使用上面介绍过的启动命令启动
geth --datadir testNet --dev console 2>>Document/someLogs/testGeth.log
上文说到了,truffle.js是truffle的配置文件,启动好以太坊本地结点以后,我们需要让truffle去识别它并使用它,这就需要在truffle.js中配置相关属性:
module.exports = {
networks: {
development: {
host: "127.0.0.1",
port: 8545,
network_id: "*" // Match any network id
}
}
};
以上两个问题解决以后,我们使用truffle migrate来部署,terminal报错:
liuwenbin@liuwenbin-H81M-DS2:~/work/truffle-workspace/MetaCoin$ truffle migrate
Could not connect to your Ethereum client. Please check that your Ethereum client:
- is running
- is accepting RPC connections (i.e., "--rpc" option is used in geth)
- is accessible over the network
- is properly configured in your Truffle configuration file (truffle.js)
错误信息很清楚,直接增加一个参数–rpc,最终修改我们的启动命令为:
geth --datadir testNet --dev --rpc console 2>>Document/someLogs/testGeth.log
继续使用truffle migrate来部署,terminal及继续报错:
Error: exceeds block gas limit
去truffle github issues中查找,找到一行解决办法,粘贴如下:
Possibility: you’re giving the transaction too high of a gasLimit. If the transaction has a limit of 2,000,000, it’d stop you since it could theoretically go over the block gas limit, even if in practice it won’t. If this is the case, see if you can reduce the transaction’s gasLimit while remaining above the amount it actually needs–that might do the trick.
好,我们再修改一下truffle.js如下:
module.exports = {
networks: {
development: {
host: "127.0.0.1",
port: 8545,
network_id: "*", // Match any network id
gas:500000
}
}
};
继续执行truffle migrate,执行成功。
liuwenbin@liuwenbin-H81M-DS2:~/work/truffle-workspace/MetaCoin$ truffle migrate
Using network 'development'.
Running migration: 1_initial_migration.js
Deploying Migrations...
... 0x2adf8c421a2814ea4d5f1a211048ac64c47f6fcf64a1418dd4abc463d604d8fc
此时terminal处于监听状态,我们先不管他,下面请转到“IDE cooking steps”章节会给出解释。
去看一下Documents/someLogs/testGeth.log文件:
INFO [02-08|14:59:39] Submitted contract creation fullhash=0x2adf8c421a2814ea4d5f1a211048ac64c47f6fcf64a1418dd4abc463d604d8fc contract=0xc8B95403276e5B4482718803C25A449743d59755
INFO [02-08|14:59:39] Commit new mining work number=23 txs=1 uncles=0 elapsed=351.917µs
INFO [02-08|14:59:39] Successfully sealed new block number=23 hash=b97b83…b19548
INFO [02-08|14:59:39] ? mined potential block number=23 hash=b97b83…b19548
我截取到了日志文件中以上的部分,可以看到,我们的智能合约已经被成功部署了,且日志中的hash值与上面监听状态的terminal中显式的是相同的,说明是一致的。下面我们就可以在终端使用该智能合约了。
上面我们介绍了智能合约的单元测试的写法,包括js版本和Solidity版本,我们也知道在执行编译时会自动执行这些单元测试,如果有一个测试未通过则会中断编译过程。而在开发阶段,我们也可以自己使用命令来测试。
truffle test
没有报错就说明通过了,绿条,有报错就会打印在下方。
经过上面truffle metacoin环境模板的搭建,我们整个智能合约的开发、编译、部署以及运行环境就搭建好了。下面我们用这套环境来重现最初的helloworld智能合约。
首先创建我们的工程Helloworld:
liuwenbin@liuwenbin-H81M-DS2:~/work/truffle-workspace$ mkdir helloworld && cd helloworld
liuwenbin@liuwenbin-H81M-DS2:~/work/truffle-workspace/helloworld$ truffle init
Downloading...
Unpacking...
Setting up...
Unbox successful. Sweet!
Commands:
Compile: truffle compile
Migrate: truffle migrate
Test contracts: truffle test
liuwenbin@liuwenbin-H81M-DS2:~/work/truffle-workspace/helloworld$ ls
contracts migrations test truffle-config.js truffle.js
liuwenbin@liuwenbin-H81M-DS2:~$ geth --datadir testNet --dev --rpc console
WARN: 这一步遇到问题,上面所谓监听状态实际上是卡住了,我们的智能合约并未部署成功,虽然在EVM中已经写入了块,但是无法识别该合约对象。理想状态下我们可以调用合约对象了,这个流程就全通了,但是没事,我去继续查一下解决方案。
上文说明了这些原因,我也在官网下载了ganache,这是一个AppImage文件,这个文件在linux系统可以直接启动,首先我们需要将它的执行权限修改一下,然后启动即可。
chmod a+x exampleName.AppImage
我想到一个事情,这里重申一下:我目前的测试开发环境,如果没有交易产生,挖矿不会自动进行。对于比特币和以太坊的正式环境来说,他们会限制出块时间,因为现在他们的交易量都很大,交易就会被拖慢,而不会产生没有交易,到了固定时间就要出个空块的情况。不过也有特例,因为共识算法加上对出块时间的限制,是有可能出现空块的。这很浪费,不过就我目前来看,算是留个思考题吧。
我们应该都可以直观的看懂,然后我们将它的网络配置到工程的truffle.js中去。
我们仍旧可以使用命令“geth attach http://localhost:7545” ,从geth命令行attach到这个ganache EVM网络中去。
配置完成后,继续执行以上命令,可以看到不再发生以上被卡住的情况了,但是不识别我的Helloworld智能合约:
Error: Could not find artifacts for Helloworld.sol from any sources
继续探索…
解决方案:居然是我的contract 名字不匹配的原因,因为我当时想统一将工程名、合约文件名都改为首字母大写,但忘记该合约文件内部的contract后面的名字了,以及构造函数,这就像你改了java的类文件名,但没有该内部类名一样,可惜goland的Solidity插件并未报错啊,害的我找了半天,不过以后还是要靠自己多注意了。
但是,仍然有问题:
Error encountered, bailing. Network state unknown. Review successful transactions manually.
应该是truffle.js中网络配置的问题。
继续探索…
解决方案:哥们定睛一看,在上面这个表明看起来的error面前,不要先入为主,下面还有一行报错信息:
Error: Helloworld contract constructor expected 1 arguments, received 0
原来是我的合约内部有问题,我们通过truffle部署的时候不知道如何去给构造函数赋值,当时我们使用Remix的时候是手动修改的WEB3DEPLOY的js代码段,这里我就直接在合约代码中修改吧,最后是这样:
pragma solidity ^0.4.0;
contract Helloworld {
string content;
function Helloworld() public {
content = "hello, world!";
}
function getContent() constant public returns (string){
return content;
}
}
多谢博友moqiang02的友情提示,这里可以在部署时进行构造函数的赋值,不必修改智能合约内容:在2_deploy_contracts.js中,修改deploy脚本,“deployer.deploy(Helloworld,“hello, world!”);”即可。下面所有流程不影响,继续
truffle migrate!
liuwenbin@liuwenbin-H81M-DS2:~/work/truffle-workspace/Helloworld$ truffle migrate
Using network 'development'.
Running migration: 2_deploy_contracts.js
Deploying Helloworld...
... 0x391f2c060b1f9cbe7b42493fc858ffa455d40f6e9af754a105092a9ac32e53c3
Helloworld: 0x2c2b9c9a4a25e24b174f26114e8926a9f2128fe4
Saving successful migration to network...
... 0x0e8fab8924d93f0b17aa1c9dc58b976089a61e4debcd185dffa2c16e5cc539e9
Saving artifacts...
liuwenbin@liuwenbin-H81M-DS2:~/work/truffle-workspace/Helloworld$
成功!
对比ganache日志来看:
[5:24:49 PM] Transaction: 0x391f2c060b1f9cbe7b42493fc858ffa455d40f6e9af754a105092a9ac32e53c3
[5:24:49 PM] Contract created: 0x2c2b9c9a4a25e24b174f26114e8926a9f2128fe4
[5:24:49 PM] Gas usage: 205611
[5:24:49 PM] Block Number: 7
[5:24:49 PM] Block Time: Thu Feb 08 2018 17:24:49 GMT+0800 (CST)
[5:24:49 PM] Transaction: 0x0e8fab8924d93f0b17aa1c9dc58b976089a61e4debcd185dffa2c16e5cc539e9
[5:24:49 PM] Gas usage: 26981
[5:24:49 PM] Block Number: 8
[5:24:49 PM] Block Time: Thu Feb 08 2018 17:24:49 GMT+0800 (CST)
可以看到我们通过truffle部署一个智能合约,要提交两个块,有两笔交易产生。为什么呢?
因为第一笔交易是来自与Helloworld.sol的创建,第二笔交易是来自于migration,每次部署一个新的合约都要执行这两步。
部署成功以后,我们可以得到合约的地址:0x2c2b9c9a4a25e24b174f26114e8926a9f2128fe4,后面会使用这个地址来实现与合约的交互。
此时如果我们直接geth attach到ganache本地环境中,无法与合约实现交互。因为目前虽然我们在EVM中创建了一个合约,但未在基于web3js的geth中注册合约对象,
geth 中是通过abi来注册合约对象的。
首先我们找到build/contracts/Helloworld.json中的abi的value,通过json压缩成一行,
abi = [{"inputs":[{"name":"con","type":"string"}],"payable":false,"stateMutability":"nonpayable","type":"constructor"},{"anonymous":false,"inputs":[{"indexed":false,"name":"arg","type":"string"}],"name":"GetGreeting","type":"event"},{"constant":true,"inputs":[],"name":"getContent","outputs":[{"name":"","type":"string"}],"payable":false,"stateMutability":"view","type":"function"}]
然后注册合约对象:
hello = eth.contract(abi).at('0x2c2b9c9a4a25e24b174f26114e8926a9f2128fe4')
对象注册成功以后,就可以像正常合约那样去调用了。
> hello.getContent()
"hello, world!"
truffle框架没有直接使用abi,而是为我们封装提供了更加方便的调用方式。
我虽然希望能够得到大一统的简单编写的开发测试环境,但是我并不愿意使用develop模式,下面我们使用console模式来与刚刚部署的Helloworld智能合约进行交互。
truffle console
执行以后,我们可以敲出Helloworld了,打印出一个json结构,展示了它的各种属性内容。它是一个TruffleContract,内容非常多。
tip: 上面提到过Solidity的event语法,里面展示了如果针对未使用event的智能合约,要通过var returnValue = exampleContract.foo.call(2);// 通过web3 的message的call来调用。
我们的Helloworld合约并未使用event方法,所以让我尝试一下这种方式来调取:
truffle(development)> Helloworld.at("0x2c2b9c9a4a25e24b174f26114e8926a9f2128fe4").getContent.call()
'hello, world!'
此刻的心情真是扬眉吐气,从来没有一次这么艰难的“helloworld”历程!
truffle debug我还没来得及体验,先使用Remix吧,等我日后体验完觉得它不错我再来补充。Remix的debug其实还不错,不过很多人好像用不明白。我这里简单介绍一下吧,当你编写完一个智能合约以后,一般它会自动帮你编译,并且会在下方展示出你的属性,方法(如果没有的话,请尝试去交易的位置把交易和gas配置一下即可),然后点击其中你想调试的方法(注意入参),在控制台会打印出它的执行过程,同时右侧会有一个“debug”的小按钮,点击它(注意要预先在代码中设置断点),然后就可以按行调试了,随着一行行的运行,属性变量的值也会有所改变。
今天是2017农历最后一个工作日,此时周围早已心飞扬的同事们呼呼啦啦地走光了,我刚刚完成了这篇文章,孤零零的我却满腹成就感。本篇文章仍旧采取我的以往习惯,采用主线分支的路线,详细介绍了如何开发一个智能合约,这里面把我这一条路线上遇到的所有的坑都趟过了,重点研究了Solidity的语法(当然并不是全面的,我只研究相关的了),智能合约的开发环境,各种新鲜工具的使用,最后着重介绍了智能合约的大杀器——truffle。希望能够对您有所帮助,一起努力!
基本全部来自于各种官方文档,stackoverflow,askUbuntu,github issues等网站,没有实体书,这种新知识实体书永远是滞后太多的。
圆方圆学院汇集大批区块链名师,打造精品的区块链技术课程。 在各大平台都长期有优质免费公开课,欢迎报名收看。
公开课地址:https://ke.qq.com/course/345101?flowToken=1007330