【原创】区块链开发:共识机制PBFT #C09


#1

拜占庭将军问题是指系统中除了网络延迟、系统宕机等问题外还存在恶意节点,会进行“精神分裂式”投票。

BFT(Byzantine Fault Tolerance)系统是指能够容忍拜占庭将军问题的系统,而PBFT(Practical Byzantine Fault Tolerance)则是其具体实现算法。其主旨是:当存在f个失效节点时必须保证存在至少3f+1个副本数量,这样才能保证在异步系统中提供安全性和活性

那为什么是3f+1个副本呢?假设总的副本数是N,已知最多f个错误节点,所有节点都可能因为系统、网络等原因而无法回复消息,这类节点称为故障节点;而错误节点可能故意不回复消息,或者回复错误消息,或者像不同节点回复不同的消息。故障节点和错误节点统称为有问题节点。

此时会有两个极端:

  1. f个错误节点也是故障节点,那么正确节点只需f+1个即可,系统至少需要2f+1个节点副本。
  2. 错误节点和故障节点都是不同的节点,因为知道f个节点的回复是不靠谱的,那么至少要收到N-f个回应后才能判断出结果。而由于到达顺序的问题,f个错误节点可能比正确节点先返回消息,所以收到的N-f个消息中能保证N-2f个消息一定是正确节点发来的。要求N-2f>f,所以N>3f。

尽管可以存在多于3f+1个副本,但是额外的副本除了降低性能之外不能提高可靠性。

三阶段流程

pbft算法的基本流程主要有以下四步:

  1. 客户端发送请求给主节点。
  2. 主节点通过广播将请求发送给其他副本(三阶段协议)。
  3. 节点处理完三阶段流程后,返回消息给客户端。
  4. 客户端收到来自f+1个不同节点的相同消息后,代表共识已经正确完成。

为什么收到f+1个不同节点的相同消息就代表共识已经正确完成?
从之前的推导可知,无论是最好的情况还是最坏的情况,只要客户端收到f+1个节点的相同回复消息,那么就代表有足够多的正确节点已全部达成共识并处理完毕了。

三阶段协议分为预准备(pre-prepare)、准备(prepare)和确认(commit)这三个阶段。

当主节点收到请求,向其他节点广播pre-prepare消息时,就开始了三阶段流程。
pre-prepare消息格式为<<PRE-PREPARE,v,n,d>,m>,这里v是视图编号,通过对v取模可以得知当前的主节点;m是请求消息;n是主节点为请求消息分配的序列号;d是请求消息的摘要。

  • 备份节点 i 收到pre-prepare消息后会进行验证,验证通过后备份节点就进入prepare阶段,广播prepare消息<PREPARE,v,n,d,i>

  • 当备份节点 i 收到2f+1个一致的prepare消息时,节点进入commit阶段,广播commit消息<COMMIT,v,n,D(m),i>

  • 当备份节点 i 收到2f+1个一致的commit消息时,表示大部分节点对这个客户端发来的请求达成了共识,节点执行请求操作,然后回复客户端。

预准备和准备两个阶段用来确保同一个视图中请求发送的时序性(即使对请求进行排序的主节点失效了),准备和确认两个阶段用来确保在不同的视图之间的确认请求是严格排序的。

消息验证

当备份节点收到消息后,如何验证该消息是正确的呢?一般从以下几个方面入手:

  1. 消息摘要d或D(m)是否正确。
  2. 当前视图编号是否为v。
  3. 该备份节点从未在视图v中接受过序号为n但是摘要d不同的消息m。
  4. 消息的序号n必须在高低水位之间。

对于第四条,什么是高低水位呢?解释之前先要引入checkpoint、stable checkpoint两个概念。

  • checkpoint 是指节点当前处理的最新请求序号。
  • stable checkpoint 是指大部分节点(2f+1)已经共识完成的最大请求序号。

stable checkpoint主要用于减少内存的占用,如果当前系统的stable checkpoint为h,那代表h之前的请求都已达成共识,节点中缓存的请求都可以清除了。

h就是系统的低水位,而高水位H=h+L,L是可以设定的。

例如节点A的checkpoint为483,B的checkpoint为503,而stable checkpoint为480,即系统低水位为480,高水位为580(L为100)。当B接收到新请求时,会选择等待A节点,当A也处理到和B差不多的请求时,比如A也处理到502,这时会有一个机制更新所有节点的stabel checkpoint,比如500,此时B又可以处理新的请求,而系统的高低水位就变为600和500。

视图更改

主节点可能会是错误节点,它可能会给不同的请求编上相同的序号,或者不去分配序号,或者让相邻的序号不连续。备份节点会检查这些序号的合法性,并通过timeout机制检测到主节点是否已经宕掉。当出现这些异常情况时,这些备份节点就会触发视图更换view change协议来选举出新的主节点。

当备份节点发出view change消息时,当前存活的节点编号最小的节点将成为新的主节点。当新的主节点收到2f个其它节点的view-change消息,则证明有足够多人的节点认为主节点有问题,于是就会向其它节点广播New-view消息。
对于新的主节点,发送new-view消息后会继续执行上个视图未处理完的请求。其它节点验证new-view消息通过后,就会处理主节点发来的pre-prepare消息,这时执行的过程就是前面描述的pbft过程。到这时,正式进入 v+1(视图编号加1)的时代了。

代码实现

