Skip to content

执行器模块

linj edited this page Nov 23, 2022 · 1 revision

执行器模块

[TOC]

1 模块介绍

顾名思义,执行器模块主要是用于执行逻辑的。在区块链中,所有的数据都是通过交易生成的,这些数据按照一定的规则,组装成一个个区块,最终形成一个完整的区块链条。

而在一个完整的区块链系统中,交易的执行是核心,比如一次A到B的转账交易,A账户的余额会减少,B账户的余额会增加,这个账户余额变更的逻辑就是执行器要执行的内容。

拿智能合约来说,合约中包含的所有逻辑,就是执行器要执行的逻辑;只不过,合约只是一个具体的逻辑,而执行器则是一个容器,它可以执行数个合约,而不需要知道合约的具体内容。

所以说,执行器模块是一个容器,它支持各种合约的运行(包括系统合约以及各种扩展合约),接收交易,并将交易分配给对应的合约具体执行,同时响应区块链上的各种事件(交易校验、查询请求、执行交易、新增区块、回滚区块),并将事件派发给具体的合约逻辑。

2 逻辑架构及上下文

执行器上下文

执行器模块在整个chain33系统中所处的地位如下图:

执行器上下文

执行器自身能力对外体现为5个接口:

两个只读接口:

  • 交易校验: 通过EventCheckTx事件触发,对交易的有效性进行检查,请求来源于Mempool模块;

  • 查询请求: 查询操作,从交易生成的数据中查询指定信息,具体查询内容不固定,因调用的查询方法而异,一般请求来源于Wallet/Client/Consensus模块;

三个写接口:

  • 交易执行:通过EventExecTxList事件触发,执行交易列表中的每一笔交易,并返回执行结果,请求来源于Blockchain模块;

  • 区块新增:通过EventAddBlock事件触发,执行区块写入链的后处理操作,生成一些本地数据并返回,请求来源于Blockchain模块;

  • 区块回滚:通过EventDelBlock事件触发,执行链中区块回滚的后处理操作,生成一些本地数据并返回,请求来源于Blockchain模块;

执行器内部逻辑结构

执行器内部逻辑结构

如上图所示,执行器本身是一个容器框架,它本身只包含一些通用的调度处理逻辑,将请求分发给具体的执行器合约进行执行,然后收集结果返回。

从设计上来说,每一个具体的执行器(这里指合约,可以认为两者等价)都实现了Driver中的几个核心方法,容器在执行操作时,会根据交易中附带的执行器信息,调用具体的XXXExecutor执行逻辑。

每一个具体的执行器在实现时均分成两部分:

  • 执行器对象(XXXExecutor)

    实现Driver接口,继承DriverBase基类;

    是执行器的主体,包含执行器的主要执行逻辑,一般根据功能划分成多个程序文件,分别执行对应的逻辑;

  • 执行器对象类型(XXXType)

    实现ExecutorType接口,继承ExecTypeBase基类;

    包装执行器类型相关的一些通用能力,如方法列表,Payload结构等,辅助执行器框架完成定制化的处理逻辑;

3 处理逻辑

上面介绍了执行器实现的5个主要能力接口,下面分别介绍每个接口的具体功能以及实现逻辑。

3.1 交易校验

接口定义

type Driver interface {
    CheckTx(tx *types.Transaction, index int) error
}

此接口包含两个参数:

  • tx *types.Transaction

    为本次交易对象,里面包含了交易的所有信息

  • index int

    为本交易在本次打包区块中的索引位置

实现功能: 对本次交易进行业务合法性检查(交易自身的合法性,如签名校验等无需检查,已经在Mempool模块进行过滤),只允许满足本执行器要求的交易执行,否则返回具体的错误信息。

通用处理逻辑

//默认情况下,tx.To 地址指向合约地址
func (d *DriverBase) CheckTx(tx *types.Transaction, index int) error {
	execer := string(tx.Execer)
	if ExecAddress(execer) != tx.To {
		return types.ErrToAddrNotSameToExecAddr
	}
	return nil
}

在DriverBase基类中,仅实现了最基础的校验逻辑,就是检查交易的目标地址是否为本执行器合约的地址,其中:

  • ExecAddress(execer) 实现的能力是从执行器名称获取执行器合约地址;

  • tx.To 则是本次交易的目标地址;

执行器实现

在实现具体的执行器交易校验逻辑时,需要根据自己的业务场景来确定对哪些内容进行检查,尽量确保,通过校验的交易,都可以正确执行;

如果,仅仅需要检查目标地址,则执行器可以不实现这个校验逻辑,系统会自动重用DriverBase的校验逻辑;

如果,本执行器不需要对交易的任何内容进行校验,则覆盖此方法,仅仅返回空即可,如下代码:

