Skip to content

2. AntChain Bridge跨链:异构链插件开发手册

zouxyan edited this page Jun 28, 2024 · 12 revisions

1 介绍

在AntChain Bridge的跨链架构中,中继和区块链之间的交互,需要通过区块链桥接组件(Blockchain Bridge Component, BBC)来完成。

在当前的工程实现中,BBC是以插件的形式实现的,具体包括链上插件和链下插件两个模块。链上插件是直接部署在异构链上的跨链系统合约,负责链上跨链消息的可信收发处理;链下插件是一个可加载的实体插件包,基于插件SDK实现,负责中继和异构链之间的信息交互处理。

2 链上插件开发

2.1 链上插件概述

AntChain Bridge基于可认证消息协议(Authentic Message Protocol, AMP)和消息推送协议(Smart Contract Datagram protocol, SDP)为每种异构链实现一套链上插件,主要包括AuthMessage合约和SDPMessage合约。

AuthMessage合约

跨链通信的基础协议,该合约将跨链消息封装为AM消息,AM消息是可验证的合法消息。

当AuthMessage合约收到中继服务提交的跨链消息时,将对消息解析并进行相关处理,然后转发消息给指定的上层协议合约。 上层协议合约即消息推送协议合约,可以是SDP合约。

SDPMessage合约

实现了智能合约之间的跨链消息传输协议,也称为SDP协议。

在AntChain Bridge跨链系统中,所有区块链都有一个域名,只要指定接收消息的区块链的域名和接收智能合约的地址,一条链上的合约可以通过SDP协议向另一条链上的合约发送任何定制消息。

!!!开发提示

AntChain Bridge链上插件提供的合约模版为基于solidity的跨链系统合约。

如果您的区块链系统支持原生evm合约,直接开发业务合约即可,其中业务合约需要实现IContractUsingSDP接口(参见2.3.2节)。

如果您的区块链系统不支持原生evm合约,请参考本模版实现AuthMsg/SDPMsg/interfaces/lib 等具体合约。

2.2 合约接口介绍

2.2.1 AuthMessage合约

AuthMessage合约主要处理中继和上层协议之间的消息处理及转发,提供上层协议地址设置的接口和消息处理转发的接口。

setProtocol

function setProtocol(address protocolAddress, uint32 protocolType) external;
  • 入参:
    • protocolAddress(address或string):上层协议合约的地址,比如部署好的SDP合约的地址
    • protocolType(uint32):上层协议类型,SDP合约的类型为0,也支持用户自行开发其他协议合约
  • 出参:无
  • 功能:本接口用于设置AM合约上层协议的合约地址信息,上层协议合约可以是SDP合约。AM合约应当记录所有支持的上层协议合约地址,这些信息可用于AM合约在与上层协议交互时的方法调用权限控制。

recvFromProtocol

function recvFromProtocol(address senderID, bytes memory message) external;
  • 入参:
    • senderID(address或string):跨链消息发送链上发送账户的标识,比如发送链为以太坊时可以是链上的合约账户(CA)或外部账户(EOA)的地址,该账户调用当前接口将跨链消息发送至上层协议
    • message(字节数组):上层协议构造并序列化后的消息,对于SDP协议该消息主要包含了接收链的域名、接收合约的地址等字段。
  • 出参:无
  • 功能:本接口用于处理上层协议发送到当前AM协议的消息。协议之间的消息传递一般通过合约调用实现,本接口按照AM协议的序列化格式来实现传递的消息。

recvPkgFromRelayer

    function recvPkgFromRelayer(bytes memory pkg) external;
  • 入参
    • pkg(字节数组):中继提交从其他链发送来的跨链消息。
  • 出参:无
  • 功能:本接口用于中继向接收链上提交跨链消息。pkg是按照AM协议序列化的字节数组,将在本链上的AM合约中反序列化出来,并传递信息给指定ID的上层协议,即通过合约调用发送给上层协议合约。

2.2.2 SDPMessage合约

SDP合约主要负责异构区块链合约消息发送的功能,提供有序消息和无序消息的发送接口及有序消息序号的查询接口。 同时SDP合约作为AM合约的上层协议,需要实现AM上层协议合约接口,包括AM合约地址设置接口和接收AM消息的接口。

sendMessage

    function sendMessage(string calldata receiverDomain, bytes32 receiverID, bytes calldata message) external;
  • 入参:
    • receiverDomain(string):接收跨链消息的区块链的域名
    • receiverID(字节数组):接收跨链消息的账户的标识,一般为接收链上接收合约的地址
    • message(字节数组):AM协议向上传递的序列化消息
  • 出参:无
  • 功能:该接口用于异构链业务合约发送有序跨链消息。需要将异构链业务合约的消息打包为统一格式的SDP消息,然后发送给AM合约。

SDP合约有序消息要求建立通道的概念。一个发送链域名、发送合约标识、接收链域名、接收合约标识】的四元组决定唯一一条通道,SDP合约中需要以该四元组作为key值保存通道的sequence值,sequence表示消息的顺序,从1开始计数。 发送跨链消息时取相应通道sequence填入SDP消息体中,接收跨链消息时,要求检验相应通道sequence的正确性。

sendUnorderedMessage

无序消息发送接口

    function sendUnorderedMessage(string calldata receiverDomain, bytes32 receiverID, bytes calldata message) external;
  • 入参:
    • receiverDomain(string):接收跨链消息的区块链的域名
    • receiverID(字节数组):接收跨链消息的账户的标识,一般为接收链上接收合约的地址
    • message(字节数组):AM协议向上传递的序列化消息
  • 出参:无
  • 功能:该接口用于异构链业务合约发送无序跨链消息。需要将异构链业务合约的消息打包为统一格式的SDP消息,再发送给AM合约。

SDP无序消息不需要维护sequence值,SDP的消息体中将sequence记为-1即表示为无序消息。

querySDPMessageSeq

function querySDPMessageSeq(string calldata senderDomain, bytes32 senderID, string calldata receiverDomain, bytes32 receiverID) external returns (uint32)
  • 入参:
    • senderDomain(string):发送跨链消息的区块链的域名
    • senderID(字节数组):发送跨链消息的账户的标识,一般为发送链上发送合约的地址
    • receiverDomain(string):接收跨链消息的区块链的域名
    • receiverID(字节数组):接收跨链消息的账户的标识,一般为接收链上接收合约的地址
  • 出参:
    • sequence(uint32):接收链上指定接收通道的最新sequence
  • 功能:该接口用于中继验证消息在接收链上的顺序是否合法有效。中继在将跨链消息转发到接收链之前要确保有序消息的顺序是合法有效的,故需要调用该接口查询接收链上相应接收通道的最新sequence,确保当前消息确实是接收链上需要接收的下一条跨链消息

