为了学习跨链的实现,找到了业界做的比较好的Wecross项目进行研究。目前网上关于Wecross项目源码解读的文章较少,写下这篇技术博客,既是为了自己学习,也是为了给大家一个参考。
Wecross项目的核心是:代理/桥接合约和跨链路由器。因此,阅读源码时,我们着重关心代理合约是如何部署在不同的链上,以及跨链路由器如何将跨链调用进行转发。在弄清楚上面两个任务后,我们还会继续弄清楚两阶段事务原子交易是如何实现的。
一、代理合约和桥接合约是如何部署的
研究项目源码中的interchain包,发现InterchainManager是关键的入口类,该类中包含注册任务的函数 registerTask(TaskManager taskManager),注册函数的核心逻辑如下:
// 实例化一个工厂类
InterchainTaskFactory interchainTaskFactory = new InterchainTaskFactory();
// 初始化系统资源,调用的是该类中的一个私有方法
SystemResource[] systemResources = initSystemResources();
if (Objects.nonNull(systemResources) && systemResources.length > 0) {
// 使用工厂方法加载job轮询任务(polling task)
Task[] tasks =
interchainTaskFactory.load(
systemResources,
InterchainDefault.INTER_CHAIN_JOB_DATA_KEY,
InterchainJob.class);
// 注册上面加在的所有任务
taskManager.registerTasks(tasks);
}
轮询任务的注册完成之后,将交到Quartz进行定时任务执行。此时需要看到InterchainJob类,该类继承了Quartz的job,实现了execute方法,具体的轮询任务逻辑将在这里展示。
public class InterchainJob implements Job {
// ...省略不重要代码
@Override
public void execute(JobExecutionContext context) throws JobExecutionException {
// 拿到job队列中的所有任务,只取出interchain类型的job
JobDataMap dataMap = context.getJobDetail().getJobDataMap();
SystemResource systemResource = (SystemResource) dataMap.get(InterchainDefault.INTER_CHAIN_JOB_DATA_KEY);
// 核心方法:这个方法在源码中是public,但根据实际的使用情况来看,应该是一个private才对
// 下面将好好看下这个方法的逻辑
handleInterchainRequests(systemResource);
}
}
接着看调用到的 handleInterchainRequests(SystemResource systemResource) 方法:
public void handleInterchainRequests(SystemResource systemResource) {
// 从systemResource中拿到admin账户信息
UniversalAccount adminUA;
try {
adminUA = systemResource.getAccountManager().getAdminUA();
} catch (WeCrossException e) {
logger.error("getAdminUA failed: ", e);
return;
}
// 调用的该方法主要作用是为了获取 “跨链请求” 数组。由于该方法原理较为简单,此处不再单独粘出来读了。核心是异步调用hub获取跨链请求,并组装为字符串数组。
String[] requests = getInterchainRequests(systemResource, adminUA);
if (Objects.nonNull(requests)) {
// 为了篇幅不要太长,一些异常处理的代码就不再展示了
// 采用信号量机制,信号量的大小和“跨链请求”数组大小一致,确保并发时不会循环多了
Semaphore semaphore = new Semaphore(requests.length, true);
semaphore.acquire(requests.length);
// 创建一个原子整型,防止并发导致的计数混乱
AtomicInteger count = new AtomicInteger(0);
String currentIndex = "0";
String hubPath = systemResource.getHubResource().getPath().toString();
for (String request : requests) {
// 将request字符串构造成一个interchainRequest对象
InterchainRequest interchainRequest = new InterchainRequest();
interchainRequest.build(request);
// 设置通用账户
UniversalAccount userUA =
systemResource
.getAccountManager()
.getUniversalAccountByIdentity(interchainRequest.getIdentity());
// 根据interchainRequest以及账户信息构造interchainScheduler对象
InterchainScheduler interchainScheduler = new InterchainScheduler();
interchainScheduler.setInterchainRequest(interchainRequest);
interchainScheduler.setSystemResource(systemResource);
interchainScheduler.setAdminUA(adminUA);
interchainScheduler.setUserUA(userUA);
// 执行定时任务interchainScheduler,回掉函数中释放信号量
interchainScheduler.start(
(exception) -> {
if (Objects.nonNull(exception)) {
logger.error(
"Failed to handle current inter chain request: {}, path: {}, errorMessage: {}, internalMessage: {}",
request,
hubPath,
exception.getLocalizedMessage(),
exception.getInternalMessage());
}
semaphore.release();
});
}
// 等待所有requesrs请求job完成
semaphore.acquire(requests.length);
// 更新request 索引
if (!"0".equals(currentIndex)) {
// 该函数逻辑为:将当前索引号组装为一个TransactionRequest对象,调用hub的异步事务发送,从而更新索引
updateCurrentRequestIndex(systemResource, currentIndex, adminUA);
}
}
}
通过阅读handleInterchainRequests方法,我们可以看出其核心逻辑是构造InterchainScheduler对象,并调用该对象的start方法完成request的执行,进一步的我们可以阅读InterchainScheduler源码,研究它是如何完成request的执行。
public class InterchainScheduler {
public interface GetTransactionStateCallback {
void onReturn(WeCrossException exception, String xaTransactionID, long xaTransactionSeq);
}
public interface InterchainCallback {
void onReturn(WeCrossException exception);
}
// 执行方法调用了getXATransactionState,该方法核心参数是GetTransactionStateCallback接口的实现方法
public void start(InterchainCallback callback) {
// 实现的这个接口 @GetTransactionStateCallback
transactionStateCallback = (getTransactionStateException, xaTransactionID, xaTransactionSeq) -> {
// 省略非核心逻辑...
// 构造真实UID, 并序列化为哈希256编码字符
String realUid = Sha256Utils.sha256String((systemResource.getHubResource() + interchainRequest.getUid()).getBytes(StandardCharsets.UTF_8));
// 取调用目标链顺序,若当前时间戳大于xaTransactionSeq则取当前时间戳
long timestamp = System.currentTimeMillis();
long callTargetChainSeq = timestamp > xaTransactionSeq ? timestamp : (xaTransactionSeq + 1L);
// 调用 callTargetChain 方法,该方法将会调用目标链
callTargetChain(realUid, xaTransactionID, callTargetChainSeq, (callTargetChainException, callTargetChainResult) -> {
boolean state = true;
String result = callTargetChainResult;
// ...省略处理异常的相关代码...
// 设置初始状态
boolean finalState = state;
String finalResult = result;
getXATransactionState((getCallbackTransactionStateException, callbackXATransactionID, callbackXATransactionSeq) -> {
long newTimestamp = System.currentTimeMillis();
long callCallbackSeq = newTimestamp > callbackXATransactionSeq ? newTimestamp : (callbackXATransactionSeq + 1L);
// 好吧,又来一个回调函数,callCallback方法后面我们再看,此处我们省略另外三个参数,只关注该方法的最后一个回调函数的实现。
callCallback(..., (callCallbackException,errorCode,message,callCallbackResult) -> {
// 省略异常处理的相关代码,核心是调用注册回调结果的函数registerCallbackResult,后面我们再来分析该函数的逻辑
registerCallbackResult(..., registerCallbackResultException -> {
callback.onReturn(registerCallbackResultException);
}
}
}
}
}
}
}
在start方法中,反复使用了 public void getXATransactionState 方法,我们单独来看下这个方法做了些什么
public void getXATransactionState(GetTransactionStateCallback callback, String resourcePath) {
// 省略异常处理相关代码,重点看向核心代码逻辑。构建TransactionRequest对象,并通过代理进行异步调用
Path path = Path.decode(resourcePath);
path.setResource(StubConstant.PROXY_NAME);
Path proxyPath = new Path(path);
Resource proxyResource = systemResource.getZoneManager().fetchResource(proxyPath);
TransactionRequest transactionRequest = new TransactionRequest();
transactionRequest.setArgs(new String[] {resourcePath});
transactionRequest.setMethod(InterchainDefault.GET_XA_TRANSACTION_STATE_METHOD);
transactionRequest.getOptions().put(Resource.RAW_TRANSACTION, true);
proxyResource.asyncCall(transactionRequest, adminUA, (transactionException, transactionResponse) -> {
// 省略判断异常错误码的if逻辑...
// 获取相应的调用结果
String result = transactionResponse.getResult()[0].trim();
String[] states = result.split(InterchainDefault.SPLIT_REGEX);
callback.onReturn(null, states[0], Long.parseLong(states[1]));
}
}
显然,getXATransactionState方法的逻辑很简单,即组装一个事物请求对象,然后通过proxy进行异步调用。那么,proxyResource.asyncCall方法的调用逻辑我们也需要了解,查看源码可以看到,该方法在Resource类中,该方法核心逻辑如下:
// driver即目标区块链的stub实现,在wecross项目中仅提供了相应的接口,driver的实现需要以插件的形式加载进来
// 第三个参数为true表示由proxy进行调用
driver.asyncCall(
context,
request,
true,
chooseConnection(),
(transactionException, transactionResponse) -> {
if (logger.isDebugEnabled()) {
logger.debug(
"asyncCall response: {}, exception: {}",
transactionResponse,
transactionException);
}
callback.onTransactionResponse(transactionException, transactionResponse);
});
看完上述方法,我们还需要关注registerCallbackResult方法,该方法将调用结果进行注册。注意,我们始终需要关注wecross官方给出的整体架构图,注册跨链调用请求和查询调用结果均是由桥接合约(hub)完成的。因此,该方法的核心逻辑一定是通过hubProxy调用完成的。下面看下该方法的核心逻辑:
public void registerCallbackResult(String xaTransactionID,long xaTransactionSeq,int errorCode,String message,String result,RegisterResultCallback callback) {
Resource hubResource = systemResource.getHubResource();
TransactionRequest transactionRequest = new TransactionRequest();
transactionRequest.setArgs(
new String[] {
interchainRequest.getUid(),
xaTransactionID,
String.valueOf(xaTransactionSeq),
String.valueOf(errorCode),
message,
result
});
transactionRequest.setMethod(InterchainDefault.REGISTER_CALLBACK_RESULT_METHOD);
hubResource.asyncSendTransaction(transactionRequest,adminUA,(transactionException, transactionResponse) -> {
// 省略判断异常错误代码相关的代码,当注册成功即将回调返回
callback.onReturn(null);
}
}
显然,在registerCallbackResult方法中,我们也需要关注hubResource.asyncSendTransaction方法。通过阅读源码发现,该方法和上面介绍的driver.asyncCall方法类似,也是由stub来实现的。
driver.asyncSendTransaction(
context,
request,
true,
chooseConnection(),
(transactionException, transactionResponse) -> {
if (logger.isDebugEnabled()) {
logger.debug(
"asyncSendTransaction response: {}, exception: ",
transactionResponse,
transactionException);
}
callback.onTransactionResponse(transactionException, transactionResponse);
});
弄清楚上面这几个方法,我们再看到InterchainScheduler.start方法中callTargetChain和callCallback两个方法的实现。先看callTargetChain,该方法的作用和名字一样,是调用目标区块链,核心代码仍然和上面说到的hub/proxy调用是一样的,都是通过asyncSendTransaction实现。
public void callTargetChain(String uid,String xaTransactionID,long xaTransactionSeq,CallTargetChainCallback callback) {
TransactionRequest transactionRequest = new TransactionRequest();
transactionRequest.setArgs(
new String[] {objectMapper.writeValueAsString(interchainRequest.getArgs())});
transactionRequest.setMethod(interchainRequest.getMethod());
transactionRequest.getOptions().put(StubConstant.TRANSACTION_UNIQUE_ID, uid);
// 设置了一个超时器,用于调用时的超时异常处理,异常代码省略...
Timeout callTargetChainTimeout = timer.newTimeout(...);
// 通过stub的asyncSendTransaction实现目标链调用
resource.asyncSendTransaction(
transactionRequest,
userUA,
(transactionException, transactionResponse) -> {
// 省略错误码和异常情况处理代码...在没有异常情况下,将执行callback的回调
callTargetChainTimeout.cancel();
if (Objects.isNull(transactionResponse.getResult())
|| transactionResponse.getResult().length == 0) {
callback.onReturn(null, "[]");
} else {
callback.onReturn(null, transactionResponse.getResult()[0]);
}
});
}
本来还需要看一下callCallback的源码,但是这个方法的逻辑和callTargetChain方法基本一致,核心代码也是通过asyncSendTransaction实现。
至此,基本上也理清楚了interchain包的核心逻辑,大致上可以用下图表示
代理/桥接合约的实现在源码中基本上是interchain包实现的,阅读这部分代码并弄清楚内在的逻辑并不难,因为这部分代码基本上是提供大量接口为主,而非具体的实现(具体的实现由stub对应的目标区块链自行适配,目前官方开发了BCOS和Fabric的stub实现)。后面我讲继续阅读路由的实现代码,在项目中应该是routine包下的htlc和XA。