PBFT算法_ide Practical Byzantine Fault Tolerance: PBFT,是联盟币的共识算法的基础。实现了在有限个节点的情况下的拜占庭问题,有3f+1的容错性,并同时保证一定的性能。



联盟链中PBFT实现

在联盟链中,联盟各个节点往往都是来自同一个行业,有着共同的行业困扰和痛点,因此联盟链往往更注重于对实际问题的高效解决。而POW算法相对低效且费时费力,因此在联盟链中并不适用。

1.PBFT算法定义

PBFT是Practical Byzantine Fault Tolerance的缩写,即:实用拜占庭容错算法。该算法是Miguel Castro(卡斯特罗)和Barbara Liskov(利斯科夫)在1999年提出来的,解决了原始拜占庭容错算法效率不高的问题,算法的时间复杂度是O(n^2),使得在实际系统应用中可以解决拜占庭容错问题。该论文发表在1999年的操作系统设计与实现国际会议上(OSDI99)。其中Barbara Liskov就是提出了著名的里氏替换原则(LSP)的人,2008年图灵奖得主。以下拜占庭容错问题简称BFT。实现了在有限个节点的情况下的拜占庭问题,有3f+1的容错性,并同时保证一定的性能。

2.PBFT算法流程

如图:PBFT算法_spring_02

主要分为5个阶段实施,执行前需要先在全网节点选举出一个主节点,新区块的生成由主节点负责。选举出主节点后,开始落实PBFT。五个阶段分别是:Request阶段、Pre-prepare阶段、Prepare阶段、Commit阶段、执行并Reply。

假设此时有主节点、从1节点、从2节点、从3节点共4个节点。

第一阶段:Request阶段

客户端发起请求。

第二阶段:Pre-prepare阶段

主节点收到客户端请求后给请求进行编号,并发送pre-pre类型信息给其他节点。

第三阶段:Prepare阶段

从1节点同意主节点请求的编号,并发送prepare类型消息给主节点和其他两个从节点。如果从1节点不同意请求的编号,则不进行处理。

从1节点如果收到另外两个从节点都发出的同意主节点分配的编号的prepare类型消息,则表示从1节点的状态wei Prepared,进入Commit阶段。

第四阶段:Commit阶段

从1节点进入Prepared状态后,将发送一个commit类型消息给其他所有节点,告诉其他节点当前从1节点已经进入Prepared状态。

如果从1节点收到2f+1条commit消息,则证明从1节点已经进入了Commit状态。当一个请求在某个节点中到达Commit状态后,该请求就会被执行。

第五阶段:执行并Reply

执行区块 生成并Reply生成结果。

目前Hyperledger Fabric已经将PBFT纳入候选共识算法。但是PBFT缺点也很明显,由于先选举主节点,因此当不得不重新选举主节点时,PBFGT将无法达成共识。

3.部分代码

3.1消息的载体:

package com.mindata.blockchain.socket.pbft.msg;

/**
* pbft算法传输prepare和commit消息的载体
* @author wuweifeng wrote on 2018/4/23.
*/
public class VoteMsg {
/**
* 当前投票状态(Prepare,commit)
*/
private byte voteType;
/**
* 区块的hash
*/
private String hash;
/**
* 区块的number
*/
private int number;
/**
* 是哪个节点传来的
*/
private String appId;
/**
* 是否同意
*/
private boolean agree;

@Override
public String toString() {
return "VoteMsg{" +
"voteType=" + voteType +
", hash='" + hash + '\'' +
", number=" + number +
", appId='" + appId + '\'' +
", agree=" + agree +
'}';
}

public byte getVoteType() {
return voteType;
}

public void setVoteType(byte voteType) {
this.voteType = voteType;
}

public String getHash() {
return hash;
}

public void setHash(String hash) {
this.hash = hash;
}

public int getNumber() {
return number;
}

public void setNumber(int number) {
this.number = number;
}

public String getAppId() {
return appId;
}

public void setAppId(String appId) {
this.appId = appId;
}

public boolean isAgree() {
return agree;
}

public void setAgree(boolean agree) {
this.agree = agree;
}
}
package com.mindata.blockchain.socket.pbft.msg;

import com.mindata.blockchain.block.Block;

/**
* @author wuweifeng wrote on 2018/4/25.
*/
public class VotePreMsg extends VoteMsg {
private Block block;

public Block getBlock() {
return block;
}

public void setBlock(Block block) {
this.block = block;
}
}


3.2算法流程定义:


