SR 基于DPOS共识,所有节点按照时间顺序轮流产块。
DPOS 共识简述 DPOS 共识即为,Delegated Proof of Stake 股份授权证明,在 POS 机制上进行改进。 相较于DPOS更为中心化,大白话主要就是两个角色:
持股人(持币用户)投票选举出委托人(Delegates)
被委托人进行出块,将奖励分给投票人
在DPOS机制下,算法要求系统做三件事:
随机指定生产者出场顺序;
必须按顺序产块,不按顺序生产的区块无效;
每过一个周期洗牌一次,打乱原有顺序;
受托人的职责主要有:
保证节点的正常运行;
收集网络里的交易;
节点验证交易,把交易打包到区块;
节点广播区块,其他节点验证后把区块添加到自己的数据库;
带领并促进区块链项目的发展;
大至概念就是这些,下面对SR产块原理进行分析。
产块机制 注意,TRON对DPOS的产块机制是做了调整的,不完全是按照这个的机制来实现。这个嘛。。。懂的都懂。
产块大流程
产块节点通过定时任务制每隔最多不超过3秒执行一次,判断是否轮到自己产块
如果是自己产块,回滚当前节点交易状态,并将交易池中的交易打包
打包成功后广播该区块给其他节点
处理刚才自己产的区块,这一步是为了走固化逻辑
产块机制需要关注的几个重点:
27节点如何论流产块
如何知道当前该我产块
产块后做什么
产块异常场景怎么处理
产块失败怎么办
成功产块,但是区块没广播出去怎么办
没有收到上一个节点产的块怎么办
27节点如何论流产块 节点有27个,且都是分布式的环境下,并没有中心化的节点进行调度。典型的拜占庭将军问题。 通过严格的时间轮进行节点控制。 啥意思?
产块逻辑入口:DposTask.init()
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 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 { long time = BLOCK_PRODUCED_INTERVAL - System.currentTimeMillis() % BLOCK_PRODUCED_INTERVAL; 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()
这段代码体现的是产块逻辑中的时间轮机制。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 private State produceBlock () { State state = stateManager.getState(); if (!State.OK.equals(state)) { return state; } synchronized (dposService.getBlockHandle().getLock()) { long slot = dposSlot.getSlot(System.currentTimeMillis() + 50 ); if (slot == 0 ) { return State.NOT_TIME_YET; } ByteString pWitness = dposSlot.getScheduledWitness(slot); Miner miner = dposService.getMiners().get(pWitness); if (miner == null ) { return State.NOT_MY_TURN; } long pTime = dposSlot.getTime(slot); 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。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 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) { if (slot == 0 ) { return System.currentTimeMillis(); } long interval = BLOCK_PRODUCED_INTERVAL; if (consensusDelegate.getLatestBlockHeaderNumber() == 0 ) { return dposService.getGenesisBlockTime() + slot * interval; } if (consensusDelegate.lastHeadBlockIsMaintenance()) { slot += consensusDelegate.getMaintenanceSkipSlots(); } long time = consensusDelegate.getLatestBlockHeaderTimestamp(); time = time - ((time - dposService.getGenesisBlockTime()) % interval); return time + interval * slot; }
拿到下个时间节点的 slot 之后,就可以判断是不是自己轮到自己产块了。 实现方式:使用当前块高对27进行取模。在启动时将27个SR加入列表。
1 2 3 4 5 6 7 8 9 10 11 12 13 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); }
产块逻辑
终于到了这个最核心的部分了。细节都在代码注释当中,有几个小点提一下:
产块是有时间限制的,不超过750ms
区块大小有限制:不会超过2MB
如果没有交易,是会产出空块的
产块后,立即处理区块,在PendingManager
中清空pending
队列
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 public synchronized BlockCapsule generateBlock (Miner miner, long blockTime, long timeout) { long postponedTrxCount = 0 ; BlockCapsule blockCapsule = new BlockCapsule(chainBaseManager.getHeadBlockNum() + 1 , chainBaseManager.getHeadBlockId(), blockTime, miner.getWitnessAddress()); blockCapsule.generatedByMyself = true ; 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 ); while (pendingTransactions.size() > 0 || rePushTransactions.size() > 0 ) { boolean fromPending = false ; TransactionCapsule trx; if (pendingTransactions.size() > 0 ) { 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(); } if (System.currentTimeMillis() > timeout) { logger.warn("Processing transaction time exceeds the producing time." ); break ; } if ((blockCapsule.getInstance().getSerializedSize() + trx.getSerializedSize() + 3 ) > ChainConstant.BLOCK_SIZE) { postponedTrxCount++; continue ; } if (isShieldedTransaction(trx.getInstance()) && shieldedTransCounts.incrementAndGet() > SHIELDED_TRANS_IN_BLOCK_COUNTS) { continue ; } 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 ); } 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) { pendingTransactions.poll(); } } catch (Exception e) { logger.error("Process trx {} failed when generating block: {}" , trx.getTransactionId(), e.getMessage()); } } accountStateCallBack.executeGenerateFinish(); 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 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 public BlockCapsule produce (Miner miner, long blockTime, long timeout) { BlockCapsule blockCapsule = manager.generateBlock(miner, blockTime, timeout); if (blockCapsule == null ) { return null ; } try { consensus.receiveBlock(blockCapsule); BlockMessage blockMessage = new BlockMessage(blockCapsule); tronNetService.broadcast(blockMessage); 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的区块链为
1 2 10000(A)--> 10001(C) \->10001'(B)
此时B的区块链为
1 2 10000(A)--> 10001(B) \->10001'(C)
此时C的区块链为
1 2 10000(A)--> 10001'(B) \->10001(C)
这么乱,怎么搞? 这个时候,就会泛及到区块链的另一个经典问题:分叉和切链。 先说解决方案:切链。 切链是走最长链原则,有分叉不要仅,继续接收分叉的区块,最后看谁的链条长,就切到到谁的链上。
处理产块后的区块 产块是产完了,产完之后怎么处理。处理在专门的Manager.pushBlock
中进行处理。 这个过程比较长,这里只说产块后需要共识的处理部份:
1 2 Manager.pushBlock() \-processBlock()
processBlock() 中处理共识的部分
1 2 3 4 5 ... if (!consensus.applyBlock(block)) { throw new BadBlockException("consensus apply block failed" ); } ...
到DposService.applyBlock()
中
1 2 3 4 5 6 7 8 9 10 @Override public boolean applyBlock (BlockCapsule blockCapsule) { statisticManager.applyBlock(blockCapsule); maintenanceManager.applyBlock(blockCapsule); updateSolidBlock(); return true ; }
更新固化块高度 的逻辑在updateSolidBlock()
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 private void updateSolidBlock () { List<Long> numbers = consensusDelegate.getActiveWitnesses().stream() .map(address -> consensusDelegate.getWitness(address.toByteArray()).getLatestBlockNum()) .sorted() .collect(Collectors.toList()); long size = consensusDelegate.getActiveWitnesses().size(); int position = (int ) (size * (1 - SOLIDIFIED_THRESHOLD * 1.0 / 100 )); long newSolidNum = numbers.get(position); long oldSolidNum = consensusDelegate.getLatestSolidifiedBlockNum(); if (newSolidNum < oldSolidNum) { logger.warn("Update solid block number failed, new: {} < old: {}" , newSolidNum, oldSolidNum); return ; } CommonParameter.getInstance() .setOldSolidityBlockNum(consensusDelegate.getLatestSolidifiedBlockNum()); consensusDelegate.saveLatestSolidifiedBlockNum(newSolidNum); logger.info("Update solid block number to {}" , newSolidNum); }
总结 TRON 的链结合了 DPOS 的机制,这种机制的优点是产块效率高,低功耗只有27个产块节点,问题也很明显,27个节点被控制,那整条链就被控制,大部分区块链的社区都希望链更加透明化公开化。 总的来说在国产链的应用上算是很广了,手续费非常便宜,值得一用。