zoukankan      html  css  js  c++  java
  • 搭建你的第一个区块链网络(二):Pow与本地化

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

    共识与本地化

    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

  • 相关阅读:
    洛谷 P2831 [NOIP2016]愤怒的小鸟
    洛谷 P1736 创意吃鱼法
    洛谷 P2347 砝码称重 + bitset简析
    洛谷 P3384 [模板] 树链剖分
    洛谷 P1038 [NOIP2012] 借教室
    洛谷 P3959 [NOIP2017]宝藏 题解
    洛谷 AT2167 Blackout 题解
    洛谷 P1246 编码 题解
    C#中ref关键字的用法总结
    C#中的值传递与引用传递(in、out、ref)
  • 原文地址:https://www.cnblogs.com/cbkj-xd/p/12904660.html
Copyright © 2011-2022 走看看