setAmContract

    function setAmContract(address newAmContract) external;
  • 入参:
    • newAmContract(address或string):AM合约地址
  • 出参:无
  • 功能:本接口用于设置AM合约地址。SDP合约需要记录AM合约地址,该信息可用于SDP合约在与AM合约交互时的方法调用权限控制。

setLocalDomain

	function setLocalDomain(string memory domain) external;
  • 入参:
    • domain(string):当前区块链的域名
  • 出参:无
  • 功能:本接口用于将当前区块链的域名设置到SDP合约中,SDP合约查询sequence时需要根据本地域名判断待查询的接收通道是否本链上的接收通道。

recvMessage

AM消息的接收接口

    function recvMessage(string calldata senderDomain, bytes32 senderID, bytes calldata pkg) external;
  • 入参:
    • senderDomain(string):发送跨链消息的区块链的域名
    • senderID(字节数组):发送跨链消息的账户的标识,一般为发送链上发送合约的地址
    • pkg(字节数组):AM协议向上传递的序列化消息
  • 出参:无
  • 功能:该接口用于接收AM合约转发的跨链消息。AM合约收到中继消息并进行解析处理后,会将调用接收链上的SDP合约的该接口将消息转发到接收链

2.3 合约模板介绍

2.3.1 模板概述

AntChain Bridge提供的系统合约模板结构如下:

.
├── AppContract.sol
├── AuthMsg.sol
├── SDPMsg.sol
├── interfaces
│   ├── IAuthMessage.sol
│   ├── IContractUsingSDP.sol
│   ├── ISDPMessage.sol
│   └── ISubProtocol.sol
└── lib
    ├── am
    │   └── AMLib.sol
    ├── sdp
    │   └── SDPLib.sol
    └── utils
        ├── BytesToTypes.sol
        ├── Context.sol
        ├── Ownable.sol
        ├── SafeMath.sol
        ├── SizeOf.sol
        ├── TLVUtils.sol
        ├── TypesToBytes.sol
        └── Utils.sol

6 directories, 17 files

合约模板主要包括四个部分:

  • interfaces:该目录下提供了系统合约的抽象接口,这些接口定义了系统合约的基本规则,具体系统合约时应当实现相应接口,接口模板介绍详见2.3.2节
  • lib:该目录下提供了系统合约依赖的方法库,包括amsdputils三个子目录,其中需要重点关注amsdp目录下的相关实现
    • am:给出了AM合约的核心结构(AM消息、中继消息)定义及相关序列化工具方法,核心结构介绍详见2.3.3节
    • sdp:给出了SDP合约的核心结构(SDP消息)定义及相关序列化工具方法,核心结构介绍详见2.3.3节
    • utils:给出了数据类型转换、数据编码等基本工具方法,工具合约概览参见2.3.4节
  • AuthMsg.solAM合约的具体实现示例,详见2.3.5节
  • SDPMsg.solSDP合约的具体实现示例,详见2.3.6节
  • AppContract.sol:异构链业务合约的简单实现示例,详见2.3.7节

2.3.2 合约接口模板

interfaces目录下提供系统合约的抽象接口,主要包括IAuthMessage、ISDPMessage、ISubProtocol、IContractUsingSDP四个合约接口。 这些接口与具体合约的关系如下图所示:

IAuthMessage

IAuthMessage.sol给出了AM合约的抽象接口,包括AM合约的方法接口定义和主要事件定义,AM合约需要提供设置上层协议地址的接口、接收上层协议消息及中继消息的接口,这些方法接口与2.1节中介绍的AuthMessage合约接口对应一致。

定义名称 类别 简介
SendAuthMessage event 标记当前区块链上跨链消息的发送事件
setProtocol function 设置上层协议合约地址,接口介绍详见2.2.1节
recvFromProtocol function 转发来自上层协议的跨链消息,接口介绍详见2.2.1节
recvPkgFromRelayer function 转发来自中继器的跨链消息,接口介绍详见2.2.1节

开发者实现的AM合约应当实现IAuthMessage接口

ISubProtocol

ISubProtocol.sol给出了AM合约上层协议的抽象接口,AM合约的上层协议需要提供设置AM合约地址及接收AM合约消息的方法接口。

定义名称 类别 简介
setAmContract function 设置AM合约地址,接口介绍详见2.2.2节
recvMessage function 接收来自AM合约的消息,接口介绍详见2.2.2节

ISDPMessage

ISDPMessage.sol给出了SDP合约的抽象接口,SDP合约需要提供发送跨链消息的方法接口。 同时使用SDP协议作为AM合约的上层协议时,要求SDP合约实现上层协议接口(ISubProtocol),故ISDPMessage接口继承了ISubProtocol接口。 整体来看,ISDPMessage中的方法接口与2.2节中介绍的SDPMessage合约接口对应一致。

抽象定义 类别 简介
sendMessage function 发送有序跨链消息,接口介绍详见2.2.2节
sendUnorderedMessage function 发送无序跨链消息,接口介绍详见2.2.2节
querySDPMessageSeq function 查询接收端通道的最新sequence,接口介绍详见2.2.2节
setLocalDomain function 本接口用于将当前区块链的域名设置到SDP合约中,接口介绍详见2.2.2节

开发者实现的SDP合约应当实现ISDPMessage接口

IContractUsingSDP

IContractUsingSDP.sol给出了使用SDP协议的业务合约的抽象接口,业务合约需要提供接收SDP消息的接口,业务合约的其他功能由开发者自定义。

抽象定义 类别 简介
recvMessage function 业务合约接收SDP发送过来的有序跨链消息
recvUnorderedMessage function 业务合约接收SDP发送过来的无序跨链消息

开发者实现的发送跨链消息的业务合约应当实现IContractUsingSDP接口

2.3.3 合约核心结构模板

lib目录下的am目录和sdp目录分别提供AM系统合约和SDP系统合约的核心数据结构,其中AMLib合约中定义了AM消息、中继消息等核心结构及序列化方法,SDPLib合约中定义了SDP消息等核心结构及序列化方法。 AM消息、中继消息及SDP消息的大致关系如下图所示(图示消息的包含关系并非简单填充,涉及序列化等解析工作):

SDP消息

SDP消息(SDPMessage)是SDP协议对跨链消息的封装,保证了异构链定制化消息在跨链系统中流转时的统一格式。同时,SDP协议作为AM上层协议的一个子类型,SDP消息和AM消息的关系就像UDP报文段和IP数据包的关系。SDP协议基于SDP消息允许异构链业务合约通过AM协议向另一个异构链上业务合约发送跨链消息。

SDP消息主要包含接收区块链域名、接收智能合约标识和跨链消息内容,具体结构定义如下:

字段名 类型 简介
receiveDomain string 接收区块链域名
receiver bytes32 接收智能合约标识
message bytes 跨链消息内容,异构链业务合约的跨链消息序列化结果
sequence uint32 唯一跨链通信通道的消息序号

Important

