BFT共识协议Java简单实现

关于拜占庭容错

拜占庭容错指的是若干节点之间要达成信息一致,但是其中节点可能会作恶,比如发送不一致的假消息,这一点与Raft等以前熟知的非拜占庭容错协议不一样,非拜占庭容错场景里的节点可能会故障,但不会故意作恶。
BFT协议里要求有3N+1个节点,那么如果诚实节点达到2N+1,则整个节点网络可以达到共识。
分为3个阶段:PRE-PREPARE, PREPARE, COMMIT

代码

public class Message {
    enum Type {PRE_PREPARE, PREPARE, COMMIT};
    Type type; //消息类型
    int viewNumber; //任期,主节点不换任期不变
    int seqNumber; //消息在某任期下的序列号
    String digest; //消息摘要
    int senderId; //发送者ID,消息是谁发的

    public Message(Type type, int viewNumber, int seqNumber, String digest, int senderId){
        this.type  = type;
        this.viewNumber = viewNumber;
        this.seqNumber = seqNumber;
        this.digest = digest;
        this.senderId = senderId;
    }
import com.alibaba.fastjson2.JSON;

import java.util.HashMap;
import java.util.Queue;
import java.util.concurrent.LinkedBlockingQueue;

/**
 * 模拟节点
 * */
public class Node implements Runnable{
    int id;
    public boolean primary;
    //List<Node> allNodes; // 模拟广播
    Queue<Message> preQueue = new LinkedBlockingQueue<Message>();
    Queue<Message> prepareQueue = new LinkedBlockingQueue<Message>();
    Queue<Message> commitQueue = new LinkedBlockingQueue<Message>();

    HashMap<String, Integer> msgMap = new HashMap<>(); //key viewnumber+seqNubmer+type+digest  value count
    HashMap<String, Boolean> msgBroadMap = new HashMap<>(); //key viewnumber+seqNubmer+type+digest  value bool


    public Node (int id, boolean primary){
        this.id = id;
        this.primary = primary;
    }

