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

开发之路 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;
????}


????/**
?????*?获取前一个区块
?????*/
????public?Block?getPrevBlock()?throws?FileNotFoundException,?ClassNotFoundException,?IOException?{
????????if(this.hasPrevBlock())
????????????return?Storage.Deserialize(this.getBlkNum()-1);
????????return?null;??????????
????}

相关推荐