文章的主要思想和内容均来自: https://jeiwan.cc/posts/building-blockchain-in-go-part-3/
上一篇 文章我们实现了区块链的工作量证明机制(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(桶)”来存储数据:
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的键值对
( 查看更加详细的解释 :_Data_Storage))
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
,主要的功能如下:
注意:BoltDB 支持 Bucket 的特性,而RocksDB 不支持,所以需要我们自己使用Map来做一个映射。
package one.wangwei.blockchain.store; import com.google.common.collect.Maps; import one.wangwei.blockchain.block.Block; import one.wangwei.blockchain.util.SerializeUtils; import org.rocksdb.RocksDB; import org.rocksdb.RocksDBException; import java.util.Map; /** * 存储工具类 * * @author wangwei * @date 2018/02/27 */ public class RocksDBUtils { /** * 区块链数据文件 */ private static final String DB_FILE = "blockchain.db"; /** * 区块桶前缀 */ private static final String BLOCKS_BUCKET_KEY = "blocks"; /** * 最新一个区块 */ private static final String LAST_BLOCK_KEY = "l"; private volatile static RocksDBUtils instance; public static RocksDBUtils getInstance() { if (instance == null) { synchronized (RocksDBUtils.class) { if (instance == null) { instance = new RocksDBUtils(); } } } return instance; } private RocksDB db; /** * block buckets */ private Map<String, byte[]> blocksBucket; private RocksDBUtils() { openDB(); initBlockBucket(); } /** * 打开数据库 */ private void openDB() { try { db = RocksDB.open(DB_FILE); } catch (RocksDBException e) { throw new RuntimeException("Fail to open db ! ", e); } } /** * 初始化 blocks 数据桶 */ private void initBlockBucket() { try { byte[] blockBucketKey = SerializeUtils.serialize(BLOCKS_BUCKET_KEY); byte[] blockBucketBytes = db.get(blockBucketKey); if (blockBucketBytes != null) { blocksBucket = (Map) SerializeUtils.deserialize(blockBucketBytes); } else { blocksBucket = Maps.newHashMap(); db.put(blockBucketKey, SerializeUtils.serialize(blocksBucket)); } } catch (RocksDBException e) { throw new RuntimeException("Fail to init block bucket ! ", e); } } /** * 保存最新一个区块的Hash值 * * @param tipBlockHash */ public void putLastBlockHash(String tipBlockHash) { try { blocksBucket.put(LAST_BLOCK_KEY, SerializeUtils.serialize(tipBlockHash)); db.put(SerializeUtils.serialize(BLOCKS_BUCKET_KEY), SerializeUtils.serialize(blocksBucket)); } catch (RocksDBException e) { throw new RuntimeException("Fail to put last block hash ! ", e); } } /** * 查询最新一个区块的Hash值 * * @return */ public String getLastBlockHash() { byte[] lastBlockHashBytes = blocksBucket.get(LAST_BLOCK_KEY); if (lastBlockHashBytes != null) { return (String) SerializeUtils.deserialize(lastBlockHashBytes); } return ""; } /** * 保存区块 * * @param block */ public void putBlock(Block block) { try { blocksBucket.put(block.getHash(), SerializeUtils.serialize(block)); db.put(SerializeUtils.serialize(BLOCKS_BUCKET_KEY), SerializeUtils.serialize(blocksBucket)); } catch (RocksDBException e) { throw new RuntimeException("Fail to put block ! ", e); } } /** * 查询区块 * * @param blockHash * @return */ public Block getBlock(String blockHash) { return (Block) SerializeUtils.deserialize(blocksBucket.get(blockHash)); } /** * 关闭数据库 */ public void closeDB() { try { db.close(); } catch (Exception e) { throw new RuntimeException("Fail to close db ! ", e); } } }
现在我们来优化 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
部分的内容,这里不做详细介绍,具体可以去查看文末的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
本篇我们实现了区块链的存储功能,接下来我们将实现地址、交易、钱包这一些列的功能。
https://press.one/file/v?s=62076a14b3d0693cf88d19db7e2d6a5a8e05b3de0c77deee242615c5480f8818104c24b65500a8cac096dee3b2e6862cec266916ee6545dd38f3650a459c7b221&h=7732ed4bedb1bb10a4d24df75b719ac2d56ae76a1b5a8f06ccc2ca6dc2d3ce30&a=23fe9bfd7ceef4b44c2ce44dcac8e4a49caf8026&f=P1&v=2