標籤:java 分布式 程式設計語言 程式員
引言
上一篇 文章我們實現了區塊鏈的工作量證明機制(Pow),儘可能地實現了挖礦。但是距離真正的區塊鏈應用還有很多重要的特性沒有實現。今天我們來實現區塊鏈資料的儲存機制,將每次產生的區塊鏈資料儲存下來。有一點需要注意,區塊鏈本質上是一款分布式的資料庫,我們這裡不實現"分布式",只聚焦於資料存放區部分。
給大家推薦一個java內部學習群:725633148,進群找管理免費領取學習資料和視頻。沒有錯就是免費領取!大佬小白都歡迎,大家一起學習共同進步!
資料庫選擇
到目前為止,我們的實現機制中還沒有區Block Storage這一環節,導致我們的區塊每次產生之後都儲存在了記憶體中。這樣不便於我們重新使用區塊鏈,每次都要從頭開始產生區塊,也不能夠跟他人共用我們的區塊鏈,因此,我們需要將其儲存在磁碟上。
我們該選擇哪一款資料庫呢?事實上,在《比特幣白皮書》中並沒有明確指定使用哪一種的資料庫,因此這個由開發人員自己決定。中本聰 開發的 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/r... ,這裡不多做介紹。
資料結構
在我們開始實現資料持久化之前,我們先要確定我們該如何去儲存我們的資料。為此,我們先來看看比特幣是怎麼做的。
簡單來講,比特幣使用了兩個"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;
/**
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_BUCKETPREFIX = "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 {
@Getterprivate 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();
}
到此,儲存部分的功能就實現完畢,我們還缺少一個功能:
檢索區塊鏈
現在,我們所有的區塊都儲存到了資料庫,因此,我們能夠重新開啟已有的區塊鏈並且向其添加新的區塊。但這也導致我們再也無法列印出區塊鏈中所有區塊的資訊,因為,我們沒有將區Block Storage在數組當中。讓我們來修複這個瑕疵!
我們在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
給大家推薦一個java內部學習群:725633148,進群找管理免費領取學習資料和視頻。沒有錯就是免費領取!大佬小白都歡迎,大家一起學習共同進步!
總結
本篇我們實現了區塊鏈的儲存功能,接下來我們將實現地址、交易、錢包這一些列的功能。
基於Java語言構建區塊鏈(三)—— 持久化 & 命令列