在AntChainBridge网络中,所有的合约ID均使用32字节表示,比如上面的receiver,在SDP合约中,需要将receiver从bytes32类型转换为address类型,以调用receiver合约,具体如何将bytes32转换为具体的合约地址,可以自行定义,比如Ethereum插件合约代码addressToBytesaddressToBytes32,以及从其他类型的区块链发送消息到你的区块链时,你需要告知发送者如何构造接收合约地址的bytes32合约ID。

SDP消息的sequence字段主要用于SDP有序消息的排序。

SDP有序消息要求建立通道的概念。在AntChain Bridge跨链系统中,一个【发送链域名、发送合约标识、接收链域名、接收合约标识】的四元组决定唯一一条跨链通信通道,一条通道中的有序消息之间可能存在依赖关系,应当按序执行。

在SDP合约状态存储中,以四元组作为key记录相应通道的sequence值,sequence表示消息的顺序,从0开始计数,当sequence值为-1时表示该SDP消息为无序消息。合约具体实现k-v存储的方式如下:

  • 关于通道发送端:
    • key:SHA256(发送合约地址,接收链域名,接收合约地址)
    • value:四元组【发送链域名(当前链域名)、发送合约地址、接收链域名、接收合约地址】决定的通道的sequence
  • 关于通道接收端:
    • key:SHA256(发送链域名,发送合约地址,接收合约地址)
    • value:四元组【发送链域名,发送合约地址,接收链域名(当前链域名),接收合约地址】决定的通道的sequence

在SDP合约的执行逻辑中,发送消息时取sequence填入SDP消息体中,接收跨链消息时要求检验sequence的正确性。

AM消息

AM消息(AuthMessage)是AM合约解析上层消息后构造的可验证合法消息,用于解决跨链通信的关键问题,即明确了消息发送的区块链及链上智能合约的身份。AM协议基于AM消息提供了一种机制来验证发送方身份的真实性,从而防止虚假消息或非法消息危害跨链系统。

AM消息主要包含发起此消息的发起者身份、发送协议类型和具体消息内容,具体结构定义如下:

字段名 类型 简介
author bytes32 消息发送者,发送消息合约账户地址的序列化结果
protocolType uint32 上层协议类型,若为0则表示上层协议时SDP协议
body bytes 上层协议发送消息的序列化结果
version uint32 消息版本,主要用于区分AM消息的不同序列化方式
  • body字段为上层协议消息的序列化结果,上层协议消息可以是SDP消息(如果上层协议使用了SDP协议),序列化方式上层协议决定(如果是SDP消息,具体序列化方法参见SDPLib)。
  • version字段用于区分AM消息的不同序列化方式,AM合约需要将序列化的AM消息发送给中继器,并从中继器消息中反序列化出AM消息,合约模板目前提供两种AM消息序列化方式,分别对应V1和V2版本(具体版本值分别为1、2)。
    • V1版本是定长序列化方式(Fixed-Length Serialization),先创建一个指定大小的byte数组body,然后通过bytesToString()函数将原来的byte数组rawMessage的数据拷贝进去。在这个过程中,byte数组body要占用指定大小的空间,不足的部分会用0填充,因此在使用这种方式时需要保证byte数组rawMessage的剩余部分足够填充。
    • V2版本是变长序列化方式(Variable-Length Serialization),使用bytesToVarBytes()函数将原始byte数组rawMessage进行序列化,并返回一个新的byte数组body。在这个过程中不需要指定固定大小的byte数组,序列化的结果会根据原始数据自动调整大小。
    • 合约模板中默认使用V1版本的AM消息。

中继消息

中继收到消息后可能会对消息进行处理,比如向PTC请求验证并得到验证结果,中继处理后的跨链消息被封装为中继消息(MessageFromRelayer)结构,具体定义如下:

字段名 类型 简介
proofData bytes 序列化的可信验证数据,可信验证数据具体结构见proof结构
hints bytes 跨链消息的相关提示信息,可以为空

中继消息封装的可信验证数据proof结构定义大致如下:

字段名 类型 简介
req Request 中继处理请求,包括reqID和rawReqBody两个部分
rawRespBody bytes 中继处理结果,若处理成功该字段包含AM可信验证消息
errorCode uint32 中继处理结果错误码,若处理成功错误码为0
errorMsg string 中继处理结果错误信息,若处理成功错误信息为空
senderDomain string 消息发送链的域名
version uint16 消息版本

可信验证数据proof采用TLV编码方式,主要tag的取值信息表如下:

tag值 编码内容
1 处理请求ID
2 处理请求内容
4 处理请求结构(需要进一步解析请求id和请求内容)
5 请求回复的结果信息
7 请求回复的错误码
8 请求回复的错误信息
9 跨链消息发送链域名
10 版本信息

中继消息中主要需要关注的是可信验证数据proof中的rawRespBody和senderDomain,rawRespBody字段可能包含了序列化的可信AM消息,senderDomain字段表示了跨链发送链的域名信息。

2.3.4 合约工具库模板

lib/utils目录下提供系统合约依赖的工具方法库,工具库简介如下:

合约名称 简介
Utils.sol 提供各种类型转换、字节拷贝、字节读取等工具方法
BytesToTypes.sol 提供bytes字节数组转换为其他数据类型的工具方法
TypesToBytes.sol 提供其他数据类型转换为bytes字节数组的工具方法
TLVUtils.sol 提供tlv编码相关的工具方法
SizeOf.sol 提供获取不同数据类型长度的工具方法
SafeMath.sol 提供安全算术运算的工具方法
Context.sol 提供智能合约上下文的工具方法
Ownable.sol 基于合约上下文提供当前合约所有者权限相关的工具方法

2.3.5 AuthMsg模板合约

AuthMsg.sol合约提供了AM合约具体实现的模板示例

2.3.5.1 合约概述

合约事件
  • SendAuthMessage(从接口中继承):AM消息发送事件,AM消息构造成功后触发该事件将AM消息持久化到发送链账本中,标记跨链消息发送成功
  • SubProtocolUpdate:上层协议更新事件,AM的上层协议可以选择SDP协议或其他自定义协议,设置AM合约的上层协议时触发该事件
合约状态
  • subProtocols 上层协议信息映射表【上层协议合约地址->协议信息:{协议类型,存在标识} 】
  • protocolRoutes 上层协议映射表【上层协议类型->协议合约地址】
函数修改器
  • onlySubProtocols 限制方法只能被上层协议调用
合约方法
  • setProtocol 设置上层协议地址,更新subProtocols和protocolRoutes映射表
  • recvFromProtocol 接收上层协议消息
  • recvPkgFromRelayer 接收中继消息

2.3.5.2 核心方法

recvFromProtocol

recvFromProtocol方法发生在跨链流程的发送链发送消息阶段。发送链上层协议并调用AM合约的该方法发送消息,AM合约基于上层协议消息构造AM可信消息,最终触发AM消息发送事件标记跨链消息发送成功

