本篇文章介绍fullnode节点收到用户发送的过来的交易之后的处理流程,一般用户通过http接口将签名好的交易发送到fullnode节点, fullnode节点会对交易进行相关的检查, 然后执行交易,如果执行的结果没有问题, 就会广播交易。当然其他节点收到这笔广播的交易之后也会做相同的操作。
//java-tron/src/main/java/org/tron/core/services/http/BroadcastServlet
protected void doPost(HttpServletRequest request, HttpServletResponse response) {
try {
String input = request.getReader().lines()
.collect(Collectors.joining(System.lineSeparator()));
Util.checkBodySize(input);
boolean visible = Util.getVisiblePost(input);
Transaction transaction = Util.packTransaction(input, visible);
GrpcAPI.Return retur = wallet.broadcastTransaction(transaction);
response.getWriter().println(JsonFormat.printToString(retur, visible));
} catch (Exception e) {
logger.debug("Exception: {}", e.getMessage());
try {
response.getWriter().println(Util.printErrorMsg(e));
} catch (IOException ioe) {
logger.debug("IOException: {}", ioe.getMessage());
}
}
}
}
用户通过http发送的广播交易的数据包会被传到这里处理,这个函数调用了wallet.broadcastTransaction进行处理。
///Users/tron/tron-ws/java-tron/src/main/java/org/tron/core/Wallet.java
//交易广播
//task1:确认可广播的节点
//task2:判断本地是否能够接收这笔交易
//task3:预执行交易,将交易放入pending列表
//task4:错误处理
public GrpcAPI.Return broadcastTransaction(Transaction signaturedTransaction) {
GrpcAPI.Return.Builder builder = GrpcAPI.Return.newBuilder();
//----------------------------------------------------------------------
//task1:确认可广播的节点
//----------------------------------------------------------------------
//将transaction类型转换成TransactionCapsule类型
TransactionCapsule trx = new TransactionCapsule(signaturedTransaction);
try {
Message message = new TransactionMessage(signaturedTransaction.toByteArray());
//如果允许的最小的链接数量不为0
if (minEffectiveConnection != 0) {
//如果当前活跃节点数为空,返回错误
if (tronNetDelegate.getActivePeer().isEmpty()) {
logger.warn("Broadcast transaction {} failed, no connection.", trx.getTransactionId());
return builder.setResult(false).setCode(response_code.NO_CONNECTION)
.setMessage(ByteString.copyFromUtf8("no connection"))
.build();
}
//找出可广播节点,满足如下条件(1.活跃, 2.不需要互相同步的节点)
int count = (int) tronNetDelegate.getActivePeer().stream()
.filter(p -> !p.isNeedSyncFromUs() && !p.isNeedSyncFromPeer())
.count();
//如果可广播节点小于允许的最小链接数量,返回错误
if (count < minEffectiveConnection) {
String info = "effective connection:" + count + " lt minEffectiveConnection:"
+ minEffectiveConnection;
logger.warn("Broadcast transaction {} failed, {}.", trx.getTransactionId(), info);
return builder.setResult(false).setCode(response_code.NOT_ENOUGH_EFFECTIVE_CONNECTION)
.setMessage(ByteString.copyFromUtf8(info))
.build();
}
}
//----------------------------------------------------------------------
//task2:判断本地是否能够接收这笔交易
//----------------------------------------------------------------------
//等待交易过多,网络阻塞,pendign列表 + repush列表总和 > 2000
if (dbManager.isTooManyPending()) {
logger.warn("Broadcast transaction {} failed, too many pending.", trx.getTransactionId());
return builder.setResult(false).setCode(response_code.SERVER_BUSY).build();
}
//如果本节点是sr节点, 并且在出块,拒绝交易,返回错误
if (dbManager.isGeneratingBlock()) {
logger
.warn("Broadcast transaction {} failed, is generating block.", trx.getTransactionId());
return builder.setResult(false).setCode(response_code.SERVER_BUSY).build();
}
//如果交易已经收到过了,拒绝交易,返回错误
if (dbManager.getTransactionIdCache().getIfPresent(trx.getTransactionId()) != null) {
logger.warn("Broadcast transaction {} failed, is already exist.", trx.getTransactionId());
return builder.setResult(false).setCode(response_code.DUP_TRANSACTION_ERROR).build();
} else {//如果没有收到过, 将交易放入idcache
dbManager.getTransactionIdCache().put(trx.getTransactionId(), true);
}
//初始化transaction的result,为执行智能合约做准备
if (dbManager.getDynamicPropertiesStore().supportVM()) {
trx.resetResult();
}
//----------------------------------------------------------------------
//task3:预执行交易,将交易放入pending列表
//----------------------------------------------------------------------
dbManager.pushTransaction(trx);
//广播交易
tronNetService.broadcast(message);
logger.info("Broadcast transaction {} successfully.", trx.getTransactionId());
return builder.setResult(true).setCode(response_code.SUCCESS).build();
//----------------------------------------------------------------------
//task4:错误处理
//----------------------------------------------------------------------
} catch (ValidateSignatureException e) {
logger.error("Broadcast transaction {} failed, {}.", trx.getTransactionId(), e.getMessage());
//签名错误处理
return builder.setResult(false).setCode(response_code.SIGERROR)
.setMessage(ByteString.copyFromUtf8("validate signature error " + e.getMessage()))
.build();
} catch (ContractValidateException e) {
//合约验证错误处理
logger.error("Broadcast transaction {} failed, {}.", trx.getTransactionId(), e.getMessage());
return builder.setResult(false).setCode(response_code.CONTRACT_VALIDATE_ERROR)
.setMessage(ByteString.copyFromUtf8("contract validate error : " + e.getMessage()))
.build();
} catch (ContractExeException e) {
//合约执行错误处理
logger.error("Broadcast transaction {} failed, {}.", trx.getTransactionId(), e.getMessage());
return builder.setResult(false).setCode(response_code.CONTRACT_EXE_ERROR)
.setMessage(ByteString.copyFromUtf8("contract execute error : " + e.getMessage()))
.build();
} catch (AccountResourceInsufficientException e) {
//资源不足出错处理
logger.error("Broadcast transaction {} failed, {}.", trx.getTransactionId(), e.getMessage());
return builder.setResult(false).setCode(response_code.BANDWITH_ERROR)
.setMessage(ByteString.copyFromUtf8("AccountResourceInsufficient error"))
.build();
} catch (DupTransactionException e) {
//重复交易发送处理
logger.error("Broadcast transaction {} failed, {}.", trx.getTransactionId(), e.getMessage());
return builder.setResult(false).setCode(response_code.DUP_TRANSACTION_ERROR)
.setMessage(ByteString.copyFromUtf8("dup transaction"))
.build();
} catch (TaposException e) {
//tapos出错处理
logger.error("Broadcast transaction {} failed, {}.", trx.getTransactionId(), e.getMessage());
return builder.setResult(false).setCode(response_code.TAPOS_ERROR)
.setMessage(ByteString.copyFromUtf8("Tapos check error"))
.build();
} catch (TooBigTransactionException e) {
//交易过大出错处理(500*1024)
logger.error("Broadcast transaction {} failed, {}.", trx.getTransactionId(), e.getMessage());
return builder.setResult(false).setCode(response_code.TOO_BIG_TRANSACTION_ERROR)
.setMessage(ByteString.copyFromUtf8("transaction size is too big"))
.build();
} catch (TransactionExpirationException e) {
//交易超时错误处理
logger.error("Broadcast transaction {} failed, {}.", trx.getTransactionId(), e.getMessage());
return builder.setResult(false).setCode(response_code.TRANSACTION_EXPIRATION_ERROR)
.setMessage(ByteString.copyFromUtf8("transaction expired"))
.build();
} catch (Exception e) {
//其他错误处理
logger.error("Broadcast transaction {} failed, {}.", trx.getTransactionId(), e.getMessage());
return builder.setResult(false).setCode(response_code.OTHER_ERROR)
.setMessage(ByteString.copyFromUtf8("other error : " + e.getMessage()))
.build();
}
}
broadcastTransaction函数主要做了如下几件事情:
fullnode节点收到交易后不会随意对本地已知的远程节点进行广播,符合如下两个条件的节点不会被广播:
也就是只有高度和本节点相同的远程节点才会被广播,通过上述条件从本地持有的远程节点中筛选出一定数量的节点, 如果满足广播条件的节点数量小于minEffectiveConnection, 就会报NOT_ENOUGH_EFFECTIVE_CONNECTION错误。minEffectiveConnection这个变量的值默认为1, 可以通过节点的启动的配置文件指定。
fullnode节点收到一笔交易,会根据自身的情况决定是否接收。如果本节点的待处理列表已经满了, 就会拒绝这笔交易,并且报SERVER_BUSY错误。如果fullnode节点同时还是一个出块节点, 并且当前正在出块, 那么也会拒绝这笔交易, 同时报SERVER_BUSY错误。同样都是SERVER_BUSY错误, 但是错误返回message是不一样的,用户可以通过字符串区分出错原因。另外如果节点之前收到过这笔交易(两个交易的hash一样),会拒绝新收到的交易,返回DUP_TRANSACTION_ERROR错误,用户可以通过稍微修改一下交易,重新发送即可。
前几步验证都通过之后, 会进入预执行的交易的流程,执行通过后会将交易放入到pending列表, 注意,这里用的预执行,因为这里执行交易是不会改变本地状态的,只是看一下能不能正确执行,对于无法正确执行的交易是不会被广播的。交易执行有错误,会直接向上抛出异常,到本函数最后进行错误处理, 如果没有错误,会将交易放入pending列表,然后广播交易。
这里统一处理前面所有的错误,包括从执行交易中抛上来的异常,然后根据异常类型,设置相应的grpc错误返回值。
pushTransaction函数主要是调用processTransaction执行交易, 执行成功后将交易加到pending列表。
//java-tron/src/main/java/org/tron/core/db/Manager.java
//将交易放入pending列表
//task1:执行交易
//task2:执行成功后将交易放入pending列表
public boolean pushTransaction(final TransactionCapsule trx)
throws ValidateSignatureException, ContractValidateException, ContractExeException,
AccountResourceInsufficientException, DupTransactionException, TaposException,
TooBigTransactionException, TransactionExpirationException,
ReceiptCheckErrException, VMIllegalException, TooBigTransactionResultException {
//锁定push列表
synchronized (pushTransactionQueue) {
pushTransactionQueue.add(trx);
}
try {
//如果签名错误抛出异常
if (!trx.validateSignature(this)) {
throw new ValidateSignatureException("trans sig validate failed");
}
synchronized (this) {
if (!session.valid()) {
session.setValue(revokingStore.buildSession());
}
//处理检查交易,检查通过后将交易放入pending列表
try (ISession tmpSession = revokingStore.buildSession()) {
//----------------------------------------------------------------------
//task1:执行交易
//----------------------------------------------------------------------
processTransaction(trx, null);
//----------------------------------------------------------------------
//task2:执行成功后将交易放入pending列表
//----------------------------------------------------------------------
pendingTransactions.add(trx);
tmpSession.merge();
}
}
} finally {
//如果有错误, 就将交易从push列表
pushTransactionQueue.remove(trx);
}
return true;
}
processTransaction函数是执行交易的入口, 在这个函数中会对交易进一步检查,检查完成后调用vm执行交易, 执行交易后结算执行产生的费用,最后将执行结果存储到数据库(TransactionInfo),另外如果节点配置的事件插件,还会触发相应的事件。
pushTransaction在tron中可能在下面3处被调用:
编号 | 流程 | 说明 |
---|---|---|
1 | fullnode节点收到一笔交易 | fullnode节点收到用户或者其他节点广播的交易后预执行,根据执行结果决定是否 继续广播 |
2 | 节点对其他节点广播的区块的校验 | 节点收到其他节点广播的区块后对区块中的交易进行执行验证 |
3 | 出块节点,打包交易 | 出块节点打包区块时,从交易的pending列表中获取交易, 并执行交易验证是否正确,验证成功,后将交易打包进区块 |
我们现在分析的流程属于第1中情况
//执行交易
//task1:执行交易前的必要检查
//task2:带宽费用和多重签名费用扣除
//task3:vm初始化并执行交易
//task4:vm执行之后的energy费用结算
//task5: 将执行结果存储到数据库并发送事件
public boolean processTransaction(final TransactionCapsule trxCap, BlockCapsule blockCap)
throws ValidateSignatureException, ContractValidateException, ContractExeException,
AccountResourceInsufficientException, TransactionExpirationException, TooBigTransactionException, TooBigTransactionResultException,
DupTransactionException, TaposException, ReceiptCheckErrException, VMIllegalException {
if (trxCap == null) {
return false;
}
//----------------------------------------------------------------------
//task1:执行交易前的必要检查
// 1. tapos检查
// 2. 检查交易大小是否超限、时间是否超期
// 3. 检查交易中的contract数量
// 4. 判断transactioncache中有没有重复交易
// 5. 签名检查
//----------------------------------------------------------------------
// 1. tapos检查
validateTapos(trxCap);
//2. 检查交易大小是否超限、时间是否超期
validateCommon(trxCap);
//3. 检查交易中的contract数量
if (trxCap.getInstance().getRawData().getContractList().size() != 1) {
throw new ContractSizeNotEqualToOneException(
"act size should be exactly 1, this is extend feature");
}
//4. 判断transactioncache中有没有重复交易
validateDup(trxCap);
//5. 签名检查
if (!trxCap.validateSignature(this)) {
throw new ValidateSignatureException("trans sig validate failed");
}
//----------------------------------------------------------------------
//task2:带宽费用和多重签名费用扣除
//----------------------------------------------------------------------
//为交易设置一个trace
TransactionTrace trace = new TransactionTrace(trxCap, this);
trxCap.setTrxTrace(trace);
//消费带宽
consumeBandwidth(trxCap, trace);
//多重签名扣费
consumeMultiSignFee(trxCap, trace);
//----------------------------------------------------------------------
//task3:evm初始化并执行交易
//----------------------------------------------------------------------
//初始化evm
VMConfig.initVmHardFork();
VMConfig.initAllowMultiSign(dynamicPropertiesStore.getAllowMultiSign());
VMConfig.initAllowTvmTransferTrc10(dynamicPropertiesStore.getAllowTvmTransferTrc10());
VMConfig.initAllowTvmConstantinople(dynamicPropertiesStore.getAllowTvmConstantinople());
trace.init(blockCap, eventPluginLoaded);
trace.checkIsConstant();
trace.exec();
//如果本次处理是因为接收到block的处理
if (Objects.nonNull(blockCap)) {
trace.setResult();
if (!blockCap.getInstance().getBlockHeader().getWitnessSignature().isEmpty()) {
//如果接受到的区块中
if (trace.checkNeedRetry()) {
String txId = Hex.toHexString(trxCap.getTransactionId().getBytes());
logger.info("Retry for tx id: {}", txId);
trace.init(blockCap, eventPluginLoaded);
trace.checkIsConstant();
trace.exec();
trace.setResult();
logger.info("Retry result for tx id: {}, tx resultCode in receipt: {}",
txId, trace.getReceipt().getResult());
}
//检查交易执行的结果是否和接收到的block中的交易的结果比较, 如果不同向上抛出异常
trace.check();
}
}
//----------------------------------------------------------------------
//task4:evm执行之后的energy费用结算
//----------------------------------------------------------------------
//支付费用
trace.finalization();
if (Objects.nonNull(blockCap) && getDynamicPropertiesStore().supportVM()) {
trxCap.setResult(trace.getRuntime());
}
//----------------------------------------------------------------------
//task5: 将执行结果存储到数据库并发送事件
//----------------------------------------------------------------------
//将transacton存储到数据库
transactionStore.put(trxCap.getTransactionId().getBytes(), trxCap);
Optional.ofNullable(transactionCache)
.ifPresent(t -> t.put(trxCap.getTransactionId().getBytes(),
new BytesCapsule(ByteArray.fromLong(trxCap.getBlockNum()))));
//将transactioninfo存储到数据库
TransactionInfoCapsule transactionInfo = TransactionInfoCapsule
.buildInstance(trxCap, blockCap, trace);
transactionHistoryStore.put(trxCap.getTransactionId().getBytes(), transactionInfo);
// if event subscribe is enabled, post contract triggers to queue
postContractTrigger(trace, false);
Contract contract = trxCap.getInstance().getRawData().getContract(0);
if (isMultSignTransaction(trxCap.getInstance())) {
ownerAddressSet.add(ByteArray.toHexString(TransactionCapsule.getOwner(contract)));
}
return true;
}
在执行交易前会进行如下检查:
tron中创建交易的时候, 会在交易中设置两个参数ref_block_bytes和 ref_block_num,这两个参数表示的是创建交易时,最新的头区块的区块ID和区块号。当然这里的区块ID和区块号并不是完整的,只是截取了一部分。
当fullnode节点收到交易后, 会在链上根据交易中的ref_block_num在链上查寻是否一个block, 如果没有的话,就会报TaposExceptiony异常,如果有的话,看一下blockid的和交易中的ref_block_bytes是否能对应上, 如果不能对上也会报TaposExceptiony异常。通过两个标志能够判断当初交易发起时基于的block,现在在链上到底存不存在。
//tapos检查
void validateTapos(TransactionCapsule transactionCapsule) throws TaposException {
//获取交易中的refBlockHash和refBlockNumBytes
byte[] refBlockHash = transactionCapsule.getInstance()
.getRawData().getRefBlockHash().toByteArray();
byte[] refBlockNumBytes = transactionCapsule.getInstance()
.getRawData().getRefBlockBytes().toByteArray();
try {
//根据交易中的refBlockNumBytes 从链上获取区块的hash
byte[] blockHash = this.recentBlockStore.get(refBlockNumBytes).getData();
//如果链上对应的区块hash和交易中的不一致,抛出
if (!Arrays.equals(blockHash, refBlockHash)) {
String str = String.format(
"Tapos failed, different block hash, %s, %s , recent block %s, solid block %s head block %s",
ByteArray.toLong(refBlockNumBytes), Hex.toHexString(refBlockHash),
Hex.toHexString(blockHash),
getSolidBlockId().getString(), getHeadBlockId().getString()).toString();
logger.info(str);
throw new TaposException(str);
}
} catch (ItemNotFoundException e) {
//链上获取不到, 抛出 TaposException异常
String str = String.
format("Tapos failed, block not found, ref block %s, %s , solid block %s head block %s",
ByteArray.toLong(refBlockNumBytes), Hex.toHexString(refBlockHash),
getSolidBlockId().getString(), getHeadBlockId().getString()).toString();
logger.info(str);
throw new TaposException(str);
}
}
tron中规定交易的大小不能超过500k,否则就会抛出TooBigTransactionException异常。创建交易时会给交易设定一个expiration参数,表示的是,这笔交易必须在这个时间内完成, 否则,就抛弃。如果用户是调用创建交易的接口自动创建, 节点会使用当前的头区块的区块时间 加上60s之后设置给expiration。当然,这个60s可以在节点启动配置文件中配置,最大不能超过24小时。下面的代码就是对交易的大小和expiration进行校验:
void validateCommon(TransactionCapsule transactionCapsule)
throws TransactionExpirationException, TooBigTransactionException {
//交易大小不能超过500k
if (transactionCapsule.getData().length > Constant.TRANSACTION_MAX_BYTE_SIZE) {
throw new TooBigTransactionException(
"too big transaction, the size is " + transactionCapsule.getData().length + " bytes");
}
long transactionExpiration = transactionCapsule.getExpiration();
long headBlockTime = getHeadBlockTimeStamp();
//交易时间不能超过规定期限
if (transactionExpiration <= headBlockTime ||
transactionExpiration > headBlockTime + Constant.MAXIMUM_TIME_UNTIL_EXPIRATION) {
throw new TransactionExpirationException(
"transaction expiration, transaction expiration time is " + transactionExpiration
+ ", but headBlockTime is " + headBlockTime);
}
}
每比交易中只能有个contract,否则就会抛出ContractSizeNotEqualToOneException异常。
fullnode节点维护一个transactionCache,当收到一笔交易,执行后没有错误,就会将这笔交易放到transactionCache。并且每次收到新的交易,执行之前都会检查transactionCache中是否能够获取到,如果存在,就说明之前已经收到过了,抛出DupTransactionException异常,不会继续往下走广播的流程了,这样可以有效的降低广播风暴。
但是这个逻辑也会给用户带来一个不方便的地方, 比如用户发送一笔交易到fullnode节点,fullnode节点检查并执行交易, 没有出现致命问题,但是合约因为能量不够导致执行失败的, 这样的交易也能够被广播。当用户从链上查到这笔交易,发现执行没有成功,于是抵押trx获得了足够的能量,再次将原有的交易发送了一遍,如果没有发生超时的情况, 一定会报DupTransactionException异常, 所以这种情况,用户应该将交易稍微修改一下,再次发送, 而不是原来的交易原封不动发送出去。
void validateDup(TransactionCapsule transactionCapsule) throws DupTransactionException {
if (containsTransaction(transactionCapsule)) {
logger.debug(ByteArray.toHexString(transactionCapsule.getTransactionId().getBytes()));
throw new DupTransactionException("dup trans");
}
}
签名检查流程分析请参考:https://blog.csdn.net/AdminZYM/article/details/94574678
这里的带宽扣费是发生在交易执行前, 对一些确定性的支出进行扣费,比如根据用户的交易字节数扣除相应带宽等等, 如果是交易使用了多重签名,要扣除一定的费用。
关于扣费细节分析,请阅读后续文章。
这里的主要工作就是初始化TVM, 然后执行交易。关于交易详细执行流程,请阅读后续章节。
执行结束后,如果本次pushTransaction是在第2和第3种调用流程中, 还需要将交易的执行结果设置到收据的result中, 因为在这种情况下,相当于交易已经上链, 需要保存交易的结果,让用户可以查询。
public void setResult() {
if (!needVM()) {
return;
}
RuntimeException exception = runtime.getResult().getException();
if (Objects.isNull(exception) && StringUtils
.isEmpty(runtime.getRuntimeError()) && !runtime.getResult().isRevert()) {
receipt.setResult(contractResult.SUCCESS);
return;
}
if (runtime.getResult().isRevert()) {
receipt.setResult(contractResult.REVERT);
return;
}
if (exception instanceof IllegalOperationException) {
receipt.setResult(contractResult.ILLEGAL_OPERATION);
return;
}
if (exception instanceof OutOfEnergyException) {
receipt.setResult(contractResult.OUT_OF_ENERGY);
return;
}
if (exception instanceof BadJumpDestinationException) {
receipt.setResult(contractResult.BAD_JUMP_DESTINATION);
return;
}
if (exception instanceof OutOfTimeException) {
receipt.setResult(contractResult.OUT_OF_TIME);
return;
}
if (exception instanceof OutOfMemoryException) {
receipt.setResult(contractResult.OUT_OF_MEMORY);
return;
}
if (exception instanceof PrecompiledContractException) {
receipt.setResult(contractResult.PRECOMPILED_CONTRACT);
return;
}
if (exception instanceof StackTooSmallException) {
receipt.setResult(contractResult.STACK_TOO_SMALL);
return;
}
if (exception instanceof StackTooLargeException) {
receipt.setResult(contractResult.STACK_TOO_LARGE);
return;
}
if (exception instanceof JVMStackOverFlowException) {
receipt.setResult(contractResult.JVM_STACK_OVER_FLOW);
return;
}
if (exception instanceof TransferException) {
receipt.setResult(contractResult.TRANSFER_FAILED);
return;
}
logger.info("uncaught exception", exception);
receipt.setResult(contractResult.UNKNOWN);
}
执行交易完成后还有一个比较特殊的处理,如果pushTransaction是在第2中流程中,并且交易需要重试,那么就会重新执行一遍交易。
为什么是第2个流程呢?
if (!blockCap.getInstance().getBlockHeader().getWitnessSignature().isEmpty())
这个判断的意思的是如果blockcap中有签名,就会进入分支执行。第3个流程是出块节点打包区块的流程, 也就是说必须执行完所有需要打包的交易后,才会进行签名, 那么执行交易的时候一定是没有签名的。所以能够进入分支执行的情况一定是第2种情况,收到别人的广播过来的区块,进行验证操作。
//交易需要重试
if (trace.checkNeedRetry()) {
.......
}
什么情况下交易需要重试呢?
public boolean checkNeedRetry() {
if (!needVM()) {
return false;
}
return trx.getContractRet() != contractResult.OUT_OF_TIME && receipt.getResult()
== contractResult.OUT_OF_TIME;
}
从上述代码上可以看到,如果交易中ContractRet的类型不为OUT_OF_TIME,但是本地执行的结果是OUT_OF_TIME, 就会被认为需要重试, 也就是别的节点执行这个交易没有超时, 但是我执行超时,那么有可能是我这个节点机器的性能有波动导致,那么再重新执行一遍试试,关于如果避免节点验证交易超时可以参考:https://blog.csdn.net/AdminZYM/article/details/93253573
不管是重试还是没有重试, 最后都要执行trace.check()来检查,本地执行的结果和接收到的交易中的结果是不是一致, 如果不一致,就会抛出ReceiptCheckErrException异常。
public void check() throws ReceiptCheckErrException {
if (!needVM()) {
return;
}
//获取交易中的合约执行结果(其他节点执行的结果)
if (Objects.isNull(trx.getContractRet())) {
throw new ReceiptCheckErrException("null resultCode");
}
//如果其他节点执行的结果和本地不一致, 抛出异常
if (!trx.getContractRet().equals(receipt.getResult())) {
logger.info(
"this tx id: {}, the resultCode in received block: {}, the resultCode in self: {}",
Hex.toHexString(trx.getTransactionId().getBytes()), trx.getContractRet(),
receipt.getResult());
throw new ReceiptCheckErrException("Different resultCode");
}
}
这里的费用结算主要是结算交易在Tvm中的能量消耗,关于energy费用结算细节分析,请阅读后续文章。
结算好费用之后, 还需要设置一下Transaction中的contractRet,这里的contractRet和之前trace.setResult设置的内容是一样的, 只不过trace.setResult设置的是交易的收据中的result,trxCap.setResult设置的是交易中的contractRet, 交易的收据是存储在TransactionInfo数据结构中, 要注意的是tron中一笔交易在数据库中会存两种形式:Transaction和TransactionInfo。
Transaction中保存的是交易的原始结构和执行结果,TransactionInfo主要保存的是执行后的收据,通过gettransactionbyid获取到的是Transaction结构,通过gettransactioninfobyid获取到的是TransactionInfo结构。
最后将Transaction和TransactionInfo分别存到数据库中,如果节点配置了event插件,还会发送交易执行成功的事件到事件队列中。
本文主要介绍了tron收到一笔交易后的执行的流程,当然只是从流程框架上大体介绍, 更深入的细节并没有详细分析, 比如详细的扣费细节和交易执行细节。为了不让大家读代码陷入细节而导致思绪混乱和迷茫, 所以这些代码的实现细节会在后面的文章分析。