引言
上一篇文章我们实现了区块链的工作量证明机制 (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
接口的代码逻辑, 改为如下逻辑:
代码如下:
- /**
- * <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 部分的内容, 这里不做实现, 感兴趣的小伙伴, 可以去使用 commons-cli 进行实现;
总结
本篇我们实现了区块链的存储功能, 接下来我们将实现地址交易钱包这一些列的功能
资料
源代码:
https://github.com/wangweiX/blockchain-java/tree/part3-persistence
https://jeiwan.cc/posts/building-blockchain-in-go-part-3/
来源: https://juejin.im/post/5a95358a5188257a5911f116