跳到主要内容

PlatON WASM contract (七) - 通用代理

熟悉传统面向对象设计模式的朋友应该知道,最原始的代理者接口与被代理实例的接口几乎是一一对应的,也就是说,一般情况下,传统代理机制几乎是针对某一类型对象实例的。

在区块链领域,智能合约的在线升级一直相对麻烦,大多公链都会面临以下问题:1)合约地址的变更,因为合约升级就意味着需要重新部署(EOS支持在不变更地址的情况下进行合约重部署);2)abi的变更,虽然可以在设计之初,对接口尽量考虑周全,但难免遇到在新增功能时,需要增加新接口的情况。

因此,传统的代理模式在区块链领域并不太适合,而合约升级又是区块链项目的推进过程中难以避免的事件。在前序文章中,Cross团队已经对代理合约的部分机制进行了介绍,接下来,将向大家介绍一种相对通用的代理机制,这种代理机制主要包含两部分:1)代理调用的通用化(Access agent);2)合约映射管理(Contract manager)。本篇主要针对代理调用的通用化进行简单的讲解。

机制简介#

图片

client可通过proxy contract获取相关业务合约的基本信息,再通过代理接口访问service contract提供的具体服务。client无需事先知晓service contract具体地址、abi等部署信息,这样能够在一定程度上,使service contract的升级、维护、更新对client透明。

本篇文章将讲述proxy contract中的Access agent机制,contract manager机制将在后续系列中进行讲解。

合约开发#

service contract#

#include <platon/platon.hpp>#include <vector>
CONTRACT AddressParam : platon::Contract {public:
    PLATON_EVENT1(addCallResult, std::string, int)
    ACTION void init(){}
    ACTION bool testAddFromGeneralProxy(std::vector<int>& eles)    {        int rst = 0;        for (auto itr = eles.begin(); itr != eles.end(); ++itr)        {            rst += *itr;        }
        PLATON_EMIT_EVENT1(addCallResult, "sum" , rst);        return true;    }};
PLATON_DISPATCH(AddressParam, (init)(testAddFromGeneralProxy))

说明:

  1. 该合约的功能非常简单,主要发挥作用的是ACTION类型的接口testAddFromGeneralProxy,接收一个int数据列表,然后对其求和,将结果通过PLATON_EMIT_EVENT1返回;
  2. 实际上,在交易(ACTION)类型的智能合约方法调用中,通过事件来返回用户想要了解的执行结果,是一种非常常用的方式,具体可参见《PlatON上的WASM智能合约开发(3)——事件机制》、《PlatON上的WASM智能合约开发(6)——进阶(代理调用的事件捕获与解析)》;
  3. service contract的被代理方法,推荐(非强制)采用bool类型的返回值,这样代理合约可以比较直观的了解其执行状态。

proxy contract#

#include <platon/platon.hpp>#include <string>
class AddrProxy: public platon::Contract{public:    ACTION void init(){}
    ACTION bool generalCall(const std::string& contractAddr, const platon::bytes& params)    {        auto rst = platon::make_address(contractAddr);        if (!rst.second)        {            platon::internal::platon_throw("invalid contract address!");            return false;        }        else        {            bool result = platon_call(rst.first, params, uint32_t(0), uint32_t(0));            if(result)            {                bool callRst = false;                platon::get_call_output(callRst);                return callRst;            }            else{                platon::internal::platon_throw("proxy call failed!");                return false;            }        }    }};
PLATON_DISPATCH(AddrProxy, (init)(generalCall))

说明:

  1. 该合约实现Access agent的功能,对应的具体接口为ACTION类型的generalCall;
  2. 需要解释的是,由于本篇中尚未对contract manager映射机制进行介绍,所以generalCall的第一个参数暂时使用合约地址;
  3. generalCall的第二个参数是platon::bytes类型(具体为std::vector<byte>,详情参见".\platon-cdt\platon.cdt\include\platon\common.h"),client在通过proxy访问service contract时,service方法名、输入参数会基于合约abi进行编码打包,PlatON在client SDK中提供了编码打包的接口(node.js、python亲测有效,java尚未进行测试),编码打包接口的使用方法将在下文“合约调用”中详细说明;
  4. 在generalCall的具体实现中,首先通过调用platon_call(以service contract地址、编码字好的调用信息作为参数),返回值result标识调用过程本身的执行结果,如果成功,则继续调用platon::get_call_output,获取调用执行的具体情况。

合约部署#

PlatON主网#

在PlatON主网进行合约部署,需要使用platon-truffle工具完成,详细操作参见《PlatON上的WASM智能合约开发(1)——合约开发入门》。

alaya#

目前,官方已经发布了alaya网络上进行合约开发、部署的IDE——PlatON Studio,其安装、配置、启动以及新建合约项目、合约编译、部署的详细教程可参见https://github.com/ObsidianLabs/PlatON-Studio,本文不再赘述。

PlatON Studio还提供了alaya网络上的合约调试工具,如下图所示:

图片

