LOADING...
LOADING...
LOADING...
当前位置: 玩币族首页 > 币圈百科 > 实例分析+ 实践步骤 手把手教你编写以太坊、EOS智能合约

实例分析+ 实践步骤 手把手教你编写以太坊、EOS智能合约

2019-12-09 区块链大本营 来源:区块链网络

来源?| 《人人都懂区块链》

作者 | Carol

出品 | 区块链大本营(blockchain_camp)

今天,想和大家聊聊编写智能合约这事儿。

不过,在讲编写智能合约之前,先说一个智能合约的故事。

绑匪、富豪和教父的故事

一个绑匪绑架了富豪的儿子。

教父是黑白两道通吃的大佬。

绑匪和富豪都相信教父。

富豪收到绑匪的电话后,特别担心给钱之后仍然救不了儿子的性命,而绑匪也担心放了人拿不到赎金。

两者相持数小时,富豪的儿子提出一个解决办法,让富豪设立一个三方交易的比特币钱包地址,该交易只有在绑匪和教父都用私钥签名后才有效,并且协议被全网广播后,绑匪必须马上放人。

作为一个自动担保账户,当情景满足规定条件时,程序就会自动释放或转移资金。整个过程可以描述为:

富豪建立智能钱包;

绑匪用自己的私钥解锁;

仲裁者调用智能合约函数;

函数触发,将资金转移到合约当中,等待回调发生。

在这个故事中,仲裁者成功调用智能合约函数解决了富豪与绑匪的困境。由此,富豪与绑匪之间的不信任博弈从技术层面被破解了。那么与智能合约相比,传统合约又是怎么运转的呢?

传统合约的订立

传统上,合同的订立是指缔约当事人相互为意思表示并达成合意而成立了合同。合同的订立由“ 订”和“ 立”两个阶段组成。

“ 订”强调缔约的行为和过程是缔约各方接触、洽商过程,包括缔约各方的接触、洽商并最终达成协议前的整个讨价还价过程。

此阶段由要约邀请、要约、反要约诸制度加以规范和约束,产生先合同义务及缔约过失责任。而“ 立”强调缔约的结果 , 指的是双方合意的达成,即双方当事人就合同条款至少是合同的主要条款已经形成一致意见,各方当事人享有的权利和承担的义务得以确定,简而言之,合同成立了。

实际而言,合约的“ 订”其实是要约,要约指一方当事人向他人做出的以一定条件订立合同的意思表示。前者称为要约人,后者称为受要约人。而要约的形式——要约作为一种意思表示,可以书面形式做出,也可以对话形式做出。书面形式包括信函、电报、电传、传真、电子邮件等函件。

要约的有效条件有以下三点。

要约必须是特定人的意思表示。

要约必须是向相对人发出的意思表示。要约的相对人应为特定的人,但在特殊情况下也可以为不特定的人。

要约必须是能够反映所要订立合同主要内容的意思表示。

而合约“ 立”这一阶段就是承诺,承诺指受要约人同意要约内容缔结合同的意思表示。承诺应以通知的方式做出,但根据交易习惯或要约表明可以通过行为做出的除外。缄默或不行为不能作为承诺的表示方式。

承诺的有效要件:承诺须由受要约人或其授权的代理人做出;承诺须在有效期内做出;承诺须与要约的内容一致;承诺须向要约人做出。所以,合同订立的一般程序应该如下。

当事人双方相互约定,双方当事人有合作意向后就合约的内容不断探讨交流,最后形成合作的一致意见。

合同起草。有了明确的合作意见后,就敲定合约的细节,由双方当事人或者第三方( 被双方所承认的)来着手合约的起草,完成合约的书面文本后,由双方当事人确认合约细节,确认无误后方可。

专业人员评估。完成合约起草后,还需要合约有关专业人员( 如律师)来确认合约本身的合法性,公证处对合同进行公证备案。

合同执行。合同执行主要靠当事人双方自觉执行,如出现违约等情况,双方当事人应相互协商解决,协商后依旧无法解决的,可申请仲裁或上诉法院,法院判决后强制执行。

智能合约的编写步骤

相比传统合约时效性受到诸多限制,智能合约则存在着诸多好处,签署效率高,安全性强,且在违约执行时实现了不可抵赖和自动执行性。

那么,怎样编写一个智能合约来解决我们的问题呢?

一般而言,一个运行智能合约的去中心化平台会提供一条公有区块链,并会制定面向智能合约的一套编程语言。智能合约的开发者可以在该智能合约平台上使 用 官 方 提 供 的 工 具 , 来 开 发 支 持 该 平 台 区 块 链 协 议 的 应 用 ( 即 所 谓 的 D A P P )。