var slot = require("./slot");
var Msg = require("../message");
var MessageType = require("../message").type;

var PBFT_N = slot.delegates;
var PBFT_F = Math.floor((PBFT_N - 1) / 3);

var State = {
    Idle: 0,
    Prepare: 1,
    Commit: 2,
};


class Pbft {
    constructor(blockchain) {
        this.block_chain_ = blockchain;

        this.pending_block_ = {};
        this.prepare_info_ = null;
        this.commit_infos_ = {};
        this.state_ = State.Idle;

        this.prepare_hash_cache_ = {};
        this.commit_hash_cache_ = {};
    }

    make_consensus(block) {

        this.pending_block_[block.get_hash()] = block;

        if (this.state_ === State.Idle) {
            this.state_ = State.Prepare;
            this.prepare_info_ = {
                "height": block.get_height(),
                "hash": block.get_hash(),
                "votesNumber": 1,
                "votes": {}
            };
            this.prepare_info_.votes[this.block_chain_.get_account_id()] = true;
            var self = this;
            setImmediate(() => {
                // console.log(`node: ${self.block_chain_.get_account_id()} \t[prepared] broadcast prepare msg to peer: ${self.block_chain_.list_peers()}`);
                self.block_chain_.broadcast(Msg.prepare({
                    "height": block.get_height(),
                    "hash": block.get_hash(),
                    "signer": self.block_chain_.get_account_id()
                }));
            });
        }
    }

    clear_state_() {
        this.state_ = State.Idle;
        this.prepare_info_ = null;
        this.commit_infos_ = {};
        this.pending_block_ = {};
    }

    commit(hash) {
        var block = this.pending_block_[hash];
        block.emit('consensus completed', block.toObject());
        this.clear_state_();
    }

    processMessage(msg) {
        var key = msg.data.hash + ':' + msg.data.height + ':' + msg.data.signer;
        switch (msg.type) {
            case MessageType.Prepare:
                // 如果从未收到过这个prepare消息,则转播出去,否则不处理,防止无限广播
                // hash+height其实就是这个消息的ID(n),而signer就是该消息的副本来源 i
                if (!this.prepare_hash_cache_[key]) {
                    this.prepare_hash_cache_[key] = true;
                    this.block_chain_.broadcast(msg);
                } else {
                    return;
                }
                // 如果当前为prepare状态,且收到的prepare消息是同一个id的消息(关于同一个block的)则这个消息的投票数加1
                // 超过2f+1票后就进入commit状态,发送commit消息
                if (this.state_ === State.Prepare &&
                    msg.data.height === this.prepare_info_.height &&
                    msg.data.hash === this.prepare_info_.hash &&
                    !this.prepare_info_.votes[msg.data.signer]) {
                    this.prepare_info_.votes[msg.data.signer] = true;
                    this.prepare_info_.votesNumber++;
                    if (this.prepare_info_.votesNumber > 2 * PBFT_F) {
                        // console.log(`node: ${this.block_chain_.get_account_id()} \t[commit] broadcast commit msg to peer: ${this.block_chain_.list_peers()}`);
                        this.state_ = State.Commit;
                        var commitInfo = {
                            "height": this.prepare_info_.height,
                            "hash": this.prepare_info_.hash,
                            "votesNumber": 1,
                            "votes": {}
                        };
                        commitInfo.votes[this.block_chain_.get_account_id()] = true;
                        this.commit_infos_[commitInfo.hash] = commitInfo;
                        this.block_chain_.broadcast(Msg.commit({
                            "height": this.prepare_info_.height,
                            "hash": this.prepare_info_.hash,
                            "signer": this.block_chain_.get_account_id()
                        }));
                    }
                }
                break;
            case MessageType.Commit:
                if (!this.commit_hash_cache_[key]) {
                    this.commit_hash_cache_[key] = true;
                    this.block_chain_.broadcast(msg);
                } else {
                    return;
                }
                // prepare消息是只能处理一个,但是commit消息却是可以处理多个,哪一个先达成共识就先处理哪个,然后剩下没处理的都清空
                var commit = this.commit_infos_[msg.data.hash];
                if (commit) {
                    if (!commit.votes[msg.data.signer]) {
                        commit.votes[msg.data.signer] = true;
                        commit.votesNumber++;
                        if (commit.votesNumber > 2 * PBFT_F) {
                            // console.log(`node: ${this.block_chain_.get_account_id()} \t[commited] do commit block}`);
                            this.commit(msg.data.hash);
                        }
                    }
                } else {
                    this.commit_infos_[msg.data.hash] = {
                        hash: msg.data.hash,
                        height: msg.data.height,
                        votesNumber: 1,
                        votes: {}
                    };
                    this.commit_infos_[msg.data.hash].votes[msg.data.signer] = true;
                }
                break;
            default:
                break;
        }
    }
}

module.exports = Pbft;

对照代码我们可以发现,pbft的共识过程只有prepare和commit两个阶段,缺少了pre-prepare的过程。但其实并不是这样,pre_prepare的过程是将主节点收到的消息广播给其他从节点,让其他节点也开始三阶段过程。对应到我们的区块链代码中,可以把当前产生区块的节点看做主节点,产生区块相当于客户端发来的请求,将新产生的区块广播给其他节点就相当于pre-prepare过程,而这一过程在generate_block函数中已经实现,所以pbft共识模块中只需要完成prepare和commit两个过程。