说明(对应图中的编号):

  1. 合约地址,输入合约地址后,即可看到相应合约的详细信息;
  2. 合约相关的ACTION接口方法,下方为接口调用需要输入的参数;
  3. 执行按钮,点击后,将执行相应合约接口的调用;
  4. 合约相关的CONST接口方法;
  5. CONST接口方法对应的输入参数;
  6. CONST方法的执行返回值;
  7. 合约访问的快捷栏,可选择已部署的合约,进入合约调试的详细页面。

合约调用#

本篇文章通过platon-clientsdk-python进行合约调用的测试,在node.js中相关操作更为简洁,本文不再赘述。

代理访问测试用例#

from client_sdk_python import Web3, HTTPProvider, WebsocketProviderfrom client_sdk_python.eth import PlatONfrom client_sdk_python.utils import contracts
false = Falsetrue = TrueinstanceABI = [{"anonymous":false,"input":[{"name":"topic","type":"string"},{"name":"arg1","type":"int32"}],"name":"addCallResult","topic":1,"type":"Event"},{"constant":false,"input":[],"name":"init","output":"void","type":"Action"},{"constant":false,"input":[{"name":"eles","type":"int32[]"}],"name":"testAddFromGeneralProxy","output":"bool","type":"Action"}]instanceContractAddr = 'lat1dgcfjgtdw56rjg7fa90gwzg0fdw9lyrs3kka28'
#测试网/主网账户地址clientAccount = 'latxxxx'
proxyABI= [{"constant":false,"input":[],"name":"init","output":"void","type":"Action"},{"constant":false,"input":[{"name":"contractAddr","type":"string"},{"name":"params","type":"uint8[]"}],"name":"generalCall","output":"bool","type":"Action"}]proxyContractAddr = 'lat1assxpver7n0a4tldhuyhpdlxt2qmdtngsetaem'
#测试网节点地址(或者主网)devIP = 'http://127.0.0.1:6789'
def find_and_cheke_fn_abi(abi, fn_name):    for i in abi:        if i['name'] == fn_name:            i['inputs'] = i.pop('input')            return i    return None
def generalProxy():    w3 = Web3(HTTPProvider(devIP))    platon = PlatON(w3)    hello = platon.wasmcontract(address=proxyContractAddr, abi=proxyABI, vmtype=1)
    instanceCalled = platon.wasmcontract(address=instanceContractAddr, abi=instanceABI,vmtype=1)
    #这里需使用实例的fn_abi进行打包(本例中为AddressParam::testAddFromGeneralProxy接口)    fn_abi = find_and_cheke_fn_abi(instanceABI, 'testAddFromGeneralProxy')    print(fn_abi)    if not fn_abi is None:        params = contracts.encode_abi(w3, fn_abi, [[11, 12, 13]], vmtype=1, data=None, setabi=instanceABI)        print(params)        print(int(params, 16))        print(Web3.toBytes(int(params, 16)))
        tx_events_hash = hello.functions.generalCall(instanceContractAddr, int(params, 16)).transact({'from':clientAccount,'gas':1500000})        tx_events_receipt = platon.waitForTransactionReceipt(tx_events_hash)
        #使用instance的wasmcontract实例来捕获事件        topic_param = instanceCalled.events.addCallResult().processReceipt(tx_events_receipt)        print(topic_param)

说明:

  1. PlatON官方发布的SDK提供了基于abi进行参数序列化的编码接口,在python SDK中是contracts.encode_abi,通过from client_sdk_python.utils import contracts引入模块;
  2. contracts.encode_abi的关键参数有4个:
    1. Web3实例;
    2. service contract相应的接口方法abi,需要从合约abi中获取,本案例中封装了find_and_cheke_fn_abi函数来获取;
    3. service contract方法调用所需的参数,参数的传入规则参见《PlatON上的WASM智能合约开发(4)——详解参数传递》;
    4. vmtype,合约类型,wasm合约固定填写1.
  3. 通过hello.functions.generalCall调用代理合约接口,访问service contract的相应接口,需要注意的是,在python中,需要将encode_abi打包后的结果,通过int(params, 16)转化为16进制整型,才能对应PlatON WASM合约中的platon::bytes数据类型;
  4. 最后通过事件机制捕获用户关心的service contract实际执行结果,事件机制参见《PlatON上的WASM智能合约开发(6)——进阶(代理调用的事件捕获与解析)》。

结果展示:#

图片

alaya相关说明:#

在alaya中执行测试的话,需要安装alaya专用的sdk,目前python的版本官网上尚未发布,node.js的版本具体参见:https://devdocs.alaya.network/alaya-devdocs/zh-CN/JS_SDK/

总结#

本文介绍了PlatON WASM合约方面通用代理代理合约的设计,对其中的Access agent机制进行了讲解,其关键要素如下:

  1. 在代理合约中,通过platon::platon_call(...)进行service contract合约调用;
  2. 在代理合约中,通过platon::get_call_output(...)获取service contract合约调用的执行状态;
  3. 在client_sdk_python中,通过contracts.encode_abi进行接口、参数编码打包;
  4. 在client_sdk_python中,进行代理访问时,需要将encode_abi打包结果转换成16进制整型。

在后续系列中,将对代理合约中的contract manager机制进行讲解。

本教程贡献者 @xiyu