PlatON/Alaya上的WASM智能合约开发(7)——进阶(通用代理1)

熟悉传统面向对象设计模式的朋友应该知道,最原始的代理者接口与被代理实例的接口几乎是一一对应的,也就是说,一般情况下,传统代理机制几乎是针对某一类型对象实例的。
在区块链领域,智能合约的在线升级一直相对麻烦,大多公链都会面临以下问题: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,详情参见".\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网络上的合约调试工具,如下图所示:
platon studio
说明(对应图中的编号):

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

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

from client_sdk_python import Web3, HTTPProvider, WebsocketProvider
from client_sdk_python.eth import PlatON
from client_sdk_python.utils import contracts

false = False
true = True
instanceABI = [{"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个:
    a. Web3实例;
    b.service contract相应的接口方法abi,需要从合约abi中获取,本案例中封装了
    find_and_cheke_fn_abi函数来获取;
    c.service contract方法调用所需的参数,参数的传入规则参见《PlatON上的WASM智能合约开发(4)——详解参数传递(PlatON上的WASM智能合约开发(4)——详解参数传递)》;
    d.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机制进行讲解。
1 个赞