java实现区块链联盟链中的PBFT拜占庭算法中节点加入view同步及共识过程

前言

在联盟链中,联盟各个节点往往都来自同一行业,有着共同的行业困扰和痛点,因此联盟链往往注重对实际问题的高效解决。而 公链中PoW 算法相对低效且费时费力,因此在联盟链中并不适用。相反在公链中很小适用的PBFT 算法在联盟链中却有用武之地。因此我们来研究研究。

一、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: 观察者节点,负责从共识节点或副本节点获取最新区块,执行并验证区块执行结果后,将产生的区块上链。

节点ID && 节点索引

为了防止节点作恶,PBFT共识过程中每个共识节点均对其发送的消息进行签名,对收到的消息包进行验签名,因此每个节点均维护一份公私钥对,私钥用于对发送的消息进行签名,公钥作为节点ID,用于标识和验签。

节点ID : 共识节点签名公钥和共识节点唯一标识, 一般是64字节二进制串,其他节点使用消息包发送者的节点ID对消息包进行验签

考虑到节点ID很长,在共识消息中包含该字段会耗费部分网络带宽,FISCO BCOS引入了节点索引,每个共识节点维护一份公共的共识节点列表,节点索引记录了每个共识节点ID在这个列表中的位置,发送网络消息包时,只需要带上节点索引,其他节点即可以从公共的共识节点列表中索引出节点的ID,进而对消息进行验签:

节点索引 : 每个共识节点ID在这个公共节点ID列表中的位置

视图(view)

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三个阶段:

  • Pre-prepare:负责执行区块,产生签名包,并将签名包广播给所有共识节点;
  • Prepare:负责收集签名包,某节点收集满2*f+1的签名包后,表明自身达到可以提交区块的状态,开始广播Commit包;
  • Commit:负责收集Commit包,某节点收集满2*f+1的Commit包后,直接将本地缓存的最新区块提交到数据库。

在这里插入图片描述

下图详细介绍了PBFT各个阶段的具体流程:

在这里插入图片描述

leader打包区块

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包后,开始进行三阶段共识。

pre-prepare阶段

共识节点收到Prepare包后,进入pre-prepare阶段,此阶段的主要工作流程包括:

  • Prepare包合法性判断:主要判断是否是重复的Prepare包、Prepare请求中包含的区块父哈希是否是当前节点最高块哈希(防止分叉)、Prepare请求中包含区块的块高是否等于最高块高加一;

  • 缓存合法的Prepare包: 若Prepare请求合法,则将其缓存到本地,用于过滤重复的Prepare请求;

  • 空块判断:若Prepare请求包含的区块中交易数目是0,则触发空块视图切换,将当前视图加一,并向所有其他节点广播视图切换请求;

  • 执行区块并缓存区块执行结果: 若Prepare请求包含的区块中交易数目大于0,则调用BlockVerifier区块执行器执行区块,并缓存执行后的区块;

  • 产生并广播签名包:基于执行后的区块哈希,产生并广播签名包,表明本节点已经完成区块执行和验证。

Prepare阶段

共识节点收到签名包后,进入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包合法性判断:主要判断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

二、了解了原理,我们自己动手来写

1.引入maven依赖

<?xml version="1.0" encoding="UTF-8"?>
<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.0</modelVersion>

    <groupId>com.larryxiang</groupId>
    <artifactId>PBFT</artifactId>
    <version>1.0-SNAPSHOT</version>
    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
        <maven.compiler.encoding>UTF-8</maven.compiler.encoding>
    </properties>

    <build>
        <finalName>pbft</finalName>

        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <configuration>
                    <source>8</source>
                    <target>8</target>
                </configuration>
            </plugin>
        </plugins>

    </build>
    <dependencies>
        <!-- 添加工具包-->
        <dependency>
            <groupId>org.iq80.leveldb</groupId>
            <artifactId>leveldb</artifactId>
            <version>0.12</version>
        </dependency>

        <dependency>
            <groupId>org.iq80.leveldb</groupId>
            <artifactId>leveldb-api</artifactId>
            <version>0.12</version>
        </dependency>


        <!--hutool 工具包,简化操作-->
        <dependency>
            <groupId>cn.hutool</groupId>
            <artifactId>hutool-all</artifactId>
            <version>5.0.7</version>
        </dependency>
        <!--阿里巴巴fastjson-->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>1.2.62</version>
        </dependency>

        <!--t-io-->
        <dependency>
            <groupId>org.t-io</groupId>
            <artifactId>tio-core</artifactId>
            <version>3.5.8.v20191228-RELEASE</version>
        </dependency>

        <!--lombok-->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.18.10</version>
            <scope>provided</scope>
        </dependency>
        <dependency>
            <groupId>org.apache.logging.log4j</groupId>
            <artifactId>log4j-core</artifactId>
            <version>2.13.2</version>
        </dependency>
        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-log4j12</artifactId>
            <version>2.0.0-alpha1</version>
        </dependency>

        <!-- 谷歌官方的包-->
        <dependency>
            <groupId>com.google.guava</groupId>
            <artifactId>guava</artifactId>
            <version>28.2-jre</version>
        </dependency>


    </dependencies>


</project>

2.核心代码




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

总结

其实实现起来还是比较简单的,如果有任何问题,欢迎在评论区下方留言,或者私信我。

猜你喜欢

转载自blog.csdn.net/ws327443752/article/details/112280438