区块链桥接组件开发手册 V1 - AntChainOpenLabs/AntChainBridge GitHub Wiki

1 介绍

区块链桥接组件(Blockchain Bridge Component, BBC)负责提供AntChain Bridge和区块链之间的交互能力,在当前的工程实现中,BBC是以插件的形式实现的,具体包括链下插件和链上插件两个模块。

  • 链下插件是一个可加载的实体插件包,其本质上是一组待实现的抽象接口,不同异构链需要结合链SDK按照约定实现接口,从而实现中继和异构链之间的信息交互处理;
  • 链上插件是直接部署在异构链上的跨链系统合约,负责链上跨链消息的可信收发处理,通常由链下插件来发起对链上合约的部署和调用。

2 链下插件开发

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

2.1 SDK安装

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

2.2 开发流程介绍

2.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

2.2.2 BBC原理概述

打开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

[!NOTE]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));
    }
}

2.2.3 BBC接口实现

Tip

建议每个接口的入口出口出打印日志,方便后续功能测试时确保接口是否调用成功!

接口:startup

void startup(AbstractBBCContext context);

接口startup用于完成TestChainBBCService的初始化,启动某些资源,比如testchain的SDK等。

实现逻辑参考
@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插件开发者自行定义。

即区块链客户端初始化时需要提供的信息都可以包含在confForBlockchainClient结构中传入。

接口:shutdown

void shutdown();

当中继完成了某个testchain的工作之后,会主动告知插件服务关闭这个BBC对象,此时插件服务会调用这个shutdown

实现逻辑参考
@Override
public void shutdown() {
    getBBCLogger().info("shut down service");
    sdk.shutdown();
}

接口:getContext

AbstractBBCContext getContext();

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

实现逻辑参考
    @Override
    public AbstractBBCContext getContext() {
        return this.bbcContext;
    }

接口:setupAuthMessageContract

void setupAuthMessageContract();

中继在执行链注册流程时会执行部署AM合约的任务,此时会调用插件服务,插件服务会调用对应BBC实例的setupAuthMessageContract接口。

实现逻辑参考
  1. 被调用到之后,调用区块链SDK,发送交易部署AM合约;

Tip

若您的SDK不支持直接部署合约,建议提前手动部署好AM合约,并在自定义的confForBlockchainClient信息中带上合约地址,然后在当前步骤解析出AM合约地址

  1. 将AM合约地址记录到bbcContext,并将其状态记为CONTRACT_READY

接口:setupSDPMessageContract

void setupSDPMessageContract();

中继在执行链注册流程时会执行部署SDP合约的任务,此时会调用插件服务,插件服务会调用对应BBC实例的setupSDPMessageContract接口。

实现逻辑参考
  1. 被调用到之后,调用区块链SDK,发送交易部署SDP合约;

Tip

若您的SDK不支持直接部署合约,建议提前手动部署好SDP合约,并在自定义的confForBlockchainClient信息中带上合约地址,然后在当前步骤解析出SDP合约地址

  1. 将SDP合约地址记录到bbcContext,并将其状态记为CONTRACT_READY

接口:setProtocol

void setProtocol(String protocolAddress, String protocolType);

中继在完成合约部署(setup)之后,会发起请求,将SDP合约地址记录到AM合约,以支持未来的跨合约调用。
当然,如果您的AM和SDP功能都开发在同一本合约,而不是使用了或者模仿了我们提供的合约模板,这里可以直接返回即可。

Important

当完成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");
}
  1. 调用区块链SDK,调用AM合约的setProtocol方法
  2. bbcContext中AM合约的状态设置为CONTRACT_READY

接口:setAmContract

void setAmContract(String contractAddress);

中继在完成合约部署(setup)之后,会发起请求,将AM合约配置到SDP合约,以支持未来的跨合约调用。
当然,如果您的AM和SDP功能都开发在同一本合约,而不是使用了或者模仿了我们提供的合约模板,这里可以直接返回即可。

Important

当完成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");
}
  1. 调用区块链SDK,调用SDP合约的setAmContract方法
  2. 判断SDP合约的setAmContract方法和setLocalDomain方法是否均已调用成功,若均已调用成功,将bbcContext中SDP合约的状态设置为CONTRACT_READY

接口:setLocalDomain

void setLocalDomain(String domain);

中继在完成合约部署(setup)之后,会发起请求,将本链的域名设置到SDP合约中。

Important

当完成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);
}
  1. 调用区块链SDK,调用SDP合约的setLocalDomain方法
  2. 判断SDP合约的setAmContract方法和setLocalDomain方法是否均已调用成功,若均已调用成功,将bbcContext中SDP合约的状态设置为CONTRACT_READY

接口:queryLatestHeight

Long queryLatestHeight();

中继会经常询问当前区块链的最新落账高度是多少,即完成共识的高度。通过接口queryLatestHeight,可以查询到区块链的当前高度。

实现逻辑参考
    @Override
    public Long queryLatestHeight() {
        getBBCLogger().info("query the latest height");
        return this.sdk.queryLatestHeight();
    }

调用区块链SDK,查询链上最新高度。

接口:readCrossChainMessagesByHeight

List<CrossChainMessage> readCrossChainMessagesByHeight(long height);

中继会对链的每一个高度,发送请求去查询跨链消息,插件服务就会相应调用当前接口查询指定高度的跨链消息。

实现逻辑参考
@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消息。

Note

当前版本不需要_ledgerData__proof_,可置为空。

接口:querySDPMessageSeq

long querySDPMessageSeq(String senderDomain, String fromAddress, String receiverDomain, String toAddress);

中继在调用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

CrossChainMessageReceipt relayAuthMessage(byte[] rawMessage);

中继在获得CrossChainMessage之后,需要经过路由等步骤,将该消息转发给对应的中继,该中继可以向接收链提交该消息,插件服务会在接收中继请求之后,调用对应接收链的relayAuthMessage接口,向接收链提交跨链消息。

实现逻辑参考

使用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

CrossChainMessageReceipt readCrossChainMessageReceipt(String txhash);

中继通过TestChainBBCService的接口relayAuthMessage提交跨链消息之后,如果是异步上链,会定时查询该交易是否链上确认,此时会调用接口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;
}

以下为V1版本新增接口

__

接口:setupPTCContract

void setupPTCContract()

中继在执行链注册流程时会执行部署PTC合约的任务,此时会调用插件服务,插件服务会调用对应BBC实例的setupPTCContract接口。

实现逻辑参考
  1. 被调用到之后,调用区块链SDK,发送交易部署PTC相关合约;

Tip

若您的SDK不支持直接部署合约,建议提前手动部署好PTC合约,并在自定义的confForBlockchainClient信息中带上合约地址,然后在当前步骤解析出PTC合约地址

  1. 将PTC合约地址记录到bbcContext,并将状态记为CONTRACT_READY

接口setPtcContract

void setPtcContract(String ptcContractAddress)

中继调用BBC向AM合约配置ptc合约地址。

实现逻辑参考
  1. 被调用到之后,调用区块链SDK,发送交易调用AM合约的接口,将PTC合约地址配置到AM合约

接口readConsensusState

