搭建你的第一個區塊鏈網路(二)

觸不可及`發表於2020-05-17

前一篇文章: 搭建你的第一個區塊鏈網路(一)

共識與本地化

POW共識

共識機制也是區塊鏈系統中不可缺少的一部分,在比特幣網路中,使用的是POW共識,概念相對比較簡單,所以我們在該專案中使用POW共識機制(後期如果可以的話修改為可插拔的共識機制)。

POW原理

POW原理是通過解決一個數學難題,其實就是通過計算一個雜湊值,如果計算出來的雜湊值的字首有足夠多個"0",就說明成功解決了該數學難題。通常雜湊值中"0"的個數越多難度越大。難度值是通過之前生成的區塊所消耗的時間動態調整的。而生成雜湊值的原資料實際上就是區塊資訊,另外再加一個nonce屬性,用於調整難度值。
在比特幣中,平均每10分鐘產出一個區塊,如果新區塊的產出只消耗了9分鐘,那麼難度值將會增加。如果算力不發生變化的話,下一次產出區塊將會消耗更多的時間。同理,如果新區塊的產出消耗了11分鐘,那麼難度值則會相應地降低。動態調整難度值維持區塊產出時間平均為10分鐘。實際上比特幣中的POW更加複雜,難度值的調整是通過過去的2016個區塊產出的時間與20160分鐘進行比較的。
在這裡,不設定那麼麻煩,難度值不再動態調整,暫時將雜湊值中"0"的數量固定保證每次生成區塊的難度是相同的。同時也要設定一個最大難度值,防止無限迴圈計算。

#Pow.java
public class Pow {
    //固定的難度值
    private static final String DIFFICULT = "0000";
    //最大難度值 防止計算難度值變為無限迴圈
    private static final int MAX_VALUE = Integer.MAX_VALUE;
    public static int calc(Block block){
        //nonce從0開始
        int nonce = 0;
        //如果nonce小於最大難度值
        while(nonce<MAX_VALUE){
            //計算雜湊值
            if(Util.getSHA256(block.toString()+nonce)
                    //如果計算出的雜湊值字首滿足條件,退出迴圈
                    .startsWith(DIFFICULT))
                break;
            //不滿足條件,nonce+1,重新計算雜湊值
            nonce++;
        }
        return nonce;
    }
}

更新屬性

一個簡單的POW共識完成了,接下來需要更新一下區塊的屬性,新增nonce屬性:

#Block.java
    //產出該區塊的難度
    public int nonce;

還要修改生成區塊的方法,每次生成區塊時需要進行POW共識計算:

    public Block CrtGenesisBlock(){
        Block block = new Block(1,"Genesis Block","00000000000000000");
        block.setNonce(
            Pow.calc(block));
        //計算區塊雜湊值
        String hash = Util.getSHA256(block.getBlkNum()+block.getData()+block.getPrevBlockHash()+block.getPrevBlockHash()+block.getNonce());
        ...
    }
    public Block addBlock(String data){
        ...
        Block block = new Block(
            num+1,data, this.block.curBlockHash);
        //每次將區塊新增進區塊鏈之前需要計算難度值
        block.setNonce(
            Pow.calc(block));
        //計算區塊雜湊值
        String hash = Util.getSHA256(block.getBlkNum()+block.getData()+block.getPrevBlockHash()+block.getPrevBlockHash()+block.getNonce());
        ...
    }

測試POW共識

OK了,還是之前的測試方法,測試一下:

#Test.java
public class Test {
    public static void main(String[] args){
        System.out.println(Blockchain.getInstance().CrtGenesisBlock().toString());
        System.out.println(Blockchain.getInstance().addBlock("Block 2").toString());
    }
}

可以看到區塊號為2的區塊nonce屬性有了具體的值,並且每次測試curBlockHash的值字首都是以"0000"開頭的。

{"blkNum":1,"curBlockHash":"000002278a13f6caefda04c77d35e14128aafbc287578b86e1f2079c0e6747b1","data":"Genesis Block","nonce":37846,"prevBlockHash":"00000000000000000","timeStamp":"2020-05-17 10:49:48"}
{"blkNum":2,"curBlockHash":"00002654109d8eb6092da686d66e70cdb1e26cf4a87e453e3d8e2ff7508f11f9","data":"Block 2","nonce":15318,"prevBlockHash":"000002278a13f6caefda04c77d35e14128aafbc287578b86e1f2079c0e6747b1","timeStamp":"2020-05-17 10:49:48"}

本地化

此外,每次重新啟動程式都需要從創世區塊重新開始生成,所以需要將區塊資訊序列化到本地。保證每次啟動程式都可以從本地讀取資料不再重新生成創世區塊。

方便起見,暫時不使用資料庫儲存區塊資訊,只簡單序列化到本地檔案中來。
首先需要修改區塊的資訊,繼承Serializable介面才能進行序列化。

#Block.java
public class Block implements Serializable{
    private static final long serialVersionUID = 1L;
    ...
}

序列化與反序列化

接下來是序列化與反序列化的方法,在這裡我們將每一個區塊都儲存為一個名字為區塊號,字尾為.block的檔案,同樣從本地反序列化到程式中也只需要通過區塊號來取。

#Storage.java
public final class Storage {
     //序列化區塊資訊
     public static void Serialize(Block block) throws IOException {
        File file = new File("src/main/resources/blocks/"+block.getBlkNum()+".block");
        if(!file.exists()) file.createNewFile();
        FileOutputStream fos = new FileOutputStream(file);
        ObjectOutputStream oos = new ObjectOutputStream(fos);
        
        oos.writeObject(block);
        oos.close();
        fos.close();
    }
    /**
     * 反序列化區塊
     */
    public static Block Deserialize(int num) throws FileNotFoundException, IOException, ClassNotFoundException {
        File file = new File("src/main/resources/blocks/"+num+".block");
        if(!file.exists()) return null;
        ObjectInputStream ois = new ObjectInputStream(new FileInputStream(file));
        
        Block block = (Block)ois.readObject();
        ois.close();
        return block;
    }
}

然後是區塊鏈的屬性,之前我們使用ArrayList儲存區塊資訊,而現在我們直接將區塊序列化到本地,需要哪一個區塊直接到本地來取,因此不再需要ArrayList儲存區塊資料。對於區塊鏈來講,僅僅需要記錄最新區塊資料即可。


public final class Blockchain {
    ...
    //Arraylist<Block> block修改為 Block block;
    public Block block;
    ...
    public static Blockchain getInstance() {
        if (BC == null) {
            synchronized (Blockchain.class) {
                if (BC == null) {
                    BC = new Blockchain();
                    //刪除建立ArrayList
                }
            }
        }
        return BC;
    }

    public Block CrtGenesisBlock() throws IOException {
        ...
        block.setCurBlockHash(hash);
        //序列化
        Storage.Serialize(block);
        this.block=block;
        return this.block;
    }
    public Block addBlock(String data) throws IOException {
        int num = this.block.getBlkNum();
        ...
        block.setCurBlockHash(hash);
        //序列化
        Storage.Serialize(block);
        this.block = block;
        return this.block;
    }
}

測試一下:

public class Test {
    public static void main(String[] args) throws IOException {
        System.out.println(Blockchain.getInstance().CrtGenesisBlock().toString());
        System.out.println(Blockchain.getInstance().addBlock("Block 2").toString());
    }
}

儲存是沒有問題的,在resources/blocks/檔案下成功生成了1.block,2.block兩個檔案。

反序列化

但是還沒有完成從本地取資料的操作,接下來的流程是這樣子的:
啟動程式後,首先例項化Blockchain的例項,然後從本地讀取資料,如果本地存在區塊資料,直接反序列化區塊號最大的區塊,如果本地沒有資料,則進行創始區塊的建立。

#Blockchain.java
public Block getLastBlock() throws FileNotFoundException, ClassNotFoundException, IOException {
        File file = new File("src/main/resources/blocks");
        String[] files = file.list();
        if(files.length!=0){
            int MaxFileNum = 1;
            //遍歷儲存區塊資料的資料夾,查詢區塊號最大的區塊
            for(String s:files){
                int num = Integer.valueOf(s.substring(0, 1));
                if(num>=MaxFileNum)
                    MaxFileNum = num;
            }
            //反序列化最大區塊號的區塊
           return Storage.Deserialize(MaxFileNum);
        }
        return null;
    }

然後是Blockchain的例項方法,在獲取例項時候判斷是否需要建立創世區塊:

#Blockchain.java
    public static Blockchain getInstance() throws FileNotFoundException, ClassNotFoundException, IOException {
        if (BC == null) {
            synchronized (Blockchain.class) {
                if (BC == null) {
                    BC = new Blockchain();
                }
            }
        }
        //獲取到Blockchain例項後,判斷是否存在區塊
        if(BC.block==null){
            //如果不存在則嘗試獲取本地區塊號最大的區塊
            //如果存在則直接賦值到Blockchain的屬性然後返回
            Block block = BC.getLastBlock();
            BC.block = block;
            if(block==null){
                //如果不存在則生成創世區塊
                BC.CrtGenesisBlock();
            }
        }
        return BC;
    }
    
    //因此建立創世區塊的方法可以修改為私有的
    private Block CrtGenesisBlock() throws IOException {
        ...
    }

接下來可以測試了:

public class Test {
    public static void main(String[] args) throws IOException, ClassNotFoundException {
        System.out.println(Blockchain.getInstance().block.toString());
        System.out.println(Blockchain.getInstance().addBlock("Block 2").toString());
    }
}

測試多次可以發現區塊並沒有重新從創世區塊開始生成,而是根據先前生成的區塊號繼續增長。

{"blkNum":1,"curBlockHash":"000002278a13f6caefda04c77d35e14128aafbc287578b86e1f2079c0e6747b1","data":"Genesis Block","nonce":37846,"prevBlockHash":"00000000000000000","timeStamp":"2020-05-17 11:51:37"}
{"blkNum":2,"curBlockHash":"00002654109d8eb6092da686d66e70cdb1e26cf4a87e453e3d8e2ff7508f11f9","data":"Block 2","nonce":15318,"prevBlockHash":"000002278a13f6caefda04c77d35e14128aafbc287578b86e1f2079c0e6747b1","timeStamp":"2020-05-17 11:51:37"}

Current Last Block num is:2
{"blkNum":2,"curBlockHash":"00002654109d8eb6092da686d66e70cdb1e26cf4a87e453e3d8e2ff7508f11f9","data":"Block 2","nonce":15318,"prevBlockHash":"000002278a13f6caefda04c77d35e14128aafbc287578b86e1f2079c0e6747b1","timeStamp":"2020-05-17 11:51:37"}
{"blkNum":3,"curBlockHash":"0000d350c1199eb51c2d43194653f5b44444665e40373d5883edd3567c60cd68","data":"Block 2","nonce":23695,"prevBlockHash":"00002654109d8eb6092da686d66e70cdb1e26cf4a87e453e3d8e2ff7508f11f9","timeStamp":"2020-05-17 11:51:44"}

大致工作已完成,接下來新增幾個額外的方法:

#Block.java
       /**
     * 是否存在前一個區塊
     */
    public boolean hasPrevBlock(){
        if(this.getBlkNum()!=1){
            return true;
        }
        return false;
    }
    @Transient
    @JsonIgnore
    /**
     * 獲取前一個區塊
     */
    public Block getPrevBlock() throws FileNotFoundException, ClassNotFoundException, IOException {
        if(this.hasPrevBlock())
            return Storage.Deserialize(this.getBlkNum()-1);
        return null;          
    }

後一篇文章: 搭建你的第一個區塊鏈網路(三)

Github倉庫地址在這裡,隨時保持更新中.....

Github地址:Jchain

相關文章