因此,可以在智能合约平台上进行编写,具体的逻辑步骤如下。

第一步,启动一个区块链节点。

第二步,使用编程语言编译智能合约,然后将源代码编译获得二进制代码。

第三步,将编译好的合约部署到网络,获得合约的区块链地址和ABI 。(这一步可能会消耗费用,还需要使用节点的默认地址或者指定地址来给合约签名。)

第四步,用JavaScript API来调用合约。(根据调用的类型有可能会消耗费用)

实例分析一:以太坊智能合约编写

1. 安装以太坊的准备工作

如果是首次接触 Ethereum( 以太坊),推荐使用下面的步骤安装部署。

第一步,安装 Ethereum。

sudo?apt-get?install?software-properties-commonsudo?add-apt-repository?-y?ppa:ethereum/ethereum?sudo?add-apt-repository?-y?ppa:ethereum/ethereum-dev?sudo?apt-get?updatesudo?apt-get?install?Ethereum

第二步,安装 solc 编译器。

sudo?add-apt-repository?ppa:ethereum/ethereum-qt?sudo?add-apt-repository?ppa:ethereum/ethereum?sudo?apt-get?updatesudo?apt-get?install?cpp-ethereum

安装后可以使用 geth 命令创建 Ethereum 账户。

geth?account?new

第三步,Solidity 语言支持。

Browser-solidity 提供了在线的 Solidity 语言测试。需要下载包括 Solidity 运行环境的安装包。

第四步,安装客户端 Mist。

官方提供钱包客户端 Mist,支持进行交易,同时支持直接编写和部署智能合约。

所编写的代码编译发布后,可以部署到区块链上。使用者可通过 Mist 发送指令,调用相应交易合约,让以太坊虚拟机(EVM)在区块链上执行交易合约。

以太坊现在有多种语言实现的客户端,包括以下几种:

ethereumjs-lib:JavaScript 语言实现;

Ethereum(J):Java 语言实现;

ethereumH:Haskell 语言实现;

go-ethereum:Go 语言实现;

Parity:Rust 语言实现;

pyethapp:Python 语言实现;

ruby-ethereum:Ruby 语言实现。

2. 在以太坊上编程时的注意事项

完成准备工作后,就可以着手编写属于自己的智能合约。在编写过程中,还有一些地方需要注意。

(1)查看验证节点能否正常运行?

在成功部署了一个智能合约后,输入数据时即可验证代码是否正常运行。

(2)部署在其他节点上?

为了使其他人可以运行你的智能合约,你需要两个信息。

① 智能合约地址Address。② 智能合约ABI。ABI其实就是一个有序的用户手册,描述了所有方法的名字和如何调用它们。可以使用以下代码获得其 ABI 和智能合约地址。

geiverCompiled.griver.info.abiDefinition;?greeter.address;

然后可以实例化一个 JavaScript 对象,该对象可以用来在任意联网机器上调用该合约,此处 ABI 和 Address 是上述代码返回值。

var?griver=?eth.contract(ABI).at(Address);

(3)自毁程序?

一个交易被发送到网络需要支付费用,自毁程序是对网络的补充,花费的费用远小于一次常用交易。

可以通过以下代码来检验是否成功,如果自毁程序运行成功,以下代码会返回 0。

giver.kill.sendTransaction({from:eth.accounts[0]})

3. 实际操作

根据以太坊白皮书上所说的对冲合约,即一种金融衍生品,我们可以进行代码编写。以下是白皮书上所举的一个例子:

等待 A 输入 1000 以太币;等待 B 输入 1000 以太币;通过查询数据提供合约,将价值 1000 以太币的等值美元,如 x 美元,记录至存储器。

30 天后,允许 A 或 B“ 重新激活”合约以发送价值 x 美元的以太币( 重新查询数据提供合约,以获取新价格并计算)给 A,并将剩余的以太币发送给 B。

实现步骤如下。

第一步,确定进行交易的双方,包含双方地址、是否投票( 默认为否)、金额等数据。代码如下。