ConsensusState readConsensusState(BigInteger height);

中继调用BBC获取包含区块头等信息的共识状态,将用于PTC验证等步骤,其中序列化的数据需要和HCDVS插件对齐。

实现逻辑参考
  1. 被调用之后,使用链SDK获取数据,依次填入ConsensusState的构造函数并返回;

构造函数如下:

public ConsensusState(
        BigInteger height,
        byte[] hash,
        byte[] parentHash,
        long stateTimestamp,
        byte[] stateData,
        byte[] consensusNodeInfo,
        byte[] endorsements
) {
    this.csVersion = 1;
    this.height = height;
    this.hash = hash;
    this.parentHash = parentHash;
    this.stateTimestamp = stateTimestamp;
    this.stateData = stateData;
    this.consensusNodeInfo = consensusNodeInfo;
    this.endorsements = endorsements;
}
  • height为当前获取的高度
  • hash记录当前高度区块的hash,注意是byte[]不要填成Hex格式
  • parentHash记录当前高度区块的父区块hash
  • stateTimestamp记录当前区块的时间戳,或者其他代表该区块落帐的时间,单位为毫秒
  • stateData记录当前状态的序列化的内容,这里会用于HCDVS的verifyAnchorConsensusStateverifyConsensusState,往往是区块头,序列化、反序列化方式需要和HCDVS插件对齐
  • consensusNodeInfo记录当前共识的描述内容,往往是当前共识的公钥集合
  • endorsements记录当前共识状态的证明,往往是共识节点的签名集合,HCDVS插件通常会用父共识状态的consensusNodeInfo去验证endorsements对于stateData内容的背书

接口updatePTCTrustRoot

void updatePTCTrustRoot(PTCTrustRoot ptcTrustRoot);
先验知识

首先了解类型PTCTrustRoot,每个PTC都会有自己的信任根,比如委员会PTC,它会将委员会成员当前的公钥放在PTCTrustRoot中并提交到BCDNS中,方便对PTC给出的TpBTA和TpProof做出验证。

  • sigAlgo:ACB SDK中提供的签名算法实现,可以提供KeyPair生成、签名、验签等功能。
  • issuerBcdnsDomainSpace:为PTC签发跨链证书的BCDNS域名空间,比如根域名空间“”或者其他“.com”等。
  • networkInfo:包含PTC的网络信息,这里是bytes类型,对于不同的PTC有不同的内容。
  • ptcCrossChainCert:PTC对应的跨链证书。
  • verifyAnchorMap:记录PTC的验证锚定信息,为Map类型,key为对应验证锚的版本,比如中继链某个高度的共识节点信息。
  • sig:PTC跨链域名证书持有者对于信任根的签名,提交到BCDNS时会被验证。
实现逻辑参考

这里假设增加了PTC合约负责维护、验证各种安全跨链相关数据,并对AM合约提供接口,验证跨链消息的证明(TpProof)。

  1. (链下)调用PTC合约的updatePTCTrustRoot方法,上传TLV序列化的PTCTrustRoot
  2. (链上,PTC合约处理)解析PTCTrustRoot,验证BCDNS对PTC证书的签名,再用PTC证书的公钥验证PTCTrustRoot的sig,并保存。

接口getPTCTrustRoot

PTCTrustRoot getPTCTrustRoot(ObjectIdentity ptcOwnerOid)

获取指定PTC的OID对应的PTCTrustRoot,ACB要求每个PTC OID只能对应一个PTC服务。

实现逻辑参考

被调用之后,调用PTC合约的查询接口,获取PTCTrustRoot,反序列化之后返回

接口hasPTCTrustRoot

boolean hasPTCTrustRoot(ObjectIdentity ptcOwnerOid)

判断是否存在指定PTC的OID对应的PTCTrustRoot。

实现逻辑参考

被调用之后,调用PTC合约的查询接口,判断是否存在对应的PTCTrustRoot并返回;

接口hasPTCVerifyAnchor

boolean hasPTCVerifyAnchor(ObjectIdentity ptcOwnerOid, BigInteger version)

判断链上是否存在PTC OID和version对应的VerifyAnchor。

实现逻辑参考

被调用之后,调用PTC合约的查询接口,判断是否存在对应的VerifyAnchor并返回;

接口getPTCVerifyAnchor

PTCVerifyAnchor getPTCVerifyAnchor(ObjectIdentity ptcOwnerOid, BigInteger version)

从BBC获取指定PTC OID和version对应的VerifyAnchor,通常是从PTC合约中查询。

接口addTpBta

void addTpBta(ThirdPartyBlockchainTrustAnchor tpbta)

向链上增加新的TpBTA,这里建议使用crossChainLane+tpbtaVersion作为key,在PTC合约中存储TpBTA。

实现逻辑参考
  1. 通过区块链SDK发送交易,调用PTC合约接口,提交序列化的TpBTA
  2. PTC合约通过TpBTA中的signerPtcCredentialSubject#applicant找到对应PTC的PTCTrustRoot
  3. 根据PTCTrustRoot中PTC的类型,找到对应的验证函数
  4. 比如Committee类型,从PTCTrustRoot中拿到委员会成员的公钥,依次验证endorseProof中签名的个数大于2/3
  5. 如果未能成功提交,应该抛出异常

接口getTpBta

 ThirdPartyBlockchainTrustAnchor getTpBta(CrossChainLane tpbtaLane, int tpBtaVersion)

通过crossChainLane+tpbtaVersion从BBC查询TpBTA。

实现逻辑参考

通过区块链SDK调用PTC合约的接口查询TpBTA

接口hasTpBta

boolean hasTpBta(CrossChainLane tpbtaLane, int tpBtaVersion)

判断在链上是否有TpBTA存在。

接口getSupportedPTCType

Set<PTCTypeEnum> getSupportedPTCType()

返回BBC当前支持验证的PTC类型,比如当前确定支持Committee类型。

接口queryValidatedBlockStateByDomain

public BlockState queryValidatedBlockStateByDomain(CrossChainDomain recvDomain)

中继调用BBC接口查询发送链SDP合约中继记录的已验证区块信息

实现逻辑参考
  1. SDP合约中需要记录接收链的已验证区块信息,示例如下:
struct BlockState {
    uint16 version;
    string domain;
    bytes32 blockHash;
    uint256 blockHeight;
    uint64 blockTimestamp;
}

    /**
    * 接收链已验证高度:接收链域名哈希 -> 已验证区块信息
    */
    mapping(bytes32 => BlockState) recvValidatedBlockState;
  1. 中继调用queryValidatedBlockStateByDomain接口,查询指定接收链的已验证区块信息,传入参数recvDomain为指定接收链的域名,域名具体字符串哈希后作为SDP合约中recvValidatedBlockState的key
  2. bbc接口调用SDP合约相应的queryValidatedBlockStateByDomain接口
  3. sdp合约中直接返回相应domain的ValidatedBlockState即可,参考实现如下:
    function queryValidatedBlockStateByDomain(string calldata recvDomain) external returns (BlockState memory) {
        return recvValidatedBlockState[keccak256(abi.encodePacked(recvDomain))];
    }

接口recvOffChainException

