美文网首页计算机-区块链区块链研习社
基于Java语言构建区块链(三)—— 持久化 & 命令行

基于Java语言构建区块链(三)—— 持久化 & 命令行

作者: wangwei_hz | 来源:发表于2018-02-27 18:43 被阅读281次
    blockchain

    最终内容请以原文为准:https://wangwei.one/posts/7890ab7e.html

    引言

    上一篇文章我们实现了区块链的工作量证明机制(Pow),尽可能地实现了挖矿。但是距离真正的区块链应用还有很多重要的特性没有实现。今天我们来实现区块链数据的存储机制,将每次生成的区块链数据保存下来。有一点需要注意,区块链本质上是一款分布式的数据库,我们这里不实现"分布式",只聚焦于数据存储部分。

    数据库选择

    到目前为止,我们的实现机制中还没有区块存储这一环节,导致我们的区块每次生成之后都保存在了内存中。这样不便于我们重新使用区块链,每次都要从头开始生成区块,也不能够跟他人共享我们的区块链,因此,我们需要将其存储在磁盘上。

    我们该选择哪一款数据库呢?事实上,在《比特币白皮书》中并没有明确指定使用哪一种的数据库,因此这个由开发人员自己决定。中本聪 开发的 Bitcoin Core 中使用的是LevelDB。原文 Building Blockchain in Go. Part 3: Persistence and CLI 中使用的是 BoltDB ,对Go语言支持比较好。

    但是我们这里使用的是Java来实现,BoltDB不支持Java,这里我们选用 Rocksdb

    RocksDB是由Facebook数据库工程团队开发和维护的一款key-value存储引擎,比LevelDB性能更加强大,有关Rocksdb的详细介绍,请移步至官方文档:https://github.com/facebook/rocksdb ,这里不多做介绍。

    数据结构

    在我们开始实现数据持久化之前,我们先要确定我们该如何去存储我们的数据。为此,我们先来看看比特币是怎么做的。

    简单来讲,比特币使用了两个"buckets(桶)"来存储数据:

    • blocks. 描述链上所有区块的元数据.
    • chainstate. 存储区块链的状态,指的是当前所有的UTXO(未花费交易输出)以及一些元数据.

    “在比特币的世界里既没有账户,也没有余额,只有分散到区块链里的UTXO。”

    详见:《精通比特币》第二版 第06章节 —— 交易的输入与输出

    此外,每个区块数据都是以单独的文件形式存储在磁盘上。这样做是出于性能的考虑:当读取某一个单独的区块数据时,不需要加载所有的区块数据到内存中来。

    blocks 这个桶中,存储的键值对:

    • 'b' + 32-byte block hash -> block index record

      区块的索引记录

    • 'f' + 4-byte file number -> file information record

      文件信息记录

    • 'l' -> 4-byte file number: the last block file number used

      最新的一个区块所使用的文件编码

    • 'R' -> 1-byte boolean: whether we're in the process of reindexing

      是否处于重建索引的进程当中

    • 'F' + 1-byte flag name length + flag name string -> 1 byte boolean: various flags that can be on or off

      各种可以打开或关闭的flag标志

    • 't' + 32-byte transaction hash -> transaction index record

      交易索引记录

    chainstate 这个桶中,存储的键值对:

    • 'c' + 32-byte transaction hash -> unspent transaction output record for that transaction

      某笔交易的UTXO记录

    • 'B' -> 32-byte block hash: the block hash up to which the database represents the unspent transaction outputs

      数据库所表示的UTXO的区块Hash(抱歉,这一点我还没弄明白……)

    由于我们还没有实现交易相关的特性,因此,我们这里只使用 block 桶。另外,前面提到过的,这里我们不会实现各个区块数据各自存储在独立的文件上,而是统一存放在一个文件里面。因此,我们不要存储和文件编码相关的数据,这样一来,我们所用到的键值对就简化为:

    • 32-byte block-hash -> Block structure (serialized)

      区块数据与区块hash的键值对

    • 'l' -> the hash of the last block in a chain

      最新一个区块hash的键值对

    序列化

    RocksDB的Key与Value只能以byte[]的形式进行存储,这里我们需要用到序列化与反序列化库 Kryo,代码如下:

    package one.wangwei.blockchain.util;
    
    import com.esotericsoftware.kryo.Kryo;
    import com.esotericsoftware.kryo.io.Input;
    import com.esotericsoftware.kryo.io.Output;
    
    /**
     * 序列化工具类
     *
     * @author wangwei
     * @date 2018/02/07
     */
    public class SerializeUtils {
    
        /**
         * 反序列化
         *
         * @param bytes 对象对应的字节数组
         * @return
         */
        public static Object deserialize(byte[] bytes) {
            Input input = new Input(bytes);
            Object obj = new Kryo().readClassAndObject(input);
            input.close();
            return obj;
        }
    
        /**
         * 序列化
         *
         * @param object 需要序列化的对象
         * @return
         */
        public static byte[] serialize(Object object) {
            Output output = new Output(4096, -1);
            new Kryo().writeClassAndObject(output, object);
            byte[] bytes = output.toBytes();
            output.close();
            return bytes;
        }
    }
    

    持久化

    上面已经说过,我们这里使用RocksDB,我们先写一个相关的工具类RocksDBUtils,主要的功能如下:

    • putLastBlockHash:保存最新一个区块的Hash值
    • getLastBlockHash:查询最新一个区块的Hash值
    • putBlock:保存区块
    • getBlock:查询区块

    注意:BoltDB 支持 Bucket 的特性,而RocksDB 不支持,我们这里采用统一前缀的方式进行处理。

    RocksDBUtils

    package one.wangwei.blockchain.util;
    
    import lombok.Getter;
    import one.wangwei.blockchain.block.Block;
    import org.rocksdb.Options;
    import org.rocksdb.RocksDB;
    import org.rocksdb.RocksDBException;
    
    /**
     * RocksDB 工具类
     *
     * @author wangwei
     * @date 2018/02/27
     */
    public class RocksDBUtils {
    
        /**
         * 区块链数据文件
         */
        private static final String DB_FILE = "blockchain.db";
        /**
         * 区块桶前缀
         */
        private static final String BLOCKS_BUCKET_PREFIX = "blocks_";
    
        private volatile static RocksDBUtils instance;
    
        public static RocksDBUtils getInstance() {
            if (instance == null) {
                synchronized (RocksDBUtils.class) {
                    if (instance == null) {
                        instance = new RocksDBUtils();
                    }
                }
            }
            return instance;
        }
    
        @Getter
        private RocksDB rocksDB;
    
        private RocksDBUtils() {
            initRocksDB();
        }
    
        /**
         * 初始化RocksDB
         */
        private void initRocksDB() {
            try {
                rocksDB = RocksDB.open(new Options().setCreateIfMissing(true), DB_FILE);
            } catch (RocksDBException e) {
                e.printStackTrace();
            }
        }
    
        /**
         * 保存最新一个区块的Hash值
         *
         * @param tipBlockHash
         */
        public void putLastBlockHash(String tipBlockHash) throws Exception {
            rocksDB.put(SerializeUtils.serialize(BLOCKS_BUCKET_PREFIX + "l"), SerializeUtils.serialize(tipBlockHash));
        }
    
        /**
         * 查询最新一个区块的Hash值
         *
         * @return
         */
        public String getLastBlockHash() throws Exception {
            byte[] lastBlockHashBytes = rocksDB.get(SerializeUtils.serialize(BLOCKS_BUCKET_PREFIX + "l"));
            if (lastBlockHashBytes != null) {
                return (String) SerializeUtils.deserialize(lastBlockHashBytes);
            }
            return "";
        }
    
        /**
         * 保存区块
         *
         * @param block
         */
        public void putBlock(Block block) throws Exception {
            byte[] key = SerializeUtils.serialize(BLOCKS_BUCKET_PREFIX + block.getHash());
            rocksDB.put(key, SerializeUtils.serialize(block));
        }
    
        /**
         * 查询区块
         *
         * @param blockHash
         * @return
         */
        public Block getBlock(String blockHash) throws Exception {
            byte[] key = SerializeUtils.serialize(BLOCKS_BUCKET_PREFIX + blockHash);
            return (Block) SerializeUtils.deserialize(rocksDB.get(key));
        }
    
    }
    
    

    创建区块链

    现在我们来优化 Blockchain.newBlockchain 接口的代码逻辑,改为如下逻辑:

    image

    代码如下:

    /**
      * <p> 创建区块链 </p>
      *
      * @return
      */
    public static Blockchain newBlockchain() throws Exception {
        String lastBlockHash = RocksDBUtils.getInstance().getLastBlockHash();
        if (StringUtils.isBlank(lastBlockHash)) {
            Block genesisBlock = Block.newGenesisBlock();
            lastBlockHash = genesisBlock.getHash();
            RocksDBUtils.getInstance().putBlock(genesisBlock);
            RocksDBUtils.getInstance().putLastBlockHash(lastBlockHash);
         }
         return new Blockchain(lastBlockHash);
    }
    

    修改 Blockchain 的数据结构,只记录最新一个区块链的Hash值

    public class Blockchain {
        
        @Getter
        private String lastBlockHash;
    
        private Blockchain(String lastBlockHash) {
            this.lastBlockHash = lastBlockHash;
        }
    }
    

    每次挖矿完成后,我们也需要将最新的区块信息保存下来,并且更新最新区块链Hash值:

    /**
     * <p> 添加区块  </p>
     *
     * @param data
     */
    public void addBlock(String data) throws Exception {
       String lastBlockHash = RocksDBUtils.getInstance().getLastBlockHash();
       if (StringUtils.isBlank(lastBlockHash)) {
           throw new Exception("Fail to add block into blockchain ! ");
       }
       this.addBlock(Block.newBlock(lastBlockHash, data));
    }
    
    /**
     * <p> 添加区块  </p>
     *
     * @param block
     */
    public void addBlock(Block block) throws Exception {
        RocksDBUtils.getInstance().putLastBlockHash(block.getHash());
        RocksDBUtils.getInstance().putBlock(block);
        this.lastBlockHash = block.getHash();
    }
    
    

    到此,存储部分的功能就实现完毕,我们还缺少一个功能:

    检索区块链

    现在,我们所有的区块都保存到了数据库,因此,我们能够重新打开已有的区块链并且向其添加新的区块。但这也导致我们再也无法打印出区块链中所有区块的信息,因为,我们没有将区块存储在数组当中。让我们来修复这个瑕疵!

    我们在Blockchain中创建一个内部内 BlockchainIterator ,作为区块链的迭代器,通过区块之前的hash连接来依次迭代输出区块信息,代码如下:

    public class Blockchain {
     
        ....
        
        /**
         * 区块链迭代器
         */
        public class BlockchainIterator {
    
            private String currentBlockHash;
    
            public BlockchainIterator(String currentBlockHash) {
                this.currentBlockHash = currentBlockHash;
            }
    
            /**
             * 是否有下一个区块
             *
             * @return
             */
            public boolean hashNext() throws Exception {
                if (StringUtils.isBlank(currentBlockHash)) {
                    return false;
                }
                Block lastBlock = RocksDBUtils.getInstance().getBlock(currentBlockHash);
                if (lastBlock == null) {
                    return false;
                }
                // 创世区块直接放行
                if (lastBlock.getPrevBlockHash().length() == 0) {
                    return true;
                }
                return RocksDBUtils.getInstance().getBlock(lastBlock.getPrevBlockHash()) != null;
            }
    
            
            /**
             * 返回区块
             *
             * @return
             */
            public Block next() throws Exception {
                Block currentBlock = RocksDBUtils.getInstance().getBlock(currentBlockHash);
                if (currentBlock != null) {
                    this.currentBlockHash = currentBlock.getPrevBlockHash();
                    return currentBlock;
                }
                return null;
            }
        }   
        
        ....    
    }
    

    测试

    /**
     * 测试
     *
     * @author wangwei
     * @date 2018/02/05
     */
    public class BlockchainTest {
    
        public static void main(String[] args) {
            try {
                Blockchain blockchain = Blockchain.newBlockchain();
    
                blockchain.addBlock("Send 1.0 BTC to wangwei");
                blockchain.addBlock("Send 2.5 more BTC to wangwei");
                blockchain.addBlock("Send 3.5 more BTC to wangwei");
    
                for (Blockchain.BlockchainIterator iterator = blockchain.getBlockchainIterator(); iterator.hashNext(); ) {
                    Block block = iterator.next();
    
                    if (block != null) {
                        boolean validate = ProofOfWork.newProofOfWork(block).validate();
                        System.out.println(block.toString() + ", validate = " + validate);
                    }
                }
    
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }
    
    
    /*输出*/
    
    Block{hash='0000012f87a0510dd0ee7048a6bd52db3002bae7d661126dc28287bd6c23189a', prevBlockHash='0000024b2c23c4fb06c2e2c1349275d415efe17a51db24cd4883da0067300ddf', data='Send 3.5 more BTC to wangwei', timeStamp=1519724875, nonce=369110}, validate = true
    Block{hash='0000024b2c23c4fb06c2e2c1349275d415efe17a51db24cd4883da0067300ddf', prevBlockHash='00000b14fefb51ba2a7428549d469bcf3efae338315e7289d3e6dc4caf589d79', data='Send 2.5 more BTC to wangwei', timeStamp=1519724872, nonce=896348}, validate = true
    Block{hash='00000b14fefb51ba2a7428549d469bcf3efae338315e7289d3e6dc4caf589d79', prevBlockHash='0000099ced1b02f40c750c5468bb8c4fd800ec9f46fea5d8b033e5d054f0f703', data='Send 1.0 BTC to wangwei', timeStamp=1519724869, nonce=673955}, validate = true
    Block{hash='0000099ced1b02f40c750c5468bb8c4fd800ec9f46fea5d8b033e5d054f0f703', prevBlockHash='', data='Genesis Block', timeStamp=1519724866, nonce=840247}, validate = true
    

    命令行界面

    CLI 部分的内容,这里不做详细介绍,具体可以去查看文末的Github源码链接。大致步骤如下:

    配置

    添加pom.xml配置

    <project>
       
        ...
        
        <dependency>
            <groupId>commons-cli</groupId>
            <artifactId>commons-cli</artifactId>
            <version>1.4</version>
        </dependency>
        
        ...
        
        <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-assembly-plugin</artifactId>
            <version>3.1.0</version>
            <configuration>
                <archive>
                    <manifest>
                        <addClasspath>true</addClasspath>
                        <classpathPrefix>lib/</classpathPrefix>
                        <mainClass>one.wangwei.blockchain.cli.Main</mainClass>
                    </manifest>
                </archive>
                <descriptorRefs>
                    <descriptorRef>jar-with-dependencies</descriptorRef>
                </descriptorRefs>
            </configuration>
            <executions>
                <execution>
                    <id>make-assembly</id>
                    <!-- this is used for inheritance merges -->
                    <phase>package</phase>
                    <!-- 指定在打包节点执行jar包合并操作 -->
                    <goals>
                        <goal>single</goal>
                    </goals>
                </execution>
            </executions>
        </plugin>
        
        ...
       
    </project>
    
    项目工程打包
    $ mvn clean && mvn package
    
    执行命令
    # 打印帮助信息
    $ java -jar blockchain-java-jar-with-dependencies.jar -h 
    
    # 添加区块
    $ java -jar blockchain-java-jar-with-dependencies.jar -add "Send 1.5 BTC to wangwei"
    $ java -jar blockchain-java-jar-with-dependencies.jar -add "Send 2.5 BTC to wangwei"
    $ java -jar blockchain-java-jar-with-dependencies.jar -add "Send 3.5 BTC to wangwei"
    
    # 打印区块链
    $ java -jar blockchain-java-jar-with-dependencies.jar -print
    

    总结

    本篇我们实现了区块链的存储功能,接下来我们将实现地址、交易、钱包这一些列的功能。

    资料

    image

    相关文章

      网友评论

      • 寄意兰舟_62db:请问Bucket 的特性指的什么
        wangwei_hz:@寄意兰舟_62db 意思四 BoltDB 支持 桶 的概念,可以对对数进行分组存储

      本文标题:基于Java语言构建区块链(三)—— 持久化 & 命令行

      本文链接:https://www.haomeiwen.com/subject/hwimxftx.html