recvPkgFromRelayer

recvPkgFromRelayer方法发生在跨链流程的接收链接收消息阶段。中继调用接收链AM合约的该方法发送中继消息,AM合约从中继消息中解析出发送链域名和AM可信消息,进而将信息转发给接收链上的SDP合约

2.3.6 SDPMsg模板合约

2.3.6.1 合约概述

合约状态
  • sendSeq:sdp消息发送端的通道信息映射表【SHA256(发送合约地址,接收链域名,接收合约地址) -> sequence】
  • recvSeq:sdp消息接收端的通道信息映射表【SHA256(发送链域名,发送合约地址,接收合约地址) -> sequence】
  • amAddress:底层am合约地址
  • localDomainHash:当前链域名哈希
函数修改器
  • onlyAM 限制方法只能被am合约调用
方法
  • setAmContract 设置底层am合约地址
  • setLocalDomain 设置当前链域名
  • sendMessage 发送有序消息
  • sendUnorderedMessage 发送无序消息
  • recvMessage 接收am消息
  • querySDPMessageSeq 查询有序消息接收端的最新序号

2.3.6.2 核心方法

sendMessage

sendMessage方法发生在发送链发送消息阶段。业务合约调用SDP合约的该方法发送SDP有序消息,SDP合约会将异构链业务合约的定制化消息打包为统一格式的SDP消息,进而转发给底层AM合约

sendUnorderedMessage方法除了不需要第二步骤(获取相应通道的seq),其他流程均相同

recvMessage

recvMessage方法发生在接收链接收消息阶段。AM合约调用该方法将AM消息发送给SDP合约,SDP合约需要检验有序消息的sequence是否正确,进而将消息转发给业务合约。

querySDPMessageSeq

querySDPMessageSeq方法发生在中继向接收链提交有序消息的阶段。中继在将有序跨链消息提交到接收链之前需要调用该方法验证消息顺序是否合法有效。

2.3.7 AppContract.sol模板合约

AppContract.sol合约是一个简单的异构链业务合约示例,主要展示异构链业务合约如何与跨链系统合约进行交互。

开发者在实现异构链业务合约时,主要有两点需要注意:

  • 跨链消息接收接口实现:业务合约应当实现IContractUsingSDP接口,即按接口规则实现消息接收方法recvUnorderedMessagerecvMessage,这两个方法是系统合约可能调用的方法;
// 接口实现
contract AppContract is IContractUsingSDP {

    // 无序消息接收方法实现
    function recvUnorderedMessage(string memory senderDomain, bytes32 author, bytes memory message) external{
        // ...
    }

    // 有序消息接收方法实现
    function recvMessage(string memory senderDomain, bytes32 author, bytes memory message) external{
       // ...
    }

    // 其他业务方法
}
  • 跨链消息发送接口调用:业务合约在发送跨链消息时,可以调用SDP合约的消息发送方法sendUnorderedMessagesendMessage,分别用于有序跨链消息和无序跨链消息的发送。
    function sendUnorderedMessage(string memory receiverDomain, bytes32 receiver, bytes memory message) external {
    	// 调用SDP合约的无序消息发送接口
        ISDPMessage(sdpAddress).sendUnorderedMessage(receiverDomain, receiver, message);

    	// 其他业务逻辑实现...
    }

    function sendMessage(string memory receiverDomain, bytes32 receiver, bytes memory message) external{
    	// 调用SDP合约的有序消息发送接口
        ISDPMessage(sdpAddress).sendMessage(receiverDomain, receiver, message);

        // 其他业务逻辑实现...
    }

2.4 工作流程介绍

以SDP合约为AM合约上层协议、发送有序跨链消息为例,链上插件的完整工作流程如下:

一步:部署系统合约,在发送链和接收链上分别部署AM合约和SDP合约,以发送链为例:

  • 部署AM合约获得AM合约地址为AMaddr
  • 部署SDP合约获得SDP合约地址为SDPaddr

第二步:系统合约信息设置,在发送链和接收链上分别进行系统合约信息设置,具体包括:

  • 设置AM合约的上层协议地址为SDP合约地址,即调用AM合约方法setProtocol(SdpAddr, 0)
  • 设置SDP合约的AM合约地址,即调用SDP合约方法setAmContract(AmAddr)
  • 设置SDP合约的当前链域名,即调用SDP合约方法setLocalDomain(domain)

第三步:合约调用,发送链业务合约调用SDP合约方法sendMessage(receiverDomain,receiverID, message) ,其中receiverDomain为接收链域名,receiverID为接收链接收合约标识,message为发送链定制化的跨链消息序列化结果。 发起合约调用后,合约模板内部调用流程如下图所示(图中蓝色部分表示发送链相关组件,绿色部分表示接收链相关组件):

3 链下插件开发

AntChain Bridge提供了一套SDK帮助开发者完成链下插件的开发。开发者通过实现SDK中规定的接口(SPI),经过简单的编译,即可生成插件包。 此外,AntChain Bridge提供了插件服务(PluginServer, PS)用来加载BBC插件,详情可以参考插件服务的介绍文档。

3.1 SDK安装

请按照SDK的README指引完成安装。

3.2 开发流程介绍

3.2.1 准备

这里我们结合一个简单的Demo工程,来完成讲解,在这里可以看到源码。

.
├── offchain-plugin
└── plugin-loader

该工程共有两个模块:plugin-loaderoffchain-pluginplugin-loader模块有部分加载插件的逻辑,offchain-plugin包含一个BBC实现的Demo,这里主要介绍offchain-plugin。 模块offchain-plugin是一个mock的插件实现,可以编译为正常的插件,且可以被插件服务加载并使用,返回一些mock的数据。 可以看到offchain-plugin的文件树,其中最关键的是类TestChainBBCService.java,它实现了testchain的BBC功能。

├── src
│   ├── main
│   │   ├── java
│   │   │   └── org
│   │   │       └── testchain
│   │   │           ├── MockDataUtils.java
│   │   │           ├── TestChainBBCService.java
│   │   │           └── TestChainSDK.java

3.2.2 BBC实现

3.2.2.1 原理概述

打开TestChainBBCService.java,可以看到下面的代码。

@BBCService(products = "testchain", pluginId = "testchain_bbcservice")
public class TestChainBBCService implements IBBCService {
    
    private TestChainSDK sdk;

    private AbstractBBCContext bbcContext;
    
    // ... 
}

这里有两个关键点:

  • 注解@BBCService

TestChainBBCService类要能成功完成插件的编译,并且被插件服务成功读取到,需要通过注解@BBCService才可以做到。 注解的代码如下,可以看到当前版本的@BBCService有两个字段productspluginIdproducts代表当前实现类对应的区块链类型,比如ethereum等,在当前的例子中填入了字符串testchain,当插件服务需要使用类型为testchain的插件时,就可以根据这个类型找到TestChainBBCService所在的插件了;字段pluginId代表当前插件的ID,由开发者给出,插件服务会根据这个ID维护、使用您的插件。

