文章的主要思想和内容均来自: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 。
当然也可以选择 LevelDB,非常不错的 LevelDB 介绍文章:https://mp.weixin.qq.com/s/rN6HX2VzsRi3_EKXYKuJAA
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
这个桶中,存储的键值对:
由于我们还没有实现交易相关的特性,因此,我们这里只使用 block
桶。另外,前面提到过的,这里我们不会实现各个区块数据各自存储在独立的文件上,而是统一存放在一个文件里面。因此,我们不要存储和文件编码相关的数据,这样一来,我们所用到的键值对就简化为:
(查看更加详细的解释 )
序列化 RocksDB 的 Key 与 Value 只能以 byte [] 的形式进行存储,这里我们需要用到序列化与反序列化库 Kryo ,代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 package one.wangwei.blockchain.util;import com.esotericsoftware.kryo.Kryo;import com.esotericsoftware.kryo.io.Input;import com.esotericsoftware.kryo.io.Output;public class SerializeUtils { public static Object deserialize (byte [] bytes) { Input input = new Input (bytes); Object obj = new Kryo ().readClassAndObject(input); input.close(); return obj; } 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 不支持,所以需要我们自己使用 Map 来做一个映射。
RocksDBUtils 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 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;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; 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); } } 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); } } 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); } } public String getLastBlockHash () { byte [] lastBlockHashBytes = blocksBucket.get(LAST_BLOCK_KEY); if (lastBlockHashBytes != null ) { return (String) SerializeUtils.deserialize(lastBlockHashBytes); } return "" ; } 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); } } 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
接口的代码逻辑,改为如下逻辑:
代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 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 值
1 2 3 4 5 6 7 8 9 public class Blockchain { @Getter private String lastBlockHash; private Blockchain (String lastBlockHash) { this .lastBlockHash = lastBlockHash; } }
每次挖矿完成后,我们也需要将最新的区块信息保存下来,并且更新最新区块链 Hash 值:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 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)); } public void addBlock (Block block) throws Exception { RocksDBUtils.getInstance().putLastBlockHash(block.getHash()); RocksDBUtils.getInstance().putBlock(block); this .lastBlockHash = block.getHash(); }
到此,存储部分的功能就实现完毕,我们还缺少一个功能:
检索区块链 现在,我们所有的区块都保存到了数据库,因此,我们能够重新打开已有的区块链并且向其添加新的区块。但这也导致我们再也无法打印出区块链中所有区块的信息,因为,我们没有将区块存储在数组当中。让我们来修复这个瑕疵!
我们在 Blockchain 中创建一个内部类 BlockchainIterator
,作为区块链的迭代器,通过区块之前的 hash 连接来依次迭代输出区块信息,代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 public class Blockchain { .... public class BlockchainIterator { private String currentBlockHash; public BlockchainIterator (String currentBlockHash) { this .currentBlockHash = currentBlockHash; } 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 ; } public Block next () throws Exception { Block currentBlock = RocksDBUtils.getInstance().getBlock(currentBlockHash); if (currentBlock != null ) { this .currentBlockHash = currentBlock.getPrevBlockHash(); return currentBlock; } return null ; } } .... }
测试 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 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 配置
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 <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 > <phase > package</phase > <goals > <goal > single</goal > </goals > </execution > </executions > </plugin > ... </project >
添加 shell 脚本 blockchain.sh
1 2 3 4 5 6 7 8 9 10 11 # !/bin/bash set -e # Check if the jar has been built. if [ ! -e target/blockchain-java-jar-with-dependencies.jar ]; then echo "Compiling blockchain project to a JAR" mvn package -DskipTests fi java -jar target/blockchain-java-jar-with-dependencies.jar "$@"
执行命令 1 2 3 4 5 6 7 8 9 10 11 12 13 $ cd blockchain-java $ ./blockchain.sh -h $ ./blockchain.sh -add "Send 1.5 BTC to wangwei" $ ./blockchain.sh -add "Send 2.5 BTC to wangwei" $ ./blockchain.sh -add "Send 3.5 BTC to wangwei" $ ./blockchain.sh -print
总结 本篇我们实现了区块链的存储功能,接下来我们将实现地址、交易、钱包这一些列的功能。
资料