中继调用该bbc接口发送链下异常消息到SDP合约以触发异常(超时)回滚

CrossChainMessageReceipt recvOffChainException(String exceptionMsgAuthor, byte[] exceptionMsgPkg)
实现逻辑参考
  1. 调用SDP合约相应的recvOffChainException接口
    • exceptionMsgAuthor:原跨链消息的发送业务合约地址
    • exceptionMsgPkg:原跨链消息的SDP消息序列化
  2. sdp合约recvOffChainException接口实现参考
    1. 解析exceptionPkg超时跨链SDP消息原文
    2. 验证SDP消息的messageid在合约中已经存在
    3. 验证exceptionMsgAuthor合约的ackOnError接口触发回滚
function recvOffChainException(bytes32 exceptionMsgAuthor, bytes calldata exceptionMsgPkg) external onlyRelayer{
    SDPMessageV3 memory sdpMessage;
    sdpMessage.decode(exceptionMsgPkg);

    if (0 == sdpMessage.timeoutMeasure) {
        // 无超时限制,不应该触发链下异常
        revert("SDP_MSG_ERROR: the message timeoutMeasure is 0");
    } else if (2 == sdpMessage.timeoutMeasure) {
        // 接收链高度超时限制
        if (recvValidatedBlockState[keccak256(abi.encodePacked(sdpMessage.receiveDomain))].blockHeight > sdpMessage.timeout) {
            // 已验证高度已经过了超时高度,说明消息确实已超时
            // 验证消息原文存在
            if (!sendSDPV3Msgs[sdpMessage.messageId]) {
                revert("SDP_MSG_ERROR: exception message hash does not exist");
            }

            // 调用onError接口
            IContractWithAcks(SDPLib.encodeCrossChainIDIntoAddress(exceptionMsgAuthor)).ackOnError(
                sdpMessage.messageId,
                sdpMessage.receiveDomain,
                sdpMessage.receiver,
                sdpMessage.sequence,
                sdpMessage.nonce,
                sdpMessage.message,
                sdpMessage.errorMsg
            );
        } else {
            revert("SDP_MSG_ERROR: the message is not timeout with timeoutMeasure 2");
        }
    } else {
        revert("SDP_MSG_ERROR: unsupported timeout measure");
    }
}

接口reliableRetry(可选,可靠上链相关接口)

配合relayAuthMessage方法,重新提交交易原文上链

CrossChainMessageReceipt reliableRetry(ReliableCrossChainMessage msg) onlyOwner

2.2.3 插件编译

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

mvn clean package

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

2.2.4 插件加载

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

  • 插件服务加载

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

  • Demo工程加载

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

3 链上插件开发

3.1 合约原理介绍

AntChain Bridge基于可认证消息协议(Authentic Message Protocol, AMP)、消息推送协议(Smart Contract Datagram protocol, SDP)和区块链证明转换组件协议(Proof transform component-Protocol,PTC-Protocol)为每种异构链实现一套链上插件,主要包括AuthMessage合约、SDPMessage合约和PTC相关合约。

其中AM+SDP为跨链消息的基本消息协议合约,PTC相关合约用于支持跨链消息的验证。

3.1.1 AM合约及SDP合约

AuthMessage合约

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

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

AM消息

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

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

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

SDPMessage合约

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

SDP协议是AM上层协议的一个子类型,其基于SDP消息允许异构链业务合约通过AM协议向另一个异构链上业务合约发送跨链消息。

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

SDP消息目前已经迭代了三个版本。

SDPv1 消息

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

字段名 类型 简介
receiveDomain string 接收区块链域名
receiver bytes32 接收智能合约ID/地址
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的正确性。

SDPv2 消息

SDPv2消息在v1版本基础上增加了消息原子性特性的支持。具体结构定义如下(红色字段为新增字段):

字段名 类型 简介
version uint32 标记SDOv2消息版本,恒为2
messageId bytes32 根据消息内容计算出的哈希值,可作为跨链消息的唯一标识。在跨链消息原子性特性中,消息请求原文和相应消息回调的messageId相同。
receiveDomain string 接收区块链域名
receiver bytes32 接收智能合约ID/地址
atomicFlag uint8 消息原子性标志:
+ 0:非原子性消息
+ 1:原子性消息请求
+ 2:原子性消息成功回调请求
+ 3:原子性消息失败回调请求
nonce uint64 无序消息的nonce值,用于保证无序消息的唯一性
sequence uint32 唯一跨链通信通道的消息序号,序号使用方式同SDPv1
message bytes 跨链消息内容,异构链业务合约的跨链消息序列化结果
errorMsg bytes 跨链消息失败信息的序列化结果

对于原子性跨链消息,每一笔跨链请求都会产生两笔跨链消息,即跨链请求消息和跨链回调消息。

发送消息时SDP合约会将请求消息的atomicFlag字段置为1。

当消息成功抵达接收链时,接收链的SDP合约会自动创建一笔成功回调请求消息,并将回调消息的atomicFlag字段置为2,回调消息将会被转发至发送链进行原子性回调操作。

跨链消息失败时同样会触发类似的失败回调。

SDPv3 消息

SDPv3消息在v2版本基础上增加了消息超时回滚特性的支持。具体结构定义如下:

字段名 类型 简介
version uint32 标记SDOv2消息版本,恒为2
messageId bytes32 根据消息内容计算出的哈希值,可作为跨链消息的唯一标识。在跨链消息原子性特性中,消息请求原文和相应消息回调的messageId相同。
receiveDomain string 接收区块链域名
receiver bytes32 接收智能合约ID/地址
atomicFlag uint8 消息原子性标志:
+ 0:非原子性消息
+ 1:原子性消息请求
+ 2:原子性消息成功回调请求
+ 3:原子性消息失败回调请求
nonce uint64 无序消息的nonce值,用于保证无序消息的唯一性
sequence uint32 唯一跨链通信通道的消息序号,序号使用方式同SDPv1
timeoutMeasure uint8 标记超时判断条件类型
+ 0:无超时条件,即不支持超时回滚特性
+ 1:按发送链高度判断是否超时(暂不支持)
+ 2:按接收链高度判断是否超时
+ 3:按发送链时间戳判断是否超时(暂不支持)
+ 4:按接收链时间戳判断是否超时(暂不支持)
timeout uint256 超时时间值,与timeoutMeasure配合使用
+ timeoutMeasure为0时,timeout无意义
+ timeoutMeasure为1时,timeout表示发送链未来高度,当发送链高度大于timeout时消息回调还未在发送链上链,该消息超时(暂不支持)
+ timeoutMeasure为2时,timeout表示接收链未来高度,若接收链高度大于timeouot时消息还未在接收链上链,该消息超时
+ timeoutMeasure为3时,timeout表示发送链未来时间戳,当发送链时间大于timeout时消息回调还未在发送链上链,该消息超时(暂不支持)
+ timeoutMeasure为4时,timeout表示接收链未来时间戳,若接收链时间大于timeouot时消息还未在接收链上链,该消息超时(暂不支持)
message bytes 跨链消息内容,异构链业务合约的跨链消息序列化结果
errorMsg bytes 跨链消息失败信息的序列化结果