func (x *XXXExecutor) CheckTx(tx *types.Transaction, index int) error {
    // 不需要执行任何校验逻辑
    return nil
}

注意: 在执行器开发时,一旦确定实行某一类校验规则,后面不能随意变更,否则可能会引起区块链分叉。如果实在需要变更,则需要对代码进行分叉逻辑处理,保证区块执行结果的一致性。

二次开发时,如果存在校验逻辑,建议单独一个check.go实现。

3.2 交易执行

接口定义

type Driver interface {
	Exec(tx *types.Transaction, index int) (*types.Receipt, error)
}

此接口同样包含两个参数,和交易校验的方法参数含义相同,唯独返回值不同:

  • tx *types.Transaction

    为本次交易对象,里面包含了交易的所有信息

  • index int

    为本交易在本次打包区块中的索引位置

  • *types.Receipt

    返回值,可以看下Receipt的消息结构定义,其中ty用于填写本次交易执行的结果类型,如成功、失败类型;KV则是用来填写本次交易执行的结果,logs为本次交易执行生成的收据信息;

message Receipt {
    int32    ty              = 1;
    repeated KeyValue KV     = 2;
    repeated ReceiptLog logs = 3;
}

实现功能: 交易执行的核心逻辑,执行器合约的主要逻辑在这里体现,它将会解析交易对象中携带的相关信息,然后根据合约设计的规则,执行不同的业务逻辑分支,最终结果将体现在返回的数据中,而这些数据将会在区块内的所有交易执行完成后,写入到区块链上。

通用处理逻辑

DriverBase基类的通用处理逻辑比较复杂,这里以伪码加注释的方式来呈现。

其中 xxx 表示具体的执行器实现, xxxType表示具体的执行器类型对象。

func (d *DriverBase) Exec(tx *types.Transaction, index int) (receipt *types.Receipt, err error) {
    if xxxType == nil {
        return nil, nil
    }
    // 获取具体执行器的消息结构
    if xxx.GetPayloadValue() == nil {
        return nil, nil
    }
    // 使用类型对象解析交易中携带的合约相关请求信息
    actionName, actionValue, err = xxxType.DecodePayloadValue(tx)
    // 约定方法名结构为固定前缀 + 合约的请求动作
    funcName = "Exec_" + actionName
    // 获取本执行器实现的所有方法对象字典,其实本质调用的是 xxxType.GetExecFuncMap
    funcMap = xxx.GetFuncMap()
    // 定位到具体的方法对象
    funcObj = funcMap[funcName]
    // 通过反射调用此方法对象
    funcObj.Call(.....)
    // 获取反射调用结果并返回
}

从上面的伪码逻辑可以看出,在DriverBase基类中,实现的是一个框架逻辑,它会将交易载体对象的解析委托给具体的执行器类型去实现,然后按照约定的规则拼装方法名,最后通过反射的方式调用具体的执行方法。

所以,一般情况下开发者在进行执行器开发时,是不需要实现此方法的,只需要实现此框架逻辑依赖的几个相关方法即可;

执行器实现

参考上面的分析,总共有几处和交易执行相关的地方需要具体执行器的开发者关注。

  • 执行器类型方法:
type ExecutorType interface {
    // 从交易中解析出action名称,action对象 (具体action信息只有具体执行器自己才认识)
    DecodePayloadValue(tx *Transaction) (string, reflect.Value, error)

    // 获取本执行器的方法字典
    GetExecFuncMap() map[string]reflect.Method
}        
  • 执行器对象方法:
type Driver interface {
    // 获取本执行器对应的Payload定义结构
    // 这个方法其实是委托给 ExecutorType.GetPayload() 方法执行的
    GetPayloadValue() types.Message
    
    // 获取本执行器可用的方法字典
    // 此方法也是委托给 ExecutorType.GetExecFuncMap() 方法执行的
    GetFuncMap() map[string]reflect.Method
}    
  • 具体执行方法签名:
func (c *XXX) Exec_Transfer(transfer *types.XXXAction, tx *types.Transaction, index int) (*types.Receipt, error) 

注意: 在真正的执行器开发中,实现执行交易逻辑时,不需要去实现基类的Exec方法,只需要按照上面的规则,实现几个周边的依赖方法; 并且根据自己的合约逻辑实现几个Exec_XXX方法即可(方法签名如上)。具体实现方式,在后面的二次开发章节会详细介绍。

二次开发时,交易执行逻辑建议单独放在一个exec.go文件中。

3.3 历史查询

接口定义

type Driver interface {
	Query(funcName string, params []byte) (types.Message, error)
}

