Spectrum光谱链共识算法的分析
Spectrum(光谱链)是SmartMesh生态下的公链,承载去中心化Mesh网络实现万物互联dapp的底层公链。由Payment Channel的建构的SmartRaiden(光子网络)和多子链侧链并行的SmartPlasma的Layer2次级架构,保证了主链安全的同时极大的提升了交易速度。Token动态转移技术(Atmosphere)是Spectrum生态重要一环,是Token可以在不同链进行兑换的跨链协议。共识机制是一种新型的能力证明机制(Proof of Capability,PoC),能力的定义是为系统贡献资源的节点,能力证明衡量了节点对系统的贡献程度,能力越强就有更高的出块权重,并且很好的支持移动设备运行光谱轻节点,保证公链在无互联网环境也可以正常运行。
核心代码实现:
https://github.com/SmartMeshFoundation/Spectrum/blob/master/contracts/chief/src/chief_0.0.6.sol
节点能力的定义
能力被定义为节点与网络共享资源的各种因素的加权权重,具体包括如下:
1,节点是否在Meshbox上运行(为系统共享多少通信带宽,数据存储,交互能量(太阳能电池的能量))
2,Token投资(LockedDeposit(要成为志愿者至少抵押1个smt)或者Photon Payment Channel(提升系统交易频率走Payment支付通道),其实就是抵押SMT。
3,节点成功出块的次数,产生坏块的次数。
Capability = (WMB * OnMeshBox) * [ (WCBW x CommBW) + (WS x Storage) + (WSBW x StorageBW) + (WE x Energy) ] + (WLD0 * SMT LockedDeposit) + (WLD1 * Token 1 LD) + ... + (WLDN * Token N LD) + (WPD * Sum of all Photon Deposits associated with the node) + (WSS x SuccessfulSigning)
Spectrum节点分为四组,每组都有一个结构体数组,签名者节点被轮流成为签名者(出块节点),志愿者节点不出块,等待被提升为签名者节点,正常节点被提升为志愿者节点,行为不端的节点。
//signer info
struct SignerInfo {
uint score;
uint number;
}
//volunteer object
struct VolunteerInfo {
uint weight; // new volunteer weight = 5
uint number;
}
每次节点成功为一个块签名,SuccessfulSigning 会增加 1.但是,如果该节点无法生成一个好块, 则 SuccessfulSigning 会减少一个大数,例如 10 作为惩罚。
从上面可以看出, 即使节点的所有者没有财务资源(MeshBox 或存款),它仍然可以通过在有机会 成为签名者时产生正确的块来增加其能力。所以还是比较公平的,只是出块的机会比较小了。
光谱链诞生需要有一个出块节点的列表,它随区块链的诞生而产生,负责形成最初的出块节点联盟(一个被初始化的出块节点列表,和一个空的候选节点列表)。
网络上的每一个普通全节点都有资格申请成为一个出块节点。但是,由于申请时的出块节点联盟的状 态不同,导致节点被提名成出块节点的流程略有不同:
• 当出块节点总数小于极限值时:普通节点发出申请,可以被现有的出块节点提名,直接进入出 块节点列表。
• 当出块节点总数等于极限值时:普通节点发出申请,可以被现有的出块节点提名,放入候选节 点(volunteer)的列表,等待轮换参与出块。
节点联盟的更新频率
普通的节点在每个出块周期(14-22 秒)内,实际大概是12.5s,都有机会被出块节点选入到联盟列表中。如果出块节点 列表有空位,则新节点进入出块节点列表中,参与下一轮的出块。 如果出块节点列表已经没有空位, 则新节点进入候选节点列表中,等待空位。 没有进入黑名单的候选节点将会有五次出块机会,然后会 放到黑名单中休息一个epoch 。
节点联盟出块规则
1,出块节点必须保证连续正确的为网络出块,如果不能正常出块(不出块,出错块)就会被从出 块节点中剔除,会有一个候选节点来替代它。
2,一个节点不能正常出块,系统会将其判断成不合格节点,将其放入黑名单中,进入黑名单的机 器,在 24 小时之内不能重复申请成为出块节点。
具体规则分以下几种情况:
-
出块节点列表未满
每个节点3分,每错出或者漏出一个块扣1分,0分时被放入黑名单,在当前epoch不在被选拔。/* 在志愿者列表中随机选出17个节点替换当前列表, 在进入这个方法之前,已经判断过志愿者列表尺寸了,所以这里只管随机拼装即可 */ function generateSignersRule3() private { address g = _signerList[0]; // 清理旧的列表 address[] memory sl = new address[](_signerList.length); for (uint j = 0; j < sl.length; j++) { sl[j] = _signerList[j]; } for (uint i0 = sl.length; i0 > 0; i0--) { uint sIndex = i0 - 1; deleteSigner(sIndex); address signerI = sl[sIndex]; if (sIndex > 0 && signerI != uint160(0)) { if (volunteersMap[signerI].weight == 0) { pushVolunteer(signerI, 5); } pushVolunteer(signerI, volunteersMap[signerI].weight - 1); } } // 顺序选出一个创世签名人放到首位 if (genesisSigner[g] && _genesisSignerList.length > 1) { // 这个循环一定会找到一个 genesisSigner 放到 signers 中 for (uint i1 = 0; i1 < _genesisSignerList.length; i1++) { if (_genesisSignerList[i1] == g) { if (i1 == (_genesisSignerList.length - 1)) { pushSigner(_genesisSignerList[0], 3); } else { pushSigner(_genesisSignerList[i1 + 1], 3); } break; } } } else { pushSigner(_genesisSignerList[0], 3); } // 随机填满整个 signerList , 走到这个逻辑时 volunteer 一定比 signers 多,所以一定能填满 // 这个地方循环下来很可能造成 signerList.length < signerLimit 的情况, 后续需要补充 uint[] memory tiList = new uint[](signerLimit); uint ii = 0; for (uint i2 = 0; i2 < _volunteerList.length; i2++) { if (ii >= signerLimit) break; uint ti = getRandomIdx(_volunteerList[i2], _volunteerList.length - uint(1)); if (repeatTi(tiList, ti)) continue; pushSigner(_volunteerList[ti], 3); tiList[ii] = ti; ii = ii + 1; } // 如果不满需要补满 if (ii < signerLimit) { for (uint i3 = 0; i3 < _volunteerList.length; i3++) { //不存在就放进去 if (signersMap[_volunteerList[i3]].number == 0) pushSigner(_volunteerList[i3], 3); //放满了就退出循环 if (_signerList.length >= signerLimit) break; } } }
-
出块节点列表已满,候选节点列表小于出块节点列表
此时主要选拔候选节点,为每个被选拔的节点设置weight = 5,出块规则与“出块节点列表未满”时规则相同。/* rule 1 : 出块节点列表未满 每个节点3分,每错出或漏出一个块扣1分,0分时被放入黑名单 在当前 epoch 不再被选拔 rule 2 : �出块节点列表已满,候选节点列表小于出块节点列表 此时主要工作是选拔候选节点,为每个被选拔的节点设置 weight = 5, 出块规则与 “出块节点列表未满” 时的规则相同 */ function updateRule1() private { fixRule1(); // mine // 如果当前块 不是 signers[ blockNumber % signers.length ] 出的,就给这个 signer 减分 // 否则恢复成 3 分 uint signerIdx = blockNumber % _signerList.length; //初始签名人不做处理 if (!genesisSigner[_signerList[signerIdx]]) { SignerInfo storage signer = signersMap[_signerList[signerIdx]]; // 序号对应的不是我,则扣它一分 if (msg.sender != _signerList[signerIdx]) { if (signer.score > 1) { signer.score -= 1; signer.number = blockNumber; } else { // move to blacklist and cannot be selected in this epoch pushVolunteer(_signerList[signerIdx], 0); // vsn-0.0.3 // score == 0 , remove on signerList deleteSigner(signerIdx); } } else { // 恢复分数 signer.score = 3; } }
-
出块节点列表已满,候选节点列表大于出块节点列表
在这个规则生效时,签名节点的分数已经没有意义了, 此时的规则是每出一轮块就要替换掉全部的出块节点, 从候选节点列表中随机提拔一批新的出块节点到出块节点列表,将原出块节点列表移入候选节点列表,并将 weight - 1, 当 weight == 0 时则移入黑名单,当前 epoch 将不在被选拔。 假设出块节点列表最大长度 17 ,候选节点列表最大长度为 1234。每一轮出块,指的就是每 17 个块,每笔交易的确认时间也是 17 块,但是对于交易所来说应该至少经过 34 个块才能确认一笔交易。/* rule 3 : 出块节点列表已满,候选节点列表大于出块节点列表 在这个规则生效时,签名节点的分数已经没有意义了, 此时的规则是每出一轮块就要替换掉全部的出块节点, 从候选节点列表中按 weight 随机提拔一批新的出块节点到出块节点列表, 将原出块节点列表移入候选节点列表,并将 weight - 1, 当 weight == 0 时则移入黑名单,等待下一个 epoch 假设出块节点列表最大长度 17 ,候选节点列表最大长度与 epoch 相等。每一轮出块,指的就是每 17 个块,每笔交易的确认时间也是 17 块,但是对于交易所来说应该至少经过 34 个块才能确认一笔交易。 */ function updateRule3() private { uint l = _signerList.length; uint signerIdx = uint(blockNumber % l); address si = _signerList[signerIdx]; //1 : 初始签名人不做处理,不是正常签名人 0 分放回志愿者列表,否则 weight - 1 if (signerIdx > uint(0)) { // 序号对应的不是我,把序号对应的 signer 以 weight=0 扔回志愿者列表 (其实就是删除) if (msg.sender != si) { pushVolunteer(si, 0); //此处还不能直接删除,因为不能破坏列表长度,否则对后续取模逻辑有影响,用 0 占位吧 delete signersMap[si]; _signerList[signerIdx] = uint160(0); } } //2 : 如果当前块是签名人列表的最后一块,则生成下一轮的列表 if (signerIdx == uint(l - 1)) { //if (volunteersMap[msg.sender].weight == 0) {pushVolunteer(msg.sender, 5);} //pushVolunteer(msg.sender, volunteersMap[msg.sender].weight - 1); generateSignersRule3(); } }
共识机制攻击分析:
1,比如一个SMT大户抵押巨多的SMT,节点权重很高,是否就可以一直霸占出块节点,然后等待作恶呢?
答:出块节点列表未满,当出块节点很少的时候,他可以很容易进入出块列表,轮流出块。但是如果出块节点已满,有越多节点出块,做恶机会就越低,因为出块后在一个epoch周期就会被换掉,让其他节点出块。
2,节点接收到一个错误的块,或者受到连续错误的块,会不会出现分叉?
答:如果节点收到一个新挖出的区块,但是 parent 与主分支上的不一致,节点将会保存该区块,但是并不切换主分支,如果多个区块中不包含预定节点的区块,则记录全部的区块,并将主链切换到时间最早的区块上,如果多个区块中包含预定节点的区块,则记录全部的区块,并将主链切换到预定节点的区块上。