WeCross Stub插件开发
本章内容介绍区块链接入WeCross的完整开发流程,用户可以根据本教程实现一个WeCross区块链Stub
插件,通过该插件接入WeCross。
注解
- Java编程语言
WeCross使用Java实现,接入的区块链需要支持Java版本的SDK,要求开发人员具备Java开发能力。
- Gradle构建工具
WeCross组件目前使用Gradle进行构建,因此假定用户能够使用Gradle,maven能达到相同的效果,用户可以自行探索。
创建Gradle项目
Gradle依赖分类
- Stub API定义
- Java SDK,区块链Java版的SDK
- 其他依赖,例如:toml解析工具
// Gradle 依赖示例
dependencies {
// Stub接口定义Jar
implementation 'com.webank:wecross-java-stub:1.0.0-rc4'
// BCOS JavaSDK
implementation 'org.fisco-bcos:web3sdk:2.4.0'
// toml文件解析
implementation
'com.moandjiezana.toml:toml4j:0.7.2'
// 其他依赖
}
新建Gradle工程,并且在build.gradle
中添加依赖。
Gradle配置参考: WeCross-BCOS-Stub build.gradle
核心组件
Stub
插件需要实现的组件接口:
- StubFactory: 创建其他组件
- Account: 账户,用于交易签名
- Connection: 与区块链交互
- Driver: 区块链数据的编解码
StubFactory
StubFactory功能
- 添加
@Stub
注解,指定插件类型 - 提供Account、Connection、Driver实例化入口
只有添加@Stub注解的插件才能被Wecross Router识别加载!
StubFactory
接口定义
public interface StubFactory {
/** 创建Driver对象 */
public Driver newDriver();
/** 解析Connection配置stub.toml,创建Connection对象 */
public Connection newConnection(String path);
/** 解析Account配置account.toml,创建Account对象 */
public Account newAccount(String name, String path);
}
BCOS Stub示例:
/** @Stub注解,插件类型: BCOS2.0 */
@Stub("BCOS2.0")
public class BCOSStubFactory implements StubFactory {
@Override
public Driver newDriver() {
// 创建返回BCOSDriver对象
}
@Override
public Connection newConnection(String path) {
// 解析stub.toml配置, 创建返回BCOSConnection对象
}
@Override
public Account newAccount(String name, String path) {
// 解析account.toml账户配置,创建返回BCOSAccount对象
}
}
Account
Account
包含账户私钥,用于交易签名。
接口定义
public interface Account {
/** 账户名称 */
String getName();
/** 账户类型 */
String getType();
/** 账户公钥 */
String getIdentity();
}
Account由StubFactory对象newAccount
接口创建,用户需要解析配置生成Account对象
Account newAccount(String name, String path)
- name: 账户名称
- path: 配置文件
account.toml
所在目录路径
账户配置文件位于conf/accounts/
目录,可以配置多个账户,每个账户置于单独的子目录。
# 目录结构, conf/accounts/账户名称/
conf/accounts
└── bcos # 账户名称: bcos
├── 0x4c9e341a015ce8200060a028ce45dfea8bf33e15.pem # BCOS私钥文件
└── account.toml # account.toml配置文件
# account.toml内容
[account]
type = "BCOS2.0" # 必须项,账户类型,与插件@Stub注解定义的类型保持一致
accountFile = '0x4c9e341a015ce8200060a028ce45dfea8bf33e15.pem' # 配置的私钥文件名称
account.toml
解析流程可以参考BCOS Stub account.toml解析
BCOS Stub示例
public class BCOSAccount implements Account {
/** 名称 */
private final String name;
/** 类型 */
private final String type;
/** 公钥 */
private final String publicKey;
/** BCOS私钥对象,交易签名,加载配置的私钥文件生成 */
private final Credentials credentials;
public Credentials getCredentials() {
return credentials;
}
/** 其他接口 */
}
Connection
Connection用于
- 为Driver提供统一发送接口,与区块链交互
- 获取配置的资源列表,资源在Connection初始化时从配置文件加载
接口定义
public interface Connection {
/** 发送接口请求给区块链 */
Response send(Request request);
/** 获取资源列表,资源列表在stub.toml文件配置 */
List<ResourceInfo> getResources();
}
接口列表
- getResources
获取资源列表,资源在Connection初始化时加载,表示Stub可以访问的区块链资源。
- send
提供统一的发送请求接口给Driver使用,Driver设置请求类型并将参数序列化生成Request,调用send接口,返回Response对象,Response包含返回值、错误信息以及返回内容。
/** Request请求对象,包含请求类型与请求内容 */
public class Request {
/** 请求类型,用户自定义,区分不同的区块链请求 */
private int type;
/** 请求内容,序列化的请求参数,用户自定义序列化方式 */
private byte[] data;
/** 请求资源 */
private ResourceInfo resourceInfo;
}
/** Response返回值对象,包含错误码、错误描述以及返回内容 */
public class Response {
/** 返回状态 */
private int errorCode;
/** 返回错误描述 */
private String errorMessage;
/** 返回内容,序列化的返回参数,可以反序列化为返回对象 */
private byte[] data;
}
/** 资源对象 */
public class ResourceInfo {
/** 资源名称 */
private String name;
/** 资源类型,用户自定义 */
private String stubType;
/** 额外属性,用户自定义 */
private Map<Object, Object> properties = new HashMap<Object, Object>();
}
Connection由StubFactory对象newConnection
接口创建,解析配置生成Connection对象
Connnection newConnection(String path)
- path: 配置文件
stub.toml
所在目录路径
- path: 配置文件
插件配置stub.toml
- 通用配置:插件名称、类型
- SDK配置:初始化JavaSDK,与区块链交互
- 资源列表:区块链可以访问的资源列表
插件配置默认位于chains/
目录,可以配置多个stub,每个stub位于单独的子目录。
# 目录结构, conf/chains/stub名称/
conf/chains/
└── bcos # stub名称: bcos
└── stub.toml # stub.toml配置文件
# 其他文件列表,比如:证书文件
stub.toml
解析流程可以参考BCOS Stub stub.toml解析
BCOS示例
/** Request type定义,用户自定义 */
public class BCOSRequestType {
// call
public static final int CALL = 1000;
// sendTransaction
public static final int SEND_TRANSACTION = 1001;
// 获取区块高度
public static final int GET_BLOCK_NUMBER = 1002;
// 获取BlockHeader
public static final int GET_BLOCK_HEADER = 1003;
// 获取交易Merkle证明
public static final int GET_TRANSACTION_PROOF = 1004;
}
public class BCOSConnection implements Connection {
/** BCOS SDK对象,调用区块链接口 */
private final Web3jWrapper web3jWrapper;
/** 为Driver提供统一发送接口,根据不同请求类型分别与区块链完成交互 */
@Override
public Response send(Request request) {
switch (request.getType()) {
/** call请求 */
case BCOSRequestType.CALL:
/** handle call request */
/** sendTransaction请求 */
case BCOSRequestType.SEND_TRANSACTION:
/** handle sendTransaction request */
/** 获取区块头 */
case BCOSRequestType.GET_BLOCK_HEADER:
/** handle getBlockeHeader request */
/** 获取块高 */
case BCOSRequestType.GET_BLOCK_NUMBER:
/** handle getBlockNumber request */
/** 获取交易Merkle证明 */
case BCOSRequestType.GET_TRANSACTION_PROOF:
/** handle getTransactionProof request */
default:
/** unrecognized request type */
}
}
/** 获取资源列表 */
@Override
public List<ResourceInfo> getResources() {
return resourceInfoList;
}
}
BCOS stub.toml示例
[common] # 通用配置
name = 'bcos' # 名称,必须项
type = 'BCOS2.0' # 必须项,插件类型,与插件@Stub注解定义的类型保持一致
[chain] # BCOS链属性配置
groupId = 1 # default 1
chainId = 1 # default 1
[channelService] # BCOS JavaSDK配置
caCert = 'ca.crt'
sslCert = 'sdk.crt'
sslKey = 'sdk.key'
timeout = 300000 # ms, default 60000ms
connectionsStr = ['127.0.0.1:20200', '127.0.0.1:20201', '127.0.0.1:20202']
[[resources]] # 资源配置列表
name = 'HelloWeCross' # 资源名称
type = 'BCOS_CONTRACT' # 资源类型,BCOS合约
contractAddress = '0x8827cca7f0f38b861b62dae6d711efe92a1e3602' # 合约地址
Driver
Driver是Stub与WeCross Router交互的入口,用途包括:
- 发送交易
- 编解码交易
- 编解码区块
- 验证交易
- 其他功能
接口定义
public interface Driver {
/** call或者sendTransaction请求,返回为true,其他类型返回false */
public boolean isTransaction(Request request);
/** 解码BlockHeader数据 */
public BlockHeader decodeBlockHeader(byte[] data);
/** 获取区块链当前块高 */
public long getBlockNumber(Connection connection);
/** 获取Block Header */
public byte[] getBlockHeader(long blockNumber, Connection connection);
/** 解析交易请求,请求可能为call或者sendTransaction */
public TransactionContext<TransactionRequest> decodeTransactionRequest(byte[] data);
/** 调用合约,查询请求 */
public TransactionResponse call(
TransactionContext<TransactionRequest> request, Connection connection);
/** 调用合约,交易请求 */
public TransactionResponse sendTransaction(
TransactionContext<TransactionRequest> request, Connection connection);
/** 获取交易,并且对交易进行验证 */
public VerifiedTransaction getVerifiedTransaction(
String transactionHash,
long blockNumber,
BlockHeaderManager blockHeaderManager,
Connection connection);
}
接口列表
isTransaction 是否为请求交易
- 参数列表:
- Request request: 请求对象
- 返回值:
- request为call或者sendTransaction请求返回true,否则返回false
- 参数列表:
getBlockNumber 获取当前区块高度
- 参数列表:
- Connection connection: Connection对象,发送请求
- 返回值
- 区块高度,负值表示获取区块高度失败
- 参数列表:
getBlockHeader 获取区块头数据,区块头为序列化的二进制数据
- 参数列表:
- long blockNumber: 块高
- Connection connection: Connection对象,发送请求
- 返回值
- 序列化的区块头数据,返回null表示获取区块头数据失败,可以使用decodeBlockHeader获取区块头对象
- 参数列表:
decodeBlockHeader 解析区块头数据,返回区块头对象
参数列表:
- byte[] data: 序列化的区块头数据
返回值
区块头BlockHeader对象,返回null表示解析区块头数据失败
区块头对象
public class BlockHeader {
/** 区块高度 */
private long number;
/** 上一个区块hash */
private String prevHash;
/** 区块hash */
private String hash;
/** 状态根,验证状态 */
private String stateRoot;
/** 交易根,验证区块交易 */
private String transactionRoot;
/** 交易回执根,验证区块交易回执 */
private String receiptRoot;
}
call、sendTransaction
call
与sendTransaction
接口类似,前者用于查询状态,后者发送交易,修改区块链状态参数列表
- TransactionContext request: 请求上下文,获取构造交易需要的数据,构造交易
- Connection connection: Connection对象,发送请求
返回值
- TransactionResponse: 返回对象
注意:
sendTransaction
接口要对返回交易进行验证,各个区块链的验证方式有所不同,BCOS采用Merkle证明的方式对交易及交易回执进行验证// 请求对象
public class TransactionRequest {
/** 接口 */
private String method;
/** 参数 */
private String[] args;
}
// 返回对象
public class TransactionResponse {
// 返回状态,0表示成功,其他表示错误码
private Integer errorCode;
// 错误信息
private String errorMessage;
// 交易hash,sendTransaction时有效
private String hash;
// 区块高度,交易所在的区块的块高,sendTransaction时有效
private long blockNumber;
// 返回结果
private String[] result;
}
// 交易请求上下文参数,获取构造交易需要的参数
public class TransactionContext<TransactionRequest> {
// 交易请求
private TransactionRequest data;
// 账户,用于交易签名
private Account account;
// 请求资源,用于获取资源相关信息
private ResourceInfo resourceInfo;
// 区块头管理器,获取区块头信息
private BlockHeaderManager blockHeaderManager;
}
// 区块头管理器
public interface BlockHeaderManager {
// 获取当前块高
public long getBlockNumber();
// 获取区块头,阻塞操作
public byte[] getBlockHeader(long blockNumber);
}
getVerifiedTransaction 根据哈希和块高查询交易并校验交易,返回交易的请求与返回对象,校验交易方式与sendTransaction接口校验交易方式保持一致。
参数列表
- String transactionHash: 交易hash
- long blockNumber: 交易所在区块高度
- BlockHeaderManager blockHeaderManager: 区块头管理器,获取区块头
- Connection connection: 发送请求
返回值
VerifiedTransaction对象
public class VerifiedTransaction {
/** 交易所在块高 */
private long blockNumber;
/** 交易hash */
private String transactionHash;
/** 交易调用的合约地址 */
private String realAddress;
/** 交易请求参数 */
private TransactionRequest transactionRequest;
/** 交易返回 */
private TransactionResponse transactionResponse;
}
BCOS示例
这里给个完整的BCOS Stub发送交易的处理流程,说明Driver与Connection的协作,以及在BCOS中如何进行交易验证。
- BCOSDriver
@Override
public TransactionResponse sendTransaction(
TransactionContext<TransactionRequest> request, Connection connection) {
TransactionResponse response = new TransactionResponse();
try {
ResourceInfo resourceInfo = request.getResourceInfo();
/** 合约的额外属性,BCOS构造交易需要这些参数,参考BCOS Stub.toml配置 */
Map<Object, Object> properties = resourceInfo.getProperties();
/** 获取合约地址 */
String contractAddress = (String) properties.get(resourceInfo.getName());
/** 获取群组Id */
Integer groupId = (Integer) properties.get(BCOSConstant.BCOS_RESOURCEINFO_GROUP_ID);
/** 获取链Id */
Integer chainId = (Integer) properties.get(BCOSConstant.BCOS_RESOURCEINFO_CHAIN_ID);
/** 获取块高 */
long blockNumber = request.getBlockHeaderManager().getBlockNumber();
BCOSAccount bcosAccount = (BCOSAccount) request.getAccount();
/** 获取私钥 参考account.toml配置 */
Credentials credentials = bcosAccount.getCredentials();
/** 交易签名,使用credentials对构造的交易进行签名 */
String signTx =
SignTransaction.sign(
credentials,
contractAddress,
BigInteger.valueOf(groupId),
BigInteger.valueOf(chainId),
BigInteger.valueOf(blockNumber),
FunctionEncoder.encode(function));
// 构造Request参数
TransactionParams transaction = new TransactionParams(request.getData(), signTx);
Request req = new Request();
/** Request类型 SEND_TRANSACTION */
req.setType(BCOSRequestType.SEND_TRANSACTION);
/** 参数JSON序列化 */
req.setData(objectMapper.writeValueAsBytes(transaction));
/** Connection send发送请求 */
Response resp = connection.send(req);
if (resp.getErrorCode() != BCOSStatusCode.Success) {
/** Connection返回异常 */
throw new BCOSStubException(resp.getErrorCode(), resp.getErrorMessage());
}
/** 获取返回的交易回执,回执被序列化为byte[] */
TransactionReceipt receipt =
objectMapper.readValue(resp.getData(), TransactionReceipt.class);
// Merkle证明,校验交易hash及交易回执,失败抛出异常
verifyTransactionProof(
receipt.getBlockNumber().longValue(),
receipt.getTransactionHash(),
request.getBlockHeaderManager(),
receipt);
/** 其他逻辑,构造返回 */
} catch (Exception e) {
/** 异常场景 */
response.setErrorCode(BCOSStatusCode.UnclassifiedError);
response.setErrorMessage(" errorMessage: " + e.getMessage());
}
return response;
}
- BCOSConnection
public class BCOSConnection implements Connection {
/** BCOS JavaSDK 实例句柄 */
private final Web3jWrapper web3jWrapper;
@Override
public Response send(Request request) {
switch (request.getType()) {
case BCOSRequestType.SEND_TRANSACTION:
/** type: SEND_TRANSACTION */
return handleTransactionRequest(request);
/** 其他case场景 */
}
}
/** 发送交易请求处理 */
public Response handleTransactionRequest(Request request) {
Response response = new Response();
try {
/** 参数JSON序列化,反序列化得到请求参数*/
TransactionParams transaction =
objectMapper.readValue(request.getData(), TransactionParams.class);
/** 签名交易 */
String signTx = transaction.getData();
/** 调用BCOS RPC发送交易接口,获取回执以及Merkle证明 */
TransactionReceipt receipt = web3jWrapper.sendTransactionAndGetProof(signTx);
/** 交易回执不存在 */
if (Objects.isNull(receipt)
|| Objects.isNull(receipt.getTransactionHash())
|| "".equals(receipt.getTransactionHash())) {
throw new BCOSStubException(
BCOSStatusCode.TransactionReceiptNotExist,
BCOSStatusCode.getStatusMessage(BCOSStatusCode.TransactionReceiptNotExist));
}
/** 交易执行失败 */
if (!receipt.isStatusOK()) {
throw new BCOSStubException(
BCOSStatusCode.SendTransactionNotSuccessStatus,
StatusCode.getStatusMessage(receipt.getStatus()));
}
/** 交易正确执行 */
response.setErrorCode(BCOSStatusCode.Success);
response.setErrorMessage(BCOSStatusCode.getStatusMessage(BCOSStatusCode.Success));
/** 返回交易回执,回执JSON方式序列化 */
response.setData(objectMapper.writeValueAsBytes(receipt));
} catch (Exception e) {
/** 异常情况 */
response.setErrorCode(BCOSStatusCode.HandleSendTransactionFailed);
response.setErrorMessage(" errorMessage: " + e.getMessage());
}
return response;
}
}
生成Jar
Stub插件需要打包生成shadow jar
才可以被WeCross Router
加载使用,在Gradle中引入shadow
插件。
shadow
插件使用: Gradle Shadow Plugin
- 引入shadow插件
plugins {
// 其他插件列表
id 'com.github.johnrengelman.shadow' version '5.2.0'
}
- 添加打包task
jar.enabled = false
project.tasks.assemble.dependsOn project.tasks.shadowJar
shadowJar {
destinationDir file('dist/apps')
archiveName project.name + '.jar'
// 其他打包逻辑
}
- 执行build操作
bash gradlew build
dist/apps
目录生成jar
文件