tron-产块机制

SR 基于DPOS共识,所有节点按照时间顺序轮流产块。

DPOS 共识简述

DPOS 共识即为,Delegated Proof of Stake 股份授权证明,在 POS 机制上进行改进。
相较于DPOS更为中心化,大白话主要就是两个角色:

  1. 持股人(持币用户)投票选举出委托人(Delegates)
  2. 被委托人进行出块,将奖励分给投票人

在DPOS机制下,算法要求系统做三件事:

  1. 随机指定生产者出场顺序;
  2. 必须按顺序妯块,不按顺序生产的区块无效;
  3. 每过一个周期洗牌一次,打乱原有顺序;

受托人的职责主要有:

  1. 保证节点的正常运行;
  2. 收集网络里的交易;
  3. 节点验证交易,把交易打包到区块;
  4. 节点广播区块,其他节点验证后把区块添加到自己的数据库;
  5. 带领并促进区块链项目的发展;

大至概念就是这些,下面对SR产块原理进行分析。

产块机制

产块大流程

  1. 产块节点通过定时任务制每隔最多不超过3秒执行一次,判断是否轮到自己产块
  2. 如果是自己产块,回滚当前节点交易状态,并将交易池中的交易打包
  3. 打包成功后广播该区块给其他节点
  4. 处理刚才自己产的区块,这一步是为了走固化逻辑

产块机制需要关注的几个重点:

  1. 27节点如何论流产块
  2. 如何知道当前该我产块
  3. 产块后做什么
  4. 产块异常场景怎么处理
    • 产块失败怎么办
    • 成功产块,但是区块没广播出去怎么办
    • 没有收到上一个节点产的块怎么办

27节点如何论流产块

节点有27个,且都是分布式的环境下,并没有中心化的节点进行调度。典型的拜占庭将军问题。
通过严格的时间轮进行节点控制。
啥意思?

产块逻辑入口:DposTask.init()

public void init() {

  if (!dposService.isEnable() || StringUtils.isEmpty(dposService.getMiners())) {
    return;
  }

  Runnable runnable = () -> {
    while (isRunning) {
      try {
        if (dposService.isNeedSyncCheck()) {
          Thread.sleep(1000);
          dposService.setNeedSyncCheck(dposSlot.getTime(1) < System.currentTimeMillis());
        } else {
          // 产块间隔时间,3S
          // 取模,可以获得整数时间
          long time =
              BLOCK_PRODUCED_INTERVAL - System.currentTimeMillis() % BLOCK_PRODUCED_INTERVAL;
          // sleep n秒,注意这里使用的是当前系统时间,得出的时间并不是一个对 3000 取模的秒
          // 假设 System.currentTimeMillis() = 1647161596195
          // 3000 - 1647161596195 % 3000,那time = 1805
          // 这么做的意义是什么?
          // 意义就是为了保证是严格的每3秒执行一次,线程自动执行后对 3000 取模,就可以知道当前时间戳离 300 还差多少豪秒
          Thread.sleep(time);
          // 产块逻辑
          State state = produceBlock();
          if (!State.OK.equals(state)) {
            logger.info("Produce block failed: {}", state);
          }
        }
      } catch (InterruptedException e) {
        logger.warn("Produce block task interrupted.");
        Thread.currentThread().interrupt();
      } catch (Throwable throwable) {
        logger.error("Produce block error.", throwable);
      }
    }
  };
  produceThread = new Thread(runnable, "DPosMiner");
  produceThread.start();
  logger.info("DPoS task started.");
}

核心逻辑produceBlock()