@Retention(RUNTIME)
@Target(TYPE)
@Inherited
@Documented
public @interface BBCService {

    /**
     * The type for blockchain like ethereum, fabric, mychain, etc.
     *
     * @return {@link String[]}
     */
    String[] products() default {};

    /**
     * the unique identity of the plugin
     *
     * @return {@link String[]}
     */
    String[] pluginId() default {};
}
  • 接口IBBCService

*IBBCService接口可能在发版之前有所变动,若有变更,AntChain Bridge相关人员会及时通知您 这个接口描述了BBC的功能,在文档“区块链桥接组件开发指南”的“链下插件的SPI定义”这一小节已经有所描述。 只有一个被注解@BBCService且实现了接口IBBCService的类,才会被编译为到插件中,且可以被成功加载。 我们在Demo里尽量详细地告诉您应该怎么实现一个插件,因此我们mock了一个TestChainSDK的实现,以及一些简单的数据结构,比如TestChainBlock等,这样您可以直观地感受到一个插件应该要做什么。 除此之外,在类TestChainBBCService中,有两个属性sdk和bbcContext,通常来讲,BBC的实现都需要维护这两个变量。

  • 抽象类AbstractBBCService

除了实现接口之外,我们提供了抽象类AbstractBBCService,它实现了接口IBBCService,并且加入了方法getBBCLogger,这个方法会返回一个slf4j的Logger对象,它为您的BBC实现提供了输出日志的功能。

使用方法如下,您的实现类继承AbstractBBCService,然后在想要打印日志的地方getBBCLogger()即可。具体logger的实例化,是插件的运行平台负责的,比如插件服务会为BBC的实例注入一个logback的Logger对象。

Caution

由于插件加载机制的要求,请不要在项目引入slf4j-api依赖,如果其他依赖中包含slf4j-api依赖,请先引入SDK的依赖,或者exclude其他依赖

@BBCService(products = "testchain", pluginId = "testchain_bbcservice")
public class TestChainBBCService extends AbstractBBCService {

    @Override
    public void startup(AbstractBBCContext abstractBBCContext) {
        // print the log
        getBBCLogger().debug("start up service");
        getBBCLogger().info("start up service");
        getBBCLogger().warn("context is {}", JSON.toJSONString(abstractBBCContext));
        getBBCLogger().error("error is {}", JSON.toJSONString(abstractBBCContext));
    }
}

3.2.2.1 接口实现

接口:startup

当中继调用中继服务,要求其启动一个BBC实例,用于中继和某条testchain交互时,会调用startup。 接口startup用于完成TestChainBBCService的初始化,启动某些资源,比如testchain的SDK等。 在TestChainBBCService中,对startup做了如下实现。

@Override
public void startup(AbstractBBCContext abstractBBCContext) {
    getBBCLogger().info("start up service");
    getBBCLogger().info("context is {}", JSON.toJSONString(abstractBBCContext));

    this.sdk = new TestChainSDK();
    this.sdk.initSDK(abstractBBCContext.getConfForBlockchainClient());
    this.bbcContext = abstractBBCContext;
}

可以看到这里使用传入的context中的confForBlockchainClient初始化了TestChainSDKconfForBlockchainClient是从中继服务提交过来的参数,它是由BBC插件开发者定义的字段,比如它的序列化、反序列化方式,您的实现类会在startup的时候接收到这个confForBlockchainClient,它往往用于区块链客户端的初始化,当然您也可以在其中包含其他信息。

接口:shutdown

当中继完成了某个testchain的工作之后,会主动告知插件服务关闭这个BBC对象,此时插件服务会调用这个shutdown。 可以看到,这里只是关闭了SDK。

@Override
public void shutdown() {
    getBBCLogger().info("shut down service");
    sdk.shutdown();
}
接口:getContext

中继在跨链流程中,会不时调用getContext获取某条testchain的BBC上下文BBCContext,上下文中包含了当前中继所需要知道的部分信息,包括系统合约的状态等。

    @Override
    public AbstractBBCContext getContext() {
        return this.bbcContext;
    }
接口:setupAuthMessageContract

中继在一条testchain注册进来的时候,会执行部署合约的任务,此时会调用插件服务,插件服务会调用对应TestChainBBCService实例的setupAuthMessageContract接口。 接口实现如下,这里我们假定您会通过SDK实现合约的部署,并将合约地址、合约状态(CONTRACT_DEPLOYED)更新到上下文中。

    @Override
    public void setupAuthMessageContract() {

        // Pretend that we deploy AuthMessage contract.
        // And add the relayer address to the AuthMessage contract
        // by calling the `addRelayer`. If you don't want to check
        // the relayer address, just remove the related code from contract.
        AuthMessageContract am = new AuthMessageContract();
        am.setContractAddress("am");
        am.setStatus(ContractStatusEnum.CONTRACT_DEPLOYED);

        // Then set contract to context.
        this.bbcContext.setAuthMessageContract(am);

        getBBCLogger().info("set up am contract");
    }

对于testchain的系统合约(AM和SDP),我们建议您编译好之后,放到插件项目的resources路径下,直接编译到Jar包中,在运行时,可以读取Jar包中合约字节码以完成合约部署。 如果要更新合约代码,您可以更新字节码之后,重新编译插件,并更新插件服务(更新操作详见插件服务文档CLI部分)。 如果您的SDK不支持或者不方便部署合约,建议您在注册一条链之前,提前手动部署好合约,并在自定义的confForBlockchainClient信息中,带上合约地址,并在该方法中解析出该地址,最重要的是请在上下文中记录下合约地址和状态

接口:setupSDPMessageContract

中继在一条testchain注册进来的时候,会执行部署合约的任务,此时会调用插件服务,插件服务会调用对应TestChainBBCService实例的setupSDPMessageContract接口。中继在完成AM的setup之后,会调用setupSDPMessageContract完成SDP的setup。

@Override
public void setupSDPMessageContract() {
    // Pretend that we deploy SDP contract.
    SDPContract sdpContract = new SDPContract();
    sdpContract.setContractAddress("sdp");
    sdpContract.setStatus(ContractStatusEnum.CONTRACT_DEPLOYED);

    // Then set contract to context.
    this.bbcContext.setSdpContract(sdpContract);

    getBBCLogger().info("set up sdp contract");
}

如果您的SDK不支持或者不方便部署合约,建议您在注册一条链之前,提前手动部署好合约,并在自定义的confForBlockchainClient信息中,带上合约地址,并在该方法中解析出该地址,最重要的是请在上下文中记录下合约地址和状态

接口:setProtocol

