13 合约交互实践

13.1 区块链应用系统架构

区块链技术被誉为继互联网之后最伟大的信息技术革命,能推动和影响社会进步的方方面面。虽然目前还处于发展阶段的早期但已经展现出巨大的技术价值和商业应用前景。区块链技术的核心未来可能会成为国之重器是国家与国家之间综合实力的集中体现。当前最为成功的区块链项目实践为比特币,或者说比特币催生了区块链的诞生。但作为通用智能合约编程平台来说比特币平台功能有限不适合复杂的非金融场景应用。其后任者以太坊平台是另外一个很成功的区块链项目实践,其强化和扩展了智能合约编程平台并将区块链技术发扬光大,成为各种区块链项目的基石和改造定制的起点。现在大部分的商业系统都是以以太坊平台为基础进行定制和升级改造,另外一些区块链项目甚至直接以以太坊公链为基础进行业务场景扩展,如各种基于公链的钱包应用、区块链游戏应用以及金融应用等。

现在一些传统的应用项目也有进行区块链技术改造和升级的要求。一般做法是将系统中业务比较独立,需要利用区块链技术优势来带的成本节约以及工作效率提升,同时区块链技术本身的性能局限性影响不大的系统模块进行区块链改造。区块链技术驱动的项目其技术架构目前主要由两种模式:混合架构与独立架构。

混合架构是指混用传统的技术解决方案如数据库中间件、应用服务器中间件以及KV缓存中间件等,再把系统中需要进行区块链技术升级改造的模块改造为基于区块链系统进行服务提供者,最终用一个总的服务接口进行各种服务的整合并统一对外服务,其系统架构大致如下:


image.png

目前大部分的区块链商业应用项目都是基于此技术架构实现,基于该技术架构的好处是技术成本迁移比较低、时间成本和开发进度可控,能利用区块链技术带来的好处同时也为用户提供高效的操作体验和数据安全性保证。但缺点也比较明显整个系统要依赖于后台服务,如果后台服务宕机整个前端系统不可用。对系统用户交互体验要求比较高同时在线用户并发性强的应用如中心化交易所、中心化钱包应用等以及传统的行业系统进行区块链技术升级和改造时建议采用此技术架构。

另外一个比较有潜力的技术架构是纯区块链技术架构。和传统的混合架构相比最明显的区别就是无单独的后台服务进行业务功能扩展,其核心功能都被封装到智能合约内实现,额外的辅助功能通过具体客户端本地进行实现。纯区块链技术架构目前日渐得到认可有望成为主流技术架构,其能充分利用区块链技术带来的各种技术好处,特别是对数据安全性的保证、系统永远在线的特点以及无独立后台或者没有有效办法对系统用户进行跟踪和定位的特点,在一些特殊应用领域是唯一选择,这个也是笔者比较倾向的技术架构。该技术方案系统架构图大致如下:


image.png

尽管该技术架构有很多好处但其局限性也比较明显,无法承载大量用户的并发操作;交互效率和操作体验依赖于整个区块网络对交易的打包速度;系统能提供的功能受限于底层智能合约执行环境提供的基础功能支持;当然更关键的是技术实现难度比较大,需要清楚所采用的底层区块链平台的各种特性以及在实际编程中采用的对应处理办法;要采用有效和合理的方式与底层智能合约进行交互并对输入参数和返回结果进行处理。虽然有这些限制但一些新兴的小型应用或特殊目的的应用都采用此技术架构。

13.2 合约交互概述

基于以太坊区块链平台的应用系统无论采用哪种技术架构,为了完成特定的功能业务需求最终都会借助于智能合约来实现目的。当智能合约完成最终的编码、调试和部署后,另外一个不得不考虑的问题就是如何和部署在链上的智能合约进行实际交互。当前以太坊作为最成熟的智能合约编程平台以及区块链公链系统,其配套的技术生态与工具链已经相当完善,和合约的交互方式也在不断升级。

和部署在链上的智能合约交互要解决的第一个问题是采用哪种节点和以太网络进行交互。目前主流的以太坊客户端节点有geth和parity。两者都支持mainnet主网和ropsten公共测试网络也都支持标准的rpc请求列表。虽然最初的官方客户端是geth,但parity的创造者也是大有来头是以太坊的原CTO,所以两者可以说是同宗同源不相上下可以凭读者喜好进行选择。但如果针对开发和定制目的来说笔者建议选择geth,其实现采用的go语言环境搭建和上手难度都比较低,可用的第三方资料也比较多。parity客户端实现时采用的是rust虽然也是一个很不错的开发语言但国内目前还相对比较小众,特别是成熟项目开发中使用相对者数量比go要少得多。

不管读者最终采用那种客户端geth、parity或其他客户端实现,都支持标准的rpc请求列表。当客户端区块高度同步到最新后就可以基于此客户端进行智能合约的开发和调试,或者利用本地客户端节点搭建私有连环境进行智能合约的开发和调试。当读者准备好可用的客户端节点并开启rpc服务功能请求支持后,上层应用程序可以通过web3.js库或其各种演化版本进行与以太区块网络的交互。传统的交互手段是在nodejs主环境下,安装web3功能扩展库然后和指定客户端节点进行通信。

然而以web.js及其各种衍生库为基础直接和客户端节点进行通信的传统交互方式首先要面对的是账户管理问题。传统通信手段是通过客户端节点自身进行管理,上层应用提取一个节点可用的账户解锁,后续完成资金转账和智能合约的交互以及最终对原始交易的签名,实际都是通过节点调用其管理的账户来完成的。如果节点放在本地则最终应用除了自己外其他人使用不了,如果节点放在服务端相当于把所有的用户私钥文件统一放在后台中心化服务器,这个也存在很大的风险和安全漏洞。因为私钥文件是区块链上的数字资产的唯一凭证,如果私钥丢失则所有东西都会丢失,账户文件虽然不是直接的私钥但现在可以通过暴力破解的方式尝试解锁。

