1. 智能合约编写工具介绍
简述
如果想编写智能合约,那么有几个常用的开发工具是应该有所了解的。下面我先初步介绍一下各个开发工具的概念。
以太坊客户端
以太坊智能合约的开发最重要的工具就是以太坊的客户端。一方面,客户端本身提供很多的调用方法;另一方面,很多其他测试工具都是基于客户端开发的。在运行以太坊客户端之后,我们可以通过使用类似CURL这样的命令,通过JSON RPC与客户端交互。此外,pyethapp与geth的客户端都有控制台,语法类似javascript,用户可以通过调用控制台命令实现很多操作,比如账户解锁、发起交易等等。
Testrpc
testrpc是一个基于pyethapp开发的工具,主要作用是模拟一条正在运行的区块链。换句话说,testrpc是一个去除了挖矿共识机制与节点间通讯的单节点区块链。在启动testrpc时会直接建立10个存有资金的外部账户,并提供了这些账户的地址和私钥。默认对外开放8545端口,监听JSON RPC信息;默认gasprice为1;默认提供了非常多的JSON RPC方法;最重要的是,去除通信与挖矿之后,运行速度非常快。testrpc如此多的优点使得它适合开发与测试。
truffle
truffle是一个构建DAPP(decentralized application去中心化应用)的快捷工具。它主要由几个工具组成
(0)truffle init
作用是建立一个truffle项目
truffle项目包括:app-dapp的建立;contracts-合约;migrations-部署合约;test-测试
(1)truffle build
作用是建立一个build文件夹,里面是根据智能合约编译后生成的js文件。
*还没有搞清楚和compile的区别。
(2)truffle migrate(deploy)
作用是执行migrations文件夹中的js脚本,通常功能是部署合约。
*在2.0以前的版本,使用truffle deploy
(3)truffle test
作用是执行test目录下的js脚本,通常功能是调用合约。
(4)truffle serve
作用是提供对外服务,启动truffle项目服务,部署到http 8080端口上。
2.智能合约调用方法
大嘎吼~我又来讲方法啦!很强!很优秀!
前面我们已经知道了,web3或者api是封装了一个JSONRPC消息。那我们现在的目标是,看看这个封装好的消息内容是怎么样的,如何让EVM执行某个智能合约的函数的。
之前说过,智能合约的调用只需要两个字段:合约地址,用来表明我们是要调用哪一个合约;还有合约abi(application binary interface),这个是为了指明具体调用合约中的哪一个方法。
为什么要用这两个字段呢?首先,合约内容是可以完全重复的,所以我们根据合约名字、合约内容或者合约内容的哈希,去调用某一个合约都是可能重复的,只有合约地址是不会重复的。另外,合约内的函数名是可以重载的,就是说我们可以有多个叫setValue的函数,但是所需要的传入参数不同。EVM中使用了abi,便于调用,同时还能区分不同的函数。
tips:还记得创建合约的消息长什么样子吗?这个消息to的字段为空。合约地址是由EVM自动生成的,生成方式是:发送建立合约消息的账户地址和该地址nonce(即发送的消息数)的RLP编码的sha3哈希值,再截取32个字节的哈希值的后20个字节作为地址,可以写成这样:“sha3(ENCODEDRLP(sender+nonce))[12:]”。
abi是什么?
应用程序二进制接口(Application
Binary Interface,ABI)定义了一组在PowerPC系统软件上编译应用程序所需要遵循的一套规则。主要包括基本数据类型,通用寄存器的使用,参数的传递规则,以及堆栈的使用等等。
这些网上的说明看着是不是很玄妙?就喜欢整些没用的,其实给你看一个例子,一下就知道了。现在的介绍文章为了逼格就喜欢整些名词,不喜欢给例子,简直了。
下面是附例中person合约的abi(我删除了用于测试的一些内容):
[{"constant":true,"inputs":[],"name":"dumpData","outputs":[{"name":"id","type":"int256"},{"name":"name","type":"string"},{"name":"info","type":"string"},{"name":"available","type":"bool"}],"type":"function"},{"constant":true,"inputs":[],"name":"getCreator","outputs":[{"name":"creator","type":"address"}],"type":"function"},{"constant":true,"inputs":[],"name":"getName","outputs":[{"name":"name","type":"string"}],"type":"function"},{"constant":true,"inputs":[],"name":"getState","outputs":[{"name":"available","type":"bool"}],"type":"function"},{"constant":true,"inputs":[],"name":"getOperation_address","outputs":[{"name":"operation_address","type":"address"}],"type":"function"},{"constant":true,"inputs":[],"name":"getInfo","outputs":[{"name":"info","type":"string"}],"type":"function"},{"constant":true,"inputs":[],"name":"getId","outputs":[{"name":"id","type":"int256"}],"type":"function"},{"constant":false,"inputs":[{"name":"info","type":"string"}],"name":"setInfo","outputs":[{"name":"flg","type":"bool"}],"type":"function"},{"constant":false,"inputs":[{"name":"state","type":"bool"}],"name":"setState","outputs":[{"name":"flg","type":"bool"}],"type":"function"},{"inputs":[{"name":"id","type":"int256"},{"name":"name","type":"string"},{"name":"OperContract","type":"address"}],"type":"constructor"},{"anonymous":false,"inputs":[{"indexed":false,"name":"contractAddress","type":"address"}],"name":"LogCreate","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"name":"contractAddress","type":"address"}],"name":"LogChange","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"name":"code","type":"uint256"},{"indexed":false,"name":"message","type":"string"}],"name":"LogError","type":"event"}]
乍一看还挺吓人的,这么多东西,乱七八糟的。不过,换一下行立即就变清晰了。
[{"constant":true,"inputs":[],"name":"dumpData","outputs":[{"name":"id","type":"int256"},{"name":"name","type":"string"},{"name":"info","type":"string"},{"name":"available","type":"bool"}],"type":"function"},
{"constant":true,"inputs":[],"name":"getCreator","outputs":[{"name":"creator","type":"address"}],"type":"function"},
{"constant":true,"inputs":[],"name":"getName","outputs":[{"name":"name","type":"string"}],"type":"function"},
{"constant":true,"inputs":[],"name":"getState","outputs":[{"name":"available","type":"bool"}],"type":"function"},
{"constant":true,"inputs":[],"name":"getOperation_address","outputs":[{"name":"operation_address","type":"address"}],"type":"function"},
{"constant":true,"inputs":[],"name":"getInfo","outputs":[{"name":"info","type":"string"}],"type":"function"},
{"constant":true,"inputs":[],"name":"getId","outputs":[{"name":"id","type":"int256"}],"type":"function"},
{"constant":false,"inputs":[{"name":"info","type":"string"}],"name":"setInfo","outputs":[{"name":"flg","type":"bool"}],"type":"function"},
{"constant":false,"inputs":[{"name":"state","type":"bool"}],"name":"setState","outputs":[{"name":"flg","type":"bool"}],"type":"function"},
{"inputs":[{"name":"id","type":"int256"},{"name":"name","type":"string"},{"name":"OperContract","type":"address"}],"type":"constructor"},
{"anonymous":false,"inputs":[{"indexed":false,"name":"contractAddress","type":"address"}],"name":"LogCreate","type":"event"},
{"anonymous":false,"inputs":[{"indexed":false,"name":"contractAddress","type":"address"}],"name":"LogChange","type":"event"},
{"anonymous":false,"inputs":[{"indexed":false,"name":"code","type":"uint256"},{"indexed":false,"name":"message","type":"string"}],"name":"LogError","type":"event"}]
这样就一目了然嘛,这其实就是Person合约中所有定义的fucntion、event和constructor的结构。包括了这个函数名称是什么;有没有constant属性(就是函数是否会修改变量,一般可以给get函数加上该属性);输入有哪些,类型分别是什么;输出有哪些,类型分别是什么等等。值得注意的有三点:一是函数分为constructor和function,其中constructor是构造函数;二是event,没有outputs,有一个anonymous属性;另外event的inputs有一个indexed属性,true表示该字段是log topic的一部分,false表示该字段会是data段的一部分。如果你使用的是新版的编译器,还会有payable属性,说明这个函数是否允许转账操作。要注意的是,技术更新还是很快的,所以请以新版编译器为准,但是abi大体上都是这样子的^_^
tips:abi细节解释。
更多详细内容在同步数据的章节介绍。
我们在调用某个合约的某个函数时,发送的消息处理步骤是:
(1)api层获取该abi
(2)计算要调用的函数abi的hash,比如如果要调用Person函数abi中的第一个DumpData函数,它的abi是这样的:
{"constant":true,"inputs":[],"name":"dumpData","outputs":[{"name":"id","type":"int256"},{"name":"name","type":"string"},{"name":"info","type":"string"},{"name":"available","type":"bool"}],"type":"function"}
那么我们就要对这段信息进行SHA3哈希运算(这里应该注意一下int/uint应先转换为int256/uint256再做hash)。假设最后的结果是0x28a93be67940cbbf349……(总共160位/40字节)
(3)截取这个哈希运算的前4个字节作为函数ID,即0x28a93be6,放到消息的data字段。
(4)发送消息。这个消息的内容是这样的
to:Person合约地址
v,r,s:根据发送者的签名生成
data:0x28a93be6
剩下一些gas之类的字段就不一一列出了。
(5)EVM会根据这个函数ID,判断需要执行哪一个函数。
(6)如果这个函数需要传入参数(比如上面的setInfo函数有inputs),那么就在data的前四个字节的hash值之后补上传入参数。data字段就类似于这样的内容:0xcdcd77c100000000000000000000000000000000000000000000000000000000000000450000000000000000000000000000000000000000000000000000000000000001。前4个字节表示函数ID,之后32个字节表示第一个参数,后32个字节表示第二个传入参数。参数至少占32个字节,所以值得吐槽的是一个bool类型的true也会扩充为32个字节的00000……0001。如果是数组,前面会有一个字节的标志位。
tips:一个小扩展。
C++编译器也使用了类似的概念实现的函数重载,只不过方法更简单明了。这里我所说的“类似”不是说方法一致,而是说在概念上,C++重载其实就是将名字相同的函数重命名,将其区分成不同的函数,它们拥有不同的函数名,不同的入口地址,每个函数名反映传入参数的个数和类型;而EVM也是对重载函数重命名,函数名是的abi的hash值,也反映了传入参数的个数和类型(还多了一些属性,比如返回值个数和类型等)。C++的编译器实现重载的方法是,将函数名重命名,重命名的规则为函数名+参数列表。如果是局部变量,会映射为作用域+函数名+参数列表。
举个例子,g++编译器的重载函数会重命名为下表这样。
其中_Z5是前缀,print是函数名,i表示int,Ss表示string,l表示long,c表示char。如果是有多个传入参数,就使用多个参数名。比如ii表示有两个int类型的传入参数。如果是在局部变量中有两个,其中test就是所在类名。
网络上有人说_Z5printi中的5表示返回值类型,值得商榷。因为C++的操作符重载是不区分返回值类型的。
据说有的C++编译器直接就将print(int
n,int m)编译为_print_int_int。这样是不是看起来更直观?
����������{��9���