关于超时回滚特性,即当跨链消息在提交至接收链后若迟迟没有反馈成功或失败,超过一定时间值后,中继将验证并判断该消息超时异常,主动触发超时回调请求(这笔回调请求不是接收链发起的而是中继发起的)。

跨链消息关系图

跨链消息由业务合约(App合约)发出后,经由SDP合约、AM合约逐层封装抵达中继器。

SDP消息、AM消息及中继消息大致关系如下图所示:

![NOTE]

图示消息的包含关系并非简单填充,涉及序列化等解析工作。

图示SDP消息展示为SDPv1消息,SDPv2/SDPv2消息包含更多字段。

中继消息

中继处理后的跨链消息被封装为中继消息(MessageFromRelayer)结构,具体定义如下:

字段名 类型 简介
proofData bytes 序列化的可信验证数据,其中包含AM跨链消息
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字段表示了跨链发送链的域名信息。

3.1.2 PTC相关合约

PTC相关合约主要提供跨链消息验证能力。

  • Ptchub:提供PTC管理能力,记录了不同类型PTC的证书、信任锚以及验证器路由等信息
  • CommitteePtcVerifier:委员会类型PTC具体验证合约,提供具体的PTC验证能力

3.2 合约接口介绍

针对AM、SDP、PTC以及业务Demo合约,抽象提取链IAuthMessage、ISubProtocol、ISDPMessage、IPtcHub、IPtcVerifier、IContractWithAcks、IContractUsingSDP七个合约接口。
这些接口与具体合约的关系如下图所示(接口参数及返回值均省略):

下面针对AM、SDP、PTC合约的接口分别展开具体介绍。

3.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的上层协议,即通过合约调用发送给上层协议合约。

3.2.2 SDPMessage合约

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

状态:sendSeq

mapping(bytes32 => uint32) sendSeq;

记录当前SDP合约发出的有序消息的序号,key为【接收链-发送业务合约-接收业务合约】(发送链为当前合约所在链)的序列化,value为有序消息序号。

状态:recvSeq

mapping(bytes32 => uint32) recvSeq;

记录当前SDP合约接收到的有序消息的序号,key为【发送链-发送业务合约-接收业务合约】(接收链为当前合约所在链)的序列化,value为有序消息序号。

状态:sendNonce

mapping(bytes32 => uint32) sendNonce;

记录当前SDP合约发送的无序消息的nonce,key为【接收链-发送业务合约-接收业务合约】(发送链为当前合约所在链)的序列化,value为无序消息的nonce值。

状态:recvValidatedBlockState

mapping(bytes32 => BlockState) recvValidatedBlockState;
struct BlockState {
    uint16 version;
    string domain;
    bytes32 blockHash;
    uint256 blockHeight;
    uint64 blockTimestamp;
}

记录接收链的已验证区块高度,用于SDPv3的超时回滚特性。

key为接收链域名哈希,value为已验证区块高度信息。

  • version:BlockState 结构版本,目前为1;
  • domain:接收链域名;
  • blockHash:区块哈希,用于标识已验证区块;
  • blockHeight:区块高度,已验证区块的块高,可作为超时判断标准值;
  • blockTimestamp:区块时间戳,已验证区块的时间戳,可作为超时判断标准值。

状态:sendSDPV3Msgs

mapping(bytes32 => bool) sendSDPV3Msgs;

记录当前SDP合约发送出的原子性消息的哈希,用于SDPv3消息的超时回滚特性,超时消息回滚前需要根据这里记录的消息哈希值来验证确保回滚的消息确实由当前SDP合约发出。

key为消息哈希值,value为true。

状态:relayerAuthMap

mapping(identity => bool) public relayerAuthMap;

记录中继地址,用于限制SDP合约中部分接口只能由中继身份调用。

接口:sendMessage

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

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

接口:sendMessageV2

function sendMessageV2(string calldata receiverDomain, identity receiverID, bool atomic, bytes calldata message) external returns (bytes32);
  • 入参:
    • receiverDomain(string):接收跨链消息的区块链的域名
    • receiverID(字节数组):接收跨链消息的账户的标识,一般为接收链上接收合约的地址
    • atomic(布尔值):是否为原子性消息
    • message(字节数组):AM协议向上传递的序列化消息
  • 出参
    • messageId(字节数组):返回SDP消息的唯一哈希标识messageId
  • 功能:该接口用于异构链业务合约发送有序跨链消息,基于SDPv2消息,支持跨链消息的原子性。

SDPv2有序消息的通道概念同sendMessage接口,即一个发送链域名、发送合约标识、接收链域名、接收合约标识】的四元组决定唯一一条通道,并对应唯一增长的通道sequence。

SDPv2消息相对v1版本新增消息原子性特性支持,通过atomic参数来区分当前消息是否为原子性消息,若为原子性消息,发送后将会生成一条回调消息向发送链同步跨链结果,以保证发送方和接收方跨链操作的一致性。

SDPv2消息相对v1版本新增SDP消息哈希标识**messageId**,即当前方法的返回值,便于标识同一个原子性消息的跨链请求消息和跨链回调消息,即跨链请求消息和跨链回调消息的哈希标识相同。

接口:sendUnorderedMessageV2

function sendUnorderedMessageV2(string calldata receiverDomain, identity receiverID, bool atomic, bytes calldata message) external returns (bytes32);
  • 入参:
    • receiverDomain(string):接收跨链消息的区块链的域名
    • receiverID(字节数组):接收跨链消息的账户的标识,一般为接收链上接收合约的地址
    • atomic(布尔值):是否为原子性消息
    • message(字节数组):AM协议向上传递的序列化消息
  • 出参:
    • messageId(字节数组):返回SDP消息的唯一哈希标识messageId
  • 功能:该接口用于异构链业务合约发送无序跨链消息,基于SDPv2消息,支持跨链消息的原子性

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

该接口同样新增原子性、SDP消息哈希标识等SDPv2消息特性。

接口:sendMessageV3

function sendMessageV3(string calldata receiverDomain, identity receiverID, bool atomic, bytes calldata message, uint8 timeoutMeasure, uint256 timeout) external returns (bytes32);
  • 入参:
    • receiverDomain(string):接收跨链消息的区块链的域名
    • receiverID(字节数组):接收跨链消息的账户的标识,一般为接收链上接收合约的地址
    • atomic(布尔值):是否为原子性消息
    • message(字节数组):AM协议向上传递的序列化消息
    • timeoutMeasure(uint8):标记超时判断条件,目前仅支持02
      • 0:无超时条件,即不支持超时回滚特性
      • 2:按接收链高度判断是否超时
    • timeout(uint256):超时时间值,与timeoutMeasure配合使用
      • timeoutMeasure为0时,timeout无意义
      • timeoutMeasure为2时,timeout表示接收链未来高度,若接收链高度大于timeouot时消息还未在接收链上链,该消息超时
  • 出参
    • messageId(字节数组):返回SDP消息的唯一哈希标识messageId
  • 功能:该接口用于异构链业务合约发送有序跨链消息,基于SDPv3消息,支持跨链消息的原子性及超时回滚特性。

