博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
基于Java语言构建区块链(三)—— 持久化 & 命令行
阅读量:6714 次
发布时间:2019-06-25

本文共 11340 字,大约阅读时间需要 37 分钟。

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

引言

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

数据库选择

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

我们该选择哪一款数据库呢?事实上,在《》中并没有明确指定使用哪一种的数据库,因此这个由开发人员自己决定。 开发的 中使用的是。原文 中使用的是 ,对Go语言支持比较好。

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

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

数据结构

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

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

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

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

详见:

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

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[]的形式进行存储,这里我们需要用到序列化与反序列化库 ,代码如下:

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 接口的代码逻辑,改为如下逻辑:

代码如下:

/**  * 

创建区块链

* * @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值:

/** * 

添加区块

* * @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));}/** *

添加区块

* * @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 = trueBlock{hash='0000024b2c23c4fb06c2e2c1349275d415efe17a51db24cd4883da0067300ddf', prevBlockHash='00000b14fefb51ba2a7428549d469bcf3efae338315e7289d3e6dc4caf589d79', data='Send 2.5 more BTC to wangwei', timeStamp=1519724872, nonce=896348}, validate = trueBlock{hash='00000b14fefb51ba2a7428549d469bcf3efae338315e7289d3e6dc4caf589d79', prevBlockHash='0000099ced1b02f40c750c5468bb8c4fd800ec9f46fea5d8b033e5d054f0f703', data='Send 1.0 BTC to wangwei', timeStamp=1519724869, nonce=673955}, validate = trueBlock{hash='0000099ced1b02f40c750c5468bb8c4fd800ec9f46fea5d8b033e5d054f0f703', prevBlockHash='', data='Genesis Block', timeStamp=1519724866, nonce=840247}, validate = true复制代码

命令行界面

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

配置

添加pom.xml配置

...
commons-cli
commons-cli
1.4
...
org.apache.maven.plugins
maven-assembly-plugin
3.1.0
true
lib/
one.wangwei.blockchain.cli.Main
jar-with-dependencies
make-assembly
package
single
...
复制代码
项目工程打包
$ 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复制代码

总结

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

资料

  • 源代码:https://github.com/wangweiX/blockchain-java/tree/part3-persistence
  • https://jeiwan.cc/posts/building-blockchain-in-go-part-3/

转载地址:http://itelo.baihongyu.com/

你可能感兴趣的文章
虚拟化中虚拟机处理器核数与物理主机cpu的关系
查看>>
org.codehaus.jackson.map.JsonMappingException: No suitable constructor found for type
查看>>
MYSQL: mysqlbinlog读取二进制文件报错read_log_event()
查看>>
随机产生由特殊字符,大小写字母以及数字组成的字符串,且每种字符都至少出现一次...
查看>>
我的友情链接
查看>>
我的友情链接
查看>>
java21:捕鱼达人
查看>>
Zabbix 服务端搭建
查看>>
Java - 一个单例
查看>>
学习JAVA 持续更新
查看>>
Spring propertyConfigurer类
查看>>
Linux系统分析工具之uptime,top(一)
查看>>
EIGRP之DUAL(扩散更新算法)
查看>>
cacti自定义数据收集脚本,创建Data Templates和Graph Templates
查看>>
对你同样重要的非技术贴,一封有效的求职信的具体写法
查看>>
在路由器里插入和删除ACL
查看>>
我的友情链接
查看>>
OpenStack从入门到放弃
查看>>
戴尔和EMC已经成为正式的竞争对手
查看>>
6425C-Lab12 管理DC(1)
查看>>