【刘文彬】【精解】开发一个智能合约

2019-03-14 14:34:45 浏览数 (1)

智能合约

这两天被老大搞去搬砖,学习计划有变但无大碍,这篇文章将仔细分析智能合约相关内容。 关键字:智能合约,remix,Solidity,truffle,geth,leveldb,datadir,ganache,web3j

合约

合约也称合同、协议,是甲乙双方参与的,制定一系列条目规范双方权利与义务的文件。智能合约是电子化的,自动执行的,去中心化的,具有不可抵赖性,本质上它是一段代码,依托于区块链技术,它可以做很多事情,基于以太坊的智能合约可以让你的区块链扩展出任何你想要的功能。

我相信,智能合约是区块链的未来,因为基于它能做的商业模型太多样了,远远不仅是数字货币一种。

Solidity

智能合约的编程语言是Solidity,扩展名为.sol,它是基于C 、JavaScript、Python创造而来的,这里是官方文档。

Solidity是静态类型的,支持继承,有自己的函数库,它同样支持面向对象语言的自定义类型等其他功能。

Solidity编写的智能合约代码运行在EVM,即以太坊虚拟机,正如java编写的代码运行在JVM一样,在同一个区块链中每一个结点的EVM都是相同的运行环境。通过智能合约,可以开发匿名投票、匿名拍卖、众筹以及多重签名的钱包等,以太坊每一个结点可以有多个账户,所以每个结点都可以称作钱包,可以管理名下的账户,以及转账、挖矿等操作。

官方推荐IDE:Remix

其实Solidity智能合约开发的IDE有很多,官方推荐的Remix是基于浏览器的,运行环境可以切换:

  • 挂在自己的JavaScript EVM
  • 也可以使用web3 provider
  • 还可以使用注入的web3连接到本机调试环境

我使用以后,觉得浏览器的方式还是不习惯,尤其保存的文件无故消失,让我始终心有余悸,经过调研,下面我们将采用goLand,安装Intellij-Solidity-2.0.4插件的方式开发智能合约,然后使用Remix环境进行智能合约的部署。当然我们也可以使用Remix进行运行、测试以及调试工作,下面酌情展示。

gas

区块链中比较有意思的命名,相当于手续费但又有些不同。gas为天然气,用来代表我们程序运行所有的能耗,当发生交易等操作时会消耗相应的gas,gas的计算方式是

gas 单价 × gas 数量

其中gas单价是由用户,像我们这样的发起者愿意为此次操作付出多少以太币而定的(相当于你开车上路前愿意给你的油箱加多少油,假设你的油箱是无限大的)。gas数量是程序根据你操作的复杂度自动定义的。

智能合约也是一样的,当一个发起者部署运行一段智能合约时,以太坊会收取gas费用,就像汽车行驶需要烧油一样,直到你的智能合约运行完毕,“油箱”中剩余的gas会退还给你,如果你的代码死循环了,耗尽了你“油箱”中的gas,那么以太坊会自动报出异常停止你的智能合约。我们在学习智能合约阶段,可以使用testnet环境来避免真的花费以太币。

Dapp

Dapp为Solidity提供了源码构建工具,包管理工具,单元测试以及智能合约部署,一会儿我们看看是否必须要用它。有时它也被称作去中心化的应用程序(Decentralized App)。这种应用程序除了有一段代码的智能合约以外,还需要UI,UE设计等,正如apple的app开发,我们未来的目标之一可以是开发自己的Dapp。

准备工作

首先要开启一个本地的EVM,前面的文章对Geth做了详细的介绍,这里直接启动一个本地开发模式的结点。

代码语言:javascript复制
`geth --datadir testNet --dev console 2>>Documents/someLogs/testGeth.log`

简介一下geth的参数选项:

dev

Ephemeral proof-of-authority network with a pre-funded developer account, mining enabled

短暂的认证证明网络,同时创建一个预存款很多钱的一个开发者账户,并自动开始挖矿。

datadir

datadir,指定结点文件目录,如果没有会自动创建一个,该目录包含:

  • geth
    • chaindata 区块数据、状态数据的目录,数据库是leveldb(一个键值对数据库)
      • 000001.log
      • CURRENT 指向MANIFEST
      • LOCK 区块数据锁定标识文件
      • LOG 数据库(区块和状态)操作日志
      • *.ldb 块数据文件
      • MANIFEST-000000 (TODO,我也不知道是什么,谁能告诉我一下)
    • LOCK 结点锁定标识文件
    • nodekey 结点身份公钥,用于p2p网络寻找结点使用
    • transactions.rlp
  • geth.ipc Mist是以太坊钱包,该文件是Mist用来内部过程通信的socket文件。
  • keystore 存储私钥
    • UTC--2018-02-06T03-46-35.626115529Z--740b9c48d67cf333c8b1c0e609b6b90b40d3cdea

以上目录中元素精解:

nodekey

结点之间相互寻找是通过一个发现协议:一个基于S/Kademlia的网络协议。这个协议会把包含IP地址的公钥联系起来。实际上在结点之间的peer连接使用的是一个完全不同的,加密的协议(RLPX)。RLPX加密的工作方式需要远程终端连接发起者的公钥作为身份识别。本质上来说,这个key链接了发现协议和RLPX。