/**
* pbft算法的各状态
*
* 算法流程如下:
*
* 当前时间片的锻造者将收集到的交易打包成block并进行广播(这一步与之前的算法一致)
* 收到block的委托人节点如果还没有收到过这个block并且验证合法后,就广播一个prepare<h, d, s>消息,其中h为block的高度,d是block的摘要,s是本节点签名
* 收到prepare消息后,节点开始在内存中累加消息数量,当收到超过f+1不同节点的prepare消息后,节点进入prepared状态,之后会广播一个commit<h, d, s>消息
* 每个节点收到超过2f+1个不同节点的commit消息后,就认为该区块已经达成一致,进入committed状态,并将其持久化到区块链数据库中
* 系统在在收到第一个高度为h的block时,启动一个定时器,当定时到期后,如果还没达成一致,就放弃本次共识。
*/
public class VoteType {
/**
* 节点自己打算生成Block
*/
public static final byte PREPREPARE = 1;
/**
* 节点收到请求生成block消息,进入准备状态,对外广播该状态
*/
public static final byte PREPARE = 2;
/**
* 每个节点收到超过2f+1个不同节点的commit消息后,就认为该区块已经达成一致,进入committed状态,并将其持久化到区块链数据库中
*/
public static final byte COMMIT = 3;
}

package com.mindata.blockchain.socket.pbft.queue;

import cn.hutool.core.bean.BeanUtil;
import com.mindata.blockchain.block.Block;
import com.mindata.blockchain.common.AppId;
import com.mindata.blockchain.common.timer.TimerManager;
import com.mindata.blockchain.core.event.AddBlockEvent;
import com.mindata.blockchain.core.sqlite.SqliteManager;
import com.mindata.blockchain.socket.pbft.VoteType;
import com.mindata.blockchain.socket.pbft.event.MsgPrepareEvent;
import com.mindata.blockchain.socket.pbft.msg.VoteMsg;
import com.mindata.blockchain.socket.pbft.msg.VotePreMsg;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.context.event.EventListener;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;

import javax.annotation.Resource;
import java.util.concurrent.ConcurrentHashMap;

/**
* preprepare消息的存储,但凡收到请求生成Block的信息,都在这里处理
*
* @author wuweifeng wrote on 2018/4/23.
*/
@Component
public class PreMsgQueue extends BaseMsgQueue {
@Resource
private SqliteManager sqliteManager;
@Resource
private PrepareMsgQueue prepareMsgQueue;
@Resource
private ApplicationEventPublisher eventPublisher;

private ConcurrentHashMap<String, VotePreMsg> blockConcurrentHashMap = new ConcurrentHashMap<>();

private Logger logger = LoggerFactory.getLogger(getClass());

@Override
protected void push(VoteMsg voteMsg) {
//该队列里的是votePreMsg
VotePreMsg votePreMsg = (VotePreMsg) voteMsg;
String hash = votePreMsg.getHash();
//避免收到重复消息
if (blockConcurrentHashMap.get(hash) != null) {
return;
}
//但凡是能进到该push方法的,都是通过基本校验的,但在并发情况下可能会相同number的block都进到投票队列中
//需要对新进来的Vote信息的number进行校验,如果在比prepre阶段靠后的阶段中,已经出现了认证OK的同number的vote,则拒绝进入该队列
if (prepareMsgQueue.otherConfirm(hash, voteMsg.getNumber())) {
logger.info("拒绝进入Prepare阶段,hash为" + hash);
return;
}
// 检测脚本是否正常
try {
sqliteManager.tryExecute(votePreMsg.getBlock());
} catch (Exception e) {
// 执行异常
logger.info("sql指令预执行失败");
return;
}

//存入Pre集合中
blockConcurrentHashMap.put(hash, votePreMsg);

//加入Prepare行列,推送给所有人
VoteMsg prepareMsg = new VoteMsg();
BeanUtil.copyProperties(voteMsg, prepareMsg);
prepareMsg.setVoteType(VoteType.PREPARE);
prepareMsg.setAppId(AppId.value);
eventPublisher.publishEvent(new MsgPrepareEvent(prepareMsg));
}

/**
* 根据hash,得到内存中的Block信息
*
* @param hash
* hash
* @return Block
*/
public Block findByHash(String hash) {
VotePreMsg votePreMsg = blockConcurrentHashMap.get(hash);
if (votePreMsg != null) {
return votePreMsg.getBlock();
}
return null;
}

/**
* 新区块生成后,clear掉map中number比区块小的所有数据
*/
@Order(3)
@EventListener(AddBlockEvent.class)
public void blockGenerated(AddBlockEvent addBlockEvent) {
Block block = (Block) addBlockEvent.getSource();
int number = block.getBlockHeader().getNumber();
TimerManager.schedule(() -> {
for (String key : blockConcurrentHashMap.keySet()) {
if (blockConcurrentHashMap.get(key).getNumber() <= number) {
blockConcurrentHashMap.remove(key);
}
}
return null;
}, 2000);
}
}
package com.mindata.blockchain.socket.pbft.event;

import com.mindata.blockchain.socket.pbft.msg.VoteMsg;
import org.springframework.context.ApplicationEvent;

/**
* 消息已被验证,进入到Prepare集合中
* @author wuweifeng wrote on 2018/4/25.
*/
public class MsgPrepareEvent extends ApplicationEvent {
public MsgPrepareEvent(VoteMsg source) {
super(source);
}
}
package com.mindata.blockchain.socket.pbft.queue;