这段代码体现的是产块逻辑中的时间轮机制。

  private State produceBlock() {

    State state = stateManager.getState();
    if (!State.OK.equals(state)) {
      return state;
    }

    synchronized (dposService.getBlockHandle().getLock()) {
      //获得一个slot,细节在下面说明
      long slot = dposSlot.getSlot(System.currentTimeMillis() + 50);
      if (slot == 0) {
        return State.NOT_TIME_YET;
      }
      //根据当前时间轮,判断是否到自己产块
      //根据slot获取自己的信息
      ByteString pWitness = dposSlot.getScheduledWitness(slot);
      
      Miner miner = dposService.getMiners().get(pWitness);
      if (miner == null) {
        return State.NOT_MY_TURN;
      }
      //获取当前的一个时间戳,用作产块时间
      long pTime = dposSlot.getTime(slot);
      // int BLOCK_PRODUCE_TIMEOUT_PERCENT = 50; // 50%
      // 3000 / 2 * 50 / 100 = 750(ms) 
      // 所以产块时间只有750豪秒
      long timeout =
          pTime + BLOCK_PRODUCED_INTERVAL / 2 * dposService.getBlockProduceTimeoutPercent() / 100;
      BlockCapsule blockCapsule = dposService.getBlockHandle().produce(miner, pTime, timeout);
      if (blockCapsule == null) {
        return State.PRODUCE_BLOCK_FAILED;
      }

      BlockHeader.raw raw = blockCapsule.getInstance().getBlockHeader().getRawData();
      logger.info("Produce block successfully, num: {}, time: {}, witness: {}, ID:{}, parentID:{}",
          raw.getNumber(),
          new DateTime(raw.getTimestamp()),
          ByteArray.toHexString(raw.getWitnessAddress().toByteArray()),
          new Sha256Hash(raw.getNumber(), Sha256Hash.of(CommonParameter
              .getInstance().isECKeyCryptoEngine(), raw.toByteArray())),
          ByteArray.toHexString(raw.getParentHash().toByteArray()));
    }

    return State.OK;
  }

时间槽机制getSlot

这个方法看似简单,实际上很有意思,这实际上是时间槽的实现。包括像EOS也是这个机制,很多DPOS的项目都是Slot机制。
Slot机制,简单的说就是把时间按单位进行分片,每3秒一个Slot,是不是很熟悉,在缓存分片中有一种方案叫哈希环也有Slot的概念。
一个是对时间进行分片,一个是对空间进行分片。Tron 是怎么实现的,看代码说明。

下面这段代码是获取一个slot,一个slot是3000ms。

  public long getSlot(long time) {
    long firstSlotTime = getTime(1);
    if (time < firstSlotTime) {
      return 0;
    }
    return (time - firstSlotTime) / BLOCK_PRODUCED_INTERVAL + 1;
  }

  public long getTime(long slot) {
    // 上面调用传入 1 不可能为0
    if (slot == 0) {
      return System.currentTimeMillis();
    }
    // BLOCK_PRODUCED_INTERVAL = 3000; 这个常量贯穿很多地方,注意这个常量
    long interval = BLOCK_PRODUCED_INTERVAL;
    // 程序刚启动 getLatestBlockHeaderNumbe=0,接收到新区块更新这个值
    if (consensusDelegate.getLatestBlockHeaderNumber() == 0) {
      return dposService.getGenesisBlockTime() + slot * interval;
    }
    if (consensusDelegate.lastHeadBlockIsMaintenance()) {
      slot += consensusDelegate.getMaintenanceSkipSlots();
    }
    // 注意这里,获取的是最新高度的区块头时间戳
    long time = consensusDelegate.getLatestBlockHeaderTimestamp();
    // GenesisBlockTime = 0 这个值在配置文件 config.conf 中配置的
    time = time - ((time - dposService.getGenesisBlockTime()) % interval);
    // 返回当前时间戳 + 3000 * 1
    return time + interval * slot;
  }

拿到下个时间节点的 slot 之后,就可以判断是不是自己轮到自己产块了。
实现方式:使用当前块高对27进行取模。在启动时将27个SR加入列表。

  public ByteString getScheduledWitness(long slot) {
    final long currentSlot = getAbSlot(consensusDelegate.getLatestBlockHeaderTimestamp()) + slot;
    if (currentSlot < 0) {
      throw new RuntimeException("current slot should be positive.");
    }
    int size = consensusDelegate.getActiveWitnesses().size();
    if (size <= 0) {
      throw new RuntimeException("active witnesses is null.");
    }
    int witnessIndex = (int) currentSlot % (size * SINGLE_REPEAT);
    witnessIndex /= SINGLE_REPEAT;
    return consensusDelegate.getActiveWitnesses().get(witnessIndex);
  }

产块逻辑