中继在完成合约部署(setup)之后,会发起请求,将SDP合约配置到AM合约,以支持未来的跨合约调用。 当然,如果您的AM和SDP功能都开发在同一本合约,而不是使用了或者模仿了我们提供的合约模板,这里可以直接返回即可。 这里的逻辑是通过SDK发送交易,调用AM合约的setProtocol接口即可,这里假定您的SDK是发送同步交易,且BBC的setProtocol接口也要求是一个同步的行为,即返回时交易已经上链。

当完成setProtocol之后,应该将AM合约的状态改为CONTRACT_READY

@Override
public void setProtocol(String protocolAddress, String protocolType) {
    this.sdk.syncCallContract(
            this.bbcContext.getAuthMessageContract().getContractAddress(),
            "setProtocol",
            ListUtil.toList(protocolAddress, protocolType)
    );
    this.bbcContext.getAuthMessageContract().setStatus(ContractStatusEnum.CONTRACT_READY);
    getBBCLogger().info("set protocol");
}
接口:setAmContract

中继在完成合约部署(setup)之后,会发起请求,将AM合约配置到SDP合约,以支持未来的跨合约调用。 当然,如果您的AM和SDP功能都开发在同一本合约,而不是使用了或者模仿了我们提供的合约模板,这里可以直接返回即可。 这里的逻辑是通过SDK发送交易,调用AM合约的setAmContract接口即可,这里假定您的SDK是发送同步交易,且BBC的setAmContract接口也要求是一个同步的行为,即返回时交易已经上链。

当完成setAmContractsetLocalDomain之后,应该将SDP合约的状态改为CONTRACT_READY

@Override
public void setAmContract(String contractAddress) {
    this.sdk.syncCallContract(
            this.bbcContext.getSdpContract().getContractAddress(),
            "setAmContract",
            ListUtil.toList(contractAddress)
    );
    // make sure both `setAmContract` and `setLocalDomain` has been called successfully
    this.bbcContext.getSdpContract().setStatus(ContractStatusEnum.CONTRACT_READY);
    getBBCLogger().info("set am contract");
}
接口:setLocalDomain

中继在完成合约部署(setup)之后,会发起请求,将本链的域名设置到SDP合约中。 这里的逻辑是通过SDK发送交易,调用AM合约的setLocalDomain接口即可,这里假定您的SDK是发送同步交易,且BBC的setLocalDomain接口也要求是一个同步的行为,即返回时交易已经上链。

当完成setAmContractsetLocalDomain之后,应该将SDP合约的状态改为CONTRACT_READY,。

@Override
public void setLocalDomain(String domain) {
    this.sdk.syncCallContract(
            this.bbcContext.getSdpContract().getContractAddress(),
            "setLocalDomain",
            ListUtil.toList(domain)
    );
    // make sure both `setAmContract` and `setLocalDomain` has been called successfully
    this.bbcContext.getSdpContract().setStatus(ContractStatusEnum.CONTRACT_READY);
    getBBCLogger().info("set local domain {}", domain);
}
接口:queryLatestHeight

中继会经常询问当前区块链的最新落账高度是多少,即完成共识的高度。通过TestChainBBCService的接口queryLatestHeight,可以查询到testchain当前的高度。 代码实现如下。简单地,通过SDK获取最新高度即可。

    @Override
    public Long queryLatestHeight() {
        getBBCLogger().info("query the latest height");
        return this.sdk.queryLatestHeight();
    }
接口:readCrossChainMessagesByHeight

中继会对testchain的每一个高度,发送请求去查询跨链消息,插件服务就会调用TestChainBBCService的此接口,查询指定高度的跨链消息。 接口实现如下。

@Override
public List<CrossChainMessage> readCrossChainMessagesByHeight(long l) {
    TestChainSDK.TestChainBlock block = this.sdk.queryABlock(l);

    getBBCLogger().info("read cross-chain msg by height");

    return block.getReceipts().stream().filter(
            // find all logs generated by AM contract
            testChainReceipt -> StrUtil.equals(
                    testChainReceipt.getContract(),
                    this.bbcContext.getAuthMessageContract().getContractAddress()
            )
    ).filter(
            // filter the logs with topic 'SendAuthMessage'
            // which contains the sending auth message.
            testChainReceipt -> StrUtil.equals(testChainReceipt.getTopic(), "SendAuthMessage")
    ).map(
            testChainReceipt -> CrossChainMessage.createCrossChainMessage(
                    CrossChainMessage.CrossChainMessageType.AUTH_MSG,
                    block.getHeight(),
                    block.getTimestamp(),
                    block.getBlockHash(),
                    // this is very important to put the auth-message inside
                    HexUtil.decodeHex(testChainReceipt.getLogValue()),
                    // put the ledger data inside, just for SPV or other attestations
                    testChainReceipt.toBytes(),
                    // this time we need no proof data. it's ok to set it with empty bytes
                    "pretend that we have merkle proof or some stuff".getBytes(),
                    testChainReceipt.getTxhash().getBytes()
            )
    ).collect(Collectors.toList());
}

这个接口一般可以分为三步:

  • 获取区块:获取指定高度的区块,或者收据集合,这里的收据中包含了AM合约emit的发送跨链的事件,在合约模板中,是事件SendAuthMessage。如果您的区块链没有收据,而是通过其他方式存储的跨链消息,请获取该高度的存储数据。
  • 解析区块:过滤区块的收据集合,找到发送合约为AM合约,且事件topic为发送跨链事件SendAuthMessage的收据,将收据中存储的AM消息解析出来,这里是存放在LogValue中。这里的目标是将AM合约发送的跨链消息从区块链的存储数据中过滤出来。
  • 组装跨链消息:CrossChainMessage是由AntChain Bridge定义的标准跨链消息体,按需要填入CrossChainMessage的各个字段。在使用CrossChainMessage.createCrossChainMessage创建跨链消息时,需要填入以下信息:
    • 跨链消息类型:这里默认使用CrossChainMessage.CrossChainMessageType.AUTH_MSG即可。
    • 消息所在的区块高度:这里请填入产生和记录跨链消息的对应区块高度。
    • 区块产生的时间戳:这里请填入产生和记录跨链消息的对应区块的事件戳,以毫秒为单位。
    • 区块哈希:这里请填入区块的哈希值,类型为byte数组,一般为32Bytes。
    • 跨链消息:跨链消息的字节数组,比如AM消息,注意这里填入的是合约构造的AM消息,不要带有其他的结构,可以参考Ethereum插件的处理,即将合约事件SendAuthMessage(bytes pkg)中的pkg拿出来填入CrossChainMessage,pkg在合约中填入了序列化的AM消息。

*这个版本不需要_ledgerData__proof_,可置为空。

接口:querySDPMessageSeq

中继在调用relayAuthMessage之前,会对SDP消息做下解析,判断该SDP消息的sequence值,是否满足调用要求,即需要查询当前消息的sequence值。通过TestChainBBCService的接口querySDPMessageSeq可以查询到该值。 实现代码如下。这里调用SDP合约的querySDPMessageSeq接口以获取sequence。当然如果您支持本地调用合约(无需上链),可采用该方式。

