搭建你的第一个区块链网络(二)

前一篇文章: 搭建你的第一个区块链网络(一)

共识与本地化

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;          
    }