终于到了这个最核心的部分了。

  /**
   * Generate a block.
   */
  public synchronized BlockCapsule generateBlock(Miner miner, long blockTime, long timeout) {

    long postponedTrxCount = 0;

    // 构建一个空的 BlockCapsule 对象三个参数:
    // chainBaseManager.getHeadBlockNum() 前一个区块高度
    // chainBaseManager.getHeadBlockId() 前一个区块hash
    // 本节点公钥
    BlockCapsule blockCapsule = new BlockCapsule(chainBaseManager.getHeadBlockNum() + 1,
        chainBaseManager.getHeadBlockId(),
        blockTime, miner.getWitnessAddress());
    blockCapsule.generatedByMyself = true;
    // 这个注意!!reset 是将当前数据快照回退!!
    // 这是一个很精随又很复杂的操作,可以理解为将上一个区块到这一行代码之前的所有数据库的所有操作回滚!!
    session.reset();
    session.setValue(revokingStore.buildSession());

    accountStateCallBack.preExecute(blockCapsule);
    // 判断是否多签
    if (getDynamicPropertiesStore().getAllowMultiSign() == 1) {
      byte[] privateKeyAddress = miner.getPrivateKeyAddress().toByteArray();
      AccountCapsule witnessAccount = getAccountStore()
          .get(miner.getWitnessAddress().toByteArray());
      if (!Arrays.equals(privateKeyAddress, witnessAccount.getWitnessPermissionAddress())) {
        logger.warn("Witness permission is wrong");
        return null;
      }
    }

    TransactionRetCapsule transactionRetCapsule = new TransactionRetCapsule(blockCapsule);

    Set<String> accountSet = new HashSet<>();
    AtomicInteger shieldedTransCounts = new AtomicInteger(0);
    // pendingTransactions 交易池
    // rePushTransactions,上次打包 和 本次打包中pendingTransactions中没执行完的交易移入 rePushTransactions
    // 所以每次打包并不一定会将pendingTransactions 中的所有交易打包完,毕竟只有750ms的打包时间
    while (pendingTransactions.size() > 0 || rePushTransactions.size() > 0) {
      boolean fromPending = false;
      TransactionCapsule trx;
      if (pendingTransactions.size() > 0) {
        // 注意这里是 peek 不是 poll,为了防止本次执行异常交易丢失
        trx = pendingTransactions.peek();
        //交易排序,默认不开启
        if (Args.getInstance().isOpenTransactionSort()) {
          TransactionCapsule trxRepush = rePushTransactions.peek();
          if (trxRepush == null || trx.getOrder() >= trxRepush.getOrder()) {
            fromPending = true;
          } else {
            trx = rePushTransactions.poll();
          }
        } else {
          fromPending = true;
        }
      } else {
        trx = rePushTransactions.poll();
      }
      // 是否 > 750ms
      if (System.currentTimeMillis() > timeout) {
        logger.warn("Processing transaction time exceeds the producing time.");
        break;
      }

      // check the block size
      // ChainConstant.BLOCK_SIZE = 2_000_000
      if ((blockCapsule.getInstance().getSerializedSize() + trx.getSerializedSize() + 3)
          > ChainConstant.BLOCK_SIZE) {
        postponedTrxCount++;
        continue;
      }
      //shielded transaction
      // 是否允许匿名交易
      if (isShieldedTransaction(trx.getInstance())
          && shieldedTransCounts.incrementAndGet() > SHIELDED_TRANS_IN_BLOCK_COUNTS) {
        continue;
      }
      //multi sign transaction
      // 判断多签交易,如果是自己的多签交易跳过去
      // 否则就添加到 accountSet 中
      Contract contract = trx.getInstance().getRawData().getContract(0);
      byte[] owner = TransactionCapsule.getOwner(contract);
      String ownerAddress = ByteArray.toHexString(owner);
      if (accountSet.contains(ownerAddress)) {
        continue;
      } else {
        if (isMultiSignTransaction(trx.getInstance())) {
          accountSet.add(ownerAddress);
        }
      }
      if (ownerAddressSet.contains(ownerAddress)) {
        trx.setVerified(false);
      }
      // apply transaction
      // 构建一个内存快照,目的是如果执行失败了,回滚所有交易状态
      try (ISession tmpSession = revokingStore.buildSession()) {
        accountStateCallBack.preExeTrans();
        // 又执行一遍交易,实际上接收效易的时候已经执行过一次
        TransactionInfo result = processTransaction(trx, blockCapsule);
        accountStateCallBack.exeTransFinish();
        // 合并当前快照状态,这个不用记较,后面转门说一下这个快照功能,非常经典,即便不做区块链
        // 这个功能也可以用在别的场景
        tmpSession.merge();
        // 将这笔交易添加到区块中!!!这样区块中就有了交易了
        blockCapsule.addTransaction(trx);
        if (Objects.nonNull(result)) {
          transactionRetCapsule.addTransactionInfo(result);
        }
        if (fromPending) {
          // 上面已经添加到 block中了,弹出这一笔交易
          // 因为中间如果执行时间超时了,这笔交易就丢了,所以到这里才poll
          // 那如果,执行到这里,这个节点挂了,交易不还是丢失了?
          // 本节点交易是会丢失,但是其他26个节点还保留着完整的数据。
          pendingTransactions.poll();
        }
      } catch (Exception e) {
        logger.error("Process trx {} failed when generating block: {}", trx.getTransactionId(),
            e.getMessage());
      }
    }

    // 构建状态根
    accountStateCallBack.executeGenerateFinish();

    // 回滚快照,这个地方很容易让人疑惑,都打完包了,为什么还要回滚一次
    // 那之前的状态不就全回去了,比如 A 给 B 转10块钱,回通后相当于这个操作没有执行?
    // 这里回滚是为了后继步骤自己处理区块的时候数据库状态回退到原始状态
    // 个人感觉这样的处理非常费性能,因为 reset 操作遍历层级太多,也比较费时。
    session.reset();

    logger.info("Generate block {} success, pendingCount: {}, rePushCount: {}, postponedCount: {}",
        blockCapsule.getNum(),
        pendingTransactions.size(), rePushTransactions.size(), postponedTrxCount);

    // 设置 默克尔根
    blockCapsule.setMerkleRoot();
    // 对整个区块签名
    blockCapsule.sign(miner.getPrivateKey());

    BlockCapsule capsule = new BlockCapsule(blockCapsule.getInstance());
    capsule.generatedByMyself = true;
    return capsule;
  }