SDPv3有序消息的通道概念同sendMessage接口,即一个发送链域名、发送合约标识、接收链域名、接收合约标识】的四元组决定唯一一条通道,并对应唯一增长的通道sequence。

SDPv3消息相对v2版本新增消息超时回滚特性支持,通过timeoutMeasuretimeout参数来判断消息是否超时,若消息超时,将会触发超时回滚处理,保证SDP消息在有限时间内有唯一确定的一致性结果。

接口:sendUnorderedMessageV3

function sendUnorderedMessageV2(string calldata receiverDomain, identity receiverID, bool atomic, bytes calldata message) external returns (bytes32);
  • 入参:
    • receiverDomain(string):接收跨链消息的区块链的域名
    • receiverID(字节数组):接收跨链消息的账户的标识,一般为接收链上接收合约的地址
    • atomic(布尔值):是否为原子性消息
    • message(字节数组):AM协议向上传递的序列化消息
  • 出参:
    • messageId(字节数组):返回SDP消息的唯一哈希标识messageId
  • 功能:该接口用于异构链业务合约发送无序跨链消息,基于SDPv2消息,支持跨链消息的原子性

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

该接口同样新增超时回滚等SDPv3消息特性。

接口: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合约的当前接口进一步解析SDP消息,以转发至接收链。
实现逻辑参考

该接口会根据SDP消息的版本进行相应的处理:

  1. SDPv1消息:直接解析SDP消息,若为有序消息需要检查sequence是否正确,然后转发消息至接收链业务合约;
  2. SDPv2消息:
    1. 若消息为非原子性消息,处理方式基本类似SDP v1消息
    2. 若消息为原子性请求消息,解析SDP消息及转发SDP至接收链业务合约时判断处理是否成功,根据处理成功与否构造相应的回调消息发送至AM合约。
    3. 若消息为原子性成功回调消息,解析消息后转发至发送链业务合约相应的成功回调接口;
    4. 若消息为原子性失败回调消息,解析消息后转换至发送链业务合约相应的失败回调接口。
  3. SDPv3消息:
    1. 若消息为非原子性消息,除了基本的消息解析和sequence检查外,还需要进行超时判断处理,即结合SDP消息的timeoutMeasuretimeout字段和SDP合约中记录的接收链区块高度情况判断消息是否超时,若超时消息处理失败;
    2. 若消息为非原子性请求消息,在SDPv2的处理方式基础上增加了超时判断处理
    3. 若消息为原子性成功回调消息,解析消息后转发至发送链业务合约相应的成功回调接口;
    4. 若消息为原子性失败回调消息,解析消息后转换至发送链业务合约相应的失败回调接口;
    5. 若消息为原子性超时回滚回调消息,解析消息后,更新SDP合约中记录的接收链区块高度情况。

接口:recvOffChainException

function recvOffChainException(bytes32 exceptionMsgAuthor, bytes calldata exceptionMsgPkg) external;
  • 入参:
    • exceptionMsgAuthor:超时异常的跨链消息原文的发起合约
    • exceptionMsgPkg:超时异常的跨链消息的SDP原文信息
  • 出参:无
  • 功能:该接口用于接收超时回滚异常信息。

实现逻辑参考

  1. 由中继调用,中继将超时异常的跨链消息原文重新打包并传入当前接口;
  2. 验证超时消息是否由当前合约发出(消息哈希是否在当前SDP合约中有记录);
  3. 验证超时消息是否确实超时(结合消息原文的超时字段和当前SDP合约中记录的接收链区块高度信息进行验证)
  4. 触发超时回调,即调用消息原文的发起合约的超时回调接口。

接口:queryValidatedBlockStateByDomain

function queryValidatedBlockStateByDomain(string calldata destDomain) external view returns (BlockState memory);
  • 入参:
    • destDomain:接收链域名
  • 出参:
    • BlockState结构:用于记录区块链最新已验证区块的超时信息,包含五个字段:
      • version:BlockState结构的版本,该字段实际在合约中暂未使用到
      • domain:相关区块链的域名,该字段实际在合约中暂未使用到
      • blockHash:相关区块链当前最新已验证区块的区块哈希,用于标记最新已验证区块
      • blockHeight:相关区块链当前最新已验证区块的区块高度,当跨链消息的超时判断条件(timeoutMeasure字段)为接收链高度时,blockHeight可用于和超时时间值(timeout字段)进行比较判读消息是否超时
      • blockTimestamp:相关区块链当前最新已验证区块的区块时间戳,当跨链消息的超时判断条件(timeoutMeasure字段)为接收链时间戳时,blockTimestamp可用于和超时时间值(timeout字段)进行比较判读消息是否超时
  • 功能:该接口用于查询指定接收链已验证区块信息。SDP合约中会记录发生过跨链交易的接收链的已验证区块信息(用于SDPv3消息的超时判断),中继可能需要调用当前接口来判断SDP合约中当前记录的已验证区块信息是否是最新的信息,若不是,则需要在有消息超时时更新SDP合约中记录的已验证区块信息。

3.2.3 PtcHub合约

PtcHub合约用于综合管理不同PTC验证组件,合约中记录了当前链上已注册的不同PTC的BCDNS证书、域名空间名、tpBta信息、具体PTC验证器合约信息等。

中继可以通过PtcHub合约找到需要的PTC验证器合约已执行区块链验证任务。

状态:bcdnsCertMap

mapping(string => bytes) public bcdnsCertMap;

记录BCDNS证书集合:公钥 -> BCDNS证书

状态:ownerOidToBcdnsDomainSpaceMap

mapping(bytes32 => string) public ownerOidToBcdnsDomainSpaceMap;

记录不同OID PTC的域名空间名:域名空间所有者OID -> 域名空间名

状态:ptcTrustRootMap

mapping(bytes32 => PtcTrustRootStorage) public ptcTrustRootMap;

记录不同OID PTC的信任根:OID哈希 -> PTC信任根序列化编码

先验知识:PTCTrustRoot

PTC信任根(PTCTrustRoot):所有PTC在网络中可被使用和发现之前,都需要在BCDNS记录自己的信任根,包含PTC类型、用于验证PTC证明的信息(比如TPBTA的合法性)、PTC网络信息等。

状态:tpBtaMap

记录不同跨链通道(CrossChainLane)的tpBta:crossChainLane序列化->TpBTA序列化

先验知识:BTA & TP-BTA
  • 区块链信任锚(Blockchain Trust Anchor, BTA):区块链的状态快照,包含用于验证账本存在性证明的根信任信息,具体由区块链类型和PTC运行的异构链逻辑决定。
  • 第三方区块链信任锚(Third Party Trust Anchor, TP-BTA):PTC在完成BTA的验证之后,会为该区块链的域名签发一个TP-BTA,其中包含用于验证PTC背书的PTC的身份信息,比如公钥等。

状态:verifierMap

记录不同类型PTC的验证器集合:PTC类型->验证器合约地址

状态:ptcTypeSupported

记录当前合约支持的PTC类型,包括:

  • EXTERNAL_VERIFIER:外部验证者
  • COMMITTEE:委员会
  • RELAY_CHAIN:中继链