另外用web.js及其各种衍生库和节点进行传统方式交互要面对的另外一个问题是存储空间和区块同步的问题。以太主网和公共测试网络运行一段时间后区块数目和占用空间会变得越来越大,截止本书写作时主网区块高度已经达到接近800万,要进行完整的区块同步不仅要花费大量的时间同时也要占用很大的空间大致是100个G左右。关键是由于国内严肃的网络环境,使得每次都能完整而快速的进行区块同步成为奢望。但区块链应用的本质是将资金转账和智能合约交互打包为原始的区块交易,然后对交易进行全网广播,进行挖矿的区块节点将收到的交易打包到新挖出的区块中的,其他节点同步了该区块后被打包的交易就生效了。简单来说如果节点不能完成区块同步则所有的交易包括智能合约的交互都不能生效,这个在目前国内严肃的上网环境中基本成为不可解决的非技术难题。

传统交互方式在基于以太坊平台开发应用的早期比较流行,但由于存在的这两个主要问题,在目前实际面向用户的应用项目中已经很少采用传统的交互手段直接和节点进行通信,唯一有价值的是在搭建私有链进行开发和测试阶段还比较常见。

针对储存空间和区块同步的难题目前比较通用的解决办法是利用成熟的以太云节点服务如infura。大名鼎鼎的浏览器钱包插件metamask其底层就是基于infura以太云节点提供的服务来完成转账相关操作。通过利用infura云节点用户不再纠结到底采用那种客户端geth还是parity,就可以利用以太坊智能合约平台提供的各种功能同时还不担心区块同步和储存空间占用问题,因为该以太云节点支持标准的以太坊rpc列表请求服务。

另外一个传统交互方式中要面对的账户管理问题,现在采用的通用做法是在应用程序端本身进行账户管理。把ETH资金的转账以及与智能合约的交互封装为原始的以太区块交易,并通过程序端本身控制的账户进行交易的离线签名,完成交易的签名后通过连接的以太节点如infura云节点进行签名交易的广播。infura云节点不仅支持以太主网还支持各种公共测试网络如ropsten等,链接示意图如下:

无论用户最终选择哪种形式的以太客户端节点,在和合约进行实际交互时都会用到原始的web3.js库或者某种形式的衍生库。因为在封装和签名原始交易的时候要涉及大量的底层操作,特别是智能合约交互时涉及到对参数按照ABI协议规范进行编码以及对返回值进行解码时,如果都手工从头开始进行编解码则工作量极大且容易出现错误,因此官方推出了基于node.js环境的web3库,然后以此库为基础各种衍生库也相继出现,其中比较著名的是truffle开发框架和web3j库。前者以web3.js库为基础并集成了其他一些方便功能库形成了一个一体化的智能合约应用开发框架;后者是web3.js库的java版本实现将以太坊和智能合约引入工业级开发领域,这样在java语言开发中也可以直接和以太网络以及智能合约进行交互。

13.3 范例合约

在具体讲解和链上智能合约交互细节前我们先来看看后面要用到的范例智能合约。为了不因为智能合约本身的功能复杂度导致后面讲解具体交互方式时产生额外困难,笔者对范例合约进行了简化。其主要功能是对指定的数据进行存取同时对外部提供了一个功能函数来处理输入数据,该合约包括了github在线引用第三方库、继承基类合约以及事件定义和触发等,是一个小而完整的合约实现。示意代码如下:

import "https://github.com/dayangxi/soliditynew/contracts/ownership/Ownable.sol";
import "https://github.com/dayangxi/soliditynew/contracts/math/SafeMath.sol";

contract Base {
    function getBalance() public view returns(uint256) {
        return address(this).balance;
    }
}

contract DataDemo is Ownable, Base {
    using SafeMath for uint256;
    uint256 public idata;
    string public strData;
    event LogVal(string info, string sstrData, uint256 iidata);

    function deposit() public payable {
        emit LogVal("deposit", "", msg.value);
    }

    function withDraw() public payable onlyOwner {
        uint256 total = getBalance();
        require(total > 0);
        msg.sender.transfer(total);
        emit LogVal("withDraw", "", total);
    }

    function handle(uint256 a, uint256 b) 
        public pure returns(uint256) {
            //a*a+2*a*b+b*b
            return a.mul(a).add(a.mul(b).mul(2)).add(b.mul(b));
    }
    function setUint(uint256 nnew) public onlyOwner {
        idata = nnew;
        emit LogVal("setUint", "", nnew);
    }

    function setStr(string memory nnew) public onlyOwner {
        strData = nnew;
        emit LogVal("setStr", nnew, 0);
    }
}

BASE合约主要提供余额查询的基础功能,这样继承此合约的子合约都自动支持余额查询;deposit函数支持eth资金的接受;withDraw函数用于提取当前合约可用的资金余额;handle函数用于对输入的数据进行处理和返回处理结果,其实现了一个计算任意两个整数和的平方值。setUint函数和setStr函数用于对合约状态变量进行设置,只能是合约管理者才能调用;LogVal事件用于对操作进行日志记录。

为了方便读者进行测试和研究笔者将该合约在公共测试ropsten上进行了部署,合约部署地址为0x47577fdf4b7B021ce399b0586e02c9a775027058。后续在介绍通过各种库与链上智能合约交互时都是与此合约进行交互,不再另行说明合约功能和用途。

13.4 web3.js与合约交互

你可能感兴趣的:(13 合约交互实践)