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 小时之内不能重复申请成为出块节点。

具体规则分以下几种情况:

  1. 出块节点列表未满
    每个节点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;
           }
        }
    }
    
  2. 出块节点列表已满,候选节点列表小于出块节点列表
    此时主要选拔候选节点,为每个被选拔的节点设置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;
        }
    }
    
  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 与主分支上的不一致,节点将会保存该区块,但是并不切换主分支,如果多个区块中不包含预定节点的区块,则记录全部的区块,并将主链切换到时间最早的区块上,如果多个区块中包含预定节点的区块,则记录全部的区块,并将主链切换到预定节点的区块上。

你可能感兴趣的:(Spectrum光谱链共识算法的分析)