接口:updatePTCTrustRoot

function updatePTCTrustRoot(bytes calldata rawPtcTrustRoot) external
  • 入参
    • rawPtcTrustRoot:PTC信任根的编码序列化,每个PTC都有自己的信任根,比如委员会PTC的信任根主要包含其委员会成员的公钥集合
  • 出参:无
  • 功能:更新PTC合约中记录某个PTC的信任根,该接口配合链下插件相应接口使用。
先验知识

关于PTCTrustRoot的介绍,可查看2.3.2节链下插件接口updatePTCTrustRoot中的先验知识%3B-,%E5%85%88%E9%AA%8C%E7%9F%A5%E8%AF%86,-%E9%A6%96%E5%85%88%E4%BA%86%E8%A7%A3%E7%B1%BB%E5%9E%8B)介绍。

实现逻辑参考
  1. 解析PTC信任根
  2. 取出PTC证书,根据颁发者公钥找到颁发者的BCDNS证书
  3. 验证颁发者对PTC证书的签名
    1. 验证颁发者身份
    2. 验证颁发者对PTC证书的签名
  4. 验证PTC证书对PTCTrustRoot的签名
  5. 更新PTC合约中记录的信任根信息(公钥->信任根序列化编码)

接口:getPTCTrustRoot

function getPTCTrustRoot(bytes calldata ptcOwnerOid) external view returns (bytes memory);
  • 入参:
    • ptcOwnerOid(字节数组):指定PTC的OID,ACB要求每个PTC OID只能对应一个PTC服务。
  • 出参:
    • 字节数组:指定PTC服务的信任根序列化编码
  • 功能:查询指定OID的PTC的PTCTrustRoot,该信息记录在ptcTrustRootMap状态变量中,该接口配合链下插件相应接口使用。

接口:hasPTCTrustRoot

function hasPTCTrustRoot(bytes calldata ptcOwnerOid) external view returns (bool);
  • 入参:
    • ptcOwnerOid(字节数组):指定PTC的OID,ACB要求每个PTC OID只能对应一个PTC服务。
  • 出参:
    • bool:是否存在指定OID的PTC
  • 功能:判断当前PTC合约是否记录了指定OID的PTC,配合链下插件相应接口使用。

接口:getPTCVerifyAnchor

function getPTCVerifyAnchor(bytes calldata ptcOwnerOid, uint256 versionNum) external view returns (bytes memory);
  • 入参:
    • ptcOwnerOid(字节数组):指定PTC的OID,ACB要求每个PTC OID只能对应一个PTC服务。
    • versionNum:PTC验证锚的版本,如中继链指定高度。
  • 出参:
    • bytes:PTC指定验证锚版本的验证锚信息的序列化编码结果,如中继链指定高度的共识节点信息的序列化编码结果。
  • 功能:查询指定OID的PTC的指定版本的验证锚定信息。该信息记录在ptcTrustRootMap的PTCTrustRoot中,该接口可配合链下插件相应接口使用。
先验知识:PTCVerifyAnchor

PTC验证锚(PTCVerifyAnchor):PTCVerifyAnchor包含PTC的验证根,比如NotaryPTC的根私钥,或者中继链的某个高度的共识节点公钥集合等,在PTCTrustRoot中包含了PTCVerifyAnchor的列表,PTCVerifyAnchor可以有多个版本方便更新。

接口:hasPTCVerifyAnchor

function hasPTCVerifyAnchor(bytes calldata ptcOwnerOid, uint256 versionNum) external view returns (bool);
  • 入参:
    • ptcOwnerOid(字节数组):指定PTC的OID,ACB要求每个PTC OID只能对应一个PTC服务。
    • versionNum:PTC验证锚的版本,如中继链指定高度。
  • 出参:
    • bool:合约中是否存在指定OID的PTC的指定版本的验证锚。
  • 功能:查询判断当前合约中是否记录了指定PTC OID和version的验证锚,配合链下插件相应接口使用。

接口:addTpBta

function addTpBta(bytes calldata rawTpBta) external;
  • 入参:
    • rawTpBta:序列化编码的TpBta
  • 出参:无
  • 功能:向链上增加新的TpBTA,配合链下插件相应接口使用。
实现逻辑参考
  1. 解析TpBTA;
  2. 根据TpBTA的signerPtcCredentialSubject#applicant找到对应PTC的PTCTrustRoot
  3. 根据PTCTrustRoot中PTC的类型,找到对应的验证器并执行验证。如Committee类型,从PTCTrustRoot中拿到委员会成员的公钥,依次验证endorseProof中签名的个数大于2/3
  4. 若验证成功,将TpBTA存储到tpBtaMap状态中。

接口:getTpBta

function getTpBta(bytes calldata tpbtaLane, uint32 tpBtaVersion) external view returns (bytes memory);
  • 入参:
    • tpbtaLane:跨链通道(CrossChainLane)的序列化编码结果
    • tpBtaVersion:版本
  • 出参:tpBTA序列化编码结果
  • 功能:查询指定跨链通道、版本的TpBTA,该信息记录在tpBtaMap状态中,该接口可配合链下插件相应接口使用。

接口:getLatestTpBta

function getLatestTpBta(bytes calldata tpbtaLane) external view returns (bytes memory);
  • 入参:
    • tpbtaLane:跨链通道(CrossChainLane)的序列化编码结果
  • 出参:tpBTA序列化编码结果
  • 功能:查询指定跨链通道最新版本的TpBTA,该信息记录在tpBtaMap状态中。

接口:hasTpBta

function hasTpBta(bytes calldata tpbtaLane, uint32 tpBtaVersion) external view returns (bool);
  • 入参:
    • tpbtaLane:跨链通道(CrossChainLane)的序列化编码结果
    • tpBtaVersion:版本
  • 出参:
    • bool:合约中是否存在指定跨链通道、版本的TpBTA
  • 功能: 查询当前合约中是否记录了指定跨链通道、版本的TpBTA信息,该接口可以配合链下插件相应接口使用。

接口:verifyProof

function verifyProof(bytes calldata rawTpProof) external;
  • 入参
    • rawTpProof:TpProof序列化编码结果
  • 出参:无
  • 功能:验证TpProof
实现逻辑参考
  1. 解析 TpProof(ThirdPartyProof);
  2. 获取 TpProof 的跨链通道(CrossChainLane);
  3. 验证合约中存在上述跨链通道的 TpBTA 并获取该 TpBTA ;
  4. 根据 TpBTA 的signerPtcCredentialSubject.applicant获取相应 PTC 的 PtcTrustRoot;
  5. 根据 PtcTrustRoot 中的 PTC 类型从合约中获取相应 PTC 验证器地址;
  6. 调用PTC验证器的verifyTpProof方法对 TpBTA 和 TpProof进行验证;
  7. 验证是否通过的结果通过合约事件抛出。

接口:getSupportedPTCType

function getSupportedPTCType() external view returns (PTCTypeEnum[] memory);
  • 入参:无
  • 出参:
    • PTCTypeEnum[]:PTC类型集合,类型可能包括外部验证器EXTERNAL_VERIFIER、委员会COMMITTEE、中继链RELAY_CHAIN等,目前给出的示例合约已支持COMMITTEE类型。
  • 功能:用于查询当前PtcHub合约支持的PTC类型集合,该接口可以配合链下插件的相应接口使用。