你可以随时删除这个nodekey,重启的时候会自动生成一个新的。

keystore/UTC--2018-02-06T03-46-35.626115529Z--740b9c48d67cf333c8b1c0e609b6b90b40d3cdea

这是存储结点私钥的位置,文件名为时间戳加上本地账户拼成的字符串。打开文件,内容为一个json,格式化以后为:

代码语言:javascript复制
{
    "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排序。

特点包括:

  • 键和值都是当作简单的字节数组,所以内容可以从ASCII字符串到二进制文件。
  • 数据按照key排序存储。
  • 调用者可以自定义一个比较方法来复写排序。
  • 基本操作有插入、获取和删除:Put(key,value), Get(key), Delete(key).
  • 一次原子批量操作可以执行多重变更操作。
  • 用户能够创建一个瞬时快照来获取一个统一的数据视图。
  • 数据可以向前亦或是向后迭代。
  • 数据采用Snappy(也是Google的一个压缩库)自动被压缩。
  • 用户可以通过一个虚拟接口自定义操作交互系统来实现一些额外的操作。

局限性包括:

  • 无SQL,无索引,非关系型数据库
  • 同时只允许一个进程访问(但支持多线程)
  • 无客户端-服务端内置库支持,一个应用程序必须要包装自己的服务器到库才能获得这样的支持。

console

console命令在EVM启动的同时开启了一个交互控制台,后面的一串命令是将输出的log转存到文件testGeth.log中去,启动时的日志文件:

代码语言:javascript复制
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: <nil> 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

我们逐行分析,

代码语言:javascript复制
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. 提交新的挖矿工作

helloworld

下面在console中查看一下当前账户的余额,发现开发环境默认给分配的余额太大,并不好测试,那么我们自己再创建一个用户,余额为0,然后用第一个“大款”账户转账给新创建用户1个以太币。

代码语言:javascript复制
> eth.sendTransaction({from: '0x740b9c48d67cf333c8b1c0e609b6b90b40d3cdea',to:'0x1d863371462223910a1f05329b6dea0b0f9c49f8',value:web3.toWei(1,"ether")})
"0xb456244e4fb25b74108f05afe53670b5f1a857f5671e7d3fa2e221419d04382c"
> eth.getBalance(eth.accounts[1])

333333333333333333

我发现一个事,之前乘三那个geth还存在呢(捂脸笑出泪),让我改一下吧。改后我重新部署了geth命令,然后将新建用户的3个以太转回大款账户,由于gas的存在(实际上即使转账时你自己指定,也是基于一个最小值,往多了给,如果低于这个最小值,就会报错:“你加的油太少啦,我根本跑不过去”。所以最终费了大力,让新账户保留下了

代码语言:javascript复制
> eth.getBalance(eth.accounts[1])
79000

这79000wei的以太币是无法转出去了,因为我的余额付不起油钱。实际上79000这个数字可读性还行,所以拿这个测试也可以。

IDE编码

上面说道了我们采用goLand安装Solidity插件的方式来开发智能合约。JetBrain系列IDE插件的安装我就不介绍了,网上随便查。下面我们开始编码:

代码语言:javascript复制
pragma solidity ^0.4.0;

contract helloworld {
    string content;

    function helloworld(string _str) public {
        content = _str;
    }

    function getContent() constant public returns (string){
        return content;
    }
}

代码编写很简单,我们逐行解读:

代码语言:javascript复制
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插件进行了合约代码的开发,然而该插件的功能仅包括:

代码语言:javascript复制
1. 语法高亮,代码提示
2. 代码完整性检查
3. 文件模板
4. goto声明
5. Find usages
6. 代码格式化

可以说都是针对编码辅助的操作,然而若我们要部署智能合约,还得回到Remix,我们新建一个sol文件,粘贴进去上面写好的helloworld代码,然后点击右侧Details,弹出的界面包含了名字、字节码、元数据等内容,我们只要其中的WEB3DEPLOY,复制出其中内容,将第一行传入参数“hello world”:

代码语言:javascript复制
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,先解锁智能合约发布者的账号,我们选择刚才新建的

代码语言:javascript复制
> personal.unlockAccount(eth.accounts[1],"lwb")
true

然后将上面的web3版的代码复制过来,回车,输出:

代码语言:javascript复制
Contract mined! address: 0x71db931bdb2f9516cf892aa0c620bd686d1095e5 transactionHash: 0x6e39a97dd2f260517bedeb9934cf88430526b46a379d5680cc092d8ea3f44602

合约被挖出,打印出来了合约地址,交易hash(这在以太坊中也被认定为是一笔交易,我们付费gas给以太坊)。 然后继续在console中输入

代码语言:javascript复制
> helloworld.getContent()
"hello world"

由于我们余额是79000,上面gas给预估的是4700000,所以预想结果是您的余额不足,合约无法运行,然而合约部署运行成功了。

我们从大款那再转账一个以太币过来。然后关闭重启geth console,重复上面的操作。

TODO: 余额仍旧未减少。不知道gas扣到哪去了。

同步查看日志输出:

代码语言:javascript复制
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] 

0 人点赞