    //接收消息
    void receive(Message message) {
        try {
            Thread.sleep(200);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        String key =  getMessageKey(message);
        if(null==msgMap.get(key)){
                msgMap.put(key, 1);
            }else {
                msgMap.put(key, msgMap.get(key)+1);
            }
        msgBroadMap.put(key, false);

        //System.out.println(msgMap);
        //System.out.println(msgBroadMap);

        switch (message.type){
            case PRE_PREPARE: preQueue.add(message); break;
            case PREPARE: prepareQueue.add(message); break;
            case COMMIT: commitQueue.add(message); break;
        }
    }

    private boolean isMessageTrue(Message msg){
        String key = getMessageKey(msg);
        if(null!=msgMap.get(key)){
            if(msgMap.get(key) >= 3) return true;
        }
        return false;
    }

    // 节点收PRE_PREPARE消息,到网络中广播 prepare消息
    void handlePrePrepare(Message msg) {
        System.out.println(this.id + "节点收到PRE-PREPARE消息" + JSON.toJSONString(msg));
        NetwokContext.broadcast(msg, Message.Type.PREPARE, this.id);
        String key = getMessageKey(msg);
        msgBroadMap.put(key, true);
    }

    // 节点收到prepare消息,判断一致的消息数量是否达到2n+1,是则广播commit消息
    void handlePrepare(Message msg) {
        String key = getMessageKey(msg);
        if(isMessageTrue(msg) && !msgBroadMap.get(key)){
            System.out.println(this.id + "节点收到PREPARE消息" + JSON.toJSONString(msg) + "后,已收到满足数量的一致消息,广播COMMIT消息");
            NetwokContext.broadcast(msg, Message.Type.COMMIT, this.id);
            msgBroadMap.put(key, true);
        }
    }

    //节点收到commit消息,判断一致的消息数量是否达到2n+1,是则最终提交
    void handleCommit(Message msg){
        String key = getMessageKey(msg);
        if(isMessageTrue(msg) && !msgBroadMap.get(key)) {
            System.out.println(this.id + "节点收到COMMIT消息" + JSON.toJSONString(msg) + "后,已收到满足数量的一致消息,进行本地提交执行");
            msgBroadMap.put(key, true);
        }
    }


    @Override
    public void run() {
        while (true){
            if(!preQueue.isEmpty()) handlePrePrepare(preQueue.poll());
            if(!prepareQueue.isEmpty()) handlePrepare(prepareQueue.poll());
            if(!commitQueue.isEmpty()) handleCommit(commitQueue.poll());
            try {
                Thread.sleep(10);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
    }

    private String getMessageKey(Message msg){
        return msg.viewNumber+""+ msg.seqNumber + "" + msg.type + msg.digest;
    }
}

上面是消息类和节点类,接下来是这个4节点网络环境的模拟上下文,以及测试程序。

/**
 * 模拟节点中保存的网络上下文
 * */
public class NetwokContext {
    public static List<Node> allNodes = new ArrayList<>(); // 模拟广播

    public static void broadcast(Message msg, Message.Type type,int senderId){
        for(Node node : allNodes){
            Message amsg = new Message(type, msg.viewNumber, msg.seqNumber, msg.digest, senderId);
            node.receive(amsg);
        }
    }
}
public class PBFTdemo {
    public static void main(String[] args) throws Exception{
        // 模拟 4 个节点,其中 Node0 是主节点
        Node node0 = new Node(0, true);
        Node node1 = new Node(1, false);
        Node node2 = new Node(2, false);
        Node node3 = new Node(3, false);
        NetwokContext.allNodes.add(node0);
        NetwokContext.allNodes.add(node1);
        NetwokContext.allNodes.add(node2);
        NetwokContext.allNodes.add(node3);
        Thread thread0 = new Thread(node0);
        Thread thread1 = new Thread(node1);
        Thread thread2 = new Thread(node2);
        Thread thread3 = new Thread(node3);
        thread0.setDaemon(true);
        thread1.setDaemon(true);
        thread2.setDaemon(true);
        thread3.setDaemon(true);
        thread0.start();
        thread1.start();
        thread2.start();
        thread3.start();

        case1();
        //case2();

        TimeUnit.SECONDS.sleep(10);
    }
    //正常情况
    private static void case1(){
        String digest = "提刀上洛"; // 简化
        Message prePrepare = new Message(Message.Type.PRE_PREPARE, 0, 1, digest, 0);
        for (Node node : NetwokContext.allNodes) {
            node.receive(prePrepare);
        }
    }
    //主节点作恶
    private static void case2(){
        NetwokContext.allNodes.get(0).receive(new Message(Message.Type.PRE_PREPARE, 0, 1, "提刀上洛", 0));
        NetwokContext.allNodes.get(1).receive(new Message(Message.Type.PRE_PREPARE, 0, 1, "提刀上洛", 0));
        NetwokContext.allNodes.get(2).receive(new Message(Message.Type.PRE_PREPARE, 0, 1, "跑路", 0));
        NetwokContext.allNodes.get(3).receive(new Message(Message.Type.PRE_PREPARE, 0, 1, "跑路", 0));
    }
}

case1运行结果:

1节点收到PRE-PREPARE消息{"digest":"提刀上洛","senderId":0,"seqNumber":1,"type":"PRE_PREPARE","viewNumber":0}
0节点收到PRE-PREPARE消息{"digest":"提刀上洛","senderId":0,"seqNumber":1,"type":"PRE_PREPARE","viewNumber":0}
2节点收到PRE-PREPARE消息{"digest":"提刀上洛","senderId":0,"seqNumber":1,"type":"PRE_PREPARE","viewNumber":0}
3节点收到PRE-PREPARE消息{"digest":"提刀上洛","senderId":0,"seqNumber":1,"type":"PRE_PREPARE","viewNumber":0}
0节点收到PREPARE消息{"digest":"提刀上洛","senderId":0,"seqNumber":1,"type":"PREPARE","viewNumber":0}后,已收到满足数量的一致消息,广播COMMIT消息
1节点收到PREPARE消息{"digest":"提刀上洛","senderId":1,"seqNumber":1,"type":"PREPARE","viewNumber":0}后,已收到满足数量的一致消息,广播COMMIT消息
2节点收到PREPARE消息{"digest":"提刀上洛","senderId":1,"seqNumber":1,"type":"PREPARE","viewNumber":0}后,已收到满足数量的一致消息,广播COMMIT消息
3节点收到PREPARE消息{"digest":"提刀上洛","senderId":0,"seqNumber":1,"type":"PREPARE","viewNumber":0}后,已收到满足数量的一致消息,广播COMMIT消息
0节点收到COMMIT消息{"digest":"提刀上洛","senderId":1,"seqNumber":1,"type":"COMMIT","viewNumber":0}后,已收到满足数量的一致消息,进行本地提交执行
1节点收到COMMIT消息{"digest":"提刀上洛","senderId":1,"seqNumber":1,"type":"COMMIT","viewNumber":0}后,已收到满足数量的一致消息,进行本地提交执行
2节点收到COMMIT消息{"digest":"提刀上洛","senderId":0,"seqNumber":1,"type":"COMMIT","viewNumber":0}后,已收到满足数量的一致消息,进行本地提交执行
3节点收到COMMIT消息{"digest":"提刀上洛","senderId":1,"seqNumber":1,"type":"COMMIT","viewNumber":0}后,已收到满足数量的一致消息,进行本地提交执行

case2运行结果:

0节点收到PRE-PREPARE消息{"digest":"提刀上洛","senderId":0,"seqNumber":1,"type":"PRE_PREPARE","viewNumber":0}
1节点收到PRE-PREPARE消息{"digest":"提刀上洛","senderId":0,"seqNumber":1,"type":"PRE_PREPARE","viewNumber":0}
2节点收到PRE-PREPARE消息{"digest":"跑路","senderId":0,"seqNumber":1,"type":"PRE_PREPARE","viewNumber":0}
3节点收到PRE-PREPARE消息{"digest":"跑路","senderId":0,"seqNumber":1,"type":"PRE_PREPARE","viewNumber":0}

posted on 2025-07-25 15:09  肥兔子爱豆畜子  阅读(11)  评论(0)    收藏  举报

导航