产块后做什么

主要就是几件事

  1. 广播区块
  2. 处理区块
  public BlockCapsule produce(Miner miner, long blockTime, long timeout) {
    // 1.产块
    BlockCapsule blockCapsule = manager.generateBlock(miner, blockTime, timeout);
    if (blockCapsule == null) {
      return null;
    }
    try {
      consensus.receiveBlock(blockCapsule);
      // 2.构建广播消息
      BlockMessage blockMessage = new BlockMessage(blockCapsule);
      // 3.广播区块
      tronNetService.broadcast(blockMessage);
      // 4.处理区块,自己生产的区块并没有在产块阶段直接入库,而是调用处理区块方法,处理并入库
      manager.pushBlock(blockCapsule);
    } catch (Exception e) {
      logger.error("Handle block {} failed.", blockCapsule.getBlockId().getString(), e);
      return null;
    }
    return blockCapsule;
  }

产块异常怎么处理

场景复现,假设只有三个节点,分别在以下假设的时间节点产块:
A 在 16000000 产块
B 在 16003000 产块
C 在 16006000 产块

A 在 16000000 时产了个块高为 10000 的块后广播给 B、C
B 在 16003000 时产了个块高为 10001 的块后广播给 A、C,但是由于网络原因这个区块没有广播出去
特殊场景来了:C 没有接到到 B 的区块,只接收到了 A 的区块高度,所以:
C 在 16006000 时产了个块高为 10001,向A、B广播

此时A的区块链为 10000(A)-->10001(C)
此时B的区块链为 10000(A)-->10001(B)
此时C的区块链为 10000(A)-->10001(C)

但是这个时候,B的网络恢复了,向 A、C 广播出块高为 10001(B) 的块,那么A、C 都会收到 B 的块,这个时候就分叉
B 也会收到 C 广播出去的块高。

此时A的区块链为

10000(A)-->10001(C)
         \->10001'(B)

此时B的区块链为

10000(A)-->10001(B)
          \->10001'(C)

此时C的区块链为

10000(A)-->10001'(B)
          \->10001(C)

这么乱,怎么搞?
这个时候,就会泛及到区块链的另一个经典问题:分叉和切链。
先说解决方案:切链。
切链是走最长链原则,有分叉不要仅,继续接收分叉的区块,最后看谁的链条长,就切到到谁的链上。

总结

TRON 的链结合了 DPOS 的机制,这种机制的优点是产块效率高,低功耗只有27个产块节点,问题也很明显,27个节点被控制,那整条链就被控制,大部分区块链的社区都希望链更加透明化公开化。

关注更多区块链更新请关注:https://liukay.com
目前主要文章更新都放自己的小站上面

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 211,376评论 6 491
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,126评论 2 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 156,966评论 0 347
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,432评论 1 283
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,519评论 6 385
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,792评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,933评论 3 406
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,701评论 0 266
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,143评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,488评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,626评论 1 340
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,292评论 4 329
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,896评论 3 313
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,742评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,977评论 1 265
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,324评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,494评论 2 348

推荐阅读更多精彩内容