@Override
public long querySDPMessageSeq(String senderDomain, String fromAddress, String receiverDomain, String toAddress) {
    // maybe localcall
    TestChainSDK.TestChainReceipt receipt = this.sdk.syncCallContract(
            this.bbcContext.getSdpContract().getContractAddress(),
            "querySDPMessageSeq",
            ListUtil.toList(
                    senderDomain,
                    HexUtil.decodeHex(fromAddress),
                    receiverDomain,
                    HexUtil.decodeHex(toAddress)
            )
    );

    getBBCLogger().info("query sdp msg seq");
    return Long.parseLong((String) receipt.getResult());
}

Important

这里的fromAddresstoAddress将会是十六进制的字符串,需要decode之后使用。

接口:relayAuthMessage

中继在获得CrossChainMessage之后,需要经过路由等步骤,将该消息转发给对应的中继,该中继可以向接收链提交该消息,假定接收链为一条testchain,插件服务会在接收中继请求之后,调用对应TestChainBBCServicerelayAuthMessage接口,向testchain提交跨链消息。 以下为实现逻辑。这里使用SDK发送同步交易,调用AM合约的recvPkgFromRelayer接口,提交序列化的AM消息即可,之后,将结果信息组装到CrossChainMessageReceipt里面,返回即可。

@Override
public CrossChainMessageReceipt relayAuthMessage(byte[] bytes) {
    // call AM contract to commit the AuthMessage.
    TestChainSDK.TestChainReceipt receipt = this.sdk.syncCallContract(
            this.bbcContext.getAuthMessageContract().getContractAddress(),
            "recvPkgFromRelayer",
            ListUtil.toList(HexUtil.encodeHexStr(bytes))
    );
    // call asyncCallContract
//        TestChainSDK.TestChainReceipt receipt = this.sdk.asyncCallContract(
//                this.bbcContext.getAuthMessageContract().getContractAddress(),
//                "recvPkgFromRelayer",
//                ListUtil.toList(HexUtil.encodeHexStr(bytes))
//        );

    // collect receipt information to fill all fields in the CrossChainReceipt
    CrossChainMessageReceipt ret = new CrossChainMessageReceipt();
    ret.setTxhash(receipt.getTxhash());
    ret.setConfirmed(ret.isConfirmed());
    ret.setSuccessful(ret.isSuccessful());
    ret.setErrorMsg(ret.getErrorMsg());

    getBBCLogger().info("crosschain msg receipt [txhash: {}, isConfirmed: {}, isSuccessful: {}, ErrorMsg: {}",
                    ret.getTxhash(), ret.isConfirmed(), ret.isSuccessful(), ret.getErrorMsg());
    return ret;
}

如果SDK返回显示交易执行失败,比如预执行失败等,请您将CrossChainMessageReceiptsuccessful字段置为false,confirmed字段置为false,即可。

ret.setConfirmed(false);
ret.setSuccessful(false);

如果您的SDK支持异步发送交易,在这里可以使用异步发送接口,调用AM合约,仅需要在组装CrossChainMessageReceipt的时候,将confirmed字段设置为false,successful字段设置为true:

ret.setConfirmed(false);
ret.setSuccessful(true);

类似地,如果使用了SDK的同步上链,即交易已经上链,则将confirmed字段设置为true,successful字段设置为true:

ret.setConfirmed(true);
ret.setSuccessful(true);

这里可以参考Ethereum插件的实现

如果想要构造接口relayAuthMessage的输入,可以参考代码

接口:readCrossChainMessageReceipt

中继通过TestChainBBCService的接口relayAuthMessage提交跨链消息之后,如果是异步上链,会定时查询该testchain的交易是否链上确认,此时会调用接口readCrossChainMessageReceipt。 实现代码如下。通过SDK获取对应交易哈希的收据,或者其他数据结构,组装CrossChainMessageReceipt即可。 如果已经落账,则setConfirmed为true,反之为false,同样地,如果交易执行成功,setSuccessful为true,反之为false,类似地,放入错误信息setErrorMsg。

@Override
public CrossChainMessageReceipt readCrossChainMessageReceipt(String txhash) {
    TestChainSDK.TestChainTransaction transaction = this.sdk.queryTx(txhash);

    CrossChainMessageReceipt crossChainMessageReceipt = new CrossChainMessageReceipt();
    crossChainMessageReceipt.setConfirmed(transaction.isConfirmed());
    crossChainMessageReceipt.setSuccessful(transaction.isSuccessToExecute());
    crossChainMessageReceipt.setTxhash(crossChainMessageReceipt.getTxhash());
    crossChainMessageReceipt.setErrorMsg(crossChainMessageReceipt.getErrorMsg());

    getBBCLogger().info("read crosschain message receipt [txhash: {}, isConfirmed: {}, isSuccessful: {}, ErrorMsg: {}",
                crossChainMessageReceipt.getTxhash(), crossChainMessageReceipt.isConfirmed(), crossChainMessageReceipt.isSuccessful(), crossChainMessageReceipt.getErrorMsg());

    return crossChainMessageReceipt;
}

3.2.3 插件编译

在插件的工程目录下,比如plugin-testchain的模块目录下,执行maven编译即可:

mvn clean package

获得的Jar包即可作为插件使用,比如testchainplugin-testchain-0.1-SNAPSHOT-plugin.jar

3.2.4 插件加载

可以通过下面两种方式加载插件:

  • 插件服务加载

直接用插件服务加载,详见插件服务使用手册。

  • Demo工程加载

在Demo工程的另一个模块app中,有类文件LoadPlugin,将编译好的testchain插件放到工程根目录的plugins目录下,运行LoadPlugin.main即可加载插件。 您可以通过debug的方式,看到您的插件,以及生成的TestChainBBCService实例。蓝色为TestChainBBCService实例,红色为插件实例。

image.png

Q&A

Q1:为什么找不到插件中的Resource?

分析

通常表现为getResource返回Null,进而导致一些NPE、IO异常等预期外的报错。

首先,要检查插件Jar包中,是否有预期的Resource,以及路径是否正确。

然后,插件是通过插件服务(Plugin-Server)加载的,每个插件都会有一个独立的类加载器,你需要的Resource应该通过该类加载器获得,插件类加载器的代码可,但是插件的代码是运行在插件服务上的,所以使用类加载器获取Resource或者某些工具库的时候应该注意,是否获取了正确的类加载器来获取Resource,比如通过当前线程的类加载器获取Resource,这样是错误的,因为当前线程将会是插件服务的线程,所以其类加载器是插件服务的类加载器。

案例

开发者在开发FISCO BCOS的BBC插件时,遇到了上述问题,BCOS的SDK无法正确的加载Resource中的动态库,导致无法正确加载证书等配置。