struct?giver{address?gaddr;//A方人地址bool?yn;//是否投票?uint}amount;//金额struct?reciever{address?raddr;//B方人地址?bool?yn;//是否投票uint?amount;//金额?}

第二步,对双方进行初始化,首先每个账户内打入 1000 以太币;确认交易后,将 bool 重新设定为 true;接着用一个 storage 保存相关地址以方便后面调用( 如果没有 amount 而使用 balance,将会使得 storage 无法调用);最后两个账户之间的转账可以只用 msg.sender( 准备下次实现),目前只有将 amount 数值设定为 0,来表示将 1000 以太币转入对冲基金,在现实生活中则有很高的风险,是明显不可取的。代码如下。

function?initializeA(address?giverA){?//?A方人初始化givers[giverA].amount?=?1000?ether;?givers[giverA].yn?=?true;p1?=?giverA;givers[giverA].amount?=?0?ether;}function?initializeB(address?recieverB){?//?B方人初始化recievers[recieverB].amount?=?1000?ether;?recievers[recieverB].yn?=?true;p2?=?recieverB;?recievers[recieverB].amount?=?0?ether;?}

第三步,实现对冲的第一步,将 1000 以太币根据汇率转换成其他货币。

inthedgevalue;function?hedging1(uintexchangerate)?returns?(uint){?hedgevalue?=?1000?ether?/exchangerate?;return?hedgevalue;}

第四步,实现对冲的第二步,30 天后再次转化回以太币。值得注意的是,使用了 bool 以防交易失败。

bool?success;function?hedging2(uintexchangerate?,uint?time)?returns(bool?success){if(time?!=?30)?return?false?;if(givers[p1].yn?==?false)?return?false;?if(recievers[p2].yn?==?false)?return?false;?givers[p1].amount?=?hedgevalue?*?exchangerate;recievers[p2].amount?=?2000?ether?-?hedgevalue?*?exchangerate;return?true?;}

第五步,确定双方交易后的金额。

通过这五步,一个简单的智能合约就建立起来了。注意,这个智能合约在转换汇率的时候用的是整型,这是一种理想状态,程序可以在 Remix 上完成调试。

所有代码组合起来如下。

以上代码可以简单构成一个智能合约——对冲合约,但这个对冲合约还不够完善,需要更加详细地引入函数和变量来优化它。

实例分析二:EOS 智能合约编写

1. 前期准备

(1)编程语言选择

基于EOS.IO的区块链,使用Web Assembly(WASM)执行开发者提供的应用代码。WASM 是一个已崭露头角的 Web 标准,受到 Google、Microsoft、Apple 及其他大公司的广泛支持。到目前为止,用于构建应用及 WASM 代码编译的最成熟的工具链是 clang/llvm 及其 C/C++ 编译器。

其他由第三方开发中的工具链包括 Rust、Python 和 Solidity。尽管用其他语言更简单,但是它们的性能很可能制约所构建的应用规模。EOS 官方推荐的是C++ 为开发高性能及安全智能合约的最佳语言。

(2)开发环境准备

EOS.IO 软件仅官方支持如下环境:

Ubuntu 16.10 或更高;MacOS Sierra 或更高。

(3)命令行工具

EOS.IO 提供了一系列工具,需要基本的命令行知识来操作它们。

(4)message 与transaction设定

一个message代表单个操作, 一个transaction是一个或多个messages的集合。合约和账户通过 messages 通信。messages 可以单个发送,如果希望一次执行批处理也可以集合起来发送。

① 单个message的transaction。

{?"ref_block_num":?"100",?"ref_block_prefix":?"14070148",?"expiration":?"2018-04-09T06:28:49",?"scope":?["initb","initc"],?"messages":?[{?"code":?"eos",?"type":?"transfer",?"authorization":?[{?"account":?"initb","permission":?"active"?}],"data":?"fbbc85598ab319612aa7f5c904b20701897722968a577a1229873aeb6293192b"}?],"signatures":?[],"authorizations":?[]?}

② 多个messages的transaction,这些messages将全部成功或全部失败。

{?"ref_block_num":?"100",?"ref_block_prefix":?"14070148",?"expiration":?"2018-04-09T06:28:49",?"scope":?[...],"messages":?[{?"code":?"...","type":?"...",?"authorization":?[...],?"data":?"..."},?{"code":?"...","type":?"...",?"authorization":?[...],?"data":?"..."},?...?],"signatures":?[],"authorizations":?[]?}

③ message名的限定。

message的类型实际上是base32编码的64位整数。所以message名的前12个字符需限制在字母a~z, 数字1~5, 以及“.”。第13个以后的字符限制在前16个字符(“.”,a~p)。

④ transaction确认。

获得一个 transaction 哈希并不等于 transaction 完成,它只表示该节点无报错地接受了,而其他区块生产者很可能也会接受它。但要确认该 transaction,你需要在 transaction 历史中查看含有该 transaction 的区块数。

2. 技术限制

(1)无浮点数合约不接受浮点小数计算,因为这在 CPU 层级上是一个不确定的行为,可能会导致意想不到的分叉。

(2)输出执行时间

transaction 需要在 1 秒内执行,transaction 的执行时间需要 * 小于或等于 1秒,否则 transaction 将会失败。

(3)tps 限制最大 30 tps。根据测试公网设置,每个账户最多每秒可发布 30 个 transactions.

3. 编写第一个 EOS 智能合约“Hello World”

第一步,使用 eoscpp 来生成智能合约的骨架。这将在 hello 文件夹里产生一个空白工程,里面有 abi、hpp 和 cpp 文件。

$?eoscpp?-n?hello

① 从.cpp文件含有一个“当收到message后打印 Hello World: ${account}->${action}”的样例代码。代码如下。

oid?apply(?uint64_t?code,?uint64_t?action?)?{?eosio::print(?"Hello?World:?",?eosio::name(code),?"->",?eosio::name(action),?"\n"?);}

② 从.cpp文件生成.wast文件。

$?eoscpp?-o?hello.wast?hello.cpp

③ 获得.wast 和 .abi 文件后,就可以将合约部署到区块链上了。

第二步,假设你的钱包已经解锁,并且有 ${account} 的 keys,你就可以上传,用下面的命令将合约上传到 EOS 区块链上。

$?eosc?set?contract?${account}?hello.wasthello.abi?Reading?WAST...Assembling?WASM...Publishing?contract...{?"transaction_id":?"1abb46f1b69feb9a88dbff881ea421fd4f399?14df769ae09f66bd684436443d5","processed":?{"ref_block_num":?144,?"ref_block_prefix":?2192682225,?"expiration":?"2017-09-14T05:39:15",?"scope":?["eos","${account}"?],"signatures":?[?"2064610856c773423d239a388d22cd30b7ba98f6a9fbabfa621e42cec?5dd03c3b87afdcbd68a3a82df020b78126366227674dfbdd33de7d488f?2d010ada914b438"],"messages":?[{"code":?"eos",?"type":?"setcode",?"authorization":?[{"account":?"${account}","permission":?"active"?}],"data":?"0000000080c758410000f1010061736d0100000001110460017f0060017e0060000060027e7e00021b0203656e76067072696e746e000103656e76067072696e7473000003030202030404017000000503010001071903066d656d6f7279020004696e69740002056170706c7900030a20020600411010010b17004120100120001000413010012001100041c00010010b0b3f050041040b04504000000041100b0d496e697420576f726c64210a000041200b0e48656c6c6f20576f726c643a20000041300b032d3e000041c0000b020a000029046e616d6504067072696e746e0100067072696e7473010004696e697400056170706c79020130013?1010b4163636f756e744e616d65044e616d6502087472616e736665720?0030466726f6d0b4163636f756e744e616d6502746f0b4163636f756e7?44e616d6506616d6f756e740655496e743634076163636f756e7400020?76163636f756e74044e616d650762616c616e63650655496e743634010?00000b298e982a4087472616e736665720100000080bafac6080369363?401076163636f756e7400076163636f756e74"}?],"output":?[{?"notify":?[],"deferred_transactions":?[]?}}]}

第三步,如果查看 eosd 进程的输出,你会看到:

...]?initt?generated?block?#188249?@?2018-04-13T22:00:24?with?0?trxs?0?pendingInit?World!Init?World!Init?World!

可以看到“Init World!”被执行了三次,这其实并不是个bug。区块链处理transactions 的流程如下。

① eosd收到一个新transaction(正在验证的transaction);

创建一个新的临时会话;尝试应用此 transaction;成功并打印出“Init World!”;失败则回滚所做的变化(也有可能打印“Init World!”后失败)。

② eosd开始产出区块;

撤销所有 pending 状态;

推动所有交易输出记录在该区块;

第二次打印“Init World!”;

完成区块;

撤销所有创造区块时的临时变化。

③ eosd如同从网络上获得区块一样将区块追加到链上;

第三次打印“Init World!”。

此时,合约就可以开始接收 messages 了。因为默认 message 处理器接收所有 messages,我们可以发送任何我们想发的东西。

第四步,我们试试发一个空的 message:

此命令将“hello”message 及十六进制字符串“abcd”所代表的二进制文件传出。注意,后面将展示如何用一个好看易读的 JSON 对象替换十六进制字符串的 ABI。以上,我们只是想证明“hello”类型的 message 是如何发送到账户的。

结果如下。

第五步,继续查看 eosd 的输出,将在屏幕上看到:

Hello?World:?${account}->hello?Hello?World:?${account}->hello?Hello?World:?${account}->hello

再一次,你的合约在 transaction 被第三次应用并成为产出的区块之前被执行和撤销了两次。

第六步,如果查看ABI文件,将会注意到这个ABI 定义了一个叫transfer的 action,它的类型也是 transfer。这就告诉 EOS.IO,当 ${account}->transfer 的message 发生时,它的 payload 是 transfer 类型的。transfer 类型是在 structs 的列表中定义的,其中有个对象,name 属性是 transfer。

第七步,在弄清骨架 ABI 后,可以构造一个 transfer 类型的 message。

第八步,继续观察 eosd 的输出,将看到:

根据ABI,transfer message应该是如下格式:

account_name -> uint64表示这个message的二进制,表示如下:

第九步,修改 hello.cpp,打印出消息内容:

第十步,重编译并部署:

① 重部署将再次调用init( ):

② 执行transfer:

③ 将看到eosd有如下输出:

④ 使用 C++ API来读取 messages,代码如下。

目前我们使用的是C API,因为这是EOS.IO直接暴露给WASM虚拟机的最底层的 API。幸运的是,eoslib 提供了一个更高级的 API,移除了很多不必要的代码。

第十一步,可以像下面一样更新 hello.cpp,把它变得更简洁。

这里可以注意到,我们更新了transfer的struct,直接使用eosio::name 类型,并将 read_message 前后的类型检查压缩为一个单个的 current-Message 调用。

在编译和上传后,还可以看到和 C 语言版本同样的结果。到此为止,已经完成了第一个智能合约“Hello World”的编写。

4. 部署和升级智能合约

如上所述,将合约部署到区块链上可以通过set contract命令简单地完成。并且如果有权限的话,set contract命令还可更新现有合约。

使用下面的命令来部署一个新合约,更新现存合约。

5. 调试智能合约

为调试智能合约,需要安装本地的 eosd 节点。本地的 eosd 节点可以以单独的调试私网来运行,也可以作为调试公网( 或官方的调试网络)的延伸来运行。

在第一次创建智能合约时,最好先在测试私网中测试调试完毕智能合约,因为这样可以完全掌握整个区块链。这使你有无限的 eos,而且可以随时重置区块链的状态。当合约可以上生产环境时,可以通过将本地 eosd 和测试公网( 或官方的调试网络)连接起来以完成公网的调试,这样就可以在本地的 eosd上看到测试网络的数据了。

如果还没有安装本地 eosd,请根据安装指南安装。默认情况下,本地 eosd将只在测试私网中运行,除非修改config.ini 文件,将其与测试公网(或官方的调试网络)节点连接,就像该指南中提到的一样。

(1)方法用于调试智能合约的主要方法是 Caveman调试法,我们使用打印的方法来

监控一个变量并检查合约的流程。在智能合约中打印信息可以通过打印 API(C和 C++)来完成。C++ API 是 C API的封装,因此,大多数情况下我们用的是C++ API。

(2)打印C API 支持打印如下数据类型:prints - a null terminated char array (string);prints_l - any char array (string) with given size;printi - 64-bit unsigned integer;printi128 - 128-bit unsigned integer;printd - double encoded as 64-bit unsigned integer;

printn - base32 string encoded as 64-bit unsigned integer;

printhex - hex given binary of data and its size。

打印时,C++ API 通过重写print( )方法封装了一些上面的C API,使用户不需要关心需要调用哪个打印函数。C++ 打印 API支持以下数据类型:

a null terminated char array (string);integer (128-bit unsigned, 64-bit unsigned, 32-bit unsigned, signed, unsigned);base32 string encoded as 64-bit unsigned integer;struct that has print( ) method。

结语 : 智能合约的漏洞隐患

智能合约的编写其实并不困难,但对于编写智能合约代码的逻辑和正确性需要认真对待。The DAO事件中,黑客就是利用了智能合约的漏洞攻击了该智能合约,使合约内源源不断地有以太币转账到黑客的账户,给以太坊和众筹投资者造成了大量损失。

所以在编写智能合约方面,如果只是想学习一下,那么请随意尝试,但如果是要实际应用智能合约,一定要注意其安全性和正确性。

恋人之间“永不分离”的海誓山盟也可以通过智能合约来实现。如婚前双方拿出部分资产写入智能合约,然后将智能合约的触发条件绑定在婚姻登记链上,一旦双方离婚,智能合约内的财产将直接转入第三方公益组织,双方都将无法拿回财产,这样就使男女双方难以离开对方。

—-

编译者/作者:区块链大本营

玩币族申明:玩币族作为开放的资讯翻译/分享平台,所提供的所有资讯仅代表作者个人观点,与玩币族平台立场无关,且不构成任何投资理财建议。文章版权归原作者所有。

LOADING...
LOADING...