此接口包含两个参数:

  • funcName string

    为本次查询请求的具体方法名,系统会将本次请求自动路由到对应的方法实现

  • params []byte

    为本次查询的具体参数信息,这里接收的为序列化之后的值,查询框架会进行反序列化后再调用具体的查询方法

实现功能: 实现任何查询操作(前提是此操作对应的方法存在),因为是只读的查询操作,不以交易的形式体现,不影响区块数据,不会写入区块链。

通用处理逻辑

本逻辑实现代码,下面以伪码形式呈现。

其中 xxx 表示具体的执行器实现。

func (d *DriverBase) Query(funcname string, params []byte) (msg types.Message, err error) {
    // 获取本执行器实现的所有方法对象字典,其实本质调用的是 xxxType.GetExecFuncMap
    funcMap = xxx.GetFuncMap()
    // 约定方法名结构为固定前缀 + 请求方法名
    funcname = "Query_" + funcname
    // 定位到具体的方法对象
    funcObj = funcMap[funcName]
    // 通过funcObj获取到查询方法参数类型,并将params反序列化
    // 通过反射调用方法对象
    funcObj.Call(.....)
    // 获取反射调用结果并返回    
}

和执行交易的逻辑类似,在DriverBase基类中实现的也是一套框架逻辑,通过方法名定位到具体的方法对象,然后在根据方法类型中定义的参数类型信息,对传入的params参数进行反向序列化,从而得到具体的参数结构信息,最后再利用反射调用具体的查询方法逻辑并返回结果;

所以,一般情况下开发者在进行执行器开发时,也不需要实现此方法的,只需要实现此框架逻辑依赖的几个相关方法即可;

执行器实现

参考上面的分析,总共有几处和查询相关的地方需要具体执行器的开发者关注。

  • 执行器类型方法:
type ExecutorType interface {
    // 获取本执行器的方法字典
    GetExecFuncMap() map[string]reflect.Method
}        
  • 执行器对象方法:
type Driver interface {
    // 获取本执行器可用的方法字典
    // 此方法也是委托给 ExecutorType.GetExecFuncMap() 方法执行的
    GetFuncMap() map[string]reflect.Method
}    
  • 具体查询方法签名:
func (c *XXX) Query_XxxYyyZzz(in *types.XXXReq) (types.Message, error) 

注意: 在真正的执行器开发中,实现查询逻辑时,不需要去实现基类的Query方法,只需要按照上面的规则,实现几个周边的依赖方法; 并且根据自己的合约逻辑实现几个Query_XXX方法即可。具体实现方式,在后面的二次开发章节会详细介绍。

二次开发时,查询逻辑建议单独放在一个query.go文件中。

3.4 新增区块

接口定义

在一个区块内的全部交易都执行完成之后,会将此区块打包上链,在上链之前,会触发这个新增区块的逻辑,系统会遍历区块内的所有交易,逐个调用对应的ExecLocal逻辑。

type Driver interface {
	ExecLocal(tx *types.Transaction, receipt *types.ReceiptData, index int) (*types.LocalDBSet, error)
}

此接口包含三个参数:

  • tx *types.Transaction

    为本次交易对象,里面包含了交易的所有信息

  • receipt *types.ReceiptData

    为本次交易执行时生成的结果信息,其结构定义如下:

message ReceiptData {
    int32    ty              = 1;
    repeated ReceiptLog logs = 3;
}
  • index int

    为本交易,在本次打包区块中的索引位置

实现功能: 根据交易执行生成的结果信息,增加自己的逻辑,生成一些新的数据,用于辅助合约逻辑。这些数据可以认为是本地数据,不会上链(但每个区块节点都会生成),一般用于做多重索引,保存中间数据等,具体视合约逻辑而定。

通用处理逻辑

DriverBase基类的通用处理逻辑比较复杂,这里以伪码加注释的方式来呈现。

其中 xxx 表示具体的执行器实现, xxxType表示具体的执行器类型对象。

func (d *DriverBase) ExecLocal(tx *types.Transaction, receipt *types.ReceiptData, index int) (*types.LocalDBSet, error) {
    // 使用类型对象解析交易中携带的合约相关请求信息
    actionName, actionValue, err = xxxType.DecodePayloadValue(tx)
    // 约定方法名结构为固定前缀 + 合约的请求动作
    funcName = "ExecLocal_" + actionName
    // 获取本执行器实现的所有方法对象字典,其实本质调用的是 xxxType.GetExecFuncMap
    funcMap = xxx.GetFuncMap()
    // 定位到具体的方法对象
    funcObj = funcMap[funcName]
    // 通过反射调用此方法对象
    funcObj.Call(.....)
    // 获取反射调用结果并返回    
}