接口:addPtcVerifier

function addPtcVerifier(identity verifierContract) external;a
  • 入参:
    • verifierContract:验证器合约地址
  • 出参:无
  • 功能:向当前合约中添加具体PTC验证器合约。
实现逻辑参考
  1. 调用验证器合约验证器对应的PTC类型,如委员会COMMITTEE
  2. 将验证器的PTC类型保存到合约记录的已支持PTC类型中,即ptcTypeSupported状态;
  3. 将验证器的合约地址和PTC类型映射关系保存到合约状态中,即verifierMap状态。

removePtcVerifier

function removePtcVerifier(PTCTypeEnum ptcType) external;
  • 入参:
    • ptcType:PTC类型
  • 出参:无
  • 功能:删除合约中指定PTC类型的验证器

3.2.4 CommitteePtcVerifier合约

CommitteePtcVerifier合约为COMMITTEE类型PTC的验证器合约,主要提供对TpBTA和TpProof的验证功能。

接口:verifyTpBta

function verifyTpBta(PTCVerifyAnchor calldata va, TpBta calldata tpBta) external returns (bool);
  • 入参
    • va:PTC验证锚,包含PTC的验证根(如验证阶段公钥集合);
    • tpBta:待验证的第三方区块链信任锚,PTC为区块链状态快照签发的锚定信息。
  • 出参:无
  • 功能:验证tpBta是否由va中相应的背书节点签发。
实现逻辑参考
  1. 验证va的PTC验证锚的版本与tpBta中记录的验证锚版本是否一致;
  2. 从va中解析出委员会PTC验证锚CommitteeVerifyAnchor,其中包含委员会PTC唯一标识ID、委员会节点公钥信息等;
struct CommitteeVerifyAnchor {
    string committeeId;
    NodeAnchorInfo[] anchors;
}
struct NodeAnchorInfo {
    string nodeId;
    NodePublicKeyEntry[] nodePublicKeys;
}
  1. 从tpBta的endorseProof中解析出签发者的委员会背书证明信息CommitteeEndorseProof,其中包括签发当前tpBTA的委员会PTC的唯一标识ID、委员会节点签名信息等;
struct CommitteeEndorseProof {
    string committeeId;
    CommitteeNodeProof[] sigs;
}
struct CommitteeNodeProof {
    string nodeId;
    string signAlgo;
    bytes signature;
}
  1. 验证tpBTA中的委员会背书信息是否和PTC验证锚中的委员会信息一致
    1. 验证第2步和第3步解析出的委员会PTC标识ID是否一致;
    2. 验证第3步解析出的委员会节点签名信息是否与第2步解析出的委员会节点公钥信息一致。

接口:verifyTpProof

function verifyTpProof(TpBta memory tpBta, ThirdPartyProof memory tpProof) external returns (bool);
  • 入参:
    • tpBta:第三方区块链信任锚,PTC为区块链状态快照签发的锚定信息。
    • tpProof:待验证的第三方证明。
  • 出参:无
  • 功能:验证tpProof是否由tpBta中相应的背书节点签发。
实现逻辑参考
  1. 从tpBta的endorseProof中解析出签发者的委员会背书根信息CommitteeEndorseRoot,其中包括签发当前tpBTA的委员会PTC的唯一标识ID、委员会背书策略、背书节点公钥信息等;
struct CommitteeEndorseRoot {
    string committeeId;
    OptionalEndorsePolicy policy;
    NodeEndorseInfo[] endorsers;
}
struct NodeEndorseInfo {
    string nodeId;
    bool required;
    NodePublicKeyEntry publicKey;
}
  1. 从tpProof的rawProof中解析出签发者的委员会证明信息CommitteeEndorseProof,其中包括签发当前tpProof的委员会PTC的唯一标识ID、委员会节点签名信息等;
struct CommitteeEndorseProof {
    string committeeId;
    CommitteeNodeProof[] sigs;
}
struct CommitteeNodeProof {
    string nodeId;
    string signAlgo;
    bytes signature;
}
  1. 验证CommitteeEndorseRoot的信息和CommitteeEndorseProof是否一致。

接口:myPtcType

function myPtcType() external returns (PTCTypeEnum);
  • 入参:无
  • 出参:PTC类型
  • 功能:用于查询当前PTC的类型,在CommitteePtcVerifier合约中恒定返回COMMITTEE

3.2.5 SenderContract合约(业务合约)

SenderContract合约为跨链消息业务合约发送方的示例合约,可用于简单的跨链消息测试,涉及实际业务时,请您自行根据业务需求进行开发。

作为跨链消息发送方的业务合约,需要调用SDP合约发送跨链消息,即应实现SDP合约配置接口和不同SDP消息版本的发送接口等。

接口:setSDPMSGAddress

function setSDPMSGAddress(address _sdp_address) public
  • 入参:SDP合约地址
  • 出参:无
  • 功能:配置SDP合约地址,方便后续调用SDP合约

接口:send & sendUnordered

function send(bytes32 receiver, string memory domain, bytes memory _msg)
function sendUnordered(bytes32 receiver, string memory domain, bytes memory _msg)
  • 入参:
    • receiver:接收业务合约地址
    • domain:接收链域名
    • _msg:业务自定义的跨链消息序列化,若仅用于测试可以直接输入0x123456
  • 出参:无
  • 功能:分别用于发起基于SDPv1消息的有序/无序跨链消息

接口:sendV2 & sendUnorderedV2

function sendV2(bytes32 receiver, string memory domain, bool atomic, bytes memory _msg)
function sendUnorderedV2(bytes32 receiver, string memory domain, bool atomic, bytes memory _msg)
  • 入参:
    • receiver:接收业务合约地址
    • domain:接收链域名
    • atomic:是否为原子性跨链消息
    • _msg:业务自定义的跨链消息序列化,若仅用于测试可以直接输入0x123456
  • 出参:无
  • 功能:分别用于发起基于SDPv2消息的有序/无序跨链消息,由于SDPv2消息支持原子性消息,存在消息回调,建议接口保存记录跨链请求消息和回调消息返回的messageId,从而可以验证当前最新请求的跨链消息和最新回调的跨链消息一致
实现逻辑参考
  1. 调用SDP合约相应的有序/无序消息发送接口
  2. 将上一步返回的消息哈希保存到合约中

接口:sendV3/sendUnorderedV3

function sendV3(bytes32 receiver, string memory domain, bool atomic, bytes memory _msg, uint8 _timeoutMeasure, uint256 _timeout)
function sendUnorderedV3(bytes32 receiver, string memory domain, bool atomic, bytes memory _msg, uint8 _timeoutMeasure, uint256 _timeout)
  • 入参:
    • receiver:接收业务合约地址
    • domain:接收链域名
    • atomic:是否为原子性跨链消息
    • _msg:业务自定义的跨链消息序列化,若仅用于测试可以直接输入0x123456
    • _timeoutMeasure:超时判断条件类型
    • _timeout:超时时间值
  • 出参:无
  • 功能:分别用于发起基于SDPv3消息的有序/无序跨链消息,由于SDPv3消息支持原子性消息,存在消息回调,建议接口保存记录跨链请求消息和回调消息返回的messageId,从而可以验证当前最新请求的跨链消息和最新回调的跨链消息一致(实现逻辑基本类似SDPv2消息发送接口)