经过分析BCOS SDK是通过加载resource中的SO,来完成JNI调用的,所以问题是未能成功加载resource。

在BCOS SDK代码中,是通过如下方式获取Classloader,进而读取特定的动态库。

public static void loadLibrary(String resourcePath) throws IOException {
    ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
    loadLibrary(resourcePath, classLoader);
}

可以看到这里使用了Thread.currentThread().getContextClassLoader()来获取Classloader,由于当前线程是插件服务的线程,所以获取的Classloader是插件服务的AppClassLoader,resource当然是从插件服务的Jar包中读取,而不是从BCOS插件Jar中读取,因此无法正确获取动态库。

因此在插件startup时,通过主动触发动态库的读取即可解决问题,BCOS SDK是通过静态代码初始化动态库相关的东西,如下代码即可:

@Override
@SneakyThrows
public void startup(AbstractBBCContext abstractBBCContext) {

    Future<?> future = ThreadUtil.execAsync(() -> {
        Thread.currentThread().setContextClassLoader(this.getClass().getClassLoader());
        NativeInterface.secp256k1GenKeyPair();
    });
    future.get();
    // ...
}

首先启动一个线程,并且在第6行设置当前的ContextClassLoader为插件的ClassLoader,然后在第7行,调用了NativeInterface类的某个静态方法,目的为执行NativeInterface的静态代码段来读取动态库,这里由于设置了插件的Classloader,所以可以正确地从插件Jar中读取到动态库。

Warning

在开发插件是要注意到一些执行上下文的问题,比如上面的线程为插件服务的线程,其获取的相关对象都是对插件服务来说的,所以当出现某些资源没有正确加载时,可以看接入链SDK中是否有使用到插件服务的资源,进而导致一些意外的问题。

Q2:为什么插件在插件服务运行时,无法正确读取密钥或者证书?但是在本地单测却可以正确读取?

分析

目前插件服务通过加载插件来运行BBC服务的这种方式,插件代码运行的运行时和本地直接测试运行是有所不同的,主要体现在插件服务对一些全局的配置做出了更改,以及插件服务和插件代码共享同一个JVM的资源等,比如像一些SPI Provider。

插件服务在密码学上使用了BouncyCastle,具体版本请见对应版本插件服务的pom文件,并且在应用启动时注册且优先使用了其Security Provider。

static {
    Security.insertProviderAt(new BouncyCastleProvider(), 1);
    Security.insertProviderAt(new BouncyCastleJsseProvider(), 2);
}

很多区块链SDK都使用了该密码学库,所以这可能有潜在的接口、类型冲突问题,目前只能要求您使用与插件服务兼容的BouncyCastle版本,或者您修改插件服务的BouncyCastle版本,然后重新编译使用。

除此之外,Security Provider使用的BouncyCastle的类来自于插件服务的类加载器,如果您的插件代码或者依赖中,使用到了BouncyCastle的类,则可能产生类冲突问题,或者一些“instanceOf”的判断不符合预期的问题。

下面结合一个案例来理解潜在的问题,来为您提供一些排查问题的思路。

案例

开发者在开发长安链插件时,遇到了密钥格式不匹配“key spec not recognized”的报错(如下图)。

报错信息

经过排查,长安链SDK代码使用了org.bouncycastle.jce.spec.ECPrivateKeySpec去解析密钥,在插件中获取该类使用了插件类加载器,即从插件Jar包中获取Class资源,长安链SDK代码如下,下面第17行报出了上面的异常,这里代码中使用了ECPrivateKeySpec去加载私钥的信息,并且传入了从全局获取的KeyFactorySpi的BouncyCastle的Provider,调用了generatePrivate。

public static PrivateKey getPrivateKeyFromBytes(byte[] pemKey) throws ChainMakerCryptoSuiteException {
    PrivateKey pk = null;

    try {
        PemReader pr = new PemReader(new StringReader(new String(pemKey)));
        PemObject po = pr.readPemObject();
        PEMParser pem = new PEMParser(new StringReader(new String(pemKey)));
        if (po.getType().equals("PRIVATE KEY")) {
            pk = (new JcaPEMKeyConverter()).getPrivateKey((PrivateKeyInfo)pem.readObject());
        } else {
            if (po.getType().equals("EC PRIVATE KEY")) {
                ASN1Sequence sequence = ASN1Sequence.getInstance(po.getContent());
                ECPrivateKey ecPrivateKey = ECPrivateKey.getInstance(sequence);
                ECParameterSpec spec = ECNamedCurveTable.getParameterSpec("secp256r1");
                ECPrivateKeySpec ecPrivateKeySpec = new ECPrivateKeySpec(ecPrivateKey.getKey(), spec);
                KeyFactory factory = KeyFactory.getInstance("ECDSA", "BC");
                return factory.generatePrivate(ecPrivateKeySpec);
            }

            PEMKeyPair kp = (PEMKeyPair)pem.readObject();
            pk = (new JcaPEMKeyConverter()).getPrivateKey(kp.getPrivateKeyInfo());
        }

        return pk;
    } catch (Exception var10) {
        Exception e = var10;
        throw new ChainMakerCryptoSuiteException(e.toString());
    }
}

由于BouncyCastle的Provider的类是插件服务的类加载器提供的,所以进入到generatePrivate之后,在执行下面代码时,并不符合预期。在第23行执行instanceOf的时候,预期应该满足该条件,但是由于var1的ECPrivateKeySpec类来自插件类加载器,而右边的ECPrivateKeySpec来自于插件服务的类加载器,导致instanceOf判断为false,最终导致了最开始的“key spec not recognized”。

image.png

目前提供的解决方案为exclude掉插件中所有BouncyCastle的依赖,不要打包到插件Jar中,并在插件的Pom中添加scope为provided的BouncyCastle依赖,比如:

<dependencies>
  <dependency>
    <groupId>org.chainmaker</groupId>
    <artifactId>chainmaker-sdk-java</artifactId>
    <version>2.3.2</version>
    <exclusions>
      <exclusion>
        <groupId>org.bouncycastle</groupId>
        <artifactId>bcpkix-jdk18on</artifactId>
      </exclusion>
      <exclusion>
        <groupId>org.bouncycastle</groupId>
        <artifactId>bcpkix-jdk15on</artifactId>
      </exclusion>
    </exclusions>
  </dependency>
  <dependency>
    <groupId>org.bouncycastle</groupId>
    <artifactId>bcpkix-jdk18on</artifactId>
    <version>1.75</version>
    <scope>provided</scope>
  </dependency>
</dependencies>

这样就可以规避上面类冲突或者不匹配的问题。

未来版本AntChain Bridge会优化插件类加载器,对一些常用的库作出规定,直接从应用类加载器获取,并且在插件服务的官方实现中提供可配置项,支持配置类加载器的行为,要求指定URL的类从特定的类加载器获取。

Clone this wiki locally