在联盟链中,联盟各个节点往往都来自同一行业,有着共同的行业困扰和痛点,因此联盟链往往注重对实际问题的高效解决。而 公链中PoW 算法相对低效且费时费力,因此在联盟链中并不适用。相反在公链中很小适用的PBFT 算法在联盟链中却有用武之地。因此我们来研究研究。
PBFT(Practical Byzantine Fault Tolerance)共识算法可以在少数节点作恶(如伪造消息)场景中达成共识,它采用签名、签名验证、哈希等密码学算法确保消息传递过程中的防篡改性、防伪造性、不可抵赖性,并优化了前人工作,将拜占庭容错算法复杂度从指数级降低到多项式级别,在一个由(3f+1)个节点构成的系统中,只要有不少于(2f+1)个非恶意节点正常工作,该系统就能达成一致性,如:7个节点的系统中允许2个节点出现拜占庭错误。FISCO BCOS区块链系统实现了PBFT共识算法。
Leader/Primary: 共识节点,负责将交易打包成区块和区块共识,每轮共识过程中有且仅有一个leader,为了防止leader伪造区块,每轮PBFT共识后,均会切换leader;
Replica: 副本节点,负责区块共识,每轮共识过程中有多个Replica节点,每个Replica节点的处理过程类似;
Observer: 观察者节点,负责从共识节点或副本节点获取最新区块,执行并验证区块执行结果后,将产生的区块上链。
为了防止节点作恶,PBFT共识过程中每个共识节点均对其发送的消息进行签名,对收到的消息包进行验签名,因此每个节点均维护一份公私钥对,私钥用于对发送的消息进行签名,公钥作为节点ID,用于标识和验签。
节点ID : 共识节点签名公钥和共识节点唯一标识, 一般是64字节二进制串,其他节点使用消息包发送者的节点ID对消息包进行验签
考虑到节点ID很长,在共识消息中包含该字段会耗费部分网络带宽,FISCO BCOS引入了节点索引,每个共识节点维护一份公共的共识节点列表,节点索引记录了每个共识节点ID在这个列表中的位置,发送网络消息包时,只需要带上节点索引,其他节点即可以从公共的共识节点列表中索引出节点的ID,进而对消息进行验签:
节点索引 : 每个共识节点ID在这个公共节点ID列表中的位置
PBFT共识算法使用视图view记录每个节点的共识状态,相同视图节点维护相同的Leader和Replicas节点列表。当Leader出现故障,会发生视图切换,若视图切换成功(至少2*f+1个节点达到相同视图),则根据新的视图选出新leader,新leader开始出块,否则继续进行视图切换,直至全网大部分节点(大于等于2*f+1)达到一致视图。
FISCO BCOS系统中,leader索引的计算公式如下:
leader_idx = (view + block_number) % node_num
下图简单展示了4(3*f+1, f=1)
节点FISCO BCOS系统中,第三个节点(node3)为拜占庭节点情况下,视图切换过程:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-5tfcbfUl-1609921746957)(…/…/…/images/consensus/pbft_view.png)]
前三轮共识: node0、node1、node2为leader,且非恶意节点数目等于2*f+1
,节点正常出块共识;
第四轮共识:node3为leader,但node3为拜占庭节点,node0-node2在给定时间内未收到node3打包的区块,触发视图切换,试图切换到view_new=view+1
的新视图,并相互间广播viewchange包,节点收集满在视图view_new
上的(2*f+1)
个viewchange包后,将自己的view切换为view_new
,并计算出新leader;
为第五轮共识:node0为leader,继续打包出块。
PBFT模块主要包括PrepareReq、SignReq、CommitReq和ViewChangeReq四种共识消息:
PrepareReqPacket: 包含区块的请求包,由leader产生并向所有Replica节点广播,Replica节点收到Prepare包后,验证PrepareReq签名、执行区块并缓存区块执行结果,达到防止拜占庭节点作恶、保证区块执行结果的最终确定性的目的;
SignReqPacket: 带有区块执行结果的签名请求,由收到Prepare包并执行完区块的共识节点产生,SignReq请求带有执行后区块的hash以及该hash的签名,分别记为SignReq.block_hash和SignReq.sig,节点将SignReq广播到所有其他共识节点后,其他节点对SignReq(即区块执行结果)进行共识;
CommitReqPacket: 用于确认区块执行结果的提交请求,由收集满(2*f+1)
个block_hash相同且来自不同节点SignReq请求的节点产生,CommitReq被广播给所有其他共识节点,其他节点收集满(2*f+1)
个block_hash相同、来自不同节点的CommitReq后,将本地节点缓存的最新区块上链;
ViewChangeReqPacket: 视图切换请求,当leader无法提供正常服务(如网络连接不正常、服务器宕机等)时, 其他共识节点会主动触发视图切换,ViewChangeReq中带有该节点即将切换到的视图(记为toView,为当前视图加一),某节点收集满(2*f+1)
个视图等于toView、来自不同节点的ViewChangeReq后,会将当前视图切换为toView。
这四类消息包包含的字段大致相同,所有消息包共有的字段如下:
字段名 | 字段含义 |
---|---|
字段名 | 字段含义 |
idx | 当前节点索引 |
packetType | 请求包类型(包括PrepareReqPacket/SignReqPacket/CommitReqPacket/ViewChangeReqPacket) |
height | 当前正在处理的区块高度(一般是本地区块高度加一) |
blockHash | 当前正在处理的区块哈希 |
view | 当前节点所处的视图 |
sig | 当前节点对blockHash的签名 |
PrepareReqPacket类型消息包包含了正在处理的区块信息:
消息包类型 | 字段名 | 字段含义 |
---|---|---|
PrepareReqPacket | block | 所有共识节点正在共识的区块数据 |
PBFT共识主要包括Pre-prepare、Prepare和Commit三个阶段:
2*f+1
的签名包后,表明自身达到可以提交区块的状态,开始广播Commit包;2*f+1
的Commit包后,直接将本地缓存的最新区块提交到数据库。下图详细介绍了PBFT各个阶段的具体流程:
PBFT共识算法中,共识节点轮流出块,每一轮共识仅有一个leader打包区块,leader索引通过公式(block_number + current_view) % consensus_node_num
计算得出。
节点计算当前leader索引与自己索引相同后,就开始打包区块。区块打包主要由PBFTSealer线程完成,Sealer线程的主要工作如下图所示:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-wJKt3aJ1-1609921746961)(…/…/…/images/consensus/sealer.png)]
产生新的空块: 通过区块链(BlockChain)获取当前最高块,并基于最高块产生新空块(将新区块父哈希置为最高块哈希,时间戳置为当前时间,交易清空);
从交易池打包交易: 产生新空块后,从交易池中获取交易,并将获取的交易插入到产生的新区块中;
组装新区块: Sealer线程打包到交易后,将新区块的打包者(Sealer字段)置为自己索引,并根据打包的交易计算出所有交易的transactionRoot;
产生Prepare包: 将组装的新区块编码到Prepare包内,通过PBFTEngine线程广播给组内所有共识节点,其他共识节点收到Prepare包后,开始进行三阶段共识。
共识节点收到Prepare包后,进入pre-prepare阶段,此阶段的主要工作流程包括:
Prepare包合法性判断:主要判断是否是重复的Prepare包、Prepare请求中包含的区块父哈希是否是当前节点最高块哈希(防止分叉)、Prepare请求中包含区块的块高是否等于最高块高加一;
缓存合法的Prepare包: 若Prepare请求合法,则将其缓存到本地,用于过滤重复的Prepare请求;
空块判断:若Prepare请求包含的区块中交易数目是0,则触发空块视图切换,将当前视图加一,并向所有其他节点广播视图切换请求;
执行区块并缓存区块执行结果: 若Prepare请求包含的区块中交易数目大于0,则调用BlockVerifier区块执行器执行区块,并缓存执行后的区块;
产生并广播签名包:基于执行后的区块哈希,产生并广播签名包,表明本节点已经完成区块执行和验证。
共识节点收到签名包后,进入Prepare阶段,此阶段的主要工作流程包括:
签名包合法性判断:主要判断签名包的哈希与pre-prepare阶段缓存的执行后的区块哈希相同,若不相同,则继续判断该请求是否属于未来块签名请求(产生未来块的原因是本节点处理性能低于其他节点,还在进行上一轮共识,判断未来块的条件是:签名包的height字段大于本地最高块高加一),若请求也非未来块,则是非法的签名请求,节点直接拒绝该签名请求;
缓存合法的签名包:节点会缓存合法的签名包;
判断pre-prepare阶段缓存的区块对应的签名包缓存是否达到2*f+1
,若收集满签名包,广播Commit包:若pre-prepare阶段缓存的区块哈希对应的签名包数目超过2*f+1
,则说明大多数节点均执行了该区块,并且执行结果一致,说明本节点已经达到可以提交区块的状态,开始广播Commit包;
若收集满签名包,备份pre-prepare阶段缓存的Prepare包落盘:为了防止Commit阶段区块未提交到数据库之前超过2*f+1
个节点宕机,这些节点启动后重新出块,导致区块链分叉(剩余的节点最新区块与这些节点最高区块不同),还需要备份pre-prepare阶段缓存的Prepare包到数据库,节点重启后,优先处理备份的Prepare包。
共识节点收到Commit包后,进入Commit阶段,此阶段工作流程包括:
Commit包合法性判断:主要判断Commit包的哈希与pre-prepare阶段缓存的执行后的区块哈希相同,若不相同,则继续判断该请求是否属于未来块Commit请求(产生未来块的原因是本节点处理性能低于其他节点,还在进行上一轮共识,判断未来块的条件是:Commit的height字段大于本地最高块高加一),若请求也非未来块,则是非法的Commit请求,节点直接拒绝该请求;
缓存合法的Commit包:节点缓存合法的Commit包;
判断pre-prepare阶段缓存的区块对应的Commit包缓存是否达到2*f+1
,若收集满Commit包,则将新区块落盘:若pre-prepare阶段缓存的区块哈希对应的Commit请求数目超过2*f+1
,则说明大多数节点达到了可提交该区块状态,且执行结果一致,则调用BlockChain模块将pre-prepare阶段缓存的区块写入数据库;
当PBFT三阶段共识超时或节点收到空块时,PBFTEngine会试图切换到更高的视图(将要切换到的视图toView加一),并触发ViewChange处理流程;节点收到ViewChange包时,也会触发ViewChange处理流程:
判断ViewChange包是否有效: 有效的ViewChange请求中带有的块高值必须不小于当前节点最高块高,视图必须大于当前节点视图;
缓存有效ViewChange包: 防止相同的ViewChange请求被处理多次,也作为判断节点是否可以切换视图的统计依据;
收集ViewChange包:若收到的ViewChange包中附带的view等于本节点的即将切换到的视图toView且本节点收集满2*f+1
来自不同节点view等于toView的ViewChange包,则说明超过三分之二的节点要切换到toView视图,切换当前视图到toView。
上述内容参考; https://fisco-bcos-documentation.readthedocs.io/zh_CN/latest/docs/design/consensus/pbft.html
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0modelVersion>
<groupId>com.larryxianggroupId>
<artifactId>PBFTartifactId>
<version>1.0-SNAPSHOTversion>
<properties>
<project.build.sourceEncoding>UTF-8project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8project.reporting.outputEncoding>
<maven.compiler.encoding>UTF-8maven.compiler.encoding>
properties>
<build>
<finalName>pbftfinalName>
<plugins>
<plugin>
<groupId>org.apache.maven.pluginsgroupId>
<artifactId>maven-compiler-pluginartifactId>
<configuration>
<source>8source>
<target>8target>
configuration>
plugin>
plugins>
build>
<dependencies>
<dependency>
<groupId>org.iq80.leveldbgroupId>
<artifactId>leveldbartifactId>
<version>0.12version>
dependency>
<dependency>
<groupId>org.iq80.leveldbgroupId>
<artifactId>leveldb-apiartifactId>
<version>0.12version>
dependency>
<dependency>
<groupId>cn.hutoolgroupId>
<artifactId>hutool-allartifactId>
<version>5.0.7version>
dependency>
<dependency>
<groupId>com.alibabagroupId>
<artifactId>fastjsonartifactId>
<version>1.2.62version>
dependency>
<dependency>
<groupId>org.t-iogroupId>
<artifactId>tio-coreartifactId>
<version>3.5.8.v20191228-RELEASEversion>
dependency>
<dependency>
<groupId>org.projectlombokgroupId>
<artifactId>lombokartifactId>
<version>1.18.10version>
<scope>providedscope>
dependency>
<dependency>
<groupId>org.apache.logging.log4jgroupId>
<artifactId>log4j-coreartifactId>
<version>2.13.2version>
dependency>
<dependency>
<groupId>org.slf4jgroupId>
<artifactId>slf4j-log4j12artifactId>
<version>2.0.0-alpha1version>
dependency>
<dependency>
<groupId>com.google.guavagroupId>
<artifactId>guavaartifactId>
<version>28.2-jreversion>
dependency>
dependencies>
project>
package com.larryxiang.dao.node;
import cn.hutool.crypto.asymmetric.RSA;
import lombok.Data;
/**
* @author: larry.xiang
* @description: 结点自身的信息
*/
@Data
public class Node extends NodeBasicInfo {
private static Node node = new Node();
/**
* 单例设计模式
*
* @return
*/
public static Node getInstance() {
return node;
}
private Node() {
RSA rsa = new RSA();
this.setPrivateKey(rsa.getPrivateKeyBase64());
this.setPublicKey(rsa.getPublicKeyBase64());
}
/**
* 判断结点是否运行
*/
private boolean isRun = false;
/**
* 视图状态,判断是否ok,
*/
private volatile boolean viewOK;
/**
* 公钥
*/
private String publicKey;
/**
* 私钥
*/
private String privateKey;
}
package com.larryxiang.dao.node;
import lombok.Data;
/**
* @author: larry.xiang
* @description: nodeAddress里面保存了结点的通信地址
*/
@Data
public class NodeAddress {
/**
* ip地址
*/
private String ip;
/**
* 通信地址的端口号
*/
private int port;
}
package com.larryxiang.dao.node;
import lombok.Data;
/**
* @author: larry.xiang
* @description: 结点节本信息
*/
@Data
public class NodeBasicInfo {
/**
* 结点地址的信息
*/
private NodeAddress address;
/**
* 这个代表了结点的序号
*/
private int index;
}
package com.larryxiang.dao.pbft;
import com.larryxiang.dao.bean.DbDao;
import com.google.common.collect.Sets;
import com.google.common.util.concurrent.AtomicLongMap;
import lombok.Data;
import java.util.Set;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.atomic.AtomicLong;
/**
* @author: larry.xiang
* @description: 这个是进行PBFT算法保存的消息
* 使用单例模式进行设计
*/
@Data
public class MsgCollection {
private static MsgCollection msgCollection = new MsgCollection();
/**
* 空的构造方法
*/
private MsgCollection() {
}
public static MsgCollection getInstance() {
return msgCollection;
}
/**
* 进行处理的消息队列,don‘t care about what is , just get it !!
*/
private BlockingQueue<PbftMsg> msgQueue = new LinkedBlockingQueue<PbftMsg>();
/**
* 这个是在初始化视图的时候会用到
*/
private AtomicLongMap<Integer> viewNumCount = AtomicLongMap.create();
/**
* 参与认证的节点
*/
private CopyOnWriteArrayList<DbDao> dbDaos = new CopyOnWriteArrayList<DbDao>();
/**
* 不赞同视图的数量
*/
private AtomicLong disagreeViewNum = new AtomicLong();
/**
* 预准备阶段
*/
private Set<String> votePrePrepare = Sets.newConcurrentHashSet();
/**
* 准备阶段
*/
private AtomicLongMap<String> agreePrepare = AtomicLongMap.create();
/**
* commit阶段
*/
private AtomicLongMap<String> agreeCommit = AtomicLongMap.create();
}
package com.larryxiang.dao.pbft;
/**
* @author: larry.xiang
* @description: 消息类型
*/
public class MsgType {
/**
* 请求视图
*/
public static final int GET_VIEW = -1;
/**
* 变更视图
*/
public static final int CHANGE_VIEW = 0;
/**
* 预准备阶段
*/
public static final int PRE_PREPARE = 1;
/**
* 准备阶段
*/
public static final int PREPARE = 2;
/**
* 提交阶段
*/
public static final int COMMIT = 3;
/**
* ip消息回复回复阶段
*/
public static final int CLIENT_REPLAY = 4;
}
package com.larryxiang.dao.pbft;
import cn.hutool.core.util.IdUtil;
import com.larryxiang.config.AllNodeCommonMsg;
import lombok.Data;
import java.util.Objects;
/**
* @author: larry.xiang
* @description: 进行Pbft发送的消息、
*/
@Data
public class PbftMsg {
/**
* 消息类型
*/
private int msgType;
/**
* 消息发起的结点编号
*/
private int node;
/**
* 消息发送的目的地
*/
private int toNode;
/**
* 消息时间戳
*/
private long time;
/**
* 消息体
*/
private String body;
/**
* 检测是否通过
*/
private boolean isOk;
/**
* 结点视图
*/
private int viewNum;
/**
* 使用UUID进行生成
*/
private String id;
/**
* 消息的签名
*/
private String sign;
private PbftMsg() {
}
public PbftMsg(int msgType, int node) {
this.msgType = msgType;
this.node = node;
this.time = System.currentTimeMillis();
this.id = IdUtil.randomUUID();
this.viewNum = AllNodeCommonMsg.view;
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (!(o instanceof PbftMsg)) {
return false;
}
PbftMsg msg = (PbftMsg) o;
return getMsgType() == msg.getMsgType() &&
getNode() == msg.getNode() &&
getToNode() == msg.getToNode() &&
getTime() == msg.getTime() &&
isOk() == msg.isOk() &&
getViewNum() == msg.getViewNum() &&
Objects.equals(getBody(), msg.getBody()) &&
Objects.equals(getId(), msg.getId());
}
@Override
public int hashCode() {
return Objects.hash(getMsgType(), getBody(), getNode(), getToNode(), getTime(), getViewNum(), getId());
}
}
package com.larryxiang.p2p.client;
import com.larryxiang.config.AllNodeCommonMsg;
import com.larryxiang.dao.node.Node;
import com.larryxiang.dao.pbft.MsgCollection;
import com.larryxiang.dao.pbft.MsgType;
import com.larryxiang.dao.pbft.PbftMsg;
import com.larryxiang.util.ClientUtil;
import com.larryxiang.util.MsgUtil;
import com.larryxiang.util.PbftUtil;
import com.larryxiang.util.TestUtil;
import lombok.extern.slf4j.Slf4j;
import org.tio.core.ChannelContext;
/**
* @author: larry.xiang
* @description: 当client接受到消息时,会在这里进行处理
*/
@Slf4j
public class ClientAction {
private MsgCollection collection = MsgCollection.getInstance();
private Node node = Node.getInstance();
/**
* 使用单例设计模式
*/
private static ClientAction action = new ClientAction();
public static ClientAction getInstance() {
return action;
}
private ClientAction() {
}
/**
* 对消息进行处理
*
* @param channelContext
*/
public void doAction(ChannelContext channelContext) {
try {
PbftMsg msg = collection.getMsgQueue().take();
// 当视图还未好的时候,不处理非视图事务
if (!node.isViewOK() && msg.getMsgType() != MsgType.GET_VIEW && msg.getMsgType() != MsgType.CHANGE_VIEW) {
collection.getMsgQueue().put(msg);
return;
}
switch (msg.getMsgType()) {
case MsgType.GET_VIEW:
getView(msg);
break;
case MsgType.CHANGE_VIEW:
onChangeView(msg);
break;
case MsgType.PREPARE:
prepare(msg);
break;
case MsgType.COMMIT:
commit(msg);
default:
break;
}
} catch (InterruptedException e) {
log.debug(String.format("消息队列take错误:%s", e.getMessage()));
}
}
/**
* 提交commit
*
* @param msg
*/
private void commit(PbftMsg msg) {
ClientUtil.clientPublish(msg);
}
/**
* 向所有节点发送prepare消息
*
* @param msg
*/
private void prepare(PbftMsg msg) {
ClientUtil.clientPublish(msg);
}
/**
* 发送重新选举的消息
*/
private void onChangeView(PbftMsg msg) {
// view进行加1处理
int viewNum = AllNodeCommonMsg.view + 1;
msg.setViewNum(viewNum);
ClientUtil.clientPublish(msg);
}
/**
* 获得view
*
* @param msg
*/
synchronized private void getView(PbftMsg msg) {
int fromNode = msg.getNode();
if (node.isViewOK()) {
return;
}
if (!MsgUtil.isRealMsg(msg) || !msg.isOk()) {
long count = collection.getDisagreeViewNum().incrementAndGet();
if (count >= AllNodeCommonMsg.getMaxf()) {
log.info("视图获取失败");
//程序结束记录时间
TestUtil.endTime = System.currentTimeMillis();
long totalTime = TestUtil.endTime - TestUtil.startTime;
TestUtil.writeBadTime(totalTime, Node.getInstance().getIndex());
System.exit(0);
}
return;
}
// 将消息添加到list中
// DbUtil.addDaotoList(fromNode,msg);
long count = collection.getViewNumCount().incrementAndGet(msg.getViewNum());
if (count >= AllNodeCommonMsg.getAgreeNum() && !node.isViewOK()) {
// 将节点认证消息保存
// DbUtil.save();
collection.getViewNumCount().clear();
//程序结束记录时间
TestUtil.endTime = System.currentTimeMillis();
//总消耗时间
long totalTime = TestUtil.endTime - TestUtil.startTime;
node.setViewOK(true);
AllNodeCommonMsg.view = msg.getViewNum();
log.info("视图初始化完成OK");
// 将节点写入文件
PbftUtil.writeIpToFile(node);
TestUtil.writeOkTime(totalTime, Node.getInstance().getIndex());
ClientUtil.publishIpPort(node.getIndex(), node.getAddress().getIp(), node.getAddress().getPort());
}
}
}
package com.larryxiang.p2p.client;
import com.larryxiang.dao.pbft.MsgType;
import com.alibaba.fastjson.JSON;
import com.larryxiang.dao.pbft.MsgCollection;
import com.larryxiang.util.MsgUtil;
import com.larryxiang.dao.pbft.PbftMsg;
import com.larryxiang.p2p.common.MsgPacket;
import lombok.extern.slf4j.Slf4j;
import org.tio.client.intf.ClientAioHandler;
import org.tio.core.ChannelContext;
import org.tio.core.TioConfig;
import org.tio.core.exception.AioDecodeException;
import org.tio.core.intf.Packet;
import java.nio.ByteBuffer;
import java.util.concurrent.BlockingQueue;
/**
* @author: larry.xiang
* @description: 客户端处理
*/
@Slf4j
public class P2pClientAioHandler implements ClientAioHandler {
/**
* this is heart packet,目的是告诉服务器端我存在
*/
private static MsgPacket heartPacket = new MsgPacket();
/**
* 消息队列
*/
private BlockingQueue<PbftMsg> msgQueue = MsgCollection.getInstance().getMsgQueue();
/**
* 创建心跳包
*
* @param channelContext
* @return
* @author tanyaowu
*/
@Override
public Packet heartbeatPacket(ChannelContext channelContext) {
return heartPacket;
}
/**
* 根据ByteBuffer解码成业务需要的Packet对象.
* 如果收到的数据不全,导致解码失败,请返回null,在下次消息来时框架层会自动续上前面的收到的数据
*
* @param buffer 参与本次希望解码的ByteBuffer
* @param limit ByteBuffer的limit
* @param position ByteBuffer的position,不一定是0哦
* @param readableLength ByteBuffer参与本次解码的有效数据(= limit - position)
* @param channelContext
* @return
* @throws AioDecodeException
*/
@Override
public Packet decode(ByteBuffer buffer, int limit, int position, int readableLength, ChannelContext channelContext) throws AioDecodeException {
if (readableLength < MsgPacket.HEADER_LENGHT) {
return null;
}
int bodyLength = buffer.getInt();
if (bodyLength < 0) {
throw new AioDecodeException("body length is invalid.romote: " + channelContext.getServerNode());
}
int usefulLength = MsgPacket.HEADER_LENGHT + bodyLength;
if (usefulLength > readableLength) {
return null;
} else {
MsgPacket packet = new MsgPacket();
byte[] body = new byte[bodyLength];
buffer.get(body);
packet.setBody(body);
return packet;
}
}
/**
* 编码
*
* @param packet
* @param tioConfig
* @param channelContext
* @return
* @author: tanyaowu
*/
@Override
public ByteBuffer encode(Packet packet, TioConfig tioConfig, ChannelContext channelContext) {
MsgPacket msgPacket = (MsgPacket) packet;
byte[] body = msgPacket.getBody();
int bodyLength = 0;
if (body != null) {
bodyLength = body.length;
}
int len = MsgPacket.HEADER_LENGHT + bodyLength;
ByteBuffer byteBuffer = ByteBuffer.allocate(len);
byteBuffer.order(tioConfig.getByteOrder());
byteBuffer.putInt(bodyLength);
if (body != null) {
byteBuffer.put(body);
}
return byteBuffer;
}
/**
* 处理消息包
*
* @param packet
* @param channelContext
* @throws Exception
* @author: tanyaowu
*/
@Override
public void handler(Packet packet, ChannelContext channelContext) throws Exception {
MsgPacket msgPacket = (MsgPacket) packet;
byte[] body = msgPacket.getBody();
// 空的很可能为心跳包
if (body == null) {
return;
}
String msg = new String(body, MsgPacket.CHARSET);
// 如果数据不是json数据,代表数据有问题
if (!JSON.isValid(msg)) {
return;
}
PbftMsg pbftMsg = JSON.parseObject(msg, PbftMsg.class);
if (pbftMsg == null) {
log.error("客户端将Json数据解析成pbft数据失败");
return;
}
if (pbftMsg.getMsgType() != MsgType.GET_VIEW && !MsgUtil.afterMsg(pbftMsg)) {
log.warn("数据检查签名或者解密失败");
return;
}
this.msgQueue.put(pbftMsg);
}
}
package com.larryxiang.p2p.client;
import com.larryxiang.config.AllNodeCommonMsg;
import com.larryxiang.dao.node.Node;
import com.larryxiang.dao.pbft.MsgCollection;
import com.larryxiang.dao.pbft.MsgType;
import com.larryxiang.dao.pbft.PbftMsg;
import com.larryxiang.p2p.P2PConnectionMsg;
import lombok.extern.slf4j.Slf4j;
import org.tio.client.intf.ClientAioListener;
import org.tio.core.ChannelContext;
import org.tio.core.intf.Packet;
import java.util.concurrent.BlockingQueue;
/**
* @author: larry.xiang
* @description: 客户端监听器
*/
@Slf4j
public class P2PClientLinstener implements ClientAioListener {
private ClientAction action = ClientAction.getInstance();
private Node node = Node.getInstance();
/**
* 消息队列
*/
private BlockingQueue<PbftMsg> msgQueue = MsgCollection.getInstance().getMsgQueue();
/**
* 建链后触发本方法,注:建链不一定成功,需要关注参数isConnected
*
* @param channelContext
* @param isConnected 是否连接成功,true:表示连接成功,false:表示连接失败
* @param isReconnect 是否是重连, true: 表示这是重新连接,false: 表示这是第一次连接
* @throws Exception
* @author: tanyaowu
*/
@Override
public void onAfterConnected(ChannelContext channelContext, boolean isConnected, boolean isReconnect) throws Exception {
if (isReconnect) {
log.warn(String.format("结点%重新连接服务端", channelContext));
}
if (isConnected) {
log.info(String.format("结点%s连接服务端成功", channelContext));
}else{
log.warn(String.format("结点%s连接服务端%s失败", node.getIndex(),channelContext.getServerNode()));
}
}
/**
* 原方法名:onAfterDecoded
* 解码成功后触发本方法
*
* @param channelContext
* @param packet
* @param packetSize
* @throws Exception
* @author: tanyaowu
*/
@Override
public void onAfterDecoded(ChannelContext channelContext, Packet packet, int packetSize) throws Exception {
}
/**
* 接收到TCP层传过来的数据后
*
* @param channelContext
* @param receivedBytes 本次接收了多少字节
* @throws Exception
*/
@Override
public void onAfterReceivedBytes(ChannelContext channelContext, int receivedBytes) throws Exception {
}
/**
* 消息包发送之后触发本方法
*
* @param channelContext
* @param packet
* @param isSentSuccess true:发送成功,false:发送失败
* @throws Exception
* @author tanyaowu
*/
@Override
public void onAfterSent(ChannelContext channelContext, Packet packet, boolean isSentSuccess) throws Exception {
}
/**
* 当处理好消息是就行action
* @param channelContext
* @param packet
* @param cost 本次处理消息耗时,单位:毫秒
* @throws Exception
*/
@Override
public void onAfterHandled(ChannelContext channelContext, Packet packet, long cost) throws Exception {
action.doAction(channelContext);
}
/**
* 连接关闭前触发本方法
*
* @param channelContext the channelcontext
* @param throwable the throwable 有可能为空
* @param remark the remark 有可能为空
* @param isRemove
* @throws Exception
* @author tanyaowu
*/
@Override
public void onBeforeClose(final ChannelContext channelContext, Throwable throwable, String remark, boolean isRemove) throws Exception {
log.warn(String.format("客户端%s连接关闭", channelContext));
// 假如连接中断则移除
P2PConnectionMsg.CLIENTS.values().removeIf(v -> v.equals(channelContext));
/**
* 假如中断的是主结点
*/
if (channelContext.equals(P2PConnectionMsg.CLIENTS.get(AllNodeCommonMsg.getPriIndex()))){
log.warn("主节点链接失败,决定发起视图选举");
node.setViewOK(false);
PbftMsg msg = new PbftMsg(MsgType.CHANGE_VIEW,node.getIndex());
msgQueue.put(msg);
action.doAction(channelContext);
}
}
}
package com.larryxiang.p2p.server;
import com.alibaba.fastjson.JSON;
import com.larryxiang.dao.pbft.MsgType;
import com.larryxiang.util.MsgUtil;
import com.larryxiang.dao.pbft.PbftMsg;
import com.larryxiang.p2p.common.MsgPacket;
import lombok.extern.slf4j.Slf4j;
import org.tio.core.ChannelContext;
import org.tio.core.TioConfig;
import org.tio.core.exception.AioDecodeException;
import org.tio.core.intf.Packet;
import org.tio.server.intf.ServerAioHandler;
import java.nio.ByteBuffer;
/**
* @author: larry.xiang
* @description: 服务端的Handler
*/
@Slf4j
public class P2PServerAioHandler implements ServerAioHandler {
private ServerAction action = ServerAction.getInstance();
/**
* 根据ByteBuffer解码成业务需要的Packet对象.
* 如果收到的数据不全,导致解码失败,请返回null,在下次消息来时框架层会自动续上前面的收到的数据
*
* @param buffer 参与本次希望解码的ByteBuffer
* @param limit ByteBuffer的limit
* @param position ByteBuffer的position,不一定是0哦
* @param readableLength ByteBuffer参与本次解码的有效数据(= limit - position)
* @param channelContext
* @return
* @throws AioDecodeException
*/
@Override
public Packet decode(ByteBuffer buffer, int limit, int position, int readableLength, ChannelContext channelContext) throws AioDecodeException {
// 假如包的长度小于基本长度,毋庸置疑,包没有接收完
if (readableLength < MsgPacket.HEADER_LENGHT) {
return null;
}
// 读取发送消息的长度
int bodyLength = buffer.getInt();
//数据不正确,则抛出AioDecodeException异常
if (bodyLength < 0) {
throw new AioDecodeException("bodyLength [" + bodyLength + "] is not right, remote:" + channelContext.getClientNode());
}
// 客户端发送消息的长度
int neededLength = MsgPacket.HEADER_LENGHT + bodyLength;
// 不够消息体长度(剩下的buffe组不了消息体)
if (readableLength < neededLength) {
return null;
} else {
//组包成功
MsgPacket imPacket = new MsgPacket();
if (bodyLength > 0) {
byte[] dst = new byte[bodyLength];
buffer.get(dst);
imPacket.setBody(dst);
}
return imPacket;
}
}
/**
* 编码
*
* @param packet
* @param tioConfig
* @param channelContext
* @return
* @author: larry.xiang
*/
@Override
public ByteBuffer encode(Packet packet, TioConfig tioConfig, ChannelContext channelContext) {
MsgPacket msgPacket = (MsgPacket) packet;
byte[] body = msgPacket.getBody();
int bodyLen = 0;
if (body != null) {
bodyLen = body.length;
}
//bytebuffer的总长度是 = 消息头的长度 + 消息体的长度
int allLen = MsgPacket.HEADER_LENGHT + bodyLen;
//创建一个新的bytebuffer
ByteBuffer buffer = ByteBuffer.allocate(allLen);
//设置字节序
buffer.order(tioConfig.getByteOrder());
//写入消息头----消息头的内容就是消息体的长度
buffer.putInt(bodyLen);
//写入消息体
if (body != null) {
buffer.put(body);
}
return buffer;
}
/**
* 处理消息包
*
* @param packet
* @param channelContext
* @throws Exception
* @author: larry.xiang
*/
@Override
public void handler(Packet packet, ChannelContext channelContext) throws Exception {
MsgPacket msgPacket = (MsgPacket) packet;
byte[] body = msgPacket.getBody();
// 空的很可能为心跳包
if (body == null) {
return;
}
String msg = new String(body, MsgPacket.CHARSET);
// 如果数据不是json数据,代表数据有问题
if (!JSON.isValid(msg)) {
return;
}
log.info("服务端接受消息:" + msg);
PbftMsg pbftMsg = JSON.parseObject(msg, PbftMsg.class);
if (pbftMsg == null) {
log.error("客户端将Json数据解析成pbft数据失败");
return;
}
if ((pbftMsg.getMsgType() != MsgType.CLIENT_REPLAY && pbftMsg.getMsgType() != MsgType.GET_VIEW) && !MsgUtil.afterMsg(pbftMsg)) {
log.warn("数据检查签名或者解密失败");
return;
}
// 服务端对消息进行处理
action.doAction(channelContext, pbftMsg);
}
}
package com.larryxiang.p2p.server;
import com.larryxiang.config.AllNodeCommonMsg;
import com.larryxiang.dao.bean.ReplayJson;
import com.larryxiang.dao.node.Node;
import com.larryxiang.dao.node.NodeAddress;
import com.larryxiang.dao.node.NodeBasicInfo;
import com.larryxiang.dao.pbft.MsgCollection;
import com.larryxiang.dao.pbft.MsgType;
import com.larryxiang.dao.pbft.PbftMsg;
import com.larryxiang.p2p.client.ClientAction;
import com.larryxiang.p2p.common.MsgPacket;
import com.larryxiang.util.ClientUtil;
import com.larryxiang.util.MsgUtil;
import com.larryxiang.util.PbftUtil;
import com.alibaba.fastjson.JSON;
import lombok.extern.slf4j.Slf4j;
import org.tio.client.ClientChannelContext;
import org.tio.core.ChannelContext;
import org.tio.core.Tio;
import java.io.UnsupportedEncodingException;
/**
* @author: larry.xiang
* @description: 服务端的Action
*/
@Slf4j
public class ServerAction {
private Node node = Node.getInstance();
private MsgCollection msgCollection = MsgCollection.getInstance();
/**
* 单例模式构建action
*/
private static ServerAction action = new ServerAction();
private MsgCollection collection = MsgCollection.getInstance();
public static ServerAction getInstance() {
return action;
}
private ServerAction() {
}
/**
* 对PBFT消息做出回应
*
* @param channelContext 谁发送的请求
* @param msg 消息内容
*/
public void doAction(ChannelContext channelContext, PbftMsg msg) {
switch (msg.getMsgType()) {
case MsgType.GET_VIEW:
onGetView(channelContext, msg);
break;
case MsgType.CHANGE_VIEW:
changeView(channelContext, msg);
break;
case MsgType.PRE_PREPARE:
prePrepare(msg);
break;
case MsgType.PREPARE:
prepare(msg);
break;
case MsgType.COMMIT:
commit(msg);
case MsgType.CLIENT_REPLAY:
addClient(msg);
break;
default:
break;
}
}
/**
* commit阶段
*
* @param msg
*/
private void commit(PbftMsg msg) {
long count = collection.getAgreeCommit().incrementAndGet(msg.getId());
log.info(String.format("server接受到commit消息:%s", msg));
if (count >= AllNodeCommonMsg.getAgreeNum()) {
log.info("数据符合,commit成功,数据可以生成块");
collection.getAgreeCommit().remove(msg.getId());
PbftUtil.save(msg);
}
}
/**
* 节点将prepare消息进行广播然后被接收到
*
* @param msg
*/
private void prepare(PbftMsg msg) {
log.info(msgCollection.getVotePrePrepare().contains(msg) + ">>>>");
if (!msgCollection.getVotePrePrepare().contains(msg.getId()) || !PbftUtil.checkMsg(msg)) {
return;
}
long count = collection.getAgreePrepare().incrementAndGet(msg.getId());
log.info(String.format("server接受到prepare消息:%s", msg));
if (count >= AllNodeCommonMsg.getAgreeNum()) {
log.info("数据符合,发送commit操作");
collection.getVotePrePrepare().remove(msg.getId());
collection.getAgreePrepare().remove(msg.getId());
// 进入Commit阶段
msg.setMsgType(MsgType.COMMIT);
try {
collection.getMsgQueue().put(msg);
ClientAction.getInstance().doAction(null);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
/**
* 主节点发送过来的pre_prepare消息
*
* @param msg
*/
private void prePrepare(PbftMsg msg) {
log.info(String.format("server接受到pre-prepare消息:%s", msg));
msgCollection.getVotePrePrepare().add(msg.getId());
if (!PbftUtil.checkMsg(msg)) {
return;
}
msg.setMsgType(MsgType.PREPARE);
try {
msgCollection.getMsgQueue().put(msg);
ClientAction.getInstance().doAction(null);
} catch (InterruptedException e) {
e.printStackTrace();
return;
}
}
/**
* 重新设置view
*
* @param channelContext
* @param msg
*/
private void changeView(ChannelContext channelContext, PbftMsg msg) {
if (node.isViewOK()) {
return;
}
long count = collection.getViewNumCount().incrementAndGet(msg.getViewNum());
if (count >= AllNodeCommonMsg.getAgreeNum() && !node.isViewOK()) {
collection.getViewNumCount().clear();
node.setViewOK(true);
AllNodeCommonMsg.view = msg.getViewNum();
log.info("视图变更完成OK");
}
}
/**
* 添加未连接的结点
*
* @param msg
*/
private void addClient(PbftMsg msg) {
if (!ClientUtil.haveClient(msg.getNode())) {
String ipStr = msg.getBody();
ReplayJson replayJson = JSON.parseObject(ipStr, ReplayJson.class);
ClientChannelContext context = ClientUtil.clientConnect(replayJson.getIp(), replayJson.getPort());
NodeAddress address = new NodeAddress();
address.setIp(replayJson.getIp());
address.setPort(replayJson.getPort());
NodeBasicInfo info = new NodeBasicInfo();
info.setIndex(msg.getNode());
info.setAddress(address);
// 添加ip地址
AllNodeCommonMsg.allNodeAddressMap.put(msg.getNode(), info);
AllNodeCommonMsg.publicKeyMap.put(msg.getNode(), replayJson.getPublicKey());
log.info(String.format("节点%s添加ip地址:%s", node, info));
if (context != null) {
// 添加client
ClientUtil.addClient(msg.getNode(), context);
}
}
}
/**
* 将自己的view发送给client
*
* @param channelContext
* @param msg
*/
private void onGetView(ChannelContext channelContext, PbftMsg msg) {
log.info("server结点回复视图请求操作");
int fromNode = msg.getNode();
// 设置消息的发送方
msg.setNode(node.getIndex());
// 设置消息的目的地
msg.setToNode(fromNode);
log.info(String.format("同意此节点%s的申请", msg));
msg.setOk(true);
msg.setViewNum(AllNodeCommonMsg.view);
MsgUtil.signMsg(msg);
String jsonView = JSON.toJSONString(msg);
MsgPacket msgPacket = new MsgPacket();
try {
msgPacket.setBody(jsonView.getBytes(MsgPacket.CHARSET));
Tio.send(channelContext, msgPacket);
} catch (UnsupportedEncodingException e) {
log.error(String.format("server结点发送view消息失败%s", e.getMessage()));
}
}
}
package com.larryxiang.p2p.server;
import cn.hutool.core.util.RandomUtil;
import lombok.extern.slf4j.Slf4j;
import org.tio.core.ChannelContext;
import org.tio.core.intf.Packet;
import org.tio.server.intf.ServerAioListener;
/**
* @author: larry.xiang
* @description: 服务监听器
*/
@Slf4j
public class ServerListener implements ServerAioListener {
/**
* 服务器检查到心跳超时时,会调用这个函数(一般场景,该方法只需要直接返回false即可)
*
* @param channelContext
* @param interval 已经多久没有收发消息了,单位:毫秒
* @param heartbeatTimeoutCount 心跳超时次数,第一次超时此值是1,以此类推。此值被保存在:channelContext.stat.heartbeatTimeoutCount
* @return 返回true,那么服务器则不关闭此连接;返回false,服务器将按心跳超时关闭该连接
*/
@Override
public boolean onHeartbeatTimeout(ChannelContext channelContext, Long interval, int heartbeatTimeoutCount) {
if (channelContext.stat.heartbeatTimeoutCount.intValue() > 5){
log.warn(String.format("结点%s连接超时5次,关闭此连接", channelContext));
return false;
}
return true;
}
/**
* 建链后触发本方法,注:建链不一定成功,需要关注参数isConnected
*
* @param channelContext
* @param isConnected 是否连接成功,true:表示连接成功,false:表示连接失败
* @param isReconnect 是否是重连, true: 表示这是重新连接,false: 表示这是第一次连接
* @throws Exception
* @author: tanyaowu
*/
@Override
public void onAfterConnected(ChannelContext channelContext, boolean isConnected, boolean isReconnect) throws Exception {
// if (isReconnect || !isConnected){
// return;
// }
//
}
/**
* 原方法名:onAfterDecoded
* 解码成功后触发本方法
*
* @param channelContext
* @param packet
* @param packetSize
* @throws Exception
* @author: tanyaowu
*/
@Override
public void onAfterDecoded(ChannelContext channelContext, Packet packet, int packetSize) throws Exception {
}
/**
* 接收到TCP层传过来的数据后
*
* @param channelContext
* @param receivedBytes 本次接收了多少字节
* @throws Exception
*/
@Override
public void onAfterReceivedBytes(ChannelContext channelContext, int receivedBytes) throws Exception {
}
/**
* 消息包发送之后触发本方法
*
* @param channelContext
* @param packet
* @param isSentSuccess true:发送成功,false:发送失败
* @throws Exception
* @author tanyaowu
*/
@Override
public void onAfterSent(ChannelContext channelContext, Packet packet, boolean isSentSuccess) throws Exception {
}
/**
* 处理一个消息包后
*
* @param channelContext
* @param packet
* @param cost 本次处理消息耗时,单位:毫秒
* @throws Exception
*/
@Override
public void onAfterHandled(ChannelContext channelContext, Packet packet, long cost) throws Exception {
}
/**
* 连接关闭前触发本方法
*
* @param channelContext the channelcontext
* @param throwable the throwable 有可能为空
* @param remark the remark 有可能为空
* @param isRemove
* @throws Exception
* @author tanyaowu
*/
@Override
public void onBeforeClose(ChannelContext channelContext, Throwable throwable, String remark, boolean isRemove) throws Exception {
}
}
package com.larryxiang.p2p;
import org.tio.client.ClientChannelContext;
import java.util.Map;
/**
* @author: larry.xiang
* @description: p2p网络的连接信息
* 这个里面有:
* 1. 自己作为服务端所连接的client信息
* 2. 自己作为客户端与server的上下文
*/
public class P2PConnectionMsg {
/**
* 代表结点的client
*/
public static Map<Integer,ClientChannelContext> CLIENTS;
}
package com.larryxiang.util;
import com.larryxiang.config.AllNodeCommonMsg;
import com.larryxiang.dao.node.Node;
import com.larryxiang.dao.pbft.MsgType;
import com.larryxiang.dao.pbft.PbftMsg;
import lombok.extern.slf4j.Slf4j;
/**
* @author: larry.xiang
* @description: pbft的工作流程
* this is the most important thing
* 这个是整个算法的流程
*/
@Slf4j
public class Pbft {
private Node node = Node.getInstance();
/**
* 发送view请求
*
* @return
*/
public boolean pubView() {
TestUtil.startTime = System.currentTimeMillis();
/**
* 如果区块链中的网络节点小于3
*/
if (AllNodeCommonMsg.allNodeAddressMap.size() < 3) {
log.warn("区块链中的节点小于等于3");
node.setViewOK(true);
// 将节点消息广播出去
ClientUtil.publishIpPort(node.getIndex(), node.getAddress().getIp(), node.getAddress().getPort());
return true;
}
log.info("结点开始进行view同步操作");
// 初始化view的msg
PbftMsg view = new PbftMsg(MsgType.GET_VIEW, node.getIndex());
ClientUtil.clientPublish(view);
return true;
}
/**
* 视图发送该表
*
* @return
*/
public boolean changeView() {
return true;
}
}
import com.larryxiang.config.StartConfig;
import com.larryxiang.dao.node.Node;
import com.larryxiang.dao.node.NodeAddress;
import com.larryxiang.util.StartPbft;
import lombok.extern.slf4j.Slf4j;
/**
* @author: larry.xiang
* @description: 程序运行开始类
* 启动参数顺序:ip,port,index,认证请求消息
*/
@Slf4j
public class Main {
public static void main(String[] args) {
String ip = null;
int port = 0;
int index = 0;
if (args.length == 0) {
int i = 0;
ip = "127.0.0.1";
port = 8080 + i;
StartConfig.basePath = "C:\\data\\";
index = i;
} else if (args.length == 4) {
//程序启动ip地址
ip = args[0];
//端口
port = Integer.parseInt(args[1]);
//程序启动index
index = Integer.parseInt(args[2]);
//文件保存位置,在文件保存位置必须存在一个oldIp.json的文件
StartConfig.basePath = args[3];
}
Node node = Node.getInstance();
node.setIndex(index);
NodeAddress nodeAddress = new NodeAddress();
nodeAddress.setIp(ip);
nodeAddress.setPort(port);
node.setAddress(nodeAddress);
StartPbft.start();
}
}
部分代码参考:
https://gitee.com/tianyalei/md_blockchain
其实实现起来还是比较简单的,如果有任何问题,欢迎在评论区下方留言,或者私信我。