接口:ackOnSuccess

function ackOnSuccess(bytes32 messageId, string memory receiverDomain, bytes32 receiver, uint32 sequence, uint64 nonce, bytes memory message)
  • 入参:
    • messageId:原跨链请求消息的hash标识
    • receiverDomain:原跨链请求消息的接收链域名
    • receiver:原跨链请求消息的接收业务合约地址
    • sequence:原跨链请求消息若为有序消息,即为该消息的sequence,若为无序消息,该值为-1
    • nonce:原跨链请求消息若为无序消息,即为该消息的nonce,若为有序消息,该值为-1
    • message:原跨链请求消息的序列化
  • 出参:无
  • 功能:跨链消息成功后,SDP合约回回调该业务合约的成功处理操作
实现逻辑参考

将原跨链请求消息的hash标识messageId保存到合约中,便于和原跨链请求消息返回的消息hash标识进行比对,以确保该请求消息确实收到了回调操作

接口:ackOnError

function ackOnError(bytes32 messageId, string memory receiverDomain, bytes32 receiver, uint32 sequence, uint64 nonce, bytes memory message, string memory errorMsg)
  • 入参:
    • messageId:原跨链请求消息的hash标识
    • receiverDomain:原跨链请求消息的接收链域名
    • receiver:原跨链请求消息的接收业务合约地址
    • sequence:有序消息的sequence,若为无序消息,该值为-1
    • nonce:无序消息的nonce,若为有序消息,该值为-1
    • message:原跨链请求消息的序列化
    • errorMsg:原跨链请求消息失败的相关错误信息
  • 出参:无
  • 功能:跨链消息失败后,SDP合约回回调该业务合约的失败处理操作,实现逻辑同ackOnSuccess接口

3.2.6 ReceiverContract合约(业务合约)

ReceiverContract合约为跨链消息业务合约接收方的示例合约,可用于简单的跨链消息测试。涉及实际业务时,请您自行根据业务需求进行开发。

作为跨链消息接收方的业务合约,会被SDP合约调用,即应实现跨链消息接收接口,为了方便验证消息接收成功建议实现接收消息查询接口。

接口:recvMessage/recvUnorderedMessage

function recvMessage(string memory domain_name, bytes32 author, bytes memory message)
function recvUnorderedMessage(string memory domain_name, bytes32 author, bytes memory message)
  • 入参:
    • domain_name:发送链域名
    • author:发送业务合约地址
    • message:业务合约自定义的跨链消息序列化,若仅为示例合约测试可能为0x123456,但建议不要每次输入相同的消息,便于区分每次收到不同的最新消息
  • 出参:无
  • 功能:被SDP合约调用,接收跨链消息,建议按有序/无序区分保存到合约中,

接口:getLastMsg/getLastUnorderedMsg

function getLastMsg() public view returns (bytes memory)
function getLastUnorderedMsg() public view returns (bytes memory) 
  • 入参:无
  • 出参:
    • bytes:合约中保存的接收到的有序/无序消息
  • 功能:便于用户验证接收合约确实收到了有序/无序消息

3.3 合约模板介绍

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

.
├── @openzeppelin
│   └── contracts
│       ├── access
│       │   └── Ownable.sol
│       ├── interfaces
│       │   └── IERC1271.sol
│       └── utils
│           ├── Address.sol
│           ├── Context.sol
│           ├── Strings.sol
│           ├── cryptography
│           │   ├── ECDSA.sol
│           │   └── SignatureChecker.sol
│           └── math
│               ├── Math.sol
│               └── SignedMath.sol
├── AuthMsg.sol
├── CommitteePtcVerifier.sol
├── PtcHub.sol
├── SDPMsg.sol
├── demo
│   ├── ReceiverContract.sol
│   └── SenderContract.sol
├── interfaces
│   ├── IAuthMessage.sol
│   ├── IContractUsingSDP.sol
│   ├── IContractWithAcks.sol
│   ├── IPtcHub.sol
│   ├── IPtcVerifier.sol
│   ├── ISDPMessage.sol
│   └── ISubProtocol.sol
└── lib
    ├── am
    │   └── AMLib.sol
    ├── commons
    │   └── AcbCommons.sol
    ├── ptc
    │   ├── CommitteeLib.sol
    │   └── PtcLib.sol
    ├── sdp
    │   └── SDPLib.sol
    └── utils
        ├── BytesToTypes.sol
        ├── Context.sol
        ├── Ownable.sol
        ├── SafeMath.sol
        ├── SizeOf.sol
        ├── TLVUtils.sol
        ├── TypesToBytes.sol
        └── Utils.sol

16 directories, 35 files

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

  1. 系统合约实现
  • **AuthMsg.sol**:AM合约的具体实现示例
  • **SDPMsg.sol**:SDP合约的具体实现示例
  • **PtcHub.sol**:PTC管理合约的具体实现示例
  • **CommitteePtcVerifier.sol**:委员会PTC验证合约具体实现示例
  1. 系统合约接口

interfaces目录下提供了系统合约的抽象接口,这些接口定义了系统合约的基本规则,具体系统合约应当实现这些接口。接口模板介绍详见3.2节

  1. 系统合约方法库

lib目录下提供了系统合约依赖的方法库,包括amsdpptccommonutils五个子目录,其中需要重点关注amsdpptc目录下的相关实现

  • am:给出了AM合约的核心结构(AM消息、中继消息)定义及相关序列化工具方法
  • sdp:给出了SDP合约的核心结构(SDP消息)定义及相关序列化工具方法
  • ptc:给出了PTC相关合约的核心结构(PtcTrustRoot、PTCVerifyAnchor、TpBTA、TpProof等)定义及相关序列化工具方法
  • common:给出ACB中其他核心结构(跨链证书CrossChainCertificate、跨链通道CrossChainLane等)定义及相关序列化工具方法
  • utils:给出了数据类型转换、数据编码等基本工具方法,工具合约概览参见2.3.4节
  1. 业务合约实现

Demo目录下提供了业务合约实现示例,合约中仅提供了简单的跨链消息发送和接收功能,可用于简单的消息跨链场景测试。

  • SenderContract.sol:跨链消息发送业务合约的简单实现示例,详见2.3.7节
  • ReceiverContract.sol:跨链消息接收业务合约的简单实现示例
  1. openzeppeliin库

openzeppeliin库提供的一些常用合约功能,如权限控制、合约地址转换等

3.4 工作流程介绍

3.4.1 消息传输工作流

以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为发送链定制化的跨链消息序列化结果。
发起合约调用后,合约模板内部调用流程如下图所示(图中蓝色部分表示发送链相关组件,绿色部分表示接收链相关组件):

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”。

目前提供的解决方案为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的类从特定的类加载器获取。

⚠️ **GitHub.com Fallback** ⚠️