import cn.hutool.core.bean.BeanUtil;
import com.mindata.blockchain.block.Block;
import com.mindata.blockchain.common.AppId;
import com.mindata.blockchain.core.event.AddBlockEvent;
import com.mindata.blockchain.socket.pbft.VoteType;
import com.mindata.blockchain.socket.pbft.event.MsgCommitEvent;
import com.mindata.blockchain.socket.pbft.msg.VoteMsg;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.context.event.EventListener;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;

import javax.annotation.Resource;
import java.util.List;

/**
* Prepare阶段的消息队列
*
* @author wuweifeng wrote on 2018/4/25.
*/
@Component
public class PrepareMsgQueue extends AbstractVoteMsgQueue {
@Resource
private CommitMsgQueue commitMsgQueue;
@Resource
private ApplicationEventPublisher eventPublisher;
private Logger logger = LoggerFactory.getLogger(getClass());

/**
* 收到节点(包括自己)针对某Block的Prepare消息
*
* @param voteMsg
* voteMsg
*/
@Override
protected void deal(VoteMsg voteMsg, List<VoteMsg> voteMsgs) {
String hash = voteMsg.getHash();
VoteMsg commitMsg = new VoteMsg();
BeanUtil.copyProperties(voteMsg, commitMsg);
commitMsg.setVoteType(VoteType.COMMIT);
commitMsg.setAppId(AppId.value);
//开始校验并决定是否进入commit阶段
//校验该vote是否合法
if (commitMsgQueue.hasOtherConfirm(hash, voteMsg.getNumber())) {
agree(commitMsg, false);
} else {
//开始校验拜占庭数量,如果agree的超过2f + 1,就commit
long agreeCount = voteMsgs.stream().filter(VoteMsg::isAgree).count();
long unAgreeCount = voteMsgs.size() - agreeCount;

//开始发出commit的同意or拒绝的消息
if (agreeCount >= pbftAgreeSize()) {
agree(commitMsg, true);
} else if (unAgreeCount >= pbftSize() + 1) {
agree(commitMsg, false);
}
}

}

private void agree(VoteMsg commitMsg, boolean flag) {
logger.info("Prepare阶段完毕,是否进入commit的标志是:" + flag);
//发出拒绝commit的消息
commitMsg.setAgree(flag);
voteStateConcurrentHashMap.put(commitMsg.getHash(), flag);
eventPublisher.publishEvent(new MsgCommitEvent(commitMsg));
}

/**
* 判断大家是否已对其他的Block达成共识,如果true,则拒绝即将进入队列的Block
*
* @param hash
* hash
* @return 是否存在
*/
public boolean otherConfirm(String hash, int number) {
if (commitMsgQueue.hasOtherConfirm(hash, number)) {
return true;
}
return hasOtherConfirm(hash, number);
}

/**
* 新区块生成后,clear掉map中number比区块小的所有数据
*
* @param addBlockEvent addBlockEvent
*/
@Order(3)
@EventListener(AddBlockEvent.class)
public void blockGenerated(AddBlockEvent addBlockEvent) {
Block block = (Block) addBlockEvent.getSource();
clearOldBlockHash(block.getBlockHeader().getNumber());
}
}
package com.mindata.blockchain.socket.pbft.event;

import com.mindata.blockchain.socket.pbft.msg.VoteMsg;
import org.springframework.context.ApplicationEvent;

/**
* 消息已被验证,进入到Commit集合中
* @author wuweifeng wrote on 2018/4/25.
*/
public class MsgCommitEvent extends ApplicationEvent {
public MsgCommitEvent(VoteMsg source) {
super(source);
}
}


3.3传输消息的载体

package com.mindata.blockchain.socket.pbft.msg;

/**
* pbft算法传输prepare和commit消息的载体
* @author wuweifeng wrote on 2018/4/23.
*/
public class VoteMsg {
/**
* 当前投票状态(Prepare,commit)
*/
private byte voteType;
/**
* 区块的hash
*/
private String hash;
/**
* 区块的number
*/
private int number;
/**
* 是哪个节点传来的
*/
private String appId;
/**
* 是否同意
*/
private boolean agree;

@Override
public String toString() {
return "VoteMsg{" +
"voteType=" + voteType +
", hash='" + hash + '\'' +
", number=" + number +
", appId='" + appId + '\'' +
", agree=" + agree +
'}';
}

public byte getVoteType() {
return voteType;
}

public void setVoteType(byte voteType) {
this.voteType = voteType;
}

public String getHash() {
return hash;
}

public void setHash(String hash) {
this.hash = hash;
}

public int getNumber() {
return number;
}

public void setNumber(int number) {
this.number = number;
}

public String getAppId() {
return appId;
}

public void setAppId(String appId) {
this.appId = appId;
}

public boolean isAgree() {
return agree;
}

public void setAgree(boolean agree) {
this.agree = agree;
}
}
package com.mindata.blockchain.socket.pbft.msg;

import com.mindata.blockchain.block.Block;

/**
* @author wuweifeng wrote on 2018/4/25.
*/
public class VotePreMsg extends VoteMsg {
private Block block;

public Block getBlock() {
return block;
}

public void setBlock(Block block) {
this.block = block;
}
}