主体框架逻辑基本和前面的执行交易类似,使用到的相关方法前面也都介绍过,唯一不同的就是方法前缀,这里是"ExecLocal_"。

同样,一般情况下开发者在进行执行器开发时,也不需要实现此方法的,只需要实现此框架逻辑依赖的几个相关方法即可;

执行器实现

本操作涉及到的具体执行器方法和执行器类型方法,和前面的重复,不再列举,这里仅列出ExecLocal_XXX的方法签名。

  • 具体查询方法签名:
func (c *XXX) ExecLocal_XYZ(action *types.XXXyz, tx *types.Transaction, receipt *types.ReceiptData, index int) (*types.LocalDBSet, error) 

注意: 在真正的执行器开发中,实现本逻辑时,只需要根据自己的合约逻辑实现几个ExecLocal_XXX方法即可。具体实现方式,在后面的二次开发章节会详细介绍。

二次开发时,本逻辑建议单独放在一个exec_local.go文件中。

3.5 回滚区块

接口定义

type Driver interface {
	ExecDelLocal(tx *types.Transaction, receipt *types.ReceiptData, index int) (*types.LocalDBSet, error)
}

本接口和新增区块相同,不再详细描述方法定义。

实现功能: 本逻辑和新增区块刚好相反,可以人会是反操作,新增区块中新增的数据,这里需要删除;新增区块中用新值代替旧值,这里需要用旧值代替新值;将数据恢复到原始状态。

通用处理逻辑

和新增区块的逻辑相同,唯一不同的地方仅为方法前缀不同,这里的方法前缀为"ExecDelLocal_";

执行器实现

本操作涉及到的具体执行器方法和执行器类型方法,和前面的重复,不再列举,这里仅列出ExecDelLocal_XXX的方法签名。

  • 具体查询方法签名:
func (c *XXX) ExecDelLocal_XYZ(action *types.XXXyz, tx *types.Transaction, receipt *types.ReceiptData, index int) (*types.LocalDBSet, error) 

注意: 在真正的执行器开发中,实现本逻辑时,只需要根据自己的合约逻辑实现几个ExecLocal_XXX方法即可。具体实现方式,在后面的二次开发章节会详细介绍。

二次开发时,本逻辑建议单独放在一个exec_del_local.go文件中。

3.6 执行器整体处理逻辑

excutor-call-back-framework

4 二次开发

本章节将会以一个简单的例子来介绍执行器的二次开发过程,例子实现的功能比较简单,但包含了实现一个执行器所需要的各方面的方法。

4.1 Echo 执行器整体介绍

本执行器实现一个简单的消息相应功能,支持两种操作ping和pang,交易执行时会往区块链上写入响应数据,本地执行时会记录同一个消息调用的次数,本执行器也支持对消息的调用次数进行查询操作。

执行器目录结构如下:

开发目录结构

其中三个目录作用如下:

  • executor

    执行器的逻辑主题,包含执行器的交易校验、交易执行、区块新增、区块回滚、历史查询的逻辑主体

  • proto

    本执行器中使用到的消息结构定义,protobuf格式

  • types

    执行器类型对象定义,以及protobuf生成的消息结构定义

4.1.1 结构定义

执行器中用到的消息结构统一使用protobuf格式定义,本执行器用到的结构如下:

syntax = "proto3";

package types;
// ping操作action
message Ping {
    string msg     = 1;
}
// pang操作action
message Pang {
    string msg     = 1;
}
// 本执行器的统一Action结构
message EchoAction {
    oneof value {
        Ping       ping       = 1;
        Pang       pang       = 2;
    }
    int32 ty = 3;
}
// ping操作生成的日志结构
message PingLog {
    string msg     = 1;
    string echo    = 2;
    int32  count    = 3;
}
// pang操作生成的日志结构
message PangLog {
    string msg     = 1;
    string echo    = 2;
    int32  count    = 3;
}
// 查询请求结构
message Query {
    string msg     = 1;
}
// 查询结果结构
message QueryResult {
    string msg     = 1;
    int32  count   = 2;
}

同目录下的create_protobuf.sh会使用此文件生成go程序,放入到types目录下:

#!/bin/sh
protoc --go_out=plugins=grpc:../types ./*.proto --proto_path=.

4.1.2 执行器类型和执行器对象

执行器类型用于定义类型相关的一些信息,本例实现如下:

package types
import (
	"github.com/33cn/chain33/types"
	log "github.com/33cn/chain33/common/log/log15"
	"reflect"
)
// 定义本执行器支持的Action种类
const (
	ActionPing = iota
	ActionPang
)
// 定义本执行器生成的log类型
const (
	TyLogPing = 100001
	TyLogPang = 100002
)

var (
	// 本执行器名称
	EchoX = "echo"
	// 定义本执行器支持的Action对应关系
	actionName = map[string]int32{
		"Ping": ActionPing,
		"Pang": ActionPang,
	}
	// 定义本执行器的Log收据解析结构
	logInfo = map[int64]*types.LogInfo{
		TyLogPing: {reflect.TypeOf(PingLog{}), "PingLog"},
		TyLogPang: {reflect.TypeOf(PangLog{}), "PangLog"},
	}
)
var elog = log.New("module", EchoX)

func init() {
	// 将本执行器添加到系统白名单
	types.AllowUserExec = append(types.AllowUserExec, []byte(EchoX))
	// 向系统注册本执行器类型
	types.RegistorExecutor(EchoX, NewType())
}
// 定义本执行器类型
type EchoType struct {
	types.ExecTypeBase
}
// 初始化本执行器类型
func NewType() *EchoType {
	c := &EchoType{}
	c.SetChild(c)
	return c
}
// 返回本执行器的负载类型
func (b *EchoType) GetPayload() types.Message {
	return &EchoAction{}
}
// 返回本执行器名称
func (b *EchoType) GetName() string {
	return EchoX
}
// 返回本执行器中的action字典,支持双向查找
func (b *EchoType) GetTypeMap() map[string]int32 {
	return actionName
}
// 返回本执行器的日志类型信息,用于rpc解析日志数据
func (b *EchoType) GetLogMap() map[int64]*types.LogInfo {
	return logInfo
}

执行器对象则定义执行器的主体逻辑

package executor
import (
	echotypes "github.com/33cn/plugin/plugin/dapp/echo/types"
	"github.com/33cn/chain33/system/dapp"
	"github.com/33cn/chain33/types"
)
var (
	// 执行交易生成的数据KEY
	KeyPrefixPing = "mavl-echo-ping:%s"
	KeyPrefixPang = "mavl-echo-pang:%s"
	// 本地执行生成的数据KEY
	KeyPrefixPingLocal = "LODB-echo-ping:%s"
	KeyPrefixPangLocal = "LODB-echo-pang:%s"
)
// 初始化时通过反射获取本执行器的方法列表
func init() {
	ety := types.LoadExecutorType(echotypes.EchoX)
	ety.InitFuncList(types.ListMethod(&Echo{}))
}
//本执行器的初始化动作,向系统注册本执行器,这里生效高度暂写为0
func Init(name string, sub []byte) {
	dapp.Register(echotypes.EchoX, newEcho, 0)
}
// 定义执行器对象
type Echo struct {
	dapp.DriverBase
}
// 执行器对象初始化包装逻辑,后面的两步设置子对象和设置执行器类型必不可少
func newEcho() dapp.Driver {
	c := &Echo{}
	c.SetChild(c)
	c.SetExecutorType(types.LoadExecutorType(echotypes.EchoX))
	return c
}
// 返回本执行器驱动名称
func (h *Echo) GetDriverName() string {
	return echotypes.EchoX
}

可以看到,这个执行器对象的实现逻辑中基本都是初始化相关的代码,没有真正的处理逻辑,这些逻辑被分散到各个具体的程序文件中实现,下面会详细介绍。

4.2 交易校验

executor/check.go

本执行器不对交易执行任何校验,实现逻辑如下:

package executor
import "github.com/33cn/chain33/types"

// 本执行器不做任何校验
func (h *Echo) CheckTx(tx *types.Transaction, index int) error {
	return nil
}

4.3 交易执行

executor/exec.go

根据交易中携带的信息,执行对应的合约逻辑,实现逻辑如下:

以Exec_Ping为例,它接收携带ping操作的交易,其中第一个参数ping即为我们在protobuf文件中定义的message Ping结构,本合约方法实现的逻辑是根据交易中的ping消息,生成对应的响应消息,如传递的ping msg为"hello",则生成的响应消息为"hello, ping ping ping!",而这个相应消息会以"mavl-echo-ping:hello"为KEY,写入到区块数据中,并且生成对应的PingLog写入交易收据中;

package executor
import (
	echotypes "github.com/33cn/plugin/plugin/dapp/echo/types"
	"github.com/33cn/chain33/types"
	"fmt"
)

func (h *Echo) Exec_Ping(ping *echotypes.Ping, tx *types.Transaction, index int) (*types.Receipt, error) {
	msg := ping.Msg
	res := fmt.Sprintf("%s, ping ping ping!", msg)
	xx := &echotypes.PingLog{Msg:msg, Echo:res}
	logs := []*types.ReceiptLog{{echotypes.TyLogPing, types.Encode(xx)}}
	kv := []*types.KeyValue{{[]byte(fmt.Sprintf(KeyPrefixPing, msg)), []byte(res)}}
	receipt := &types.Receipt{types.ExecOk, kv, logs}
	return receipt, nil
}

func (h *Echo) Exec_Pang(ping *echotypes.Pang, tx *types.Transaction, index int) (*types.Receipt, error) {
	msg := ping.Msg
	res := fmt.Sprintf("%s, pang pang pang!", msg)
	xx := &echotypes.PangLog{Msg:msg, Echo:res}
	logs := []*types.ReceiptLog{{echotypes.TyLogPang, types.Encode(xx)}}
	kv := []*types.KeyValue{{[]byte(fmt.Sprintf(KeyPrefixPang, msg)), []byte(res)}}
	receipt := &types.Receipt{types.ExecOk, kv, logs}
	return receipt, nil
}

4.4 新增区块

executor/exec_local.go

根据交易执行生成的收据信息,执行对应的本地附加逻辑,实现逻辑如下:

以ExecLocal_Ping为例,它根据ping操作的交易执行生成的PingLog,获取到msg信息(还是以hello为例),然后组装成固定的KEY "LODB-echo-ping:hello",然后从LocalDB中读取此KEY数据,并将此消息的调用次数加1,最后将新的KV信息返回;系统会将新的KV写入LocalDB中;

package executor
import (
	"fmt"
	echotypes "github.com/33cn/plugin/plugin/dapp/echo/types"
	"github.com/33cn/chain33/types"
)
// 交易执行成功,将本消息对应的数值加1
func (h *Echo) ExecLocal_Ping(ping *echotypes.Ping, tx *types.Transaction, receipt *types.ReceiptData, index int) (*types.LocalDBSet, error) {
	// 这里简化处理,不做基本的零值及错误检查了
	var pingLog echotypes.PingLog
	types.Decode(receipt.Logs[0].Log, &pingLog)
	localKey := []byte(fmt.Sprintf(KeyPrefixPingLocal, pingLog.Msg))
	oldValue, err := h.GetLocalDB().Get(localKey)
	if err != nil && err != types.ErrNotFound {
		return nil, err
	}
	if err == nil {
		types.Decode(oldValue, &pingLog)
	}
	pingLog.Count += 1
	kv := []*types.KeyValue{{localKey, types.Encode(&pingLog)}}
	return &types.LocalDBSet{kv}, nil
}

// 交易执行成功,将本消息对应的数值加1
func (h *Echo) ExecLocal_Pang(ping *echotypes.Pang, tx *types.Transaction, receipt *types.ReceiptData, index int) (*types.LocalDBSet, error) {
	// 这里简化处理,不做基本的零值及错误检查了
	var pangLog echotypes.PangLog
	types.Decode(receipt.Logs[0].Log, &pangLog)
	localKey := []byte(fmt.Sprintf(KeyPrefixPangLocal, pangLog.Msg))
	oldValue, err := h.GetLocalDB().Get(localKey)
	if err != nil && err != types.ErrNotFound {
		return nil, err
	}
	if err == nil {
		types.Decode(oldValue, &pangLog)
	}
	pangLog.Count += 1
	kv := []*types.KeyValue{{localKey, types.Encode(&pangLog)}}
	return &types.LocalDBSet{kv}, nil
}

4.5 回滚区块

executor/exec_del_local.go

根据交易执行生成的收据信息,执行对应的本地附加逻辑,实现逻辑如下:

以ExecLocal_Ping为例,和上面新增区块逻辑相反,这里会将消息的调用计数减1,如果减到0,则会删除此条数据;

package executor
import (
	"fmt"
	echotypes "github.com/33cn/plugin/plugin/dapp/echo/types"
	"github.com/33cn/chain33/types"
)

// 交易执行成功,将本消息对应的数值减1
func (h *Echo) ExecDelLocal_Ping(ping *echotypes.Ping, tx *types.Transaction, receipt *types.ReceiptData, index int) (*types.LocalDBSet, error) {
	// 这里简化处理,不做基本的零值及错误检查了
	var pingLog echotypes.PingLog
	types.Decode(receipt.Logs[0].Log, &pingLog)
	localKey := []byte(fmt.Sprintf(KeyPrefixPingLocal, pingLog.Msg))
	oldValue, err := h.GetLocalDB().Get(localKey)
	if err != nil {
		return nil, err
	}
	types.Decode(oldValue, &pingLog)
	if pingLog.Count > 0 {
		pingLog.Count -= 1
	}
	val := types.Encode(&pingLog)
	if pingLog.Count == 0 {
		val = nil
	}
	kv := []*types.KeyValue{{localKey, val}}
	return &types.LocalDBSet{kv}, nil
}

// 交易执行成功,将本消息对应的数值减1
func (h *Echo) ExecDelLocal_Pang(ping *echotypes.Pang, tx *types.Transaction, receipt *types.ReceiptData, index int) (*types.LocalDBSet, error) {
	// 这里简化处理,不做基本的零值及错误检查了
	var pangLog echotypes.PangLog
	types.Decode(receipt.Logs[0].Log, &pangLog)
	localKey := []byte(fmt.Sprintf(KeyPrefixPangLocal, pangLog.Msg))
	oldValue, err := h.GetLocalDB().Get(localKey)
	if err != nil {
		return nil, err
	}
	types.Decode(oldValue, &pangLog)
	if pangLog.Count > 0 {
		pangLog.Count -= 1
	}
	val := types.Encode(&pangLog)
	if pangLog.Count == 0 {
		val = nil
	}
	kv := []*types.KeyValue{{localKey, val}}
	return &types.LocalDBSet{kv}, nil
}

4.6 历史查询

executor/query.go

查询交易生成的数据,本合约实现了两个查询方法,分别查询ping操作和pang操作,针对某一个msg的调用次数,实现逻辑如下:

以Query_Ping为例,根据查询请求传入的msg,使用同样的规则拼装KEY,然后从LocalDB中查询新增区块时写入的数据,然后返回此消息的调用次数信息;

package executor
import (
	echotypes "github.com/33cn/plugin/plugin/dapp/echo/types"
	"github.com/33cn/chain33/types"
	"fmt"
)
func (h *Echo) Query_GetPing(in *echotypes.Query) (types.Message, error) {
	var pingLog echotypes.PingLog
	localKey := []byte(fmt.Sprintf(KeyPrefixPingLocal, in.Msg))
	value, err := h.GetLocalDB().Get(localKey)
	if err != nil {
		return nil, err
	}
	types.Decode(value, &pingLog)
	res := echotypes.QueryResult{Msg:in.Msg, Count:pingLog.Count}
	return &res, nil
}
func (h *Echo) Query_GetPang(in *echotypes.Query) (types.Message, error) {
	var pangLog echotypes.PangLog
	localKey := []byte(fmt.Sprintf(KeyPrefixPangLocal, in.Msg))
	value, err := h.GetLocalDB().Get(localKey)
	if err != nil {
		return nil, err
	}
	types.Decode(value, &pangLog)
	res := echotypes.QueryResult{Msg:in.Msg, Count: pangLog.Count}
	return &res, nil
}

4.7 创建交易逻辑

types/echo.go

前面基本已经实现了执行器的大部分逻辑,但是如果想要在区块链上调用此执行器,还需要创建本执行器合约相关的交易,创建交易的逻辑在执行器类型对象中实现。

需要实现CreateTx接口方法,示例如下:

package types

import (
	"encoding/json"
	"math/rand"
	"strings"
	"time"
	"github.com/33cn/chain33/types"
	"github.com/33cn/chain33/common/address"
)

func (e EchoType) CreateTx(action string, message json.RawMessage) (*types.Transaction, error) {
	elog.Debug("echo.CreateTx", "action", action)
	// 只接受ping/pang两种交易操作
	if action == "ping" || action == "pang" {
		var param EchoTx
		err := json.Unmarshal(message, &param)
		if err != nil {
			elog.Error("CreateTx", "Error", err)
			return nil, types.ErrInvalidParam
		}
		return createPingTx(action, &param)
	} else {
		return nil, types.ErrNotSupport
	}
}
func createPingTx(op string, parm *EchoTx) (*types.Transaction, error) {
	var action *EchoAction
	var err error
	if strings.EqualFold(op, "ping") {
		action, err = getPingAction(parm)
	} else {
		action, err = getPangAction(parm)
	}
	if err != nil {
		return nil, err
	}
	tx := &types.Transaction{
		Execer:  []byte(types.ExecName(EchoX)),
		Payload: types.Encode(action),
		Nonce:   rand.New(rand.NewSource(time.Now().UnixNano())).Int63(),
		To:      address.ExecAddress(types.ExecName(EchoX)),
	}
	return tx, nil
}
func getPingAction(parm *EchoTx) (*EchoAction, error) {
	pingAction := &Ping{Msg: parm.Message}
	action := &EchoAction{
		Value: &EchoAction_Ping{Ping: pingAction},
		Ty:    ActionPing,
	}
	return action, nil
}
func getPangAction(parm *EchoTx) (*EchoAction, error) {
	pangAction := &Pang{Msg: parm.Message}
	action := &EchoAction{
		Value: &EchoAction_Pang{Pang: pangAction},
		Ty:    ActionPang,
	}
	return action, nil
}

types/tx.go 定义消息结构体

package types

type EchoTx struct {
	Message   string `json:"msg"`
}

4.8 注册执行器

plugin.go

向系统注册本dapp插件,实现逻辑如下:

一般情况下一个完整的dapp至少还要包含对应的命令行和rpc接口,可参考对应的章节进行实现;

package echo

import (
	"github.com/33cn/chain33/pluginmgr"
	"github.com/33cn/plugin/plugin/dapp/echo/executor"
	echotypes  "github.com/33cn/plugin/plugin/dapp/echo/types"
)

func init() {
	pluginmgr.Register(&pluginmgr.PluginBase{
		Name:     "echo",
		ExecName: echotypes.EchoX,
		Exec:     executor.Init,
		Cmd:      nil,
		RPC:      nil,
	})
}

plugin/init/init.go

在init.go中导入本执行器:

	_ "github.com/33cn/plugin/plugin/dapp/echo"

4.9 测试执行器

经过上面几个步骤,执行器已经构建完成,使用chain33自带的部署工具部署完成后,可以测试下我们开发的执行器运行效果。

a. 执行Ping操作

curl --data-binary '{"jsonrpc":"2.0", "id": 1, "method":"Chain33.CreateTransaction","params":[{"execer":"echo", "actionName":"ping", "payload":{"msg": "hello"}}] }' \
    -H 'content-type:text/plain;' \
    http://localhost:8801

其中,msg属性即为我们要发送的ping消息内容,可自行更改。

输出结果如下:

{"id":2,"result":"0a046563686f12090a070a0568656c6c6f3081ec84bab6b28bbe6c3a223145414b6f7252777837426b51536e575155594b725558594e716f6d31473154366b","error":null}

其中,result内容即为未签名的交易原始内容。

b. 对交易进行签名

 curl --data-binary '{"jsonrpc":"2.0", "id": 1, "method":"Chain33.SignRawTx","params":[{"addr":"1KhZDqKCFWgmCnWzmYhfBkjnW1AZ1SEdDn", "expire":"2h", "txHex":"0a046563686f12090a070a0568656c6c6f3081ec84bab6b28bbe6c3a223145414b6f7252777837426b51536e575155594b725558594e716f6d31473154366b"}] }' \
    -H 'content-type:text/plain;' \
    http://localhost:8901

其中,txHex内容为上一步输出的结果,未签名的交易内容。addr为发送此交易的地址,可更改为自己的地址信息。

输出结果如下:

{"id":2,"result":"0a046563686f12090a070a0568656c6c6f1a6d0801122102114453cbc6043184f17c106a21d65898c844e0b10bac38d9097229f537d09d331a46304402204f64f315637bf7bcdf82ef321c4516f7e77582ca854b301d2374d9248fa373d502202f1f24d769636a006652a0e96eee4579b7b6ae74295f0b8b3a912521cd71c3a328a9e6ffde053081ec84bab6b28bbe6c3a223145414b6f7252777837426b51536e575155594b725558594e716f6d31473154366b","error":null}

其中,result为经过签名的交易内容;

c. 发送交易

 curl --data-binary '{"jsonrpc":"2.0", "id": 1, "method":"Chain33.SendTransaction","params":[{"data":"0a046563686f12090a070a0568656c6c6f1a6d0801122102114453cbc6043184f17c106a21d65898c844e0b10bac38d9097229f537d09d331a46304402204f64f315637bf7bcdf82ef321c4516f7e77582ca854b301d2374d9248fa373d502202f1f24d769636a006652a0e96eee4579b7b6ae74295f0b8b3a912521cd71c3a328a9e6ffde053081ec84bab6b28bbe6c3a223145414b6f7252777837426b51536e575155594b725558594e716f6d31473154366b"}] }' \
    -H 'content-type:text/plain;' \
    http://localhost:8901 

其中,data为签名后的交易内容。

输出结果如下:

{"id":2,"result":"0xe912cdf7b7d132bf7915e0db80bad8be6ce2510c60c25b66fa96dff7cec89404","error":null}

其中,result为发送交易的哈希,可以使用下面命令查看具体的交易发送信息:

./chain33-cli tx query -s 0xe912cdf7b7d132bf7915e0db80bad8be6ce2510c60c25b66fa96dff7cec89404

d. 查询历史数据

重复步骤a-c几次,然后调用下面的命令,来查询携带hello消息的ping操作被调用了多少次:

 curl --data-binary '{"jsonrpc":"2.0", "id": 1, "method":"Chain33.Query","params":[{"execer":"echo", "funcName":"GetPing", "payload":{"msg": "hello"}}] }' \
    -H 'content-type:text/plain;' \
    http://localhost:8901

输出结果如下:

{"id":2,"result":{"msg":"hello","count":3},"error":null}
Clone this wiki locally