波场java-tron3.6 fullnode节点广播交易前的流程分析

本篇文章介绍fullnode节点收到用户发送的过来的交易之后的处理流程,一般用户通过http接口将签名好的交易发送到fullnode节点, fullnode节点会对交易进行相关的检查, 然后执行交易,如果执行的结果没有问题, 就会广播交易。当然其他节点收到这笔广播的交易之后也会做相同的操作。

1 fullnode http 处理函数

  //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进行处理。

2 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函数主要做了如下几件事情:

  • 确认可广播的节点
  • 判断本地是否能够接收这笔交易
  • 预执行交易,将交易放入pending列表
  • 错误处理

2.1 确认可广播节点

fullnode节点收到交易后不会随意对本地已知的远程节点进行广播,符合如下两个条件的节点不会被广播:

  • 远程节点高度不足,需要向自身节点进行同步
  • 自身节点高度不足,需要向远程节点进行同步

也就是只有高度和本节点相同的远程节点才会被广播,通过上述条件从本地持有的远程节点中筛选出一定数量的节点, 如果满足广播条件的节点数量小于minEffectiveConnection, 就会报NOT_ENOUGH_EFFECTIVE_CONNECTION错误。minEffectiveConnection这个变量的值默认为1, 可以通过节点的启动的配置文件指定。

2.2 判断本地是否能够接收这笔交易

fullnode节点收到一笔交易,会根据自身的情况决定是否接收。如果本节点的待处理列表已经满了, 就会拒绝这笔交易,并且报SERVER_BUSY错误。如果fullnode节点同时还是一个出块节点, 并且当前正在出块, 那么也会拒绝这笔交易, 同时报SERVER_BUSY错误。同样都是SERVER_BUSY错误, 但是错误返回message是不一样的,用户可以通过字符串区分出错原因。另外如果节点之前收到过这笔交易(两个交易的hash一样),会拒绝新收到的交易,返回DUP_TRANSACTION_ERROR错误,用户可以通过稍微修改一下交易,重新发送即可。

2.3 预执行交易,将交易放入pending列表

前几步验证都通过之后, 会进入预执行的交易的流程,执行通过后会将交易放入到pending列表, 注意,这里用的预执行,因为这里执行交易是不会改变本地状态的,只是看一下能不能正确执行,对于无法正确执行的交易是不会被广播的。交易执行有错误,会直接向上抛出异常,到本函数最后进行错误处理, 如果没有错误,会将交易放入pending列表,然后广播交易。

2.3 错误处理

这里统一处理前面所有的错误,包括从执行交易中抛上来的异常,然后根据异常类型,设置相应的grpc错误返回值。

3 pushTransaction 函数

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;
  }

4 processTransaction函数

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;
  }

3.1 执行交易前的必要检查

在执行交易前会进行如下检查:

  1. tapos检查
  2. 检查交易大小是否超限、时间是否超期
  3. 检查交易中的contract数量
  4. 判断transactioncache中有没有重复交易
  5. 签名检查

4.1.1 tapos检查

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);
    }
  }

4.1.2 检查交易大小非否超限、时间是否超期

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);
    }
  }

4.1.3 检查交易中的contract数量

每比交易中只能有个contract,否则就会抛出ContractSizeNotEqualToOneException异常。

4.1.4 判断transactioncache中有没有重复交易

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");
    }
  }

4.1.5 签名检查

签名检查流程分析请参考:https://blog.csdn.net/AdminZYM/article/details/94574678

4.2 带宽费用和多重签名费用扣除

这里的带宽扣费是发生在交易执行前, 对一些确定性的支出进行扣费,比如根据用户的交易字节数扣除相应带宽等等, 如果是交易使用了多重签名,要扣除一定的费用。

关于扣费细节分析,请阅读后续文章。

4.3 vm初始化并执行交易

这里的主要工作就是初始化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");
    }
  }

3.4 vm执行之后的energy费用结算

这里的费用结算主要是结算交易在Tvm中的能量消耗,关于energy费用结算细节分析,请阅读后续文章。

结算好费用之后, 还需要设置一下Transaction中的contractRet,这里的contractRet和之前trace.setResult设置的内容是一样的, 只不过trace.setResult设置的是交易的收据中的result,trxCap.setResult设置的是交易中的contractRet, 交易的收据是存储在TransactionInfo数据结构中, 要注意的是tron中一笔交易在数据库中会存两种形式:Transaction和TransactionInfo。

Transaction中保存的是交易的原始结构和执行结果,TransactionInfo主要保存的是执行后的收据,通过gettransactionbyid获取到的是Transaction结构,通过gettransactioninfobyid获取到的是TransactionInfo结构。

4.5 将执行结果存储到数据库并发送事件

最后将Transaction和TransactionInfo分别存到数据库中,如果节点配置了event插件,还会发送交易执行成功的事件到事件队列中。

5总结

本文主要介绍了tron收到一笔交易后的执行的流程,当然只是从流程框架上大体介绍, 更深入的细节并没有详细分析, 比如详细的扣费细节和交易执行细节。为了不让大家读代码陷入细节而导致思绪混乱和迷茫, 所以这些代码的实现细节会在后面的文章分析。

你可能感兴趣的:(tron,区块链,tron)